| /* |
| * 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 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.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.os.Binder; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.RemoteException; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.util.Slog; |
| |
| import com.android.internal.logging.MetricsLogger; |
| import com.android.internal.logging.nano.MetricsProto; |
| import com.android.server.pm.EphemeralResolverConnection.PhaseTwoCallback; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.UUID; |
| |
| /** @hide */ |
| public abstract class InstantAppResolver { |
| private static final boolean DEBUG_EPHEMERAL = Build.IS_DEBUGGABLE; |
| private static final String TAG = "PackageManager"; |
| |
| private static int RESOLUTION_SUCCESS = 0; |
| private static int RESOLUTION_FAILURE = 1; |
| |
| private static MetricsLogger sMetricsLogger; |
| private static MetricsLogger getLogger() { |
| if (sMetricsLogger == null) { |
| sMetricsLogger = new MetricsLogger(); |
| } |
| return sMetricsLogger; |
| } |
| |
| public static AuxiliaryResolveInfo doInstantAppResolutionPhaseOne(Context context, |
| EphemeralResolverConnection connection, InstantAppRequest requestObj) { |
| final long startTime = System.currentTimeMillis(); |
| final String token = UUID.randomUUID().toString(); |
| if (DEBUG_EPHEMERAL) { |
| Log.d(TAG, "[" + token + "] Resolving phase 1"); |
| } |
| final Intent intent = requestObj.origIntent; |
| final InstantAppDigest digest = |
| new InstantAppDigest(intent.getData().getHost(), 5 /*maxDigests*/); |
| final int[] shaPrefix = digest.getDigestPrefix(); |
| final List<InstantAppResolveInfo> instantAppResolveInfoList = |
| connection.getInstantAppResolveInfoList(shaPrefix, token); |
| |
| if (instantAppResolveInfoList == null || instantAppResolveInfoList.size() == 0) { |
| // No hash prefix match; there are no instant apps for this domain. |
| if (DEBUG_EPHEMERAL) { |
| Log.d(TAG, "[" + token + "] No results returned"); |
| } |
| return null; |
| } |
| final AuxiliaryResolveInfo resolveInfo = InstantAppResolver.filterInstantAppIntent( |
| instantAppResolveInfoList, intent, requestObj.resolvedType, requestObj.userId, |
| intent.getPackage(), digest, token); |
| logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_ONE, startTime, token, |
| RESOLUTION_SUCCESS); |
| if (DEBUG_EPHEMERAL && resolveInfo == null) { |
| Log.d(TAG, "[" + token + "] No results matched"); |
| } |
| return resolveInfo; |
| } |
| |
| public static void doInstantAppResolutionPhaseTwo(Context context, |
| EphemeralResolverConnection connection, InstantAppRequest requestObj, |
| ActivityInfo instantAppInstaller, Handler callbackHandler) { |
| final long startTime = System.currentTimeMillis(); |
| final String token = requestObj.responseObj.token; |
| if (DEBUG_EPHEMERAL) { |
| Log.d(TAG, "[" + token + "] Resolving phase 2"); |
| } |
| final Intent intent = requestObj.origIntent; |
| final String hostName = intent.getData().getHost(); |
| final InstantAppDigest digest = new InstantAppDigest(hostName, 5 /*maxDigests*/); |
| final int[] shaPrefix = digest.getDigestPrefix(); |
| |
| final PhaseTwoCallback callback = new PhaseTwoCallback() { |
| @Override |
| void onPhaseTwoResolved(List<InstantAppResolveInfo> instantAppResolveInfoList, |
| long startTime) { |
| final String packageName; |
| final String splitName; |
| final int versionCode; |
| if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) { |
| final AuxiliaryResolveInfo instantAppIntentInfo = |
| InstantAppResolver.filterInstantAppIntent( |
| instantAppResolveInfoList, intent, null /*resolvedType*/, |
| 0 /*userId*/, intent.getPackage(), digest, token); |
| if (instantAppIntentInfo != null |
| && instantAppIntentInfo.resolveInfo != null) { |
| packageName = instantAppIntentInfo.resolveInfo.getPackageName(); |
| splitName = instantAppIntentInfo.splitName; |
| versionCode = instantAppIntentInfo.resolveInfo.getVersionCode(); |
| } else { |
| packageName = null; |
| splitName = null; |
| versionCode = -1; |
| } |
| } else { |
| packageName = null; |
| splitName = null; |
| versionCode = -1; |
| } |
| final Intent installerIntent = buildEphemeralInstallerIntent( |
| requestObj.origIntent, |
| requestObj.callingPackage, |
| requestObj.verificationBundle, |
| requestObj.resolvedType, |
| requestObj.userId, |
| packageName, |
| splitName, |
| versionCode, |
| token, |
| false /*needsPhaseTwo*/); |
| installerIntent.setComponent(new ComponentName( |
| instantAppInstaller.packageName, instantAppInstaller.name)); |
| |
| logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO, startTime, token, |
| packageName != null ? RESOLUTION_SUCCESS : RESOLUTION_FAILURE); |
| |
| context.startActivity(installerIntent); |
| } |
| }; |
| connection.getInstantAppIntentFilterList( |
| shaPrefix, token, hostName, callback, callbackHandler, startTime); |
| } |
| |
| /** |
| * Builds and returns an intent to launch the instant installer. |
| */ |
| public static Intent buildEphemeralInstallerIntent(@NonNull Intent origIntent, |
| @NonNull String callingPackage, |
| @Nullable Bundle verificationBundle, |
| @NonNull String resolvedType, |
| int userId, |
| @NonNull String instantAppPackageName, |
| @Nullable String instantAppSplitName, |
| int versionCode, |
| @Nullable String token, |
| boolean needsPhaseTwo) { |
| // Construct the intent that launches the instant installer |
| int flags = origIntent.getFlags(); |
| final Intent intent = new Intent(); |
| intent.setFlags(flags |
| | Intent.FLAG_ACTIVITY_NEW_TASK |
| | Intent.FLAG_ACTIVITY_CLEAR_TASK |
| | Intent.FLAG_ACTIVITY_NO_HISTORY |
| | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); |
| if (token != null) { |
| intent.putExtra(Intent.EXTRA_EPHEMERAL_TOKEN, token); |
| } |
| if (origIntent.getData() != null) { |
| intent.putExtra(Intent.EXTRA_EPHEMERAL_HOSTNAME, origIntent.getData().getHost()); |
| } |
| |
| // We have all of the data we need; just start the installer without a second phase |
| if (!needsPhaseTwo) { |
| // Intent that is launched if the package couldn't be installed for any reason. |
| final Intent failureIntent = new Intent(origIntent); |
| failureIntent.setFlags(failureIntent.getFlags() | Intent.FLAG_IGNORE_EPHEMERAL); |
| failureIntent.setLaunchToken(token); |
| try { |
| final IIntentSender failureIntentTarget = ActivityManager.getService() |
| .getIntentSender( |
| ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage, |
| null /*token*/, null /*resultWho*/, 1 /*requestCode*/, |
| new Intent[] { failureIntent }, |
| new String[] { resolvedType }, |
| PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT |
| | PendingIntent.FLAG_IMMUTABLE, |
| null /*bOptions*/, userId); |
| intent.putExtra(Intent.EXTRA_EPHEMERAL_FAILURE, |
| new IntentSender(failureIntentTarget)); |
| } 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); |
| intent.putExtra(Intent.EXTRA_EPHEMERAL_SUCCESS, |
| new IntentSender(successIntentTarget)); |
| } catch (RemoteException ignore) { /* ignore; same process */ } |
| |
| intent.putExtra(Intent.EXTRA_PACKAGE_NAME, instantAppPackageName); |
| intent.putExtra(Intent.EXTRA_SPLIT_NAME, instantAppSplitName); |
| intent.putExtra(Intent.EXTRA_VERSION_CODE, versionCode); |
| intent.putExtra(Intent.EXTRA_CALLING_PACKAGE, callingPackage); |
| if (verificationBundle != null) { |
| intent.putExtra(Intent.EXTRA_VERIFICATION_BUNDLE, verificationBundle); |
| } |
| } |
| |
| return intent; |
| } |
| |
| private static AuxiliaryResolveInfo filterInstantAppIntent( |
| List<InstantAppResolveInfo> instantAppResolveInfoList, |
| Intent intent, String resolvedType, int userId, String packageName, |
| InstantAppDigest digest, String token) { |
| final int[] shaPrefix = digest.getDigestPrefix(); |
| final byte[][] digestBytes = digest.getDigestBytes(); |
| // Go in reverse order so we match the narrowest scope first. |
| for (int i = shaPrefix.length - 1; i >= 0 ; --i) { |
| for (InstantAppResolveInfo instantAppInfo : instantAppResolveInfoList) { |
| if (!Arrays.equals(digestBytes[i], instantAppInfo.getDigestBytes())) { |
| continue; |
| } |
| if (packageName != null |
| && !packageName.equals(instantAppInfo.getPackageName())) { |
| continue; |
| } |
| final List<InstantAppIntentFilter> instantAppFilters = |
| instantAppInfo.getIntentFilters(); |
| // No filters; we need to start phase two |
| if (instantAppFilters == null || instantAppFilters.isEmpty()) { |
| if (DEBUG_EPHEMERAL) { |
| Log.d(TAG, "No app filters; go to phase 2"); |
| } |
| return new AuxiliaryResolveInfo(instantAppInfo, |
| new IntentFilter(Intent.ACTION_VIEW) /*intentFilter*/, |
| null /*splitName*/, token, true /*needsPhase2*/); |
| } |
| // We have a domain match; resolve the filters to see if anything matches. |
| final PackageManagerService.EphemeralIntentResolver instantAppResolver = |
| new PackageManagerService.EphemeralIntentResolver(); |
| 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) { |
| final AuxiliaryResolveInfo intentInfo = |
| new AuxiliaryResolveInfo(instantAppInfo, |
| splitFilters.get(k), instantAppFilter.getSplitName(), |
| token, false /*needsPhase2*/); |
| instantAppResolver.addFilter(intentInfo); |
| } |
| } |
| List<AuxiliaryResolveInfo> matchedResolveInfoList = instantAppResolver.queryIntent( |
| intent, resolvedType, false /*defaultOnly*/, userId); |
| if (!matchedResolveInfoList.isEmpty()) { |
| if (DEBUG_EPHEMERAL) { |
| final AuxiliaryResolveInfo info = matchedResolveInfoList.get(0); |
| Log.d(TAG, "[" + token + "] Found match;" |
| + " package: " + info.packageName |
| + ", split: " + info.splitName |
| + ", versionCode: " + info.versionCode); |
| } |
| return matchedResolveInfoList.get(0); |
| } else if (DEBUG_EPHEMERAL) { |
| Log.d(TAG, "[" + token + "] No matches found" |
| + " package: " + instantAppInfo.getPackageName() |
| + ", versionCode: " + instantAppInfo.getVersionCode()); |
| } |
| } |
| } |
| // Hash or filter mis-match; no instant apps for this domain. |
| return null; |
| } |
| |
| private static void logMetrics(int action, long startTime, String token, 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); |
| } |
| } |