InstantAppResolver.java revision b7080e35b720aec68cf4771637f46d9f6e1c7d66
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_NO_HISTORY 260 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 261 if (token != null) { 262 // TODO(b/72700831): remove populating old extra 263 intent.putExtra(Intent.EXTRA_EPHEMERAL_TOKEN, token); 264 intent.putExtra(Intent.EXTRA_INSTANT_APP_TOKEN, token); 265 } 266 if (origIntent.getData() != null) { 267 // TODO(b/72700831): remove populating old extra 268 intent.putExtra(Intent.EXTRA_EPHEMERAL_HOSTNAME, origIntent.getData().getHost()); 269 intent.putExtra(Intent.EXTRA_INSTANT_APP_HOSTNAME, origIntent.getData().getHost()); 270 } 271 intent.putExtra(Intent.EXTRA_INSTANT_APP_ACTION, origIntent.getAction()); 272 intent.putExtra(Intent.EXTRA_INTENT, sanitizedIntent); 273 274 if (needsPhaseTwo) { 275 intent.setAction(Intent.ACTION_RESOLVE_INSTANT_APP_PACKAGE); 276 } else { 277 // We have all of the data we need; just start the installer without a second phase 278 if (failureIntent != null || installFailureActivity != null) { 279 // Intent that is launched if the package couldn't be installed for any reason. 280 try { 281 final Intent onFailureIntent; 282 if (installFailureActivity != null) { 283 onFailureIntent = new Intent(); 284 onFailureIntent.setComponent(installFailureActivity); 285 if (filters != null && filters.size() == 1) { 286 onFailureIntent.putExtra(Intent.EXTRA_SPLIT_NAME, 287 filters.get(0).splitName); 288 } 289 onFailureIntent.putExtra(Intent.EXTRA_INTENT, origIntent); 290 } else { 291 onFailureIntent = failureIntent; 292 } 293 final IIntentSender failureIntentTarget = ActivityManager.getService() 294 .getIntentSender( 295 ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage, 296 null /*token*/, null /*resultWho*/, 1 /*requestCode*/, 297 new Intent[] { onFailureIntent }, 298 new String[] { resolvedType }, 299 PendingIntent.FLAG_CANCEL_CURRENT 300 | PendingIntent.FLAG_ONE_SHOT 301 | PendingIntent.FLAG_IMMUTABLE, 302 null /*bOptions*/, userId); 303 IntentSender failureSender = new IntentSender(failureIntentTarget); 304 // TODO(b/72700831): remove populating old extra 305 intent.putExtra(Intent.EXTRA_EPHEMERAL_FAILURE, failureSender); 306 intent.putExtra(Intent.EXTRA_INSTANT_APP_FAILURE, failureSender); 307 } catch (RemoteException ignore) { /* ignore; same process */ } 308 } 309 310 // Intent that is launched if the package was installed successfully. 311 final Intent successIntent = new Intent(origIntent); 312 successIntent.setLaunchToken(token); 313 try { 314 final IIntentSender successIntentTarget = ActivityManager.getService() 315 .getIntentSender( 316 ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage, 317 null /*token*/, null /*resultWho*/, 0 /*requestCode*/, 318 new Intent[] { successIntent }, 319 new String[] { resolvedType }, 320 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT 321 | PendingIntent.FLAG_IMMUTABLE, 322 null /*bOptions*/, userId); 323 IntentSender successSender = new IntentSender(successIntentTarget); 324 // TODO(b/72700831): remove populating old extra 325 intent.putExtra(Intent.EXTRA_EPHEMERAL_SUCCESS, successSender); 326 intent.putExtra(Intent.EXTRA_INSTANT_APP_SUCCESS, successSender); 327 } catch (RemoteException ignore) { /* ignore; same process */ } 328 if (verificationBundle != null) { 329 intent.putExtra(Intent.EXTRA_VERIFICATION_BUNDLE, verificationBundle); 330 } 331 intent.putExtra(Intent.EXTRA_CALLING_PACKAGE, callingPackage); 332 333 if (filters != null) { 334 Bundle resolvableFilters[] = new Bundle[filters.size()]; 335 for (int i = 0, max = filters.size(); i < max; i++) { 336 Bundle resolvableFilter = new Bundle(); 337 AuxiliaryResolveInfo.AuxiliaryFilter filter = filters.get(i); 338 resolvableFilter.putBoolean(Intent.EXTRA_UNKNOWN_INSTANT_APP, 339 filter.resolveInfo != null 340 && filter.resolveInfo.shouldLetInstallerDecide()); 341 resolvableFilter.putString(Intent.EXTRA_PACKAGE_NAME, filter.packageName); 342 resolvableFilter.putString(Intent.EXTRA_SPLIT_NAME, filter.splitName); 343 resolvableFilter.putLong(Intent.EXTRA_LONG_VERSION_CODE, filter.versionCode); 344 resolvableFilter.putBundle(Intent.EXTRA_INSTANT_APP_EXTRAS, filter.extras); 345 resolvableFilters[i] = resolvableFilter; 346 if (i == 0) { 347 // for backwards compat, always set the first result on the intent and add 348 // the int version code 349 intent.putExtras(resolvableFilter); 350 intent.putExtra(Intent.EXTRA_VERSION_CODE, (int) filter.versionCode); 351 } 352 } 353 intent.putExtra(Intent.EXTRA_INSTANT_APP_BUNDLES, resolvableFilters); 354 } 355 intent.setAction(Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE); 356 } 357 return intent; 358 } 359 360 private static AuxiliaryResolveInfo filterInstantAppIntent( 361 List<InstantAppResolveInfo> instantAppResolveInfoList, 362 Intent origIntent, String resolvedType, int userId, String packageName, 363 InstantAppDigest digest, String token) { 364 final int[] shaPrefix = digest.getDigestPrefix(); 365 final byte[][] digestBytes = digest.getDigestBytes(); 366 final Intent failureIntent = new Intent(origIntent); 367 boolean requiresSecondPhase = false; 368 failureIntent.setFlags(failureIntent.getFlags() | Intent.FLAG_IGNORE_EPHEMERAL); 369 failureIntent.setLaunchToken(token); 370 ArrayList<AuxiliaryResolveInfo.AuxiliaryFilter> filters = null; 371 boolean isWebIntent = origIntent.isWebIntent(); 372 for (InstantAppResolveInfo instantAppResolveInfo : instantAppResolveInfoList) { 373 if (shaPrefix.length > 0 && instantAppResolveInfo.shouldLetInstallerDecide()) { 374 Slog.e(TAG, "InstantAppResolveInfo with mShouldLetInstallerDecide=true when digest" 375 + " provided; ignoring"); 376 continue; 377 } 378 byte[] filterDigestBytes = instantAppResolveInfo.getDigestBytes(); 379 // Only include matching digests if we have a prefix and we're either dealing with a 380 // web intent or the resolveInfo specifies digest details. 381 if (shaPrefix.length > 0 && (isWebIntent || filterDigestBytes.length > 0)) { 382 boolean matchFound = false; 383 // Go in reverse order so we match the narrowest scope first. 384 for (int i = shaPrefix.length - 1; i >= 0; --i) { 385 if (Arrays.equals(digestBytes[i], filterDigestBytes)) { 386 matchFound = true; 387 break; 388 } 389 } 390 if (!matchFound) { 391 continue; 392 } 393 } 394 // We matched a resolve info; resolve the filters to see if anything matches completely. 395 List<AuxiliaryResolveInfo.AuxiliaryFilter> matchFilters = computeResolveFilters( 396 origIntent, resolvedType, userId, packageName, token, instantAppResolveInfo); 397 if (matchFilters != null) { 398 if (matchFilters.isEmpty()) { 399 requiresSecondPhase = true; 400 } 401 if (filters == null) { 402 filters = new ArrayList<>(matchFilters); 403 } else { 404 filters.addAll(matchFilters); 405 } 406 } 407 } 408 if (filters != null && !filters.isEmpty()) { 409 return new AuxiliaryResolveInfo(token, requiresSecondPhase, failureIntent, filters); 410 } 411 // Hash or filter mis-match; no instant apps for this domain. 412 return null; 413 } 414 415 /** 416 * Returns one of three states: <p/> 417 * <ul> 418 * <li>{@code null} if there are no matches will not be; resolution is unnecessary.</li> 419 * <li>An empty list signifying that a 2nd phase of resolution is required.</li> 420 * <li>A populated list meaning that matches were found and should be sent directly to the 421 * installer</li> 422 * </ul> 423 * 424 */ 425 private static List<AuxiliaryResolveInfo.AuxiliaryFilter> computeResolveFilters( 426 Intent origIntent, String resolvedType, int userId, String packageName, String token, 427 InstantAppResolveInfo instantAppInfo) { 428 if (instantAppInfo.shouldLetInstallerDecide()) { 429 return Collections.singletonList( 430 new AuxiliaryResolveInfo.AuxiliaryFilter( 431 instantAppInfo, null /* splitName */, 432 instantAppInfo.getExtras())); 433 } 434 if (packageName != null 435 && !packageName.equals(instantAppInfo.getPackageName())) { 436 return null; 437 } 438 final List<InstantAppIntentFilter> instantAppFilters = 439 instantAppInfo.getIntentFilters(); 440 if (instantAppFilters == null || instantAppFilters.isEmpty()) { 441 // No filters on web intent; no matches, 2nd phase unnecessary. 442 if (origIntent.isWebIntent()) { 443 return null; 444 } 445 // No filters; we need to start phase two 446 if (DEBUG_INSTANT) { 447 Log.d(TAG, "No app filters; go to phase 2"); 448 } 449 return Collections.emptyList(); 450 } 451 final PackageManagerService.InstantAppIntentResolver instantAppResolver = 452 new PackageManagerService.InstantAppIntentResolver(); 453 for (int j = instantAppFilters.size() - 1; j >= 0; --j) { 454 final InstantAppIntentFilter instantAppFilter = instantAppFilters.get(j); 455 final List<IntentFilter> splitFilters = instantAppFilter.getFilters(); 456 if (splitFilters == null || splitFilters.isEmpty()) { 457 continue; 458 } 459 for (int k = splitFilters.size() - 1; k >= 0; --k) { 460 IntentFilter filter = splitFilters.get(k); 461 Iterator<IntentFilter.AuthorityEntry> authorities = 462 filter.authoritiesIterator(); 463 // ignore http/s-only filters. 464 if ((authorities == null || !authorities.hasNext()) 465 && (filter.hasDataScheme("http") || filter.hasDataScheme("https")) 466 && filter.hasAction(Intent.ACTION_VIEW) 467 && filter.hasCategory(Intent.CATEGORY_BROWSABLE)) { 468 continue; 469 } 470 instantAppResolver.addFilter( 471 new AuxiliaryResolveInfo.AuxiliaryFilter( 472 filter, 473 instantAppInfo, 474 instantAppFilter.getSplitName(), 475 instantAppInfo.getExtras() 476 )); 477 } 478 } 479 List<AuxiliaryResolveInfo.AuxiliaryFilter> matchedResolveInfoList = 480 instantAppResolver.queryIntent( 481 origIntent, resolvedType, false /*defaultOnly*/, userId); 482 if (!matchedResolveInfoList.isEmpty()) { 483 if (DEBUG_INSTANT) { 484 Log.d(TAG, "[" + token + "] Found match(es); " + matchedResolveInfoList); 485 } 486 return matchedResolveInfoList; 487 } else if (DEBUG_INSTANT) { 488 Log.d(TAG, "[" + token + "] No matches found" 489 + " package: " + instantAppInfo.getPackageName() 490 + ", versionCode: " + instantAppInfo.getVersionCode()); 491 } 492 return null; 493 } 494 495 private static void logMetrics(int action, long startTime, String token, 496 @ResolutionStatus int status) { 497 final LogMaker logMaker = new LogMaker(action) 498 .setType(MetricsProto.MetricsEvent.TYPE_ACTION) 499 .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_DELAY_MS, 500 new Long(System.currentTimeMillis() - startTime)) 501 .addTaggedData(FIELD_INSTANT_APP_LAUNCH_TOKEN, token) 502 .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_STATUS, new Integer(status)); 503 getLogger().write(logMaker); 504 } 505} 506