/* * 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.InstantAppRequest; import android.content.pm.AuxiliaryResolveInfo; import android.content.pm.InstantAppIntentFilter; 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(), 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(), 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) { // TODO(b/72700831): remove populating old extra intent.putExtra(Intent.EXTRA_EPHEMERAL_TOKEN, token); intent.putExtra(Intent.EXTRA_INSTANT_APP_TOKEN, token); } if (origIntent.getData() != null) { // TODO(b/72700831): remove populating old extra intent.putExtra(Intent.EXTRA_EPHEMERAL_HOSTNAME, origIntent.getData().getHost()); 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_EPHEMERAL_FAILURE, failureSender); 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); // TODO(b/72700831): remove populating old extra intent.putExtra(Intent.EXTRA_EPHEMERAL_SUCCESS, successSender); 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 PackageManagerService.InstantAppIntentResolver instantAppResolver = new PackageManagerService.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); } }