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 com.android.server.pm;
  
  import static android.content.Intent.FLAG_ACTIVITY_MATCH_EXTERNAL;
  
  import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_INSTANT_APP_RESOLUTION_PHASE_ONE;
  import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO;
  import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_INSTANT_APP_LAUNCH_TOKEN;
  import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_INSTANT_APP_RESOLUTION_DELAY_MS;
  import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_INSTANT_APP_RESOLUTION_STATUS;
  
  import android.annotation.IntDef;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.app.ActivityManager;
  import android.app.PendingIntent;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.IIntentSender;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.content.IntentSender;
  import android.content.pm.ActivityInfo;
  import android.content.pm.AuxiliaryResolveInfo;
  import android.content.pm.InstantAppIntentFilter;
  import android.content.pm.InstantAppRequest;
  import android.content.pm.InstantAppResolveInfo;
  import android.content.pm.InstantAppResolveInfo.InstantAppDigest;
  import android.metrics.LogMaker;
  import android.net.Uri;
  import android.os.Build;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.RemoteException;
  import android.util.Log;
  import android.util.Slog;
  
  import com.android.internal.logging.MetricsLogger;
  import com.android.internal.logging.nano.MetricsProto;
  import com.android.server.pm.InstantAppResolverConnection.ConnectionException;
  import com.android.server.pm.InstantAppResolverConnection.PhaseTwoCallback;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Set;
  import java.util.UUID;
  
  /** @hide */
  public abstract class InstantAppResolver {
      private static final boolean DEBUG_INSTANT = Build.IS_DEBUGGABLE;
      private static final String TAG = "PackageManager";
  
      private static final int RESOLUTION_SUCCESS = 0;
      private static final int RESOLUTION_FAILURE = 1;
      /** Binding to the external service timed out */
      private static final int RESOLUTION_BIND_TIMEOUT = 2;
      /** The call to retrieve an instant application response timed out */
      private static final int RESOLUTION_CALL_TIMEOUT = 3;
  
      @IntDef(flag = true, prefix = { "RESOLUTION_" }, value = {
              RESOLUTION_SUCCESS,
              RESOLUTION_FAILURE,
              RESOLUTION_BIND_TIMEOUT,
              RESOLUTION_CALL_TIMEOUT,
      })
      @Retention(RetentionPolicy.SOURCE)
      public @interface ResolutionStatus {}
  
      private static MetricsLogger sMetricsLogger;
  
      private static MetricsLogger getLogger() {
          if (sMetricsLogger == null) {
              sMetricsLogger = new MetricsLogger();
          }
          return sMetricsLogger;
      }
  
      /**
       * Returns an intent with potential PII removed from the original intent. Fields removed
       * include extras and the host + path of the data, if defined.
       */
      public static Intent sanitizeIntent(Intent origIntent) {
          final Intent sanitizedIntent;
          sanitizedIntent = new Intent(origIntent.getAction());
          Set<String> categories = origIntent.getCategories();
          if (categories != null) {
              for (String category : categories) {
                  sanitizedIntent.addCategory(category);
              }
          }
          Uri sanitizedUri = origIntent.getData() == null
                  ? null
                  : Uri.fromParts(origIntent.getScheme(), "", "");
          sanitizedIntent.setDataAndType(sanitizedUri, origIntent.getType());
          sanitizedIntent.addFlags(origIntent.getFlags());
          sanitizedIntent.setPackage(origIntent.getPackage());
          return sanitizedIntent;
      }
  
      public static AuxiliaryResolveInfo doInstantAppResolutionPhaseOne(
              InstantAppResolverConnection connection, InstantAppRequest requestObj) {
          final long startTime = System.currentTimeMillis();
          final String token = UUID.randomUUID().toString();
          if (DEBUG_INSTANT) {
              Log.d(TAG, "[" + token + "] Phase1; resolving");
          }
          final Intent origIntent = requestObj.origIntent;
          final Intent sanitizedIntent = sanitizeIntent(origIntent);
  
          AuxiliaryResolveInfo resolveInfo = null;
          @ResolutionStatus int resolutionStatus = RESOLUTION_SUCCESS;
          try {
              final List<InstantAppResolveInfo> instantAppResolveInfoList =
                      connection.getInstantAppResolveInfoList(sanitizedIntent,
                              requestObj.digest.getDigestPrefixSecure(), requestObj.userId, token);
              if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) {
                  resolveInfo = InstantAppResolver.filterInstantAppIntent(
                          instantAppResolveInfoList, origIntent, requestObj.resolvedType,
                          requestObj.userId, origIntent.getPackage(), requestObj.digest, token);
              }
          } catch (ConnectionException e) {
              if (e.failure == ConnectionException.FAILURE_BIND) {
                  resolutionStatus = RESOLUTION_BIND_TIMEOUT;
              } else if (e.failure == ConnectionException.FAILURE_CALL) {
                  resolutionStatus = RESOLUTION_CALL_TIMEOUT;
              } else {
                  resolutionStatus = RESOLUTION_FAILURE;
              }
          }
          // Only log successful instant application resolution
          if (requestObj.resolveForStart && resolutionStatus == RESOLUTION_SUCCESS) {
              logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_ONE, startTime, token,
                      resolutionStatus);
          }
          if (DEBUG_INSTANT && resolveInfo == null) {
              if (resolutionStatus == RESOLUTION_BIND_TIMEOUT) {
                  Log.d(TAG, "[" + token + "] Phase1; bind timed out");
              } else if (resolutionStatus == RESOLUTION_CALL_TIMEOUT) {
                  Log.d(TAG, "[" + token + "] Phase1; call timed out");
              } else if (resolutionStatus != RESOLUTION_SUCCESS) {
                  Log.d(TAG, "[" + token + "] Phase1; service connection error");
              } else {
                  Log.d(TAG, "[" + token + "] Phase1; No results matched");
              }
          }
          // if the match external flag is set, return an empty resolve info instead of a null result.
          if (resolveInfo == null && (origIntent.getFlags() & FLAG_ACTIVITY_MATCH_EXTERNAL) != 0) {
              return new AuxiliaryResolveInfo(token, false, createFailureIntent(origIntent, token),
                      null /* filters */);
          }
          return resolveInfo;
      }
  
      public static void doInstantAppResolutionPhaseTwo(Context context,
              InstantAppResolverConnection connection, InstantAppRequest requestObj,
              ActivityInfo instantAppInstaller, Handler callbackHandler) {
          final long startTime = System.currentTimeMillis();
          final String token = requestObj.responseObj.token;
          if (DEBUG_INSTANT) {
              Log.d(TAG, "[" + token + "] Phase2; resolving");
          }
          final Intent origIntent = requestObj.origIntent;
          final Intent sanitizedIntent = sanitizeIntent(origIntent);
  
          final PhaseTwoCallback callback = new PhaseTwoCallback() {
              @Override
              void onPhaseTwoResolved(List<InstantAppResolveInfo> instantAppResolveInfoList,
                      long startTime) {
                  final Intent failureIntent;
                  if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) {
                      final AuxiliaryResolveInfo instantAppIntentInfo =
                              InstantAppResolver.filterInstantAppIntent(
                                      instantAppResolveInfoList, origIntent, null /*resolvedType*/,
                                      0 /*userId*/, origIntent.getPackage(), requestObj.digest,
                                      token);
                      if (instantAppIntentInfo != null) {
                          failureIntent = instantAppIntentInfo.failureIntent;
                      } else {
                          failureIntent = null;
                      }
                  } else {
                      failureIntent = null;
                  }
                  final Intent installerIntent = buildEphemeralInstallerIntent(
                          requestObj.origIntent,
                          sanitizedIntent,
                          failureIntent,
                          requestObj.callingPackage,
                          requestObj.verificationBundle,
                          requestObj.resolvedType,
                          requestObj.userId,
                          requestObj.responseObj.installFailureActivity,
                          token,
                          false /*needsPhaseTwo*/,
                          requestObj.responseObj.filters);
                  installerIntent.setComponent(new ComponentName(
                          instantAppInstaller.packageName, instantAppInstaller.name));
  
                  logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO, startTime, token,
                          requestObj.responseObj.filters != null ? RESOLUTION_SUCCESS : RESOLUTION_FAILURE);
  
                  context.startActivity(installerIntent);
              }
          };
          try {
              connection.getInstantAppIntentFilterList(sanitizedIntent,
                      requestObj.digest.getDigestPrefixSecure(), requestObj.userId, token, callback,
                      callbackHandler, startTime);
          } catch (ConnectionException e) {
              @ResolutionStatus int resolutionStatus = RESOLUTION_FAILURE;
              if (e.failure == ConnectionException.FAILURE_BIND) {
                  resolutionStatus = RESOLUTION_BIND_TIMEOUT;
              }
              logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO, startTime, token,
                      resolutionStatus);
              if (DEBUG_INSTANT) {
                  if (resolutionStatus == RESOLUTION_BIND_TIMEOUT) {
                      Log.d(TAG, "[" + token + "] Phase2; bind timed out");
                  } else {
                      Log.d(TAG, "[" + token + "] Phase2; service connection error");
                  }
              }
          }
      }
  
      /**
       * Builds and returns an intent to launch the instant installer.
       */
      public static Intent buildEphemeralInstallerIntent(
              @NonNull Intent origIntent,
              @NonNull Intent sanitizedIntent,
              @Nullable Intent failureIntent,
              @NonNull String callingPackage,
              @Nullable Bundle verificationBundle,
              @NonNull String resolvedType,
              int userId,
              @Nullable ComponentName installFailureActivity,
              @Nullable String token,
              boolean needsPhaseTwo,
              List<AuxiliaryResolveInfo.AuxiliaryFilter> filters) {
          // Construct the intent that launches the instant installer
          int flags = origIntent.getFlags();
          final Intent intent = new Intent();
          intent.setFlags(flags
                  | Intent.FLAG_ACTIVITY_NO_HISTORY
                  | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
          if (token != null) {
              intent.putExtra(Intent.EXTRA_INSTANT_APP_TOKEN, token);
          }
          if (origIntent.getData() != null) {
              intent.putExtra(Intent.EXTRA_INSTANT_APP_HOSTNAME, origIntent.getData().getHost());
          }
          intent.putExtra(Intent.EXTRA_INSTANT_APP_ACTION, origIntent.getAction());
          intent.putExtra(Intent.EXTRA_INTENT, sanitizedIntent);
  
          if (needsPhaseTwo) {
              intent.setAction(Intent.ACTION_RESOLVE_INSTANT_APP_PACKAGE);
          } else {
              // We have all of the data we need; just start the installer without a second phase
              if (failureIntent != null || installFailureActivity != null) {
                  // Intent that is launched if the package couldn't be installed for any reason.
                  try {
                      final Intent onFailureIntent;
                      if (installFailureActivity != null) {
                          onFailureIntent = new Intent();
                          onFailureIntent.setComponent(installFailureActivity);
                          if (filters != null && filters.size() == 1) {
                              onFailureIntent.putExtra(Intent.EXTRA_SPLIT_NAME,
                                      filters.get(0).splitName);
                          }
                          onFailureIntent.putExtra(Intent.EXTRA_INTENT, origIntent);
                      } else {
                          onFailureIntent = failureIntent;
                      }
                      final IIntentSender failureIntentTarget = ActivityManager.getService()
                              .getIntentSender(
                                      ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage,
                                      null /*token*/, null /*resultWho*/, 1 /*requestCode*/,
                                      new Intent[] { onFailureIntent },
                                      new String[] { resolvedType },
                                      PendingIntent.FLAG_CANCEL_CURRENT
                                              | PendingIntent.FLAG_ONE_SHOT
                                              | PendingIntent.FLAG_IMMUTABLE,
                                      null /*bOptions*/, userId);
                      IntentSender failureSender = new IntentSender(failureIntentTarget);
                      // TODO(b/72700831): remove populating old extra
                      intent.putExtra(Intent.EXTRA_INSTANT_APP_FAILURE, failureSender);
                  } catch (RemoteException ignore) { /* ignore; same process */ }
              }
  
              // Intent that is launched if the package was installed successfully.
              final Intent successIntent = new Intent(origIntent);
              successIntent.setLaunchToken(token);
              try {
                  final IIntentSender successIntentTarget = ActivityManager.getService()
                          .getIntentSender(
                                  ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage,
                                  null /*token*/, null /*resultWho*/, 0 /*requestCode*/,
                                  new Intent[] { successIntent },
                                  new String[] { resolvedType },
                                  PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT
                                          | PendingIntent.FLAG_IMMUTABLE,
                                  null /*bOptions*/, userId);
                  IntentSender successSender = new IntentSender(successIntentTarget);
                  intent.putExtra(Intent.EXTRA_INSTANT_APP_SUCCESS, successSender);
              } catch (RemoteException ignore) { /* ignore; same process */ }
              if (verificationBundle != null) {
                  intent.putExtra(Intent.EXTRA_VERIFICATION_BUNDLE, verificationBundle);
              }
              intent.putExtra(Intent.EXTRA_CALLING_PACKAGE, callingPackage);
  
              if (filters != null) {
                  Bundle resolvableFilters[] = new Bundle[filters.size()];
                  for (int i = 0, max = filters.size(); i < max; i++) {
                      Bundle resolvableFilter = new Bundle();
                      AuxiliaryResolveInfo.AuxiliaryFilter filter = filters.get(i);
                      resolvableFilter.putBoolean(Intent.EXTRA_UNKNOWN_INSTANT_APP,
                              filter.resolveInfo != null
                                      && filter.resolveInfo.shouldLetInstallerDecide());
                      resolvableFilter.putString(Intent.EXTRA_PACKAGE_NAME, filter.packageName);
                      resolvableFilter.putString(Intent.EXTRA_SPLIT_NAME, filter.splitName);
                      resolvableFilter.putLong(Intent.EXTRA_LONG_VERSION_CODE, filter.versionCode);
                      resolvableFilter.putBundle(Intent.EXTRA_INSTANT_APP_EXTRAS, filter.extras);
                      resolvableFilters[i] = resolvableFilter;
                      if (i == 0) {
                          // for backwards compat, always set the first result on the intent and add
                          // the int version code
                          intent.putExtras(resolvableFilter);
                          intent.putExtra(Intent.EXTRA_VERSION_CODE, (int) filter.versionCode);
                      }
                  }
                  intent.putExtra(Intent.EXTRA_INSTANT_APP_BUNDLES, resolvableFilters);
              }
              intent.setAction(Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE);
          }
          return intent;
      }
  
      private static AuxiliaryResolveInfo filterInstantAppIntent(
              List<InstantAppResolveInfo> instantAppResolveInfoList,
              Intent origIntent, String resolvedType, int userId, String packageName,
              InstantAppDigest digest, String token) {
          final int[] shaPrefix = digest.getDigestPrefix();
          final byte[][] digestBytes = digest.getDigestBytes();
          boolean requiresSecondPhase = false;
          ArrayList<AuxiliaryResolveInfo.AuxiliaryFilter> filters = null;
          boolean requiresPrefixMatch = origIntent.isWebIntent() || (shaPrefix.length > 0
                          && (origIntent.getFlags() & Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) == 0);
          for (InstantAppResolveInfo instantAppResolveInfo : instantAppResolveInfoList) {
              if (requiresPrefixMatch && instantAppResolveInfo.shouldLetInstallerDecide()) {
                  Slog.d(TAG, "InstantAppResolveInfo with mShouldLetInstallerDecide=true when digest"
                          + " required; ignoring");
                  continue;
              }
              byte[] filterDigestBytes = instantAppResolveInfo.getDigestBytes();
              // Only include matching digests if we have a prefix and we're either dealing with a
              // prefixed request or the resolveInfo specifies digest details.
              if (shaPrefix.length > 0 && (requiresPrefixMatch || filterDigestBytes.length > 0)) {
                  boolean matchFound = false;
                  // Go in reverse order so we match the narrowest scope first.
                  for (int i = shaPrefix.length - 1; i >= 0; --i) {
                      if (Arrays.equals(digestBytes[i], filterDigestBytes)) {
                          matchFound = true;
                          break;
                      }
                  }
                  if (!matchFound) {
                      continue;
                  }
              }
              // We matched a resolve info; resolve the filters to see if anything matches completely.
              List<AuxiliaryResolveInfo.AuxiliaryFilter> matchFilters = computeResolveFilters(
                      origIntent, resolvedType, userId, packageName, token, instantAppResolveInfo);
              if (matchFilters != null) {
                  if (matchFilters.isEmpty()) {
                      requiresSecondPhase = true;
                  }
                  if (filters == null) {
                      filters = new ArrayList<>(matchFilters);
                  } else {
                      filters.addAll(matchFilters);
                  }
              }
          }
          if (filters != null && !filters.isEmpty()) {
              return new AuxiliaryResolveInfo(token, requiresSecondPhase,
                      createFailureIntent(origIntent, token), filters);
          }
          // Hash or filter mis-match; no instant apps for this domain.
          return null;
      }
  
      /**
       * Creates a failure intent for the installer to send in the case that the instant app cannot be
       * launched for any reason.
       */
      private static Intent createFailureIntent(Intent origIntent, String token) {
          final Intent failureIntent = new Intent(origIntent);
          failureIntent.setFlags(failureIntent.getFlags() | Intent.FLAG_IGNORE_EPHEMERAL);
          failureIntent.setFlags(failureIntent.getFlags() & ~Intent.FLAG_ACTIVITY_MATCH_EXTERNAL);
          failureIntent.setLaunchToken(token);
          return failureIntent;
      }
  
      /**
       * Returns one of three states: <p/>
       * <ul>
       *     <li>{@code null} if there are no matches will not be; resolution is unnecessary.</li>
       *     <li>An empty list signifying that a 2nd phase of resolution is required.</li>
       *     <li>A populated list meaning that matches were found and should be sent directly to the
       *     installer</li>
       * </ul>
       *
       */
      private static List<AuxiliaryResolveInfo.AuxiliaryFilter> computeResolveFilters(
              Intent origIntent, String resolvedType, int userId, String packageName, String token,
              InstantAppResolveInfo instantAppInfo) {
          if (instantAppInfo.shouldLetInstallerDecide()) {
              return Collections.singletonList(
                      new AuxiliaryResolveInfo.AuxiliaryFilter(
                              instantAppInfo, null /* splitName */,
                              instantAppInfo.getExtras()));
          }
          if (packageName != null
                  && !packageName.equals(instantAppInfo.getPackageName())) {
              return null;
          }
          final List<InstantAppIntentFilter> instantAppFilters =
                  instantAppInfo.getIntentFilters();
          if (instantAppFilters == null || instantAppFilters.isEmpty()) {
              // No filters on web intent; no matches, 2nd phase unnecessary.
              if (origIntent.isWebIntent()) {
                  return null;
              }
              // No filters; we need to start phase two
              if (DEBUG_INSTANT) {
                  Log.d(TAG, "No app filters; go to phase 2");
              }
              return Collections.emptyList();
          }
          final ComponentResolver.InstantAppIntentResolver instantAppResolver =
                  new ComponentResolver.InstantAppIntentResolver();
          for (int j = instantAppFilters.size() - 1; j >= 0; --j) {
              final InstantAppIntentFilter instantAppFilter = instantAppFilters.get(j);
              final List<IntentFilter> splitFilters = instantAppFilter.getFilters();
              if (splitFilters == null || splitFilters.isEmpty()) {
                  continue;
              }
              for (int k = splitFilters.size() - 1; k >= 0; --k) {
                  IntentFilter filter = splitFilters.get(k);
                  Iterator<IntentFilter.AuthorityEntry> authorities =
                          filter.authoritiesIterator();
                  // ignore http/s-only filters.
                  if ((authorities == null || !authorities.hasNext())
                          && (filter.hasDataScheme("http") || filter.hasDataScheme("https"))
                          && filter.hasAction(Intent.ACTION_VIEW)
                          && filter.hasCategory(Intent.CATEGORY_BROWSABLE)) {
                      continue;
                  }
                  instantAppResolver.addFilter(
                          new AuxiliaryResolveInfo.AuxiliaryFilter(
                                  filter,
                                  instantAppInfo,
                                  instantAppFilter.getSplitName(),
                                  instantAppInfo.getExtras()
                          ));
              }
          }
          List<AuxiliaryResolveInfo.AuxiliaryFilter> matchedResolveInfoList =
                  instantAppResolver.queryIntent(
                          origIntent, resolvedType, false /*defaultOnly*/, userId);
          if (!matchedResolveInfoList.isEmpty()) {
              if (DEBUG_INSTANT) {
                  Log.d(TAG, "[" + token + "] Found match(es); " + matchedResolveInfoList);
              }
              return matchedResolveInfoList;
          } else if (DEBUG_INSTANT) {
              Log.d(TAG, "[" + token + "] No matches found"
                      + " package: " + instantAppInfo.getPackageName()
                      + ", versionCode: " + instantAppInfo.getVersionCode());
          }
          return null;
      }
  
      private static void logMetrics(int action, long startTime, String token,
              @ResolutionStatus int status) {
          final LogMaker logMaker = new LogMaker(action)
                  .setType(MetricsProto.MetricsEvent.TYPE_ACTION)
                  .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_DELAY_MS,
                          new Long(System.currentTimeMillis() - startTime))
                  .addTaggedData(FIELD_INSTANT_APP_LAUNCH_TOKEN, token)
                  .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_STATUS, new Integer(status));
          getLogger().write(logMaker);
      }
  }
  