blob: 3fa6102b64e02faf2506b85ce3760559741968cd [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.am;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.ChangeId;
import android.compat.annotation.Disabled;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManager.Property;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Binder;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.content.PackageMonitor;
import com.android.internal.os.BackgroundThread;
import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.compat.PlatformCompat;
import java.io.PrintWriter;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
/**
* @deprecated This feature is no longer used. Delete this class.
*
* Also delete Intnt.(set|get)OriginalIntent.
*/
@Deprecated
public class ComponentAliasResolver {
private static final String TAG = "ComponentAliasResolver";
private static final boolean DEBUG = true;
/**
* This flag has to be enabled for the "android" package to use component aliases.
*/
@ChangeId
@Disabled
public static final long USE_EXPERIMENTAL_COMPONENT_ALIAS = 196254758L;
private final Object mLock = new Object();
private final ActivityManagerService mAm;
private final Context mContext;
@GuardedBy("mLock")
private boolean mEnabledByDeviceConfig;
@GuardedBy("mLock")
private boolean mEnabled;
@GuardedBy("mLock")
private String mOverrideString;
@GuardedBy("mLock")
private final ArrayMap<ComponentName, ComponentName> mFromTo = new ArrayMap<>();
@GuardedBy("mLock")
private PlatformCompat mPlatformCompat;
private static final String OPT_IN_PROPERTY = "com.android.EXPERIMENTAL_COMPONENT_ALIAS_OPT_IN";
private static final String ALIAS_FILTER_ACTION =
"com.android.intent.action.EXPERIMENTAL_IS_ALIAS";
private static final String ALIAS_FILTER_ACTION_ALT =
"android.intent.action.EXPERIMENTAL_IS_ALIAS";
private static final String META_DATA_ALIAS_TARGET = "alias_target";
private static final int PACKAGE_QUERY_FLAGS =
PackageManager.MATCH_UNINSTALLED_PACKAGES
| PackageManager.MATCH_ANY_USER
| PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE
| PackageManager.GET_META_DATA;
public ComponentAliasResolver(ActivityManagerService service) {
mAm = service;
mContext = service.mContext;
}
public boolean isEnabled() {
synchronized (mLock) {
return mEnabled;
}
}
/**
* When there's any change to packages, we refresh all the aliases.
* TODO: In the production version, we should update only the changed package.
*/
final PackageMonitor mPackageMonitor = new PackageMonitor() {
@Override
public void onPackageModified(String packageName) {
refresh();
}
@Override
public void onPackageAdded(String packageName, int uid) {
refresh();
}
@Override
public void onPackageRemoved(String packageName, int uid) {
refresh();
}
};
/**
* Call this on systemRead().
*/
public void onSystemReady(boolean enabledByDeviceConfig, String overrides) {
synchronized (mLock) {
mPlatformCompat = (PlatformCompat) ServiceManager.getService(
Context.PLATFORM_COMPAT_SERVICE);
}
if (DEBUG) Slog.d(TAG, "Compat listener set.");
update(enabledByDeviceConfig, overrides);
}
/**
* (Re-)loads aliases from <meta-data> and the device config override.
*/
public void update(boolean enabledByDeviceConfig, String overrides) {
synchronized (mLock) {
if (mPlatformCompat == null) {
return; // System not ready.
}
// Never enable it.
final boolean enabled = false;
if (enabled != mEnabled) {
Slog.i(TAG, (enabled ? "Enabling" : "Disabling") + " component aliases...");
FgThread.getHandler().post(() -> {
// Registering/unregistering a receiver internally takes the AM lock, but AM
// calls into this class while holding the AM lock. So do it on a handler to
// avoid deadlocks.
if (enabled) {
mPackageMonitor.register(mAm.mContext, UserHandle.ALL,
BackgroundThread.getHandler());
} else {
mPackageMonitor.unregister();
}
});
}
mEnabled = enabled;
mEnabledByDeviceConfig = enabledByDeviceConfig;
mOverrideString = overrides;
if (mEnabled) {
refreshLocked();
} else {
mFromTo.clear();
}
}
}
private void refresh() {
synchronized (mLock) {
update(mEnabledByDeviceConfig, mOverrideString);
}
}
@GuardedBy("mLock")
private void refreshLocked() {
if (DEBUG) Slog.d(TAG, "Refreshing aliases...");
mFromTo.clear();
loadFromMetadataLocked();
loadOverridesLocked();
}
/**
* Scans all the "alias" components and inserts the from-to pairs to the map.
*/
@GuardedBy("mLock")
private void loadFromMetadataLocked() {
if (DEBUG) Slog.d(TAG, "Scanning service aliases...");
// PM.queryInetntXxx() doesn't support "OR" queries, so we search for
// both the com.android... action and android... action on by one.
// It's okay if a single component handles both actions because the resulting aliases
// will be stored in a map and duplicates will naturally be removed.
loadFromMetadataLockedInner(new Intent(ALIAS_FILTER_ACTION_ALT));
loadFromMetadataLockedInner(new Intent(ALIAS_FILTER_ACTION));
}
private void loadFromMetadataLockedInner(Intent i) {
final List<ResolveInfo> services = mContext.getPackageManager().queryIntentServicesAsUser(
i, PACKAGE_QUERY_FLAGS, UserHandle.USER_SYSTEM);
extractAliasesLocked(services);
if (DEBUG) Slog.d(TAG, "Scanning receiver aliases...");
final List<ResolveInfo> receivers = mContext.getPackageManager()
.queryBroadcastReceiversAsUser(i, PACKAGE_QUERY_FLAGS, UserHandle.USER_SYSTEM);
extractAliasesLocked(receivers);
// TODO: Scan for other component types as well.
}
/**
* Make sure a given package is opted into component alias, by having a
* "com.android.EXPERIMENTAL_COMPONENT_ALIAS_OPT_IN" property set to true in the manifest.
*
* The implementation isn't optimized -- in every call we scan the package's properties,
* even thought we're likely going to call it with the same packages multiple times.
* But that's okay since this feature is experimental, and this code path won't be called
* until explicitly enabled.
*/
@GuardedBy("mLock")
private boolean isEnabledForPackageLocked(String packageName) {
boolean enabled = false;
try {
final Property p = mContext.getPackageManager().getProperty(
OPT_IN_PROPERTY, packageName);
enabled = p.getBoolean();
} catch (NameNotFoundException e) {
}
if (!enabled) {
Slog.w(TAG, "USE_EXPERIMENTAL_COMPONENT_ALIAS not enabled for " + packageName);
}
return enabled;
}
/**
* Make sure an alias and its target are the same package, or, the target is in a "sub" package.
*/
private static boolean validateAlias(ComponentName from, ComponentName to) {
final String fromPackage = from.getPackageName();
final String toPackage = to.getPackageName();
if (Objects.equals(fromPackage, toPackage)) { // Same package?
return true;
}
if (toPackage.startsWith(fromPackage + ".")) { // Prefix?
return true;
}
Slog.w(TAG, "Invalid alias: "
+ from.flattenToShortString() + " -> " + to.flattenToShortString());
return false;
}
@GuardedBy("mLock")
private void validateAndAddAliasLocked(ComponentName from, ComponentName to) {
if (DEBUG) {
Slog.d(TAG,
"" + from.flattenToShortString() + " -> " + to.flattenToShortString());
}
if (!validateAlias(from, to)) {
return;
}
// Make sure both packages have
if (!isEnabledForPackageLocked(from.getPackageName())
|| !isEnabledForPackageLocked(to.getPackageName())) {
return;
}
mFromTo.put(from, to);
}
@GuardedBy("mLock")
private void extractAliasesLocked(List<ResolveInfo> components) {
for (ResolveInfo ri : components) {
final ComponentInfo ci = ri.getComponentInfo();
final ComponentName from = ci.getComponentName();
final ComponentName to = unflatten(ci.metaData.getString(META_DATA_ALIAS_TARGET));
if (to == null) {
continue;
}
validateAndAddAliasLocked(from, to);
}
}
/**
* Parses an "override" string and inserts the from-to pairs to the map.
*
* The format is:
* ALIAS-COMPONENT-1 ":" TARGET-COMPONENT-1 ( "," ALIAS-COMPONENT-2 ":" TARGET-COMPONENT-2 )*
*/
@GuardedBy("mLock")
private void loadOverridesLocked() {
if (DEBUG) Slog.d(TAG, "Loading aliases overrides ...");
for (String line : mOverrideString.split("\\,+")) {
final String[] fields = line.split("\\:+", 2);
if (TextUtils.isEmpty(fields[0])) {
continue;
}
final ComponentName from = unflatten(fields[0]);
if (from == null) {
continue;
}
if (fields.length == 1) {
if (DEBUG) Slog.d(TAG, "" + from.flattenToShortString() + " [removed]");
mFromTo.remove(from);
} else {
final ComponentName to = unflatten(fields[1]);
if (to == null) {
continue;
}
validateAndAddAliasLocked(from, to);
}
}
}
private static ComponentName unflatten(String name) {
final ComponentName cn = ComponentName.unflattenFromString(name);
if (cn != null) {
return cn;
}
Slog.e(TAG, "Invalid component name detected: " + name);
return null;
}
/**
* Dump the aliases for dumpsys / bugrports.
*/
public void dump(PrintWriter pw) {
synchronized (mLock) {
pw.println("ACTIVITY MANAGER COMPONENT-ALIAS (dumpsys activity component-alias)");
pw.print(" Enabled: "); pw.println(mEnabled);
pw.println(" Aliases:");
for (int i = 0; i < mFromTo.size(); i++) {
ComponentName from = mFromTo.keyAt(i);
ComponentName to = mFromTo.valueAt(i);
pw.print(" ");
pw.print(from.flattenToShortString());
pw.print(" -> ");
pw.print(to.flattenToShortString());
pw.println();
}
pw.println();
}
}
/**
* Contains alias resolution information.
*/
public static class Resolution<T> {
/** "From" component. Null if component alias is disabled. */
@Nullable
public final T source;
/** "To" component. Null if component alias is disabled, or the source isn't an alias. */
@Nullable
public final T resolved;
public Resolution(T source, T resolved) {
this.source = source;
this.resolved = resolved;
}
@Nullable
public boolean isAlias() {
return this.resolved != null;
}
@Nullable
public T getAlias() {
return isAlias() ? source : null;
}
@Nullable
public T getTarget() {
return isAlias() ? resolved : null;
}
}
@NonNull
public Resolution<ComponentName> resolveComponentAlias(
@NonNull Supplier<ComponentName> aliasSupplier) {
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
if (!mEnabled) {
return new Resolution<>(null, null);
}
final ComponentName alias = aliasSupplier.get();
final ComponentName target = mFromTo.get(alias);
if (target != null) {
if (DEBUG) {
Exception stacktrace = null;
if (Log.isLoggable(TAG, Log.VERBOSE)) {
stacktrace = new RuntimeException("STACKTRACE");
}
Slog.d(TAG, "Alias resolved: " + alias.flattenToShortString()
+ " -> " + target.flattenToShortString(), stacktrace);
}
}
return new Resolution<>(alias, target);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Nullable
public Resolution<ComponentName> resolveService(
@NonNull Intent service, @Nullable String resolvedType,
int packageFlags, int userId, int callingUid) {
Resolution<ComponentName> result = resolveComponentAlias(() -> {
PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
ResolveInfo rInfo = pmi.resolveService(service,
resolvedType, packageFlags, userId, callingUid);
ServiceInfo sInfo = rInfo != null ? rInfo.serviceInfo : null;
if (sInfo == null) {
return null; // Service not found.
}
return new ComponentName(sInfo.applicationInfo.packageName, sInfo.name);
});
// TODO: To make it consistent with resolveReceiver(), let's ensure the target service
// is resolvable, and if not, return null.
if (result != null && result.isAlias()) {
// It's an alias. Keep the original intent, and rewrite it.
service.setOriginalIntent(new Intent(service));
service.setPackage(null);
service.setComponent(result.getTarget());
}
return result;
}
@Nullable
public Resolution<ResolveInfo> resolveReceiver(@NonNull Intent intent,
@NonNull ResolveInfo receiver, @Nullable String resolvedType,
long packageFlags, int userId, int callingUid, boolean forSend) {
// Resolve this alias.
final Resolution<ComponentName> resolution = resolveComponentAlias(() ->
receiver.activityInfo.getComponentName());
final ComponentName target = resolution.getTarget();
if (target == null) {
return new Resolution<>(receiver, null); // It's not an alias.
}
// Convert the target component name to a ResolveInfo.
final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
// Rewrite the intent to search the target intent.
// - We don't actually rewrite the intent we deliver to the receiver here, which is what
// resolveService() does, because this intent many be send to other receivers as well.
// - But we don't have to do that here either, because the actual receiver component
// will be set in BroadcastQueue anyway, before delivering the intent to each receiver.
// - However, we're not able to set the original intent either, for the time being.
Intent i = new Intent(intent);
i.setPackage(null);
i.setComponent(resolution.getTarget());
List<ResolveInfo> resolved = pmi.queryIntentReceivers(
i, resolvedType, packageFlags, callingUid, userId, forSend);
if (resolved == null || resolved.size() == 0) {
// Target component not found.
Slog.w(TAG, "Alias target " + target.flattenToShortString() + " not found");
return null;
}
return new Resolution<>(receiver, resolved.get(0));
}
}