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;