content: /*
   * Copyright (C) 2016 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.content.pm;
  
  import android.annotation.IntDef;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.SystemApi;
  import android.annotation.TestApi;
  import android.annotation.UnsupportedAppUsage;
  import android.annotation.UserIdInt;
  import android.app.Notification;
  import android.app.Person;
  import android.app.TaskStackBuilder;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.LocusId;
  import android.content.pm.LauncherApps.ShortcutQuery;
  import android.content.res.Resources;
  import android.content.res.Resources.NotFoundException;
  import android.graphics.Bitmap;
  import android.graphics.drawable.Icon;
  import android.os.Build;
  import android.os.Bundle;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.os.PersistableBundle;
  import android.os.UserHandle;
  import android.text.TextUtils;
  import android.util.ArraySet;
  import android.util.Log;
  import android.view.contentcapture.ContentCaptureContext;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.Preconditions;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.List;
  import java.util.Set;
  
  /**
   * Represents a shortcut that can be published via {@link ShortcutManager}.
   *
   * @see ShortcutManager
   */
  public final class ShortcutInfo implements Parcelable {
      static final String TAG = "Shortcut";
  
      private static final String RES_TYPE_STRING = "string";
  
      private static final String ANDROID_PACKAGE_NAME = "android";
  
      private static final int IMPLICIT_RANK_MASK = 0x7fffffff;
  
      private static final int RANK_CHANGED_BIT = ~IMPLICIT_RANK_MASK;
  
      /** @hide */
      public static final int RANK_NOT_SET = Integer.MAX_VALUE;
  
      /** @hide */
      public static final int FLAG_DYNAMIC = 1 << 0;
  
      /** @hide */
      public static final int FLAG_PINNED = 1 << 1;
  
      /** @hide */
      public static final int FLAG_HAS_ICON_RES = 1 << 2;
  
      /** @hide */
      public static final int FLAG_HAS_ICON_FILE = 1 << 3;
  
      /** @hide */
      public static final int FLAG_KEY_FIELDS_ONLY = 1 << 4;
  
      /** @hide */
      public static final int FLAG_MANIFEST = 1 << 5;
  
      /** @hide */
      public static final int FLAG_DISABLED = 1 << 6;
  
      /** @hide */
      public static final int FLAG_STRINGS_RESOLVED = 1 << 7;
  
      /** @hide */
      public static final int FLAG_IMMUTABLE = 1 << 8;
  
      /** @hide */
      public static final int FLAG_ADAPTIVE_BITMAP = 1 << 9;
  
      /** @hide */
      public static final int FLAG_RETURNED_BY_SERVICE = 1 << 10;
  
      /** @hide When this is set, the bitmap icon is waiting to be saved. */
      public static final int FLAG_ICON_FILE_PENDING_SAVE = 1 << 11;
  
      /**
       * "Shadow" shortcuts are the ones that are restored, but the owner package hasn't been
       * installed yet.
       * @hide
       */
      public static final int FLAG_SHADOW = 1 << 12;
  
      /** @hide */
      public static final int FLAG_LONG_LIVED = 1 << 13;
  
      /** @hide */
      @IntDef(flag = true, prefix = { "FLAG_" }, value = {
              FLAG_DYNAMIC,
              FLAG_PINNED,
              FLAG_HAS_ICON_RES,
              FLAG_HAS_ICON_FILE,
              FLAG_KEY_FIELDS_ONLY,
              FLAG_MANIFEST,
              FLAG_DISABLED,
              FLAG_STRINGS_RESOLVED,
              FLAG_IMMUTABLE,
              FLAG_ADAPTIVE_BITMAP,
              FLAG_RETURNED_BY_SERVICE,
              FLAG_ICON_FILE_PENDING_SAVE,
              FLAG_SHADOW,
              FLAG_LONG_LIVED,
      })
      @Retention(RetentionPolicy.SOURCE)
      public @interface ShortcutFlags {}
  
      // Cloning options.
  
      /** @hide */
      private static final int CLONE_REMOVE_ICON = 1 << 0;
  
      /** @hide */
      private static final int CLONE_REMOVE_INTENT = 1 << 1;
  
      /** @hide */
      public static final int CLONE_REMOVE_NON_KEY_INFO = 1 << 2;
  
      /** @hide */
      public static final int CLONE_REMOVE_RES_NAMES = 1 << 3;
  
      /** @hide */
      public static final int CLONE_REMOVE_PERSON = 1 << 4;
  
      /** @hide */
      public static final int CLONE_REMOVE_FOR_CREATOR = CLONE_REMOVE_ICON | CLONE_REMOVE_RES_NAMES;
  
      /** @hide */
      public static final int CLONE_REMOVE_FOR_LAUNCHER = CLONE_REMOVE_ICON | CLONE_REMOVE_INTENT
              | CLONE_REMOVE_RES_NAMES | CLONE_REMOVE_PERSON;
  
      /** @hide */
      public static final int CLONE_REMOVE_FOR_LAUNCHER_APPROVAL = CLONE_REMOVE_INTENT
              | CLONE_REMOVE_RES_NAMES | CLONE_REMOVE_PERSON;
  
      /** @hide */
      public static final int CLONE_REMOVE_FOR_APP_PREDICTION = CLONE_REMOVE_ICON
              | CLONE_REMOVE_RES_NAMES;
  
      /** @hide */
      @IntDef(flag = true, prefix = { "CLONE_" }, value = {
              CLONE_REMOVE_ICON,
              CLONE_REMOVE_INTENT,
              CLONE_REMOVE_NON_KEY_INFO,
              CLONE_REMOVE_RES_NAMES,
              CLONE_REMOVE_PERSON,
              CLONE_REMOVE_FOR_CREATOR,
              CLONE_REMOVE_FOR_LAUNCHER,
              CLONE_REMOVE_FOR_LAUNCHER_APPROVAL,
              CLONE_REMOVE_FOR_APP_PREDICTION
      })
      @Retention(RetentionPolicy.SOURCE)
      public @interface CloneFlags {}
  
      /**
       * Shortcut is not disabled.
       */
      public static final int DISABLED_REASON_NOT_DISABLED = 0;
  
      /**
       * Shortcut has been disabled by the publisher app with the
       * {@link ShortcutManager#disableShortcuts(List)} API.
       */
      public static final int DISABLED_REASON_BY_APP = 1;
  
      /**
       * Shortcut has been disabled due to changes to the publisher app. (e.g. a manifest shortcut
       * no longer exists.)
       */
      public static final int DISABLED_REASON_APP_CHANGED = 2;
  
      /**
       * Shortcut is disabled for an unknown reason.
       */
      public static final int DISABLED_REASON_UNKNOWN = 3;
  
      /**
       * A disabled reason that's equal to or bigger than this is due to backup and restore issue.
       * A shortcut with such a reason wil be visible to the launcher, but not to the publisher.
       * ({@link #isVisibleToPublisher()} will be false.)
       */
      private static final int DISABLED_REASON_RESTORE_ISSUE_START = 100;
  
      /**
       * Shortcut has been restored from the previous device, but the publisher app on the current
       * device is of a lower version. The shortcut will not be usable until the app is upgraded to
       * the same version or higher.
       */
      public static final int DISABLED_REASON_VERSION_LOWER = 100;
  
      /**
       * Shortcut has not been restored because the publisher app does not support backup and restore.
       */
      public static final int DISABLED_REASON_BACKUP_NOT_SUPPORTED = 101;
  
      /**
       * Shortcut has not been restored because the publisher app's signature has changed.
       */
      public static final int DISABLED_REASON_SIGNATURE_MISMATCH = 102;
  
      /**
       * Shortcut has not been restored for unknown reason.
       */
      public static final int DISABLED_REASON_OTHER_RESTORE_ISSUE = 103;
  
      /** @hide */
      @IntDef(prefix = { "DISABLED_REASON_" }, value = {
              DISABLED_REASON_NOT_DISABLED,
              DISABLED_REASON_BY_APP,
              DISABLED_REASON_APP_CHANGED,
              DISABLED_REASON_UNKNOWN,
              DISABLED_REASON_VERSION_LOWER,
              DISABLED_REASON_BACKUP_NOT_SUPPORTED,
              DISABLED_REASON_SIGNATURE_MISMATCH,
              DISABLED_REASON_OTHER_RESTORE_ISSUE,
      })
      @Retention(RetentionPolicy.SOURCE)
      public @interface DisabledReason{}
  
      /**
       * Return a label for disabled reasons, which are *not* supposed to be shown to the user.
       * @hide
       */
      public static String getDisabledReasonDebugString(@DisabledReason int disabledReason) {
          switch (disabledReason) {
              case DISABLED_REASON_NOT_DISABLED:
                  return "[Not disabled]";
              case DISABLED_REASON_BY_APP:
                  return "[Disabled: by app]";
              case DISABLED_REASON_APP_CHANGED:
                  return "[Disabled: app changed]";
              case DISABLED_REASON_VERSION_LOWER:
                  return "[Disabled: lower version]";
              case DISABLED_REASON_BACKUP_NOT_SUPPORTED:
                  return "[Disabled: backup not supported]";
              case DISABLED_REASON_SIGNATURE_MISMATCH:
                  return "[Disabled: signature mismatch]";
              case DISABLED_REASON_OTHER_RESTORE_ISSUE:
                  return "[Disabled: unknown restore issue]";
          }
          return "[Disabled: unknown reason:" + disabledReason + "]";
      }
  
      /**
       * Return a label for a disabled reason for shortcuts that are disabled due to a backup and
       * restore issue. If the reason is not due to backup & restore, then it'll return null.
       *
       * This method returns localized, user-facing strings, which will be returned by
       * {@link #getDisabledMessage()}.
       *
       * @hide
       */
      public static String getDisabledReasonForRestoreIssue(Context context,
              @DisabledReason int disabledReason) {
          final Resources res = context.getResources();
  
          switch (disabledReason) {
              case DISABLED_REASON_VERSION_LOWER:
                  return res.getString(
                          com.android.internal.R.string.shortcut_restored_on_lower_version);
              case DISABLED_REASON_BACKUP_NOT_SUPPORTED:
                  return res.getString(
                          com.android.internal.R.string.shortcut_restore_not_supported);
              case DISABLED_REASON_SIGNATURE_MISMATCH:
                  return res.getString(
                          com.android.internal.R.string.shortcut_restore_signature_mismatch);
              case DISABLED_REASON_OTHER_RESTORE_ISSUE:
                  return res.getString(
                          com.android.internal.R.string.shortcut_restore_unknown_issue);
              case DISABLED_REASON_UNKNOWN:
                  return res.getString(
                          com.android.internal.R.string.shortcut_disabled_reason_unknown);
          }
          return null;
      }
  
      /** @hide */
      public static boolean isDisabledForRestoreIssue(@DisabledReason int disabledReason) {
          return disabledReason >= DISABLED_REASON_RESTORE_ISSUE_START;
      }
  
      /**
       * Shortcut category for messaging related actions, such as chat.
       */
      public static final String SHORTCUT_CATEGORY_CONVERSATION = "android.shortcut.conversation";
  
      private final String mId;
  
      @NonNull
      private final String mPackageName;
  
      @Nullable
      private ComponentName mActivity;
  
      @Nullable
      private Icon mIcon;
  
      private int mTitleResId;
  
      private String mTitleResName;
  
      @Nullable
      private CharSequence mTitle;
  
      private int mTextResId;
  
      private String mTextResName;
  
      @Nullable
      private CharSequence mText;
  
      private int mDisabledMessageResId;
  
      private String mDisabledMessageResName;
  
      @Nullable
      private CharSequence mDisabledMessage;
  
      @Nullable
      private ArraySet<String> mCategories;
  
      /**
       * Intents *with extras removed*.
       */
      @Nullable
      private Intent[] mIntents;
  
      /**
       * Extras for the intents.
       */
      @Nullable
      private PersistableBundle[] mIntentPersistableExtrases;
  
      @Nullable
      private Person[] mPersons;
  
      @Nullable
      private LocusId mLocusId;
  
      private int mRank;
  
      /**
       * Internally used for auto-rank-adjustment.
       *
       * RANK_CHANGED_BIT is used to denote that the rank of a shortcut is changing.
       * The rest of the bits are used to denote the order in which shortcuts are passed to
       * APIs, which is used to preserve the argument order when ranks are tie.
       */
      private int mImplicitRank;
  
      @Nullable
      private PersistableBundle mExtras;
  
      private long mLastChangedTimestamp;
  
      // Internal use only.
      @ShortcutFlags
      private int mFlags;
  
      // Internal use only.
      private int mIconResId;
  
      private String mIconResName;
  
      // Internal use only.
      @Nullable
      private String mBitmapPath;
  
      private final int mUserId;
  
      /** @hide */
      public static final int VERSION_CODE_UNKNOWN = -1;
  
      private int mDisabledReason;
  
      private ShortcutInfo(Builder b) {
          mUserId = b.mContext.getUserId();
  
          mId = Preconditions.checkStringNotEmpty(b.mId, "Shortcut ID must be provided");
  
          // Note we can't do other null checks here because SM.updateShortcuts() takes partial
          // information.
          mPackageName = b.mContext.getPackageName();
          mActivity = b.mActivity;
          mIcon = b.mIcon;
          mTitle = b.mTitle;
          mTitleResId = b.mTitleResId;
          mText = b.mText;
          mTextResId = b.mTextResId;
          mDisabledMessage = b.mDisabledMessage;
          mDisabledMessageResId = b.mDisabledMessageResId;
          mCategories = cloneCategories(b.mCategories);
          mIntents = cloneIntents(b.mIntents);
          fixUpIntentExtras();
          mPersons = clonePersons(b.mPersons);
          if (b.mIsLongLived) {
              setLongLived();
          }
          mRank = b.mRank;
          mExtras = b.mExtras;
          mLocusId = b.mLocusId;
  
          updateTimestamp();
      }
  
      /**
       * Extract extras from {@link #mIntents} and set them to {@link #mIntentPersistableExtrases}
       * as {@link PersistableBundle}, and remove extras from the original intents.
       */
      private void fixUpIntentExtras() {
          if (mIntents == null) {
              mIntentPersistableExtrases = null;
              return;
          }
          mIntentPersistableExtrases = new PersistableBundle[mIntents.length];
          for (int i = 0; i < mIntents.length; i++) {
              final Intent intent = mIntents[i];
              final Bundle extras = intent.getExtras();
              if (extras == null) {
                  mIntentPersistableExtrases[i] = null;
              } else {
                  mIntentPersistableExtrases[i] = new PersistableBundle(extras);
                  intent.replaceExtras((Bundle) null);
              }
          }
      }
  
      private static ArraySet<String> cloneCategories(Set<String> source) {
          if (source == null) {
              return null;
          }
          final ArraySet<String> ret = new ArraySet<>(source.size());
          for (CharSequence s : source) {
              if (!TextUtils.isEmpty(s)) {
                  ret.add(s.toString().intern());
              }
          }
          return ret;
      }
  
      private static Intent[] cloneIntents(Intent[] intents) {
          if (intents == null) {
              return null;
          }
          final Intent[] ret = new Intent[intents.length];
          for (int i = 0; i < ret.length; i++) {
              if (intents[i] != null) {
                  ret[i] = new Intent(intents[i]);
              }
          }
          return ret;
      }
  
      private static PersistableBundle[] clonePersistableBundle(PersistableBundle[] bundle) {
          if (bundle == null) {
              return null;
          }
          final PersistableBundle[] ret = new PersistableBundle[bundle.length];
          for (int i = 0; i < ret.length; i++) {
              if (bundle[i] != null) {
                  ret[i] = new PersistableBundle(bundle[i]);
              }
          }
          return ret;
      }
  
      private static Person[] clonePersons(Person[] persons) {
          if (persons == null) {
              return null;
          }
          final Person[] ret = new Person[persons.length];
          for (int i = 0; i < ret.length; i++) {
              if (persons[i] != null) {
                  // Don't need to keep the icon, remove it to save space
                  ret[i] = persons[i].toBuilder().setIcon(null).build();
              }
          }
          return ret;
      }
  
      /**
       * Throws if any of the mandatory fields is not set.
       *
       * @hide
       */
      public void enforceMandatoryFields(boolean forPinned) {
          Preconditions.checkStringNotEmpty(mId, "Shortcut ID must be provided");
          if (!forPinned) {
              Preconditions.checkNotNull(mActivity, "Activity must be provided");
          }
          if (mTitle == null && mTitleResId == 0) {
              throw new IllegalArgumentException("Short label must be provided");
          }
          Preconditions.checkNotNull(mIntents, "Shortcut Intent must be provided");
          Preconditions.checkArgument(mIntents.length > 0, "Shortcut Intent must be provided");
      }
  
      /**
       * Copy constructor.
       */
      private ShortcutInfo(ShortcutInfo source, @CloneFlags int cloneFlags) {
          mUserId = source.mUserId;
          mId = source.mId;
          mPackageName = source.mPackageName;
          mActivity = source.mActivity;
          mFlags = source.mFlags;
          mLastChangedTimestamp = source.mLastChangedTimestamp;
          mDisabledReason = source.mDisabledReason;
          mLocusId = source.mLocusId;
  
          // Just always keep it since it's cheep.
          mIconResId = source.mIconResId;
  
          if ((cloneFlags & CLONE_REMOVE_NON_KEY_INFO) == 0) {
  
              if ((cloneFlags & CLONE_REMOVE_ICON) == 0) {
                  mIcon = source.mIcon;
                  mBitmapPath = source.mBitmapPath;
              }
  
              mTitle = source.mTitle;
              mTitleResId = source.mTitleResId;
              mText = source.mText;
              mTextResId = source.mTextResId;
              mDisabledMessage = source.mDisabledMessage;
              mDisabledMessageResId = source.mDisabledMessageResId;
              mCategories = cloneCategories(source.mCategories);
              if ((cloneFlags & CLONE_REMOVE_PERSON) == 0) {
                  mPersons = clonePersons(source.mPersons);
              }
              if ((cloneFlags & CLONE_REMOVE_INTENT) == 0) {
                  mIntents = cloneIntents(source.mIntents);
                  mIntentPersistableExtrases =
                          clonePersistableBundle(source.mIntentPersistableExtrases);
              }
              mRank = source.mRank;
              mExtras = source.mExtras;
  
              if ((cloneFlags & CLONE_REMOVE_RES_NAMES) == 0) {
                  mTitleResName = source.mTitleResName;
                  mTextResName = source.mTextResName;
                  mDisabledMessageResName = source.mDisabledMessageResName;
                  mIconResName = source.mIconResName;
              }
          } else {
              // Set this bit.
              mFlags |= FLAG_KEY_FIELDS_ONLY;
          }
      }
  
      /**
       * Load a string resource from the publisher app.
       *
       * @param resId resource ID
       * @param defValue default value to be returned when the specified resource isn't found.
       */
      private CharSequence getResourceString(Resources res, int resId, CharSequence defValue) {
          try {
              return res.getString(resId);
          } catch (NotFoundException e) {
              Log.e(TAG, "Resource for ID=" + resId + " not found in package " + mPackageName);
              return defValue;
          }
      }
  
      /**
       * Load the string resources for the text fields and set them to the actual value fields.
       * This will set {@link #FLAG_STRINGS_RESOLVED}.
       *
       * @param res {@link Resources} for the publisher.  Must have been loaded with
       * {@link PackageManager#getResourcesForApplicationAsUser}.
       *
       * @hide
       */
      public void resolveResourceStrings(@NonNull Resources res) {
          mFlags |= FLAG_STRINGS_RESOLVED;
  
          if ((mTitleResId == 0) && (mTextResId == 0) && (mDisabledMessageResId == 0)) {
              return; // Bail early.
          }
  
          if (mTitleResId != 0) {
              mTitle = getResourceString(res, mTitleResId, mTitle);
          }
          if (mTextResId != 0) {
              mText = getResourceString(res, mTextResId, mText);
          }
          if (mDisabledMessageResId != 0) {
              mDisabledMessage = getResourceString(res, mDisabledMessageResId, mDisabledMessage);
          }
      }
  
      /**
       * Look up resource name for a given resource ID.
       *
       * @return a simple resource name (e.g. "text_1") when {@code withType} is false, or with the
       * type (e.g. "string/text_1").
       *
       * @hide
       */
      @VisibleForTesting
      public static String lookUpResourceName(@NonNull Resources res, int resId, boolean withType,
              @NonNull String packageName) {
          if (resId == 0) {
              return null;
          }
          try {
              final String fullName = res.getResourceName(resId);
  
              if (ANDROID_PACKAGE_NAME.equals(getResourcePackageName(fullName))) {
                  // If it's a framework resource, the value won't change, so just return the ID
                  // value as a string.
                  return String.valueOf(resId);
              }
              return withType ? getResourceTypeAndEntryName(fullName)
                      : getResourceEntryName(fullName);
          } catch (NotFoundException e) {
              Log.e(TAG, "Resource name for ID=" + resId + " not found in package " + packageName
                      + ". Resource IDs may change when the application is upgraded, and the system"
                      + " may not be able to find the correct resource.");
              return null;
          }
      }
  
      /**
       * Extract the package name from a fully-donated resource name.
       * e.g. "com.android.app1:drawable/icon1" -> "com.android.app1"
       * @hide
       */
      @VisibleForTesting
      public static String getResourcePackageName(@NonNull String fullResourceName) {
          final int p1 = fullResourceName.indexOf(':');
          if (p1 < 0) {
              return null;
          }
          return fullResourceName.substring(0, p1);
      }
  
      /**
       * Extract the type name from a fully-donated resource name.
       * e.g. "com.android.app1:drawable/icon1" -> "drawable"
       * @hide
       */
      @VisibleForTesting
      public static String getResourceTypeName(@NonNull String fullResourceName) {
          final int p1 = fullResourceName.indexOf(':');
          if (p1 < 0) {
              return null;
          }
          final int p2 = fullResourceName.indexOf('/', p1 + 1);
          if (p2 < 0) {
              return null;
          }
          return fullResourceName.substring(p1 + 1, p2);
      }
  
      /**
       * Extract the type name + the entry name from a fully-donated resource name.
       * e.g. "com.android.app1:drawable/icon1" -> "drawable/icon1"
       * @hide
       */
      @VisibleForTesting
      public static String getResourceTypeAndEntryName(@NonNull String fullResourceName) {
          final int p1 = fullResourceName.indexOf(':');
          if (p1 < 0) {
              return null;
          }
          return fullResourceName.substring(p1 + 1);
      }
  
      /**
       * Extract the entry name from a fully-donated resource name.
       * e.g. "com.android.app1:drawable/icon1" -> "icon1"
       * @hide
       */
      @VisibleForTesting
      public static String getResourceEntryName(@NonNull String fullResourceName) {
          final int p1 = fullResourceName.indexOf('/');
          if (p1 < 0) {
              return null;
          }
          return fullResourceName.substring(p1 + 1);
      }
  
      /**
       * Return the resource ID for a given resource ID.
       *
       * Basically its' a wrapper over {@link Resources#getIdentifier(String, String, String)}, except
       * if {@code resourceName} is an integer then it'll just return its value.  (Which also the
       * aforementioned method would do internally, but not documented, so doing here explicitly.)
       *
       * @param res {@link Resources} for the publisher.  Must have been loaded with
       * {@link PackageManager#getResourcesForApplicationAsUser}.
       *
       * @hide
       */
      @VisibleForTesting
      public static int lookUpResourceId(@NonNull Resources res, @Nullable String resourceName,
              @Nullable String resourceType, String packageName) {
          if (resourceName == null) {
              return 0;
          }
          try {
              try {
                  // It the name can be parsed as an integer, just use it.
                  return Integer.parseInt(resourceName);
              } catch (NumberFormatException ignore) {
              }
  
              return res.getIdentifier(resourceName, resourceType, packageName);
          } catch (NotFoundException e) {
              Log.e(TAG, "Resource ID for name=" + resourceName + " not found in package "
                      + packageName);
              return 0;
          }
      }
  
      /**
       * Look up resource names from the resource IDs for the icon res and the text fields, and fill
       * in the resource name fields.
       *
       * @param res {@link Resources} for the publisher.  Must have been loaded with
       * {@link PackageManager#getResourcesForApplicationAsUser}.
       *
       * @hide
       */
      public void lookupAndFillInResourceNames(@NonNull Resources res) {
          if ((mTitleResId == 0) && (mTextResId == 0) && (mDisabledMessageResId == 0)
                  && (mIconResId == 0)) {
              return; // Bail early.
          }
  
          // We don't need types for strings because their types are always "string".
          mTitleResName = lookUpResourceName(res, mTitleResId, /*withType=*/ false, mPackageName);
          mTextResName = lookUpResourceName(res, mTextResId, /*withType=*/ false, mPackageName);
          mDisabledMessageResName = lookUpResourceName(res, mDisabledMessageResId,
                  /*withType=*/ false, mPackageName);
  
          // But icons have multiple possible types, so include the type.
          mIconResName = lookUpResourceName(res, mIconResId, /*withType=*/ true, mPackageName);
      }
  
      /**
       * Look up resource IDs from the resource names for the icon res and the text fields, and fill
       * in the resource ID fields.
       *
       * This is called when an app is updated.
       *
       * @hide
       */
      public void lookupAndFillInResourceIds(@NonNull Resources res) {
          if ((mTitleResName == null) && (mTextResName == null) && (mDisabledMessageResName == null)
                  && (mIconResName == null)) {
              return; // Bail early.
          }
  
          mTitleResId = lookUpResourceId(res, mTitleResName, RES_TYPE_STRING, mPackageName);
          mTextResId = lookUpResourceId(res, mTextResName, RES_TYPE_STRING, mPackageName);
          mDisabledMessageResId = lookUpResourceId(res, mDisabledMessageResName, RES_TYPE_STRING,
                  mPackageName);
  
          // mIconResName already contains the type, so the third argument is not needed.
          mIconResId = lookUpResourceId(res, mIconResName, null, mPackageName);
      }
  
      /**
       * Copy a {@link ShortcutInfo}, optionally removing fields.
       * @hide
       */
      public ShortcutInfo clone(@CloneFlags int cloneFlags) {
          return new ShortcutInfo(this, cloneFlags);
      }
  
      /**
       * @hide
       *
       * @isUpdating set true if it's "update", as opposed to "replace".
       */
      public void ensureUpdatableWith(ShortcutInfo source, boolean isUpdating) {
          if (isUpdating) {
              Preconditions.checkState(isVisibleToPublisher(),
                      "[Framework BUG] Invisible shortcuts can't be updated");
          }
          Preconditions.checkState(mUserId == source.mUserId, "Owner User ID must match");
          Preconditions.checkState(mId.equals(source.mId), "ID must match");
          Preconditions.checkState(mPackageName.equals(source.mPackageName),
                  "Package name must match");
  
          if (isVisibleToPublisher()) {
              // Don't do this check for restore-blocked shortcuts.
              Preconditions.checkState(!isImmutable(), "Target ShortcutInfo is immutable");
          }
      }
  
      /**
       * Copy non-null/zero fields from another {@link ShortcutInfo}.  Only "public" information
       * will be overwritten.  The timestamp will *not* be updated to be consistent with other
       * setters (and also the clock is not injectable in this file).
       *
       * - Flags will not change
       * - mBitmapPath will not change
       * - Current time will be set to timestamp
       *
       * @throws IllegalStateException if source is not compatible.
       *
       * @hide
       */
      public void copyNonNullFieldsFrom(ShortcutInfo source) {
          ensureUpdatableWith(source, /*isUpdating=*/ true);
  
          if (source.mActivity != null) {
              mActivity = source.mActivity;
          }
  
          if (source.mIcon != null) {
              mIcon = source.mIcon;
  
              mIconResId = 0;
              mIconResName = null;
              mBitmapPath = null;
          }
          if (source.mTitle != null) {
              mTitle = source.mTitle;
              mTitleResId = 0;
              mTitleResName = null;
          } else if (source.mTitleResId != 0) {
              mTitle = null;
              mTitleResId = source.mTitleResId;
              mTitleResName = null;
          }
  
          if (source.mText != null) {
              mText = source.mText;
              mTextResId = 0;
              mTextResName = null;
          } else if (source.mTextResId != 0) {
              mText = null;
              mTextResId = source.mTextResId;
              mTextResName = null;
          }
          if (source.mDisabledMessage != null) {
              mDisabledMessage = source.mDisabledMessage;
              mDisabledMessageResId = 0;
              mDisabledMessageResName = null;
          } else if (source.mDisabledMessageResId != 0) {
              mDisabledMessage = null;
              mDisabledMessageResId = source.mDisabledMessageResId;
              mDisabledMessageResName = null;
          }
          if (source.mCategories != null) {
              mCategories = cloneCategories(source.mCategories);
          }
          if (source.mPersons != null) {
              mPersons = clonePersons(source.mPersons);
          }
          if (source.mIntents != null) {
              mIntents = cloneIntents(source.mIntents);
              mIntentPersistableExtrases =
                      clonePersistableBundle(source.mIntentPersistableExtrases);
          }
          if (source.mRank != RANK_NOT_SET) {
              mRank = source.mRank;
          }
          if (source.mExtras != null) {
              mExtras = source.mExtras;
          }
  
          if (source.mLocusId != null) {
              mLocusId = source.mLocusId;
          }
      }
  
      /**
       * @hide
       */
      public static Icon validateIcon(Icon icon) {
          switch (icon.getType()) {
              case Icon.TYPE_RESOURCE:
              case Icon.TYPE_BITMAP:
              case Icon.TYPE_ADAPTIVE_BITMAP:
                  break; // OK
              default:
                  throw getInvalidIconException();
          }
          if (icon.hasTint()) {
              throw new IllegalArgumentException("Icons with tints are not supported");
          }
  
          return icon;
      }
  
      /** @hide */
      public static IllegalArgumentException getInvalidIconException() {
          return new IllegalArgumentException("Unsupported icon type:"
                  +" only the bitmap and resource types are supported");
      }
  
      /**
       * Builder class for {@link ShortcutInfo} objects.
       *
       * @see ShortcutManager
       */
      public static class Builder {
          private final Context mContext;
  
          private String mId;
  
          private ComponentName mActivity;
  
          private Icon mIcon;
  
          private int mTitleResId;
  
          private CharSequence mTitle;
  
          private int mTextResId;
  
          private CharSequence mText;
  
          private int mDisabledMessageResId;
  
          private CharSequence mDisabledMessage;
  
          private Set<String> mCategories;
  
          private Intent[] mIntents;
  
          private Person[] mPersons;
  
          private boolean mIsLongLived;
  
          private int mRank = RANK_NOT_SET;
  
          private PersistableBundle mExtras;
  
          private LocusId mLocusId;
  
          /**
           * Old style constructor.
           * @hide
           */
          @Deprecated
          public Builder(Context context) {
              mContext = context;
          }
  
          /**
           * Used with the old style constructor, kept for unit tests.
           * @hide
           */
          @NonNull
          @Deprecated
          public Builder setId(@NonNull String id) {
              mId = Preconditions.checkStringNotEmpty(id, "id cannot be empty");
              return this;
          }
  
          /**
           * Constructor.
           *
           * @param context Client context.
           * @param id ID of the shortcut.
           */
          public Builder(Context context, String id) {
              mContext = context;
              mId = Preconditions.checkStringNotEmpty(id, "id cannot be empty");
          }
  
          /**
           * Sets the {@link LocusId} associated with this shortcut.
           *
           * <p>This method should be called when the {@link LocusId} is used in other places (such
           * as {@link Notification} and {@link ContentCaptureContext}) so the Android system can
           * correlate them.
           */
          @NonNull
          public Builder setLocusId(@NonNull LocusId locusId) {
              mLocusId = Preconditions.checkNotNull(locusId, "locusId cannot be null");
              return this;
          }
  
          /**
           * Sets the target activity.  A shortcut will be shown along with this activity's icon
           * on the launcher.
           *
           * When selecting a target activity, keep the following in mind:
           * <ul>
           * <li>All dynamic shortcuts must have a target activity.  When a shortcut with no target
           * activity is published using
           * {@link ShortcutManager#addDynamicShortcuts(List)} or
           * {@link ShortcutManager#setDynamicShortcuts(List)},
           * the first main activity defined in the app's <code>AndroidManifest.xml</code>
           * file is used.
           *
           * <li>Only "main" activities&mdash;ones that define the {@link Intent#ACTION_MAIN}
           * and {@link Intent#CATEGORY_LAUNCHER} intent filters&mdash;can be target
           * activities.
           *
           * <li>By default, the first main activity defined in the app's manifest is
           * the target activity.
           *
           * <li>A target activity must belong to the publisher app.
           * </ul>
           *
           * @see ShortcutInfo#getActivity()
           */
          @NonNull
          public Builder setActivity(@NonNull ComponentName activity) {
              mActivity = Preconditions.checkNotNull(activity, "activity cannot be null");
              return this;
          }
  
          /**
           * Sets an icon of a shortcut.
           *
           * <p>Icons are not available on {@link ShortcutInfo} instances
           * returned by {@link ShortcutManager} or {@link LauncherApps}.  The default launcher
           * app can use {@link LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)}
           * or {@link LauncherApps#getShortcutBadgedIconDrawable(ShortcutInfo, int)} to fetch
           * shortcut icons.
           *
           * <p>Tints set with {@link Icon#setTint} or {@link Icon#setTintList} are not supported
           * and will be ignored.
           *
           * <p>Only icons created with {@link Icon#createWithBitmap(Bitmap)},
           * {@link Icon#createWithAdaptiveBitmap(Bitmap)}
           * and {@link Icon#createWithResource} are supported.
           * Other types, such as URI-based icons, are not supported.
           *
           * @see LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)
           * @see LauncherApps#getShortcutBadgedIconDrawable(ShortcutInfo, int)
           */
          @NonNull
          public Builder setIcon(Icon icon) {
              mIcon = validateIcon(icon);
              return this;
          }
  
          /**
           * @hide We don't support resource strings for dynamic shortcuts for now.  (But unit tests
           * use it.)
           */
          @Deprecated
          public Builder setShortLabelResId(int shortLabelResId) {
              Preconditions.checkState(mTitle == null, "shortLabel already set");
              mTitleResId = shortLabelResId;
              return this;
          }
  
          /**
           * Sets the short title of a shortcut.
           *
           * <p>This is a mandatory field when publishing a new shortcut with
           * {@link ShortcutManager#addDynamicShortcuts(List)} or
           * {@link ShortcutManager#setDynamicShortcuts(List)}.
           *
           * <p>This field is intended to be a concise description of a shortcut.
           *
           * <p>The recommended maximum length is 10 characters.
           *
           * @see ShortcutInfo#getShortLabel()
           */
          @NonNull
          public Builder setShortLabel(@NonNull CharSequence shortLabel) {
              Preconditions.checkState(mTitleResId == 0, "shortLabelResId already set");
              mTitle = Preconditions.checkStringNotEmpty(shortLabel, "shortLabel cannot be empty");
              return this;
          }
  
          /**
           * @hide We don't support resource strings for dynamic shortcuts for now.  (But unit tests
           * use it.)
           */
          @Deprecated
          public Builder setLongLabelResId(int longLabelResId) {
              Preconditions.checkState(mText == null, "longLabel already set");
              mTextResId = longLabelResId;
              return this;
          }
  
          /**
           * Sets the text of a shortcut.
           *
           * <p>This field is intended to be more descriptive than the shortcut title.  The launcher
           * shows this instead of the short title when it has enough space.
           *
           * <p>The recommend maximum length is 25 characters.
           *
           * @see ShortcutInfo#getLongLabel()
           */
          @NonNull
          public Builder setLongLabel(@NonNull CharSequence longLabel) {
              Preconditions.checkState(mTextResId == 0, "longLabelResId already set");
              mText = Preconditions.checkStringNotEmpty(longLabel, "longLabel cannot be empty");
              return this;
          }
  
          /** @hide -- old signature, the internal code still uses it. */
          @Deprecated
          public Builder setTitle(@NonNull CharSequence value) {
              return setShortLabel(value);
          }
  
          /** @hide -- old signature, the internal code still uses it. */
          @Deprecated
          public Builder setTitleResId(int value) {
              return setShortLabelResId(value);
          }
  
          /** @hide -- old signature, the internal code still uses it. */
          @Deprecated
          public Builder setText(@NonNull CharSequence value) {
              return setLongLabel(value);
          }
  
          /** @hide -- old signature, the internal code still uses it. */
          @Deprecated
          public Builder setTextResId(int value) {
              return setLongLabelResId(value);
          }
  
          /**
           * @hide We don't support resource strings for dynamic shortcuts for now.  (But unit tests
           * use it.)
           */
          @Deprecated
          public Builder setDisabledMessageResId(int disabledMessageResId) {
              Preconditions.checkState(mDisabledMessage == null, "disabledMessage already set");
              mDisabledMessageResId = disabledMessageResId;
              return this;
          }
  
          /**
           * Sets the message that should be shown when the user attempts to start a shortcut that
           * is disabled.
           *
           * @see ShortcutInfo#getDisabledMessage()
           */
          @NonNull
          public Builder setDisabledMessage(@NonNull CharSequence disabledMessage) {
              Preconditions.checkState(
                      mDisabledMessageResId == 0, "disabledMessageResId already set");
              mDisabledMessage =
                      Preconditions.checkStringNotEmpty(disabledMessage,
                              "disabledMessage cannot be empty");
              return this;
          }
  
          /**
           * Sets categories for a shortcut.  Launcher apps may use this information to
           * categorize shortcuts.
           *
           * @see #SHORTCUT_CATEGORY_CONVERSATION
           * @see ShortcutInfo#getCategories()
           */
          @NonNull
          public Builder setCategories(Set<String> categories) {
              mCategories = categories;
              return this;
          }
  
          /**
           * Sets the intent of a shortcut.  Alternatively, {@link #setIntents(Intent[])} can be used
           * to launch an activity with other activities in the back stack.
           *
           * <p>This is a mandatory field when publishing a new shortcut with
           * {@link ShortcutManager#addDynamicShortcuts(List)} or
           * {@link ShortcutManager#setDynamicShortcuts(List)}.
           *
           * <p>A shortcut can launch any intent that the publisher app has permission to
           * launch.  For example, a shortcut can launch an unexported activity within the publisher
           * app.  A shortcut intent doesn't have to point at the target activity.
           *
           * <p>The given {@code intent} can contain extras, but these extras must contain values
           * of primitive types in order for the system to persist these values.
           *
           * @see ShortcutInfo#getIntent()
           * @see #setIntents(Intent[])
           */
          @NonNull
          public Builder setIntent(@NonNull Intent intent) {
              return setIntents(new Intent[]{intent});
          }
  
          /**
           * Sets multiple intents instead of a single intent, in order to launch an activity with
           * other activities in back stack.  Use {@link TaskStackBuilder} to build intents. The
           * last element in the list represents the only intent that doesn't place an activity on
           * the back stack.
           * See the {@link ShortcutManager} javadoc for details.
           *
           * @see Builder#setIntent(Intent)
           * @see ShortcutInfo#getIntents()
           * @see Context#startActivities(Intent[])
           * @see TaskStackBuilder
           */
          @NonNull
          public Builder setIntents(@NonNull Intent[] intents) {
              Preconditions.checkNotNull(intents, "intents cannot be null");
              Preconditions.checkNotNull(intents.length, "intents cannot be empty");
              for (Intent intent : intents) {
                  Preconditions.checkNotNull(intent, "intents cannot contain null");
                  Preconditions.checkNotNull(intent.getAction(), "intent's action must be set");
              }
              // Make sure always clone incoming intents.
              mIntents = cloneIntents(intents);
              return this;
          }
  
          /**
           * Add a person that is relevant to this shortcut. Alternatively,
           * {@link #setPersons(Person[])} can be used to add multiple persons to a shortcut.
           *
           * <p> This is an optional field, but the addition of person may cause this shortcut to
           * appear more prominently in the user interface (e.g. ShareSheet).
           *
           * <p> A person should usually contain a uri in order to benefit from the ranking boost.
           * However, even if no uri is provided, it's beneficial to provide people in the shortcut,
           * such that listeners and voice only devices can announce and handle them properly.
           *
           * @see Person
           * @see #setPersons(Person[])
           */
          @NonNull
          public Builder setPerson(@NonNull Person person) {
              return setPersons(new Person[]{person});
          }
  
          /**
           * Sets multiple persons instead of a single person.
           *
           * @see Person
           * @see #setPerson(Person)
           */
          @NonNull
          public Builder setPersons(@NonNull Person[] persons) {
              Preconditions.checkNotNull(persons, "persons cannot be null");
              Preconditions.checkNotNull(persons.length, "persons cannot be empty");
              for (Person person : persons) {
                  Preconditions.checkNotNull(person, "persons cannot contain null");
              }
              mPersons = clonePersons(persons);
              return this;
          }
  
          /**
           * Sets if a shortcut would be valid even if it has been unpublished/invisible by the app
           * (as a dynamic or pinned shortcut). If it is long lived, it can be cached by various
           * system services even after it has been unpublished as a dynamic shortcut.
           */
          @NonNull
          public Builder setLongLived(boolean londLived) {
              mIsLongLived = londLived;
              return this;
          }
  
          /**
           * "Rank" of a shortcut, which is a non-negative value that's used by the launcher app
           * to sort shortcuts.
           *
           * See {@link ShortcutInfo#getRank()} for details.
           */
          @NonNull
          public Builder setRank(int rank) {
              Preconditions.checkArgument((0 <= rank),
                      "Rank cannot be negative or bigger than MAX_RANK");
              mRank = rank;
              return this;
          }
  
          /**
           * Extras that the app can set for any purpose.
           *
           * <p>Apps can store arbitrary shortcut metadata in extras and retrieve the
           * metadata later using {@link ShortcutInfo#getExtras()}.
           */
          @NonNull
          public Builder setExtras(@NonNull PersistableBundle extras) {
              mExtras = extras;
              return this;
          }
  
          /**
           * Creates a {@link ShortcutInfo} instance.
           */
          @NonNull
          public ShortcutInfo build() {
              return new ShortcutInfo(this);
          }
      }
  
      /**
       * Returns the ID of a shortcut.
       *
       * <p>Shortcut IDs are unique within each publisher app and must be stable across
       * devices so that shortcuts will still be valid when restored on a different device.
       * See {@link ShortcutManager} for details.
       */
      @NonNull
      public String getId() {
          return mId;
      }
  
      /**
       * Gets the {@link LocusId} associated with this shortcut.
       *
       * <p>Used by the Android system to correlate objects (such as
       * {@link Notification} and {@link ContentCaptureContext}).
       */
      @Nullable
      public LocusId getLocusId() {
          return mLocusId;
      }
  
      /**
       * Return the package name of the publisher app.
       */
      @NonNull
      public String getPackage() {
          return mPackageName;
      }
  
      /**
       * Return the target activity.
       *
       * <p>This has nothing to do with the activity that this shortcut will launch.
       * Launcher apps should show the launcher icon for the returned activity alongside
       * this shortcut.
       *
       * @see Builder#setActivity
       */
      @Nullable
      public ComponentName getActivity() {
          return mActivity;
      }
  
      /** @hide */
      public void setActivity(ComponentName activity) {
          mActivity = activity;
      }
  
      /**
       * Returns the shortcut icon.
       *
       * @hide
       */
      @Nullable
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
      public Icon getIcon() {
          return mIcon;
      }
  
      /** @hide -- old signature, the internal code still uses it. */
      @Nullable
      @Deprecated
      public CharSequence getTitle() {
          return mTitle;
      }
  
      /** @hide -- old signature, the internal code still uses it. */
      @Deprecated
      public int getTitleResId() {
          return mTitleResId;
      }
  
      /** @hide -- old signature, the internal code still uses it. */
      @Nullable
      @Deprecated
      public CharSequence getText() {
          return mText;
      }
  
      /** @hide -- old signature, the internal code still uses it. */
      @Deprecated
      public int getTextResId() {
          return mTextResId;
      }
  
      /**
       * Return the short description of a shortcut.
       *
       * @see Builder#setShortLabel(CharSequence)
       */
      @Nullable
      public CharSequence getShortLabel() {
          return mTitle;
      }
  
      /** @hide */
      public int getShortLabelResourceId() {
          return mTitleResId;
      }
  
      /**
       * Return the long description of a shortcut.
       *
       * @see Builder#setLongLabel(CharSequence)
       */
      @Nullable
      public CharSequence getLongLabel() {
          return mText;
      }
  
      /** @hide */
      public int getLongLabelResourceId() {
          return mTextResId;
      }
  
      /**
       * Return the message that should be shown when the user attempts to start a shortcut
       * that is disabled.
       *
       * @see Builder#setDisabledMessage(CharSequence)
       */
      @Nullable
      public CharSequence getDisabledMessage() {
          return mDisabledMessage;
      }
  
      /** @hide */
      public int getDisabledMessageResourceId() {
          return mDisabledMessageResId;
      }
  
      /** @hide */
      public void setDisabledReason(@DisabledReason int reason) {
          mDisabledReason = reason;
      }
  
      /**
       * Returns why a shortcut has been disabled.
       */
      @DisabledReason
      public int getDisabledReason() {
          return mDisabledReason;
      }
  
      /**
       * Return the shortcut's categories.
       *
       * @see Builder#setCategories(Set)
       */
      @Nullable
      public Set<String> getCategories() {
          return mCategories;
      }
  
      /**
       * Returns the intent that is executed when the user selects this shortcut.
       * If setIntents() was used, then return the last intent in the array.
       *
       * <p>Launcher apps <b>cannot</b> see the intent.  If a {@link ShortcutInfo} is
       * obtained via {@link LauncherApps}, then this method will always return null.
       * Launchers can only start a shortcut intent with {@link LauncherApps#startShortcut}.
       *
       * @see Builder#setIntent(Intent)
       */
      @Nullable
      public Intent getIntent() {
          if (mIntents == null || mIntents.length == 0) {
              return null;
          }
          final int last = mIntents.length - 1;
          final Intent intent = new Intent(mIntents[last]);
          return setIntentExtras(intent, mIntentPersistableExtrases[last]);
      }
  
      /**
       * Return the intent set with {@link Builder#setIntents(Intent[])}.
       *
       * <p>Launcher apps <b>cannot</b> see the intents.  If a {@link ShortcutInfo} is
       * obtained via {@link LauncherApps}, then this method will always return null.
       * Launchers can only start a shortcut intent with {@link LauncherApps#startShortcut}.
       *
       * @see Builder#setIntents(Intent[])
       */
      @Nullable
      public Intent[] getIntents() {
          final Intent[] ret = new Intent[mIntents.length];
  
          for (int i = 0; i < ret.length; i++) {
              ret[i] = new Intent(mIntents[i]);
              setIntentExtras(ret[i], mIntentPersistableExtrases[i]);
          }
  
          return ret;
      }
  
      /**
       * Return "raw" intents, which is the original intents without the extras.
       * @hide
       */
      @Nullable
      public Intent[] getIntentsNoExtras() {
          return mIntents;
      }
  
      /**
       * Return the Persons set with {@link Builder#setPersons(Person[])}.
       *
       * @hide
       */
      @Nullable
      @SystemApi
      public Person[] getPersons() {
          return clonePersons(mPersons);
      }
  
      /**
       * The extras in the intents.  We convert extras into {@link PersistableBundle} so we can
       * persist them.
       * @hide
       */
      @Nullable
      public PersistableBundle[] getIntentPersistableExtrases() {
          return mIntentPersistableExtrases;
      }
  
      /**
       * "Rank" of a shortcut, which is a non-negative, sequential value that's unique for each
       * {@link #getActivity} for each of the two types of shortcuts (static and dynamic).
       *
       * <p>Because static shortcuts and dynamic shortcuts have overlapping ranks,
       * when a launcher app shows shortcuts for an activity, it should first show
       * the static shortcuts, followed by the dynamic shortcuts.  Within each of those categories,
       * shortcuts should be sorted by rank in ascending order.
       *
       * <p><em>Floating shortcuts</em>, or shortcuts that are neither static nor dynamic, will all
       * have rank 0, because they aren't sorted.
       *
       * See the {@link ShortcutManager}'s class javadoc for details.
       *
       * @see Builder#setRank(int)
       */
      public int getRank() {
          return mRank;
      }
  
      /** @hide */
      public boolean hasRank() {
          return mRank != RANK_NOT_SET;
      }
  
      /** @hide */
      public void setRank(int rank) {
          mRank = rank;
      }
  
      /** @hide */
      public void clearImplicitRankAndRankChangedFlag() {
          mImplicitRank = 0;
      }
  
      /** @hide */
      public void setImplicitRank(int rank) {
          // Make sure to keep RANK_CHANGED_BIT.
          mImplicitRank = (mImplicitRank & RANK_CHANGED_BIT) | (rank & IMPLICIT_RANK_MASK);
      }
  
      /** @hide */
      public int getImplicitRank() {
          return mImplicitRank & IMPLICIT_RANK_MASK;
      }
  
      /** @hide */
      public void setRankChanged() {
          mImplicitRank |= RANK_CHANGED_BIT;
      }
  
      /** @hide */
      public boolean isRankChanged() {
          return (mImplicitRank & RANK_CHANGED_BIT) != 0;
      }
  
      /**
       * Extras that the app can set for any purpose.
       *
       * @see Builder#setExtras(PersistableBundle)
       */
      @Nullable
      public PersistableBundle getExtras() {
          return mExtras;
      }
  
      /** @hide */
      public int getUserId() {
          return mUserId;
      }
  
      /**
       * {@link UserHandle} on which the publisher created this shortcut.
       */
      public UserHandle getUserHandle() {
          return UserHandle.of(mUserId);
      }
  
      /**
       * Last time when any of the fields was updated.
       */
      public long getLastChangedTimestamp() {
          return mLastChangedTimestamp;
      }
  
      /** @hide */
      @ShortcutFlags
      public int getFlags() {
          return mFlags;
      }
  
      /** @hide*/
      public void replaceFlags(@ShortcutFlags int flags) {
          mFlags = flags;
      }
  
      /** @hide*/
      public void addFlags(@ShortcutFlags int flags) {
          mFlags |= flags;
      }
  
      /** @hide*/
      public void clearFlags(@ShortcutFlags int flags) {
          mFlags &= ~flags;
      }
  
      /** @hide*/
      public boolean hasFlags(@ShortcutFlags int flags) {
          return (mFlags & flags) == flags;
      }
  
      /** @hide */
      public boolean isReturnedByServer() {
          return hasFlags(FLAG_RETURNED_BY_SERVICE);
      }
  
      /** @hide */
      public void setReturnedByServer() {
          addFlags(FLAG_RETURNED_BY_SERVICE);
      }
  
      /** @hide */
      public boolean isLongLived() {
          return hasFlags(FLAG_LONG_LIVED);
      }
  
      /** @hide */
      public void setLongLived() {
          addFlags(FLAG_LONG_LIVED);
      }
  
      /** Return whether a shortcut is dynamic. */
      public boolean isDynamic() {
          return hasFlags(FLAG_DYNAMIC);
      }
  
      /** Return whether a shortcut is pinned. */
      public boolean isPinned() {
          return hasFlags(FLAG_PINNED);
      }
  
      /**
       * Return whether a shortcut is static; that is, whether a shortcut is
       * published from AndroidManifest.xml.  If {@code true}, the shortcut is
       * also {@link #isImmutable()}.
       *
       * <p>When an app is upgraded and a shortcut is no longer published from AndroidManifest.xml,
       * this will be set to {@code false}.  If the shortcut is not pinned, then it'll disappear.
       * However, if it's pinned, it will still be visible, {@link #isEnabled()} will be
       * {@code false} and {@link #isImmutable()} will be {@code true}.
       */
      public boolean isDeclaredInManifest() {
          return hasFlags(FLAG_MANIFEST);
      }
  
      /** @hide kept for unit tests */
      @Deprecated
      public boolean isManifestShortcut() {
          return isDeclaredInManifest();
      }
  
      /**
       * @return true if pinned but neither static nor dynamic.
       * @hide
       */
      public boolean isFloating() {
          return isPinned() && !(isDynamic() || isManifestShortcut());
      }
  
      /** @hide */
      public boolean isOriginallyFromManifest() {
          return hasFlags(FLAG_IMMUTABLE);
      }
  
      /** @hide */
      public boolean isDynamicVisible() {
          return isDynamic() && isVisibleToPublisher();
      }
  
      /** @hide */
      public boolean isPinnedVisible() {
          return isPinned() && isVisibleToPublisher();
      }
  
      /** @hide */
      public boolean isManifestVisible() {
          return isDeclaredInManifest() && isVisibleToPublisher();
      }
  
      /**
       * Return if a shortcut is immutable, in which case it cannot be modified with any of
       * {@link ShortcutManager} APIs.
       *
       * <p>All static shortcuts are immutable.  When a static shortcut is pinned and is then
       * disabled because it doesn't appear in AndroidManifest.xml for a newer version of the
       * app, {@link #isDeclaredInManifest()} returns {@code false}, but the shortcut
       * is still immutable.
       *
       * <p>All shortcuts originally published via the {@link ShortcutManager} APIs
       * are all mutable.
       */
      public boolean isImmutable() {
          return hasFlags(FLAG_IMMUTABLE);
      }
  
      /**
       * Returns {@code false} if a shortcut is disabled with
       * {@link ShortcutManager#disableShortcuts}.
       */
      public boolean isEnabled() {
          return !hasFlags(FLAG_DISABLED);
      }
  
      /** @hide */
      public boolean isAlive() {
          return hasFlags(FLAG_PINNED) || hasFlags(FLAG_DYNAMIC) || hasFlags(FLAG_MANIFEST);
      }
  
      /** @hide */
      public boolean usesQuota() {
          return hasFlags(FLAG_DYNAMIC) || hasFlags(FLAG_MANIFEST);
      }
  
      /**
       * Return whether a shortcut's icon is a resource in the owning package.
       *
       * @hide internal/unit tests only
       */
      public boolean hasIconResource() {
          return hasFlags(FLAG_HAS_ICON_RES);
      }
  
      /** @hide */
      public boolean hasStringResources() {
          return (mTitleResId != 0) || (mTextResId != 0) || (mDisabledMessageResId != 0);
      }
  
      /** @hide */
      public boolean hasAnyResources() {
          return hasIconResource() || hasStringResources();
      }
  
      /**
       * Return whether a shortcut's icon is stored as a file.
       *
       * @hide internal/unit tests only
       */
      public boolean hasIconFile() {
          return hasFlags(FLAG_HAS_ICON_FILE);
      }
  
      /**
       * Return whether a shortcut's icon is adaptive bitmap following design guideline
       * defined in {@link android.graphics.drawable.AdaptiveIconDrawable}.
       *
       * @hide internal/unit tests only
       */
      public boolean hasAdaptiveBitmap() {
          return hasFlags(FLAG_ADAPTIVE_BITMAP);
      }
  
      /** @hide */
      public boolean isIconPendingSave() {
          return hasFlags(FLAG_ICON_FILE_PENDING_SAVE);
      }
  
      /** @hide */
      public void setIconPendingSave() {
          addFlags(FLAG_ICON_FILE_PENDING_SAVE);
      }
  
      /** @hide */
      public void clearIconPendingSave() {
          clearFlags(FLAG_ICON_FILE_PENDING_SAVE);
      }
  
      /**
       * When the system wasn't able to restore a shortcut, it'll still be registered to the system
       * but disabled, and such shortcuts will not be visible to the publisher. They're still visible
       * to launchers though.
       *
       * @hide
       */
      @TestApi
      public boolean isVisibleToPublisher() {
          return !isDisabledForRestoreIssue(mDisabledReason);
      }
  
      /**
       * Return whether a shortcut only contains "key" information only or not.  If true, only the
       * following fields are available.
       * <ul>
       *     <li>{@link #getId()}
       *     <li>{@link #getPackage()}
       *     <li>{@link #getActivity()}
       *     <li>{@link #getLastChangedTimestamp()}
       *     <li>{@link #isDynamic()}
       *     <li>{@link #isPinned()}
       *     <li>{@link #isDeclaredInManifest()}
       *     <li>{@link #isImmutable()}
       *     <li>{@link #isEnabled()}
       *     <li>{@link #getUserHandle()}
       * </ul>
       *
       * <p>For performance reasons, shortcuts passed to
       * {@link LauncherApps.Callback#onShortcutsChanged(String, List, UserHandle)} as well as those
       * returned from {@link LauncherApps#getShortcuts(ShortcutQuery, UserHandle)}
       * while using the {@link ShortcutQuery#FLAG_GET_KEY_FIELDS_ONLY} option contain only key
       * information.
       */
      public boolean hasKeyFieldsOnly() {
          return hasFlags(FLAG_KEY_FIELDS_ONLY);
      }
  
      /** @hide */
      public boolean hasStringResourcesResolved() {
          return hasFlags(FLAG_STRINGS_RESOLVED);
      }
  
      /** @hide */
      public void updateTimestamp() {
          mLastChangedTimestamp = System.currentTimeMillis();
      }
  
      /** @hide */
      // VisibleForTesting
      public void setTimestamp(long value) {
          mLastChangedTimestamp = value;
      }
  
      /** @hide */
      public void clearIcon() {
          mIcon = null;
      }
  
      /** @hide */
      public void setIconResourceId(int iconResourceId) {
          if (mIconResId != iconResourceId) {
              mIconResName = null;
          }
          mIconResId = iconResourceId;
      }
  
      /**
       * Get the resource ID for the icon, valid only when {@link #hasIconResource()} } is true.
       * @hide internal / tests only.
       */
      public int getIconResourceId() {
          return mIconResId;
      }
  
      /**
       * Bitmap path.  Note this will be null even if {@link #hasIconFile()} is set when the save
       * is pending.  Use {@link #isIconPendingSave()} to check it.
       *
       * @hide
       */
      public String getBitmapPath() {
          return mBitmapPath;
      }
  
      /** @hide */
      public void setBitmapPath(String bitmapPath) {
          mBitmapPath = bitmapPath;
      }
  
      /** @hide */
      public void setDisabledMessageResId(int disabledMessageResId) {
          if (mDisabledMessageResId != disabledMessageResId) {
              mDisabledMessageResName = null;
          }
          mDisabledMessageResId = disabledMessageResId;
          mDisabledMessage = null;
      }
  
      /** @hide */
      public void setDisabledMessage(String disabledMessage) {
          mDisabledMessage = disabledMessage;
          mDisabledMessageResId = 0;
          mDisabledMessageResName = null;
      }
  
      /** @hide */
      public String getTitleResName() {
          return mTitleResName;
      }
  
      /** @hide */
      public void setTitleResName(String titleResName) {
          mTitleResName = titleResName;
      }
  
      /** @hide */
      public String getTextResName() {
          return mTextResName;
      }
  
      /** @hide */
      public void setTextResName(String textResName) {
          mTextResName = textResName;
      }
  
      /** @hide */
      public String getDisabledMessageResName() {
          return mDisabledMessageResName;
      }
  
      /** @hide */
      public void setDisabledMessageResName(String disabledMessageResName) {
          mDisabledMessageResName = disabledMessageResName;
      }
  
      /** @hide */
      public String getIconResName() {
          return mIconResName;
      }
  
      /** @hide */
      public void setIconResName(String iconResName) {
          mIconResName = iconResName;
      }
  
      /**
       * Replaces the intent.
       *
       * @throws IllegalArgumentException when extra is not compatible with {@link PersistableBundle}.
       *
       * @hide
       */
      public void setIntents(Intent[] intents) throws IllegalArgumentException {
          Preconditions.checkNotNull(intents);
          Preconditions.checkArgument(intents.length > 0);
  
          mIntents = cloneIntents(intents);
          fixUpIntentExtras();
      }
  
      /** @hide */
      public static Intent setIntentExtras(Intent intent, PersistableBundle extras) {
          if (extras == null) {
              intent.replaceExtras((Bundle) null);
          } else {
              intent.replaceExtras(new Bundle(extras));
          }
          return intent;
      }
  
      /**
       * Replaces the categories.
       *
       * @hide
       */
      public void setCategories(Set<String> categories) {
          mCategories = cloneCategories(categories);
      }
  
      private ShortcutInfo(Parcel source) {
          final ClassLoader cl = getClass().getClassLoader();
  
          mUserId = source.readInt();
          mId = source.readString();
          mPackageName = source.readString();
          mActivity = source.readParcelable(cl);
          mFlags = source.readInt();
          mIconResId = source.readInt();
          mLastChangedTimestamp = source.readLong();
          mDisabledReason = source.readInt();
  
          if (source.readInt() == 0) {
              return; // key information only.
          }
  
          mIcon = source.readParcelable(cl);
          mTitle = source.readCharSequence();
          mTitleResId = source.readInt();
          mText = source.readCharSequence();
          mTextResId = source.readInt();
          mDisabledMessage = source.readCharSequence();
          mDisabledMessageResId = source.readInt();
          mIntents = source.readParcelableArray(cl, Intent.class);
          mIntentPersistableExtrases = source.readParcelableArray(cl, PersistableBundle.class);
          mRank = source.readInt();
          mExtras = source.readParcelable(cl);
          mBitmapPath = source.readString();
  
          mIconResName = source.readString();
          mTitleResName = source.readString();
          mTextResName = source.readString();
          mDisabledMessageResName = source.readString();
  
          int N = source.readInt();
          if (N == 0) {
              mCategories = null;
          } else {
              mCategories = new ArraySet<>(N);
              for (int i = 0; i < N; i++) {
                  mCategories.add(source.readString().intern());
              }
          }
  
          mPersons = source.readParcelableArray(cl, Person.class);
          mLocusId = source.readParcelable(cl);
      }
  
      @Override
      public void writeToParcel(Parcel dest, int flags) {
          dest.writeInt(mUserId);
          dest.writeString(mId);
          dest.writeString(mPackageName);
          dest.writeParcelable(mActivity, flags);
          dest.writeInt(mFlags);
          dest.writeInt(mIconResId);
          dest.writeLong(mLastChangedTimestamp);
          dest.writeInt(mDisabledReason);
  
          if (hasKeyFieldsOnly()) {
              dest.writeInt(0);
              return;
          }
          dest.writeInt(1);
  
          dest.writeParcelable(mIcon, flags);
          dest.writeCharSequence(mTitle);
          dest.writeInt(mTitleResId);
          dest.writeCharSequence(mText);
          dest.writeInt(mTextResId);
          dest.writeCharSequence(mDisabledMessage);
          dest.writeInt(mDisabledMessageResId);
  
          dest.writeParcelableArray(mIntents, flags);
          dest.writeParcelableArray(mIntentPersistableExtrases, flags);
          dest.writeInt(mRank);
          dest.writeParcelable(mExtras, flags);
          dest.writeString(mBitmapPath);
  
          dest.writeString(mIconResName);
          dest.writeString(mTitleResName);
          dest.writeString(mTextResName);
          dest.writeString(mDisabledMessageResName);
  
          if (mCategories != null) {
              final int N = mCategories.size();
              dest.writeInt(N);
              for (int i = 0; i < N; i++) {
                  dest.writeString(mCategories.valueAt(i));
              }
          } else {
              dest.writeInt(0);
          }
  
          dest.writeParcelableArray(mPersons, flags);
          dest.writeParcelable(mLocusId, flags);
      }
  
      public static final @android.annotation.NonNull Creator<ShortcutInfo> CREATOR =
              new Creator<ShortcutInfo>() {
                  public ShortcutInfo createFromParcel(Parcel source) {
                      return new ShortcutInfo(source);
                  }
                  public ShortcutInfo[] newArray(int size) {
                      return new ShortcutInfo[size];
                  }
              };
  
      @Override
      public int describeContents() {
          return 0;
      }
  
  
      /**
       * Return a string representation, intended for logging.  Some fields will be retracted.
       */
      @Override
      public String toString() {
          return toStringInner(/* secure =*/ true, /* includeInternalData =*/ false,
                  /*indent=*/ null);
      }
  
      /** @hide */
      public String toInsecureString() {
          return toStringInner(/* secure =*/ false, /* includeInternalData =*/ true,
                  /*indent=*/ null);
      }
  
      /** @hide */
      public String toDumpString(String indent) {
          return toStringInner(/* secure =*/ false, /* includeInternalData =*/ true, indent);
      }
  
      private void addIndentOrComma(StringBuilder sb, String indent) {
          if (indent != null) {
              sb.append("\n  ");
              sb.append(indent);
          } else {
              sb.append(", ");
          }
      }
  
      private String toStringInner(boolean secure, boolean includeInternalData, String indent) {
          final StringBuilder sb = new StringBuilder();
  
          if (indent != null) {
              sb.append(indent);
          }
  
          sb.append("ShortcutInfo {");
  
          sb.append("id=");
          sb.append(secure ? "***" : mId);
  
          sb.append(", flags=0x");
          sb.append(Integer.toHexString(mFlags));
          sb.append(" [");
          if ((mFlags & FLAG_SHADOW) != 0) {
              // Note the shadow flag isn't actually used anywhere and it's just for dumpsys, so
              // we don't have an isXxx for this.
              sb.append("Sdw");
          }
          if (!isEnabled()) {
              sb.append("Dis");
          }
          if (isImmutable()) {
              sb.append("Im");
          }
          if (isManifestShortcut()) {
              sb.append("Man");
          }
          if (isDynamic()) {
              sb.append("Dyn");
          }
          if (isPinned()) {
              sb.append("Pin");
          }
          if (hasIconFile()) {
              sb.append("Ic-f");
          }
          if (isIconPendingSave()) {
              sb.append("Pens");
          }
          if (hasIconResource()) {
              sb.append("Ic-r");
          }
          if (hasKeyFieldsOnly()) {
              sb.append("Key");
          }
          if (hasStringResourcesResolved()) {
              sb.append("Str");
          }
          if (isReturnedByServer()) {
              sb.append("Rets");
          }
          if (isLongLived()) {
              sb.append("Liv");
          }
          sb.append("]");
  
          addIndentOrComma(sb, indent);
  
          sb.append("packageName=");
          sb.append(mPackageName);
  
          addIndentOrComma(sb, indent);
  
          sb.append("activity=");
          sb.append(mActivity);
  
          addIndentOrComma(sb, indent);
  
          sb.append("shortLabel=");
          sb.append(secure ? "***" : mTitle);
          sb.append(", resId=");
          sb.append(mTitleResId);
          sb.append("[");
          sb.append(mTitleResName);
          sb.append("]");
  
          addIndentOrComma(sb, indent);
  
          sb.append("longLabel=");
          sb.append(secure ? "***" : mText);
          sb.append(", resId=");
          sb.append(mTextResId);
          sb.append("[");
          sb.append(mTextResName);
          sb.append("]");
  
          addIndentOrComma(sb, indent);
  
          sb.append("disabledMessage=");
          sb.append(secure ? "***" : mDisabledMessage);
          sb.append(", resId=");
          sb.append(mDisabledMessageResId);
          sb.append("[");
          sb.append(mDisabledMessageResName);
          sb.append("]");
  
          addIndentOrComma(sb, indent);
  
          sb.append("disabledReason=");
          sb.append(getDisabledReasonDebugString(mDisabledReason));
  
          addIndentOrComma(sb, indent);
  
          sb.append("categories=");
          sb.append(mCategories);
  
          addIndentOrComma(sb, indent);
  
          sb.append("persons=");
          sb.append(mPersons);
  
          addIndentOrComma(sb, indent);
  
          sb.append("icon=");
          sb.append(mIcon);
  
          addIndentOrComma(sb, indent);
  
          sb.append("rank=");
          sb.append(mRank);
  
          sb.append(", timestamp=");
          sb.append(mLastChangedTimestamp);
  
          addIndentOrComma(sb, indent);
  
          sb.append("intents=");
          if (mIntents == null) {
              sb.append("null");
          } else {
              if (secure) {
                  sb.append("size:");
                  sb.append(mIntents.length);
              } else {
                  final int size = mIntents.length;
                  sb.append("[");
                  String sep = "";
                  for (int i = 0; i < size; i++) {
                      sb.append(sep);
                      sep = ", ";
                      sb.append(mIntents[i]);
                      sb.append("/");
                      sb.append(mIntentPersistableExtrases[i]);
                  }
                  sb.append("]");
              }
          }
  
          addIndentOrComma(sb, indent);
  
          sb.append("extras=");
          sb.append(mExtras);
  
          if (includeInternalData) {
              addIndentOrComma(sb, indent);
  
              sb.append("iconRes=");
              sb.append(mIconResId);
              sb.append("[");
              sb.append(mIconResName);
              sb.append("]");
  
              sb.append(", bitmapPath=");
              sb.append(mBitmapPath);
          }
  
          if (mLocusId != null) {
              sb.append("locusId="); sb.append(mLocusId); // LocusId.toString() is PII-safe.
          }
  
          sb.append("}");
          return sb.toString();
      }
  
      /** @hide */
      public ShortcutInfo(
              @UserIdInt int userId, String id, String packageName, ComponentName activity,
              Icon icon, CharSequence title, int titleResId, String titleResName,
              CharSequence text, int textResId, String textResName,
              CharSequence disabledMessage, int disabledMessageResId, String disabledMessageResName,
              Set<String> categories, Intent[] intentsWithExtras, int rank, PersistableBundle extras,
              long lastChangedTimestamp,
              int flags, int iconResId, String iconResName, String bitmapPath, int disabledReason,
              Person[] persons, LocusId locusId) {
          mUserId = userId;
          mId = id;
          mPackageName = packageName;
          mActivity = activity;
          mIcon = icon;
          mTitle = title;
          mTitleResId = titleResId;
          mTitleResName = titleResName;
          mText = text;
          mTextResId = textResId;
          mTextResName = textResName;
          mDisabledMessage = disabledMessage;
          mDisabledMessageResId = disabledMessageResId;
          mDisabledMessageResName = disabledMessageResName;
          mCategories = cloneCategories(categories);
          mIntents = cloneIntents(intentsWithExtras);
          fixUpIntentExtras();
          mRank = rank;
          mExtras = extras;
          mLastChangedTimestamp = lastChangedTimestamp;
          mFlags = flags;
          mIconResId = iconResId;
          mIconResName = iconResName;
          mBitmapPath = bitmapPath;
          mDisabledReason = disabledReason;
          mPersons = persons;
          mLocusId = locusId;
      }
  }
  