content: /*
   * Copyright (C) 2019 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.view.textclassifier.intent;
  
  import android.annotation.Nullable;
  import android.app.PendingIntent;
  import android.app.RemoteAction;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.PackageManager;
  import android.content.pm.ResolveInfo;
  import android.graphics.drawable.Icon;
  import android.os.Bundle;
  import android.text.TextUtils;
  import android.view.textclassifier.ExtrasUtils;
  import android.view.textclassifier.Log;
  import android.view.textclassifier.TextClassification;
  import android.view.textclassifier.TextClassifier;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.Preconditions;
  
  /**
   * Helper class to store the information from which RemoteActions are built.
   *
   * @hide
   */
  @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
  public final class LabeledIntent {
      private static final String TAG = "LabeledIntent";
      public static final int DEFAULT_REQUEST_CODE = 0;
      private static final TitleChooser DEFAULT_TITLE_CHOOSER =
              (labeledIntent, resolveInfo) -> {
                  if (!TextUtils.isEmpty(labeledIntent.titleWithEntity)) {
                      return labeledIntent.titleWithEntity;
                  }
                  return labeledIntent.titleWithoutEntity;
              };
  
      @Nullable
      public final String titleWithoutEntity;
      @Nullable
      public final String titleWithEntity;
      public final String description;
      @Nullable
      public final String descriptionWithAppName;
      // Do not update this intent.
      public final Intent intent;
      public final int requestCode;
  
      /**
       * Initializes a LabeledIntent.
       *
       * <p>NOTE: {@code requestCode} is required to not be {@link #DEFAULT_REQUEST_CODE}
       * if distinguishing info (e.g. the classified text) is represented in intent extras only.
       * In such circumstances, the request code should represent the distinguishing info
       * (e.g. by generating a hashcode) so that the generated PendingIntent is (somewhat)
       * unique. To be correct, the PendingIntent should be definitely unique but we try a
       * best effort approach that avoids spamming the system with PendingIntents.
       */
      // TODO: Fix the issue mentioned above so the behaviour is correct.
      public LabeledIntent(
              @Nullable String titleWithoutEntity,
              @Nullable String titleWithEntity,
              String description,
              @Nullable String descriptionWithAppName,
              Intent intent,
              int requestCode) {
          if (TextUtils.isEmpty(titleWithEntity) && TextUtils.isEmpty(titleWithoutEntity)) {
              throw new IllegalArgumentException(
                      "titleWithEntity and titleWithoutEntity should not be both null");
          }
          this.titleWithoutEntity = titleWithoutEntity;
          this.titleWithEntity = titleWithEntity;
          this.description = Preconditions.checkNotNull(description);
          this.descriptionWithAppName = descriptionWithAppName;
          this.intent = Preconditions.checkNotNull(intent);
          this.requestCode = requestCode;
      }
  
      /**
       * Return the resolved result.
       *
       * @param context the context to resolve the result's intent and action
       * @param titleChooser for choosing an action title
       * @param textLanguagesBundle containing language detection information
       */
      @Nullable
      public Result resolve(
              Context context,
              @Nullable TitleChooser titleChooser,
              @Nullable Bundle textLanguagesBundle) {
          final PackageManager pm = context.getPackageManager();
          final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
  
          if (resolveInfo == null || resolveInfo.activityInfo == null) {
              Log.w(TAG, "resolveInfo or activityInfo is null");
              return null;
          }
          final String packageName = resolveInfo.activityInfo.packageName;
          final String className = resolveInfo.activityInfo.name;
          if (packageName == null || className == null) {
              Log.w(TAG, "packageName or className is null");
              return null;
          }
          Intent resolvedIntent = new Intent(intent);
          resolvedIntent.putExtra(
                  TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER,
                  getFromTextClassifierExtra(textLanguagesBundle));
          boolean shouldShowIcon = false;
          Icon icon = null;
          if (!"android".equals(packageName)) {
              // We only set the component name when the package name is not resolved to "android"
              // to workaround a bug that explicit intent with component name == ResolverActivity
              // can't be launched on keyguard.
              resolvedIntent.setComponent(new ComponentName(packageName, className));
              if (resolveInfo.activityInfo.getIconResource() != 0) {
                  icon = Icon.createWithResource(
                          packageName, resolveInfo.activityInfo.getIconResource());
                  shouldShowIcon = true;
              }
          }
          if (icon == null) {
              // RemoteAction requires that there be an icon.
              icon = Icon.createWithResource(
                      "android", com.android.internal.R.drawable.ic_more_items);
          }
          final PendingIntent pendingIntent =
                  TextClassification.createPendingIntent(context, resolvedIntent, requestCode);
          titleChooser = titleChooser == null ? DEFAULT_TITLE_CHOOSER : titleChooser;
          CharSequence title = titleChooser.chooseTitle(this, resolveInfo);
          if (TextUtils.isEmpty(title)) {
              Log.w(TAG, "Custom titleChooser return null, fallback to the default titleChooser");
              title = DEFAULT_TITLE_CHOOSER.chooseTitle(this, resolveInfo);
          }
          final RemoteAction action =
                  new RemoteAction(icon, title, resolveDescription(resolveInfo, pm), pendingIntent);
          action.setShouldShowIcon(shouldShowIcon);
          return new Result(resolvedIntent, action);
      }
  
      private String resolveDescription(ResolveInfo resolveInfo, PackageManager packageManager) {
          if (!TextUtils.isEmpty(descriptionWithAppName)) {
              // Example string format of descriptionWithAppName: "Use %1$s to open map".
              String applicationName = getApplicationName(resolveInfo, packageManager);
              if (!TextUtils.isEmpty(applicationName)) {
                  return String.format(descriptionWithAppName, applicationName);
              }
          }
          return description;
      }
  
      @Nullable
      private String getApplicationName(
              ResolveInfo resolveInfo, PackageManager packageManager) {
          if (resolveInfo.activityInfo == null) {
              return null;
          }
          if ("android".equals(resolveInfo.activityInfo.packageName)) {
              return null;
          }
          if (resolveInfo.activityInfo.applicationInfo == null) {
              return null;
          }
          return (String) packageManager.getApplicationLabel(
                  resolveInfo.activityInfo.applicationInfo);
      }
  
      private Bundle getFromTextClassifierExtra(@Nullable Bundle textLanguagesBundle) {
          if (textLanguagesBundle != null) {
              final Bundle bundle = new Bundle();
              ExtrasUtils.putTextLanguagesExtra(bundle, textLanguagesBundle);
              return bundle;
          } else {
              return Bundle.EMPTY;
          }
      }
  
      /**
       * Data class that holds the result.
       */
      public static final class Result {
          public final Intent resolvedIntent;
          public final RemoteAction remoteAction;
  
          public Result(Intent resolvedIntent, RemoteAction remoteAction) {
              this.resolvedIntent = Preconditions.checkNotNull(resolvedIntent);
              this.remoteAction = Preconditions.checkNotNull(remoteAction);
          }
      }
  
      /**
       * An object to choose a title from resolved info.  If {@code null} is returned,
       * {@link #titleWithEntity} will be used if it exists, {@link #titleWithoutEntity} otherwise.
       */
      public interface TitleChooser {
          /**
           * Picks a title from a {@link LabeledIntent} by looking into resolved info.
           * {@code resolveInfo} is guaranteed to have a non-null {@code activityInfo}.
           */
          @Nullable
          CharSequence chooseTitle(LabeledIntent labeledIntent, ResolveInfo resolveInfo);
      }
  }
  