summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Amith Yamasani <yamasani@google.com> 2017-09-29 13:17:43 -0700
committer Amith Yamasani <yamasani@google.com> 2017-10-24 19:24:15 -0700
commit17fffee4908f11038ba9cc5a672d15cb25be3dfe (patch)
tree4777a31456f8d8d30b0a8d59bef0ada54145dffe
parent22910ada9abbda61346d3a28470be2176364f77e (diff)
App Bucketing for Standby
Manage the standby bucket in AppStandbyController Default implementation of bucketing based on simple timeout: ACTIVE, if recently used 12 hrs to move to WORKING_SET 2 days to move to FREQUENT 7 days to move to RARE (subject to change) RARE bucket equates to the old "idle" or "inactive" state for an app. Bug: 63527785 Test: AppStandbyControllerTests.java Change-Id: I970d7afcdf47c31a9413da8fd4852066a13676a2
-rw-r--r--core/java/android/app/usage/AppStandby.java83
-rw-r--r--core/java/android/app/usage/IUsageStatsManager.aidl2
-rw-r--r--core/java/android/app/usage/UsageStatsManager.java24
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerShellCommand.java25
-rw-r--r--services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java57
-rw-r--r--services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java322
-rw-r--r--services/usage/java/com/android/server/usage/AppIdleHistory.java234
-rw-r--r--services/usage/java/com/android/server/usage/AppStandbyController.java399
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsService.java50
9 files changed, 1029 insertions, 167 deletions
diff --git a/core/java/android/app/usage/AppStandby.java b/core/java/android/app/usage/AppStandby.java
new file mode 100644
index 000000000000..6f9fc2fa5d36
--- /dev/null
+++ b/core/java/android/app/usage/AppStandby.java
@@ -0,0 +1,83 @@
+/*
+ * 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 android.app.usage;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Set of constants for app standby buckets and reasons. Apps will be moved into different buckets
+ * that affect how frequently they can run in the background or perform other battery-consuming
+ * actions. Buckets will be assigned based on how frequently or when the system thinks the user
+ * is likely to use the app.
+ * @hide
+ */
+public class AppStandby {
+
+ /** The app was used very recently, currently in use or likely to be used very soon. */
+ public static final int STANDBY_BUCKET_ACTIVE = 0;
+
+ // Leave some gap in case we want to increase the number of buckets
+
+ /** The app was used recently and/or likely to be used in the next few hours */
+ public static final int STANDBY_BUCKET_WORKING_SET = 3;
+
+ // Leave some gap in case we want to increase the number of buckets
+
+ /** The app was used in the last few days and/or likely to be used in the next few days */
+ public static final int STANDBY_BUCKET_FREQUENT = 6;
+
+ // Leave some gap in case we want to increase the number of buckets
+
+ /** The app has not be used for several days and/or is unlikely to be used for several days */
+ public static final int STANDBY_BUCKET_RARE = 9;
+
+ // Leave some gap in case we want to increase the number of buckets
+
+ /** The app has never been used. */
+ public static final int STANDBY_BUCKET_NEVER = 12;
+
+ /** Reason for bucketing -- default initial state */
+ public static final String REASON_DEFAULT = "default";
+
+ /** Reason for bucketing -- timeout */
+ public static final String REASON_TIMEOUT = "timeout";
+
+ /** Reason for bucketing -- usage */
+ public static final String REASON_USAGE = "usage";
+
+ /** Reason for bucketing -- forced by user / shell command */
+ public static final String REASON_FORCED = "forced";
+
+ /**
+ * Reason for bucketing -- predicted. This is a prefix and the UID of the bucketeer will
+ * be appended.
+ */
+ public static final String REASON_PREDICTED = "predicted";
+
+ @IntDef(flag = false, value = {
+ STANDBY_BUCKET_ACTIVE,
+ STANDBY_BUCKET_WORKING_SET,
+ STANDBY_BUCKET_FREQUENT,
+ STANDBY_BUCKET_RARE,
+ STANDBY_BUCKET_NEVER,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StandbyBuckets {}
+}
diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl
index 31b235977a04..4fbbdf2a9281 100644
--- a/core/java/android/app/usage/IUsageStatsManager.aidl
+++ b/core/java/android/app/usage/IUsageStatsManager.aidl
@@ -36,4 +36,6 @@ interface IUsageStatsManager {
void onCarrierPrivilegedAppsChanged();
void reportChooserSelection(String packageName, int userId, String contentType,
in String[] annotations, String action);
+ int getAppStandbyBucket(String packageName, String callingPackage, int userId);
+ void setAppStandbyBucket(String packageName, int bucket, int userId);
}
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index 051dccbd86c0..1359d9b582aa 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -19,6 +19,7 @@ package android.app.usage;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.app.usage.AppStandby.StandbyBuckets;
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.os.RemoteException;
@@ -248,6 +249,29 @@ public final class UsageStatsManager {
}
/**
+ * @hide
+ */
+ public @StandbyBuckets int getAppStandbyBucket(String packageName) {
+ try {
+ return mService.getAppStandbyBucket(packageName, mContext.getOpPackageName(),
+ mContext.getUserId());
+ } catch (RemoteException e) {
+ }
+ return AppStandby.STANDBY_BUCKET_ACTIVE;
+ }
+
+ /**
+ * @hide
+ */
+ public void setAppStandbyBucket(String packageName, @StandbyBuckets int bucket) {
+ try {
+ mService.setAppStandbyBucket(packageName, bucket, mContext.getUserId());
+ } catch (RemoteException e) {
+ // Nothing to do
+ }
+ }
+
+ /**
* {@hide}
* Temporarily whitelist the specified app for a short duration. This is to allow an app
* receiving a high priority message to be able to access the network and acquire wakelocks
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index f03d2d5352b7..4cf2794c3584 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -222,6 +222,8 @@ final class ActivityManagerShellCommand extends ShellCommand {
return runSetInactive(pw);
case "get-inactive":
return runGetInactive(pw);
+ case "set-standby-bucket":
+ return runSetStandbyBucket(pw);
case "send-trim-memory":
return runSendTrimMemory(pw);
case "display":
@@ -1824,6 +1826,27 @@ final class ActivityManagerShellCommand extends ShellCommand {
return 0;
}
+ int runSetStandbyBucket(PrintWriter pw) throws RemoteException {
+ int userId = UserHandle.USER_CURRENT;
+
+ String opt;
+ while ((opt=getNextOption()) != null) {
+ if (opt.equals("--user")) {
+ userId = UserHandle.parseUserArg(getNextArgRequired());
+ } else {
+ getErrPrintWriter().println("Error: Unknown option: " + opt);
+ return -1;
+ }
+ }
+ String packageName = getNextArgRequired();
+ String value = getNextArgRequired();
+
+ IUsageStatsManager usm = IUsageStatsManager.Stub.asInterface(ServiceManager.getService(
+ Context.USAGE_STATS_SERVICE));
+ usm.setAppStandbyBucket(packageName, Integer.parseInt(value), userId);
+ return 0;
+ }
+
int runGetInactive(PrintWriter pw) throws RemoteException {
int userId = UserHandle.USER_CURRENT;
@@ -2571,6 +2594,8 @@ final class ActivityManagerShellCommand extends ShellCommand {
pw.println(" Sets the inactive state of an app.");
pw.println(" get-inactive [--user <USER_ID>] <PACKAGE>");
pw.println(" Returns the inactive state of an app.");
+ pw.println(" set-standby-bucket [--user <USER_ID>] <PACKAGE> <BUCKET>");
+ pw.println(" Puts an app in the standby bucket.");
pw.println(" send-trim-memory [--user <USER_ID>] <PROCESS>");
pw.println(" [HIDDEN|RUNNING_MODERATE|BACKGROUND|RUNNING_LOW|MODERATE|RUNNING_CRITICAL|COMPLETE]");
pw.println(" Send a memory trim event to a <PROCESS>. May also supply a raw trim int level.");
diff --git a/services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java b/services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java
index 42ddedf0b340..67ffe5847cbc 100644
--- a/services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java
+++ b/services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java
@@ -16,6 +16,11 @@
package com.android.server.usage;
+import static android.app.usage.AppStandby.REASON_TIMEOUT;
+import static android.app.usage.AppStandby.STANDBY_BUCKET_ACTIVE;
+import static android.app.usage.AppStandby.STANDBY_BUCKET_RARE;
+
+import android.app.usage.AppStandby;
import android.os.FileUtils;
import android.test.AndroidTestCase;
@@ -28,6 +33,8 @@ public class AppIdleHistoryTests extends AndroidTestCase {
final static String PACKAGE_1 = "com.android.testpackage1";
final static String PACKAGE_2 = "com.android.testpackage2";
+ final static int USER_ID = 0;
+
@Override
protected void setUp() throws Exception {
super.setUp();
@@ -42,7 +49,6 @@ public class AppIdleHistoryTests extends AndroidTestCase {
}
public void testFilesCreation() {
- final int userId = 0;
AppIdleHistory aih = new AppIdleHistory(mStorageDir, 0);
aih.updateDisplay(true, /* elapsedRealtime= */ 1000);
@@ -50,9 +56,9 @@ public class AppIdleHistoryTests extends AndroidTestCase {
// Screen On time file should be written right away
assertTrue(aih.getScreenOnTimeFile().exists());
- aih.writeAppIdleTimes(userId);
+ aih.writeAppIdleTimes(USER_ID);
// stats file should be written now
- assertTrue(new File(new File(mStorageDir, "users/" + userId),
+ assertTrue(new File(new File(mStorageDir, "users/" + USER_ID),
AppIdleHistory.APP_IDLE_FILENAME).exists());
}
@@ -77,24 +83,33 @@ public class AppIdleHistoryTests extends AndroidTestCase {
assertEquals(aih2.getScreenOnTime(13000), 4000);
}
- public void testPackageEvents() {
+ public void testBuckets() {
AppIdleHistory aih = new AppIdleHistory(mStorageDir, 1000);
- aih.setThresholds(4000, 1000);
- aih.updateDisplay(true, 1000);
- // App is not-idle by default
- assertFalse(aih.isIdle(PACKAGE_1, 0, 1500));
- // Still not idle
- assertFalse(aih.isIdle(PACKAGE_1, 0, 3000));
- // Idle now
- assertTrue(aih.isIdle(PACKAGE_1, 0, 8000));
- // Not idle
- assertFalse(aih.isIdle(PACKAGE_2, 0, 9000));
-
- // Screen off
- aih.updateDisplay(false, 9100);
- // Still idle after 10 seconds because screen hasn't been on long enough
- assertFalse(aih.isIdle(PACKAGE_2, 0, 20000));
- aih.updateDisplay(true, 21000);
- assertTrue(aih.isIdle(PACKAGE_2, 0, 23000));
+
+ aih.setAppStandbyBucket(PACKAGE_1, USER_ID, 1000, STANDBY_BUCKET_ACTIVE,
+ AppStandby.REASON_USAGE);
+ // ACTIVE means not idle
+ assertFalse(aih.isIdle(PACKAGE_1, USER_ID, 2000));
+
+ aih.setAppStandbyBucket(PACKAGE_2, USER_ID, 2000, STANDBY_BUCKET_ACTIVE,
+ AppStandby.REASON_USAGE);
+ aih.setAppStandbyBucket(PACKAGE_1, USER_ID, 3000, STANDBY_BUCKET_RARE,
+ REASON_TIMEOUT);
+
+ assertEquals(aih.getAppStandbyBucket(PACKAGE_1, USER_ID, 3000), STANDBY_BUCKET_RARE);
+ assertEquals(aih.getAppStandbyBucket(PACKAGE_2, USER_ID, 3000), STANDBY_BUCKET_ACTIVE);
+ assertEquals(aih.getAppStandbyReason(PACKAGE_1, USER_ID, 3000), REASON_TIMEOUT);
+
+ // RARE is considered idle
+ assertTrue(aih.isIdle(PACKAGE_1, USER_ID, 3000));
+ assertFalse(aih.isIdle(PACKAGE_2, USER_ID, 3000));
+
+ // Check persistence
+ aih.writeAppIdleDurations();
+ aih.writeAppIdleTimes(USER_ID);
+ aih = new AppIdleHistory(mStorageDir, 4000);
+ assertEquals(aih.getAppStandbyBucket(PACKAGE_1, USER_ID, 5000), STANDBY_BUCKET_RARE);
+ assertEquals(aih.getAppStandbyBucket(PACKAGE_2, USER_ID, 5000), STANDBY_BUCKET_ACTIVE);
+ assertEquals(aih.getAppStandbyReason(PACKAGE_1, USER_ID, 5000), REASON_TIMEOUT);
}
} \ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
new file mode 100644
index 000000000000..9846d6f6f346
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
@@ -0,0 +1,322 @@
+/*
+ * 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.usage;
+
+import static android.app.usage.AppStandby.STANDBY_BUCKET_ACTIVE;
+import static android.app.usage.AppStandby.STANDBY_BUCKET_FREQUENT;
+import static android.app.usage.AppStandby.STANDBY_BUCKET_RARE;
+import static android.app.usage.AppStandby.STANDBY_BUCKET_WORKING_SET;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import android.app.usage.AppStandby;
+import android.app.usage.UsageEvents;
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.Display;
+
+import com.android.server.SystemService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit test for AppStandbyController.
+ */
+@RunWith(AndroidJUnit4.class)
+public class AppStandbyControllerTests {
+
+ private static final String PACKAGE_1 = "com.example.foo";
+ private static final int UID_1 = 10000;
+ private static final int USER_ID = 0;
+
+ private static final long MINUTE_MS = 60 * 1000;
+ private static final long HOUR_MS = 60 * MINUTE_MS;
+ private static final long DAY_MS = 24 * HOUR_MS;
+
+ private MyInjector mInjector;
+
+ static class MyContextWrapper extends ContextWrapper {
+ PackageManager mockPm = mock(PackageManager.class);
+
+ public MyContextWrapper(Context base) {
+ super(base);
+ }
+
+ public PackageManager getPackageManager() {
+ return mockPm;
+ }
+ }
+
+ static class MyInjector extends AppStandbyController.Injector {
+ long mElapsedRealtime;
+ boolean mIsCharging;
+ List<String> mPowerSaveWhitelistExceptIdle = new ArrayList<>();
+ boolean mDisplayOn;
+ DisplayManager.DisplayListener mDisplayListener;
+ String mBoundWidgetPackage;
+
+ MyInjector(Context context, Looper looper) {
+ super(context, looper);
+ }
+
+ @Override
+ void onBootPhase(int phase) {
+ }
+
+ @Override
+ int getBootPhase() {
+ return SystemService.PHASE_BOOT_COMPLETED;
+ }
+
+ @Override
+ long elapsedRealtime() {
+ return mElapsedRealtime;
+ }
+
+ @Override
+ long currentTimeMillis() {
+ return mElapsedRealtime;
+ }
+
+ @Override
+ boolean isAppIdleEnabled() {
+ return true;
+ }
+
+ @Override
+ boolean isCharging() {
+ return mIsCharging;
+ }
+
+ @Override
+ boolean isPowerSaveWhitelistExceptIdleApp(String packageName) throws RemoteException {
+ return mPowerSaveWhitelistExceptIdle.contains(packageName);
+ }
+
+ @Override
+ File getDataSystemDirectory() {
+ return new File(getContext().getFilesDir(), Long.toString(Math.randomLongInternal()));
+ }
+
+ @Override
+ void noteEvent(int event, String packageName, int uid) throws RemoteException {
+ }
+
+ @Override
+ boolean isPackageEphemeral(int userId, String packageName) {
+ // TODO: update when testing ephemeral apps scenario
+ return false;
+ }
+
+ @Override
+ int[] getRunningUserIds() {
+ return new int[] {USER_ID};
+ }
+
+ @Override
+ boolean isDefaultDisplayOn() {
+ return mDisplayOn;
+ }
+
+ @Override
+ void registerDisplayListener(DisplayManager.DisplayListener listener, Handler handler) {
+ mDisplayListener = listener;
+ }
+
+ @Override
+ String getActiveNetworkScorer() {
+ return null;
+ }
+
+ @Override
+ public boolean isBoundWidgetPackage(AppWidgetManager appWidgetManager, String packageName,
+ int userId) {
+ return packageName != null && packageName.equals(mBoundWidgetPackage);
+ }
+
+ // Internal methods
+
+ void setDisplayOn(boolean on) {
+ mDisplayOn = on;
+ if (mDisplayListener != null) {
+ mDisplayListener.onDisplayChanged(Display.DEFAULT_DISPLAY);
+ }
+ }
+ }
+
+ private void setupPm(PackageManager mockPm) throws PackageManager.NameNotFoundException {
+ List<PackageInfo> packages = new ArrayList<>();
+ PackageInfo pi = new PackageInfo();
+ pi.applicationInfo = new ApplicationInfo();
+ pi.applicationInfo.uid = UID_1;
+ pi.packageName = PACKAGE_1;
+ packages.add(pi);
+
+ doReturn(packages).when(mockPm).getInstalledPackagesAsUser(anyInt(), anyInt());
+ try {
+ doReturn(UID_1).when(mockPm).getPackageUidAsUser(anyString(), anyInt(), anyInt());
+ doReturn(pi.applicationInfo).when(mockPm).getApplicationInfo(anyString(), anyInt());
+ } catch (PackageManager.NameNotFoundException nnfe) {}
+ }
+
+ private void setChargingState(AppStandbyController controller, boolean charging) {
+ mInjector.mIsCharging = charging;
+ if (controller != null) {
+ controller.setChargingState(charging);
+ }
+ }
+
+ private AppStandbyController setupController() throws Exception {
+ mInjector.mElapsedRealtime = 0;
+ AppStandbyController controller = new AppStandbyController(mInjector);
+ controller.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
+ controller.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+ mInjector.setDisplayOn(false);
+ mInjector.setDisplayOn(true);
+ setChargingState(controller, false);
+ setupPm(mInjector.getContext().getPackageManager());
+ controller.checkIdleStates(USER_ID);
+
+ return controller;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MyContextWrapper myContext = new MyContextWrapper(InstrumentationRegistry.getContext());
+ mInjector = new MyInjector(myContext, Looper.getMainLooper());
+ }
+
+ @Test
+ public void testCharging() throws Exception {
+ AppStandbyController controller = setupController();
+
+ setChargingState(controller, true);
+ mInjector.mElapsedRealtime = 8 * DAY_MS;
+ assertFalse(controller.isAppIdleFilteredOrParoled(PACKAGE_1, USER_ID,
+ mInjector.mElapsedRealtime, false));
+
+ setChargingState(controller, false);
+ mInjector.mElapsedRealtime = 16 * DAY_MS;
+ controller.checkIdleStates(USER_ID);
+ assertTrue(controller.isAppIdleFilteredOrParoled(PACKAGE_1, USER_ID,
+ mInjector.mElapsedRealtime, false));
+ setChargingState(controller, true);
+ assertFalse(controller.isAppIdleFilteredOrParoled(PACKAGE_1,USER_ID,
+ mInjector.mElapsedRealtime, false));
+ }
+
+ private void assertTimeout(AppStandbyController controller, long elapsedTime, int bucket) {
+ mInjector.mElapsedRealtime = elapsedTime;
+ controller.checkIdleStates(USER_ID);
+ assertEquals(bucket,
+ controller.getAppStandbyBucket(PACKAGE_1, USER_ID, mInjector.mElapsedRealtime,
+ false));
+ }
+
+ @Test
+ public void testBuckets() throws Exception {
+ AppStandbyController controller = setupController();
+
+ // ACTIVE bucket
+ assertTimeout(controller, 11 * HOUR_MS, STANDBY_BUCKET_ACTIVE);
+
+ // WORKING_SET bucket
+ assertTimeout(controller, 25 * HOUR_MS, STANDBY_BUCKET_WORKING_SET);
+
+ // WORKING_SET bucket
+ assertTimeout(controller, 47 * HOUR_MS, STANDBY_BUCKET_WORKING_SET);
+
+ // FREQUENT bucket
+ assertTimeout(controller, 4 * DAY_MS, STANDBY_BUCKET_FREQUENT);
+
+ // RARE bucket
+ assertTimeout(controller, 9 * DAY_MS, STANDBY_BUCKET_RARE);
+
+ // Back to ACTIVE on event
+ UsageEvents.Event ev = new UsageEvents.Event();
+ ev.mPackage = PACKAGE_1;
+ ev.mEventType = UsageEvents.Event.USER_INTERACTION;
+ controller.reportEvent(ev, mInjector.mElapsedRealtime, USER_ID);
+
+ assertTimeout(controller, 9 * DAY_MS, STANDBY_BUCKET_ACTIVE);
+
+ // RARE bucket
+ assertTimeout(controller, 18 * DAY_MS, STANDBY_BUCKET_RARE);
+ }
+
+ @Test
+ public void testScreenTimeAndBuckets() throws Exception {
+ AppStandbyController controller = setupController();
+ mInjector.setDisplayOn(false);
+
+ // ACTIVE bucket
+ assertTimeout(controller, 11 * HOUR_MS, STANDBY_BUCKET_ACTIVE);
+
+ // WORKING_SET bucket
+ assertTimeout(controller, 25 * HOUR_MS, STANDBY_BUCKET_WORKING_SET);
+
+ // RARE bucket, should fail because the screen wasn't ON.
+ mInjector.mElapsedRealtime = 9 * DAY_MS;
+ controller.checkIdleStates(USER_ID);
+ assertNotEquals(STANDBY_BUCKET_RARE,
+ controller.getAppStandbyBucket(PACKAGE_1, USER_ID, mInjector.mElapsedRealtime,
+ false));
+
+ mInjector.setDisplayOn(true);
+ assertTimeout(controller, 18 * DAY_MS, STANDBY_BUCKET_RARE);
+ }
+
+ @Test
+ public void testForcedIdle() throws Exception {
+ AppStandbyController controller = setupController();
+ setChargingState(controller, false);
+
+ controller.forceIdleState(PACKAGE_1, USER_ID, true);
+ assertEquals(STANDBY_BUCKET_RARE, controller.getAppStandbyBucket(PACKAGE_1, USER_ID, 0,
+ true));
+ assertTrue(controller.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
+
+ controller.forceIdleState(PACKAGE_1, USER_ID, false);
+ assertEquals(STANDBY_BUCKET_ACTIVE, controller.getAppStandbyBucket(PACKAGE_1, USER_ID, 0,
+ true));
+ assertFalse(controller.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0));
+ }
+}
diff --git a/services/usage/java/com/android/server/usage/AppIdleHistory.java b/services/usage/java/com/android/server/usage/AppIdleHistory.java
index f2985597573f..6dd9aa4094f5 100644
--- a/services/usage/java/com/android/server/usage/AppIdleHistory.java
+++ b/services/usage/java/com/android/server/usage/AppIdleHistory.java
@@ -16,6 +16,9 @@
package com.android.server.usage;
+import static android.app.usage.AppStandby.*;
+
+import android.app.usage.AppStandby;
import android.os.Environment;
import android.os.SystemClock;
import android.util.ArrayMap;
@@ -51,8 +54,10 @@ public class AppIdleHistory {
private static final String TAG = "AppIdleHistory";
+ private static final boolean DEBUG = AppStandbyController.DEBUG;
+
// History for all users and all packages
- private SparseArray<ArrayMap<String,PackageHistory>> mIdleHistory = new SparseArray<>();
+ private SparseArray<ArrayMap<String,AppUsageHistory>> mIdleHistory = new SparseArray<>();
private long mLastPeriod = 0;
private static final long ONE_MINUTE = 60 * 1000;
private static final int HISTORY_SIZE = 100;
@@ -70,6 +75,13 @@ public class AppIdleHistory {
private static final String ATTR_SCREEN_IDLE = "screenIdleTime";
// Elapsed timebase time when app was last used
private static final String ATTR_ELAPSED_IDLE = "elapsedIdleTime";
+ private static final String ATTR_CURRENT_BUCKET = "appLimitBucket";
+ private static final String ATTR_BUCKETING_REASON = "bucketReason";
+
+ // State that was last informed to listeners, since boot
+ private static final int STATE_UNINFORMED = 0;
+ private static final int STATE_ACTIVE = 1;
+ private static final int STATE_IDLE = 2;
// device on time = mElapsedDuration + (timeNow - mElapsedSnapshot)
private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration
@@ -85,17 +97,15 @@ public class AppIdleHistory {
private boolean mScreenOn;
- private static class PackageHistory {
+ private static class AppUsageHistory {
final byte[] recent = new byte[HISTORY_SIZE];
long lastUsedElapsedTime;
long lastUsedScreenTime;
+ @StandbyBuckets int currentBucket;
+ String bucketingReason;
+ int lastInformedState;
}
- AppIdleHistory(long elapsedRealtime) {
- this(Environment.getDataSystemDirectory(), elapsedRealtime);
- }
-
- @VisibleForTesting
AppIdleHistory(File storageDir, long elapsedRealtime) {
mElapsedSnapshot = elapsedRealtime;
mScreenOnSnapshot = elapsedRealtime;
@@ -119,6 +129,9 @@ public class AppIdleHistory {
mElapsedDuration += elapsedRealtime - mElapsedSnapshot;
mElapsedSnapshot = elapsedRealtime;
}
+ if (DEBUG) Slog.d(TAG, "mScreenOnSnapshot=" + mScreenOnSnapshot
+ + ", mScreenOnDuration=" + mScreenOnDuration
+ + ", mScreenOn=" + mScreenOn);
}
public long getScreenOnTime(long elapsedRealtime) {
@@ -174,29 +187,35 @@ public class AppIdleHistory {
}
public void reportUsage(String packageName, int userId, long elapsedRealtime) {
- ArrayMap<String, PackageHistory> userHistory = getUserHistory(userId);
- PackageHistory packageHistory = getPackageHistory(userHistory, packageName,
- elapsedRealtime);
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
+ elapsedRealtime, true);
shiftHistoryToNow(userHistory, elapsedRealtime);
- packageHistory.lastUsedElapsedTime = mElapsedDuration
+ appUsageHistory.lastUsedElapsedTime = mElapsedDuration
+ (elapsedRealtime - mElapsedSnapshot);
- packageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime);
- packageHistory.recent[HISTORY_SIZE - 1] = FLAG_LAST_STATE | FLAG_PARTIAL_ACTIVE;
+ appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime);
+ appUsageHistory.recent[HISTORY_SIZE - 1] = FLAG_LAST_STATE | FLAG_PARTIAL_ACTIVE;
+ appUsageHistory.currentBucket = AppStandby.STANDBY_BUCKET_ACTIVE;
+ appUsageHistory.bucketingReason = AppStandby.REASON_USAGE;
+ if (DEBUG) {
+ Slog.d(TAG, "Moved " + packageName + " to bucket=" + appUsageHistory.currentBucket
+ + ", reason=" + appUsageHistory.bucketingReason);
+ }
}
public void setIdle(String packageName, int userId, long elapsedRealtime) {
- ArrayMap<String, PackageHistory> userHistory = getUserHistory(userId);
- PackageHistory packageHistory = getPackageHistory(userHistory, packageName,
- elapsedRealtime);
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
+ elapsedRealtime, true);
shiftHistoryToNow(userHistory, elapsedRealtime);
- packageHistory.recent[HISTORY_SIZE - 1] &= ~FLAG_LAST_STATE;
+ appUsageHistory.recent[HISTORY_SIZE - 1] &= ~FLAG_LAST_STATE;
}
- private void shiftHistoryToNow(ArrayMap<String, PackageHistory> userHistory,
+ private void shiftHistoryToNow(ArrayMap<String, AppUsageHistory> userHistory,
long elapsedRealtime) {
long thisPeriod = elapsedRealtime / PERIOD_DURATION;
// Has the period switched over? Slide all users' package histories
@@ -206,7 +225,7 @@ public class AppIdleHistory {
final int NUSERS = mIdleHistory.size();
for (int u = 0; u < NUSERS; u++) {
userHistory = mIdleHistory.valueAt(u);
- for (PackageHistory idleState : userHistory.values()) {
+ for (AppUsageHistory idleState : userHistory.values()) {
// Shift left
System.arraycopy(idleState.recent, diff, idleState.recent, 0,
HISTORY_SIZE - diff);
@@ -221,8 +240,8 @@ public class AppIdleHistory {
mLastPeriod = thisPeriod;
}
- private ArrayMap<String, PackageHistory> getUserHistory(int userId) {
- ArrayMap<String, PackageHistory> userHistory = mIdleHistory.get(userId);
+ private ArrayMap<String, AppUsageHistory> getUserHistory(int userId) {
+ ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId);
if (userHistory == null) {
userHistory = new ArrayMap<>();
mIdleHistory.put(userId, userHistory);
@@ -231,16 +250,18 @@ public class AppIdleHistory {
return userHistory;
}
- private PackageHistory getPackageHistory(ArrayMap<String, PackageHistory> userHistory,
- String packageName, long elapsedRealtime) {
- PackageHistory packageHistory = userHistory.get(packageName);
- if (packageHistory == null) {
- packageHistory = new PackageHistory();
- packageHistory.lastUsedElapsedTime = getElapsedTime(elapsedRealtime);
- packageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime);
- userHistory.put(packageName, packageHistory);
+ private AppUsageHistory getPackageHistory(ArrayMap<String, AppUsageHistory> userHistory,
+ String packageName, long elapsedRealtime, boolean create) {
+ AppUsageHistory appUsageHistory = userHistory.get(packageName);
+ if (appUsageHistory == null && create) {
+ appUsageHistory = new AppUsageHistory();
+ appUsageHistory.lastUsedElapsedTime = getElapsedTime(elapsedRealtime);
+ appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime);
+ appUsageHistory.currentBucket = AppStandby.STANDBY_BUCKET_NEVER;
+ appUsageHistory.bucketingReason = REASON_DEFAULT;
+ userHistory.put(packageName, appUsageHistory);
}
- return packageHistory;
+ return appUsageHistory;
}
public void onUserRemoved(int userId) {
@@ -248,48 +269,124 @@ public class AppIdleHistory {
}
public boolean isIdle(String packageName, int userId, long elapsedRealtime) {
- ArrayMap<String, PackageHistory> userHistory = getUserHistory(userId);
- PackageHistory packageHistory =
- getPackageHistory(userHistory, packageName, elapsedRealtime);
- if (packageHistory == null) {
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory =
+ getPackageHistory(userHistory, packageName, elapsedRealtime, true);
+ if (appUsageHistory == null) {
return false; // Default to not idle
} else {
- return hasPassedThresholds(packageHistory, elapsedRealtime);
+ return appUsageHistory.currentBucket >= AppStandby.STANDBY_BUCKET_RARE;
+ // Whether or not it's passed will now be externally calculated and the
+ // bucket will be pushed to the history using setAppStandbyBucket()
+ //return hasPassedThresholds(appUsageHistory, elapsedRealtime);
+ }
+ }
+
+ public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime,
+ int bucket, String reason) {
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory =
+ getPackageHistory(userHistory, packageName, elapsedRealtime, true);
+ appUsageHistory.currentBucket = bucket;
+ appUsageHistory.bucketingReason = reason;
+ if (DEBUG) {
+ Slog.d(TAG, "Moved " + packageName + " to bucket=" + appUsageHistory.currentBucket
+ + ", reason=" + appUsageHistory.bucketingReason);
}
}
+ public int getAppStandbyBucket(String packageName, int userId, long elapsedRealtime) {
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory =
+ getPackageHistory(userHistory, packageName, elapsedRealtime, true);
+ return appUsageHistory.currentBucket;
+ }
+
+ public String getAppStandbyReason(String packageName, int userId, long elapsedRealtime) {
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory =
+ getPackageHistory(userHistory, packageName, elapsedRealtime, false);
+ return appUsageHistory != null ? appUsageHistory.bucketingReason : null;
+ }
+
private long getElapsedTime(long elapsedRealtime) {
return (elapsedRealtime - mElapsedSnapshot + mElapsedDuration);
}
public void setIdle(String packageName, int userId, boolean idle, long elapsedRealtime) {
- ArrayMap<String, PackageHistory> userHistory = getUserHistory(userId);
- PackageHistory packageHistory = getPackageHistory(userHistory, packageName,
- elapsedRealtime);
- packageHistory.lastUsedElapsedTime = getElapsedTime(elapsedRealtime)
- - mElapsedTimeThreshold;
- packageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime)
- - (idle ? mScreenOnTimeThreshold : 0) - 1000 /* just a second more */;
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
+ elapsedRealtime, true);
+ if (idle) {
+ appUsageHistory.currentBucket = STANDBY_BUCKET_RARE;
+ appUsageHistory.bucketingReason = REASON_FORCED;
+ } else {
+ appUsageHistory.currentBucket = STANDBY_BUCKET_ACTIVE;
+ // This is to pretend that the app was just used, don't freeze the state anymore.
+ appUsageHistory.bucketingReason = REASON_USAGE;
+ }
}
public void clearUsage(String packageName, int userId) {
- ArrayMap<String, PackageHistory> userHistory = getUserHistory(userId);
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
userHistory.remove(packageName);
}
- private boolean hasPassedThresholds(PackageHistory packageHistory, long elapsedRealtime) {
- return (packageHistory.lastUsedScreenTime
- <= getScreenOnTime(elapsedRealtime) - mScreenOnTimeThreshold)
- && (packageHistory.lastUsedElapsedTime
- <= getElapsedTime(elapsedRealtime) - mElapsedTimeThreshold);
+ boolean shouldInformListeners(String packageName, int userId,
+ long elapsedRealtime, boolean isIdle) {
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
+ elapsedRealtime, true);
+ int targetState = isIdle? STATE_IDLE : STATE_ACTIVE;
+ if (appUsageHistory.lastInformedState != (isIdle ? STATE_IDLE : STATE_ACTIVE)) {
+ appUsageHistory.lastInformedState = targetState;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the index in the arrays of screenTimeThresholds and elapsedTimeThresholds
+ * that corresponds to how long since the app was used.
+ * @param packageName
+ * @param userId
+ * @param elapsedRealtime current time
+ * @param screenTimeThresholds Array of screen times, in ascending order, first one is 0
+ * @param elapsedTimeThresholds Array of elapsed time, in ascending order, first one is 0
+ * @return The index whose values the app's used time exceeds (in both arrays)
+ */
+ int getThresholdIndex(String packageName, int userId, long elapsedRealtime,
+ long[] screenTimeThresholds, long[] elapsedTimeThresholds) {
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
+ elapsedRealtime, false);
+ // If we don't have any state for the app, assume never used
+ if (appUsageHistory == null) return screenTimeThresholds.length - 1;
+
+ long screenOnDelta = getScreenOnTime(elapsedRealtime) - appUsageHistory.lastUsedScreenTime;
+ long elapsedDelta = getElapsedTime(elapsedRealtime) - appUsageHistory.lastUsedElapsedTime;
+
+ if (DEBUG) Slog.d(TAG, packageName
+ + " lastUsedScreen=" + appUsageHistory.lastUsedScreenTime
+ + " lastUsedElapsed=" + appUsageHistory.lastUsedElapsedTime);
+ if (DEBUG) Slog.d(TAG, packageName + " screenOn=" + screenOnDelta
+ + ", elapsed=" + elapsedDelta);
+ for (int i = screenTimeThresholds.length - 1; i >= 0; i--) {
+ if (screenOnDelta >= screenTimeThresholds[i]
+ && elapsedDelta >= elapsedTimeThresholds[i]) {
+ return i;
+ }
+ }
+ return 0;
}
- private File getUserFile(int userId) {
+ @VisibleForTesting
+ File getUserFile(int userId) {
return new File(new File(new File(mStorageDir, "users"),
Integer.toString(userId)), APP_IDLE_FILENAME);
}
- private void readAppIdleTimes(int userId, ArrayMap<String, PackageHistory> userHistory) {
+ private void readAppIdleTimes(int userId, ArrayMap<String, AppUsageHistory> userHistory) {
FileInputStream fis = null;
try {
AtomicFile appIdleFile = new AtomicFile(getUserFile(userId));
@@ -315,12 +412,22 @@ public class AppIdleHistory {
final String name = parser.getName();
if (name.equals(TAG_PACKAGE)) {
final String packageName = parser.getAttributeValue(null, ATTR_NAME);
- PackageHistory packageHistory = new PackageHistory();
- packageHistory.lastUsedElapsedTime =
+ AppUsageHistory appUsageHistory = new AppUsageHistory();
+ appUsageHistory.lastUsedElapsedTime =
Long.parseLong(parser.getAttributeValue(null, ATTR_ELAPSED_IDLE));
- packageHistory.lastUsedScreenTime =
+ appUsageHistory.lastUsedScreenTime =
Long.parseLong(parser.getAttributeValue(null, ATTR_SCREEN_IDLE));
- userHistory.put(packageName, packageHistory);
+ String currentBucketString = parser.getAttributeValue(null,
+ ATTR_CURRENT_BUCKET);
+ appUsageHistory.currentBucket = currentBucketString == null
+ ? AppStandby.STANDBY_BUCKET_ACTIVE
+ : Integer.parseInt(currentBucketString);
+ appUsageHistory.bucketingReason =
+ parser.getAttributeValue(null, ATTR_BUCKETING_REASON);
+ if (appUsageHistory.bucketingReason == null) {
+ appUsageHistory.bucketingReason = REASON_DEFAULT;
+ }
+ userHistory.put(packageName, appUsageHistory);
}
}
}
@@ -345,17 +452,20 @@ public class AppIdleHistory {
xml.startTag(null, TAG_PACKAGES);
- ArrayMap<String,PackageHistory> userHistory = getUserHistory(userId);
+ ArrayMap<String,AppUsageHistory> userHistory = getUserHistory(userId);
final int N = userHistory.size();
for (int i = 0; i < N; i++) {
String packageName = userHistory.keyAt(i);
- PackageHistory history = userHistory.valueAt(i);
+ AppUsageHistory history = userHistory.valueAt(i);
xml.startTag(null, TAG_PACKAGE);
xml.attribute(null, ATTR_NAME, packageName);
xml.attribute(null, ATTR_ELAPSED_IDLE,
Long.toString(history.lastUsedElapsedTime));
xml.attribute(null, ATTR_SCREEN_IDLE,
Long.toString(history.lastUsedScreenTime));
+ xml.attribute(null, ATTR_CURRENT_BUCKET,
+ Integer.toString(history.currentBucket));
+ xml.attribute(null, ATTR_BUCKETING_REASON, history.bucketingReason);
xml.endTag(null, TAG_PACKAGE);
}
@@ -371,7 +481,7 @@ public class AppIdleHistory {
public void dump(IndentingPrintWriter idpw, int userId) {
idpw.println("Package idle stats:");
idpw.increaseIndent();
- ArrayMap<String, PackageHistory> userHistory = mIdleHistory.get(userId);
+ ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId);
final long elapsedRealtime = SystemClock.elapsedRealtime();
final long totalElapsedTime = getElapsedTime(elapsedRealtime);
final long screenOnTime = getScreenOnTime(elapsedRealtime);
@@ -379,13 +489,15 @@ public class AppIdleHistory {
final int P = userHistory.size();
for (int p = 0; p < P; p++) {
final String packageName = userHistory.keyAt(p);
- final PackageHistory packageHistory = userHistory.valueAt(p);
+ final AppUsageHistory appUsageHistory = userHistory.valueAt(p);
idpw.print("package=" + packageName);
idpw.print(" lastUsedElapsed=");
- TimeUtils.formatDuration(totalElapsedTime - packageHistory.lastUsedElapsedTime, idpw);
+ TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedElapsedTime, idpw);
idpw.print(" lastUsedScreenOn=");
- TimeUtils.formatDuration(screenOnTime - packageHistory.lastUsedScreenTime, idpw);
+ TimeUtils.formatDuration(screenOnTime - appUsageHistory.lastUsedScreenTime, idpw);
idpw.print(" idle=" + (isIdle(packageName, userId, elapsedRealtime) ? "y" : "n"));
+ idpw.print(" bucket=" + appUsageHistory.currentBucket
+ + " reason=" + appUsageHistory.bucketingReason);
idpw.println();
}
idpw.println();
@@ -399,7 +511,7 @@ public class AppIdleHistory {
}
public void dumpHistory(IndentingPrintWriter idpw, int userId) {
- ArrayMap<String, PackageHistory> userHistory = mIdleHistory.get(userId);
+ ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId);
final long elapsedRealtime = SystemClock.elapsedRealtime();
if (userHistory == null) return;
final int P = userHistory.size();
diff --git a/services/usage/java/com/android/server/usage/AppStandbyController.java b/services/usage/java/com/android/server/usage/AppStandbyController.java
index b2446ba7158d..dad595071df5 100644
--- a/services/usage/java/com/android/server/usage/AppStandbyController.java
+++ b/services/usage/java/com/android/server/usage/AppStandbyController.java
@@ -18,13 +18,14 @@ package com.android.server.usage;
import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
-import static com.android.server.usage.UsageStatsService.MSG_REPORT_EVENT;
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.admin.DevicePolicyManager;
+import android.app.usage.AppStandby;
+import android.app.usage.AppStandby.StandbyBuckets;
import android.app.usage.UsageEvents;
-import android.app.usage.UsageStatsManagerInternal;
+import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
@@ -41,6 +42,7 @@ import android.hardware.display.DisplayManager;
import android.net.NetworkScoreManager;
import android.os.BatteryManager;
import android.os.BatteryStats;
+import android.os.Environment;
import android.os.Handler;
import android.os.IDeviceIdleController;
import android.os.Looper;
@@ -66,8 +68,10 @@ import com.android.internal.util.ArrayUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.LocalServices;
+import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
/**
@@ -76,10 +80,33 @@ import java.util.List;
public class AppStandbyController {
private static final String TAG = "AppStandbyController";
- private static final boolean DEBUG = false;
+ static final boolean DEBUG = false;
static final boolean COMPRESS_TIME = false;
private static final long ONE_MINUTE = 60 * 1000;
+ private static final long ONE_HOUR = ONE_MINUTE * 60;
+ private static final long ONE_DAY = ONE_HOUR * 24;
+
+ static final long[] SCREEN_TIME_THRESHOLDS = {
+ 0,
+ 0,
+ COMPRESS_TIME ? 120 * 1000 : 1 * ONE_HOUR,
+ COMPRESS_TIME ? 240 * 1000 : 8 * ONE_HOUR
+ };
+
+ static final long[] ELAPSED_TIME_THRESHOLDS = {
+ 0,
+ COMPRESS_TIME ? 1 * ONE_MINUTE : 12 * ONE_HOUR,
+ COMPRESS_TIME ? 4 * ONE_MINUTE : 2 * ONE_DAY,
+ COMPRESS_TIME ? 16 * ONE_MINUTE : 8 * ONE_DAY
+ };
+
+ static final int[] THRESHOLD_BUCKETS = {
+ AppStandby.STANDBY_BUCKET_ACTIVE,
+ AppStandby.STANDBY_BUCKET_WORKING_SET,
+ AppStandby.STANDBY_BUCKET_FREQUENT,
+ AppStandby.STANDBY_BUCKET_RARE
+ };
// To name the lock for stack traces
static class Lock {}
@@ -92,7 +119,7 @@ public class AppStandbyController {
private AppIdleHistory mAppIdleHistory;
@GuardedBy("mAppIdleLock")
- private ArrayList<UsageStatsManagerInternal.AppIdleStateChangeListener>
+ private ArrayList<AppIdleStateChangeListener>
mPackageAccessListeners = new ArrayList<>();
/** Whether we've queried the list of carrier privileged apps. */
@@ -118,6 +145,9 @@ public class AppStandbyController {
long mAppIdleWallclockThresholdMillis;
long mAppIdleParoleIntervalMillis;
long mAppIdleParoleDurationMillis;
+ long[] mAppStandbyScreenThresholds = SCREEN_TIME_THRESHOLDS;
+ long[] mAppStandbyElapsedThresholds = ELAPSED_TIME_THRESHOLDS;
+
boolean mAppIdleEnabled;
boolean mAppIdleTempParoled;
boolean mCharging;
@@ -129,20 +159,26 @@ public class AppStandbyController {
private final Handler mHandler;
private final Context mContext;
- private DisplayManager mDisplayManager;
- private IDeviceIdleController mDeviceIdleController;
+ // TODO: Provide a mechanism to set an external bucketing service
+ private boolean mUseInternalBucketingHeuristics = true;
+
private AppWidgetManager mAppWidgetManager;
- private IBatteryStats mBatteryStats;
private PowerManager mPowerManager;
private PackageManager mPackageManager;
- private PackageManagerInternal mPackageManagerInternal;
+ private Injector mInjector;
+
AppStandbyController(Context context, Looper looper) {
- mContext = context;
- mHandler = new AppStandbyHandler(looper);
+ this(new Injector(context, looper));
+ }
+
+ AppStandbyController(Injector injector) {
+ mInjector = injector;
+ mContext = mInjector.getContext();
+ mHandler = new AppStandbyHandler(mInjector.getLooper());
mPackageManager = mContext.getPackageManager();
- mAppIdleEnabled = mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_enableAutoPowerModes);
+ mAppIdleEnabled = mInjector.isAppIdleEnabled();
+
if (mAppIdleEnabled) {
IntentFilter deviceStates = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
deviceStates.addAction(BatteryManager.ACTION_DISCHARGING);
@@ -150,7 +186,8 @@ public class AppStandbyController {
mContext.registerReceiver(new DeviceStateReceiver(), deviceStates);
}
synchronized (mAppIdleLock) {
- mAppIdleHistory = new AppIdleHistory(SystemClock.elapsedRealtime());
+ mAppIdleHistory = new AppIdleHistory(mInjector.getDataSystemDirectory(),
+ mInjector.elapsedRealtime());
}
IntentFilter packageFilter = new IntentFilter();
@@ -164,6 +201,7 @@ public class AppStandbyController {
}
public void onBootPhase(int phase) {
+ mInjector.onBootPhase(phase);
if (phase == PHASE_SYSTEM_SERVICES_READY) {
// Observe changes to the threshold
SettingsObserver settingsObserver = new SettingsObserver(mHandler);
@@ -171,18 +209,11 @@ public class AppStandbyController {
settingsObserver.updateSettings();
mAppWidgetManager = mContext.getSystemService(AppWidgetManager.class);
- mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
- ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
- mBatteryStats = IBatteryStats.Stub.asInterface(
- ServiceManager.getService(BatteryStats.SERVICE_NAME));
- mDisplayManager = (DisplayManager) mContext.getSystemService(
- Context.DISPLAY_SERVICE);
mPowerManager = mContext.getSystemService(PowerManager.class);
- mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
- mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
+ mInjector.registerDisplayListener(mDisplayListener, mHandler);
synchronized (mAppIdleLock) {
- mAppIdleHistory.updateDisplay(isDisplayOn(), SystemClock.elapsedRealtime());
+ mAppIdleHistory.updateDisplay(isDisplayOn(), mInjector.elapsedRealtime());
}
if (mPendingOneTimeCheckIdleStates) {
@@ -191,7 +222,7 @@ public class AppStandbyController {
mSystemServicesReady = true;
} else if (phase == PHASE_BOOT_COMPLETED) {
- setChargingState(mContext.getSystemService(BatteryManager.class).isCharging());
+ setChargingState(mInjector.isCharging());
}
}
@@ -229,7 +260,7 @@ public class AppStandbyController {
/** Paroled here means temporary pardon from being inactive */
void setAppIdleParoled(boolean paroled) {
synchronized (mAppIdleLock) {
- final long now = System.currentTimeMillis();
+ final long now = mInjector.currentTimeMillis();
if (mAppIdleTempParoled != paroled) {
mAppIdleTempParoled = paroled;
if (DEBUG) Slog.d(TAG, "Changing paroled to " + mAppIdleTempParoled);
@@ -284,7 +315,7 @@ public class AppStandbyController {
* scheduling a series of repeating checkIdleStates each time we fired off one.
*/
void postOneTimeCheckIdleStates() {
- if (mDeviceIdleController == null) {
+ if (mInjector.getBootPhase() < PHASE_SYSTEM_SERVICES_READY) {
// Not booted yet; wait for it!
mPendingOneTimeCheckIdleStates = true;
} else {
@@ -304,7 +335,7 @@ public class AppStandbyController {
final int[] runningUserIds;
try {
- runningUserIds = ActivityManager.getService().getRunningUserIds();
+ runningUserIds = mInjector.getRunningUserIds();
if (checkUserId != UserHandle.USER_ALL
&& !ArrayUtils.contains(runningUserIds, checkUserId)) {
return false;
@@ -313,7 +344,7 @@ public class AppStandbyController {
throw re.rethrowFromSystemServer();
}
- final long elapsedRealtime = SystemClock.elapsedRealtime();
+ final long elapsedRealtime = mInjector.elapsedRealtime();
for (int i = 0; i < runningUserIds.length; i++) {
final int userId = runningUserIds[i];
if (checkUserId != UserHandle.USER_ALL && checkUserId != userId) {
@@ -329,30 +360,71 @@ public class AppStandbyController {
for (int p = 0; p < packageCount; p++) {
final PackageInfo pi = packages.get(p);
final String packageName = pi.packageName;
- final boolean isIdle = isAppIdleFiltered(packageName,
+ final boolean isSpecial = isAppSpecial(packageName,
UserHandle.getAppId(pi.applicationInfo.uid),
- userId, elapsedRealtime);
- mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS,
- userId, isIdle ? 1 : 0, packageName));
- if (isIdle) {
+ userId);
+ if (DEBUG) {
+ Slog.d(TAG, " Checking idle state for " + packageName);
+ }
+ if (isSpecial) {
+ maybeInformListeners(packageName, userId, elapsedRealtime, false);
+ } else if (mUseInternalBucketingHeuristics) {
synchronized (mAppIdleLock) {
- mAppIdleHistory.setIdle(packageName, userId, elapsedRealtime);
+ int oldBucket = mAppIdleHistory.getAppStandbyBucket(packageName, userId,
+ elapsedRealtime);
+ String bucketingReason = mAppIdleHistory.getAppStandbyReason(packageName,
+ userId, elapsedRealtime);
+ if (bucketingReason != null
+ && (bucketingReason.equals(AppStandby.REASON_FORCED)
+ || bucketingReason.startsWith(AppStandby.REASON_PREDICTED))) {
+ continue;
+ }
+ int newBucket = getBucketForLocked(packageName, userId,
+ elapsedRealtime);
+ if (DEBUG) {
+ Slog.d(TAG, " Old bucket=" + oldBucket
+ + ", newBucket=" + newBucket);
+ }
+ if (oldBucket != newBucket) {
+ mAppIdleHistory.setAppStandbyBucket(packageName, userId,
+ elapsedRealtime, newBucket, AppStandby.REASON_TIMEOUT);
+ maybeInformListeners(packageName, userId, elapsedRealtime,
+ newBucket >= AppStandby.STANDBY_BUCKET_RARE);
+ }
}
}
}
}
if (DEBUG) {
Slog.d(TAG, "checkIdleStates took "
- + (SystemClock.elapsedRealtime() - elapsedRealtime));
+ + (mInjector.elapsedRealtime() - elapsedRealtime));
}
return true;
}
+ private void maybeInformListeners(String packageName, int userId,
+ long elapsedRealtime, boolean isIdle) {
+ synchronized (mAppIdleLock) {
+ if (mAppIdleHistory.shouldInformListeners(packageName, userId,
+ elapsedRealtime, isIdle)) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS,
+ userId, isIdle ? 1 : 0, packageName));
+ }
+ }
+ }
+
+ @StandbyBuckets int getBucketForLocked(String packageName, int userId,
+ long elapsedRealtime) {
+ int bucketIndex = mAppIdleHistory.getThresholdIndex(packageName, userId,
+ elapsedRealtime, mAppStandbyScreenThresholds, mAppStandbyElapsedThresholds);
+ return THRESHOLD_BUCKETS[bucketIndex];
+ }
+
/** Check if it's been a while since last parole and let idle apps do some work */
void checkParoleTimeout() {
boolean setParoled = false;
synchronized (mAppIdleLock) {
- final long now = System.currentTimeMillis();
+ final long now = mInjector.currentTimeMillis();
if (!mAppIdleTempParoled) {
final long timeSinceLastParole = now - mLastAppIdleParoledTime;
if (timeSinceLastParole > mAppIdleParoleIntervalMillis) {
@@ -374,10 +446,10 @@ public class AppStandbyController {
final int uid = mPackageManager.getPackageUidAsUser(packageName,
PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
if (idle) {
- mBatteryStats.noteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE,
+ mInjector.noteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_INACTIVE,
packageName, uid);
} else {
- mBatteryStats.noteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE,
+ mInjector.noteEvent(BatteryStats.HistoryItem.EVENT_PACKAGE_ACTIVE,
packageName, uid);
}
} catch (PackageManager.NameNotFoundException | RemoteException e) {
@@ -389,7 +461,7 @@ public class AppStandbyController {
if (DEBUG) Slog.i(TAG, "DeviceIdleMode changed to " + deviceIdle);
boolean paroled = false;
synchronized (mAppIdleLock) {
- final long timeSinceLastParole = System.currentTimeMillis() - mLastAppIdleParoledTime;
+ final long timeSinceLastParole = mInjector.currentTimeMillis() - mLastAppIdleParoledTime;
if (!deviceIdle
&& timeSinceLastParole >= mAppIdleParoleIntervalMillis) {
if (DEBUG) {
@@ -419,13 +491,11 @@ public class AppStandbyController {
|| event.mEventType == UsageEvents.Event.USER_INTERACTION)) {
mAppIdleHistory.reportUsage(event.mPackage, userId, elapsedRealtime);
if (previouslyIdle) {
- mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS, userId,
- /* idle = */ 0, event.mPackage));
+ maybeInformListeners(event.mPackage, userId, elapsedRealtime, false);
notifyBatteryStats(event.mPackage, userId, false);
}
}
}
-
}
/**
@@ -439,7 +509,7 @@ public class AppStandbyController {
void forceIdleState(String packageName, int userId, boolean idle) {
final int appId = getAppId(packageName);
if (appId < 0) return;
- final long elapsedRealtime = SystemClock.elapsedRealtime();
+ final long elapsedRealtime = mInjector.elapsedRealtime();
final boolean previouslyIdle = isAppIdleFiltered(packageName, appId,
userId, elapsedRealtime);
@@ -470,7 +540,7 @@ public class AppStandbyController {
}
}
- void addListener(UsageStatsManagerInternal.AppIdleStateChangeListener listener) {
+ void addListener(AppIdleStateChangeListener listener) {
synchronized (mAppIdleLock) {
if (!mPackageAccessListeners.contains(listener)) {
mPackageAccessListeners.add(listener);
@@ -478,7 +548,7 @@ public class AppStandbyController {
}
}
- void removeListener(UsageStatsManagerInternal.AppIdleStateChangeListener listener) {
+ void removeListener(AppIdleStateChangeListener listener) {
synchronized (mAppIdleLock) {
mPackageAccessListeners.remove(listener);
}
@@ -501,75 +571,79 @@ public class AppStandbyController {
return false;
}
if (shouldObfuscateInstantApps &&
- mPackageManagerInternal.isPackageEphemeral(userId, packageName)) {
+ mInjector.isPackageEphemeral(userId, packageName)) {
return false;
}
return isAppIdleFiltered(packageName, getAppId(packageName), userId, elapsedRealtime);
}
- /**
- * Checks if an app has been idle for a while and filters out apps that are excluded.
- * It returns false if the current system state allows all apps to be considered active.
- * This happens if the device is plugged in or temporarily allowed to make exceptions.
- * Called by interface impls.
- */
- boolean isAppIdleFiltered(String packageName, int appId, int userId,
- long elapsedRealtime) {
+ /** Returns true if this app should be whitelisted for some reason, to never go into standby */
+ boolean isAppSpecial(String packageName, int appId, int userId) {
if (packageName == null) return false;
// If not enabled at all, of course nobody is ever idle.
if (!mAppIdleEnabled) {
- return false;
+ return true;
}
if (appId < Process.FIRST_APPLICATION_UID) {
// System uids never go idle.
- return false;
+ return true;
}
if (packageName.equals("android")) {
// Nor does the framework (which should be redundant with the above, but for MR1 we will
// retain this for safety).
- return false;
+ return true;
}
if (mSystemServicesReady) {
try {
// We allow all whitelisted apps, including those that don't want to be whitelisted
// for idle mode, because app idle (aka app standby) is really not as big an issue
// for controlling who participates vs. doze mode.
- if (mDeviceIdleController.isPowerSaveWhitelistExceptIdleApp(packageName)) {
- return false;
+ if (mInjector.isPowerSaveWhitelistExceptIdleApp(packageName)) {
+ return true;
}
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
if (isActiveDeviceAdmin(packageName, userId)) {
- return false;
+ return true;
}
if (isActiveNetworkScorer(packageName)) {
- return false;
+ return true;
}
if (mAppWidgetManager != null
- && mAppWidgetManager.isBoundWidgetPackage(packageName, userId)) {
- return false;
+ && mInjector.isBoundWidgetPackage(mAppWidgetManager, packageName, userId)) {
+ return true;
}
if (isDeviceProvisioningPackage(packageName)) {
- return false;
+ return true;
}
}
- if (!isAppIdleUnfiltered(packageName, userId, elapsedRealtime)) {
- return false;
+ // Check this last, as it can be the most expensive check
+ if (isCarrierApp(packageName)) {
+ return true;
}
- // Check this last, as it is the most expensive check
- // TODO: Optimize this by fetching the carrier privileged apps ahead of time
- if (isCarrierApp(packageName)) {
+ return false;
+ }
+
+ /**
+ * Checks if an app has been idle for a while and filters out apps that are excluded.
+ * It returns false if the current system state allows all apps to be considered active.
+ * This happens if the device is plugged in or temporarily allowed to make exceptions.
+ * Called by interface impls.
+ */
+ boolean isAppIdleFiltered(String packageName, int appId, int userId,
+ long elapsedRealtime) {
+ if (isAppSpecial(packageName, appId, userId)) {
return false;
+ } else {
+ return isAppIdleUnfiltered(packageName, userId, elapsedRealtime);
}
-
- return true;
}
int[] getIdleUidsForUser(int userId) {
@@ -577,7 +651,7 @@ public class AppStandbyController {
return new int[0];
}
- final long elapsedRealtime = SystemClock.elapsedRealtime();
+ final long elapsedRealtime = mInjector.elapsedRealtime();
List<ApplicationInfo> apps;
try {
@@ -613,7 +687,7 @@ public class AppStandbyController {
}
}
if (DEBUG) {
- Slog.d(TAG, "getIdleUids took " + (SystemClock.elapsedRealtime() - elapsedRealtime));
+ Slog.d(TAG, "getIdleUids took " + (mInjector.elapsedRealtime() - elapsedRealtime));
}
int numIdle = 0;
for (int i = uidStates.size() - 1; i >= 0; i--) {
@@ -643,6 +717,21 @@ public class AppStandbyController {
.sendToTarget();
}
+ @StandbyBuckets int getAppStandbyBucket(String packageName, int userId,
+ long elapsedRealtime, boolean shouldObfuscateInstantApps) {
+ if (shouldObfuscateInstantApps &&
+ mInjector.isPackageEphemeral(userId, packageName)) {
+ return AppStandby.STANDBY_BUCKET_ACTIVE;
+ }
+
+ return mAppIdleHistory.getAppStandbyBucket(packageName, userId, elapsedRealtime);
+ }
+
+ void setAppStandbyBucket(String packageName, int userId, @StandbyBuckets int newBucket,
+ String reason, long elapsedRealtime) {
+ mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket, reason);
+ }
+
private boolean isActiveDeviceAdmin(String packageName, int userId) {
DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
if (dpm == null) return false;
@@ -662,7 +751,7 @@ public class AppStandbyController {
private boolean isCarrierApp(String packageName) {
synchronized (mAppIdleLock) {
if (!mHaveCarrierPrivilegedApps) {
- fetchCarrierPrivilegedAppsLA();
+ fetchCarrierPrivilegedAppsLocked();
}
if (mCarrierPrivilegedApps != null) {
return mCarrierPrivilegedApps.contains(packageName);
@@ -682,7 +771,7 @@ public class AppStandbyController {
}
@GuardedBy("mAppIdleLock")
- private void fetchCarrierPrivilegedAppsLA() {
+ private void fetchCarrierPrivilegedAppsLocked() {
TelephonyManager telephonyManager =
mContext.getSystemService(TelephonyManager.class);
mCarrierPrivilegedApps = telephonyManager.getPackagesWithCarrierPrivileges();
@@ -693,20 +782,19 @@ public class AppStandbyController {
}
private boolean isActiveNetworkScorer(String packageName) {
- NetworkScoreManager nsm = (NetworkScoreManager) mContext.getSystemService(
- Context.NETWORK_SCORE_SERVICE);
- return packageName != null && packageName.equals(nsm.getActiveScorerPackage());
+ String activeScorer = mInjector.getActiveNetworkScorer();
+ return packageName != null && packageName.equals(activeScorer);
}
void informListeners(String packageName, int userId, boolean isIdle) {
- for (UsageStatsManagerInternal.AppIdleStateChangeListener listener : mPackageAccessListeners) {
+ for (AppIdleStateChangeListener listener : mPackageAccessListeners) {
listener.onAppIdleStateChanged(packageName, userId, isIdle);
}
}
void informParoleStateChanged() {
final boolean paroled = isParoledOrCharging();
- for (UsageStatsManagerInternal.AppIdleStateChangeListener listener : mPackageAccessListeners) {
+ for (AppIdleStateChangeListener listener : mPackageAccessListeners) {
listener.onParoleStateChanged(paroled);
}
}
@@ -726,8 +814,7 @@ public class AppStandbyController {
}
boolean isDisplayOn() {
- return mDisplayManager
- .getDisplay(Display.DEFAULT_DISPLAY).getState() == Display.STATE_ON;
+ return mInjector.isDefaultDisplayOn();
}
void clearAppIdleForPackage(String packageName, int userId) {
@@ -755,7 +842,7 @@ public class AppStandbyController {
void initializeDefaultsForSystemApps(int userId) {
Slog.d(TAG, "Initializing defaults for system apps on user " + userId);
- final long elapsedRealtime = SystemClock.elapsedRealtime();
+ final long elapsedRealtime = mInjector.elapsedRealtime();
List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser(
PackageManager.MATCH_DISABLED_COMPONENTS,
userId);
@@ -828,6 +915,116 @@ public class AppStandbyController {
pw.print(" mLastAppIdleParoledTime=");
TimeUtils.formatDuration(mLastAppIdleParoledTime, pw);
pw.println();
+ pw.print("mScreenThresholds="); pw.println(Arrays.toString(mAppStandbyScreenThresholds));
+ pw.print("mElapsedThresholds="); pw.println(Arrays.toString(mAppStandbyElapsedThresholds));
+ }
+
+ /**
+ * Injector for interaction with external code. Override methods to provide a mock
+ * implementation for tests.
+ * onBootPhase() must be called with at least the PHASE_SYSTEM_SERVICES_READY
+ */
+ static class Injector {
+
+ private final Context mContext;
+ private final Looper mLooper;
+ private IDeviceIdleController mDeviceIdleController;
+ private IBatteryStats mBatteryStats;
+ private PackageManagerInternal mPackageManagerInternal;
+ private DisplayManager mDisplayManager;
+ int mBootPhase;
+
+ Injector(Context context, Looper looper) {
+ mContext = context;
+ mLooper = looper;
+ }
+
+ Context getContext() {
+ return mContext;
+ }
+
+ Looper getLooper() {
+ return mLooper;
+ }
+
+ void onBootPhase(int phase) {
+ if (phase == PHASE_SYSTEM_SERVICES_READY) {
+ mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
+ ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
+ mBatteryStats = IBatteryStats.Stub.asInterface(
+ ServiceManager.getService(BatteryStats.SERVICE_NAME));
+ mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
+ mDisplayManager = (DisplayManager) mContext.getSystemService(
+ Context.DISPLAY_SERVICE);
+ }
+ mBootPhase = phase;
+ }
+
+ int getBootPhase() {
+ return mBootPhase;
+ }
+
+ /**
+ * Returns the elapsed realtime since the device started. Override this
+ * to control the clock.
+ * @return elapsed realtime
+ */
+ long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ long currentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ boolean isAppIdleEnabled() {
+ return mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_enableAutoPowerModes);
+ }
+
+ boolean isCharging() {
+ return mContext.getSystemService(BatteryManager.class).isCharging();
+ }
+
+ boolean isPowerSaveWhitelistExceptIdleApp(String packageName) throws RemoteException {
+ return mDeviceIdleController.isPowerSaveWhitelistExceptIdleApp(packageName);
+ }
+
+ File getDataSystemDirectory() {
+ return Environment.getDataSystemDirectory();
+ }
+
+ void noteEvent(int event, String packageName, int uid) throws RemoteException {
+ mBatteryStats.noteEvent(event, packageName, uid);
+ }
+
+ boolean isPackageEphemeral(int userId, String packageName) {
+ return mPackageManagerInternal.isPackageEphemeral(userId, packageName);
+ }
+
+ int[] getRunningUserIds() throws RemoteException {
+ return ActivityManager.getService().getRunningUserIds();
+ }
+
+ boolean isDefaultDisplayOn() {
+ return mDisplayManager
+ .getDisplay(Display.DEFAULT_DISPLAY).getState() == Display.STATE_ON;
+ }
+
+ void registerDisplayListener(DisplayManager.DisplayListener listener, Handler handler) {
+ mDisplayManager.registerDisplayListener(listener, handler);
+ }
+
+ String getActiveNetworkScorer() {
+ NetworkScoreManager nsm = (NetworkScoreManager) mContext.getSystemService(
+ Context.NETWORK_SCORE_SERVICE);
+ return nsm.getActiveScorerPackage();
+ }
+
+ public boolean isBoundWidgetPackage(AppWidgetManager appWidgetManager, String packageName,
+ int userId) {
+ return appWidgetManager.isBoundWidgetPackage(packageName, userId);
+ }
}
class AppStandbyHandler extends Handler {
@@ -839,6 +1036,10 @@ public class AppStandbyController {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
+ case MSG_INFORM_LISTENERS:
+ informListeners((String) msg.obj, msg.arg1, msg.arg2 == 1);
+ break;
+
case MSG_FORCE_IDLE_STATE:
forceIdleState((String) msg.obj, msg.arg1, msg.arg2 == 1);
break;
@@ -911,7 +1112,7 @@ public class AppStandbyController {
if (displayId == Display.DEFAULT_DISPLAY) {
final boolean displayOn = isDisplayOn();
synchronized (mAppIdleLock) {
- mAppIdleHistory.updateDisplay(displayOn, SystemClock.elapsedRealtime());
+ mAppIdleHistory.updateDisplay(displayOn, mInjector.elapsedRealtime());
}
}
}
@@ -931,6 +1132,8 @@ public class AppStandbyController {
private static final String KEY_WALLCLOCK_THRESHOLD = "wallclock_threshold";
private static final String KEY_PAROLE_INTERVAL = "parole_interval";
private static final String KEY_PAROLE_DURATION = "parole_duration";
+ private static final String KEY_SCREEN_TIME_THRESHOLDS = "screen_thresholds";
+ private static final String KEY_ELAPSED_TIME_THRESHOLDS = "elapsed_thresholds";
private final KeyValueListParser mParser = new KeyValueListParser(',');
@@ -969,7 +1172,7 @@ public class AppStandbyController {
COMPRESS_TIME ? ONE_MINUTE * 8 : 2L * 24 * 60 * ONE_MINUTE); // 2 days
mCheckIdleIntervalMillis = Math.min(mAppIdleScreenThresholdMillis / 4,
- COMPRESS_TIME ? ONE_MINUTE : 8 * 60 * ONE_MINUTE); // 8 hours
+ COMPRESS_TIME ? ONE_MINUTE : 4 * 60 * ONE_MINUTE); // 4 hours
// Default: 24 hours between paroles
mAppIdleParoleIntervalMillis = mParser.getLong(KEY_PAROLE_INTERVAL,
@@ -979,9 +1182,35 @@ public class AppStandbyController {
COMPRESS_TIME ? ONE_MINUTE : 10 * ONE_MINUTE); // 10 minutes
mAppIdleHistory.setThresholds(mAppIdleWallclockThresholdMillis,
mAppIdleScreenThresholdMillis);
+
+ String screenThresholdsValue = mParser.getString(KEY_SCREEN_TIME_THRESHOLDS, null);
+ mAppStandbyScreenThresholds = parseLongArray(screenThresholdsValue,
+ SCREEN_TIME_THRESHOLDS);
+
+ String elapsedThresholdsValue = mParser.getString(KEY_ELAPSED_TIME_THRESHOLDS, null);
+ mAppStandbyElapsedThresholds = parseLongArray(elapsedThresholdsValue,
+ ELAPSED_TIME_THRESHOLDS);
}
}
- }
+ long[] parseLongArray(String values, long[] defaults) {
+ if (values == null) return defaults;
+ if (values.isEmpty()) {
+ // Reset to defaults
+ return defaults;
+ } else {
+ String[] thresholds = values.split("/");
+ if (thresholds.length == THRESHOLD_BUCKETS.length) {
+ long[] array = new long[THRESHOLD_BUCKETS.length];
+ for (int i = 0; i < THRESHOLD_BUCKETS.length; i++) {
+ array[i] = Long.parseLong(thresholds[i]);
+ }
+ return array;
+ } else {
+ return defaults;
+ }
+ }
+ }
+ }
}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index afafea19afd0..3a958da2db16 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -20,6 +20,7 @@ import android.Manifest;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.IUidObserver;
+import android.app.usage.AppStandby;
import android.app.usage.ConfigurationStats;
import android.app.usage.IUsageStatsManager;
import android.app.usage.UsageEvents;
@@ -654,6 +655,55 @@ public class UsageStatsService extends SystemService implements
}
@Override
+ public int getAppStandbyBucket(String packageName, String callingPackage, int userId) {
+ if (!hasPermission(callingPackage)) {
+ throw new SecurityException("Don't have permission to query app standby bucket");
+ }
+
+ final int callingUid = Binder.getCallingUid();
+ try {
+ userId = ActivityManager.getService().handleIncomingUser(
+ Binder.getCallingPid(), callingUid, userId, false, true,
+ "getAppStandbyBucket", null);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ final boolean obfuscateInstantApps = shouldObfuscateInstantAppsForCaller(callingUid,
+ userId);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return mAppStandby.getAppStandbyBucket(packageName, userId,
+ SystemClock.elapsedRealtime(), obfuscateInstantApps);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void setAppStandbyBucket(String packageName,
+ int bucket, int userId) {
+ getContext().enforceCallingPermission(Manifest.permission.CHANGE_APP_IDLE_STATE,
+ "No permission to change app standby state");
+
+ final int callingUid = Binder.getCallingUid();
+ try {
+ userId = ActivityManager.getService().handleIncomingUser(
+ Binder.getCallingPid(), callingUid, userId, false, true,
+ "setAppStandbyBucket", null);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mAppStandby.setAppStandbyBucket(packageName, userId, bucket,
+ AppStandby.REASON_PREDICTED + ":" + callingUid,
+ SystemClock.elapsedRealtime());
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
public void whitelistAppTemporarily(String packageName, long duration, int userId)
throws RemoteException {
StringBuilder reason = new StringBuilder(32);