1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app.contentsuggestions;
18 
19 import android.annotation.CallbackExecutor;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.SystemApi;
23 import android.annotation.UserIdInt;
24 import android.os.Binder;
25 import android.os.Bundle;
26 import android.os.RemoteException;
27 import android.util.Log;
28 
29 import com.android.internal.util.SyncResultReceiver;
30 
31 import java.util.List;
32 import java.util.concurrent.Executor;
33 
34 /**
35  * When provided with content from an app, can suggest selections and classifications of that
36  * content.
37  *
38  * <p>The content is mainly a snapshot of a running task, the selections will be text and image
39  * selections with that image content. These mSelections can then be classified to find actions and
40  * entities on those selections.
41  *
42  * <p>Only accessible to blessed components such as Overview.
43  *
44  * @hide
45  */
46 @SystemApi
47 public final class ContentSuggestionsManager {
48     /**
49      * Key into the extras Bundle passed to {@link #provideContextImage(int, Bundle)}.
50      * This can be used to provide the bitmap to
51      * {@link android.service.contentsuggestions.ContentSuggestionsService}.
52      * The value must be a {@link android.graphics.Bitmap} with the
53      * config {@link android.graphics.Bitmap.Config.HARDWARE}.
54      *
55      * @hide
56      */
57     public static final String EXTRA_BITMAP = "android.contentsuggestions.extra.BITMAP";
58 
59     private static final String TAG = ContentSuggestionsManager.class.getSimpleName();
60 
61     /**
62      * Timeout for calls to system_server.
63      */
64     private static final int SYNC_CALLS_TIMEOUT_MS = 5000;
65 
66     @Nullable
67     private final IContentSuggestionsManager mService;
68 
69     @NonNull
70     private final int mUser;
71 
72     /** @hide */
ContentSuggestionsManager( @serIdInt int userId, @Nullable IContentSuggestionsManager service)73     public ContentSuggestionsManager(
74             @UserIdInt int userId, @Nullable IContentSuggestionsManager service) {
75         mService = service;
76         mUser = userId;
77     }
78 
79     /**
80      * Hints to the system that a new context image for the provided task should be sent to the
81      * system content suggestions service.
82      *
83      * @param taskId of the task to snapshot.
84      * @param imageContextRequestExtras sent with request to provide implementation specific
85      *                                  extra information.
86      */
provideContextImage( int taskId, @NonNull Bundle imageContextRequestExtras)87     public void provideContextImage(
88             int taskId, @NonNull Bundle imageContextRequestExtras) {
89         if (mService == null) {
90             Log.e(TAG, "provideContextImage called, but no ContentSuggestionsManager configured");
91             return;
92         }
93 
94         try {
95             mService.provideContextImage(mUser, taskId, imageContextRequestExtras);
96         } catch (RemoteException e) {
97             e.rethrowFromSystemServer();
98         }
99     }
100 
101     /**
102      * Suggest content selections, based on the provided task id and optional
103      * location on screen provided in the request. Called after provideContextImage().
104      * The result can be passed to
105      * {@link #classifyContentSelections(ClassificationsRequest, Executor, ClassificationsCallback)}
106      *  to classify actions and entities on these selections.
107      *
108      * @param request containing the task and point location.
109      * @param callbackExecutor to execute the provided callback on.
110      * @param callback to receive the selections.
111      */
suggestContentSelections( @onNull SelectionsRequest request, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull SelectionsCallback callback)112     public void suggestContentSelections(
113             @NonNull SelectionsRequest request,
114             @NonNull @CallbackExecutor Executor callbackExecutor,
115             @NonNull SelectionsCallback callback) {
116         if (mService == null) {
117             Log.e(TAG,
118                     "suggestContentSelections called, but no ContentSuggestionsManager configured");
119             return;
120         }
121 
122         try {
123             mService.suggestContentSelections(
124                     mUser, request, new SelectionsCallbackWrapper(callback, callbackExecutor));
125         } catch (RemoteException e) {
126             e.rethrowFromSystemServer();
127         }
128     }
129 
130     /**
131      * Classify actions and entities in content selections, as returned from
132      * suggestContentSelections. Note these selections may be modified by the
133      * caller before being passed here.
134      *
135      * @param request containing the selections to classify.
136      * @param callbackExecutor to execute the provided callback on.
137      * @param callback to receive the classifications.
138      */
classifyContentSelections( @onNull ClassificationsRequest request, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull ClassificationsCallback callback)139     public void classifyContentSelections(
140             @NonNull ClassificationsRequest request,
141             @NonNull @CallbackExecutor Executor callbackExecutor,
142             @NonNull ClassificationsCallback callback) {
143         if (mService == null) {
144             Log.e(TAG, "classifyContentSelections called, "
145                     + "but no ContentSuggestionsManager configured");
146             return;
147         }
148 
149         try {
150             mService.classifyContentSelections(
151                     mUser, request, new ClassificationsCallbackWrapper(callback, callbackExecutor));
152         } catch (RemoteException e) {
153             e.rethrowFromSystemServer();
154         }
155     }
156 
157     /**
158      * Report telemetry for interaction with suggestions / classifications.
159      *
160      * @param requestId the id for the associated interaction
161      * @param interaction to report back to the system content suggestions service.
162      */
notifyInteraction( @onNull String requestId, @NonNull Bundle interaction)163     public void notifyInteraction(
164             @NonNull String requestId, @NonNull Bundle interaction) {
165         if (mService == null) {
166             Log.e(TAG, "notifyInteraction called, but no ContentSuggestionsManager configured");
167             return;
168         }
169 
170         try {
171             mService.notifyInteraction(mUser, requestId, interaction);
172         } catch (RemoteException e) {
173             e.rethrowFromSystemServer();
174         }
175     }
176 
177     /**
178      * Indicates that Content Suggestions is available and enabled for the provided user. That is,
179      * has an implementation and not disabled through device management.
180      *
181      * @return {@code true} if Content Suggestions is enabled and available for the provided user.
182      */
isEnabled()183     public boolean isEnabled() {
184         if (mService == null) {
185             return false;
186         }
187 
188         SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
189         try {
190             mService.isEnabled(mUser, receiver);
191             return receiver.getIntResult() != 0;
192         } catch (RemoteException e) {
193             e.rethrowFromSystemServer();
194         }
195         return false;
196     }
197 
198     /**
199      * Callback to receive content selections from
200      *  {@link #suggestContentSelections(SelectionsRequest, Executor, SelectionsCallback)}.
201      */
202     public interface SelectionsCallback {
203         /**
204          * Async callback called when the content suggestions service has selections available.
205          * These can be modified and sent back to the manager for classification. The contents of
206          * the selection is implementation dependent.
207          *
208          * @param statusCode as defined by the implementation of content suggestions service.
209          * @param selections not {@code null}, but can be size {@code 0}.
210          */
onContentSelectionsAvailable( int statusCode, @NonNull List<ContentSelection> selections)211         void onContentSelectionsAvailable(
212                 int statusCode, @NonNull List<ContentSelection> selections);
213     }
214 
215     /**
216      * Callback to receive classifications from
217      * {@link #classifyContentSelections(ClassificationsRequest, Executor, ClassificationsCallback)}
218      */
219     public interface ClassificationsCallback {
220         /**
221          * Async callback called when the content suggestions service has classified selections. The
222          * contents of the classification is implementation dependent.
223          *
224          * @param statusCode as defined by the implementation of content suggestions service.
225          * @param classifications not {@code null}, but can be size {@code 0}.
226          */
onContentClassificationsAvailable(int statusCode, @NonNull List<ContentClassification> classifications)227         void onContentClassificationsAvailable(int statusCode,
228                 @NonNull List<ContentClassification> classifications);
229     }
230 
231     private static class SelectionsCallbackWrapper extends ISelectionsCallback.Stub {
232         private final SelectionsCallback mCallback;
233         private final Executor mExecutor;
234 
SelectionsCallbackWrapper( @onNull SelectionsCallback callback, @NonNull Executor executor)235         SelectionsCallbackWrapper(
236                 @NonNull SelectionsCallback callback, @NonNull Executor executor) {
237             mCallback = callback;
238             mExecutor = executor;
239         }
240 
241         @Override
onContentSelectionsAvailable( int statusCode, List<ContentSelection> selections)242         public void onContentSelectionsAvailable(
243                 int statusCode, List<ContentSelection> selections) {
244             final long identity = Binder.clearCallingIdentity();
245             try {
246                 mExecutor.execute(() ->
247                         mCallback.onContentSelectionsAvailable(statusCode, selections));
248             } finally {
249                 Binder.restoreCallingIdentity(identity);
250             }
251         }
252     }
253 
254     private static final class ClassificationsCallbackWrapper extends
255             IClassificationsCallback.Stub {
256         private final ClassificationsCallback mCallback;
257         private final Executor mExecutor;
258 
ClassificationsCallbackWrapper(@onNull ClassificationsCallback callback, @NonNull Executor executor)259         ClassificationsCallbackWrapper(@NonNull ClassificationsCallback callback,
260                 @NonNull Executor executor) {
261             mCallback = callback;
262             mExecutor = executor;
263         }
264 
265         @Override
onContentClassificationsAvailable( int statusCode, List<ContentClassification> classifications)266         public void onContentClassificationsAvailable(
267                 int statusCode, List<ContentClassification> classifications) {
268             final long identity = Binder.clearCallingIdentity();
269             try {
270                 mExecutor.execute(() ->
271                         mCallback.onContentClassificationsAvailable(statusCode, classifications));
272             } finally {
273                 Binder.restoreCallingIdentity(identity);
274             }
275         }
276     }
277 }
278