content: /*
   * Copyright (C) 2018 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.app.contentsuggestions;
  
  import android.annotation.CallbackExecutor;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.SystemApi;
  import android.annotation.UserIdInt;
  import android.os.Binder;
  import android.os.Bundle;
  import android.os.RemoteException;
  import android.util.Log;
  
  import com.android.internal.util.SyncResultReceiver;
  
  import java.util.List;
  import java.util.concurrent.Executor;
  
  /**
   * When provided with content from an app, can suggest selections and classifications of that
   * content.
   *
   * <p>The content is mainly a snapshot of a running task, the selections will be text and image
   * selections with that image content. These mSelections can then be classified to find actions and
   * entities on those selections.
   *
   * <p>Only accessible to blessed components such as Overview.
   *
   * @hide
   */
  @SystemApi
  public final class ContentSuggestionsManager {
      /**
       * Key into the extras Bundle passed to {@link #provideContextImage(int, Bundle)}.
       * This can be used to provide the bitmap to
       * {@link android.service.contentsuggestions.ContentSuggestionsService}.
       * The value must be a {@link android.graphics.Bitmap} with the
       * config {@link android.graphics.Bitmap.Config.HARDWARE}.
       *
       * @hide
       */
      public static final String EXTRA_BITMAP = "android.contentsuggestions.extra.BITMAP";
  
      private static final String TAG = ContentSuggestionsManager.class.getSimpleName();
  
      /**
       * Timeout for calls to system_server.
       */
      private static final int SYNC_CALLS_TIMEOUT_MS = 5000;
  
      @Nullable
      private final IContentSuggestionsManager mService;
  
      @NonNull
      private final int mUser;
  
      /** @hide */
      public ContentSuggestionsManager(
              @UserIdInt int userId, @Nullable IContentSuggestionsManager service) {
          mService = service;
          mUser = userId;
      }
  
      /**
       * Hints to the system that a new context image for the provided task should be sent to the
       * system content suggestions service.
       *
       * @param taskId of the task to snapshot.
       * @param imageContextRequestExtras sent with request to provide implementation specific
       *                                  extra information.
       */
      public void provideContextImage(
              int taskId, @NonNull Bundle imageContextRequestExtras) {
          if (mService == null) {
              Log.e(TAG, "provideContextImage called, but no ContentSuggestionsManager configured");
              return;
          }
  
          try {
              mService.provideContextImage(mUser, taskId, imageContextRequestExtras);
          } catch (RemoteException e) {
              e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Suggest content selections, based on the provided task id and optional
       * location on screen provided in the request. Called after provideContextImage().
       * The result can be passed to
       * {@link #classifyContentSelections(ClassificationsRequest, Executor, ClassificationsCallback)}
       *  to classify actions and entities on these selections.
       *
       * @param request containing the task and point location.
       * @param callbackExecutor to execute the provided callback on.
       * @param callback to receive the selections.
       */
      public void suggestContentSelections(
              @NonNull SelectionsRequest request,
              @NonNull @CallbackExecutor Executor callbackExecutor,
              @NonNull SelectionsCallback callback) {
          if (mService == null) {
              Log.e(TAG,
                      "suggestContentSelections called, but no ContentSuggestionsManager configured");
              return;
          }
  
          try {
              mService.suggestContentSelections(
                      mUser, request, new SelectionsCallbackWrapper(callback, callbackExecutor));
          } catch (RemoteException e) {
              e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Classify actions and entities in content selections, as returned from
       * suggestContentSelections. Note these selections may be modified by the
       * caller before being passed here.
       *
       * @param request containing the selections to classify.
       * @param callbackExecutor to execute the provided callback on.
       * @param callback to receive the classifications.
       */
      public void classifyContentSelections(
              @NonNull ClassificationsRequest request,
              @NonNull @CallbackExecutor Executor callbackExecutor,
              @NonNull ClassificationsCallback callback) {
          if (mService == null) {
              Log.e(TAG, "classifyContentSelections called, "
                      + "but no ContentSuggestionsManager configured");
              return;
          }
  
          try {
              mService.classifyContentSelections(
                      mUser, request, new ClassificationsCallbackWrapper(callback, callbackExecutor));
          } catch (RemoteException e) {
              e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Report telemetry for interaction with suggestions / classifications.
       *
       * @param requestId the id for the associated interaction
       * @param interaction to report back to the system content suggestions service.
       */
      public void notifyInteraction(
              @NonNull String requestId, @NonNull Bundle interaction) {
          if (mService == null) {
              Log.e(TAG, "notifyInteraction called, but no ContentSuggestionsManager configured");
              return;
          }
  
          try {
              mService.notifyInteraction(mUser, requestId, interaction);
          } catch (RemoteException e) {
              e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Indicates that Content Suggestions is available and enabled for the provided user. That is,
       * has an implementation and not disabled through device management.
       *
       * @return {@code true} if Content Suggestions is enabled and available for the provided user.
       */
      public boolean isEnabled() {
          if (mService == null) {
              return false;
          }
  
          SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
          try {
              mService.isEnabled(mUser, receiver);
              return receiver.getIntResult() != 0;
          } catch (RemoteException e) {
              e.rethrowFromSystemServer();
          }
          return false;
      }
  
      /**
       * Callback to receive content selections from
       *  {@link #suggestContentSelections(SelectionsRequest, Executor, SelectionsCallback)}.
       */
      public interface SelectionsCallback {
          /**
           * Async callback called when the content suggestions service has selections available.
           * These can be modified and sent back to the manager for classification. The contents of
           * the selection is implementation dependent.
           *
           * @param statusCode as defined by the implementation of content suggestions service.
           * @param selections not {@code null}, but can be size {@code 0}.
           */
          void onContentSelectionsAvailable(
                  int statusCode, @NonNull List<ContentSelection> selections);
      }
  
      /**
       * Callback to receive classifications from
       * {@link #classifyContentSelections(ClassificationsRequest, Executor, ClassificationsCallback)}
       */
      public interface ClassificationsCallback {
          /**
           * Async callback called when the content suggestions service has classified selections. The
           * contents of the classification is implementation dependent.
           *
           * @param statusCode as defined by the implementation of content suggestions service.
           * @param classifications not {@code null}, but can be size {@code 0}.
           */
          void onContentClassificationsAvailable(int statusCode,
                  @NonNull List<ContentClassification> classifications);
      }
  
      private static class SelectionsCallbackWrapper extends ISelectionsCallback.Stub {
          private final SelectionsCallback mCallback;
          private final Executor mExecutor;
  
          SelectionsCallbackWrapper(
                  @NonNull SelectionsCallback callback, @NonNull Executor executor) {
              mCallback = callback;
              mExecutor = executor;
          }
  
          @Override
          public void onContentSelectionsAvailable(
                  int statusCode, List<ContentSelection> selections) {
              final long identity = Binder.clearCallingIdentity();
              try {
                  mExecutor.execute(() ->
                          mCallback.onContentSelectionsAvailable(statusCode, selections));
              } finally {
                  Binder.restoreCallingIdentity(identity);
              }
          }
      }
  
      private static final class ClassificationsCallbackWrapper extends
              IClassificationsCallback.Stub {
          private final ClassificationsCallback mCallback;
          private final Executor mExecutor;
  
          ClassificationsCallbackWrapper(@NonNull ClassificationsCallback callback,
                  @NonNull Executor executor) {
              mCallback = callback;
              mExecutor = executor;
          }
  
          @Override
          public void onContentClassificationsAvailable(
                  int statusCode, List<ContentClassification> classifications) {
              final long identity = Binder.clearCallingIdentity();
              try {
                  mExecutor.execute(() ->
                          mCallback.onContentClassificationsAvailable(statusCode, classifications));
              } finally {
                  Binder.restoreCallingIdentity(identity);
              }
          }
      }
  }
  