Component Alias initial (rudimentary) prototype

See go/component-alias-prototype-overview for what it does.

This is disabled by default. Enable it via device config.

Test: atest ComponentAliasTests
Bug: 196254758
Change-Id: I2d177f6a475d878abbde55c3642c09da99dfea30
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 846349d..ce9c2ef 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -6993,6 +6993,7 @@
     private int mContentUserHint = UserHandle.USER_CURRENT;
     /** Token to track instant app launches. Local only; do not copy cross-process. */
     private String mLaunchToken;
+    private Intent mOriginalIntent; // Used for the experimental "component alias" feature.
 
     // ---------------------------------------------------------------------
 
@@ -7029,6 +7030,7 @@
         this.mIdentifier = o.mIdentifier;
         this.mPackage = o.mPackage;
         this.mComponent = o.mComponent;
+        this.mOriginalIntent = o.mOriginalIntent;
 
         if (o.mCategories != null) {
             this.mCategories = new ArraySet<>(o.mCategories);
@@ -8193,6 +8195,22 @@
         return mType;
     }
 
+
+    /**
+     * @hide For the experimental component alias feature. Do not use, unless you know what it is.
+     */
+    @Nullable
+    public Intent getOriginalIntent() {
+        return mOriginalIntent;
+    }
+
+    /**
+     * @hide For the experimental component alias feature. Do not use, unless you know what it is.
+     */
+    public void setOriginalIntent(@Nullable Intent originalIntent) {
+        mOriginalIntent = originalIntent;
+    }
+
     /**
      * Return the MIME data type of this intent.  If the type field is
      * explicitly set, that is simply returned.  Otherwise, if the data is set,
@@ -10838,6 +10856,11 @@
             mSelector.toShortString(b, secure, comp, extras, clip);
             b.append("}");
         }
+        if (mOriginalIntent != null) {
+            b.append(" org={");
+            mOriginalIntent.toShortString(b, secure, comp, extras, clip);
+            b.append("}");
+        }
     }
 
     /** @hide */
@@ -11133,6 +11156,13 @@
         }
         out.writeInt(mContentUserHint);
         out.writeBundle(mExtras);
+
+        if (mOriginalIntent != null) {
+            out.writeInt(1);
+            mOriginalIntent.writeToParcel(out, flags);
+        } else {
+            out.writeInt(0);
+        }
     }
 
     public static final @android.annotation.NonNull Parcelable.Creator<Intent> CREATOR
@@ -11186,6 +11216,9 @@
         }
         mContentUserHint = in.readInt();
         mExtras = in.readBundle();
+        if (in.readInt() != 0) {
+            mOriginalIntent = new Intent(in);
+        }
     }
 
     /**
@@ -11410,6 +11443,9 @@
         if (mClipData != null) {
             mClipData.prepareToLeaveProcess(leavingPackage, getFlags());
         }
+        if (mOriginalIntent != null) {
+            mOriginalIntent.prepareToLeaveProcess(leavingPackage);
+        }
 
         if (mExtras != null && !mExtras.isParcelled()) {
             final Object intent = mExtras.get(Intent.EXTRA_INTENT);
@@ -11505,6 +11541,9 @@
         if (mClipData != null) {
             mClipData.prepareToEnterProcess(source);
         }
+        if (mOriginalIntent != null) {
+            mOriginalIntent.prepareToEnterProcess(false, source);
+        }
 
         if (mContentUserHint != UserHandle.USER_CURRENT) {
             if (UserHandle.getAppId(Process.myUid()) != Process.SYSTEM_UID) {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index e4c0765..49722d0 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -816,8 +816,19 @@
             return null;
         }
 
-        return startServiceInnerLocked(r, service, callingUid, callingPid, fgRequired, callerFg,
+        // If what the client try to start/connect was an alias, then we need to return the
+        // alias component name to the client, not the "target" component name, which is
+        // what realResult contains.
+        final ComponentName realResult =
+                startServiceInnerLocked(r, service, callingUid, callingPid, fgRequired, callerFg,
                 allowBackgroundActivityStarts, backgroundActivityStartsToken);
+        if (res.aliasComponent != null
+                && !realResult.getPackageName().startsWith("!")
+                && !realResult.getPackageName().startsWith("?")) {
+            return res.aliasComponent;
+        } else {
+            return realResult;
+        }
     }
 
     private ComponentName startServiceInnerLocked(ServiceRecord r, Intent service,
@@ -2838,7 +2849,7 @@
             AppBindRecord b = s.retrieveAppBindingLocked(service, callerApp);
             ConnectionRecord c = new ConnectionRecord(b, activity,
                     connection, flags, clientLabel, clientIntent,
-                    callerApp.uid, callerApp.processName, callingPackage);
+                    callerApp.uid, callerApp.processName, callingPackage, res.aliasComponent);
 
             IBinder binder = connection.asBinder();
             s.addConnection(binder, c);
@@ -2915,8 +2926,13 @@
             if (s.app != null && b.intent.received) {
                 // Service is already running, so we can immediately
                 // publish the connection.
+
+                // If what the client try to start/connect was an alias, then we need to
+                // pass the alias component name instead to the client.
+                final ComponentName clientSideComponentName =
+                        res.aliasComponent != null ? res.aliasComponent : s.name;
                 try {
-                    c.conn.connected(s.name, b.intent.binder, false);
+                    c.conn.connected(clientSideComponentName, b.intent.binder, false);
                 } catch (Exception e) {
                     Slog.w(TAG, "Failure sending service " + s.shortInstanceName
                             + " to connection " + c.conn.asBinder()
@@ -2987,8 +3003,12 @@
                                 continue;
                             }
                             if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Publishing to: " + c);
+                            // If what the client try to start/connect was an alias, then we need to
+                            // pass the alias component name instead to the client.
+                            final ComponentName clientSideComponentName =
+                                    c.aliasComponent != null ? c.aliasComponent : r.name;
                             try {
-                                c.conn.connected(r.name, service, false);
+                                c.conn.connected(clientSideComponentName, service, false);
                             } catch (Exception e) {
                                 Slog.w(TAG, "Failure sending service " + r.shortInstanceName
                                       + " to connection " + c.conn.asBinder()
@@ -3140,9 +3160,22 @@
         final ServiceRecord record;
         final String permission;
 
-        ServiceLookupResult(ServiceRecord _record, String _permission) {
+        /**
+         * Set only when we looked up to this service via an alias. Otherwise, it's null.
+         */
+        @Nullable
+        final ComponentName aliasComponent;
+
+        ServiceLookupResult(ServiceRecord _record, ComponentName _aliasComponent) {
             record = _record;
+            permission = null;
+            aliasComponent = _aliasComponent;
+        }
+
+        ServiceLookupResult(String _permission) {
+            record = null;
             permission = _permission;
+            aliasComponent = null;
         }
     }
 
@@ -3174,10 +3207,19 @@
                 /* name= */ "service", callingPackage);
 
         ServiceMap smap = getServiceMapLocked(userId);
+
+        // See if the intent refers to an alias. If so, update the intent with the target component
+        // name. `resolution` will contain the alias component name, which we need to return
+        // to the client.
+        final ComponentAliasResolver.Resolution resolution =
+                mAm.mComponentAliasResolver.resolveService(service, resolvedType,
+                        /* match flags */ 0, userId, callingUid);
+
         final ComponentName comp;
         if (instanceName == null) {
             comp = service.getComponent();
         } else {
+            // This is for isolated services
             final ComponentName realComp = service.getComponent();
             if (realComp == null) {
                 throw new IllegalArgumentException("Can't use custom instance name '" + instanceName
@@ -3186,6 +3228,7 @@
             comp = new ComponentName(realComp.getPackageName(),
                     realComp.getClassName() + ":" + instanceName);
         }
+
         if (comp != null) {
             r = smap.mServicesByInstanceName.get(comp);
             if (DEBUG_SERVICE && r != null) Slog.v(TAG_SERVICE, "Retrieved by component: " + r);
@@ -3243,7 +3286,7 @@
                     String msg = "association not allowed between packages "
                             + callingPackage + " and " + name.getPackageName();
                     Slog.w(TAG, "Service lookup failed: " + msg);
-                    return new ServiceLookupResult(null, msg);
+                    return new ServiceLookupResult(msg);
                 }
 
                 // Store the defining packageName and uid, as they might be changed in
@@ -3361,11 +3404,11 @@
                 String msg = "association not allowed between packages "
                         + callingPackage + " and " + r.packageName;
                 Slog.w(TAG, "Service lookup failed: " + msg);
-                return new ServiceLookupResult(null, msg);
+                return new ServiceLookupResult(msg);
             }
             if (!mAm.mIntentFirewall.checkService(r.name, service, callingUid, callingPid,
                     resolvedType, r.appInfo)) {
-                return new ServiceLookupResult(null, "blocked by firewall");
+                return new ServiceLookupResult("blocked by firewall");
             }
             if (mAm.checkComponentPermission(r.permission,
                     callingPid, callingUid, r.appInfo.uid, r.exported) != PERMISSION_GRANTED) {
@@ -3374,14 +3417,14 @@
                             + " from pid=" + callingPid
                             + ", uid=" + callingUid
                             + " that is not exported from uid " + r.appInfo.uid);
-                    return new ServiceLookupResult(null, "not exported from uid "
+                    return new ServiceLookupResult("not exported from uid "
                             + r.appInfo.uid);
                 }
                 Slog.w(TAG, "Permission Denial: Accessing service " + r.shortInstanceName
                         + " from pid=" + callingPid
                         + ", uid=" + callingUid
                         + " requires " + r.permission);
-                return new ServiceLookupResult(null, r.permission);
+                return new ServiceLookupResult(r.permission);
             } else if (Manifest.permission.BIND_HOTWORD_DETECTION_SERVICE.equals(r.permission)
                     && callingUid != Process.SYSTEM_UID) {
                 // Hotword detection must run in its own sandbox, and we don't even trust
@@ -3392,7 +3435,7 @@
                         + ", uid=" + callingUid
                         + " requiring permission " + r.permission
                         + " can only be bound to from the system.");
-                return new ServiceLookupResult(null, "can only be bound to "
+                return new ServiceLookupResult("can only be bound to "
                         + "by the system.");
             } else if (r.permission != null && callingPackage != null) {
                 final int opCode = AppOpsManager.permissionToOpCode(r.permission);
@@ -3405,7 +3448,7 @@
                     return null;
                 }
             }
-            return new ServiceLookupResult(r, null);
+            return new ServiceLookupResult(r, resolution.getAliasComponent());
         }
         return null;
     }
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 048a787..ac537fa 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -123,6 +123,8 @@
     static final String KEY_KILL_BG_RESTRICTED_CACHED_IDLE = "kill_bg_restricted_cached_idle";
     static final String KEY_KILL_BG_RESTRICTED_CACHED_IDLE_SETTLE_TIME =
             "kill_bg_restricted_cached_idle_settle_time";
+    static final String KEY_ENABLE_COMPONENT_ALIAS = "enable_experimental_component_alias";
+    static final String KEY_COMPONENT_ALIAS_OVERRIDES = "component_alias_overrides";
 
     private static final int DEFAULT_MAX_CACHED_PROCESSES = 32;
     private static final long DEFAULT_FGSERVICE_MIN_SHOWN_TIME = 2*1000;
@@ -197,6 +199,8 @@
      * Whether or not to enable the extra delays to service restarts on memory pressure.
      */
     private static final boolean DEFAULT_ENABLE_EXTRA_SERVICE_RESTART_DELAY_ON_MEM_PRESSURE = true;
+    private static final boolean DEFAULT_ENABLE_COMPONENT_ALIAS = false;
+    private static final String DEFAULT_COMPONENT_ALIAS_OVERRIDES = "";
 
     // Flag stored in the DeviceConfig API.
     /**
@@ -580,6 +584,14 @@
     @GuardedBy("mService")
     boolean mEnableExtraServiceRestartDelayOnMemPressure =
             DEFAULT_ENABLE_EXTRA_SERVICE_RESTART_DELAY_ON_MEM_PRESSURE;
+    /** Whether to enable "component alias" experimental feature. */
+    volatile boolean mEnableComponentAlias = DEFAULT_ENABLE_COMPONENT_ALIAS;
+
+    /**
+     * Defines component aliases. Format
+     * ComponentName ":" ComponentName ( "," ComponentName ":" ComponentName )*
+     */
+    volatile String mComponentAliasOverrides = DEFAULT_COMPONENT_ALIAS_OVERRIDES;
 
     private final ActivityManagerService mService;
     private ContentResolver mResolver;
@@ -812,6 +824,10 @@
                             case KEY_ENABLE_EXTRA_SERVICE_RESTART_DELAY_ON_MEM_PRESSURE:
                                 updateEnableExtraServiceRestartDelayOnMemPressure();
                                 break;
+                            case KEY_ENABLE_COMPONENT_ALIAS:
+                            case KEY_COMPONENT_ALIAS_OVERRIDES:
+                                updateComponentAliases();
+                                break;
                             default:
                                 break;
                         }
@@ -1243,6 +1259,18 @@
         return def;
     }
 
+    private void updateComponentAliases() {
+        mEnableComponentAlias = DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                KEY_ENABLE_COMPONENT_ALIAS,
+                DEFAULT_ENABLE_COMPONENT_ALIAS);
+        mComponentAliasOverrides = DeviceConfig.getString(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                KEY_COMPONENT_ALIAS_OVERRIDES,
+                DEFAULT_COMPONENT_ALIAS_OVERRIDES);
+        mService.mComponentAliasResolver.update(mEnableComponentAlias, mComponentAliasOverrides);
+    }
+
     private void updateImperceptibleKillExemptions() {
         IMPERCEPTIBLE_KILL_EXEMPT_PACKAGES.clear();
         IMPERCEPTIBLE_KILL_EXEMPT_PACKAGES.addAll(mDefaultImperceptibleKillExemptPackages);
@@ -1472,6 +1500,10 @@
         pw.print("="); pw.println(mPushMessagingOverQuotaBehavior);
         pw.print("  "); pw.print(KEY_FGS_ALLOW_OPT_OUT);
         pw.print("="); pw.println(mFgsAllowOptOut);
+        pw.print("  "); pw.print(KEY_ENABLE_COMPONENT_ALIAS);
+        pw.print("="); pw.println(mEnableComponentAlias);
+        pw.print("  "); pw.print(KEY_COMPONENT_ALIAS_OVERRIDES);
+        pw.print("="); pw.println(mComponentAliasOverrides);
 
         pw.println();
         if (mOverrideMaxCachedProcesses >= 0) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 7fc35d4..b202c23 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -779,6 +779,9 @@
     @GuardedBy("this")
     private ArrayMap<String, PackageAssociationInfo> mAllowedAssociations;
 
+    @GuardedBy("this")
+    final ComponentAliasResolver mComponentAliasResolver;
+
     /**
      * Tracks association information for a particular package along with debuggability.
      * <p> Associations for a package A are allowed to package B if B is part of the
@@ -2220,6 +2223,7 @@
         mUseFifoUiScheduling = false;
         mEnableOffloadQueue = false;
         mFgBroadcastQueue = mBgBroadcastQueue = mOffloadBroadcastQueue = null;
+        mComponentAliasResolver = new ComponentAliasResolver(this);
     }
 
     // Note: This method is invoked on the main thread but may need to attach various
@@ -2346,6 +2350,7 @@
         mInternal = new LocalService();
         mPendingStartActivityUids = new PendingStartActivityUids(mContext);
         mTraceErrorLogger = new TraceErrorLogger();
+        mComponentAliasResolver = new ComponentAliasResolver(this);
     }
 
     public void setSystemServiceManager(SystemServiceManager mgr) {
@@ -4784,6 +4789,10 @@
         showConsoleNotificationIfActive();
 
         t.traceEnd();
+
+        // Load the component aliases.
+        mComponentAliasResolver.update(
+                mConstants.mEnableComponentAlias, mConstants.mComponentAliasOverrides);
     }
 
     private void showConsoleNotificationIfActive() {
@@ -8791,6 +8800,12 @@
                 pw.println("-------------------------------------------------------------------------------");
             }
             dumpUsers(pw);
+
+            pw.println();
+            if (dumpAll) {
+                pw.println("-------------------------------------------------------------------------------");
+            }
+            mComponentAliasResolver.dump(pw);
         }
     }
 
@@ -9107,6 +9122,8 @@
                     opti++;
                 }
                 mProcessList.mAppExitInfoTracker.dumpHistoryProcessExitInfo(pw, dumpPackage);
+            } else if ("component-alias".equals(cmd)) {
+                mComponentAliasResolver.dump(pw);
             } else {
                 // Dumping a single activity?
                 if (!mAtmInternal.dumpActivity(fd, pw, cmd, args, opti, dumpAll,
diff --git a/services/core/java/com/android/server/am/ComponentAliasResolver.java b/services/core/java/com/android/server/am/ComponentAliasResolver.java
new file mode 100644
index 0000000..6ed9447
--- /dev/null
+++ b/services/core/java/com/android/server/am/ComponentAliasResolver.java
@@ -0,0 +1,326 @@
+/*
+ * 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.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ComponentInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Binder;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+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.LocalServices;
+
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Manages and handles component aliases, which is an experimental feature.
+ *
+ * For now, this is an experimental feature to evaluate feasibility, so the implementation is
+ * "quick & dirty". For example, to define aliases, we use a regular intent filter and meta-data
+ * in the manifest, instead of adding proper tags/attributes to AndroidManifest.xml.
+ *
+ * Also, for now, aliases can be defined across any packages, but in the final version, there'll
+ * be restrictions:
+ * - We probably should only allow either privileged or preinstalled apps.
+ * - Aliases can only be defined across packages that are atomically installed, and signed with the
+ *   same key.
+ */
+public class ComponentAliasResolver {
+    private static final String TAG = "ComponentAliasResolver";
+    private static final boolean DEBUG = true;
+
+    private final Object mLock = new Object();
+    private final ActivityManagerService mAm;
+    private final Context mContext;
+
+    @GuardedBy("mLock")
+    private boolean mEnabled;
+
+    @GuardedBy("mLock")
+    private String mOverrideString;
+
+    @GuardedBy("mLock")
+    private final ArrayMap<ComponentName, ComponentName> mFromTo = new ArrayMap<>();
+
+    private static final String ALIAS_FILTER_ACTION = "android.intent.action.EXPERIMENTAL_IS_ALIAS";
+    private static final String META_DATA_ALIAS_TARGET = "alias_target";
+
+    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();
+        }
+    };
+
+    /**
+     * (Re-)loads aliases from <meta-data> and the device config override.
+     */
+    public void update(boolean enabled, String overrides) {
+        synchronized (mLock) {
+            if (enabled == mEnabled && Objects.equals(overrides, mOverrideString)) {
+                return;
+            }
+            if (enabled != mEnabled) {
+                Slog.i(TAG, (enabled ? "Enabling" : "Disabling") + " component aliases...");
+                if (enabled) {
+                    mPackageMonitor.register(mAm.mContext, UserHandle.ALL,
+                            /* externalStorage= */ false, BackgroundThread.getHandler());
+                } else {
+                    mPackageMonitor.unregister();
+                }
+            }
+            mEnabled = enabled;
+            mOverrideString = overrides;
+
+            if (mEnabled) {
+                refreshLocked();
+            } else {
+                mFromTo.clear();
+            }
+        }
+    }
+
+    private void refresh() {
+        synchronized (mLock) {
+            refreshLocked();
+        }
+    }
+
+    @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 aliases...");
+        Intent i = new Intent(ALIAS_FILTER_ACTION);
+
+        List<ResolveInfo> services = mContext.getPackageManager().queryIntentServicesAsUser(
+                i,
+                PackageManager.MATCH_UNINSTALLED_PACKAGES
+                        | PackageManager.MATCH_ANY_USER
+                        | PackageManager.MATCH_DIRECT_BOOT_AWARE
+                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+                        | PackageManager.GET_META_DATA,
+                UserHandle.USER_SYSTEM);
+
+        for (ResolveInfo ri : services) {
+            final ComponentInfo ci = ri.getComponentInfo();
+            final ComponentName from = ci.getComponentName();
+            final ComponentName to = ComponentName.unflattenFromString(
+                    ci.metaData.getString(META_DATA_ALIAS_TARGET));
+            if (!validateComponentName(to)) {
+                continue;
+            }
+            if (DEBUG) {
+                Slog.d(TAG, "" + from.flattenToShortString() + " -> " + to.flattenToShortString());
+            }
+            mFromTo.put(from, to);
+        }
+
+        // TODO: Scan for other component types as well.
+    }
+
+    /**
+     * 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);
+            final ComponentName from = ComponentName.unflattenFromString(fields[0]);
+            if (!validateComponentName(from)) {
+                continue;
+            }
+
+            if (fields.length == 1) {
+                if (DEBUG) Slog.d(TAG, "" + from.flattenToShortString() + " [removed]");
+                mFromTo.remove(from);
+            } else {
+                final ComponentName to = ComponentName.unflattenFromString(fields[1]);
+                if (!validateComponentName(to)) {
+                    continue;
+                }
+
+                if (DEBUG) {
+                    Slog.d(TAG,
+                            "" + from.flattenToShortString() + " -> " + to.flattenToShortString());
+                }
+                mFromTo.put(from, to);
+            }
+        }
+    }
+
+    private boolean validateComponentName(ComponentName cn) {
+        if (cn != null) {
+            return true;
+        }
+        Slog.e(TAG, "Invalid component name detected: " + cn);
+        return false;
+    }
+
+    /**
+     * 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 {
+        @NonNull
+        public final Intent sourceIntent;
+
+        /** "From" component. Null if component alias is disabled. */
+        @Nullable
+        public final ComponentName sourceComponent;
+
+        /** "To" component. Null if component alias is disabled, or the source isn't an alias. */
+        @Nullable
+        public final ComponentName resolvedComponent;
+
+        public Resolution(Intent sourceIntent,
+                ComponentName sourceComponent, ComponentName resolvedComponent) {
+            this.sourceIntent = sourceIntent;
+            this.sourceComponent = sourceComponent;
+            this.resolvedComponent = resolvedComponent;
+        }
+
+        @Nullable
+        public boolean isAlias() {
+            return this.resolvedComponent != null;
+        }
+
+        @Nullable
+        public ComponentName getAliasComponent() {
+            return isAlias() ? sourceComponent : null;
+        }
+
+        @Nullable
+        public ComponentName getTargetComponent() {
+            return isAlias() ? resolvedComponent : null;
+        }
+    }
+
+    @Nullable
+    public Resolution resolveService(
+            @NonNull Intent service, @Nullable String resolvedType,
+            int packageFlags, int userId, int callingUid) {
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                if (!mEnabled) {
+                    return new Resolution(service, null, null);
+                }
+
+                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.
+                }
+                final ComponentName alias =
+                        new ComponentName(sInfo.applicationInfo.packageName, sInfo.name);
+                final ComponentName target = mFromTo.get(alias);
+
+                if (target != null) {
+                    // It's an alias. Keep the original intent, and rewrite it.
+                    service.setOriginalIntent(new Intent(service));
+
+                    service.setPackage(null);
+                    service.setComponent(target);
+
+                    if (DEBUG) {
+                        Slog.d(TAG, "Alias resolved: " + alias.flattenToShortString()
+                                + " -> " + target.flattenToShortString());
+                    }
+                }
+                return new Resolution(service, alias, target);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/am/ConnectionRecord.java b/services/core/java/com/android/server/am/ConnectionRecord.java
index 9161271..b434328 100644
--- a/services/core/java/com/android/server/am/ConnectionRecord.java
+++ b/services/core/java/com/android/server/am/ConnectionRecord.java
@@ -18,8 +18,10 @@
 
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
 
+import android.annotation.Nullable;
 import android.app.IServiceConnection;
 import android.app.PendingIntent;
+import android.content.ComponentName;
 import android.content.Context;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
@@ -48,6 +50,12 @@
     String stringName;              // Caching of toString.
     boolean serviceDead;            // Well is it?
     private Object mProcStatsLock;  // Internal lock for accessing AssociationState
+    /**
+     * If the connection was made against an alias, then the alias conponent name. Otherwise, null.
+     * We return this component name to the client.
+     */
+    @Nullable
+    final ComponentName aliasComponent;
 
     // Please keep the following two enum list synced.
     private static final int[] BIND_ORIG_ENUMS = new int[] {
@@ -102,7 +110,8 @@
             ActivityServiceConnectionsHolder<ConnectionRecord> _activity,
             IServiceConnection _conn, int _flags,
             int _clientLabel, PendingIntent _clientIntent,
-            int _clientUid, String _clientProcessName, String _clientPackageName) {
+            int _clientUid, String _clientProcessName, String _clientPackageName,
+            ComponentName _aliasComponent) {
         binding = _binding;
         activity = _activity;
         conn = _conn;
@@ -112,6 +121,7 @@
         clientUid = _clientUid;
         clientProcessName = _clientProcessName;
         clientPackageName = _clientPackageName;
+        aliasComponent = _aliasComponent;
     }
 
     public boolean hasFlag(final int flag) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 18f1267..284a754 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -2174,7 +2174,7 @@
         ConnectionRecord cr = spy(new ConnectionRecord(binding,
                 mock(ActivityServiceConnectionsHolder.class),
                 mock(IServiceConnection.class), bindFlags,
-                0, null, client.uid, client.processName, client.info.packageName));
+                0, null, client.uid, client.processName, client.info.packageName, null));
         doCallRealMethod().when(record).addConnection(any(IBinder.class),
                 any(ConnectionRecord.class));
         record.addConnection(binder, cr);
diff --git a/tests/componentalias/Android.bp b/tests/componentalias/Android.bp
new file mode 100644
index 0000000..15a680d
--- /dev/null
+++ b/tests/componentalias/Android.bp
@@ -0,0 +1,48 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "ComponentAliasTestsCommon",
+    srcs: [
+        "common/**/*.java",
+    ],
+    plugins: [
+        "staledataclass-annotation-processor",
+    ],
+    platform_apis: true, // We use hidden APIs in the test.
+}
+
+android_test {
+    name: "ComponentAliasTests",
+    static_libs: [
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "mockito-target-extended-minus-junit4",
+        "truth-prebuilt",
+        "ub-uiautomator",
+        "ComponentAliasTestsCommon",
+    ],
+    libs: ["android.test.base"],
+    srcs: [
+        "src/**/*.java",
+    ],
+    test_suites: [
+        "general-tests",
+    ],
+    platform_apis: true, // We use hidden APIs in the test.
+}
diff --git a/tests/componentalias/AndroidManifest.xml b/tests/componentalias/AndroidManifest.xml
new file mode 100755
index 0000000..893be8e
--- /dev/null
+++ b/tests/componentalias/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.content.componentalias.tests" >
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.content.componentalias.tests" >
+    </instrumentation>
+</manifest>
diff --git a/tests/componentalias/AndroidTest.xml b/tests/componentalias/AndroidTest.xml
new file mode 100644
index 0000000..e2c37d2
--- /dev/null
+++ b/tests/componentalias/AndroidTest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="ComponentAliasTests.apk" />
+        <option name="test-file-name" value="ComponentAliasAppMain.apk" />
+        <option name="test-file-name" value="ComponentAliasAppSub1.apk" />
+        <option name="test-file-name" value="ComponentAliasAppSub2.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <!-- Exempt the helper APKs from the BG restriction, so they can start BG services. -->
+        <option name="run-command" value="cmd deviceidle whitelist +android.content.componentalias.tests.app" />
+        <option name="run-command" value="cmd deviceidle whitelist +android.content.componentalias.tests.app.sub1" />
+        <option name="run-command" value="cmd deviceidle whitelist +android.content.componentalias.tests.app.sub2" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.content.componentalias.tests" />
+        <option name="runtime-hint" value="2m" />
+        <option name="isolated-storage" value="false" />
+    </test>
+</configuration>
diff --git a/tests/componentalias/OWNERS b/tests/componentalias/OWNERS
new file mode 100644
index 0000000..1073c81
--- /dev/null
+++ b/tests/componentalias/OWNERS
@@ -0,0 +1,2 @@
+omakoto@google.com
+yamasani@google.com
\ No newline at end of file
diff --git a/tests/componentalias/apps/Android.bp b/tests/componentalias/apps/Android.bp
new file mode 100644
index 0000000..b2cb0f7
--- /dev/null
+++ b/tests/componentalias/apps/Android.bp
@@ -0,0 +1,76 @@
+// 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.
+// Copyright (C) 2018 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// We build three helper APKs from the same source.
+// - The main APK (ComponentAliasAppMain), which contains both the alias and the target components.
+// - The sub APKs (ComponentAliasAppSub*), which only contains the target components.
+
+java_defaults {
+    name: "component_alias_app_defaults",
+    libs: ["android.test.base"],
+    sdk_version: "test_current",
+    srcs: [
+        "src/**/*.java",
+    ],
+    static_libs: [
+        "compatibility-device-util-axt",
+        "androidx.legacy_legacy-support-v4",
+        "androidx.test.rules",
+        "ComponentAliasTestsCommon",
+    ],
+}
+
+android_test_helper_app {
+    name: "ComponentAliasAppMain",
+    defaults: [
+        "component_alias_app_defaults",
+    ],
+    package_name: "android.content.componentalias.tests.app",
+    manifest: "AndroidManifest_main.xml",
+}
+
+android_test_helper_app {
+    name: "ComponentAliasAppSub1",
+    defaults: [
+        "component_alias_app_defaults",
+    ],
+    package_name: "android.content.componentalias.tests.app.sub1",
+    manifest: "AndroidManifest_sub.xml",
+}
+
+android_test_helper_app {
+    name: "ComponentAliasAppSub2",
+    defaults: [
+        "component_alias_app_defaults",
+    ],
+    package_name: "android.content.componentalias.tests.app.sub2",
+    manifest: "AndroidManifest_sub.xml",
+}
diff --git a/tests/componentalias/apps/AndroidManifest_main.xml b/tests/componentalias/apps/AndroidManifest_main.xml
new file mode 100755
index 0000000..2caef3e
--- /dev/null
+++ b/tests/componentalias/apps/AndroidManifest_main.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.content.componentalias.tests.app" >
+
+    <application>
+        <!--
+            Alias components.
+            Note aliases do not have the actual implementation, because they're never called
+            directly.
+        -->
+
+        <service android:name=".s.Alias01" android:exported="true" android:enabled="true" >
+            <meta-data android:name="alias_target" android:value="android.content.componentalias.tests.app.sub1/android.content.componentalias.tests.app.s.Target01" />
+            <intent-filter><action android:name="android.intent.action.EXPERIMENTAL_IS_ALIAS" /></intent-filter>
+            <intent-filter><action android:name="android.content.componentalias.tests.app.IS_ALIAS_01" /></intent-filter>
+        </service>
+        <service android:name=".s.Alias02" android:exported="true" android:enabled="true" >
+            <meta-data android:name="alias_target" android:value="android.content.componentalias.tests.app.sub2/android.content.componentalias.tests.app.s.Target02" />
+            <intent-filter><action android:name="android.intent.action.EXPERIMENTAL_IS_ALIAS" /></intent-filter>
+            <intent-filter><action android:name="android.content.componentalias.tests.app.IS_ALIAS_02" /></intent-filter>
+        </service>
+        <service android:name=".s.Alias03" android:exported="true" android:enabled="true" >
+            <meta-data android:name="alias_target" android:value="android.content.componentalias.tests.app.sub1/android.content.componentalias.tests.app.s.Target03" />
+            <intent-filter><action android:name="android.intent.action.EXPERIMENTAL_IS_ALIAS" /></intent-filter>
+            <intent-filter><action android:name="android.content.componentalias.tests.app.IS_ALIAS_03" /></intent-filter>
+        </service>
+        <service android:name=".s.Alias04" android:exported="true" android:enabled="true" >
+            <meta-data android:name="alias_target" android:value="android.content.componentalias.tests.app.sub2/android.content.componentalias.tests.app.s.Target04" />
+            <intent-filter><action android:name="android.intent.action.EXPERIMENTAL_IS_ALIAS" /></intent-filter>
+            <intent-filter><action android:name="android.content.componentalias.tests.app.IS_ALIAS_04" /></intent-filter>
+        </service>
+        <service android:name=".s.Alias05" android:exported="true" android:enabled="true" >
+            <meta-data android:name="alias_target" android:value="android.content.componentalias.tests.app.sub1/android.content.componentalias.tests.app.s.Target05" />
+            <intent-filter><action android:name="android.intent.action.EXPERIMENTAL_IS_ALIAS" /></intent-filter>
+            <intent-filter><action android:name="android.content.componentalias.tests.app.IS_ALIAS_05" /></intent-filter>
+        </service>
+
+        <!-- Target components -->
+
+        <service android:name=".s.Target01" android:exported="true" android:enabled="true" >
+        </service>
+        <service android:name=".s.Target02" android:exported="true" android:enabled="true" >
+        </service>
+        <service android:name=".s.Target03" android:exported="true" android:enabled="true" >
+        </service>
+        <service android:name=".s.Target04" android:exported="true" android:enabled="true" >
+        </service>
+        <service android:name=".s.Target05" android:exported="true" android:enabled="true" >
+        </service>
+    </application>
+</manifest>
diff --git a/tests/componentalias/apps/AndroidManifest_sub.xml b/tests/componentalias/apps/AndroidManifest_sub.xml
new file mode 100755
index 0000000..2ddd965
--- /dev/null
+++ b/tests/componentalias/apps/AndroidManifest_sub.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.content.componentalias.tests.app" >
+
+    <application>
+        <!-- Only contain the target components -->
+
+        <service android:name=".s.Target01" android:exported="true" android:enabled="true" >
+        </service>
+        <service android:name=".s.Target02" android:exported="true" android:enabled="true" >
+        </service>
+        <service android:name=".s.Target03" android:exported="true" android:enabled="true" >
+        </service>
+        <service android:name=".s.Target04" android:exported="true" android:enabled="true" >
+        </service>
+        <service android:name=".s.Target05" android:exported="true" android:enabled="true" >
+        </service>
+    </application>
+</manifest>
diff --git a/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/BaseService.java b/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/BaseService.java
new file mode 100644
index 0000000..fb67829
--- /dev/null
+++ b/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/BaseService.java
@@ -0,0 +1,71 @@
+/*
+ * 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 android.content.componentalias.tests.app.s;
+
+import static android.content.componentalias.tests.common.ComponentAliasTestCommon.TAG;
+import static android.content.componentalias.tests.common.ComponentAliasTestCommon.TEST_PACKAGE;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.componentalias.tests.common.ComponentAliasMessage;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.compatibility.common.util.BroadcastMessenger;
+
+public class BaseService extends Service {
+    private String getMyIdentity() {
+        return (new ComponentName(this.getPackageName(), this.getClass().getCanonicalName()))
+                .flattenToShortString();
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        Log.i(TAG, "onStartCommand: on " + getMyIdentity() + " intent=" + intent);
+        ComponentAliasMessage m = new ComponentAliasMessage()
+                .setSenderIdentity(getMyIdentity())
+                .setMethodName("onStartCommand")
+                .setIntent(intent);
+        BroadcastMessenger.send(this, TEST_PACKAGE, m);
+
+        return START_NOT_STICKY;
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.i(TAG, "onDestroy: on " + getMyIdentity());
+
+        ComponentAliasMessage m = new ComponentAliasMessage()
+                .setSenderIdentity(getMyIdentity())
+                .setMethodName("onDestroy");
+        BroadcastMessenger.send(this, TEST_PACKAGE, m);
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        Log.i(TAG, "onBind: on " + getMyIdentity() + " intent=" + intent);
+
+        ComponentAliasMessage m = new ComponentAliasMessage()
+                .setSenderIdentity(getMyIdentity())
+                .setMethodName("onBind")
+                .setIntent(intent);
+        BroadcastMessenger.send(this, TEST_PACKAGE, m);
+
+        return new Binder();
+    }
+}
diff --git a/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target01.java b/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target01.java
new file mode 100644
index 0000000..87e48cb
--- /dev/null
+++ b/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target01.java
@@ -0,0 +1,19 @@
+/*
+ * 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 android.content.componentalias.tests.app.s;
+
+public class Target01 extends BaseService {
+}
diff --git a/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target02.java b/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target02.java
new file mode 100644
index 0000000..0e8a6a8
--- /dev/null
+++ b/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target02.java
@@ -0,0 +1,19 @@
+/*
+ * 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 android.content.componentalias.tests.app.s;
+
+public class Target02 extends BaseService {
+}
diff --git a/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target03.java b/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target03.java
new file mode 100644
index 0000000..b7990bb
--- /dev/null
+++ b/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target03.java
@@ -0,0 +1,19 @@
+/*
+ * 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 android.content.componentalias.tests.app.s;
+
+public class Target03 extends BaseService {
+}
diff --git a/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target04.java b/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target04.java
new file mode 100644
index 0000000..0e51a6b
--- /dev/null
+++ b/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target04.java
@@ -0,0 +1,19 @@
+/*
+ * 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 android.content.componentalias.tests.app.s;
+
+public class Target04 extends BaseService {
+}
diff --git a/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target05.java b/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target05.java
new file mode 100644
index 0000000..77060cd
--- /dev/null
+++ b/tests/componentalias/apps/src/android/content/componentalias/tests/app/s/Target05.java
@@ -0,0 +1,19 @@
+/*
+ * 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 android.content.componentalias.tests.app.s;
+
+public class Target05 extends BaseService {
+}
diff --git a/tests/componentalias/common/android/content/componentalias/tests/common/ComponentAliasMessage.java b/tests/componentalias/common/android/content/componentalias/tests/common/ComponentAliasMessage.java
new file mode 100644
index 0000000..399c070
--- /dev/null
+++ b/tests/componentalias/common/android/content/componentalias/tests/common/ComponentAliasMessage.java
@@ -0,0 +1,215 @@
+/*
+ * 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 android.content.componentalias.tests.common;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+/**
+ * Parcelabe containing a "message" that's meant to be delivered via BroadcastMessenger.
+ *
+ * To add a new field, just add a private member field, and run:
+ * codegen $ANDROID_BUILD_TOP/frameworks/base/tests/componentalias/common/android/content/componentalias/tests/common/ComponentAliasMessage.java
+ */
+@DataClass(
+        genConstructor = false,
+        genSetters = true,
+        genToString = true,
+        genAidl = false)
+public final class ComponentAliasMessage implements Parcelable {
+    public ComponentAliasMessage() {
+    }
+
+    @Nullable
+    private String mMessage;
+
+    @Nullable
+    private String mMethodName;
+
+    @Nullable
+    private String mSenderIdentity;
+
+    @Nullable
+    private Intent mIntent;
+
+    @Nullable
+    private ComponentName mComponent;
+
+
+
+    // Code below generated by codegen v1.0.23.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen $ANDROID_BUILD_TOP/frameworks/base/tests/componentalias/common/android/content/componentalias/tests/common/ComponentAliasMessage.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    //@formatter:off
+
+
+    @DataClass.Generated.Member
+    public @Nullable String getMessage() {
+        return mMessage;
+    }
+
+    @DataClass.Generated.Member
+    public @Nullable String getMethodName() {
+        return mMethodName;
+    }
+
+    @DataClass.Generated.Member
+    public @Nullable String getSenderIdentity() {
+        return mSenderIdentity;
+    }
+
+    @DataClass.Generated.Member
+    public @Nullable Intent getIntent() {
+        return mIntent;
+    }
+
+    @DataClass.Generated.Member
+    public @Nullable ComponentName getComponent() {
+        return mComponent;
+    }
+
+    @DataClass.Generated.Member
+    public @NonNull ComponentAliasMessage setMessage(@NonNull String value) {
+        mMessage = value;
+        return this;
+    }
+
+    @DataClass.Generated.Member
+    public @NonNull ComponentAliasMessage setMethodName(@NonNull String value) {
+        mMethodName = value;
+        return this;
+    }
+
+    @DataClass.Generated.Member
+    public @NonNull ComponentAliasMessage setSenderIdentity(@NonNull String value) {
+        mSenderIdentity = value;
+        return this;
+    }
+
+    @DataClass.Generated.Member
+    public @NonNull ComponentAliasMessage setIntent(@NonNull Intent value) {
+        mIntent = value;
+        return this;
+    }
+
+    @DataClass.Generated.Member
+    public @NonNull ComponentAliasMessage setComponent(@NonNull ComponentName value) {
+        mComponent = value;
+        return this;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public String toString() {
+        // You can override field toString logic by defining methods like:
+        // String fieldNameToString() { ... }
+
+        return "ComponentAliasMessage { " +
+                "message = " + mMessage + ", " +
+                "methodName = " + mMethodName + ", " +
+                "senderIdentity = " + mSenderIdentity + ", " +
+                "intent = " + mIntent + ", " +
+                "component = " + mComponent +
+        " }";
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        // You can override field parcelling by defining methods like:
+        // void parcelFieldName(Parcel dest, int flags) { ... }
+
+        byte flg = 0;
+        if (mMessage != null) flg |= 0x1;
+        if (mMethodName != null) flg |= 0x2;
+        if (mSenderIdentity != null) flg |= 0x4;
+        if (mIntent != null) flg |= 0x8;
+        if (mComponent != null) flg |= 0x10;
+        dest.writeByte(flg);
+        if (mMessage != null) dest.writeString(mMessage);
+        if (mMethodName != null) dest.writeString(mMethodName);
+        if (mSenderIdentity != null) dest.writeString(mSenderIdentity);
+        if (mIntent != null) dest.writeTypedObject(mIntent, flags);
+        if (mComponent != null) dest.writeTypedObject(mComponent, flags);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int describeContents() { return 0; }
+
+    /** @hide */
+    @SuppressWarnings({"unchecked", "RedundantCast"})
+    @DataClass.Generated.Member
+    /* package-private */ ComponentAliasMessage(@NonNull Parcel in) {
+        // You can override field unparcelling by defining methods like:
+        // static FieldType unparcelFieldName(Parcel in) { ... }
+
+        byte flg = in.readByte();
+        String message = (flg & 0x1) == 0 ? null : in.readString();
+        String methodName = (flg & 0x2) == 0 ? null : in.readString();
+        String senderIdentity = (flg & 0x4) == 0 ? null : in.readString();
+        Intent intent = (flg & 0x8) == 0 ? null : (Intent) in.readTypedObject(Intent.CREATOR);
+        ComponentName component = (flg & 0x10) == 0 ? null : (ComponentName) in.readTypedObject(ComponentName.CREATOR);
+
+        this.mMessage = message;
+        this.mMethodName = methodName;
+        this.mSenderIdentity = senderIdentity;
+        this.mIntent = intent;
+        this.mComponent = component;
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public static final @NonNull Parcelable.Creator<ComponentAliasMessage> CREATOR
+            = new Parcelable.Creator<ComponentAliasMessage>() {
+        @Override
+        public ComponentAliasMessage[] newArray(int size) {
+            return new ComponentAliasMessage[size];
+        }
+
+        @Override
+        public ComponentAliasMessage createFromParcel(@NonNull Parcel in) {
+            return new ComponentAliasMessage(in);
+        }
+    };
+
+    @DataClass.Generated(
+            time = 1629137098129L,
+            codegenVersion = "1.0.23",
+            sourceFile = "frameworks/base/tests/componentalias/common/android/content/componentalias/tests/common/ComponentAliasMessage.java",
+            inputSignatures = "private @android.annotation.Nullable java.lang.String mMessage\nprivate @android.annotation.Nullable java.lang.String mMethodName\nprivate @android.annotation.Nullable java.lang.String mSenderIdentity\nprivate @android.annotation.Nullable android.content.Intent mIntent\nprivate @android.annotation.Nullable android.content.ComponentName mComponent\nclass ComponentAliasMessage extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genSetters=true, genToString=true, genAidl=false)")
+    @Deprecated
+    private void __metadata() {}
+
+
+    //@formatter:on
+    // End of generated code
+
+}
diff --git a/tests/componentalias/common/android/content/componentalias/tests/common/ComponentAliasTestCommon.java b/tests/componentalias/common/android/content/componentalias/tests/common/ComponentAliasTestCommon.java
new file mode 100644
index 0000000..f306072
--- /dev/null
+++ b/tests/componentalias/common/android/content/componentalias/tests/common/ComponentAliasTestCommon.java
@@ -0,0 +1,29 @@
+/*
+ * 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 android.content.componentalias.tests.common;
+
+public final class ComponentAliasTestCommon {
+    private ComponentAliasTestCommon() {
+    }
+
+    public static final String TAG = "ComponentAliasTest";
+
+    public static final String TEST_PACKAGE = "android.content.componentalias.tests";
+
+    public static final String APP_PACKAGE = "android.content.componentalias.tests.app";
+    public static final String SUB1_PACKAGE = "android.content.componentalias.tests.app.sub1";
+    public static final String SUB2_PACKAGE = "android.content.componentalias.tests.app.sub2";
+}
diff --git a/tests/componentalias/common/com/android/compatibility/common/util/BroadcastMessenger.java b/tests/componentalias/common/com/android/compatibility/common/util/BroadcastMessenger.java
new file mode 100644
index 0000000..b6dc09f
--- /dev/null
+++ b/tests/componentalias/common/com/android/compatibility/common/util/BroadcastMessenger.java
@@ -0,0 +1,125 @@
+/*
+ * 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.compatibility.common.util;
+
+import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Parcelable;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+
+/**
+ * Provides a one-way communication mechanism using a Parcelable as a payload, via broadcasts.
+ *
+ * TODO: Move it to compatibility-device-util-axt.
+ */
+public final class BroadcastMessenger {
+    private static final String TAG = "BroadcastMessenger";
+
+    private static final String ACTION_MESSAGE =
+            "com.android.compatibility.common.util.BroadcastMessenger.ACTION_MESSAGE";
+    private static final String EXTRA_MESSAGE =
+            "com.android.compatibility.common.util.BroadcastMessenger.EXTRA_MESSAGE";
+
+    /** Send a message to the {@link Receiver} in a given package. */
+    public static <T extends Parcelable> void send(@NonNull Context context,
+            @NonNull String receiverPackage,
+            @NonNull T message) {
+        final Intent i = new Intent(ACTION_MESSAGE);
+        i.putExtra(EXTRA_MESSAGE, message);
+        i.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        i.setPackage(receiverPackage);
+
+        Log.d(TAG, "Sending: " + message);
+        context.sendBroadcast(i);
+    }
+
+    /**
+     * Receive messages from the test app.
+     */
+    public static final class Receiver<T extends Parcelable> implements AutoCloseable {
+        private final Context mContext;
+        @GuardedBy("mMessages")
+        private final ArrayList<T> mMessages = new ArrayList<>();
+        private boolean mRegistered;
+
+        private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (!ACTION_MESSAGE.equals(intent.getAction())) {
+                    throw new RuntimeException("Unknown message received: " + intent);
+                }
+                final T message = intent.getParcelableExtra(EXTRA_MESSAGE);
+                Log.d(TAG, "Received: " + message);
+                synchronized (mMessages) {
+                    mMessages.add(message);
+                    mMessages.notifyAll();
+                }
+            }
+        };
+
+        /**
+         * Constructor.
+         */
+        public Receiver(@NonNull Context context) {
+            mContext = context;
+
+            final IntentFilter fi = new IntentFilter(ACTION_MESSAGE);
+            context.registerReceiver(mReceiver, fi);
+            mRegistered = true;
+        }
+
+        @Override
+        public void close() {
+            if (mRegistered) {
+                mContext.unregisterReceiver(mReceiver);
+                mRegistered = false;
+            }
+        }
+
+        /**
+         * Receive the next message with a 60 second timeout.
+         */
+        @NonNull
+        public T waitForNextMessage() throws Exception {
+            return waitForNextMessage(60_000);
+        }
+
+        /**
+         * Receive the next message.
+         */
+        @NonNull
+        public T waitForNextMessage(long timeoutMillis) throws Exception {
+            synchronized (mMessages) {
+                final long timeout = System.currentTimeMillis() + timeoutMillis;
+                while (mMessages.size() == 0) {
+                    final long wait = timeout - System.currentTimeMillis();
+                    if (wait <= 0) {
+                        throw new RuntimeException("Timeout waiting for the next message");
+                    }
+                    mMessages.wait(wait);
+                }
+                return mMessages.remove(0);
+            }
+        }
+    }
+}
diff --git a/tests/componentalias/src/android/content/componentalias/tests/ComponentAliasServiceTest.java b/tests/componentalias/src/android/content/componentalias/tests/ComponentAliasServiceTest.java
new file mode 100644
index 0000000..1de6277
--- /dev/null
+++ b/tests/componentalias/src/android/content/componentalias/tests/ComponentAliasServiceTest.java
@@ -0,0 +1,278 @@
+/*
+ * 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 android.content.componentalias.tests;
+
+import static android.content.Context.BIND_AUTO_CREATE;
+import static android.content.componentalias.tests.common.ComponentAliasTestCommon.APP_PACKAGE;
+import static android.content.componentalias.tests.common.ComponentAliasTestCommon.SUB1_PACKAGE;
+import static android.content.componentalias.tests.common.ComponentAliasTestCommon.SUB2_PACKAGE;
+import static android.content.componentalias.tests.common.ComponentAliasTestCommon.TAG;
+import static android.content.componentalias.tests.common.ComponentAliasTestCommon.TEST_PACKAGE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.componentalias.tests.common.ComponentAliasMessage;
+import android.os.IBinder;
+import android.provider.DeviceConfig;
+import android.util.Log;
+
+import com.android.compatibility.common.util.BroadcastMessenger;
+import com.android.compatibility.common.util.BroadcastMessenger.Receiver;
+import com.android.compatibility.common.util.DeviceConfigStateHelper;
+import com.android.compatibility.common.util.ShellUtils;
+import com.android.compatibility.common.util.TestUtils;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Test for the experimental "Component alias" feature.
+ *
+ * Note this test exercises the relevant APIs, but don't actually check if the aliases are
+ * resolved.
+ *
+ * Note all the helper APKs are battery-exempted (via AndroidTest.xml), so they can run
+ * BG services.
+ */
+public class ComponentAliasServiceTest {
+
+    private static final Context sContext = InstrumentationRegistry.getTargetContext();
+
+    private static final DeviceConfigStateHelper sDeviceConfig = new DeviceConfigStateHelper(
+            DeviceConfig.NAMESPACE_ACTIVITY_MANAGER);
+    @Before
+    public void enableComponentAlias() throws Exception {
+        sDeviceConfig.set("component_alias_overrides", "");
+        sDeviceConfig.set("enable_experimental_component_alias", "true");
+
+        // Device config propagation happens on a handler, so we need to wait for AM to
+        // actually set it.
+        TestUtils.waitUntil("Wait until component alias is actually enabled", () -> {
+            return ShellUtils.runShellCommand("dumpsys activity component-alias")
+                    .indexOf("Enabled: true") > 0;
+        });
+    }
+
+    @AfterClass
+    public static void restoreDeviceConfig() throws Exception {
+        sDeviceConfig.close();
+    }
+
+    /**
+     * Service connection used throughout the tests. It sends a message for each callback via
+     * the messenger.
+     */
+    private static final ServiceConnection sServiceConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            Log.w(TAG, "onServiceConnected: " + name);
+
+            ComponentAliasMessage m = new ComponentAliasMessage()
+                    .setSenderIdentity("sServiceConnection")
+                    .setMethodName("onServiceConnected")
+                    .setComponent(name);
+
+            BroadcastMessenger.send(sContext, TEST_PACKAGE, m);
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            Log.w(TAG, "onServiceDisconnected: " + name);
+
+            ComponentAliasMessage m = new ComponentAliasMessage()
+                    .setSenderIdentity("sServiceConnection")
+                    .setMethodName("onServiceDisconnected")
+                    .setComponent(name);
+
+            BroadcastMessenger.send(sContext, TEST_PACKAGE, m);
+        }
+
+        @Override
+        public void onBindingDied(ComponentName name) {
+            Log.w(TAG, "onBindingDied: " + name);
+
+            ComponentAliasMessage m = new ComponentAliasMessage()
+                    .setSenderIdentity("sServiceConnection")
+                    .setMethodName("onBindingDied");
+
+            BroadcastMessenger.send(sContext, TEST_PACKAGE, m);
+        }
+
+        @Override
+        public void onNullBinding(ComponentName name) {
+            Log.w(TAG, "onNullBinding: " + name);
+
+            ComponentAliasMessage m = new ComponentAliasMessage()
+                    .setSenderIdentity("sServiceConnection")
+                    .setMethodName("onNullBinding");
+
+            BroadcastMessenger.send(sContext, TEST_PACKAGE, m);
+        }
+    };
+
+    private void testStartAndStopService_common(
+            Intent originalIntent,
+            ComponentName componentNameForClient,
+            ComponentName componentNameForTarget) throws Exception {
+
+        ComponentAliasMessage m;
+
+        try (Receiver<ComponentAliasMessage> receiver = new Receiver<>(sContext)) {
+            // Start the service.
+            ComponentName result = sContext.startService(originalIntent);
+            assertThat(result).isEqualTo(componentNameForClient);
+
+            // Check
+            m = receiver.waitForNextMessage();
+
+            assertThat(m.getMethodName()).isEqualTo("onStartCommand");
+            // The app sees the rewritten intent.
+            assertThat(m.getIntent().getComponent()).isEqualTo(componentNameForTarget);
+
+            // Verify the original intent.
+            assertThat(m.getIntent().getOriginalIntent().getComponent())
+                    .isEqualTo(originalIntent.getComponent());
+            assertThat(m.getIntent().getOriginalIntent().getPackage())
+                    .isEqualTo(originalIntent.getPackage());
+
+            // Stop the service.
+            sContext.stopService(originalIntent);
+
+            // Check
+            m = receiver.waitForNextMessage();
+
+            assertThat(m.getMethodName()).isEqualTo("onDestroy");
+        }
+    }
+
+    @Test
+    public void testStartAndStopService_explicitComponentName() throws Exception {
+        Intent i = new Intent().setComponent(
+                new ComponentName(APP_PACKAGE, APP_PACKAGE + ".s.Alias01"));
+
+        ComponentName alias = new ComponentName(APP_PACKAGE, APP_PACKAGE + ".s.Alias01");
+        ComponentName target = new ComponentName(SUB1_PACKAGE, APP_PACKAGE + ".s.Target01");
+
+        testStartAndStopService_common(i, alias, target);
+    }
+
+    @Test
+    public void testStartAndStopService_explicitPackageName() throws Exception {
+        Intent i = new Intent().setPackage(APP_PACKAGE);
+        i.setAction(APP_PACKAGE + ".IS_ALIAS_02");
+
+        ComponentName alias = new ComponentName(APP_PACKAGE, APP_PACKAGE + ".s.Alias02");
+        ComponentName target = new ComponentName(SUB2_PACKAGE, APP_PACKAGE + ".s.Target02");
+
+        testStartAndStopService_common(i, alias, target);
+    }
+
+    @Test
+    public void testStartAndStopService_override() throws Exception {
+        Intent i = new Intent().setPackage(APP_PACKAGE);
+        i.setAction(APP_PACKAGE + ".IS_ALIAS_02");
+
+        ComponentName alias = new ComponentName(APP_PACKAGE, APP_PACKAGE + ".s.Alias02");
+
+        // Note, alias02 originally points at sub*2* package, but we override it in this test.
+        ComponentName target = new ComponentName(SUB1_PACKAGE, APP_PACKAGE + ".s.Target02");
+
+        sDeviceConfig.set("component_alias_overrides",
+                alias.flattenToShortString() + ":" + target.flattenToShortString());
+
+        TestUtils.waitUntil("Wait until component alias is actually enabled", () -> {
+            return ShellUtils.runShellCommand("dumpsys activity component-alias")
+                    .indexOf(alias.flattenToShortString() + " -> " + target.flattenToShortString())
+                    > 0;
+        });
+
+
+        testStartAndStopService_common(i, alias, target);
+    }
+
+    private void testBindAndUnbindService_common(
+            Intent originalIntent,
+            ComponentName componentNameForClient,
+            ComponentName componentNameForTarget) throws Exception {
+        ComponentAliasMessage m;
+
+        try (Receiver<ComponentAliasMessage> receiver = new Receiver<>(sContext)) {
+            // Bind to the service.
+            assertThat(sContext.bindService(
+                    originalIntent, sServiceConnection, BIND_AUTO_CREATE)).isTrue();
+
+            // Check the target side behavior.
+            m = receiver.waitForNextMessage();
+
+            assertThat(m.getMethodName()).isEqualTo("onBind");
+            // The app sees the rewritten intent.
+            assertThat(m.getIntent().getComponent()).isEqualTo(componentNameForTarget);
+
+            // Verify the original intent.
+            assertThat(m.getIntent().getOriginalIntent().getComponent())
+                    .isEqualTo(originalIntent.getComponent());
+            assertThat(m.getIntent().getOriginalIntent().getPackage())
+                    .isEqualTo(originalIntent.getPackage());
+
+            // Check the client side behavior.
+            m = receiver.waitForNextMessage();
+
+            assertThat(m.getMethodName()).isEqualTo("onServiceConnected");
+            // The app sees the rewritten intent.
+            assertThat(m.getComponent()).isEqualTo(componentNameForClient);
+
+            // Unbind.
+            sContext.unbindService(sServiceConnection);
+
+            // Check the target side behavior.
+            m = receiver.waitForNextMessage();
+
+            assertThat(m.getMethodName()).isEqualTo("onDestroy");
+
+            // Note onServiceDisconnected() won't be called in this case.
+        }
+    }
+
+    @Test
+    public void testBindService_explicitComponentName() throws Exception {
+        Intent i = new Intent().setComponent(
+                new ComponentName(APP_PACKAGE, APP_PACKAGE + ".s.Alias01"));
+
+        testBindAndUnbindService_common(i,
+                new ComponentName(APP_PACKAGE, APP_PACKAGE + ".s.Alias01"),
+                new ComponentName(SUB1_PACKAGE, APP_PACKAGE + ".s.Target01"));
+    }
+
+    @Test
+    public void testBindService_explicitPackageName() throws Exception {
+        Intent i = new Intent().setPackage(APP_PACKAGE);
+        i.setAction(APP_PACKAGE + ".IS_ALIAS_02");
+
+        testBindAndUnbindService_common(i,
+                new ComponentName(APP_PACKAGE, APP_PACKAGE + ".s.Alias02"),
+                new ComponentName(SUB2_PACKAGE, APP_PACKAGE + ".s.Target02"));
+    }
+}