Merge "Overlayable actor enforcement"
diff --git a/core/java/android/content/om/OverlayableInfo.java b/core/java/android/content/om/OverlayableInfo.java
new file mode 100644
index 0000000..5923907
--- /dev/null
+++ b/core/java/android/content/om/OverlayableInfo.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2019 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.om;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.util.DataClass;
+
+import java.util.Objects;
+
+/**
+ * Immutable info on an overlayable defined inside a target package.
+ *
+ * @hide
+ */
+@DataClass(genSetters = false, genEqualsHashCode = true, genHiddenConstructor = true)
+public final class OverlayableInfo {
+
+    /**
+     * The "name" attribute of the overlayable tag. Used to identify the set of resources overlaid.
+     */
+    @NonNull
+    public final String name;
+
+    /**
+     * The "actor" attribute of the overlayable tag. Used to signal which apps are allowed to
+     * modify overlay state for this overlayable.
+     */
+    @Nullable
+    public final String actor;
+
+    // CHECKSTYLE:OFF Generated code
+    //
+
+
+
+    // Code below generated by codegen v1.0.3.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/content/om/OverlayableInfo.java
+
+
+    /**
+     * Creates a new OverlayableInfo.
+     *
+     * @param name
+     *   The "name" attribute of the overlayable tag. Used to identify the set of resources overlaid.
+     * @param actor
+     *   The "actor" attribute of the overlayable tag. Used to signal which apps are allowed to
+     *   modify overlay state for this overlayable.
+     * @hide
+     */
+    @DataClass.Generated.Member
+    public OverlayableInfo(
+            @NonNull String name,
+            @Nullable String actor) {
+        this.name = name;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, name);
+        this.actor = actor;
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public boolean equals(Object o) {
+        // You can override field equality logic by defining either of the methods like:
+        // boolean fieldNameEquals(OverlayableInfo other) { ... }
+        // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        @SuppressWarnings("unchecked")
+        OverlayableInfo that = (OverlayableInfo) o;
+        //noinspection PointlessBooleanExpression
+        return true
+                && Objects.equals(name, that.name)
+                && Objects.equals(actor, that.actor);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int hashCode() {
+        // You can override field hashCode logic by defining methods like:
+        // int fieldNameHashCode() { ... }
+
+        int _hash = 1;
+        _hash = 31 * _hash + Objects.hashCode(name);
+        _hash = 31 * _hash + Objects.hashCode(actor);
+        return _hash;
+    }
+
+    @DataClass.Generated(
+            time = 1570059850579L,
+            codegenVersion = "1.0.3",
+            sourceFile = "frameworks/base/core/java/android/content/om/OverlayableInfo.java",
+            inputSignatures = "public final @android.annotation.NonNull java.lang.String name\npublic final @android.annotation.Nullable java.lang.String actor\nclass OverlayableInfo extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genSetters=false, genEqualsHashCode=true, genHiddenConstructor=true)")
+    @Deprecated
+    private void __metadata() {}
+
+}
diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java
index de1d514..ad37555 100644
--- a/core/java/android/content/res/ApkAssets.java
+++ b/core/java/android/content/res/ApkAssets.java
@@ -18,6 +18,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UnsupportedAppUsage;
+import android.content.om.OverlayableInfo;
 import android.content.res.loader.ResourcesProvider;
 import android.text.TextUtils;
 
@@ -254,6 +255,17 @@
         }
     }
 
+    /** @hide */
+    @Nullable
+    public OverlayableInfo getOverlayableInfo(String overlayableName) throws IOException {
+        return nativeGetOverlayableInfo(mNativePtr, overlayableName);
+    }
+
+    /** @hide */
+    public boolean definesOverlayable() throws IOException {
+        return nativeDefinesOverlayable(mNativePtr);
+    }
+
     /**
      * Returns false if the underlying APK was changed since this ApkAssets was loaded.
      */
@@ -305,4 +317,7 @@
     private static native long nativeGetStringBlock(long ptr);
     private static native boolean nativeIsUpToDate(long ptr);
     private static native long nativeOpenXml(long ptr, @NonNull String fileName) throws IOException;
+    private static native @Nullable OverlayableInfo nativeGetOverlayableInfo(long ptr,
+            String overlayableName) throws IOException;
+    private static native boolean nativeDefinesOverlayable(long ptr) throws IOException;
 }
diff --git a/core/java/com/android/server/SystemConfig.java b/core/java/com/android/server/SystemConfig.java
index ed7f5de..49a73ee 100644
--- a/core/java/com/android/server/SystemConfig.java
+++ b/core/java/com/android/server/SystemConfig.java
@@ -18,6 +18,7 @@
 
 import static com.android.internal.util.ArrayUtils.appendInt;
 
+import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.content.ComponentName;
 import android.content.pm.FeatureInfo;
@@ -221,6 +222,12 @@
     private ArrayMap<String, Set<String>> mPackageToUserTypeWhitelist = new ArrayMap<>();
     private ArrayMap<String, Set<String>> mPackageToUserTypeBlacklist = new ArrayMap<>();
 
+    /**
+     * Map of system pre-defined, uniquely named actors; keys are namespace,
+     * value maps actor name to package name.
+     */
+    private ArrayMap<String, ArrayMap<String, String>> mNamedActors = null;
+
     public static SystemConfig getInstance() {
         if (!isSystemProcess()) {
             Slog.wtf(TAG, "SystemConfig is being accessed by a process other than "
@@ -398,12 +405,17 @@
         return r;
     }
 
+    @NonNull
+    public Map<String, ? extends Map<String, String>> getNamedActors() {
+        return mNamedActors != null ? mNamedActors : Collections.emptyMap();
+    }
+
     /**
      * Only use for testing. Do NOT use in production code.
      * @param readPermissions false to create an empty SystemConfig; true to read the permissions.
      */
     @VisibleForTesting
-    protected SystemConfig(boolean readPermissions) {
+    public SystemConfig(boolean readPermissions) {
         if (readPermissions) {
             Slog.w(TAG, "Constructing a test SystemConfig");
             readAllPermissions();
@@ -1028,6 +1040,44 @@
                         readInstallInUserType(parser,
                                 mPackageToUserTypeWhitelist, mPackageToUserTypeBlacklist);
                     } break;
+                    case "named-actor": {
+                        String namespace = TextUtils.safeIntern(
+                                parser.getAttributeValue(null, "namespace"));
+                        String actorName = parser.getAttributeValue(null, "name");
+                        String pkgName = TextUtils.safeIntern(
+                                parser.getAttributeValue(null, "package"));
+                        if (TextUtils.isEmpty(namespace)) {
+                            Slog.wtf(TAG, "<" + name + "> without namespace in " + permFile
+                                    + " at " + parser.getPositionDescription());
+                        } else if (TextUtils.isEmpty(actorName)) {
+                            Slog.wtf(TAG, "<" + name + "> without actor name in " + permFile
+                                    + " at " + parser.getPositionDescription());
+                        } else if (TextUtils.isEmpty(pkgName)) {
+                            Slog.wtf(TAG, "<" + name + "> without package name in " + permFile
+                                    + " at " + parser.getPositionDescription());
+                        } else if ("android".equalsIgnoreCase(namespace)) {
+                            throw new IllegalStateException("Defining " + actorName + " as "
+                                    + pkgName + " for the android namespace is not allowed");
+                        } else {
+                            if (mNamedActors == null) {
+                                mNamedActors = new ArrayMap<>();
+                            }
+
+                            ArrayMap<String, String> nameToPkgMap = mNamedActors.get(namespace);
+                            if (nameToPkgMap == null) {
+                                nameToPkgMap = new ArrayMap<>();
+                                mNamedActors.put(namespace, nameToPkgMap);
+                            } else if (nameToPkgMap.containsKey(actorName)) {
+                                String existing = nameToPkgMap.get(actorName);
+                                throw new IllegalStateException("Duplicate actor definition for "
+                                        + namespace + "/" + actorName
+                                        + "; defined as both " + existing + " and " + pkgName);
+                            }
+
+                            nameToPkgMap.put(actorName, pkgName);
+                        }
+                        XmlUtils.skipCurrentTag(parser);
+                    } break;
                     default: {
                         Slog.w(TAG, "Tag " + name + " is unknown in "
                                 + permFile + " at " + parser.getPositionDescription());
diff --git a/core/jni/android_content_res_ApkAssets.cpp b/core/jni/android_content_res_ApkAssets.cpp
index 6370253..f3a626e 100644
--- a/core/jni/android_content_res_ApkAssets.cpp
+++ b/core/jni/android_content_res_ApkAssets.cpp
@@ -194,6 +194,59 @@
   return reinterpret_cast<jlong>(xml_tree.release());
 }
 
+static jobject NativeGetOverlayableInfo(JNIEnv* env, jclass /*clazz*/, jlong ptr,
+                                         jstring overlayable_name) {
+  const ApkAssets* apk_assets = reinterpret_cast<const ApkAssets*>(ptr);
+
+  const auto& packages = apk_assets->GetLoadedArsc()->GetPackages();
+  if (packages.empty()) {
+    jniThrowException(env, "java/io/IOException", "Error reading overlayable from APK");
+    return 0;
+  }
+
+  // TODO(b/119899133): Convert this to a search for the info rather than assuming it's at index 0
+  const auto& overlayable_map = packages[0]->GetOverlayableMap();
+  if (overlayable_map.empty()) {
+    return nullptr;
+  }
+
+  auto overlayable_name_native = std::string(env->GetStringUTFChars(overlayable_name, NULL));
+  auto actor = overlayable_map.find(overlayable_name_native);
+  if (actor == overlayable_map.end()) {
+    return nullptr;
+  }
+
+  jstring actor_string = env->NewStringUTF(actor->first.c_str());
+  if (env->ExceptionCheck() || actor_string == nullptr) {
+    jniThrowException(env, "java/io/IOException", "Error reading overlayable from APK");
+    return 0;
+  }
+
+  jclass overlayable_class = env->FindClass("android/content/om/OverlayableInfo");
+  jmethodID overlayable_constructor = env->GetMethodID(overlayable_class, "<init>",
+                                                       "(Ljava/lang/String;Ljava/lang/String;I)V");
+  return env->NewObject(
+      overlayable_class,
+      overlayable_constructor,
+      overlayable_name,
+      actor_string
+  );
+}
+
+static jboolean NativeDefinesOverlayable(JNIEnv* env, jclass /*clazz*/, jlong ptr) {
+  const ApkAssets* apk_assets = reinterpret_cast<const ApkAssets*>(ptr);
+
+  const auto& packages = apk_assets->GetLoadedArsc()->GetPackages();
+  if (packages.empty()) {
+    // Must throw to prevent bypass by returning false
+    jniThrowException(env, "java/io/IOException", "Error reading overlayable from APK");
+    return 0;
+  }
+
+  const auto& overlayable_infos = packages[0]->GetOverlayableMap();
+  return overlayable_infos.empty() ? JNI_FALSE : JNI_TRUE;
+}
+
 // JNI registration.
 static const JNINativeMethod gApkAssetsMethods[] = {
     {"nativeLoad", "(Ljava/lang/String;ZZZZ)J", (void*)NativeLoad},
@@ -208,6 +261,9 @@
     {"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock},
     {"nativeIsUpToDate", "(J)Z", (void*)NativeIsUpToDate},
     {"nativeOpenXml", "(JLjava/lang/String;)J", (void*)NativeOpenXml},
+    {"nativeGetOverlayableInfo", "(JLjava/lang/String;)Landroid/content/om/OverlayableInfo;",
+     (void*)NativeGetOverlayableInfo},
+    {"nativeDefinesOverlayable", "(J)Z", (void*)NativeDefinesOverlayable},
 };
 
 int register_android_content_res_ApkAssets(JNIEnv* env) {
diff --git a/libs/androidfw/include/androidfw/LoadedArsc.h b/libs/androidfw/include/androidfw/LoadedArsc.h
index 6cbda07..b5d3a1f 100644
--- a/libs/androidfw/include/androidfw/LoadedArsc.h
+++ b/libs/androidfw/include/androidfw/LoadedArsc.h
@@ -273,6 +273,8 @@
   ByteBucketArray<uint32_t> resource_ids_;
   std::vector<DynamicPackageEntry> dynamic_package_map_;
   std::vector<const std::pair<OverlayableInfo, std::unordered_set<uint32_t>>> overlayable_infos_;
+
+  // A map of overlayable name to actor
   std::unordered_map<std::string, std::string> overlayable_map_;
 };
 
diff --git a/services/core/java/com/android/server/om/OverlayActorEnforcer.java b/services/core/java/com/android/server/om/OverlayActorEnforcer.java
new file mode 100644
index 0000000..e055116
--- /dev/null
+++ b/services/core/java/com/android/server/om/OverlayActorEnforcer.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2019 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.om;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.om.OverlayInfo;
+import android.content.om.OverlayableInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.net.Uri;
+import android.os.Process;
+import android.os.RemoteException;
+import android.text.TextUtils;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.CollectionUtils;
+import com.android.server.SystemConfig;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Performs verification that a calling UID can act on a target package's overlayable.
+ *
+ * @hide
+ */
+public class OverlayActorEnforcer {
+
+    private final VerifyCallback mVerifyCallback;
+
+    public OverlayActorEnforcer(@NonNull VerifyCallback verifyCallback) {
+        mVerifyCallback = verifyCallback;
+    }
+
+    void enforceActor(@NonNull OverlayInfo overlayInfo, @NonNull String methodName,
+            int callingUid, int userId) throws SecurityException {
+        ActorState actorState = isAllowedActor(methodName, overlayInfo, callingUid, userId);
+        if (actorState == ActorState.ALLOWED) {
+            return;
+        }
+
+        String targetOverlayableName = overlayInfo.targetOverlayableName;
+        throw new SecurityException("UID" + callingUid + " is not allowed to call "
+                + methodName + " for "
+                + (TextUtils.isEmpty(targetOverlayableName) ? "" : (targetOverlayableName + " in "))
+                + overlayInfo.targetPackageName + " because " + actorState
+        );
+    }
+
+    /**
+     * An actor is valid if any of the following is true:
+     * - is {@link Process#ROOT_UID}, {@link Process#SYSTEM_UID}
+     * - is the target overlay package
+     * - has the CHANGE_OVERLAY_PACKAGES permission and an actor is not defined
+     * - is the same the as the package defined in {@link SystemConfig#getNamedActors()} for a given
+     *     namespace and actor name
+     *
+     * @return true if the actor is allowed to act on the target overlayInfo
+     */
+    private ActorState isAllowedActor(String methodName, OverlayInfo overlayInfo,
+            int callingUid, int userId) {
+        switch (callingUid) {
+            case Process.ROOT_UID:
+            case Process.SYSTEM_UID:
+                return ActorState.ALLOWED;
+        }
+
+        String[] callingPackageNames = mVerifyCallback.getPackagesForUid(callingUid);
+        if (ArrayUtils.isEmpty(callingPackageNames)) {
+            return ActorState.NO_PACKAGES_FOR_UID;
+        }
+
+        // A target is always an allowed actor for itself
+        String targetPackageName = overlayInfo.targetPackageName;
+        if (ArrayUtils.contains(callingPackageNames, targetPackageName)) {
+            return ActorState.ALLOWED;
+        }
+
+        String targetOverlayableName = overlayInfo.targetOverlayableName;
+
+        if (TextUtils.isEmpty(targetOverlayableName)) {
+            try {
+                if (mVerifyCallback.doesTargetDefineOverlayable(targetPackageName, userId)) {
+                    return ActorState.MISSING_TARGET_OVERLAYABLE_NAME;
+                } else {
+                    // If there's no overlayable defined, fallback to the legacy permission check
+                    try {
+                        mVerifyCallback.enforcePermission(
+                                android.Manifest.permission.CHANGE_OVERLAY_PACKAGES, methodName);
+
+                        // If the previous method didn't throw, check passed
+                        return ActorState.ALLOWED;
+                    } catch (SecurityException e) {
+                        return ActorState.MISSING_LEGACY_PERMISSION;
+                    }
+                }
+            } catch (RemoteException | IOException e) {
+                return ActorState.ERROR_READING_OVERLAYABLE;
+            }
+        }
+
+        OverlayableInfo targetOverlayable;
+        try {
+            targetOverlayable = mVerifyCallback.getOverlayableForTarget(targetPackageName,
+                    targetOverlayableName, userId);
+        } catch (IOException e) {
+            return ActorState.UNABLE_TO_GET_TARGET;
+        }
+
+        if (targetOverlayable == null) {
+            return ActorState.MISSING_OVERLAYABLE;
+        }
+
+        String actor = targetOverlayable.actor;
+        if (TextUtils.isEmpty(actor)) {
+            // If there's no actor defined, fallback to the legacy permission check
+            try {
+                mVerifyCallback.enforcePermission(
+                        android.Manifest.permission.CHANGE_OVERLAY_PACKAGES, methodName);
+
+                // If the previous method didn't throw, check passed
+                return ActorState.ALLOWED;
+            } catch (SecurityException e) {
+                return ActorState.MISSING_LEGACY_PERMISSION;
+            }
+        }
+
+        Map<String, ? extends Map<String, String>> namedActors = mVerifyCallback.getNamedActors();
+        if (namedActors.isEmpty()) {
+            return ActorState.NO_NAMED_ACTORS;
+        }
+
+        Uri actorUri = Uri.parse(actor);
+
+        String actorScheme = actorUri.getScheme();
+        List<String> actorPathSegments = actorUri.getPathSegments();
+        if (!"overlay".equals(actorScheme) || CollectionUtils.size(actorPathSegments) != 1) {
+            return ActorState.INVALID_OVERLAYABLE_ACTOR_NAME;
+        }
+
+        String actorNamespace = actorUri.getAuthority();
+        Map<String, String> namespace = namedActors.get(actorNamespace);
+        if (namespace == null) {
+            return ActorState.MISSING_NAMESPACE;
+        }
+
+        String actorName = actorPathSegments.get(0);
+        String packageName = namespace.get(actorName);
+        if (TextUtils.isEmpty(packageName)) {
+            return ActorState.MISSING_ACTOR_NAME;
+        }
+
+        PackageInfo packageInfo = mVerifyCallback.getPackageInfo(packageName, userId);
+        if (packageInfo == null) {
+            return ActorState.MISSING_APP_INFO;
+        }
+
+        ApplicationInfo appInfo = packageInfo.applicationInfo;
+        if (appInfo == null) {
+            return ActorState.MISSING_APP_INFO;
+        }
+
+        // Currently only pre-installed apps can be actors
+        if (!appInfo.isSystemApp() && !appInfo.isUpdatedSystemApp()) {
+            return ActorState.ACTOR_NOT_PREINSTALLED;
+        }
+
+        if (ArrayUtils.contains(callingPackageNames, packageName)) {
+            return ActorState.ALLOWED;
+        }
+
+        return ActorState.INVALID_ACTOR;
+    }
+
+    /**
+     * For easier logging/debugging, a set of all possible failure/success states when running
+     * enforcement.
+     */
+    private enum ActorState {
+        ALLOWED,
+        INVALID_ACTOR,
+        MISSING_NAMESPACE,
+        MISSING_PACKAGE,
+        MISSING_APP_INFO,
+        ACTOR_NOT_PREINSTALLED,
+        NO_PACKAGES_FOR_UID,
+        MISSING_ACTOR_NAME,
+        ERROR_READING_OVERLAYABLE,
+        MISSING_TARGET_OVERLAYABLE_NAME,
+        MISSING_OVERLAYABLE,
+        INVALID_OVERLAYABLE_ACTOR_NAME,
+        NO_NAMED_ACTORS,
+        UNABLE_TO_GET_TARGET,
+        MISSING_LEGACY_PERMISSION
+    }
+
+    /**
+     * Delegate to the system for querying information about packages.
+     */
+    public interface VerifyCallback {
+
+        /**
+         * Read from the APK and AndroidManifest of a package to return the overlayable defined for
+         * a given name.
+         *
+         * @throws IOException if the target can't be read
+         */
+        @Nullable
+        OverlayableInfo getOverlayableForTarget(@NonNull String packageName,
+                @Nullable String targetOverlayableName, int userId)
+                throws IOException;
+
+        /**
+         * @see android.content.pm.PackageManager#getPackagesForUid(int)
+         */
+        @Nullable
+        String[] getPackagesForUid(int uid);
+
+        /**
+         * @param userId user to filter package visibility by
+         * @see android.content.pm.PackageManager#getPackageInfo(String, int)
+         */
+        @Nullable
+        PackageInfo getPackageInfo(@NonNull String packageName, int userId);
+
+        /**
+         * @return map of system pre-defined, uniquely named actors; keys are namespace,
+         * value maps actor name to package name
+         */
+        @NonNull
+        Map<String, ? extends Map<String, String>> getNamedActors();
+
+        /**
+         * @return true if the target package has declared an overlayable
+         */
+        boolean doesTargetDefineOverlayable(String targetPackageName, int userId)
+                throws RemoteException, IOException;
+
+        /**
+         * @throws SecurityException containing message if the caller doesn't have the given
+         *                           permission
+         */
+        void enforcePermission(String permission, String message) throws SecurityException;
+    }
+}
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 5f3e503..63de61c 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -39,10 +39,12 @@
 import android.content.IntentFilter;
 import android.content.om.IOverlayManager;
 import android.content.om.OverlayInfo;
+import android.content.om.OverlayableInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.UserInfo;
+import android.content.res.ApkAssets;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Environment;
@@ -63,6 +65,7 @@
 import com.android.server.FgThread;
 import com.android.server.IoThread;
 import com.android.server.LocalServices;
+import com.android.server.SystemConfig;
 import com.android.server.SystemService;
 import com.android.server.pm.UserManagerService;
 
@@ -229,6 +232,8 @@
 
     private final OverlayManagerServiceImpl mImpl;
 
+    private final OverlayActorEnforcer mActorEnforcer;
+
     private final AtomicBoolean mPersistSettingsScheduled = new AtomicBoolean(false);
 
     public OverlayManagerService(@NonNull final Context context) {
@@ -237,12 +242,13 @@
             traceBegin(TRACE_TAG_RRO, "OMS#OverlayManagerService");
             mSettingsFile = new AtomicFile(
                     new File(Environment.getDataSystemDirectory(), "overlays.xml"), "overlays");
-            mPackageManager = new PackageManagerHelper();
+            mPackageManager = new PackageManagerHelper(context);
             mUserManager = UserManagerService.getInstance();
             IdmapManager im = new IdmapManager(mPackageManager);
             mSettings = new OverlayManagerSettings();
             mImpl = new OverlayManagerServiceImpl(mPackageManager, im, mSettings,
                     getDefaultOverlayPackages(), new OverlayChangeListener());
+            mActorEnforcer = new OverlayActorEnforcer(mPackageManager);
 
             final IntentFilter packageFilter = new IntentFilter();
             packageFilter.addAction(ACTION_PACKAGE_ADDED);
@@ -581,7 +587,7 @@
                 int userId) throws RemoteException {
             try {
                 traceBegin(TRACE_TAG_RRO, "OMS#setEnabled " + packageName + " " + enable);
-                enforceChangeOverlayPackagesPermission("setEnabled");
+                enforceActor(packageName, "setEnabled", userId);
                 userId = handleIncomingUser(userId, "setEnabled");
                 if (packageName == null) {
                     return false;
@@ -605,7 +611,7 @@
                 int userId) throws RemoteException {
             try {
                 traceBegin(TRACE_TAG_RRO, "OMS#setEnabledExclusive " + packageName + " " + enable);
-                enforceChangeOverlayPackagesPermission("setEnabledExclusive");
+                enforceActor(packageName, "setEnabledExclusive", userId);
                 userId = handleIncomingUser(userId, "setEnabledExclusive");
                 if (packageName == null || !enable) {
                     return false;
@@ -630,7 +636,7 @@
                 throws RemoteException {
             try {
                 traceBegin(TRACE_TAG_RRO, "OMS#setEnabledExclusiveInCategory " + packageName);
-                enforceChangeOverlayPackagesPermission("setEnabledExclusiveInCategory");
+                enforceActor(packageName, "setEnabledExclusiveInCategory", userId);
                 userId = handleIncomingUser(userId, "setEnabledExclusiveInCategory");
                 if (packageName == null) {
                     return false;
@@ -656,7 +662,7 @@
             try {
                 traceBegin(TRACE_TAG_RRO, "OMS#setPriority " + packageName + " "
                         + parentPackageName);
-                enforceChangeOverlayPackagesPermission("setPriority");
+                enforceActor(packageName, "setPriority", userId);
                 userId = handleIncomingUser(userId, "setPriority");
                 if (packageName == null || parentPackageName == null) {
                     return false;
@@ -680,7 +686,7 @@
                 throws RemoteException {
             try {
                 traceBegin(TRACE_TAG_RRO, "OMS#setHighestPriority " + packageName);
-                enforceChangeOverlayPackagesPermission("setHighestPriority");
+                enforceActor(packageName, "setHighestPriority", userId);
                 userId = handleIncomingUser(userId, "setHighestPriority");
                 if (packageName == null) {
                     return false;
@@ -704,7 +710,7 @@
                 throws RemoteException {
             try {
                 traceBegin(TRACE_TAG_RRO, "OMS#setLowestPriority " + packageName);
-                enforceChangeOverlayPackagesPermission("setLowestPriority");
+                enforceActor(packageName, "setLowestPriority", userId);
                 userId = handleIncomingUser(userId, "setLowestPriority");
                 if (packageName == null) {
                     return false;
@@ -750,7 +756,7 @@
                 return;
             }
 
-            enforceChangeOverlayPackagesPermission("invalidateCachesForOverlay");
+            enforceActor(packageName, "invalidateCachesForOverlay", userId);
             userId = handleIncomingUser(userId, "invalidateCachesForOverlay");
             final long ident = Binder.clearCallingIdentity();
             try {
@@ -861,18 +867,6 @@
         }
 
         /**
-         * Enforce that the caller holds the CHANGE_OVERLAY_PACKAGES permission (or is
-         * system or root).
-         *
-         * @param message used as message if SecurityException is thrown
-         * @throws SecurityException if the permission check fails
-         */
-        private void enforceChangeOverlayPackagesPermission(@NonNull final String message) {
-            getContext().enforceCallingOrSelfPermission(
-                    android.Manifest.permission.CHANGE_OVERLAY_PACKAGES, message);
-        }
-
-        /**
          * Enforce that the caller holds the DUMP permission (or is system or root).
          *
          * @param message used as message if SecurityException is thrown
@@ -881,6 +875,13 @@
         private void enforceDumpPermission(@NonNull final String message) {
             getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, message);
         }
+
+        private void enforceActor(String packageName, String methodName, int userId)
+                throws SecurityException {
+            OverlayInfo overlayInfo = mImpl.getOverlayInfo(packageName, userId);
+            int callingUid = Binder.getCallingUid();
+            mActorEnforcer.enforceActor(overlayInfo, methodName, callingUid, userId);
+        }
     };
 
     private final class OverlayChangeListener
@@ -1035,9 +1036,16 @@
         }
     }
 
-    private static final class PackageManagerHelper implements
-            OverlayManagerServiceImpl.PackageManagerHelper {
+    /**
+     * Delegate for {@link android.content.pm.PackageManager} and {@link PackageManagerInternal}
+     * functionality, separated for easy testing.
+     *
+     * @hide
+     */
+    public static final class PackageManagerHelper implements
+            OverlayManagerServiceImpl.PackageManagerHelper, OverlayActorEnforcer.VerifyCallback {
 
+        private final Context mContext;
         private final IPackageManager mPackageManager;
         private final PackageManagerInternal mPackageManagerInternal;
 
@@ -1048,11 +1056,14 @@
         // behind until all pending intents have been processed.
         private final SparseArray<HashMap<String, PackageInfo>> mCache = new SparseArray<>();
 
-        PackageManagerHelper() {
+        PackageManagerHelper(Context context) {
+            mContext = context;
             mPackageManager = getPackageManager();
             mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
         }
 
+        // TODO(b/143096091): Remove PackageInfo cache so that PackageManager is always queried
+        //  to enforce visibility/other permission checks
         public PackageInfo getPackageInfo(@NonNull final String packageName, final int userId,
                 final boolean useCache) {
             if (useCache) {
@@ -1075,7 +1086,19 @@
 
         @Override
         public PackageInfo getPackageInfo(@NonNull final String packageName, final int userId) {
-            return getPackageInfo(packageName, userId, true);
+            // TODO(b/143096091): Remove clearing calling ID
+            long callingIdentity = Binder.clearCallingIdentity();
+            try {
+                return getPackageInfo(packageName, userId, true);
+            } finally {
+                Binder.restoreCallingIdentity(callingIdentity);
+            }
+        }
+
+        @NonNull
+        @Override
+        public Map<String, ? extends Map<String, String>> getNamedActors() {
+            return SystemConfig.getInstance().getNamedActors();
         }
 
         @Override
@@ -1097,6 +1120,70 @@
             return mPackageManagerInternal.getOverlayPackages(userId);
         }
 
+        @Nullable
+        @Override
+        public OverlayableInfo getOverlayableForTarget(@NonNull String packageName,
+                @Nullable String targetOverlayableName, int userId)
+                throws IOException {
+            // TODO(b/143096091): Remove clearing calling ID
+            long callingIdentity = Binder.clearCallingIdentity();
+            try {
+                PackageInfo packageInfo = getPackageInfo(packageName, userId);
+                if (packageInfo == null) {
+                    throw new IOException("Unable to get target package");
+                }
+
+                String baseCodePath = packageInfo.applicationInfo.getBaseCodePath();
+
+                ApkAssets apkAssets = null;
+                try {
+                    apkAssets = ApkAssets.loadFromPath(baseCodePath);
+                    return apkAssets.getOverlayableInfo(targetOverlayableName);
+                } finally {
+                    if (apkAssets != null) {
+                        try {
+                            apkAssets.close();
+                        } catch (Throwable ignored) {
+                        }
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(callingIdentity);
+            }
+        }
+
+        @Override
+        public boolean doesTargetDefineOverlayable(String targetPackageName, int userId)
+                throws RemoteException, IOException {
+            // TODO(b/143096091): Remove clearing calling ID
+            long callingIdentity = Binder.clearCallingIdentity();
+            try {
+                PackageInfo packageInfo = mPackageManager.getPackageInfo(targetPackageName, 0,
+                        userId);
+                String baseCodePath = packageInfo.applicationInfo.getBaseCodePath();
+
+                ApkAssets apkAssets = null;
+                try {
+                    apkAssets = ApkAssets.loadFromPath(baseCodePath);
+                    return apkAssets.definesOverlayable();
+                } finally {
+                    if (apkAssets != null) {
+                        try {
+                            apkAssets.close();
+                        } catch (Throwable ignored) {
+                        }
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(callingIdentity);
+            }
+        }
+
+        @Override
+        public void enforcePermission(String permission, String message) throws SecurityException {
+            mContext.enforceCallingOrSelfPermission(permission, message);
+        }
+
         public PackageInfo getCachedPackageInfo(@NonNull final String packageName,
                 final int userId) {
             final HashMap<String, PackageInfo> map = mCache.get(userId);
@@ -1128,6 +1215,22 @@
             mCache.delete(userId);
         }
 
+        @Nullable
+        @Override
+        public String[] getPackagesForUid(int uid) {
+            // TODO(b/143096091): Remove clearing calling ID
+            long callingIdentity = Binder.clearCallingIdentity();
+            try {
+                try {
+                    return mPackageManager.getPackagesForUid(uid);
+                } catch (RemoteException ignored) {
+                    return null;
+                }
+            } finally {
+                Binder.restoreCallingIdentity(callingIdentity);
+            }
+        }
+
         private static final String TAB1 = "    ";
         private static final String TAB2 = TAB1 + TAB1;
 
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 30ccb71..52fb69e 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -8,6 +8,7 @@
     // Include all test java files.
     srcs: [
         "src/**/*.java",
+        "src/**/*.kt",
 
         "aidl/com/android/servicestests/aidl/INetworkStateObserver.aidl",
         "aidl/com/android/servicestests/aidl/ICmdReceiverService.aidl",
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
new file mode 100644
index 0000000..233e16c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2019 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.om
+
+import android.content.om.OverlayInfo
+import android.content.om.OverlayableInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.os.Process
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.ExpectedException
+
+class OverlayActorEnforcerTests {
+    companion object {
+        private const val NAMESPACE = "testnamespace"
+        private const val ACTOR_NAME = "testactor"
+        private const val ACTOR_PKG_NAME = "com.test.actor.one"
+        private const val OVERLAYABLE_NAME = "TestOverlayable"
+        private const val UID = 3536
+        private const val USER_ID = 55
+    }
+
+    @get:Rule
+    val expectedException = ExpectedException.none()!!
+
+    @Test
+    fun isRoot() {
+        verify(callingUid = Process.ROOT_UID)
+    }
+
+    @Test(expected = SecurityException::class)
+    fun isShell() {
+        verify(callingUid = Process.SHELL_UID)
+    }
+
+    @Test
+    fun isSystem() {
+        verify(callingUid = Process.SYSTEM_UID)
+    }
+
+    @Test(expected = SecurityException::class)
+    fun noOverlayable_noTarget() {
+        verify(targetOverlayableName = null)
+    }
+
+    @Test
+    fun noOverlayable_noTarget_withPermission() {
+        verify(targetOverlayableName = null, hasPermission = true)
+    }
+
+    @Test(expected = SecurityException::class)
+    fun noOverlayable_withTarget() {
+        verify(targetOverlayableName = OVERLAYABLE_NAME)
+    }
+
+    @Test(expected = SecurityException::class)
+    fun withOverlayable_noTarget() {
+        verify(
+                targetOverlayableName = null,
+                overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, null)
+        )
+    }
+
+    @Test(expected = SecurityException::class)
+    fun withOverlayable_noActor() {
+        verify(
+                overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, null)
+        )
+    }
+
+    @Test
+    fun withOverlayable_noActor_withPermission() {
+        verify(
+                hasPermission = true,
+                overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, null)
+        )
+    }
+
+    @Test(expected = SecurityException::class)
+    fun withOverlayable_withActor_notActor() {
+        verify(
+                isActor = false,
+                overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
+                        "overlay://$NAMESPACE/$ACTOR_NAME")
+        )
+    }
+
+    @Test(expected = SecurityException::class)
+    fun withOverlayable_withActor_isActor_notPreInstalled() {
+        verify(
+                isActor = true,
+                isPreInstalled = false,
+                overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
+                        "overlay://$NAMESPACE/$ACTOR_NAME")
+        )
+    }
+
+    @Test
+    fun withOverlayable_withActor_isActor_isPreInstalled() {
+        verify(
+                isActor = true,
+                isPreInstalled = true,
+                overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
+                        "overlay://$NAMESPACE/$ACTOR_NAME")
+        )
+    }
+
+    @Test(expected = SecurityException::class)
+    fun withOverlayable_invalidActor() {
+        verify(
+                isActor = true,
+                isPreInstalled = true,
+                overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, "notValidActor")
+        )
+    }
+
+    private fun verify(
+        isActor: Boolean = false,
+        isPreInstalled: Boolean = false,
+        hasPermission: Boolean = false,
+        overlayableInfo: OverlayableInfo? = null,
+        callingUid: Int = UID,
+        targetOverlayableName: String? = OVERLAYABLE_NAME
+    ) {
+        val callback = MockCallback(
+                isActor = isActor,
+                isPreInstalled = isPreInstalled,
+                hasPermission = hasPermission,
+                overlayableInfo = overlayableInfo
+        )
+
+        val overlayInfo = overlayInfo(targetOverlayableName)
+        OverlayActorEnforcer(callback)
+                .enforceActor(overlayInfo, "test", callingUid, USER_ID)
+    }
+
+    private fun overlayInfo(targetOverlayableName: String?) = OverlayInfo("com.test.overlay",
+            "com.test.target", targetOverlayableName, null, "/path", OverlayInfo.STATE_UNKNOWN, 0,
+            0, false)
+
+    private class MockCallback(
+        private val isActor: Boolean = false,
+        private val isPreInstalled: Boolean = false,
+        private val hasPermission: Boolean = false,
+        private val overlayableInfo: OverlayableInfo? = null,
+        private vararg val packageNames: String = arrayOf("com.test.actor.one")
+    ) : OverlayActorEnforcer.VerifyCallback {
+
+        override fun getNamedActors() = if (isActor) {
+            mapOf(NAMESPACE to mapOf(ACTOR_NAME to ACTOR_PKG_NAME))
+        } else {
+            emptyMap()
+        }
+
+        override fun getOverlayableForTarget(
+            packageName: String,
+            targetOverlayableName: String?,
+            userId: Int
+        ) = overlayableInfo
+
+        override fun getPackagesForUid(uid: Int) = when (uid) {
+            UID -> packageNames
+            else -> null
+        }
+
+        override fun getPackageInfo(packageName: String, userId: Int) = PackageInfo().apply {
+            applicationInfo = ApplicationInfo().apply {
+                flags = if (isPreInstalled) ApplicationInfo.FLAG_SYSTEM else 0
+            }
+        }
+
+        override fun doesTargetDefineOverlayable(targetPackageName: String?, userId: Int): Boolean {
+            return overlayableInfo != null
+        }
+
+        override fun enforcePermission(permission: String?, message: String?) {
+            if (!hasPermission) {
+                throw SecurityException()
+            }
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt
new file mode 100644
index 0000000..b7199d4
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2019 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.systemconfig
+
+import android.content.Context
+import androidx.test.InstrumentationRegistry
+import com.android.server.SystemConfig
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.ExpectedException
+import org.junit.rules.TemporaryFolder
+
+class SystemConfigNamedActorTest {
+
+    companion object {
+        private const val NAMESPACE_TEST = "someTestNamespace"
+        private const val NAMESPACE_ANDROID = "android"
+        private const val ACTOR_ONE = "iconShaper"
+        private const val ACTOR_TWO = "colorChanger"
+        private const val PACKAGE_ONE = "com.test.actor.one"
+        private const val PACKAGE_TWO = "com.test.actor.two"
+    }
+
+    private val context: Context = InstrumentationRegistry.getContext()
+
+    @get:Rule
+    val tempFolder = TemporaryFolder(context.filesDir)
+
+    @get:Rule
+    val expected = ExpectedException.none()
+
+    private var uniqueCounter = 0
+
+    @Test
+    fun twoUnique() {
+        """
+            <config>
+                <named-actor
+                    namespace="$NAMESPACE_TEST"
+                    name="$ACTOR_ONE"
+                    package="$PACKAGE_ONE"
+                    />
+                <named-actor
+                    namespace="$NAMESPACE_TEST"
+                    name="$ACTOR_TWO"
+                    package="$PACKAGE_TWO"
+                    />
+            </config>
+        """.write()
+
+        assertPermissions().containsExactlyEntriesIn(
+                mapOf(
+                        NAMESPACE_TEST to mapOf(
+                            ACTOR_ONE to PACKAGE_ONE,
+                            ACTOR_TWO to PACKAGE_TWO
+                        )
+                )
+        )
+    }
+
+    @Test
+    fun twoSamePackage() {
+        """
+            <config>
+                <named-actor
+                    namespace="$NAMESPACE_TEST"
+                    name="$ACTOR_ONE"
+                    package="$PACKAGE_ONE"
+                    />
+                <named-actor
+                    namespace="$NAMESPACE_TEST"
+                    name="$ACTOR_TWO"
+                    package="$PACKAGE_ONE"
+                    />
+            </config>
+        """.write()
+
+        assertPermissions().containsExactlyEntriesIn(
+                mapOf(
+                        NAMESPACE_TEST to mapOf(
+                            ACTOR_ONE to PACKAGE_ONE,
+                            ACTOR_TWO to PACKAGE_ONE
+                        )
+                )
+        )
+    }
+
+    @Test
+    fun missingNamespace() {
+        """
+            <config>
+                <named-actor
+                    name="$ACTOR_ONE"
+                    package="$PACKAGE_ONE"
+                    />
+                <named-actor
+                    namespace="$NAMESPACE_TEST"
+                    name="$ACTOR_TWO"
+                    package="$PACKAGE_TWO"
+                    />
+            </config>
+        """.write()
+
+        assertPermissions().containsExactlyEntriesIn(
+                mapOf(
+                        NAMESPACE_TEST to mapOf(
+                                ACTOR_TWO to PACKAGE_TWO
+                        )
+                )
+        )
+    }
+
+    @Test
+    fun missingName() {
+        """
+            <config>
+                <named-actor
+                    namespace="$NAMESPACE_TEST"
+                    package="$PACKAGE_ONE"
+                    />
+                <named-actor
+                    namespace="$NAMESPACE_TEST"
+                    name="$ACTOR_TWO"
+                    package="$PACKAGE_TWO"
+                    />
+            </config>
+        """.write()
+
+        assertPermissions().containsExactlyEntriesIn(
+                mapOf(
+                        NAMESPACE_TEST to mapOf(
+                                ACTOR_TWO to PACKAGE_TWO
+                        )
+                )
+        )
+    }
+
+    @Test
+    fun missingPackage() {
+        """
+            <config>
+                <named-actor
+                    namespace="$NAMESPACE_TEST"
+                    name="$ACTOR_ONE"
+                    />
+                <named-actor
+                    namespace="$NAMESPACE_TEST"
+                    name="$ACTOR_TWO"
+                    package="$PACKAGE_TWO"
+                    />
+            </config>
+        """.write()
+
+        assertPermissions().containsExactlyEntriesIn(
+                mapOf(
+                        NAMESPACE_TEST to mapOf(
+                                ACTOR_TWO to PACKAGE_TWO
+                        )
+                )
+        )
+    }
+
+    @Test
+    fun androidNamespaceThrows() {
+        """
+            <config>
+                <named-actor
+                    namespace="$NAMESPACE_TEST"
+                    name="$ACTOR_ONE"
+                    package="$PACKAGE_ONE"
+                    />
+                <named-actor
+                    namespace="$NAMESPACE_ANDROID"
+                    name="$ACTOR_ONE"
+                    package="$PACKAGE_ONE"
+                    />
+            </config>
+        """.write()
+
+        expected.expect(IllegalStateException::class.java)
+        expected.expectMessage("Defining $ACTOR_ONE as $PACKAGE_ONE " +
+                "for the android namespace is not allowed")
+
+        assertPermissions()
+    }
+
+    @Test
+    fun duplicateActorNameThrows() {
+        """
+            <config>
+                <named-actor
+                    namespace="$NAMESPACE_TEST"
+                    name="$ACTOR_ONE"
+                    package="$PACKAGE_ONE"
+                    />
+                <named-actor
+                    namespace="$NAMESPACE_TEST"
+                    name="$ACTOR_ONE"
+                    package="$PACKAGE_TWO"
+                    />
+            </config>
+        """.write()
+
+        expected.expect(IllegalStateException::class.java)
+        expected.expectMessage("Duplicate actor definition for $NAMESPACE_TEST/$ACTOR_ONE;" +
+                " defined as both $PACKAGE_ONE and $PACKAGE_TWO")
+
+        assertPermissions()
+    }
+
+    private fun String.write() = tempFolder.root.resolve("${uniqueCounter++}.xml")
+            .writeText(this.trimIndent())
+
+    private fun assertPermissions() = SystemConfig(false).apply {
+        readPermissions(tempFolder.root, 0)
+    }. let { assertThat(it.namedActors) }
+}
diff --git a/services/tests/servicestests/src/com/android/server/SystemConfigTest.java b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
similarity index 98%
rename from services/tests/servicestests/src/com/android/server/SystemConfigTest.java
rename to services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
index ff03391..fde0ddf 100644
--- a/services/tests/servicestests/src/com/android/server/SystemConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server;
+package com.android.server.systemconfig;
 
 import static org.junit.Assert.assertEquals;
 
@@ -25,6 +25,8 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.SystemConfig;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;