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