1 /* 2 * Copyright (C) 2021 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 package com.android.server.am; 17 18 import android.annotation.NonNull; 19 import android.annotation.Nullable; 20 import android.compat.annotation.ChangeId; 21 import android.compat.annotation.Disabled; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.ComponentInfo; 26 import android.content.pm.PackageManager; 27 import android.content.pm.PackageManager.NameNotFoundException; 28 import android.content.pm.PackageManager.Property; 29 import android.content.pm.PackageManagerInternal; 30 import android.content.pm.ResolveInfo; 31 import android.content.pm.ServiceInfo; 32 import android.os.Binder; 33 import android.os.Build; 34 import android.os.ServiceManager; 35 import android.os.UserHandle; 36 import android.text.TextUtils; 37 import android.util.ArrayMap; 38 import android.util.Log; 39 import android.util.Slog; 40 41 import com.android.internal.annotations.GuardedBy; 42 import com.android.internal.content.PackageMonitor; 43 import com.android.internal.os.BackgroundThread; 44 import com.android.server.FgThread; 45 import com.android.server.LocalServices; 46 import com.android.server.compat.CompatChange; 47 import com.android.server.compat.PlatformCompat; 48 49 import java.io.PrintWriter; 50 import java.util.List; 51 import java.util.Objects; 52 import java.util.function.Supplier; 53 54 /** 55 * Manages and handles component aliases, which is an experimental feature. 56 * 57 * NOTE: THIS CLASS IS PURELY EXPERIMENTAL AND WILL BE REMOVED IN FUTURE ANDROID VERSIONS. 58 * DO NOT USE IT. 59 * 60 * "Component alias" allows an android manifest component (for now only broadcasts and services) 61 * to be defined in one android package while having the implementation in a different package. 62 * 63 * When/if this becomes a real feature, it will be most likely implemented very differently, 64 * which is why this shouldn't be used. 65 * 66 * For now, because this is an experimental feature to evaluate feasibility, the implementation is 67 * "quick & dirty". For example, to define aliases, we use a regular intent filter and meta-data 68 * in the manifest, instead of adding proper tags/attributes to AndroidManifest.xml. 69 * 70 * This feature is disabled by default. 71 * 72 * Also, for now, aliases can be defined across packages with different certificates, but 73 * in a final version this will most likely be tightened. 74 */ 75 public class ComponentAliasResolver { 76 private static final String TAG = "ComponentAliasResolver"; 77 private static final boolean DEBUG = true; 78 79 /** 80 * This flag has to be enabled for the "android" package to use component aliases. 81 */ 82 @ChangeId 83 @Disabled 84 public static final long USE_EXPERIMENTAL_COMPONENT_ALIAS = 196254758L; 85 86 private final Object mLock = new Object(); 87 private final ActivityManagerService mAm; 88 private final Context mContext; 89 90 @GuardedBy("mLock") 91 private boolean mEnabledByDeviceConfig; 92 93 @GuardedBy("mLock") 94 private boolean mEnabled; 95 96 @GuardedBy("mLock") 97 private String mOverrideString; 98 99 @GuardedBy("mLock") 100 private final ArrayMap<ComponentName, ComponentName> mFromTo = new ArrayMap<>(); 101 102 @GuardedBy("mLock") 103 private PlatformCompat mPlatformCompat; 104 105 private static final String OPT_IN_PROPERTY = "com.android.EXPERIMENTAL_COMPONENT_ALIAS_OPT_IN"; 106 107 private static final String ALIAS_FILTER_ACTION = 108 "com.android.intent.action.EXPERIMENTAL_IS_ALIAS"; 109 private static final String ALIAS_FILTER_ACTION_ALT = 110 "android.intent.action.EXPERIMENTAL_IS_ALIAS"; 111 private static final String META_DATA_ALIAS_TARGET = "alias_target"; 112 113 private static final int PACKAGE_QUERY_FLAGS = 114 PackageManager.MATCH_UNINSTALLED_PACKAGES 115 | PackageManager.MATCH_ANY_USER 116 | PackageManager.MATCH_DIRECT_BOOT_AWARE 117 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 118 | PackageManager.GET_META_DATA; 119 ComponentAliasResolver(ActivityManagerService service)120 public ComponentAliasResolver(ActivityManagerService service) { 121 mAm = service; 122 mContext = service.mContext; 123 } 124 isEnabled()125 public boolean isEnabled() { 126 synchronized (mLock) { 127 return mEnabled; 128 } 129 } 130 131 /** 132 * When there's any change to packages, we refresh all the aliases. 133 * TODO: In the production version, we should update only the changed package. 134 */ 135 final PackageMonitor mPackageMonitor = new PackageMonitor() { 136 @Override 137 public void onPackageModified(String packageName) { 138 refresh(); 139 } 140 141 @Override 142 public void onPackageAdded(String packageName, int uid) { 143 refresh(); 144 } 145 146 @Override 147 public void onPackageRemoved(String packageName, int uid) { 148 refresh(); 149 } 150 }; 151 152 private final CompatChange.ChangeListener mCompatChangeListener = (packageName) -> { 153 if (DEBUG) Slog.d(TAG, "USE_EXPERIMENTAL_COMPONENT_ALIAS changed."); 154 BackgroundThread.getHandler().post(this::refresh); 155 }; 156 157 /** 158 * Call this on systemRead(). 159 */ onSystemReady(boolean enabledByDeviceConfig, String overrides)160 public void onSystemReady(boolean enabledByDeviceConfig, String overrides) { 161 synchronized (mLock) { 162 mPlatformCompat = (PlatformCompat) ServiceManager.getService( 163 Context.PLATFORM_COMPAT_SERVICE); 164 mPlatformCompat.registerListener(USE_EXPERIMENTAL_COMPONENT_ALIAS, 165 mCompatChangeListener); 166 } 167 if (DEBUG) Slog.d(TAG, "Compat listener set."); 168 update(enabledByDeviceConfig, overrides); 169 } 170 171 /** 172 * (Re-)loads aliases from <meta-data> and the device config override. 173 */ update(boolean enabledByDeviceConfig, String overrides)174 public void update(boolean enabledByDeviceConfig, String overrides) { 175 synchronized (mLock) { 176 if (mPlatformCompat == null) { 177 return; // System not ready. 178 } 179 final boolean enabled = Build.isDebuggable() 180 && (enabledByDeviceConfig 181 || mPlatformCompat.isChangeEnabledByPackageName( 182 USE_EXPERIMENTAL_COMPONENT_ALIAS, "android", UserHandle.USER_SYSTEM)); 183 if (enabled != mEnabled) { 184 Slog.i(TAG, (enabled ? "Enabling" : "Disabling") + " component aliases..."); 185 FgThread.getHandler().post(() -> { 186 // Registering/unregistering a receiver internally takes the AM lock, but AM 187 // calls into this class while holding the AM lock. So do it on a handler to 188 // avoid deadlocks. 189 if (enabled) { 190 mPackageMonitor.register(mAm.mContext, UserHandle.ALL, 191 /* externalStorage= */ false, BackgroundThread.getHandler()); 192 } else { 193 mPackageMonitor.unregister(); 194 } 195 }); 196 } 197 mEnabled = enabled; 198 mEnabledByDeviceConfig = enabledByDeviceConfig; 199 mOverrideString = overrides; 200 201 if (mEnabled) { 202 refreshLocked(); 203 } else { 204 mFromTo.clear(); 205 } 206 } 207 } 208 refresh()209 private void refresh() { 210 synchronized (mLock) { 211 update(mEnabledByDeviceConfig, mOverrideString); 212 } 213 } 214 215 @GuardedBy("mLock") refreshLocked()216 private void refreshLocked() { 217 if (DEBUG) Slog.d(TAG, "Refreshing aliases..."); 218 mFromTo.clear(); 219 loadFromMetadataLocked(); 220 loadOverridesLocked(); 221 } 222 223 /** 224 * Scans all the "alias" components and inserts the from-to pairs to the map. 225 */ 226 @GuardedBy("mLock") loadFromMetadataLocked()227 private void loadFromMetadataLocked() { 228 if (DEBUG) Slog.d(TAG, "Scanning service aliases..."); 229 230 // PM.queryInetntXxx() doesn't support "OR" queries, so we search for 231 // both the com.android... action and android... action on by one. 232 // It's okay if a single component handles both actions because the resulting aliases 233 // will be stored in a map and duplicates will naturally be removed. 234 loadFromMetadataLockedInner(new Intent(ALIAS_FILTER_ACTION_ALT)); 235 loadFromMetadataLockedInner(new Intent(ALIAS_FILTER_ACTION)); 236 } 237 loadFromMetadataLockedInner(Intent i)238 private void loadFromMetadataLockedInner(Intent i) { 239 final List<ResolveInfo> services = mContext.getPackageManager().queryIntentServicesAsUser( 240 i, PACKAGE_QUERY_FLAGS, UserHandle.USER_SYSTEM); 241 242 extractAliasesLocked(services); 243 244 if (DEBUG) Slog.d(TAG, "Scanning receiver aliases..."); 245 final List<ResolveInfo> receivers = mContext.getPackageManager() 246 .queryBroadcastReceiversAsUser(i, PACKAGE_QUERY_FLAGS, UserHandle.USER_SYSTEM); 247 248 extractAliasesLocked(receivers); 249 250 // TODO: Scan for other component types as well. 251 } 252 253 /** 254 * Make sure a given package is opted into component alias, by having a 255 * "com.android.EXPERIMENTAL_COMPONENT_ALIAS_OPT_IN" property set to true in the manifest. 256 * 257 * The implementation isn't optimized -- in every call we scan the package's properties, 258 * even thought we're likely going to call it with the same packages multiple times. 259 * But that's okay since this feature is experimental, and this code path won't be called 260 * until explicitly enabled. 261 */ 262 @GuardedBy("mLock") isEnabledForPackageLocked(String packageName)263 private boolean isEnabledForPackageLocked(String packageName) { 264 boolean enabled = false; 265 try { 266 final Property p = mContext.getPackageManager().getProperty( 267 OPT_IN_PROPERTY, packageName); 268 enabled = p.getBoolean(); 269 } catch (NameNotFoundException e) { 270 } 271 if (!enabled) { 272 Slog.w(TAG, "USE_EXPERIMENTAL_COMPONENT_ALIAS not enabled for " + packageName); 273 } 274 return enabled; 275 } 276 277 /** 278 * Make sure an alias and its target are the same package, or, the target is in a "sub" package. 279 */ validateAlias(ComponentName from, ComponentName to)280 private static boolean validateAlias(ComponentName from, ComponentName to) { 281 final String fromPackage = from.getPackageName(); 282 final String toPackage = to.getPackageName(); 283 284 if (Objects.equals(fromPackage, toPackage)) { // Same package? 285 return true; 286 } 287 if (toPackage.startsWith(fromPackage + ".")) { // Prefix? 288 return true; 289 } 290 Slog.w(TAG, "Invalid alias: " 291 + from.flattenToShortString() + " -> " + to.flattenToShortString()); 292 return false; 293 } 294 295 @GuardedBy("mLock") validateAndAddAliasLocked(ComponentName from, ComponentName to)296 private void validateAndAddAliasLocked(ComponentName from, ComponentName to) { 297 if (DEBUG) { 298 Slog.d(TAG, 299 "" + from.flattenToShortString() + " -> " + to.flattenToShortString()); 300 } 301 if (!validateAlias(from, to)) { 302 return; 303 } 304 305 // Make sure both packages have 306 if (!isEnabledForPackageLocked(from.getPackageName()) 307 || !isEnabledForPackageLocked(to.getPackageName())) { 308 return; 309 } 310 311 mFromTo.put(from, to); 312 } 313 314 @GuardedBy("mLock") extractAliasesLocked(List<ResolveInfo> components)315 private void extractAliasesLocked(List<ResolveInfo> components) { 316 for (ResolveInfo ri : components) { 317 final ComponentInfo ci = ri.getComponentInfo(); 318 final ComponentName from = ci.getComponentName(); 319 final ComponentName to = unflatten(ci.metaData.getString(META_DATA_ALIAS_TARGET)); 320 if (to == null) { 321 continue; 322 } 323 validateAndAddAliasLocked(from, to); 324 } 325 } 326 327 /** 328 * Parses an "override" string and inserts the from-to pairs to the map. 329 * 330 * The format is: 331 * ALIAS-COMPONENT-1 ":" TARGET-COMPONENT-1 ( "," ALIAS-COMPONENT-2 ":" TARGET-COMPONENT-2 )* 332 */ 333 @GuardedBy("mLock") loadOverridesLocked()334 private void loadOverridesLocked() { 335 if (DEBUG) Slog.d(TAG, "Loading aliases overrides ..."); 336 for (String line : mOverrideString.split("\\,+")) { 337 final String[] fields = line.split("\\:+", 2); 338 if (TextUtils.isEmpty(fields[0])) { 339 continue; 340 } 341 final ComponentName from = unflatten(fields[0]); 342 if (from == null) { 343 continue; 344 } 345 346 if (fields.length == 1) { 347 if (DEBUG) Slog.d(TAG, "" + from.flattenToShortString() + " [removed]"); 348 mFromTo.remove(from); 349 } else { 350 final ComponentName to = unflatten(fields[1]); 351 if (to == null) { 352 continue; 353 } 354 355 validateAndAddAliasLocked(from, to); 356 } 357 } 358 } 359 unflatten(String name)360 private static ComponentName unflatten(String name) { 361 final ComponentName cn = ComponentName.unflattenFromString(name); 362 if (cn != null) { 363 return cn; 364 } 365 Slog.e(TAG, "Invalid component name detected: " + name); 366 return null; 367 } 368 369 /** 370 * Dump the aliases for dumpsys / bugrports. 371 */ dump(PrintWriter pw)372 public void dump(PrintWriter pw) { 373 synchronized (mLock) { 374 pw.println("ACTIVITY MANAGER COMPONENT-ALIAS (dumpsys activity component-alias)"); 375 pw.print(" Enabled: "); pw.println(mEnabled); 376 377 pw.println(" Aliases:"); 378 for (int i = 0; i < mFromTo.size(); i++) { 379 ComponentName from = mFromTo.keyAt(i); 380 ComponentName to = mFromTo.valueAt(i); 381 pw.print(" "); 382 pw.print(from.flattenToShortString()); 383 pw.print(" -> "); 384 pw.print(to.flattenToShortString()); 385 pw.println(); 386 } 387 pw.println(); 388 } 389 } 390 391 /** 392 * Contains alias resolution information. 393 */ 394 public static class Resolution<T> { 395 /** "From" component. Null if component alias is disabled. */ 396 @Nullable 397 public final T source; 398 399 /** "To" component. Null if component alias is disabled, or the source isn't an alias. */ 400 @Nullable 401 public final T resolved; 402 Resolution(T source, T resolved)403 public Resolution(T source, T resolved) { 404 this.source = source; 405 this.resolved = resolved; 406 } 407 408 @Nullable isAlias()409 public boolean isAlias() { 410 return this.resolved != null; 411 } 412 413 @Nullable getAlias()414 public T getAlias() { 415 return isAlias() ? source : null; 416 } 417 418 @Nullable getTarget()419 public T getTarget() { 420 return isAlias() ? resolved : null; 421 } 422 } 423 424 @NonNull resolveComponentAlias( @onNull Supplier<ComponentName> aliasSupplier)425 public Resolution<ComponentName> resolveComponentAlias( 426 @NonNull Supplier<ComponentName> aliasSupplier) { 427 final long identity = Binder.clearCallingIdentity(); 428 try { 429 synchronized (mLock) { 430 if (!mEnabled) { 431 return new Resolution<>(null, null); 432 } 433 434 final ComponentName alias = aliasSupplier.get(); 435 final ComponentName target = mFromTo.get(alias); 436 437 if (target != null) { 438 if (DEBUG) { 439 Exception stacktrace = null; 440 if (Log.isLoggable(TAG, Log.VERBOSE)) { 441 stacktrace = new RuntimeException("STACKTRACE"); 442 } 443 Slog.d(TAG, "Alias resolved: " + alias.flattenToShortString() 444 + " -> " + target.flattenToShortString(), stacktrace); 445 } 446 } 447 return new Resolution<>(alias, target); 448 } 449 } finally { 450 Binder.restoreCallingIdentity(identity); 451 } 452 } 453 454 @Nullable resolveService( @onNull Intent service, @Nullable String resolvedType, int packageFlags, int userId, int callingUid)455 public Resolution<ComponentName> resolveService( 456 @NonNull Intent service, @Nullable String resolvedType, 457 int packageFlags, int userId, int callingUid) { 458 Resolution<ComponentName> result = resolveComponentAlias(() -> { 459 PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); 460 461 ResolveInfo rInfo = pmi.resolveService(service, 462 resolvedType, packageFlags, userId, callingUid); 463 ServiceInfo sInfo = rInfo != null ? rInfo.serviceInfo : null; 464 if (sInfo == null) { 465 return null; // Service not found. 466 } 467 return new ComponentName(sInfo.applicationInfo.packageName, sInfo.name); 468 }); 469 470 // TODO: To make it consistent with resolveReceiver(), let's ensure the target service 471 // is resolvable, and if not, return null. 472 473 if (result != null && result.isAlias()) { 474 // It's an alias. Keep the original intent, and rewrite it. 475 service.setOriginalIntent(new Intent(service)); 476 477 service.setPackage(null); 478 service.setComponent(result.getTarget()); 479 } 480 return result; 481 } 482 483 @Nullable resolveReceiver(@onNull Intent intent, @NonNull ResolveInfo receiver, @Nullable String resolvedType, int packageFlags, int userId, int callingUid, boolean forSend)484 public Resolution<ResolveInfo> resolveReceiver(@NonNull Intent intent, 485 @NonNull ResolveInfo receiver, @Nullable String resolvedType, 486 int packageFlags, int userId, int callingUid, boolean forSend) { 487 // Resolve this alias. 488 final Resolution<ComponentName> resolution = resolveComponentAlias(() -> 489 receiver.activityInfo.getComponentName()); 490 final ComponentName target = resolution.getTarget(); 491 if (target == null) { 492 return new Resolution<>(receiver, null); // It's not an alias. 493 } 494 495 // Convert the target component name to a ResolveInfo. 496 497 final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); 498 499 // Rewrite the intent to search the target intent. 500 // - We don't actually rewrite the intent we deliver to the receiver here, which is what 501 // resolveService() does, because this intent many be send to other receivers as well. 502 // - But we don't have to do that here either, because the actual receiver component 503 // will be set in BroadcastQueue anyway, before delivering the intent to each receiver. 504 // - However, we're not able to set the original intent either, for the time being. 505 Intent i = new Intent(intent); 506 i.setPackage(null); 507 i.setComponent(resolution.getTarget()); 508 509 List<ResolveInfo> resolved = pmi.queryIntentReceivers( 510 i, resolvedType, packageFlags, callingUid, userId, forSend); 511 if (resolved == null || resolved.size() == 0) { 512 // Target component not found. 513 Slog.w(TAG, "Alias target " + target.flattenToShortString() + " not found"); 514 return null; 515 } 516 return new Resolution<>(receiver, resolved.get(0)); 517 } 518 } 519