diff options
| author | 2017-09-29 13:17:43 -0700 | |
|---|---|---|
| committer | 2017-10-24 19:24:15 -0700 | |
| commit | 17fffee4908f11038ba9cc5a672d15cb25be3dfe (patch) | |
| tree | 4777a31456f8d8d30b0a8d59bef0ada54145dffe | |
| parent | 22910ada9abbda61346d3a28470be2176364f77e (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
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); |