InstantAppResolver.java revision 0da8537cf610cff71a2ef9ae946a7d3cb4232de4
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 final InstantAppDigest digest = getInstantAppDigest(origIntent); 130 final int[] shaPrefix = digest.getDigestPrefix(); 131 AuxiliaryResolveInfo resolveInfo = null; 132 @ResolutionStatus int resolutionStatus = RESOLUTION_SUCCESS; 133 try { 134 final List<InstantAppResolveInfo> instantAppResolveInfoList = 135 connection.getInstantAppResolveInfoList(sanitizedIntent, shaPrefix, token); 136 if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) { 137 resolveInfo = InstantAppResolver.filterInstantAppIntent( 138 instantAppResolveInfoList, origIntent, requestObj.resolvedType, 139 requestObj.userId, origIntent.getPackage(), digest, token); 140 } 141 } catch (ConnectionException e) { 142 if (e.failure == ConnectionException.FAILURE_BIND) { 143 resolutionStatus = RESOLUTION_BIND_TIMEOUT; 144 } else if (e.failure == ConnectionException.FAILURE_CALL) { 145 resolutionStatus = RESOLUTION_CALL_TIMEOUT; 146 } else { 147 resolutionStatus = RESOLUTION_FAILURE; 148 } 149 } 150 // Only log successful instant application resolution 151 if (requestObj.resolveForStart && resolutionStatus == RESOLUTION_SUCCESS) { 152 logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_ONE, startTime, token, 153 resolutionStatus); 154 } 155 if (DEBUG_INSTANT && resolveInfo == null) { 156 if (resolutionStatus == RESOLUTION_BIND_TIMEOUT) { 157 Log.d(TAG, "[" + token + "] Phase1; bind timed out"); 158 } else if (resolutionStatus == RESOLUTION_CALL_TIMEOUT) { 159 Log.d(TAG, "[" + token + "] Phase1; call timed out"); 160 } else if (resolutionStatus != RESOLUTION_SUCCESS) { 161 Log.d(TAG, "[" + token + "] Phase1; service connection error"); 162 } else { 163 Log.d(TAG, "[" + token + "] Phase1; No results matched"); 164 } 165 } 166 return resolveInfo; 167 } 168 169 private static InstantAppDigest getInstantAppDigest(Intent origIntent) { 170 return origIntent.getData() != null && !TextUtils.isEmpty(origIntent.getData().getHost()) 171 ? new InstantAppDigest(origIntent.getData().getHost(), 5 /*maxDigests*/) 172 : InstantAppDigest.UNDEFINED; 173 } 174 175 public static void doInstantAppResolutionPhaseTwo(Context context, 176 InstantAppResolverConnection connection, InstantAppRequest requestObj, 177 ActivityInfo instantAppInstaller, Handler callbackHandler) { 178 final long startTime = System.currentTimeMillis(); 179 final String token = requestObj.responseObj.token; 180 if (DEBUG_INSTANT) { 181 Log.d(TAG, "[" + token + "] Phase2; resolving"); 182 } 183 final Intent origIntent = requestObj.origIntent; 184 final Intent sanitizedIntent = sanitizeIntent(origIntent); 185 final InstantAppDigest digest = getInstantAppDigest(origIntent); 186 final int[] shaPrefix = digest.getDigestPrefix(); 187 188 final PhaseTwoCallback callback = new PhaseTwoCallback() { 189 @Override 190 void onPhaseTwoResolved(List<InstantAppResolveInfo> instantAppResolveInfoList, 191 long startTime) { 192 final Intent failureIntent; 193 if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) { 194 final AuxiliaryResolveInfo instantAppIntentInfo = 195 InstantAppResolver.filterInstantAppIntent( 196 instantAppResolveInfoList, origIntent, null /*resolvedType*/, 197 0 /*userId*/, origIntent.getPackage(), digest, token); 198 if (instantAppIntentInfo != null) { 199 failureIntent = instantAppIntentInfo.failureIntent; 200 } else { 201 failureIntent = null; 202 } 203 } else { 204 failureIntent = null; 205 } 206 final Intent installerIntent = buildEphemeralInstallerIntent( 207 requestObj.origIntent, 208 sanitizedIntent, 209 failureIntent, 210 requestObj.callingPackage, 211 requestObj.verificationBundle, 212 requestObj.resolvedType, 213 requestObj.userId, 214 requestObj.responseObj.installFailureActivity, 215 token, 216 false /*needsPhaseTwo*/, 217 requestObj.responseObj.filters); 218 installerIntent.setComponent(new ComponentName( 219 instantAppInstaller.packageName, instantAppInstaller.name)); 220 221 logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO, startTime, token, 222 requestObj.responseObj.filters != null ? RESOLUTION_SUCCESS : RESOLUTION_FAILURE); 223 224 context.startActivity(installerIntent); 225 } 226 }; 227 try { 228 connection.getInstantAppIntentFilterList(sanitizedIntent, shaPrefix, token, callback, 229 callbackHandler, startTime); 230 } catch (ConnectionException e) { 231 @ResolutionStatus int resolutionStatus = RESOLUTION_FAILURE; 232 if (e.failure == ConnectionException.FAILURE_BIND) { 233 resolutionStatus = RESOLUTION_BIND_TIMEOUT; 234 } 235 logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO, startTime, token, 236 resolutionStatus); 237 if (DEBUG_INSTANT) { 238 if (resolutionStatus == RESOLUTION_BIND_TIMEOUT) { 239 Log.d(TAG, "[" + token + "] Phase2; bind timed out"); 240 } else { 241 Log.d(TAG, "[" + token + "] Phase2; service connection error"); 242 } 243 } 244 } 245 } 246 247 /** 248 * Builds and returns an intent to launch the instant installer. 249 */ 250 public static Intent buildEphemeralInstallerIntent( 251 @NonNull Intent origIntent, 252 @NonNull Intent sanitizedIntent, 253 @Nullable Intent failureIntent, 254 @NonNull String callingPackage, 255 @Nullable Bundle verificationBundle, 256 @NonNull String resolvedType, 257 int userId, 258 @Nullable ComponentName installFailureActivity, 259 @Nullable String token, 260 boolean needsPhaseTwo, 261 List<AuxiliaryResolveInfo.AuxiliaryFilter> filters) { 262 // Construct the intent that launches the instant installer 263 int flags = origIntent.getFlags(); 264 final Intent intent = new Intent(); 265 intent.setFlags(flags 266 | Intent.FLAG_ACTIVITY_NEW_TASK 267 | Intent.FLAG_ACTIVITY_CLEAR_TASK 268 | Intent.FLAG_ACTIVITY_NO_HISTORY 269 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 270 if (token != null) { 271 // TODO(b/72700831): remove populating old extra 272 intent.putExtra(Intent.EXTRA_EPHEMERAL_TOKEN, token); 273 intent.putExtra(Intent.EXTRA_INSTANT_APP_TOKEN, token); 274 } 275 if (origIntent.getData() != null) { 276 // TODO(b/72700831): remove populating old extra 277 intent.putExtra(Intent.EXTRA_EPHEMERAL_HOSTNAME, origIntent.getData().getHost()); 278 intent.putExtra(Intent.EXTRA_INSTANT_APP_HOSTNAME, origIntent.getData().getHost()); 279 } 280 intent.putExtra(Intent.EXTRA_INSTANT_APP_ACTION, origIntent.getAction()); 281 intent.putExtra(Intent.EXTRA_INTENT, sanitizedIntent); 282 283 if (needsPhaseTwo) { 284 intent.setAction(Intent.ACTION_RESOLVE_INSTANT_APP_PACKAGE); 285 } else { 286 // We have all of the data we need; just start the installer without a second phase 287 if (failureIntent != null || installFailureActivity != null) { 288 // Intent that is launched if the package couldn't be installed for any reason. 289 try { 290 final Intent onFailureIntent; 291 if (installFailureActivity != null) { 292 onFailureIntent = new Intent(); 293 onFailureIntent.setComponent(installFailureActivity); 294 if (filters != null && filters.size() == 1) { 295 onFailureIntent.putExtra(Intent.EXTRA_SPLIT_NAME, 296 filters.get(0).splitName); 297 } 298 onFailureIntent.putExtra(Intent.EXTRA_INTENT, origIntent); 299 } else { 300 onFailureIntent = failureIntent; 301 } 302 final IIntentSender failureIntentTarget = ActivityManager.getService() 303 .getIntentSender( 304 ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage, 305 null /*token*/, null /*resultWho*/, 1 /*requestCode*/, 306 new Intent[] { onFailureIntent }, 307 new String[] { resolvedType }, 308 PendingIntent.FLAG_CANCEL_CURRENT 309 | PendingIntent.FLAG_ONE_SHOT 310 | PendingIntent.FLAG_IMMUTABLE, 311 null /*bOptions*/, userId); 312 IntentSender failureSender = new IntentSender(failureIntentTarget); 313 // TODO(b/72700831): remove populating old extra 314 intent.putExtra(Intent.EXTRA_EPHEMERAL_FAILURE, failureSender); 315 intent.putExtra(Intent.EXTRA_INSTANT_APP_FAILURE, failureSender); 316 } catch (RemoteException ignore) { /* ignore; same process */ } 317 } 318 319 // Intent that is launched if the package was installed successfully. 320 final Intent successIntent = new Intent(origIntent); 321 successIntent.setLaunchToken(token); 322 try { 323 final IIntentSender successIntentTarget = ActivityManager.getService() 324 .getIntentSender( 325 ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage, 326 null /*token*/, null /*resultWho*/, 0 /*requestCode*/, 327 new Intent[] { successIntent }, 328 new String[] { resolvedType }, 329 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT 330 | PendingIntent.FLAG_IMMUTABLE, 331 null /*bOptions*/, userId); 332 IntentSender successSender = new IntentSender(successIntentTarget); 333 // TODO(b/72700831): remove populating old extra 334 intent.putExtra(Intent.EXTRA_EPHEMERAL_SUCCESS, successSender); 335 intent.putExtra(Intent.EXTRA_INSTANT_APP_SUCCESS, successSender); 336 } catch (RemoteException ignore) { /* ignore; same process */ } 337 if (verificationBundle != null) { 338 intent.putExtra(Intent.EXTRA_VERIFICATION_BUNDLE, verificationBundle); 339 } 340 intent.putExtra(Intent.EXTRA_CALLING_PACKAGE, callingPackage); 341 342 if (filters != null) { 343 Bundle resolvableFilters[] = new Bundle[filters.size()]; 344 for (int i = 0, max = filters.size(); i < max; i++) { 345 Bundle resolvableFilter = new Bundle(); 346 AuxiliaryResolveInfo.AuxiliaryFilter filter = filters.get(i); 347 resolvableFilter.putBoolean(Intent.EXTRA_UNKNOWN_INSTANT_APP, 348 filter.resolveInfo != null 349 && filter.resolveInfo.shouldLetInstallerDecide()); 350 resolvableFilter.putString(Intent.EXTRA_PACKAGE_NAME, filter.packageName); 351 resolvableFilter.putString(Intent.EXTRA_SPLIT_NAME, filter.splitName); 352 resolvableFilter.putLong(Intent.EXTRA_LONG_VERSION_CODE, filter.versionCode); 353 resolvableFilter.putBundle(Intent.EXTRA_INSTANT_APP_EXTRAS, filter.extras); 354 resolvableFilters[i] = resolvableFilter; 355 if (i == 0) { 356 // for backwards compat, always set the first result on the intent and add 357 // the int version code 358 intent.putExtras(resolvableFilter); 359 intent.putExtra(Intent.EXTRA_VERSION_CODE, (int) filter.versionCode); 360 } 361 } 362 intent.putExtra(Intent.EXTRA_INSTANT_APP_BUNDLES, resolvableFilters); 363 } 364 intent.setAction(Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE); 365 } 366 return intent; 367 } 368 369 private static AuxiliaryResolveInfo filterInstantAppIntent( 370 List<InstantAppResolveInfo> instantAppResolveInfoList, 371 Intent origIntent, String resolvedType, int userId, String packageName, 372 InstantAppDigest digest, String token) { 373 final int[] shaPrefix = digest.getDigestPrefix(); 374 final byte[][] digestBytes = digest.getDigestBytes(); 375 final Intent failureIntent = new Intent(origIntent); 376 boolean requiresSecondPhase = false; 377 failureIntent.setFlags(failureIntent.getFlags() | Intent.FLAG_IGNORE_EPHEMERAL); 378 failureIntent.setLaunchToken(token); 379 ArrayList<AuxiliaryResolveInfo.AuxiliaryFilter> filters = null; 380 boolean isWebIntent = origIntent.isWebIntent(); 381 for (InstantAppResolveInfo instantAppResolveInfo : instantAppResolveInfoList) { 382 if (shaPrefix.length > 0 && instantAppResolveInfo.shouldLetInstallerDecide()) { 383 Slog.e(TAG, "InstantAppResolveInfo with mShouldLetInstallerDecide=true when digest" 384 + " provided; ignoring"); 385 continue; 386 } 387 byte[] filterDigestBytes = instantAppResolveInfo.getDigestBytes(); 388 // Only include matching digests if we have a prefix and we're either dealing with a 389 // web intent or the resolveInfo specifies digest details. 390 if (shaPrefix.length > 0 && (isWebIntent || filterDigestBytes.length > 0)) { 391 boolean matchFound = false; 392 // Go in reverse order so we match the narrowest scope first. 393 for (int i = shaPrefix.length - 1; i >= 0; --i) { 394 if (Arrays.equals(digestBytes[i], filterDigestBytes)) { 395 matchFound = true; 396 break; 397 } 398 } 399 if (!matchFound) { 400 continue; 401 } 402 } 403 // We matched a resolve info; resolve the filters to see if anything matches completely. 404 List<AuxiliaryResolveInfo.AuxiliaryFilter> matchFilters = computeResolveFilters( 405 origIntent, resolvedType, userId, packageName, token, instantAppResolveInfo); 406 if (matchFilters != null) { 407 if (matchFilters.isEmpty()) { 408 requiresSecondPhase = true; 409 } 410 if (filters == null) { 411 filters = new ArrayList<>(matchFilters); 412 } else { 413 filters.addAll(matchFilters); 414 } 415 } 416 } 417 if (filters != null && !filters.isEmpty()) { 418 return new AuxiliaryResolveInfo(token, requiresSecondPhase, failureIntent, filters); 419 } 420 // Hash or filter mis-match; no instant apps for this domain. 421 return null; 422 } 423 424 /** 425 * Returns one of three states: <p/> 426 * <ul> 427 * <li>{@code null} if there are no matches will not be; resolution is unnecessary.</li> 428 * <li>An empty list signifying that a 2nd phase of resolution is required.</li> 429 * <li>A populated list meaning that matches were found and should be sent directly to the 430 * installer</li> 431 * </ul> 432 * 433 */ 434 private static List<AuxiliaryResolveInfo.AuxiliaryFilter> computeResolveFilters( 435 Intent origIntent, String resolvedType, int userId, String packageName, String token, 436 InstantAppResolveInfo instantAppInfo) { 437 if (instantAppInfo.shouldLetInstallerDecide()) { 438 return Collections.singletonList( 439 new AuxiliaryResolveInfo.AuxiliaryFilter( 440 instantAppInfo, null /* splitName */, 441 instantAppInfo.getExtras())); 442 } 443 if (packageName != null 444 && !packageName.equals(instantAppInfo.getPackageName())) { 445 return null; 446 } 447 final List<InstantAppIntentFilter> instantAppFilters = 448 instantAppInfo.getIntentFilters(); 449 if (instantAppFilters == null || instantAppFilters.isEmpty()) { 450 // No filters on web intent; no matches, 2nd phase unnecessary. 451 if (origIntent.isWebIntent()) { 452 return null; 453 } 454 // No filters; we need to start phase two 455 if (DEBUG_INSTANT) { 456 Log.d(TAG, "No app filters; go to phase 2"); 457 } 458 return Collections.emptyList(); 459 } 460 final PackageManagerService.InstantAppIntentResolver instantAppResolver = 461 new PackageManagerService.InstantAppIntentResolver(); 462 for (int j = instantAppFilters.size() - 1; j >= 0; --j) { 463 final InstantAppIntentFilter instantAppFilter = instantAppFilters.get(j); 464 final List<IntentFilter> splitFilters = instantAppFilter.getFilters(); 465 if (splitFilters == null || splitFilters.isEmpty()) { 466 continue; 467 } 468 for (int k = splitFilters.size() - 1; k >= 0; --k) { 469 IntentFilter filter = splitFilters.get(k); 470 Iterator<IntentFilter.AuthorityEntry> authorities = 471 filter.authoritiesIterator(); 472 // ignore http/s-only filters. 473 if ((authorities == null || !authorities.hasNext()) 474 && (filter.hasDataScheme("http") || filter.hasDataScheme("https")) 475 && filter.hasAction(Intent.ACTION_VIEW) 476 && filter.hasCategory(Intent.CATEGORY_BROWSABLE)) { 477 continue; 478 } 479 instantAppResolver.addFilter( 480 new AuxiliaryResolveInfo.AuxiliaryFilter( 481 filter, 482 instantAppInfo, 483 instantAppFilter.getSplitName(), 484 instantAppInfo.getExtras() 485 )); 486 } 487 } 488 List<AuxiliaryResolveInfo.AuxiliaryFilter> matchedResolveInfoList = 489 instantAppResolver.queryIntent( 490 origIntent, resolvedType, false /*defaultOnly*/, userId); 491 if (!matchedResolveInfoList.isEmpty()) { 492 if (DEBUG_INSTANT) { 493 Log.d(TAG, "[" + token + "] Found match(es); " + matchedResolveInfoList); 494 } 495 return matchedResolveInfoList; 496 } else if (DEBUG_INSTANT) { 497 Log.d(TAG, "[" + token + "] No matches found" 498 + " package: " + instantAppInfo.getPackageName() 499 + ", versionCode: " + instantAppInfo.getVersionCode()); 500 } 501 return null; 502 } 503 504 private static void logMetrics(int action, long startTime, String token, 505 @ResolutionStatus int status) { 506 final LogMaker logMaker = new LogMaker(action) 507 .setType(MetricsProto.MetricsEvent.TYPE_ACTION) 508 .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_DELAY_MS, 509 new Long(System.currentTimeMillis() - startTime)) 510 .addTaggedData(FIELD_INSTANT_APP_LAUNCH_TOKEN, token) 511 .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_STATUS, new Integer(status)); 512 getLogger().write(logMaker); 513 } 514} 515