• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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