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.RequiresPermission; 23 import android.annotation.SystemApi; 24 import android.annotation.TestApi; 25 import android.annotation.UserIdInt; 26 import android.graphics.Bitmap; 27 import android.os.Binder; 28 import android.os.Bundle; 29 import android.os.RemoteException; 30 import android.util.Log; 31 32 import com.android.internal.util.SyncResultReceiver; 33 34 import java.util.List; 35 import java.util.concurrent.Executor; 36 37 /** 38 * When provided with content from an app, can suggest selections and classifications of that 39 * content. 40 * 41 * <p>The content is mainly a snapshot of a running task, the selections will be text and image 42 * selections with that image content. These mSelections can then be classified to find actions and 43 * entities on those selections. 44 * 45 * <p>Only accessible to blessed components such as Overview. 46 * 47 * @hide 48 */ 49 @SystemApi 50 public final class ContentSuggestionsManager { 51 /** 52 * Key into the extras Bundle passed to {@link #provideContextImage(int, Bundle)}. 53 * This can be used to provide the bitmap to 54 * {@link android.service.contentsuggestions.ContentSuggestionsService}. 55 * The value must be a {@link android.graphics.Bitmap} with the 56 * config {@link android.graphics.Bitmap.Config.HARDWARE}. 57 * 58 * @hide 59 */ 60 public static final String EXTRA_BITMAP = "android.contentsuggestions.extra.BITMAP"; 61 62 private static final String TAG = ContentSuggestionsManager.class.getSimpleName(); 63 64 /** 65 * Timeout for calls to system_server. 66 */ 67 private static final int SYNC_CALLS_TIMEOUT_MS = 5000; 68 69 @Nullable 70 private final IContentSuggestionsManager mService; 71 72 @NonNull 73 private final int mUser; 74 75 /** @hide */ ContentSuggestionsManager( @serIdInt int userId, @Nullable IContentSuggestionsManager service)76 public ContentSuggestionsManager( 77 @UserIdInt int userId, @Nullable IContentSuggestionsManager service) { 78 mService = service; 79 mUser = userId; 80 } 81 82 /** 83 * Hints to the system that a new context image using the provided bitmap should be sent to 84 * the system content suggestions service. 85 * 86 * @param bitmap the new context image 87 * @param imageContextRequestExtras sent with request to provide implementation specific 88 * extra information. 89 */ provideContextImage( @onNull Bitmap bitmap, @NonNull Bundle imageContextRequestExtras)90 public void provideContextImage( 91 @NonNull Bitmap bitmap, @NonNull Bundle imageContextRequestExtras) { 92 if (mService == null) { 93 Log.e(TAG, "provideContextImage called, but no ContentSuggestionsManager configured"); 94 return; 95 } 96 97 try { 98 mService.provideContextBitmap(mUser, bitmap, imageContextRequestExtras); 99 } catch (RemoteException e) { 100 throw e.rethrowFromSystemServer(); 101 } 102 } 103 104 /** 105 * Hints to the system that a new context image for the provided task should be sent to the 106 * system content suggestions service. 107 * 108 * @param taskId of the task to snapshot. 109 * @param imageContextRequestExtras sent with request to provide implementation specific 110 * extra information. 111 */ provideContextImage( int taskId, @NonNull Bundle imageContextRequestExtras)112 public void provideContextImage( 113 int taskId, @NonNull Bundle imageContextRequestExtras) { 114 if (mService == null) { 115 Log.e(TAG, "provideContextImage called, but no ContentSuggestionsManager configured"); 116 return; 117 } 118 119 try { 120 mService.provideContextImage(mUser, taskId, imageContextRequestExtras); 121 } catch (RemoteException e) { 122 throw e.rethrowFromSystemServer(); 123 } 124 } 125 126 /** 127 * Suggest content selections, based on the provided task id and optional 128 * location on screen provided in the request. Called after provideContextImage(). 129 * The result can be passed to 130 * {@link #classifyContentSelections(ClassificationsRequest, Executor, ClassificationsCallback)} 131 * to classify actions and entities on these selections. 132 * 133 * @param request containing the task and point location. 134 * @param callbackExecutor to execute the provided callback on. 135 * @param callback to receive the selections. 136 */ suggestContentSelections( @onNull SelectionsRequest request, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull SelectionsCallback callback)137 public void suggestContentSelections( 138 @NonNull SelectionsRequest request, 139 @NonNull @CallbackExecutor Executor callbackExecutor, 140 @NonNull SelectionsCallback callback) { 141 if (mService == null) { 142 Log.e(TAG, 143 "suggestContentSelections called, but no ContentSuggestionsManager configured"); 144 return; 145 } 146 147 try { 148 mService.suggestContentSelections( 149 mUser, request, new SelectionsCallbackWrapper(callback, callbackExecutor)); 150 } catch (RemoteException e) { 151 throw e.rethrowFromSystemServer(); 152 } 153 } 154 155 /** 156 * Classify actions and entities in content selections, as returned from 157 * suggestContentSelections. Note these selections may be modified by the 158 * caller before being passed here. 159 * 160 * @param request containing the selections to classify. 161 * @param callbackExecutor to execute the provided callback on. 162 * @param callback to receive the classifications. 163 */ classifyContentSelections( @onNull ClassificationsRequest request, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull ClassificationsCallback callback)164 public void classifyContentSelections( 165 @NonNull ClassificationsRequest request, 166 @NonNull @CallbackExecutor Executor callbackExecutor, 167 @NonNull ClassificationsCallback callback) { 168 if (mService == null) { 169 Log.e(TAG, "classifyContentSelections called, " 170 + "but no ContentSuggestionsManager configured"); 171 return; 172 } 173 174 try { 175 mService.classifyContentSelections( 176 mUser, request, new ClassificationsCallbackWrapper(callback, callbackExecutor)); 177 } catch (RemoteException e) { 178 throw e.rethrowFromSystemServer(); 179 } 180 } 181 182 /** 183 * Report telemetry for interaction with suggestions / classifications. 184 * 185 * @param requestId the id for the associated interaction 186 * @param interaction to report back to the system content suggestions service. 187 */ notifyInteraction( @onNull String requestId, @NonNull Bundle interaction)188 public void notifyInteraction( 189 @NonNull String requestId, @NonNull Bundle interaction) { 190 if (mService == null) { 191 Log.e(TAG, "notifyInteraction called, but no ContentSuggestionsManager configured"); 192 return; 193 } 194 195 try { 196 mService.notifyInteraction(mUser, requestId, interaction); 197 } catch (RemoteException e) { 198 throw e.rethrowFromSystemServer(); 199 } 200 } 201 202 /** 203 * Indicates that Content Suggestions is available and enabled for the provided user. That is, 204 * has an implementation and not disabled through device management. 205 * 206 * @return {@code true} if Content Suggestions is enabled and available for the provided user. 207 */ isEnabled()208 public boolean isEnabled() { 209 if (mService == null) { 210 return false; 211 } 212 213 SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); 214 try { 215 mService.isEnabled(mUser, receiver); 216 return receiver.getIntResult() != 0; 217 } catch (RemoteException e) { 218 throw e.rethrowFromSystemServer(); 219 } catch (SyncResultReceiver.TimeoutException e) { 220 throw new RuntimeException("Fail to get the enable status."); 221 } 222 } 223 224 /** 225 * Resets the temporary service implementation to the default component. 226 * 227 * @hide 228 */ 229 @TestApi 230 @RequiresPermission(android.Manifest.permission.MANAGE_CONTENT_SUGGESTIONS) resetTemporaryService(@serIdInt int userId)231 public void resetTemporaryService(@UserIdInt int userId) { 232 if (mService == null) { 233 Log.e(TAG, "resetTemporaryService called, but no ContentSuggestionsManager " 234 + "configured"); 235 return; 236 } 237 try { 238 mService.resetTemporaryService(userId); 239 } catch (RemoteException e) { 240 throw e.rethrowFromSystemServer(); 241 } 242 } 243 244 /** 245 * Temporarily sets the service implementation. 246 * 247 * @param userId user Id to set the temporary service on. 248 * @param serviceName name of the new component 249 * @param duration how long the change will be valid (the service will be automatically reset 250 * to the default component after this timeout expires). 251 * 252 * @hide 253 */ 254 @TestApi 255 @RequiresPermission(android.Manifest.permission.MANAGE_CONTENT_SUGGESTIONS) setTemporaryService( @serIdInt int userId, @NonNull String serviceName, int duration)256 public void setTemporaryService( 257 @UserIdInt int userId, @NonNull String serviceName, int duration) { 258 if (mService == null) { 259 Log.e(TAG, "setTemporaryService called, but no ContentSuggestionsManager " 260 + "configured"); 261 return; 262 } 263 try { 264 mService.setTemporaryService(userId, serviceName, duration); 265 } catch (RemoteException e) { 266 throw e.rethrowFromSystemServer(); 267 } 268 } 269 270 /** 271 * Sets whether the default service should be used. 272 * 273 * @hide 274 */ 275 @TestApi 276 @RequiresPermission(android.Manifest.permission.MANAGE_CONTENT_SUGGESTIONS) setDefaultServiceEnabled(@serIdInt int userId, boolean enabled)277 public void setDefaultServiceEnabled(@UserIdInt int userId, boolean enabled) { 278 if (mService == null) { 279 Log.e(TAG, "setDefaultServiceEnabled called, but no ContentSuggestionsManager " 280 + "configured"); 281 return; 282 } 283 try { 284 mService.setDefaultServiceEnabled(userId, enabled); 285 } catch (RemoteException e) { 286 throw e.rethrowFromSystemServer(); 287 } 288 } 289 290 /** 291 * Callback to receive content selections from 292 * {@link #suggestContentSelections(SelectionsRequest, Executor, SelectionsCallback)}. 293 */ 294 public interface SelectionsCallback { 295 /** 296 * Async callback called when the content suggestions service has selections available. 297 * These can be modified and sent back to the manager for classification. The contents of 298 * the selection is implementation dependent. 299 * 300 * @param statusCode as defined by the implementation of content suggestions service. 301 * @param selections not {@code null}, but can be size {@code 0}. 302 */ onContentSelectionsAvailable( int statusCode, @NonNull List<ContentSelection> selections)303 void onContentSelectionsAvailable( 304 int statusCode, @NonNull List<ContentSelection> selections); 305 } 306 307 /** 308 * Callback to receive classifications from 309 * {@link #classifyContentSelections(ClassificationsRequest, Executor, ClassificationsCallback)} 310 */ 311 public interface ClassificationsCallback { 312 /** 313 * Async callback called when the content suggestions service has classified selections. The 314 * contents of the classification is implementation dependent. 315 * 316 * @param statusCode as defined by the implementation of content suggestions service. 317 * @param classifications not {@code null}, but can be size {@code 0}. 318 */ onContentClassificationsAvailable(int statusCode, @NonNull List<ContentClassification> classifications)319 void onContentClassificationsAvailable(int statusCode, 320 @NonNull List<ContentClassification> classifications); 321 } 322 323 private static class SelectionsCallbackWrapper extends ISelectionsCallback.Stub { 324 private final SelectionsCallback mCallback; 325 private final Executor mExecutor; 326 SelectionsCallbackWrapper( @onNull SelectionsCallback callback, @NonNull Executor executor)327 SelectionsCallbackWrapper( 328 @NonNull SelectionsCallback callback, @NonNull Executor executor) { 329 mCallback = callback; 330 mExecutor = executor; 331 } 332 333 @Override onContentSelectionsAvailable( int statusCode, List<ContentSelection> selections)334 public void onContentSelectionsAvailable( 335 int statusCode, List<ContentSelection> selections) { 336 final long identity = Binder.clearCallingIdentity(); 337 try { 338 mExecutor.execute(() -> 339 mCallback.onContentSelectionsAvailable(statusCode, selections)); 340 } finally { 341 Binder.restoreCallingIdentity(identity); 342 } 343 } 344 } 345 346 private static final class ClassificationsCallbackWrapper extends 347 IClassificationsCallback.Stub { 348 private final ClassificationsCallback mCallback; 349 private final Executor mExecutor; 350 ClassificationsCallbackWrapper(@onNull ClassificationsCallback callback, @NonNull Executor executor)351 ClassificationsCallbackWrapper(@NonNull ClassificationsCallback callback, 352 @NonNull Executor executor) { 353 mCallback = callback; 354 mExecutor = executor; 355 } 356 357 @Override onContentClassificationsAvailable( int statusCode, List<ContentClassification> classifications)358 public void onContentClassificationsAvailable( 359 int statusCode, List<ContentClassification> classifications) { 360 final long identity = Binder.clearCallingIdentity(); 361 try { 362 mExecutor.execute(() -> 363 mCallback.onContentClassificationsAvailable(statusCode, classifications)); 364 } finally { 365 Binder.restoreCallingIdentity(identity); 366 } 367 } 368 } 369 } 370