InstantAppResolver.java revision 4db6bc16d0cc3c3b60a6b5305118f266ab2028a8
1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.server.pm; 18 19import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_INSTANT_APP_RESOLUTION_PHASE_ONE; 20import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO; 21import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_INSTANT_APP_LAUNCH_TOKEN; 22import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_INSTANT_APP_RESOLUTION_DELAY_MS; 23import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_INSTANT_APP_RESOLUTION_STATUS; 24 25import android.annotation.IntDef; 26import android.annotation.NonNull; 27import android.annotation.Nullable; 28import android.app.ActivityManager; 29import android.app.PendingIntent; 30import android.content.ComponentName; 31import android.content.Context; 32import android.content.IIntentSender; 33import android.content.Intent; 34import android.content.IntentFilter; 35import android.content.IntentSender; 36import android.content.pm.ActivityInfo; 37import android.content.pm.InstantAppRequest; 38import android.content.pm.AuxiliaryResolveInfo; 39import android.content.pm.InstantAppIntentFilter; 40import android.content.pm.InstantAppResolveInfo; 41import android.content.pm.InstantAppResolveInfo.InstantAppDigest; 42import android.metrics.LogMaker; 43import android.net.Uri; 44import android.os.Build; 45import android.os.Bundle; 46import android.os.Handler; 47import android.os.RemoteException; 48import android.text.TextUtils; 49import android.util.Log; 50import android.util.Slog; 51 52import com.android.internal.logging.MetricsLogger; 53import com.android.internal.logging.nano.MetricsProto; 54import com.android.server.pm.InstantAppResolverConnection.ConnectionException; 55import com.android.server.pm.InstantAppResolverConnection.PhaseTwoCallback; 56 57import java.lang.annotation.Retention; 58import java.lang.annotation.RetentionPolicy; 59import java.util.ArrayList; 60import java.util.Arrays; 61import java.util.Collections; 62import java.util.Iterator; 63import java.util.List; 64import java.util.Set; 65import java.util.UUID; 66 67/** @hide */ 68public abstract class InstantAppResolver { 69 private static final boolean DEBUG_INSTANT = Build.IS_DEBUGGABLE; 70 private static final String TAG = "PackageManager"; 71 72 private static final int RESOLUTION_SUCCESS = 0; 73 private static final int RESOLUTION_FAILURE = 1; 74 /** Binding to the external service timed out */ 75 private static final int RESOLUTION_BIND_TIMEOUT = 2; 76 /** The call to retrieve an instant application response timed out */ 77 private static final int RESOLUTION_CALL_TIMEOUT = 3; 78 79 @IntDef(flag = true, prefix = { "RESOLUTION_" }, value = { 80 RESOLUTION_SUCCESS, 81 RESOLUTION_FAILURE, 82 RESOLUTION_BIND_TIMEOUT, 83 RESOLUTION_CALL_TIMEOUT, 84 }) 85 @Retention(RetentionPolicy.SOURCE) 86 public @interface ResolutionStatus {} 87 88 private static MetricsLogger sMetricsLogger; 89 90 private static MetricsLogger getLogger() { 91 if (sMetricsLogger == null) { 92 sMetricsLogger = new MetricsLogger(); 93 } 94 return sMetricsLogger; 95 } 96 97 /** 98 * Returns an intent with potential PII removed from the original intent. Fields removed 99 * include extras and the host + path of the data, if defined. 100 */ 101 public static Intent sanitizeIntent(Intent origIntent) { 102 final Intent sanitizedIntent; 103 sanitizedIntent = new Intent(origIntent.getAction()); 104 Set<String> categories = origIntent.getCategories(); 105 if (categories != null) { 106 for (String category : categories) { 107 sanitizedIntent.addCategory(category); 108 } 109 } 110 Uri sanitizedUri = origIntent.getData() == null 111 ? null 112 : Uri.fromParts(origIntent.getScheme(), "", ""); 113 sanitizedIntent.setDataAndType(sanitizedUri, origIntent.getType()); 114 sanitizedIntent.addFlags(origIntent.getFlags()); 115 sanitizedIntent.setPackage(origIntent.getPackage()); 116 return sanitizedIntent; 117 } 118 119 public static AuxiliaryResolveInfo doInstantAppResolutionPhaseOne( 120 InstantAppResolverConnection connection, InstantAppRequest requestObj) { 121 final long startTime = System.currentTimeMillis(); 122 final String token = UUID.randomUUID().toString(); 123 if (DEBUG_INSTANT) { 124 Log.d(TAG, "[" + token + "] Phase1; resolving"); 125 } 126 final Intent origIntent = requestObj.origIntent; 127 final Intent sanitizedIntent = sanitizeIntent(origIntent); 128 129 AuxiliaryResolveInfo resolveInfo = null; 130 @ResolutionStatus int resolutionStatus = RESOLUTION_SUCCESS; 131 try { 132 final List<InstantAppResolveInfo> instantAppResolveInfoList = 133 connection.getInstantAppResolveInfoList(sanitizedIntent, 134 requestObj.digest.getDigestPrefixSecure(), token); 135 if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) { 136 resolveInfo = InstantAppResolver.filterInstantAppIntent( 137 instantAppResolveInfoList, origIntent, requestObj.resolvedType, 138 requestObj.userId, origIntent.getPackage(), requestObj.digest, token); 139 } 140 } catch (ConnectionException e) { 141 if (e.failure == ConnectionException.FAILURE_BIND) { 142 resolutionStatus = RESOLUTION_BIND_TIMEOUT; 143 } else if (e.failure == ConnectionException.FAILURE_CALL) { 144 resolutionStatus = RESOLUTION_CALL_TIMEOUT; 145 } else { 146 resolutionStatus = RESOLUTION_FAILURE; 147 } 148 } 149 // Only log successful instant application resolution 150 if (requestObj.resolveForStart && resolutionStatus == RESOLUTION_SUCCESS) { 151 logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_ONE, startTime, token, 152 resolutionStatus); 153 } 154 if (DEBUG_INSTANT && resolveInfo == null) { 155 if (resolutionStatus == RESOLUTION_BIND_TIMEOUT) { 156 Log.d(TAG, "[" + token + "] Phase1; bind timed out"); 157 } else if (resolutionStatus == RESOLUTION_CALL_TIMEOUT) { 158 Log.d(TAG, "[" + token + "] Phase1; call timed out"); 159 } else if (resolutionStatus != RESOLUTION_SUCCESS) { 160 Log.d(TAG, "[" + token + "] Phase1; service connection error"); 161 } else { 162 Log.d(TAG, "[" + token + "] Phase1; No results matched"); 163 } 164 } 165 return resolveInfo; 166 } 167 168 public static void doInstantAppResolutionPhaseTwo(Context context, 169 InstantAppResolverConnection connection, InstantAppRequest requestObj, 170 ActivityInfo instantAppInstaller, Handler callbackHandler) { 171 final long startTime = System.currentTimeMillis(); 172 final String token = requestObj.responseObj.token; 173 if (DEBUG_INSTANT) { 174 Log.d(TAG, "[" + token + "] Phase2; resolving"); 175 } 176 final Intent origIntent = requestObj.origIntent; 177 final Intent sanitizedIntent = sanitizeIntent(origIntent); 178 179 final PhaseTwoCallback callback = new PhaseTwoCallback() { 180 @Override 181 void onPhaseTwoResolved(List<InstantAppResolveInfo> instantAppResolveInfoList, 182 long startTime) { 183 final Intent failureIntent; 184 if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) { 185 final AuxiliaryResolveInfo instantAppIntentInfo = 186 InstantAppResolver.filterInstantAppIntent( 187 instantAppResolveInfoList, origIntent, null /*resolvedType*/, 188 0 /*userId*/, origIntent.getPackage(), requestObj.digest, 189 token); 190 if (instantAppIntentInfo != null) { 191 failureIntent = instantAppIntentInfo.failureIntent; 192 } else { 193 failureIntent = null; 194 } 195 } else { 196 failureIntent = null; 197 } 198 final Intent installerIntent = buildEphemeralInstallerIntent( 199 requestObj.origIntent, 200 sanitizedIntent, 201 failureIntent, 202 requestObj.callingPackage, 203 requestObj.verificationBundle, 204 requestObj.resolvedType, 205 requestObj.userId, 206 requestObj.responseObj.installFailureActivity, 207 token, 208 false /*needsPhaseTwo*/, 209 requestObj.responseObj.filters); 210 installerIntent.setComponent(new ComponentName( 211 instantAppInstaller.packageName, instantAppInstaller.name)); 212 213 logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO, startTime, token, 214 requestObj.responseObj.filters != null ? RESOLUTION_SUCCESS : RESOLUTION_FAILURE); 215 216 context.startActivity(installerIntent); 217 } 218 }; 219 try { 220 connection.getInstantAppIntentFilterList(sanitizedIntent, 221 requestObj.digest.getDigestPrefixSecure(), token, callback, callbackHandler, 222 startTime); 223 } catch (ConnectionException e) { 224 @ResolutionStatus int resolutionStatus = RESOLUTION_FAILURE; 225 if (e.failure == ConnectionException.FAILURE_BIND) { 226 resolutionStatus = RESOLUTION_BIND_TIMEOUT; 227 } 228 logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO, startTime, token, 229 resolutionStatus); 230 if (DEBUG_INSTANT) { 231 if (resolutionStatus == RESOLUTION_BIND_TIMEOUT) { 232 Log.d(TAG, "[" + token + "] Phase2; bind timed out"); 233 } else { 234 Log.d(TAG, "[" + token + "] Phase2; service connection error"); 235 } 236 } 237 } 238 } 239 240 /** 241 * Builds and returns an intent to launch the instant installer. 242 */ 243 public static Intent buildEphemeralInstallerIntent( 244 @NonNull Intent origIntent, 245 @NonNull Intent sanitizedIntent, 246 @Nullable Intent failureIntent, 247 @NonNull String callingPackage, 248 @Nullable Bundle verificationBundle, 249 @NonNull String resolvedType, 250 int userId, 251 @Nullable ComponentName installFailureActivity, 252 @Nullable String token, 253 boolean needsPhaseTwo, 254 List<AuxiliaryResolveInfo.AuxiliaryFilter> filters) { 255 // Construct the intent that launches the instant installer 256 int flags = origIntent.getFlags(); 257 final Intent intent = new Intent(); 258 intent.setFlags(flags 259 | Intent.FLAG_ACTIVITY_NEW_TASK 260 | Intent.FLAG_ACTIVITY_CLEAR_TASK 261 | Intent.FLAG_ACTIVITY_NO_HISTORY 262 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 263 if (token != null) { 264 // TODO(b/72700831): remove populating old extra 265 intent.putExtra(Intent.EXTRA_EPHEMERAL_TOKEN, token); 266 intent.putExtra(Intent.EXTRA_INSTANT_APP_TOKEN, token); 267 } 268 if (origIntent.getData() != null) { 269 // TODO(b/72700831): remove populating old extra 270 intent.putExtra(Intent.EXTRA_EPHEMERAL_HOSTNAME, origIntent.getData().getHost()); 271 intent.putExtra(Intent.EXTRA_INSTANT_APP_HOSTNAME, origIntent.getData().getHost()); 272 } 273 intent.putExtra(Intent.EXTRA_INSTANT_APP_ACTION, origIntent.getAction()); 274 intent.putExtra(Intent.EXTRA_INTENT, sanitizedIntent); 275 276 if (needsPhaseTwo) { 277 intent.setAction(Intent.ACTION_RESOLVE_INSTANT_APP_PACKAGE); 278 } else { 279 // We have all of the data we need; just start the installer without a second phase 280 if (failureIntent != null || installFailureActivity != null) { 281 // Intent that is launched if the package couldn't be installed for any reason. 282 try { 283 final Intent onFailureIntent; 284 if (installFailureActivity != null) { 285 onFailureIntent = new Intent(); 286 onFailureIntent.setComponent(installFailureActivity); 287 if (filters != null && filters.size() == 1) { 288 onFailureIntent.putExtra(Intent.EXTRA_SPLIT_NAME, 289 filters.get(0).splitName); 290 } 291 onFailureIntent.putExtra(Intent.EXTRA_INTENT, origIntent); 292 } else { 293 onFailureIntent = failureIntent; 294 } 295 final IIntentSender failureIntentTarget = ActivityManager.getService() 296 .getIntentSender( 297 ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage, 298 null /*token*/, null /*resultWho*/, 1 /*requestCode*/, 299 new Intent[] { onFailureIntent }, 300 new String[] { resolvedType }, 301 PendingIntent.FLAG_CANCEL_CURRENT 302 | PendingIntent.FLAG_ONE_SHOT 303 | PendingIntent.FLAG_IMMUTABLE, 304 null /*bOptions*/, userId); 305 IntentSender failureSender = new IntentSender(failureIntentTarget); 306 // TODO(b/72700831): remove populating old extra 307 intent.putExtra(Intent.EXTRA_EPHEMERAL_FAILURE, failureSender); 308 intent.putExtra(Intent.EXTRA_INSTANT_APP_FAILURE, failureSender); 309 } catch (RemoteException ignore) { /* ignore; same process */ } 310 } 311 312 // Intent that is launched if the package was installed successfully. 313 final Intent successIntent = new Intent(origIntent); 314 successIntent.setLaunchToken(token); 315 try { 316 final IIntentSender successIntentTarget = ActivityManager.getService() 317 .getIntentSender( 318 ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage, 319 null /*token*/, null /*resultWho*/, 0 /*requestCode*/, 320 new Intent[] { successIntent }, 321 new String[] { resolvedType }, 322 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT 323 | PendingIntent.FLAG_IMMUTABLE, 324 null /*bOptions*/, userId); 325 IntentSender successSender = new IntentSender(successIntentTarget); 326 // TODO(b/72700831): remove populating old extra 327 intent.putExtra(Intent.EXTRA_EPHEMERAL_SUCCESS, successSender); 328 intent.putExtra(Intent.EXTRA_INSTANT_APP_SUCCESS, successSender); 329 } catch (RemoteException ignore) { /* ignore; same process */ } 330 if (verificationBundle != null) { 331 intent.putExtra(Intent.EXTRA_VERIFICATION_BUNDLE, verificationBundle); 332 } 333 intent.putExtra(Intent.EXTRA_CALLING_PACKAGE, callingPackage); 334 335 if (filters != null) { 336 Bundle resolvableFilters[] = new Bundle[filters.size()]; 337 for (int i = 0, max = filters.size(); i < max; i++) { 338 Bundle resolvableFilter = new Bundle(); 339 AuxiliaryResolveInfo.AuxiliaryFilter filter = filters.get(i); 340 resolvableFilter.putBoolean(Intent.EXTRA_UNKNOWN_INSTANT_APP, 341 filter.resolveInfo != null 342 && filter.resolveInfo.shouldLetInstallerDecide()); 343 resolvableFilter.putString(Intent.EXTRA_PACKAGE_NAME, filter.packageName); 344 resolvableFilter.putString(Intent.EXTRA_SPLIT_NAME, filter.splitName); 345 resolvableFilter.putLong(Intent.EXTRA_LONG_VERSION_CODE, filter.versionCode); 346 resolvableFilter.putBundle(Intent.EXTRA_INSTANT_APP_EXTRAS, filter.extras); 347 resolvableFilters[i] = resolvableFilter; 348 if (i == 0) { 349 // for backwards compat, always set the first result on the intent and add 350 // the int version code 351 intent.putExtras(resolvableFilter); 352 intent.putExtra(Intent.EXTRA_VERSION_CODE, (int) filter.versionCode); 353 } 354 } 355 intent.putExtra(Intent.EXTRA_INSTANT_APP_BUNDLES, resolvableFilters); 356 } 357 intent.setAction(Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE); 358 } 359 return intent; 360 } 361 362 private static AuxiliaryResolveInfo filterInstantAppIntent( 363 List<InstantAppResolveInfo> instantAppResolveInfoList, 364 Intent origIntent, String resolvedType, int userId, String packageName, 365 InstantAppDigest digest, String token) { 366 final int[] shaPrefix = digest.getDigestPrefix(); 367 final byte[][] digestBytes = digest.getDigestBytes(); 368 final Intent failureIntent = new Intent(origIntent); 369 boolean requiresSecondPhase = false; 370 failureIntent.setFlags(failureIntent.getFlags() | Intent.FLAG_IGNORE_EPHEMERAL); 371 failureIntent.setLaunchToken(token); 372 ArrayList<AuxiliaryResolveInfo.AuxiliaryFilter> filters = null; 373 boolean isWebIntent = origIntent.isBrowsableWebIntent(); 374 for (InstantAppResolveInfo instantAppResolveInfo : instantAppResolveInfoList) { 375 if (shaPrefix.length > 0 && instantAppResolveInfo.shouldLetInstallerDecide()) { 376 Slog.e(TAG, "InstantAppResolveInfo with mShouldLetInstallerDecide=true when digest" 377 + " provided; ignoring"); 378 continue; 379 } 380 byte[] filterDigestBytes = instantAppResolveInfo.getDigestBytes(); 381 // Only include matching digests if we have a prefix and we're either dealing with a 382 // web intent or the resolveInfo specifies digest details. 383 if (shaPrefix.length > 0 && (isWebIntent || filterDigestBytes.length > 0)) { 384 boolean matchFound = false; 385 // Go in reverse order so we match the narrowest scope first. 386 for (int i = shaPrefix.length - 1; i >= 0; --i) { 387 if (Arrays.equals(digestBytes[i], filterDigestBytes)) { 388 matchFound = true; 389 break; 390 } 391 } 392 if (!matchFound) { 393 continue; 394 } 395 } 396 // We matched a resolve info; resolve the filters to see if anything matches completely. 397 List<AuxiliaryResolveInfo.AuxiliaryFilter> matchFilters = computeResolveFilters( 398 origIntent, resolvedType, userId, packageName, token, instantAppResolveInfo); 399 if (matchFilters != null) { 400 if (matchFilters.isEmpty()) { 401 requiresSecondPhase = true; 402 } 403 if (filters == null) { 404 filters = new ArrayList<>(matchFilters); 405 } else { 406 filters.addAll(matchFilters); 407 } 408 } 409 } 410 if (filters != null && !filters.isEmpty()) { 411 return new AuxiliaryResolveInfo(token, requiresSecondPhase, failureIntent, filters); 412 } 413 // Hash or filter mis-match; no instant apps for this domain. 414 return null; 415 } 416 417 /** 418 * Returns one of three states: <p/> 419 * <ul> 420 * <li>{@code null} if there are no matches will not be; resolution is unnecessary.</li> 421 * <li>An empty list signifying that a 2nd phase of resolution is required.</li> 422 * <li>A populated list meaning that matches were found and should be sent directly to the 423 * installer</li> 424 * </ul> 425 * 426 */ 427 private static List<AuxiliaryResolveInfo.AuxiliaryFilter> computeResolveFilters( 428 Intent origIntent, String resolvedType, int userId, String packageName, String token, 429 InstantAppResolveInfo instantAppInfo) { 430 if (instantAppInfo.shouldLetInstallerDecide()) { 431 return Collections.singletonList( 432 new AuxiliaryResolveInfo.AuxiliaryFilter( 433 instantAppInfo, null /* splitName */, 434 instantAppInfo.getExtras())); 435 } 436 if (packageName != null 437 && !packageName.equals(instantAppInfo.getPackageName())) { 438 return null; 439 } 440 final List<InstantAppIntentFilter> instantAppFilters = 441 instantAppInfo.getIntentFilters(); 442 if (instantAppFilters == null || instantAppFilters.isEmpty()) { 443 // No filters on web intent; no matches, 2nd phase unnecessary. 444 if (origIntent.isBrowsableWebIntent()) { 445 return null; 446 } 447 // No filters; we need to start phase two 448 if (DEBUG_INSTANT) { 449 Log.d(TAG, "No app filters; go to phase 2"); 450 } 451 return Collections.emptyList(); 452 } 453 final PackageManagerService.InstantAppIntentResolver instantAppResolver = 454 new PackageManagerService.InstantAppIntentResolver(); 455 for (int j = instantAppFilters.size() - 1; j >= 0; --j) { 456 final InstantAppIntentFilter instantAppFilter = instantAppFilters.get(j); 457 final List<IntentFilter> splitFilters = instantAppFilter.getFilters(); 458 if (splitFilters == null || splitFilters.isEmpty()) { 459 continue; 460 } 461 for (int k = splitFilters.size() - 1; k >= 0; --k) { 462 IntentFilter filter = splitFilters.get(k); 463 Iterator<IntentFilter.AuthorityEntry> authorities = 464 filter.authoritiesIterator(); 465 // ignore http/s-only filters. 466 if ((authorities == null || !authorities.hasNext()) 467 && (filter.hasDataScheme("http") || filter.hasDataScheme("https")) 468 && filter.hasAction(Intent.ACTION_VIEW) 469 && filter.hasCategory(Intent.CATEGORY_BROWSABLE)) { 470 continue; 471 } 472 instantAppResolver.addFilter( 473 new AuxiliaryResolveInfo.AuxiliaryFilter( 474 filter, 475 instantAppInfo, 476 instantAppFilter.getSplitName(), 477 instantAppInfo.getExtras() 478 )); 479 } 480 } 481 List<AuxiliaryResolveInfo.AuxiliaryFilter> matchedResolveInfoList = 482 instantAppResolver.queryIntent( 483 origIntent, resolvedType, false /*defaultOnly*/, userId); 484 if (!matchedResolveInfoList.isEmpty()) { 485 if (DEBUG_INSTANT) { 486 Log.d(TAG, "[" + token + "] Found match(es); " + matchedResolveInfoList); 487 } 488 return matchedResolveInfoList; 489 } else if (DEBUG_INSTANT) { 490 Log.d(TAG, "[" + token + "] No matches found" 491 + " package: " + instantAppInfo.getPackageName() 492 + ", versionCode: " + instantAppInfo.getVersionCode()); 493 } 494 return null; 495 } 496 497 private static void logMetrics(int action, long startTime, String token, 498 @ResolutionStatus int status) { 499 final LogMaker logMaker = new LogMaker(action) 500 .setType(MetricsProto.MetricsEvent.TYPE_ACTION) 501 .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_DELAY_MS, 502 new Long(System.currentTimeMillis() - startTime)) 503 .addTaggedData(FIELD_INSTANT_APP_LAUNCH_TOKEN, token) 504 .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_STATUS, new Integer(status)); 505 getLogger().write(logMaker); 506 } 507} 508