Implement force-all-apps-standly in job scheduler.
Bug: 68769804
Test: Manual test
Change-Id: I70c28b7841165414cc8d27bf3466401c541d0569
diff --git a/services/core/java/com/android/server/ForceAppStandbyTracker.java b/services/core/java/com/android/server/ForceAppStandbyTracker.java
new file mode 100644
index 0000000..5dd3ee0
--- /dev/null
+++ b/services/core/java/com/android/server/ForceAppStandbyTracker.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright (C) 2017 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.app.AppOpsManager.PackageOps;
+import android.app.IUidObserver;
+import android.content.Context;
+import android.os.Handler;
+import android.os.PowerManager.ServiceType;
+import android.os.PowerManagerInternal;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.app.IAppOpsCallback;
+import com.android.internal.app.IAppOpsService;
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * Class to track OP_RUN_ANY_IN_BACKGROUND, UID foreground state and "force all app standby".
+ *
+ * TODO Clean up cache when a user is deleted.
+ * TODO Add unit tests. b/68769804.
+ */
+public class ForceAppStandbyTracker {
+ private static final String TAG = "ForceAppStandbyTracker";
+
+ @GuardedBy("ForceAppStandbyTracker.class")
+ private static ForceAppStandbyTracker sInstance;
+
+ private final Object mLock = new Object();
+ private final Context mContext;
+
+ AppOpsManager mAppOpsManager;
+ IAppOpsService mAppOpsService;
+ PowerManagerInternal mPowerManagerInternal;
+
+ private final Handler mCallbackHandler;
+
+ /**
+ * Pair of (uid (not user-id), packageName) with OP_RUN_ANY_IN_BACKGROUND *not* allowed.
+ */
+ @GuardedBy("mLock")
+ final ArraySet<Pair<Integer, String>> mForcedAppStandbyUidPackages = new ArraySet<>();
+
+ @GuardedBy("mLock")
+ final SparseBooleanArray mForegroundUids = new SparseBooleanArray();
+
+ @GuardedBy("mLock")
+ final ArraySet<Listener> mListeners = new ArraySet<>();
+
+ @GuardedBy("mLock")
+ boolean mStarted;
+
+ @GuardedBy("mLock")
+ boolean mForceAllAppsStandby;
+
+ public static abstract class Listener {
+ public void onRestrictionChanged(int uid, @Nullable String packageName) {
+ }
+
+ public void onGlobalRestrictionChanged() {
+ }
+ }
+
+ private ForceAppStandbyTracker(Context context) {
+ mContext = context;
+ mCallbackHandler = FgThread.getHandler();
+ }
+
+ /**
+ * Get the singleton instance.
+ */
+ public static synchronized ForceAppStandbyTracker getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new ForceAppStandbyTracker(context);
+ }
+ return sInstance;
+ }
+
+ /**
+ * Call it when the system is ready.
+ */
+ public void start() {
+ synchronized (mLock) {
+ if (mStarted) {
+ return;
+ }
+ mStarted = true;
+
+ mAppOpsManager = Preconditions.checkNotNull(
+ mContext.getSystemService(AppOpsManager.class));
+ mAppOpsService = Preconditions.checkNotNull(
+ IAppOpsService.Stub.asInterface(
+ ServiceManager.getService(Context.APP_OPS_SERVICE)));
+ mPowerManagerInternal = Preconditions.checkNotNull(
+ LocalServices.getService(PowerManagerInternal.class));
+
+ try {
+ ActivityManager.getService().registerUidObserver(new UidObserver(),
+ ActivityManager.UID_OBSERVER_GONE | ActivityManager.UID_OBSERVER_IDLE
+ | ActivityManager.UID_OBSERVER_ACTIVE,
+ ActivityManager.PROCESS_STATE_UNKNOWN, null);
+ mAppOpsService.startWatchingMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, null,
+ new AppOpsWatcher());
+ } catch (RemoteException e) {
+ // shouldn't happen.
+ }
+
+ mPowerManagerInternal.registerLowPowerModeObserver(
+ ServiceType.FORCE_ALL_APPS_STANDBY,
+ state -> updateForceAllAppsStandby(state.batterySaverEnabled));
+
+ updateForceAllAppsStandby(
+ mPowerManagerInternal.getLowPowerState(ServiceType.FORCE_ALL_APPS_STANDBY)
+ .batterySaverEnabled);
+
+ refreshForcedAppStandbyUidPackagesLocked();
+ }
+ }
+
+ /**
+ * Update {@link #mForcedAppStandbyUidPackages} with the current app ops state.
+ */
+ private void refreshForcedAppStandbyUidPackagesLocked() {
+ final int op = AppOpsManager.OP_RUN_ANY_IN_BACKGROUND;
+
+ mForcedAppStandbyUidPackages.clear();
+ final List<PackageOps> ops = mAppOpsManager.getPackagesForOps(new int[] {op});
+
+ if (ops == null) {
+ return;
+ }
+ final int size = ops.size();
+ for (int i = 0; i < size; i++) {
+ final AppOpsManager.PackageOps pkg = ops.get(i);
+ final List<AppOpsManager.OpEntry> entries = ops.get(i).getOps();
+
+ for (int j = 0; j < entries.size(); j++) {
+ AppOpsManager.OpEntry ent = entries.get(j);
+ if (ent.getOp() != op) {
+ continue;
+ }
+ if (ent.getMode() != AppOpsManager.MODE_ALLOWED) {
+ mForcedAppStandbyUidPackages.add(Pair.create(
+ pkg.getUid(), pkg.getPackageName()));
+ }
+ }
+ }
+ }
+
+ boolean isRunAnyInBackgroundAppOpRestricted(int uid, @NonNull String packageName) {
+ try {
+ return mAppOpsService.checkOperation(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
+ uid, packageName) != AppOpsManager.MODE_ALLOWED;
+ } catch (RemoteException e) {
+ return false; // shouldn't happen.
+ }
+ }
+
+ private int findForcedAppStandbyUidPackageIndexLocked(int uid, @NonNull String packageName) {
+ // TODO Maybe we should switch to indexOf(Pair.create()) if the array size is too big.
+ final int size = mForcedAppStandbyUidPackages.size();
+ for (int i = 0; i < size; i++) {
+ final Pair<Integer, String> pair = mForcedAppStandbyUidPackages.valueAt(i);
+
+ if ((pair.first == uid) && packageName.equals(pair.second)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * @return whether a uid package-name pair is in mForcedAppStandbyUidPackages.
+ */
+ boolean isUidPackageRestrictedLocked(int uid, @NonNull String packageName) {
+ return findForcedAppStandbyUidPackageIndexLocked(uid, packageName) >= 0;
+ }
+
+ boolean updateRestrictedUidPackageLocked(int uid, @NonNull String packageName,
+ boolean restricted) {
+ final int index = findForcedAppStandbyUidPackageIndexLocked(uid, packageName);
+ final boolean wasRestricted = index >= 0;
+ if (wasRestricted == restricted) {
+ return false;
+ }
+ if (restricted) {
+ mForcedAppStandbyUidPackages.add(Pair.create(uid, packageName));
+ } else {
+ mForcedAppStandbyUidPackages.removeAt(index);
+ }
+ return true;
+ }
+
+ void uidToForeground(int uid) {
+ synchronized (mLock) {
+ if (!UserHandle.isApp(uid)) {
+ return;
+ }
+ // TODO This can be optimized by calling indexOfKey and sharing the index for get and
+ // put.
+ if (mForegroundUids.get(uid)) {
+ return;
+ }
+ mForegroundUids.put(uid, true);
+ notifyForUidPackage(uid, null);
+ }
+ }
+
+ void uidToBackground(int uid, boolean remove) {
+ synchronized (mLock) {
+ if (!UserHandle.isApp(uid)) {
+ return;
+ }
+ // TODO This can be optimized by calling indexOfKey and sharing the index for get and
+ // put.
+ if (!mForegroundUids.get(uid)) {
+ return;
+ }
+ if (remove) {
+ mForegroundUids.delete(uid);
+ } else {
+ mForegroundUids.put(uid, false);
+ }
+ notifyForUidPackage(uid, null);
+ }
+ }
+
+ // Event handlers
+
+ final class UidObserver extends IUidObserver.Stub {
+ @Override public void onUidStateChanged(int uid, int procState, long procStateSeq) {
+ }
+
+ @Override public void onUidGone(int uid, boolean disabled) {
+ uidToBackground(uid, /*remove=*/ true);
+ }
+
+ @Override public void onUidActive(int uid) {
+ uidToForeground(uid);
+ }
+
+ @Override public void onUidIdle(int uid, boolean disabled) {
+ // Just to avoid excessive memcpy, don't remove from the array in this case.
+ uidToBackground(uid, /*remove=*/ false);
+ }
+
+ @Override public void onUidCachedChanged(int uid, boolean cached) {
+ }
+ };
+
+ private final class AppOpsWatcher extends IAppOpsCallback.Stub {
+ @Override
+ public void opChanged(int op, int uid, String packageName) throws RemoteException {
+ synchronized (mLock) {
+ final boolean restricted = isRunAnyInBackgroundAppOpRestricted(uid, packageName);
+
+ if (updateRestrictedUidPackageLocked(uid, packageName, restricted)) {
+ notifyForUidPackage(uid, packageName);
+ }
+ }
+ }
+ }
+
+ private Listener[] cloneListeners() {
+ synchronized (mLock) {
+ return mListeners.toArray(new Listener[mListeners.size()]);
+ }
+ }
+
+ void notifyForUidPackage(int uid, String packageName) {
+ mCallbackHandler.post(() -> {
+ for (Listener l : cloneListeners()) {
+ l.onRestrictionChanged(uid, packageName);
+ }
+ });
+ }
+
+ void notifyGlobal() {
+ mCallbackHandler.post(() -> {
+ for (Listener l : cloneListeners()) {
+ l.onGlobalRestrictionChanged();
+ }
+ });
+ }
+
+ void updateForceAllAppsStandby(boolean forceAllAppsStandby) {
+ synchronized (mLock) {
+ if (mForceAllAppsStandby == forceAllAppsStandby) {
+ return;
+ }
+ mForceAllAppsStandby = forceAllAppsStandby;
+ Slog.i(TAG, "Force all app standby: " + mForceAllAppsStandby);
+ notifyGlobal();
+ }
+ }
+
+ // Public interface.
+
+ /**
+ * Register a new listener.
+ */
+ public void addListener(@NonNull Listener listener) {
+ synchronized (mLock) {
+ mListeners.add(listener);
+ }
+ }
+
+ /**
+ * Whether force-app-standby is effective for a UID package-name.
+ */
+ public boolean isRestricted(int uid, @NonNull String packageName) {
+ if (isInForeground(uid)) {
+ return false;
+ }
+ synchronized (mLock) {
+ if (mForceAllAppsStandby) {
+ return true;
+ }
+ return isUidPackageRestrictedLocked(uid, packageName);
+ }
+ }
+
+ /** For dumpsys -- otherwise the callers don't need to know it. */
+ public boolean isInForeground(int uid) {
+ if (!UserHandle.isApp(uid)) {
+ return true;
+ }
+ synchronized (mLock) {
+ return mForegroundUids.get(uid);
+ }
+ }
+
+ /** For dumpsys -- otherwise the callers don't need to know it. */
+ public boolean isForceAllAppsStandbyEnabled() {
+ synchronized (mLock) {
+ return mForceAllAppsStandby;
+ }
+ }
+
+ /** For dumpsys -- otherwise the callers don't need to know it. */
+ public boolean isRunAnyInBackgroundAppOpsAllowed(int uid, @NonNull String packageName) {
+ synchronized (mLock) {
+ return !isUidPackageRestrictedLocked(uid, packageName);
+ }
+ }
+
+ /** For dumpsys -- otherwise the callers don't need to know it. */
+ public SparseBooleanArray getForegroudUids() {
+ synchronized (mLock) {
+ return mForegroundUids.clone();
+ }
+ }
+
+ /** For dumpsys -- otherwise the callers don't need to know it. */
+ public ArraySet<Pair<Integer, String>> getRestrictedUidPackages() {
+ synchronized (mLock) {
+ return new ArraySet(mForcedAppStandbyUidPackages);
+ }
+ }
+}