Add background restriction levels definition and its controller
This CL adds the definition of background app restriction levels.
The existing various system background app restriction features will
be mapped into the levels here, including app standby buckets,
background restrictions (forced-app-standby) etc. Future CLs may
apply restrictions to background apps accordingly.
BYPASS_INCLUSIVE_LANGUAGE_REASON=Legacy API name
Bug: 200326767
Test: atest FrameworksMockingServicesTests:BackgroundRestrictionTest
Change-Id: Icf82031c572e7b8e82e6528402e9df4de5b7a675
diff --git a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
index 0f36d32..9b1f2d0 100644
--- a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
+++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
@@ -4,8 +4,8 @@
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.usage.AppStandbyInfo;
+import android.app.usage.UsageStatsManager.ForcedReasons;
import android.app.usage.UsageStatsManager.StandbyBuckets;
-import android.app.usage.UsageStatsManager.SystemForcedReasons;
import android.content.Context;
import android.util.IndentingPrintWriter;
@@ -152,7 +152,7 @@
* UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_* reasons.
*/
void restrictApp(@NonNull String packageName, int userId,
- @SystemForcedReasons int restrictReason);
+ @ForcedReasons int restrictReason);
/**
* Put the specified app in the
@@ -169,7 +169,29 @@
* UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_* reasons.
*/
void restrictApp(@NonNull String packageName, int userId, int mainReason,
- @SystemForcedReasons int restrictReason);
+ @ForcedReasons int restrictReason);
+
+ /**
+ * Unrestrict an app if there is no other reason to restrict it.
+ *
+ * <p>
+ * The {@code prevMainReasonRestrict} and {@code prevSubReasonRestrict} are the previous
+ * reasons of why it was restricted, but the caller knows that these conditions are not true
+ * anymore; therefore if there is no other reasons to restrict it (as there could bemultiple
+ * reasons to restrict it), lift the restriction.
+ * </p>
+ *
+ * @param packageName The package name of the app.
+ * @param userId The user id that this app runs in.
+ * @param prevMainReasonRestrict The main reason that why it was restricted, must be either
+ * {@link android.app.usage.UsageStatsManager#REASON_MAIN_FORCED_BY_SYSTEM}
+ * or {@link android.app.usage.UsageStatsManager#REASON_MAIN_FORCED_BY_USER}.
+ * @param prevSubReasonRestrict The subreason that why it was restricted before.
+ * @param mainReasonUnrestrict The main reason that why it could be unrestricted now.
+ * @param subReasonUnrestrict The subreason that why it could be unrestricted now.
+ */
+ void maybeUnrestrictApp(@NonNull String packageName, int userId, int prevMainReasonRestrict,
+ int prevSubReasonRestrict, int mainReasonUnrestrict, int subReasonUnrestrict);
void addActiveDeviceAdmin(String adminPkg, int userId);
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index 0ad70e4..523f32c 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -63,8 +63,8 @@
import android.app.ActivityManager;
import android.app.usage.AppStandbyInfo;
import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManager.ForcedReasons;
import android.app.usage.UsageStatsManager.StandbyBuckets;
-import android.app.usage.UsageStatsManager.SystemForcedReasons;
import android.app.usage.UsageStatsManagerInternal;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
@@ -1406,13 +1406,13 @@
@Override
public void restrictApp(@NonNull String packageName, int userId,
- @SystemForcedReasons int restrictReason) {
+ @ForcedReasons int restrictReason) {
restrictApp(packageName, userId, REASON_MAIN_FORCED_BY_SYSTEM, restrictReason);
}
@Override
public void restrictApp(@NonNull String packageName, int userId, int mainReason,
- @SystemForcedReasons int restrictReason) {
+ @ForcedReasons int restrictReason) {
if (mainReason != REASON_MAIN_FORCED_BY_SYSTEM
&& mainReason != REASON_MAIN_FORCED_BY_USER) {
Slog.e(TAG, "Tried to restrict app " + packageName + " for an unsupported reason");
@@ -1799,27 +1799,36 @@
* bucket if it was forced into the bucket by the system because it was buggy.
*/
@VisibleForTesting
- void maybeUnrestrictBuggyApp(String packageName, int userId) {
+ void maybeUnrestrictBuggyApp(@NonNull String packageName, int userId) {
+ maybeUnrestrictApp(packageName, userId,
+ REASON_MAIN_FORCED_BY_SYSTEM, REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY,
+ REASON_MAIN_DEFAULT, REASON_SUB_DEFAULT_APP_UPDATE);
+ }
+
+ @Override
+ public void maybeUnrestrictApp(@NonNull String packageName, int userId,
+ int prevMainReasonRestrict, int prevSubReasonRestrict,
+ int mainReasonUnrestrict, int subReasonUnrestrict) {
synchronized (mAppIdleLock) {
final long elapsedRealtime = mInjector.elapsedRealtime();
final AppIdleHistory.AppUsageHistory app =
mAppIdleHistory.getAppUsageHistory(packageName, userId, elapsedRealtime);
if (app.currentBucket != STANDBY_BUCKET_RESTRICTED
- || (app.bucketingReason & REASON_MAIN_MASK) != REASON_MAIN_FORCED_BY_SYSTEM) {
+ || (app.bucketingReason & REASON_MAIN_MASK) != prevMainReasonRestrict) {
return;
}
final int newBucket;
final int newReason;
- if ((app.bucketingReason & REASON_SUB_MASK) == REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY) {
- // If bugginess was the only reason the app should be restricted, then lift it out.
+ if ((app.bucketingReason & REASON_SUB_MASK) == prevSubReasonRestrict) {
+ // If it was the only reason the app should be restricted, then lift it out.
newBucket = STANDBY_BUCKET_RARE;
- newReason = REASON_MAIN_DEFAULT | REASON_SUB_DEFAULT_APP_UPDATE;
+ newReason = mainReasonUnrestrict | subReasonUnrestrict;
} else {
- // There's another reason the app was restricted. Remove the buggy bit and call
+ // There's another reason the app was restricted. Remove the subreason bit and call
// it a day.
newBucket = STANDBY_BUCKET_RESTRICTED;
- newReason = app.bucketingReason & ~REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY;
+ newReason = app.bucketingReason & ~prevSubReasonRestrict;
}
mAppIdleHistory.setAppStandbyBucket(
packageName, userId, elapsedRealtime, newBucket, newReason);
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index a140983..e910370 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -937,6 +937,121 @@
@EnabledSince(targetSdkVersion = VERSION_CODES.S)
public static final long LOCK_DOWN_CLOSE_SYSTEM_DIALOGS = 174664365L;
+ // The background process restriction levels. The definitions here are meant for internal
+ // bookkeeping only.
+
+ /**
+ * Not a valid restriction level.
+ *
+ * @hide
+ */
+ public static final int RESTRICTION_LEVEL_UNKNOWN = 0;
+
+ /**
+ * No background restrictions at all, this should NEVER be used
+ * for any process other than selected system processes, currently it's reserved.
+ *
+ * <p>In the future, apps in {@link #RESTRICTION_LEVEL_EXEMPTED} would receive permissive
+ * background restrictions to protect the system from buggy behaviors; in other words,
+ * the {@link #RESTRICTION_LEVEL_EXEMPTED} would not be the truly "unrestricted" state, while
+ * the {@link #RESTRICTION_LEVEL_UNRESTRICTED} here would be the last resort if there is
+ * a strong reason to grant such a capability to a system app. </p>
+ *
+ * @hide
+ */
+ public static final int RESTRICTION_LEVEL_UNRESTRICTED = 10;
+
+ /**
+ * The default background restriction level for the "unrestricted" apps set by the user,
+ * where it'll have the {@link android.app.AppOpsManager#OP_RUN_ANY_IN_BACKGROUND} set to
+ * ALLOWED, being added into the device idle allow list; however there will be still certain
+ * restrictions to apps in this level.
+ *
+ * @hide
+ */
+ public static final int RESTRICTION_LEVEL_EXEMPTED = 20;
+
+ /**
+ * The default background restriction level for all other apps, they'll be moved between
+ * various standby buckets, including
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_ACTIVE},
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_WORKING_SET},
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_FREQUENT},
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RARE}.
+ *
+ * @hide
+ */
+ public static final int RESTRICTION_LEVEL_ADAPTIVE_BUCKET = 30;
+
+ /**
+ * The background restriction level where the apps will be placed in the restricted bucket
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED}.
+ *
+ * @hide
+ */
+ public static final int RESTRICTION_LEVEL_RESTRICTED_BUCKET = 40;
+
+ /**
+ * The background restricted level, where apps would get more restrictions,
+ * such as not allowed to launch foreground services besides on TOP.
+ *
+ * @hide
+ */
+ public static final int RESTRICTION_LEVEL_BACKGROUND_RESTRICTED = 50;
+
+ /**
+ * The most restricted level where the apps are considered "in-hibernation",
+ * its package visibility to the rest of the system is limited.
+ *
+ * @hide
+ */
+ public static final int RESTRICTION_LEVEL_HIBERNATION = 60;
+
+ /**
+ * Not a valid restriction level, it defines the maximum numerical value of restriction level.
+ *
+ * @hide
+ */
+ public static final int RESTRICTION_LEVEL_MAX = 100;
+
+ /** @hide */
+ @IntDef(prefix = { "RESTRICTION_LEVEL_" }, value = {
+ RESTRICTION_LEVEL_UNKNOWN,
+ RESTRICTION_LEVEL_UNRESTRICTED,
+ RESTRICTION_LEVEL_EXEMPTED,
+ RESTRICTION_LEVEL_ADAPTIVE_BUCKET,
+ RESTRICTION_LEVEL_RESTRICTED_BUCKET,
+ RESTRICTION_LEVEL_BACKGROUND_RESTRICTED,
+ RESTRICTION_LEVEL_HIBERNATION,
+ RESTRICTION_LEVEL_MAX,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RestrictionLevel{}
+
+ /** @hide */
+ public static String restrictionLevelToName(@RestrictionLevel int level) {
+ switch (level) {
+ case RESTRICTION_LEVEL_UNKNOWN:
+ return "unknown";
+ case RESTRICTION_LEVEL_UNRESTRICTED:
+ return "unrestricted";
+ case RESTRICTION_LEVEL_EXEMPTED:
+ return "exempted";
+ case RESTRICTION_LEVEL_ADAPTIVE_BUCKET:
+ return "adaptive_bucket";
+ case RESTRICTION_LEVEL_RESTRICTED_BUCKET:
+ return "restricted_bucket";
+ case RESTRICTION_LEVEL_BACKGROUND_RESTRICTED:
+ return "background_restricted";
+ case RESTRICTION_LEVEL_HIBERNATION:
+ return "hibernation";
+ case RESTRICTION_LEVEL_MAX:
+ return "max";
+ default:
+ return "";
+ }
+ }
+
/** @hide */
public int getFrontActivityScreenCompatMode() {
try {
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 96487de..ae36646 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -22,6 +22,7 @@
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager.ProcessCapability;
+import android.app.ActivityManager.RestrictionLevel;
import android.content.ComponentName;
import android.content.IIntentReceiver;
import android.content.IIntentSender;
@@ -712,4 +713,15 @@
*/
void notifyActivityEventChanged();
}
+
+ /**
+ * Get the restriction level of the given UID, if it hosts multiple packages,
+ * return least restricted level.
+ */
+ public abstract @RestrictionLevel int getRestrictionLevel(int uid);
+
+ /**
+ * Get the restriction level of the given package for given user id.
+ */
+ public abstract @RestrictionLevel int getRestrictionLevel(String pkg, @UserIdInt int userId);
}
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index d7e197e..33efa01 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -320,6 +320,17 @@
* @hide
*/
public static final int REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY = 1 << 2;
+ /**
+ * The app was moved to restricted bucket due to user interaction, i.e., toggling FAS.
+ *
+ * <p>
+ * Note: This should be coming from the more end-user facing UX, not from developer
+ * options nor adb command.
+ </p>
+ *
+ * @hide
+ */
+ public static final int REASON_SUB_FORCED_USER_FLAG_INTERACTION = 1 << 1;
/** @hide */
@@ -336,14 +347,15 @@
public @interface StandbyBuckets {}
/** @hide */
- @IntDef(flag = true, prefix = {"REASON_SUB_FORCED_SYSTEM_FLAG_FLAG_"}, value = {
+ @IntDef(flag = true, prefix = {"REASON_SUB_FORCED_"}, value = {
REASON_SUB_FORCED_SYSTEM_FLAG_UNDEFINED,
REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE,
REASON_SUB_FORCED_SYSTEM_FLAG_ABUSE,
REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY,
+ REASON_SUB_FORCED_USER_FLAG_INTERACTION,
})
@Retention(RetentionPolicy.SOURCE)
- public @interface SystemForcedReasons {
+ public @interface ForcedReasons {
}
/**
@@ -1188,11 +1200,6 @@
case REASON_MAIN_FORCED_BY_USER:
sb.append("f");
if (subReason > 0) {
- // Although not expected and shouldn't happen, this could potentially have a
- // sub-reason if the system tries to give a reason when applying the
- // FORCED_BY_USER reason. The sub-reason is undefined (though most likely a
- // REASON_SUB_FORCED_SYSTEM_FLAG_ sub-reason), but it's better to note it in the
- // log than to exclude it altogether.
sb.append("-").append(Integer.toBinaryString(subReason));
}
break;
diff --git a/core/java/android/util/SparseArrayMap.java b/core/java/android/util/SparseArrayMap.java
index cd592a7..e5bb9f45 100644
--- a/core/java/android/util/SparseArrayMap.java
+++ b/core/java/android/util/SparseArrayMap.java
@@ -62,6 +62,14 @@
}
/**
+ * Removes all the data for the keyIndex, if there was any.
+ * @hide
+ */
+ public void deleteAt(int keyIndex) {
+ mData.removeAt(keyIndex);
+ }
+
+ /**
* Removes the data for the key and mapKey, if there was any.
*
* @return Returns the value that was stored under the keys, or null if there was none.
@@ -142,6 +150,15 @@
return data == null ? 0 : data.size();
}
+ /**
+ * Returns the number of elements in the map of the given keyIndex.
+ * @hide
+ */
+ public int numElementsForKeyAt(int keyIndex) {
+ ArrayMap<K, V> data = mData.valueAt(keyIndex);
+ return data == null ? 0 : data.size();
+ }
+
/** Returns the value V at the given key and map index. */
@Nullable
public V valueAt(int keyIndex, int mapIndex) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b1b4c44..a9263fe 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -153,6 +153,7 @@
import android.app.ActivityManager;
import android.app.ActivityManager.PendingIntentInfo;
import android.app.ActivityManager.ProcessCapability;
+import android.app.ActivityManager.RestrictionLevel;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityManagerInternal;
import android.app.ActivityTaskManager.RootTaskInfo;
@@ -186,7 +187,6 @@
import android.app.PendingIntent;
import android.app.ProcessMemoryState;
import android.app.ProfilerInfo;
-import android.app.PropertyInvalidatedCache;
import android.app.SyncNotedAppOp;
import android.app.WaitResult;
import android.app.backup.BackupManager.OperationType;
@@ -233,11 +233,9 @@
import android.content.pm.ProviderInfo;
import android.content.pm.ProviderInfoList;
import android.content.pm.ResolveInfo;
-import com.android.server.pm.pkg.SELinuxUtil;
import android.content.pm.ServiceInfo;
import android.content.pm.TestUtilityService;
import android.content.pm.UserInfo;
-import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -396,6 +394,8 @@
import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.parsing.pkg.AndroidPackage;
import com.android.server.pm.permission.PermissionManagerServiceInternal;
+import com.android.server.pm.pkg.SELinuxUtil;
+import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
import com.android.server.uri.GrantUri;
import com.android.server.uri.NeededUriGrants;
import com.android.server.uri.UriGrantsManagerInternal;
@@ -1456,6 +1456,8 @@
final UidObserverController mUidObserverController;
+ final AppRestrictionController mAppRestrictionController;
+
private final class AppDeathRecipient implements IBinder.DeathRecipient {
final ProcessRecord mApp;
final int mPid;
@@ -2269,6 +2271,7 @@
mPendingIntentController = hasHandlerThread
? new PendingIntentController(handlerThread.getLooper(), mUserController,
mConstants) : null;
+ mAppRestrictionController = new AppRestrictionController(mContext);
mProcStartHandlerThread = null;
mProcStartHandler = null;
mHiddenApiBlacklist = null;
@@ -2378,6 +2381,8 @@
mPendingIntentController = new PendingIntentController(
mHandlerThread.getLooper(), mUserController, mConstants);
+ mAppRestrictionController = new AppRestrictionController(mContext);
+
mUseFifoUiScheduling = SystemProperties.getInt("sys.use_fifo_ui", 0) != 0;
mTrackingAssociations = "1".equals(SystemProperties.get("debug.track-associations"));
@@ -7746,6 +7751,7 @@
mUserController.onSystemReady();
mAppOpsService.systemReady();
mProcessList.onSystemReady();
+ mAppRestrictionController.onSystemReady();
mSystemReady = true;
t.traceEnd();
}
@@ -9029,6 +9035,10 @@
}
mComponentAliasResolver.dump(pw);
}
+ if (dumpAll) {
+ pw.println("-------------------------------------------------------------------------------");
+ mAppRestrictionController.dump(pw, "");
+ }
}
/**
@@ -16818,6 +16828,16 @@
public void setStopUserOnSwitch(int value) {
ActivityManagerService.this.setStopUserOnSwitch(value);
}
+
+ @Override
+ public @RestrictionLevel int getRestrictionLevel(int uid) {
+ return mAppRestrictionController.getRestrictionLevel(uid);
+ }
+
+ @Override
+ public @RestrictionLevel int getRestrictionLevel(String pkg, @UserIdInt int userId) {
+ return mAppRestrictionController.getRestrictionLevel(pkg, userId);
+ }
}
long inputDispatchingTimedOut(int pid, final boolean aboveSystem, String reason) {
diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java
new file mode 100644
index 0000000..85cccee
--- /dev/null
+++ b/services/core/java/com/android/server/am/AppRestrictionController.java
@@ -0,0 +1,1074 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static android.app.ActivityManager.RESTRICTION_LEVEL_ADAPTIVE_BUCKET;
+import static android.app.ActivityManager.RESTRICTION_LEVEL_BACKGROUND_RESTRICTED;
+import static android.app.ActivityManager.RESTRICTION_LEVEL_EXEMPTED;
+import static android.app.ActivityManager.RESTRICTION_LEVEL_HIBERNATION;
+import static android.app.ActivityManager.RESTRICTION_LEVEL_RESTRICTED_BUCKET;
+import static android.app.ActivityManager.RESTRICTION_LEVEL_UNKNOWN;
+import static android.app.ActivityManager.UID_OBSERVER_ACTIVE;
+import static android.app.ActivityManager.UID_OBSERVER_GONE;
+import static android.app.ActivityManager.UID_OBSERVER_IDLE;
+import static android.app.usage.UsageStatsManager.REASON_MAIN_DEFAULT;
+import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_SYSTEM;
+import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_USER;
+import static android.app.usage.UsageStatsManager.REASON_MAIN_MASK;
+import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE;
+import static android.app.usage.UsageStatsManager.REASON_SUB_DEFAULT_UNDEFINED;
+import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_UNDEFINED;
+import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_USER_FLAG_INTERACTION;
+import static android.app.usage.UsageStatsManager.REASON_SUB_MASK;
+import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_USER_INTERACTION;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_EXEMPTED;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
+import static android.app.usage.UsageStatsManager.reasonToString;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+import static android.content.pm.PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
+
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.annotation.ElapsedRealtimeLong;
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.ActivityManager.RestrictionLevel;
+import android.app.ActivityThread;
+import android.app.AppOpsManager;
+import android.app.IActivityManager;
+import android.app.IUidObserver;
+import android.app.usage.AppStandbyInfo;
+import android.app.usage.UsageStatsManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.OnPropertiesChangedListener;
+import android.provider.DeviceConfig.Properties;
+import android.util.Slog;
+import android.util.SparseArrayMap;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.function.TriConsumer;
+import com.android.server.AppStateTracker;
+import com.android.server.LocalServices;
+import com.android.server.apphibernation.AppHibernationManagerInternal;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.usage.AppStandbyInternal;
+import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * This class tracks various state of the apps and mutates their restriction levels accordingly.
+ */
+public final class AppRestrictionController {
+ static final String TAG = TAG_WITH_CLASS_NAME ? "AppRestrictionController" : TAG_AM;
+ static final boolean DEBUG_BG_RESTRICTION_CONTROLLER = false;
+
+ /**
+ * The prefix for the sub-namespace of our device configs under
+ * the {@link android.provider.DeviceConfig#NAMESPACE_ACTIVITY_MANAGER}.
+ */
+ static final String DEVICE_CONFIG_SUBNAMESPACE_PREFIX = "bg_";
+
+ static final int STOCK_PM_FLAGS = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE
+ | MATCH_DISABLED_UNTIL_USED_COMPONENTS;
+
+ private final Context mContext;
+ private final HandlerThread mBgHandlerThread;
+ private final BgHandler mBgHandler;
+
+ // No lock is needed, as it's immutable after initialization in constructor.
+ private final ArrayList<BaseAppStateTracker> mAppStateTrackers = new ArrayList<>();
+
+ @GuardedBy("mLock")
+ private final RestrictionSettings mRestrictionSettings = new RestrictionSettings();
+
+ private final CopyOnWriteArraySet<AppRestrictionLevelListener> mRestrictionLevelListeners =
+ new CopyOnWriteArraySet<>();
+
+ /**
+ * A mapping between the UID/Pkg and its pending work which should be triggered on inactive;
+ * an active UID/pkg pair should have an entry here, although its pending work could be null.
+ */
+ @GuardedBy("mLock")
+ private final SparseArrayMap<String, Runnable> mActiveUids = new SparseArrayMap<>();
+
+ // No lock is needed as it's accessed in bg handler thread only.
+ private final ArrayList<Runnable> mTmpRunnables = new ArrayList<>();
+
+ private final Object mLock = new Object();
+ private final Injector mInjector;
+
+ /**
+ * The restriction levels that each package is on, the levels here are defined in
+ * {@link android.app.ActivityManager.RESTRICTION_LEVEL_*}.
+ */
+ final class RestrictionSettings {
+ @GuardedBy("mLock")
+ final SparseArrayMap<String, PkgSettings> mRestrictionLevels = new SparseArrayMap();
+
+ final class PkgSettings {
+ private final String mPackageName;
+ private final int mUid;
+
+ private @RestrictionLevel int mCurrentRestrictionLevel;
+ private @RestrictionLevel int mLastRestrictionLevel;
+ private @ElapsedRealtimeLong long mLevelChangeTimeElapsed;
+ private int mReason;
+
+ PkgSettings(String packageName, int uid) {
+ mPackageName = packageName;
+ mUid = uid;
+ mCurrentRestrictionLevel = mLastRestrictionLevel = RESTRICTION_LEVEL_UNKNOWN;
+ }
+
+ @RestrictionLevel int update(@RestrictionLevel int level, int reason, int subReason) {
+ if (level != mCurrentRestrictionLevel) {
+ mLastRestrictionLevel = mCurrentRestrictionLevel;
+ mCurrentRestrictionLevel = level;
+ mLevelChangeTimeElapsed = SystemClock.elapsedRealtime();
+ mReason = (REASON_MAIN_MASK & reason) | (REASON_SUB_MASK & subReason);
+ mBgHandler.obtainMessage(BgHandler.MSG_APP_RESTRICTION_LEVEL_CHANGED,
+ mUid, level, mPackageName).sendToTarget();
+ }
+ return mLastRestrictionLevel;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder(128);
+ sb.append("RestrictionLevel{");
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ sb.append(':');
+ sb.append(mPackageName);
+ sb.append('/');
+ sb.append(UserHandle.formatUid(mUid));
+ sb.append('}');
+ sb.append(' ');
+ sb.append(ActivityManager.restrictionLevelToName(mCurrentRestrictionLevel));
+ sb.append('(');
+ sb.append(reasonToString(mReason));
+ sb.append(')');
+ return sb.toString();
+ }
+
+ void dump(PrintWriter pw, @ElapsedRealtimeLong long nowElapsed) {
+ pw.print(toString());
+ if (mLastRestrictionLevel != RESTRICTION_LEVEL_UNKNOWN) {
+ pw.print('/');
+ pw.print(ActivityManager.restrictionLevelToName(mLastRestrictionLevel));
+ }
+ pw.print(' ');
+ TimeUtils.formatDuration(mLevelChangeTimeElapsed - nowElapsed, pw);
+ }
+
+ String getPackageName() {
+ return mPackageName;
+ }
+
+ int getUid() {
+ return mUid;
+ }
+
+ @RestrictionLevel int getCurrentRestrictionLevel() {
+ return mCurrentRestrictionLevel;
+ }
+
+ @RestrictionLevel int getLastRestrictionLevel() {
+ return mLastRestrictionLevel;
+ }
+
+ int getReason() {
+ return mReason;
+ }
+ }
+
+ /**
+ * Update the restriction level.
+ *
+ * @return The previous restriction level.
+ */
+ @RestrictionLevel int update(String packageName, int uid, @RestrictionLevel int level,
+ int reason, int subReason) {
+ synchronized (mLock) {
+ PkgSettings settings = mRestrictionLevels.get(uid, packageName);
+ if (settings == null) {
+ settings = new PkgSettings(packageName, uid);
+ mRestrictionLevels.add(uid, packageName, settings);
+ }
+ return settings.update(level, reason, subReason);
+ }
+ }
+
+ /**
+ * @return The reason of why it's in this level.
+ */
+ int getReason(String packageName, int uid) {
+ synchronized (mLock) {
+ final PkgSettings settings = mRestrictionLevels.get(uid, packageName);
+ return settings != null ? settings.getReason()
+ : (REASON_MAIN_DEFAULT | REASON_SUB_DEFAULT_UNDEFINED);
+ }
+ }
+
+ @RestrictionLevel int getRestrictionLevel(int uid) {
+ synchronized (mLock) {
+ final int uidKeyIndex = mRestrictionLevels.indexOfKey(uid);
+ if (uidKeyIndex < 0) {
+ return RESTRICTION_LEVEL_UNKNOWN;
+ }
+ final int numPackages = mRestrictionLevels.numElementsForKeyAt(uidKeyIndex);
+ if (numPackages == 0) {
+ return RESTRICTION_LEVEL_UNKNOWN;
+ }
+ @RestrictionLevel int level = RESTRICTION_LEVEL_UNKNOWN;
+ for (int i = 0; i < numPackages; i++) {
+ final PkgSettings setting = mRestrictionLevels.valueAt(uidKeyIndex, i);
+ if (setting != null) {
+ final @RestrictionLevel int l = setting.getCurrentRestrictionLevel();
+ level = (level == RESTRICTION_LEVEL_UNKNOWN) ? l : Math.min(level, l);
+ }
+ }
+ return level;
+ }
+ }
+
+ @RestrictionLevel int getRestrictionLevel(int uid, String packageName) {
+ synchronized (mLock) {
+ final PkgSettings settings = mRestrictionLevels.get(uid, packageName);
+ return settings == null
+ ? getRestrictionLevel(uid) : settings.getCurrentRestrictionLevel();
+ }
+ }
+
+ @RestrictionLevel int getRestrictionLevel(String packageName, @UserIdInt int userId) {
+ final PackageManagerInternal pm = mInjector.getPackageManagerInternal();
+ final int uid = pm.getPackageUid(packageName, STOCK_PM_FLAGS, userId);
+ return getRestrictionLevel(uid, packageName);
+ }
+
+ private @RestrictionLevel int getLastRestrictionLevel(int uid, String packageName) {
+ synchronized (mLock) {
+ final PkgSettings settings = mRestrictionLevels.get(uid, packageName);
+ return settings.getLastRestrictionLevel();
+ }
+ }
+
+ @GuardedBy("mLock")
+ void forEachPackageInUidLocked(int uid,
+ @NonNull TriConsumer<String, Integer, Integer> consumer) {
+ final int uidKeyIndex = mRestrictionLevels.indexOfKey(uid);
+ if (uidKeyIndex < 0) {
+ return;
+ }
+ final int numPackages = mRestrictionLevels.numElementsForKeyAt(uidKeyIndex);
+ for (int i = 0; i < numPackages; i++) {
+ final PkgSettings settings = mRestrictionLevels.valueAt(uidKeyIndex, i);
+ consumer.accept(mRestrictionLevels.keyAt(uidKeyIndex, i),
+ settings.getCurrentRestrictionLevel(), settings.getReason());
+ }
+ }
+
+ void removeUser(@UserIdInt int userId) {
+ synchronized (mLock) {
+ for (int i = mRestrictionLevels.numMaps() - 1; i >= 0; i--) {
+ final int uid = mRestrictionLevels.keyAt(i);
+ if (UserHandle.getUserId(uid) != userId) {
+ continue;
+ }
+ mRestrictionLevels.deleteAt(i);
+ }
+ }
+ }
+
+ void removePackage(String pkgName, int uid) {
+ synchronized (mLock) {
+ mRestrictionLevels.delete(uid, pkgName);
+ }
+ }
+
+ void removeUid(int uid) {
+ synchronized (mLock) {
+ mRestrictionLevels.delete(uid);
+ }
+ }
+
+ @GuardedBy("mLock")
+ void dumpLocked(PrintWriter pw, String prefix) {
+ final ArrayList<PkgSettings> settings = new ArrayList<>();
+ mRestrictionLevels.forEach(setting -> settings.add(setting));
+ Collections.sort(settings, Comparator.comparingInt(PkgSettings::getUid));
+ final long nowElapsed = SystemClock.elapsedRealtime();
+ for (int i = 0, size = settings.size(); i < size; i++) {
+ pw.print(prefix);
+ pw.print('#');
+ pw.print(i);
+ pw.print(' ');
+ settings.get(i).dump(pw, nowElapsed);
+ pw.println();
+ }
+ }
+ }
+
+ private final OnPropertiesChangedListener mOnDeviceConfigChangedListener =
+ new OnPropertiesChangedListener() {
+ @Override
+ public void onPropertiesChanged(Properties properties) {
+ for (String name : properties.getKeyset()) {
+ if (name == null || !name.startsWith(DEVICE_CONFIG_SUBNAMESPACE_PREFIX)) {
+ return;
+ }
+ AppRestrictionController.this.onPropertiesChanged(name);
+ }
+ }
+ };
+
+ private final AppStateTracker.BackgroundRestrictedAppListener mBackgroundRestrictionListener =
+ new AppStateTracker.BackgroundRestrictedAppListener() {
+ @Override
+ public void updateBackgroundRestrictedForUidPackage(int uid, String packageName,
+ boolean restricted) {
+ mBgHandler.obtainMessage(BgHandler.MSG_BACKGROUND_RESTRICTION_CHANGED,
+ uid, restricted ? 1 : 0, packageName).sendToTarget();
+ }
+ };
+
+ private final AppIdleStateChangeListener mAppIdleStateChangeListener =
+ new AppIdleStateChangeListener() {
+ @Override
+ public void onAppIdleStateChanged(String packageName, @UserIdInt int userId,
+ boolean idle, int bucket, int reason) {
+ mBgHandler.obtainMessage(BgHandler.MSG_APP_STANDBY_BUCKET_CHANGED,
+ userId, bucket, packageName).sendToTarget();
+ }
+
+ @Override
+ public void onUserInteractionStarted(String packageName, @UserIdInt int userId) {
+ mBgHandler.obtainMessage(BgHandler.MSG_USER_INTERACTION_STARTED,
+ userId, 0, packageName).sendToTarget();
+ }
+ };
+
+ private final IUidObserver mUidObserver =
+ new IUidObserver.Stub() {
+ @Override
+ public void onUidGone(int uid, boolean disabled) {
+ mBgHandler.obtainMessage(BgHandler.MSG_UID_INACTIVE, uid, disabled ? 1 : 0)
+ .sendToTarget();
+ }
+
+ @Override
+ public void onUidIdle(int uid, boolean disabled) {
+ mBgHandler.obtainMessage(BgHandler.MSG_UID_INACTIVE, uid, disabled ? 1 : 0)
+ .sendToTarget();
+ }
+
+ @Override
+ public void onUidActive(int uid) {
+ mBgHandler.obtainMessage(BgHandler.MSG_UID_ACTIVE, uid, 0).sendToTarget();
+ }
+
+ @Override
+ public void onUidStateChanged(int uid, int procState, long procStateSeq,
+ int capability) {
+ }
+
+ @Override
+ public void onUidCachedChanged(int uid, boolean cached) {
+ }
+ };
+
+ /**
+ * A listener interface, which will be notified on restriction level changes.
+ */
+ public interface AppRestrictionLevelListener {
+ /**
+ * Called when the restriction level of given uid/package is changed.
+ */
+ void onRestrictionLevelChanged(int uid, String packageName, @RestrictionLevel int newLevel);
+ }
+
+ /**
+ * Register the restriction level listener callback.
+ */
+ public void addAppRestrictionLevelListener(@NonNull AppRestrictionLevelListener listener) {
+ mRestrictionLevelListeners.add(listener);
+ }
+
+ AppRestrictionController(final Context context) {
+ this(new Injector(context));
+ }
+
+ AppRestrictionController(Injector injector) {
+ mInjector = injector;
+ mContext = injector.getContext();
+ mBgHandlerThread = new HandlerThread("bgres-controller");
+ mBgHandlerThread.start();
+ mBgHandler = new BgHandler(mBgHandlerThread.getLooper(), injector);
+ injector.initAppStateTrackers(this);
+ }
+
+ void onSystemReady() {
+ DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ ActivityThread.currentApplication().getMainExecutor(),
+ mOnDeviceConfigChangedListener);
+ initRestrictionStates();
+ registerForUidObservers();
+ registerForSystemBroadcasts();
+ mInjector.getAppStateTracker().addBackgroundRestrictedAppListener(
+ mBackgroundRestrictionListener);
+ mInjector.getAppStandbyInternal().addListener(mAppIdleStateChangeListener);
+ for (int i = 0, size = mAppStateTrackers.size(); i < size; i++) {
+ mAppStateTrackers.get(i).onSystemReady();
+ }
+ }
+
+ private void initRestrictionStates() {
+ final int[] allUsers = mInjector.getUserManagerInternal().getUserIds();
+ for (int userId : allUsers) {
+ refreshAppRestrictionLevelForUser(userId, REASON_MAIN_FORCED_BY_USER,
+ REASON_SUB_FORCED_USER_FLAG_INTERACTION);
+ }
+ }
+
+ private void registerForUidObservers() {
+ try {
+ mInjector.getIActivityManager().registerUidObserver(mUidObserver,
+ UID_OBSERVER_ACTIVE | UID_OBSERVER_GONE | UID_OBSERVER_IDLE,
+ ActivityManager.PROCESS_STATE_UNKNOWN, "android");
+ } catch (RemoteException e) {
+ // Intra-process call, it won't happen.
+ }
+ }
+
+ /**
+ * Called when initializing a user.
+ */
+ private void refreshAppRestrictionLevelForUser(@UserIdInt int userId, int reason,
+ int subReason) {
+ final List<AppStandbyInfo> appStandbyInfos = mInjector.getAppStandbyInternal()
+ .getAppStandbyBuckets(userId);
+ if (ArrayUtils.isEmpty(appStandbyInfos)) {
+ return;
+ }
+
+ if (DEBUG_BG_RESTRICTION_CONTROLLER) {
+ Slog.i(TAG, "Refreshing restriction levels of user " + userId);
+ }
+ final PackageManagerInternal pm = mInjector.getPackageManagerInternal();
+ for (AppStandbyInfo info: appStandbyInfos) {
+ final int uid = pm.getPackageUid(info.mPackageName, STOCK_PM_FLAGS, userId);
+ if (uid < 0) {
+ // Shouldn't happen.
+ Slog.e(TAG, "Unable to find " + info.mPackageName + "/u" + userId);
+ continue;
+ }
+ final @RestrictionLevel int level = calcAppRestrictionLevel(
+ userId, uid, info.mPackageName, info.mStandbyBucket, false, false);
+ applyRestrictionLevel(info.mPackageName, uid, level,
+ info.mStandbyBucket, true, reason, subReason);
+ }
+ }
+
+ void refreshAppRestrictionLevelForUid(int uid, int reason, int subReason,
+ boolean allowRequestBgRestricted) {
+ final String[] packages = mInjector.getPackageManager().getPackagesForUid(uid);
+ if (ArrayUtils.isEmpty(packages)) {
+ return;
+ }
+ final AppStandbyInternal appStandbyInternal = mInjector.getAppStandbyInternal();
+ final int userId = UserHandle.getUserId(uid);
+ final long now = SystemClock.elapsedRealtime();
+ for (String pkg: packages) {
+ final int curBucket = appStandbyInternal.getAppStandbyBucket(pkg, userId, now, false);
+ final @RestrictionLevel int level = calcAppRestrictionLevel(userId, uid, pkg,
+ curBucket, allowRequestBgRestricted, true);
+ if (DEBUG_BG_RESTRICTION_CONTROLLER) {
+ Slog.i(TAG, "Proposed restriction level of " + pkg + "/"
+ + UserHandle.formatUid(uid) + ": "
+ + ActivityManager.restrictionLevelToName(level));
+ }
+ applyRestrictionLevel(pkg, uid, level, curBucket, true, reason, subReason);
+ }
+ }
+
+ private @RestrictionLevel int calcAppRestrictionLevel(@UserIdInt int userId, int uid,
+ String packageName, @UsageStatsManager.StandbyBuckets int standbyBucket,
+ boolean allowRequestBgRestricted, boolean calcTrackers) {
+ if (mInjector.getAppHibernationInternal().isHibernatingForUser(packageName, userId)) {
+ return RESTRICTION_LEVEL_HIBERNATION;
+ }
+ @RestrictionLevel int level;
+ switch (standbyBucket) {
+ case STANDBY_BUCKET_EXEMPTED:
+ level = RESTRICTION_LEVEL_EXEMPTED;
+ break;
+ case STANDBY_BUCKET_NEVER:
+ level = RESTRICTION_LEVEL_BACKGROUND_RESTRICTED;
+ break;
+ case STANDBY_BUCKET_ACTIVE:
+ case STANDBY_BUCKET_WORKING_SET:
+ case STANDBY_BUCKET_FREQUENT:
+ case STANDBY_BUCKET_RARE:
+ case STANDBY_BUCKET_RESTRICTED:
+ default:
+ if (mInjector.getAppStateTracker()
+ .isAppBackgroundRestricted(uid, packageName)) {
+ return RESTRICTION_LEVEL_BACKGROUND_RESTRICTED;
+ }
+ level = standbyBucket == STANDBY_BUCKET_RESTRICTED
+ ? RESTRICTION_LEVEL_RESTRICTED_BUCKET
+ : RESTRICTION_LEVEL_ADAPTIVE_BUCKET;
+ if (calcTrackers) {
+ @RestrictionLevel int l = calcAppRestrictionLevelFromTackers(uid, packageName);
+ if (l == RESTRICTION_LEVEL_EXEMPTED) {
+ return RESTRICTION_LEVEL_EXEMPTED;
+ }
+ level = Math.max(l, level);
+ if (level == RESTRICTION_LEVEL_BACKGROUND_RESTRICTED) {
+ // This level can't be entered without user consent
+ if (allowRequestBgRestricted) {
+ mBgHandler.obtainMessage(BgHandler.MSG_REQUEST_BG_RESTRICTED,
+ uid, 0, packageName).sendToTarget();
+ }
+ // Lower the level.
+ level = RESTRICTION_LEVEL_RESTRICTED_BUCKET;
+ }
+ }
+ break;
+ }
+ return level;
+ }
+
+ /**
+ * Ask each of the trackers for their proposed restriction levels for the given uid/package,
+ * and return the most restrictive level.
+ *
+ * <p>Note, it's different from the {@link #getRestrictionLevel} where it returns the least
+ * restrictive level. We're returning the most restrictive level here because each tracker
+ * monitors certain dimensions of the app, the abusive behaviors could be detected in one or
+ * more of these dimensions, but not necessarily all of them. </p>
+ */
+ private @RestrictionLevel int calcAppRestrictionLevelFromTackers(int uid, String packageName) {
+ @RestrictionLevel int level = RESTRICTION_LEVEL_UNKNOWN;
+ for (int i = mAppStateTrackers.size() - 1; i >= 0; i--) {
+ @RestrictionLevel int l = mAppStateTrackers.get(i).getPolicy()
+ .getProposedRestrictionLevel(packageName, uid);
+ level = Math.max(level, l);
+ }
+ return level;
+ }
+
+ /**
+ * Get the restriction level of the given UID, if it hosts multiple packages,
+ * return least restricted one (or if any of them is exempted).
+ */
+ @RestrictionLevel int getRestrictionLevel(int uid) {
+ return mRestrictionSettings.getRestrictionLevel(uid);
+ }
+
+ /**
+ * Get the restriction level of the given UID and package.
+ */
+ @RestrictionLevel int getRestrictionLevel(int uid, String packageName) {
+ return mRestrictionSettings.getRestrictionLevel(uid, packageName);
+ }
+
+ /**
+ * Get the restriction level of the given package in given user id.
+ */
+ @RestrictionLevel int getRestrictionLevel(String packageName, @UserIdInt int userId) {
+ return mRestrictionSettings.getRestrictionLevel(packageName, userId);
+ }
+
+ void dump(PrintWriter pw, String prefix) {
+ pw.print(prefix);
+ pw.println("BACKGROUND RESTRICTION LEVEL SETTINGS");
+ synchronized (mLock) {
+ mRestrictionSettings.dumpLocked(pw, prefix + " ");
+ }
+ }
+
+ private void applyRestrictionLevel(String pkgName, int uid, @RestrictionLevel int level,
+ int curBucket, boolean allowUpdateBucket, int reason, int subReason) {
+ int curLevel;
+ int prevReason;
+ synchronized (mLock) {
+ curLevel = getRestrictionLevel(uid, pkgName);
+ if (curLevel == level) {
+ // Nothing to do.
+ return;
+ }
+ if (DEBUG_BG_RESTRICTION_CONTROLLER) {
+ Slog.i(TAG, "Updating the restriction level of " + pkgName + "/"
+ + UserHandle.formatUid(uid) + " from "
+ + ActivityManager.restrictionLevelToName(curLevel) + " to "
+ + ActivityManager.restrictionLevelToName(level)
+ + " reason=" + reason + ", subReason=" + subReason);
+ }
+
+ prevReason = mRestrictionSettings.getReason(pkgName, uid);
+ mRestrictionSettings.update(pkgName, uid, level, reason, subReason);
+ }
+
+ if (!allowUpdateBucket || curBucket == STANDBY_BUCKET_EXEMPTED) {
+ return;
+ }
+ final AppStandbyInternal appStandbyInternal = mInjector.getAppStandbyInternal();
+ if (level >= RESTRICTION_LEVEL_RESTRICTED_BUCKET
+ && curLevel < RESTRICTION_LEVEL_RESTRICTED_BUCKET) {
+ // Moving the app standby bucket to restricted in the meanwhile.
+ if (DEBUG_BG_RESTRICTION_CONTROLLER
+ && level == RESTRICTION_LEVEL_BACKGROUND_RESTRICTED) {
+ Slog.i(TAG, pkgName + "/" + UserHandle.formatUid(uid)
+ + " is bg-restricted, moving to restricted standby bucket");
+ }
+ if (curBucket != STANDBY_BUCKET_RESTRICTED) {
+ // restrict the app if it hasn't done so.
+ boolean doIt = true;
+ synchronized (mLock) {
+ final int index = mActiveUids.indexOfKey(uid, pkgName);
+ if (index >= 0) {
+ // It's currently active, enqueue it.
+ mActiveUids.add(uid, pkgName, () -> appStandbyInternal.restrictApp(
+ pkgName, UserHandle.getUserId(uid), reason, subReason));
+ doIt = false;
+ }
+ }
+ if (doIt) {
+ appStandbyInternal.restrictApp(pkgName, UserHandle.getUserId(uid),
+ reason, subReason);
+ }
+ }
+ } else if (curLevel >= RESTRICTION_LEVEL_RESTRICTED_BUCKET
+ && level < RESTRICTION_LEVEL_RESTRICTED_BUCKET) {
+ // Moved out of the background-restricted state.
+ if (curBucket != STANDBY_BUCKET_RARE) {
+ synchronized (mLock) {
+ final int index = mActiveUids.indexOfKey(uid, pkgName);
+ if (index >= 0) {
+ mActiveUids.add(uid, pkgName, null);
+ }
+ }
+ appStandbyInternal.maybeUnrestrictApp(pkgName, UserHandle.getUserId(uid),
+ prevReason & REASON_MAIN_MASK, prevReason & REASON_SUB_MASK,
+ reason, subReason);
+ }
+ }
+ }
+
+ private void handleBackgroundRestrictionChanged(int uid, String pkgName, boolean restricted) {
+ // Firstly, notify the trackers.
+ for (int i = 0, size = mAppStateTrackers.size(); i < size; i++) {
+ mAppStateTrackers.get(i)
+ .onBackgroundRestrictionChanged(uid, pkgName, restricted);
+ }
+
+ final AppStandbyInternal appStandbyInternal = mInjector.getAppStandbyInternal();
+ final int userId = UserHandle.getUserId(uid);
+ final long now = SystemClock.elapsedRealtime();
+ final int curBucket = appStandbyInternal.getAppStandbyBucket(pkgName, userId, now, false);
+ if (restricted) {
+ // The app could fall into the background restricted with user consent only,
+ // so set the reason to it.
+ applyRestrictionLevel(pkgName, uid, RESTRICTION_LEVEL_BACKGROUND_RESTRICTED,
+ curBucket, true, REASON_MAIN_FORCED_BY_USER,
+ REASON_SUB_FORCED_USER_FLAG_INTERACTION);
+ } else {
+ // Moved out of the background-restricted state, we'd need to check if it should
+ // stay in the restricted standby bucket.
+ final @RestrictionLevel int lastLevel =
+ mRestrictionSettings.getLastRestrictionLevel(uid, pkgName);
+ final int tentativeBucket = curBucket == STANDBY_BUCKET_EXEMPTED
+ ? STANDBY_BUCKET_EXEMPTED
+ : (lastLevel == RESTRICTION_LEVEL_RESTRICTED_BUCKET
+ ? STANDBY_BUCKET_RESTRICTED : STANDBY_BUCKET_RARE);
+ final @RestrictionLevel int level = calcAppRestrictionLevel(
+ UserHandle.getUserId(uid), uid, pkgName, tentativeBucket, false, true);
+
+ applyRestrictionLevel(pkgName, uid, level, curBucket, true,
+ REASON_MAIN_USAGE, REASON_SUB_USAGE_USER_INTERACTION);
+ }
+ }
+
+ private void dispatchAppRestrictionLevelChanges(int uid, String pkgName,
+ @RestrictionLevel int newLevel) {
+ mRestrictionLevelListeners.forEach(
+ l -> l.onRestrictionLevelChanged(uid, pkgName, newLevel));
+ }
+
+ private void handleAppStandbyBucketChanged(int bucket, String packageName,
+ @UserIdInt int userId) {
+ final int uid = mInjector.getPackageManagerInternal().getPackageUid(
+ packageName, STOCK_PM_FLAGS, userId);
+ final @RestrictionLevel int level = calcAppRestrictionLevel(
+ userId, uid, packageName, bucket, false, false);
+ applyRestrictionLevel(packageName, uid, level, bucket, false,
+ REASON_MAIN_DEFAULT, REASON_SUB_DEFAULT_UNDEFINED);
+ }
+
+ void handleRequestBgRestricted(String packageName, int uid) {
+ if (DEBUG_BG_RESTRICTION_CONTROLLER) {
+ Slog.i(TAG, "Requesting background restricted " + packageName + " "
+ + UserHandle.formatUid(uid));
+ }
+ // TODO: b/200326767 - show the request notification.
+ }
+
+ void handleUidInactive(int uid, boolean disabled) {
+ final ArrayList<Runnable> pendingTasks = mTmpRunnables;
+ synchronized (mLock) {
+ final int index = mActiveUids.indexOfKey(uid);
+ if (index < 0) {
+ return;
+ }
+ final int numPackages = mActiveUids.numElementsForKeyAt(index);
+ for (int i = 0; i < numPackages; i++) {
+ final Runnable pendingTask = mActiveUids.valueAt(index, i);
+ if (pendingTask != null) {
+ pendingTasks.add(pendingTask);
+ }
+ }
+ mActiveUids.deleteAt(index);
+ }
+ for (int i = 0, size = pendingTasks.size(); i < size; i++) {
+ pendingTasks.get(i).run();
+ }
+ pendingTasks.clear();
+ }
+
+ void handleUidActive(int uid) {
+ synchronized (mLock) {
+ final AppStandbyInternal appStandbyInternal = mInjector.getAppStandbyInternal();
+ final int userId = UserHandle.getUserId(uid);
+ mRestrictionSettings.forEachPackageInUidLocked(uid, (pkgName, level, reason) -> {
+ if (level == RESTRICTION_LEVEL_BACKGROUND_RESTRICTED) {
+ mActiveUids.add(uid, pkgName, () -> appStandbyInternal.restrictApp(pkgName,
+ userId, reason & REASON_MAIN_MASK, reason & REASON_SUB_MASK));
+ } else {
+ mActiveUids.add(uid, pkgName, null);
+ }
+ });
+ }
+ }
+
+ /**
+ * @return The background handler of this controller.
+ */
+ Handler getBackgroundHandler() {
+ return mBgHandler;
+ }
+
+ /**
+ * @return The background handler thread of this controller.
+ */
+ @VisibleForTesting
+ HandlerThread getBackgroundHandlerThread() {
+ return mBgHandlerThread;
+ }
+
+ /**
+ * @return The global lock of this controller.
+ */
+ Object getLock() {
+ return mLock;
+ }
+
+ static class BgHandler extends Handler {
+ static final int MSG_BACKGROUND_RESTRICTION_CHANGED = 0;
+ static final int MSG_APP_RESTRICTION_LEVEL_CHANGED = 1;
+ static final int MSG_APP_STANDBY_BUCKET_CHANGED = 2;
+ static final int MSG_USER_INTERACTION_STARTED = 3;
+ static final int MSG_REQUEST_BG_RESTRICTED = 4;
+ static final int MSG_UID_INACTIVE = 5;
+ static final int MSG_UID_ACTIVE = 6;
+
+ private final Injector mInjector;
+
+ BgHandler(Looper looper, Injector injector) {
+ super(looper);
+ mInjector = injector;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final AppRestrictionController c = mInjector
+ .getAppRestrictionController();
+ switch (msg.what) {
+ case MSG_BACKGROUND_RESTRICTION_CHANGED: {
+ c.handleBackgroundRestrictionChanged(msg.arg1, (String) msg.obj, msg.arg2 == 1);
+ } break;
+ case MSG_APP_RESTRICTION_LEVEL_CHANGED: {
+ c.dispatchAppRestrictionLevelChanges(msg.arg1, (String) msg.obj, msg.arg2);
+ } break;
+ case MSG_APP_STANDBY_BUCKET_CHANGED: {
+ c.handleAppStandbyBucketChanged(msg.arg2, (String) msg.obj, msg.arg1);
+ } break;
+ case MSG_USER_INTERACTION_STARTED: {
+ c.onUserInteractionStarted((String) msg.obj, msg.arg1);
+ } break;
+ case MSG_REQUEST_BG_RESTRICTED: {
+ c.handleRequestBgRestricted((String) msg.obj, msg.arg1);
+ } break;
+ case MSG_UID_INACTIVE : {
+ c.handleUidInactive(msg.arg1, msg.arg2 == 1);
+ } break;
+ case MSG_UID_ACTIVE: {
+ c.handleUidActive(msg.arg1);
+ } break;
+ }
+ }
+ }
+
+ static class Injector {
+ private final Context mContext;
+ private AppRestrictionController mAppRestrictionController;
+ private AppOpsManager mAppOpsManager;
+ private AppStandbyInternal mAppStandbyInternal;
+ private AppStateTracker mAppStateTracker;
+ private AppHibernationManagerInternal mAppHibernationInternal;
+ private IActivityManager mIActivityManager;
+ private UserManagerInternal mUserManagerInternal;
+ private PackageManagerInternal mPackageManagerInternal;
+
+ Injector(Context context) {
+ mContext = context;
+ }
+
+ Context getContext() {
+ return mContext;
+ }
+
+ void initAppStateTrackers(AppRestrictionController controller) {
+ mAppRestrictionController = controller;
+ }
+
+ AppRestrictionController getAppRestrictionController() {
+ return mAppRestrictionController;
+ }
+
+ AppOpsManager getAppOpsManager() {
+ if (mAppOpsManager == null) {
+ mAppOpsManager = getContext().getSystemService(AppOpsManager.class);
+ }
+ return mAppOpsManager;
+ }
+
+ AppStandbyInternal getAppStandbyInternal() {
+ if (mAppStandbyInternal == null) {
+ mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class);
+ }
+ return mAppStandbyInternal;
+ }
+
+ AppHibernationManagerInternal getAppHibernationInternal() {
+ if (mAppHibernationInternal == null) {
+ mAppHibernationInternal = LocalServices.getService(
+ AppHibernationManagerInternal.class);
+ }
+ return mAppHibernationInternal;
+ }
+
+ AppStateTracker getAppStateTracker() {
+ if (mAppStateTracker == null) {
+ mAppStateTracker = LocalServices.getService(AppStateTracker.class);
+ }
+ return mAppStateTracker;
+ }
+
+ IActivityManager getIActivityManager() {
+ return ActivityManager.getService();
+ }
+
+ UserManagerInternal getUserManagerInternal() {
+ if (mUserManagerInternal == null) {
+ mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
+ }
+ return mUserManagerInternal;
+ }
+
+ PackageManagerInternal getPackageManagerInternal() {
+ if (mPackageManagerInternal == null) {
+ mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
+ }
+ return mPackageManagerInternal;
+ }
+
+ PackageManager getPackageManager() {
+ return getContext().getPackageManager();
+ }
+ }
+
+ private void registerForSystemBroadcasts() {
+ final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ switch (intent.getAction()) {
+ case Intent.ACTION_PACKAGE_ADDED: {
+ if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+ final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+ if (uid >= 0) {
+ onUidAdded(uid);
+ }
+ }
+ } break;
+ case Intent.ACTION_PACKAGE_FULLY_REMOVED: {
+ final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+ final Uri data = intent.getData();
+ String ssp;
+ if (uid >= 0 && data != null
+ && (ssp = data.getSchemeSpecificPart()) != null) {
+ onPackageRemoved(ssp, uid);
+ }
+ } break;
+ case Intent.ACTION_UID_REMOVED: {
+ if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+ final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+ if (uid >= 0) {
+ onUidRemoved(uid);
+ }
+ }
+ } break;
+ case Intent.ACTION_USER_ADDED: {
+ final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ if (userId >= 0) {
+ onUserAdded(userId);
+ }
+ } break;
+ case Intent.ACTION_USER_STARTED: {
+ final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ if (userId >= 0) {
+ onUserStarted(userId);
+ }
+ } break;
+ case Intent.ACTION_USER_STOPPED: {
+ final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ if (userId >= 0) {
+ onUserStopped(userId);
+ }
+ } break;
+ case Intent.ACTION_USER_REMOVED: {
+ final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ if (userId >= 0) {
+ onUserRemoved(userId);
+ }
+ } break;
+ }
+ }
+ };
+ final IntentFilter packageFilter = new IntentFilter();
+ packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ packageFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
+ packageFilter.addDataScheme("package");
+ mContext.registerReceiverForAllUsers(broadcastReceiver, packageFilter, null, mBgHandler);
+ final IntentFilter userFilter = new IntentFilter();
+ userFilter.addAction(Intent.ACTION_USER_ADDED);
+ userFilter.addAction(Intent.ACTION_USER_REMOVED);
+ userFilter.addAction(Intent.ACTION_UID_REMOVED);
+ mContext.registerReceiverForAllUsers(broadcastReceiver, userFilter, null, mBgHandler);
+ }
+
+ private void onUserAdded(@UserIdInt int userId) {
+ for (int i = 0, size = mAppStateTrackers.size(); i < size; i++) {
+ mAppStateTrackers.get(i).onUserAdded(userId);
+ }
+ }
+
+ private void onUserStarted(@UserIdInt int userId) {
+ refreshAppRestrictionLevelForUser(userId, REASON_MAIN_FORCED_BY_USER,
+ REASON_SUB_FORCED_USER_FLAG_INTERACTION);
+ for (int i = 0, size = mAppStateTrackers.size(); i < size; i++) {
+ mAppStateTrackers.get(i).onUserStarted(userId);
+ }
+ }
+
+ private void onUserStopped(@UserIdInt int userId) {
+ for (int i = 0, size = mAppStateTrackers.size(); i < size; i++) {
+ mAppStateTrackers.get(i).onUserStopped(userId);
+ }
+ }
+
+ private void onUserRemoved(@UserIdInt int userId) {
+ for (int i = 0, size = mAppStateTrackers.size(); i < size; i++) {
+ mAppStateTrackers.get(i).onUserRemoved(userId);
+ }
+ mRestrictionSettings.removeUser(userId);
+ }
+
+ private void onUidAdded(int uid) {
+ refreshAppRestrictionLevelForUid(uid, REASON_MAIN_FORCED_BY_SYSTEM,
+ REASON_SUB_FORCED_SYSTEM_FLAG_UNDEFINED, false);
+ for (int i = 0, size = mAppStateTrackers.size(); i < size; i++) {
+ mAppStateTrackers.get(i).onUidAdded(uid);
+ }
+ }
+
+ private void onPackageRemoved(String pkgName, int uid) {
+ mRestrictionSettings.removePackage(pkgName, uid);
+ }
+
+ private void onUidRemoved(int uid) {
+ for (int i = 0, size = mAppStateTrackers.size(); i < size; i++) {
+ mAppStateTrackers.get(i).onUidRemoved(uid);
+ }
+ mRestrictionSettings.removeUid(uid);
+ }
+
+ private void onPropertiesChanged(String name) {
+ for (int i = 0, size = mAppStateTrackers.size(); i < size; i++) {
+ mAppStateTrackers.get(i).onPropertiesChanged(name);
+ }
+ }
+
+ private void onUserInteractionStarted(String packageName, @UserIdInt int userId) {
+ final int uid = mInjector.getPackageManagerInternal()
+ .getPackageUid(packageName, STOCK_PM_FLAGS, userId);
+ for (int i = 0, size = mAppStateTrackers.size(); i < size; i++) {
+ mAppStateTrackers.get(i).onUserInteractionStarted(packageName, uid);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/am/BaseAppStatePolicy.java b/services/core/java/com/android/server/am/BaseAppStatePolicy.java
new file mode 100644
index 0000000..1970332
--- /dev/null
+++ b/services/core/java/com/android/server/am/BaseAppStatePolicy.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.app.ActivityManager.RestrictionLevel;
+import android.os.UserHandle;
+
+import com.android.server.am.BaseAppStateTracker.Injector;
+
+/**
+ * Base class to track the policy for certain state of the app.
+ *
+ * @param <T> A class derived from BaseAppStateTracker.
+ */
+public abstract class BaseAppStatePolicy<T extends BaseAppStateTracker> {
+
+ protected final Injector<?> mInjector;
+ protected final T mTracker;
+
+ BaseAppStatePolicy(@NonNull Injector<?> injector, @NonNull T tracker) {
+ mInjector = injector;
+ mTracker = tracker;
+ }
+
+ /**
+ * Called when a device config property in the activity manager namespace
+ * has changed.
+ */
+ public abstract void onPropertiesChanged(@NonNull String name);
+
+ /**
+ * @return The proposed background restriction policy for the givenp package/uid.
+ */
+ public abstract @RestrictionLevel int getProposedRestrictionLevel(String packageName, int uid);
+
+ /**
+ * Called when the system is ready to rock.
+ */
+ public abstract void onSystemReady();
+
+ /**
+ * @return If this policy is enabled or not.
+ */
+ public abstract boolean isEnabled();
+
+ /**
+ * @return If the given UID should be exempted.
+ *
+ * <p>
+ * Note: Call it with caution as it'll try to acquire locks in other services.
+ * </p>
+ */
+ @CallSuper
+ public boolean shouldExemptUid(int uid) {
+ if (UserHandle.isCore(uid)) {
+ return true;
+ }
+ if (mInjector.getDeviceIdleInternal().isAppOnWhitelist(UserHandle.getAppId(uid))) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/services/core/java/com/android/server/am/BaseAppStateTracker.java b/services/core/java/com/android/server/am/BaseAppStateTracker.java
new file mode 100644
index 0000000..ee3521c
--- /dev/null
+++ b/services/core/java/com/android/server/am/BaseAppStateTracker.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManagerInternal;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Slog;
+
+import com.android.server.DeviceIdleInternal;
+import com.android.server.LocalServices;
+import com.android.server.pm.UserManagerInternal;
+
+import java.lang.reflect.Constructor;
+
+/**
+ * Base class to track certain state of the app, could be used to determine the restriction level.
+ *
+ * @param <T> A class derived from BaseAppStatePolicy.
+ */
+public abstract class BaseAppStateTracker<T extends BaseAppStatePolicy> {
+ protected static final String TAG = TAG_WITH_CLASS_NAME ? "BaseAppStatePolicy" : TAG_AM;
+
+ static final long ONE_MINUTE = 60 * 1_000L;
+ static final long ONE_HOUR = 60 * ONE_MINUTE;
+ static final long ONE_DAY = 24 * ONE_HOUR;
+
+ protected final AppRestrictionController mAppRestrictionController;
+ protected final Injector<T> mInjector;
+ protected final Context mContext;
+ protected final Handler mBgHandler;
+ protected final Object mLock;
+
+ BaseAppStateTracker(Context context, AppRestrictionController controller,
+ @Nullable Constructor<? extends Injector<T>> injector, Object outerContext) {
+ mContext = context;
+ mAppRestrictionController = controller;
+ mBgHandler = controller.getBackgroundHandler();
+ mLock = controller.getLock();
+ if (injector == null) {
+ mInjector = new Injector<>();
+ } else {
+ Injector<T> localInjector = null;
+ try {
+ localInjector = injector.newInstance(outerContext);
+ } catch (Exception e) {
+ Slog.w(TAG, "Unable to instantiate " + injector, e);
+ }
+ mInjector = (localInjector == null) ? new Injector<>() : localInjector;
+ }
+ }
+
+ /**
+ * Return the policy holder of this tracker.
+ */
+ T getPolicy() {
+ return mInjector.getPolicy();
+ }
+
+ /**
+ * Called when the system is ready to rock.
+ */
+ void onSystemReady() {
+ mInjector.onSystemReady();
+ }
+
+ /**
+ * Called when a user with the given uid is added.
+ */
+ void onUidAdded(final int uid) {
+ }
+
+ /**
+ * Called when a user with the given uid is removed.
+ */
+ void onUidRemoved(final int uid) {
+ }
+
+ /**
+ * Called when a user with the given userId is added.
+ */
+ void onUserAdded(final @UserIdInt int userId) {
+ }
+
+ /**
+ * Called when a user with the given userId is started.
+ */
+ void onUserStarted(final @UserIdInt int userId) {
+ }
+
+ /**
+ * Called when a user with the given userId is stopped.
+ */
+ void onUserStopped(final @UserIdInt int userId) {
+ }
+
+ /**
+ * Called when a user with the given userId is removed.
+ */
+ void onUserRemoved(final @UserIdInt int userId) {
+ }
+
+ /**
+ * Called when a device config property in the activity manager namespace
+ * has changed.
+ */
+ void onPropertiesChanged(@NonNull String name) {
+ getPolicy().onPropertiesChanged(name);
+ }
+
+ /**
+ * Called when an app has transitioned into an active state due to user interaction.
+ */
+ void onUserInteractionStarted(String packageName, int uid) {
+ }
+
+ /**
+ * Called when the background restriction settings of the given app is changed.
+ */
+ void onBackgroundRestrictionChanged(int uid, String pkgName, boolean restricted) {
+ }
+
+ static class Injector<T extends BaseAppStatePolicy> {
+ T mAppStatePolicy;
+
+ ActivityManagerInternal mActivityManagerInternal;
+ DeviceIdleInternal mDeviceIdleInternal;
+ UserManagerInternal mUserManagerInternal;
+
+ void setPolicy(T policy) {
+ mAppStatePolicy = policy;
+ }
+
+ void onSystemReady() {
+ mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+ mDeviceIdleInternal = LocalServices.getService(DeviceIdleInternal.class);
+ mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
+
+ getPolicy().onSystemReady();
+ }
+
+ ActivityManagerInternal getActivityManagerInternal() {
+ return mActivityManagerInternal;
+ }
+
+ T getPolicy() {
+ return mAppStatePolicy;
+ }
+
+ DeviceIdleInternal getDeviceIdleInternal() {
+ return mDeviceIdleInternal;
+ }
+
+ UserManagerInternal getUserManagerInternal() {
+ return mUserManagerInternal;
+ }
+ }
+}
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index 36246e5..9e221be 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -62,12 +62,14 @@
"truth-prebuilt",
// TODO: remove once Android migrates to JUnit 4.12, which provides assertThrows
"testng",
+ "compatibility-device-util-axt",
],
libs: [
"android.test.mock",
"android.test.base",
"android.test.runner",
+ "servicestests-core-utils",
],
jni_libs: [
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
new file mode 100644
index 0000000..0574a0b
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
@@ -0,0 +1,524 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static android.app.ActivityManager.RESTRICTION_LEVEL_ADAPTIVE_BUCKET;
+import static android.app.ActivityManager.RESTRICTION_LEVEL_BACKGROUND_RESTRICTED;
+import static android.app.ActivityManager.RESTRICTION_LEVEL_EXEMPTED;
+import static android.app.ActivityManager.RESTRICTION_LEVEL_RESTRICTED_BUCKET;
+import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_SYSTEM;
+import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_USER;
+import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE;
+import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_USER_FLAG_INTERACTION;
+import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_USER_INTERACTION;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_EXEMPTED;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.server.am.AppRestrictionController.STOCK_PM_FLAGS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.app.ActivityManagerInternal;
+import android.app.AppOpsManager;
+import android.app.IActivityManager;
+import android.app.IUidObserver;
+import android.app.usage.AppStandbyInfo;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.os.Handler;
+import android.os.MessageQueue;
+import android.os.Process;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.AppStateTracker;
+import com.android.server.DeviceIdleInternal;
+import com.android.server.am.AppRestrictionController.AppRestrictionLevelListener;
+import com.android.server.apphibernation.AppHibernationManagerInternal;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.usage.AppStandbyInternal;
+import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Tests for {@link AppRestrictionController}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksMockingServicesTests:BackgroundRestrictionTest
+ */
+@RunWith(AndroidJUnit4.class)
+public final class BackgroundRestrictionTest {
+ private static final String TAG = BackgroundRestrictionTest.class.getSimpleName();
+
+ private static final int TEST_USER0 = UserHandle.USER_SYSTEM;
+ private static final int TEST_USER1 = UserHandle.MIN_SECONDARY_USER_ID;
+ private static final int[] TEST_USERS = new int[] {TEST_USER0, TEST_USER1};
+ private static final String TEST_PACKAGE_BASE = "test_";
+ private static final int TEST_PACKAGE_APPID_BASE = Process.FIRST_APPLICATION_UID;
+ private static final int[] TEST_PACKAGE_USER0_UIDS = new int[] {
+ UserHandle.getUid(TEST_USER0, TEST_PACKAGE_APPID_BASE + 0),
+ UserHandle.getUid(TEST_USER0, TEST_PACKAGE_APPID_BASE + 1),
+ UserHandle.getUid(TEST_USER0, TEST_PACKAGE_APPID_BASE + 2),
+ UserHandle.getUid(TEST_USER0, TEST_PACKAGE_APPID_BASE + 3),
+ UserHandle.getUid(TEST_USER0, TEST_PACKAGE_APPID_BASE + 4),
+ UserHandle.getUid(TEST_USER0, TEST_PACKAGE_APPID_BASE + 5),
+ UserHandle.getUid(TEST_USER0, TEST_PACKAGE_APPID_BASE + 6),
+ };
+ private static final int[] TEST_PACKAGE_USER1_UIDS = new int[] {
+ UserHandle.getUid(TEST_USER1, TEST_PACKAGE_APPID_BASE + 0),
+ UserHandle.getUid(TEST_USER1, TEST_PACKAGE_APPID_BASE + 1),
+ UserHandle.getUid(TEST_USER1, TEST_PACKAGE_APPID_BASE + 2),
+ UserHandle.getUid(TEST_USER1, TEST_PACKAGE_APPID_BASE + 3),
+ UserHandle.getUid(TEST_USER1, TEST_PACKAGE_APPID_BASE + 4),
+ UserHandle.getUid(TEST_USER1, TEST_PACKAGE_APPID_BASE + 5),
+ UserHandle.getUid(TEST_USER1, TEST_PACKAGE_APPID_BASE + 6),
+ };
+ private static final int[][] TEST_UIDS = new int[][] {
+ TEST_PACKAGE_USER0_UIDS,
+ TEST_PACKAGE_USER1_UIDS,
+ };
+ private static final int[] TEST_STANDBY_BUCKETS = new int[] {
+ STANDBY_BUCKET_EXEMPTED,
+ STANDBY_BUCKET_ACTIVE,
+ STANDBY_BUCKET_WORKING_SET,
+ STANDBY_BUCKET_FREQUENT,
+ STANDBY_BUCKET_RARE,
+ STANDBY_BUCKET_RESTRICTED,
+ STANDBY_BUCKET_NEVER,
+ };
+
+ @Mock private ActivityManagerInternal mActivityManagerInternal;
+ @Mock private AppOpsManager mAppOpsManager;
+ @Mock private AppStandbyInternal mAppStandbyInternal;
+ @Mock private AppHibernationManagerInternal mAppHibernationInternal;
+ @Mock private AppStateTracker mAppStateTracker;
+ @Mock private DeviceIdleInternal mDeviceIdleInternal;
+ @Mock private IActivityManager mIActivityManager;
+ @Mock private UserManagerInternal mUserManagerInternal;
+ @Mock private PackageManager mPackageManager;
+ @Mock private PackageManagerInternal mPackageManagerInternal;
+
+ @Captor private ArgumentCaptor<AppStateTracker.BackgroundRestrictedAppListener> mFasListenerCap;
+ private AppStateTracker.BackgroundRestrictedAppListener mFasListener;
+
+ @Captor private ArgumentCaptor<AppIdleStateChangeListener> mIdleStateListenerCap;
+ private AppIdleStateChangeListener mIdleStateListener;
+
+ @Captor private ArgumentCaptor<IUidObserver> mUidObserversCap;
+ private IUidObserver mUidObservers;
+
+ private Context mContext = getInstrumentation().getTargetContext();
+ private TestBgRestrictionInjector mInjector;
+ private AppRestrictionController mBgRestrictionController;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ initController();
+ }
+
+ private void initController() throws Exception {
+ mInjector = new TestBgRestrictionInjector(mContext);
+ mBgRestrictionController = spy(new AppRestrictionController(mInjector));
+
+ doReturn(TEST_USERS).when(mUserManagerInternal).getUserIds();
+ for (int userId: TEST_USERS) {
+ final ArrayList<AppStandbyInfo> appStandbyInfoList = new ArrayList<>();
+ for (int i = 0; i < TEST_STANDBY_BUCKETS.length; i++) {
+ final String packageName = TEST_PACKAGE_BASE + i;
+ final int uid = UserHandle.getUid(userId, TEST_PACKAGE_APPID_BASE + i);
+ appStandbyInfoList.add(new AppStandbyInfo(packageName, TEST_STANDBY_BUCKETS[i]));
+ doReturn(uid)
+ .when(mPackageManagerInternal)
+ .getPackageUid(packageName, STOCK_PM_FLAGS, userId);
+ doReturn(false)
+ .when(mAppStateTracker)
+ .isAppBackgroundRestricted(uid, packageName);
+ doReturn(TEST_STANDBY_BUCKETS[i])
+ .when(mAppStandbyInternal)
+ .getAppStandbyBucket(eq(packageName), eq(userId), anyLong(), anyBoolean());
+ doReturn(new String[]{packageName})
+ .when(mPackageManager)
+ .getPackagesForUid(eq(uid));
+ }
+ doReturn(appStandbyInfoList).when(mAppStandbyInternal).getAppStandbyBuckets(userId);
+ }
+
+ mBgRestrictionController.onSystemReady();
+
+ verify(mInjector.getAppStateTracker())
+ .addBackgroundRestrictedAppListener(mFasListenerCap.capture());
+ mFasListener = mFasListenerCap.getValue();
+ verify(mInjector.getAppStandbyInternal())
+ .addListener(mIdleStateListenerCap.capture());
+ mIdleStateListener = mIdleStateListenerCap.getValue();
+ verify(mInjector.getIActivityManager())
+ .registerUidObserver(mUidObserversCap.capture(),
+ anyInt(), anyInt(), anyString());
+ mUidObservers = mUidObserversCap.getValue();
+ }
+
+ @After
+ public void tearDown() {
+ mBgRestrictionController.getBackgroundHandlerThread().quitSafely();
+ }
+
+ @Test
+ public void testInitialLevels() throws Exception {
+ final int[] expectedLevels = {
+ RESTRICTION_LEVEL_EXEMPTED,
+ RESTRICTION_LEVEL_ADAPTIVE_BUCKET,
+ RESTRICTION_LEVEL_ADAPTIVE_BUCKET,
+ RESTRICTION_LEVEL_ADAPTIVE_BUCKET,
+ RESTRICTION_LEVEL_ADAPTIVE_BUCKET,
+ RESTRICTION_LEVEL_RESTRICTED_BUCKET,
+ RESTRICTION_LEVEL_BACKGROUND_RESTRICTED,
+ };
+ for (int i = 0; i < TEST_UIDS.length; i++) {
+ final int[] uids = TEST_UIDS[i];
+ for (int j = 0; j < uids.length; j++) {
+ assertEquals(expectedLevels[j],
+ mBgRestrictionController.getRestrictionLevel(uids[j]));
+ assertEquals(expectedLevels[j],
+ mBgRestrictionController.getRestrictionLevel(uids[j],
+ TEST_PACKAGE_BASE + j));
+ }
+ }
+ }
+
+ @Test
+ public void testTogglingBackgroundRestrict() throws Exception {
+ final int testPkgIndex = 2;
+ final String testPkgName = TEST_PACKAGE_BASE + testPkgIndex;
+ final int testUser = TEST_USER0;
+ final int testUid = UserHandle.getUid(testUser, TEST_PACKAGE_APPID_BASE + testPkgIndex);
+ final TestAppRestrictionLevelListener listener = new TestAppRestrictionLevelListener();
+ final long timeout = 1_000; // ms
+
+ mBgRestrictionController.addAppRestrictionLevelListener(listener);
+
+ setBackgroundRestrict(testPkgName, testUid, false, listener);
+
+ // Verify the current settings.
+ verifyRestrictionLevel(RESTRICTION_LEVEL_ADAPTIVE_BUCKET, testPkgName, testUid);
+ assertEquals(STANDBY_BUCKET_WORKING_SET, mInjector.getAppStandbyInternal()
+ .getAppStandbyBucket(testPkgName, testUser, SystemClock.elapsedRealtime(), false));
+
+ // Now toggling ON the background restrict.
+ setBackgroundRestrict(testPkgName, testUid, true, listener);
+
+ // We should have been in the background restricted level.
+ verifyRestrictionLevel(RESTRICTION_LEVEL_BACKGROUND_RESTRICTED, testPkgName, testUid);
+
+ listener.verify(timeout, testUid, testPkgName, RESTRICTION_LEVEL_BACKGROUND_RESTRICTED);
+
+ // The app should have been put into the restricted standby bucket.
+ verify(mInjector.getAppStandbyInternal(), atLeast(1)).restrictApp(
+ eq(testPkgName),
+ eq(testUser),
+ eq(REASON_MAIN_FORCED_BY_USER),
+ eq(REASON_SUB_FORCED_USER_FLAG_INTERACTION));
+
+ // Changing to the restricted standby bucket won't make a difference.
+ listener.mLatchHolder[0] = new CountDownLatch(1);
+ mIdleStateListener.onAppIdleStateChanged(testPkgName, testUser, false,
+ STANDBY_BUCKET_RESTRICTED, REASON_MAIN_USAGE);
+ waitForIdleHandler(mBgRestrictionController.getBackgroundHandler());
+ verifyRestrictionLevel(RESTRICTION_LEVEL_BACKGROUND_RESTRICTED, testPkgName, testUid);
+ try {
+ listener.verify(timeout, testUid, testPkgName, RESTRICTION_LEVEL_BACKGROUND_RESTRICTED);
+ fail("There shouldn't be any level change events");
+ } catch (Exception e) {
+ // Expected.
+ }
+
+ clearInvocations(mInjector.getAppStandbyInternal());
+
+ // Toggling back.
+ setBackgroundRestrict(testPkgName, testUid, false, listener);
+
+ // It should have gone back to adaptive level.
+ verifyRestrictionLevel(RESTRICTION_LEVEL_ADAPTIVE_BUCKET, testPkgName, testUid);
+
+ // The app standby bucket should be the rare.
+ verify(mInjector.getAppStandbyInternal(), atLeast(1)).maybeUnrestrictApp(
+ eq(testPkgName),
+ eq(testUser),
+ eq(REASON_MAIN_FORCED_BY_USER),
+ eq(REASON_SUB_FORCED_USER_FLAG_INTERACTION),
+ eq(REASON_MAIN_USAGE),
+ eq(REASON_SUB_USAGE_USER_INTERACTION));
+
+ listener.verify(timeout, testUid, testPkgName, RESTRICTION_LEVEL_ADAPTIVE_BUCKET);
+
+ clearInvocations(mInjector.getAppStandbyInternal());
+
+ // Now set its UID state active.
+ mUidObservers.onUidActive(testUid);
+
+ // Now toggling ON the background restrict.
+ setBackgroundRestrict(testPkgName, testUid, true, listener);
+
+ // We should have been in the background restricted level.
+ verifyRestrictionLevel(RESTRICTION_LEVEL_BACKGROUND_RESTRICTED, testPkgName, testUid);
+
+ listener.verify(timeout, testUid, testPkgName, RESTRICTION_LEVEL_BACKGROUND_RESTRICTED);
+
+ // The app should have NOT been put into the restricted standby bucket.
+ verify(mInjector.getAppStandbyInternal(), never()).restrictApp(
+ eq(testPkgName),
+ eq(testUser),
+ eq(REASON_MAIN_FORCED_BY_USER),
+ eq(REASON_SUB_FORCED_USER_FLAG_INTERACTION));
+
+ // Now set its UID to idle.
+ mUidObservers.onUidIdle(testUid, false);
+
+ // The app should have been put into the restricted standby bucket because we're idle now.
+ verify(mInjector.getAppStandbyInternal(), timeout(timeout).times(1)).restrictApp(
+ eq(testPkgName),
+ eq(testUser),
+ eq(REASON_MAIN_FORCED_BY_USER),
+ eq(REASON_SUB_FORCED_USER_FLAG_INTERACTION));
+ }
+
+ @Test
+ public void testTogglingStandbyBucket() throws Exception {
+ final int testPkgIndex = 2;
+ final String testPkgName = TEST_PACKAGE_BASE + testPkgIndex;
+ final int testUser = TEST_USER0;
+ final int testUid = UserHandle.getUid(testUser, TEST_PACKAGE_APPID_BASE + testPkgIndex);
+ final TestAppRestrictionLevelListener listener = new TestAppRestrictionLevelListener();
+ final long timeout = 1_000; // ms
+
+ mBgRestrictionController.addAppRestrictionLevelListener(listener);
+
+ setBackgroundRestrict(testPkgName, testUid, false, listener);
+
+ // Verify the current settings.
+ verifyRestrictionLevel(RESTRICTION_LEVEL_ADAPTIVE_BUCKET, testPkgName, testUid);
+
+ for (int bucket: Arrays.asList(STANDBY_BUCKET_ACTIVE, STANDBY_BUCKET_WORKING_SET,
+ STANDBY_BUCKET_FREQUENT, STANDBY_BUCKET_RARE)) {
+ listener.mLatchHolder[0] = new CountDownLatch(1);
+ mIdleStateListener.onAppIdleStateChanged(testPkgName, testUser, false,
+ bucket, REASON_MAIN_USAGE);
+ waitForIdleHandler(mBgRestrictionController.getBackgroundHandler());
+ verifyRestrictionLevel(RESTRICTION_LEVEL_ADAPTIVE_BUCKET, testPkgName, testUid);
+
+ try {
+ listener.verify(timeout, testUid, testPkgName, RESTRICTION_LEVEL_ADAPTIVE_BUCKET);
+ fail("There shouldn't be any level change events");
+ } catch (Exception e) {
+ // Expected.
+ }
+ }
+
+ // Toggling restricted bucket.
+ listener.mLatchHolder[0] = new CountDownLatch(1);
+ mIdleStateListener.onAppIdleStateChanged(testPkgName, testUser, false,
+ STANDBY_BUCKET_RESTRICTED, REASON_MAIN_USAGE);
+ waitForIdleHandler(mBgRestrictionController.getBackgroundHandler());
+ verifyRestrictionLevel(RESTRICTION_LEVEL_RESTRICTED_BUCKET, testPkgName, testUid);
+ listener.verify(timeout, testUid, testPkgName, RESTRICTION_LEVEL_RESTRICTED_BUCKET);
+
+ // Toggling exempted bucket.
+ listener.mLatchHolder[0] = new CountDownLatch(1);
+ mIdleStateListener.onAppIdleStateChanged(testPkgName, testUser, false,
+ STANDBY_BUCKET_EXEMPTED, REASON_MAIN_FORCED_BY_SYSTEM);
+ waitForIdleHandler(mBgRestrictionController.getBackgroundHandler());
+ verifyRestrictionLevel(RESTRICTION_LEVEL_EXEMPTED, testPkgName, testUid);
+ listener.verify(timeout, testUid, testPkgName, RESTRICTION_LEVEL_EXEMPTED);
+ }
+
+ private void setBackgroundRestrict(String pkgName, int uid, boolean restricted,
+ TestAppRestrictionLevelListener listener) throws Exception {
+ Log.i(TAG, "Setting background restrict to " + restricted + " for " + pkgName + " " + uid);
+ listener.mLatchHolder[0] = new CountDownLatch(1);
+ doReturn(restricted).when(mAppStateTracker).isAppBackgroundRestricted(uid, pkgName);
+ mFasListener.updateBackgroundRestrictedForUidPackage(uid, pkgName, restricted);
+ waitForIdleHandler(mBgRestrictionController.getBackgroundHandler());
+ }
+
+ private class TestAppRestrictionLevelListener implements AppRestrictionLevelListener {
+ final CountDownLatch[] mLatchHolder = new CountDownLatch[1];
+ final int[] mUidHolder = new int[1];
+ final String[] mPkgNameHolder = new String[1];
+ final int[] mLevelHolder = new int[1];
+
+ @Override
+ public void onRestrictionLevelChanged(int uid, String packageName, int newLevel) {
+ mUidHolder[0] = uid;
+ mPkgNameHolder[0] = packageName;
+ mLevelHolder[0] = newLevel;
+ mLatchHolder[0].countDown();
+ };
+
+ void verify(long timeout, int uid, String pkgName, int level) throws Exception {
+ if (!mLatchHolder[0].await(timeout, TimeUnit.MILLISECONDS)) {
+ throw new TimeoutException();
+ }
+ assertEquals(uid, mUidHolder[0]);
+ assertEquals(pkgName, mPkgNameHolder[0]);
+ assertEquals(level, mLevelHolder[0]);
+ }
+ }
+
+ private void verifyRestrictionLevel(int level, String pkgName, int uid) {
+ assertEquals(level, mBgRestrictionController.getRestrictionLevel(uid));
+ assertEquals(level, mBgRestrictionController.getRestrictionLevel(uid, pkgName));
+ }
+
+ private void waitForIdleHandler(Handler handler) {
+ waitForIdleHandler(handler, Duration.ofSeconds(1));
+ }
+
+ private void waitForIdleHandler(Handler handler, Duration timeout) {
+ final MessageQueue queue = handler.getLooper().getQueue();
+ final CountDownLatch latch = new CountDownLatch(1);
+ queue.addIdleHandler(() -> {
+ latch.countDown();
+ // Remove idle handler
+ return false;
+ });
+ try {
+ latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ fail("Interrupted unexpectedly: " + e);
+ }
+ }
+
+ private class TestBgRestrictionInjector extends AppRestrictionController.Injector {
+ private Context mContext;
+
+ TestBgRestrictionInjector(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ @Override
+ void initAppStateTrackers(AppRestrictionController controller) {
+ }
+
+ @Override
+ AppRestrictionController getAppRestrictionController() {
+ return mBgRestrictionController;
+ }
+
+ @Override
+ AppOpsManager getAppOpsManager() {
+ return mAppOpsManager;
+ }
+
+ @Override
+ AppStandbyInternal getAppStandbyInternal() {
+ return mAppStandbyInternal;
+ }
+
+ @Override
+ AppHibernationManagerInternal getAppHibernationInternal() {
+ return mAppHibernationInternal;
+ }
+
+ @Override
+ AppStateTracker getAppStateTracker() {
+ return mAppStateTracker;
+ }
+
+ @Override
+ IActivityManager getIActivityManager() {
+ return mIActivityManager;
+ }
+
+ @Override
+ UserManagerInternal getUserManagerInternal() {
+ return mUserManagerInternal;
+ }
+
+ @Override
+ PackageManagerInternal getPackageManagerInternal() {
+ return mPackageManagerInternal;
+ }
+
+ @Override
+ PackageManager getPackageManager() {
+ return mPackageManager;
+ }
+ }
+
+ private class TestBaseTrackerInjector<T extends BaseAppStatePolicy>
+ extends BaseAppStateTracker.Injector<T> {
+ @Override
+ void onSystemReady() {
+ getPolicy().onSystemReady();
+ }
+
+ @Override
+ ActivityManagerInternal getActivityManagerInternal() {
+ return BackgroundRestrictionTest.this.mActivityManagerInternal;
+ }
+
+ @Override
+ DeviceIdleInternal getDeviceIdleInternal() {
+ return BackgroundRestrictionTest.this.mDeviceIdleInternal;
+ }
+
+ @Override
+ UserManagerInternal getUserManagerInternal() {
+ return BackgroundRestrictionTest.this.mUserManagerInternal;
+ }
+ }
+}
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index f24059c..a6c81a0 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -133,9 +133,11 @@
name: "servicestests-core-utils",
srcs: [
"src/com/android/server/pm/PackageSettingBuilder.java",
+ "src/com/android/server/am/DeviceConfigSession.java",
],
static_libs: [
"services.core",
+ "compatibility-device-util-axt",
],
}