Reduce the timeframe which Deletion Helper queries for usage stats.
By querying only 1 year back instead of infinity, we can improve
get results slightly faster and avoid some issues w.r.t. querying
for all time (i.e. start time 0).
As a side-note, this should also fix the problem where apps which
should be ineligible were showing up as eligible. We will need
to monitor that situation in the dogfood population.
Bug: 34400961
Test: Robotests
Change-Id: I5cd3202ad25db5716150c857c7da2422c0fa741e
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1259e30..07f747e 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -24,8 +24,8 @@
<string name="deletion_helper_title">Choose items to remove</string>
<!-- Summary of how much storage an app is using and the number of days since last use. [CHAR LIMIT=NONE]-->
<string name="deletion_helper_app_summary"><xliff:g id="used" example="1.2GB">%1$s</xliff:g> • <xliff:g id="days" example="67">%2$d</xliff:g> days ago</string>
- <!-- Summary of how much storage an app is using when it has never been used before. [CHAR LIMIT=NONE]-->
- <string name="deletion_helper_app_summary_never_used"><xliff:g id="used" example="1.2GB">%1$s</xliff:g> • Never used before</string>
+ <!-- Summary of how much storage an app is using when it has not been used in the last year. [CHAR LIMIT=NONE]-->
+ <string name="deletion_helper_app_summary_never_used"><xliff:g id="used" example="1.2GB">%1$s</xliff:g> • Not used in last year</string>
<!-- Summary of how much storage an app is using when its last use is unknown. [CHAR LIMIT=NONE]-->
<string name="deletion_helper_app_summary_unknown_used"><xliff:g id="used" example="1.2GB">%1$s</xliff:g> • Not sure when last used</string>
<!-- Button which clears out storage in the deletion helper. [CHAR LIMIT=60]-->
diff --git a/robotests/src/com/android/storagemanager/automatic/AutomaticStorageManagementJobServiceTest.java b/robotests/src/com/android/storagemanager/automatic/AutomaticStorageManagementJobServiceTest.java
deleted file mode 100644
index c0e1cd4..0000000
--- a/robotests/src/com/android/storagemanager/automatic/AutomaticStorageManagementJobServiceTest.java
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
- * Copyright (C) 2016 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.storagemanager.automatic;
-
-import android.app.NotificationManager;
-import android.app.job.JobParameters;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.BatteryManager;
-import android.os.storage.StorageManager;
-import android.os.storage.VolumeInfo;
-import android.provider.Settings;
-import com.android.settingslib.deviceinfo.StorageVolumeProvider;
-import com.android.storagemanager.overlay.FeatureFactory;
-import com.android.storagemanager.overlay.StorageManagementJobProvider;
-import com.android.storagemanager.testing.TestingConstants;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.Robolectric;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-import org.robolectric.shadows.ShadowApplication;
-import org.robolectric.util.ReflectionHelpers;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest=TestingConstants.MANIFEST, sdk=TestingConstants.SDK_VERSION)
-public class AutomaticStorageManagementJobServiceTest {
- @Mock private BatteryManager mBatteryManager;
- @Mock private NotificationManager mNotificationManager;
- @Mock private VolumeInfo mVolumeInfo;
- @Mock private File mFile;
- @Mock private JobParameters mJobParameters;
- @Mock private StorageManagementJobProvider mStorageManagementJobProvider;
- @Mock private FeatureFactory mFeatureFactory;
- @Mock private StorageVolumeProvider mStorageVolumeProvider;
- private AutomaticStorageManagementJobService mJobService;
- private ShadowApplication mApplication;
- private List<VolumeInfo> mVolumes;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- when(mJobParameters.getJobId()).thenReturn(0);
-
- // Let's set up our system services to act like a device that has the following conditions:
- // 1. We're plugged in and charging.
- // 2. We have a completely full device.
- // 3. ASM is disabled.
- when(mBatteryManager.isCharging()).thenReturn(true);
- mVolumes = new ArrayList<>();
- when(mVolumeInfo.getPath()).thenReturn(mFile);
- when(mVolumeInfo.getType()).thenReturn(VolumeInfo.TYPE_PRIVATE);
- when(mVolumeInfo.getFsUuid()).thenReturn(StorageManager.UUID_PRIMARY_PHYSICAL);
- when(mFile.getTotalSpace()).thenReturn(100L);
- when(mFile.getFreeSpace()).thenReturn(0L);
- mVolumes.add(mVolumeInfo);
- when(mStorageVolumeProvider.getPrimaryStorageSize()).thenReturn(100L);
- when(mStorageVolumeProvider.getVolumes()).thenReturn(mVolumes);
-
- mApplication = ShadowApplication.getInstance();
- mApplication.setSystemService(Context.BATTERY_SERVICE, mBatteryManager);
- mApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNotificationManager);
-
- // This is a hack-y injection of our own FeatureFactory.
- // By default, the Storage Manager has a FeatureFactory which returns null for all features.
- // Using reflection, we can inject our own FeatureFactory which returns a mock for the
- // StorageManagementJobProvider feature. This lets us observe when the ASMJobService
- // actually tries to run the job.
- when(mFeatureFactory.getStorageManagementJobProvider())
- .thenReturn(mStorageManagementJobProvider);
- when(mStorageManagementJobProvider.onStartJob(any(Context.class),
- any(JobParameters.class), any(Integer.class))).thenReturn(false);
- ReflectionHelpers.setStaticField(FeatureFactory.class, "sFactory", mFeatureFactory);
-
- // And we can't forget to initialize the actual job service.
- mJobService = spy(Robolectric.setupService(AutomaticStorageManagementJobService.class));
- mJobService.setStorageVolumeProvider(mStorageVolumeProvider);
- }
-
- @Test
- public void testJobRequiresCharging() {
- when(mBatteryManager.isCharging()).thenReturn(false);
- assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
- // The job should report that it needs to be retried, if not charging.
- assertJobFinished(true);
-
- when(mBatteryManager.isCharging()).thenReturn(true);
- assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
- assertJobFinished(false);
- }
-
- @Test
- public void testStartJobTriesUpsellWhenASMDisabled() {
- assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
- assertJobFinished(false);
- mApplication.runBackgroundTasks();
-
- List<Intent> broadcastedIntents = mApplication.getBroadcastIntents();
- assertThat(broadcastedIntents.size()).isEqualTo(1);
-
- Intent lastIntent = broadcastedIntents.get(0);
- assertThat(lastIntent.getAction())
- .isEqualTo(NotificationController.INTENT_ACTION_SHOW_NOTIFICATION);
- assertThat(lastIntent.getComponent().getClassName())
- .isEqualTo(NotificationController.class.getCanonicalName());
-
- assertStorageManagerJobDidNotRun();
- }
-
- @Test
- public void testASMJobRunsWithValidConditions() {
- activateASM();
- assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
- assertStorageManagerJobRan();
- }
-
- @Test
- public void testJobDoesntRunIfStorageNotFull() {
- activateASM();
- when(mFile.getFreeSpace()).thenReturn(100L);
- assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
- assertStorageManagerJobDidNotRun();
- }
-
- @Test
- public void testJobOnlyRunsIfFreeStorageIsUnder15Percent() {
- activateASM();
- when(mFile.getFreeSpace()).thenReturn(15L);
- assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
- assertStorageManagerJobDidNotRun();
-
- when(mFile.getFreeSpace()).thenReturn(14L);
- assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
- assertStorageManagerJobRan();
- }
-
- @Test
- public void testNonDefaultDaysToRetain() {
- ContentResolver resolver = mApplication.getApplicationContext().getContentResolver();
- Settings.Secure.putInt(resolver, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN,
- 30);
- activateASM();
- assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
- assertStorageManagerJobRan(30);
- }
-
- @Test
- public void testNonPrivateDrivesIgnoredForFreeSpaceCalculation() {
- File notPrivate = mock(File.class);
- VolumeInfo nonPrivateVolume = mock(VolumeInfo.class);
- when(nonPrivateVolume.getPath()).thenReturn(notPrivate);
- when(nonPrivateVolume.getType()).thenReturn(VolumeInfo.TYPE_PUBLIC);
- when(notPrivate.getTotalSpace()).thenReturn(100L);
- when(notPrivate.getFreeSpace()).thenReturn(0L);
- mVolumes.add(nonPrivateVolume);
- activateASM();
- when(mFile.getFreeSpace()).thenReturn(15L);
-
- assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
- assertStorageManagerJobDidNotRun();
- }
-
- @Test
- public void testMultiplePrivateVolumesCountedForASMActivationThrsehold() {
- File privateVolume = mock(File.class);
- VolumeInfo privateVolumeInfo = mock(VolumeInfo.class);
- when(privateVolumeInfo.getPath()).thenReturn(privateVolume);
- when(privateVolumeInfo.getType()).thenReturn(VolumeInfo.TYPE_PRIVATE);
- when(privateVolumeInfo.getFsUuid()).thenReturn(StorageManager.UUID_PRIVATE_INTERNAL);
- when(privateVolume.getTotalSpace()).thenReturn(100L);
- when(privateVolume.getFreeSpace()).thenReturn(0L);
- mVolumes.add(privateVolumeInfo);
- activateASM();
- when(mFile.getFreeSpace()).thenReturn(15L);
-
- assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
- assertStorageManagerJobRan();
- }
-
- @Test
- public void testPrimaryPrivateStorageDefersToStorageManager() {
- // The Storage Manager has a more accurate calculation for the UUID_PRIVATE_INTERNAL
- // volume, so we don't use the getTotalSpace() method to find it.
- // If the job accidentally uses the getTotalSpace(), it will run the job accidentally.
- when(mVolumeInfo.getFsUuid()).thenReturn(StorageManager.UUID_PRIVATE_INTERNAL);
- when(mFile.getTotalSpace()).thenReturn(1000L);
- when(mFile.getFreeSpace()).thenReturn(15L);
- activateASM();
-
- assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
- assertStorageManagerJobDidNotRun();
- }
-
- private void assertJobFinished(boolean retryNeeded) {
- verify(mJobService).jobFinished(any(JobParameters.class), eq(retryNeeded));
- }
-
- private void assertStorageManagerJobRan() {
- assertStorageManagerJobRan(
- Settings.Secure.AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN_DEFAULT);
- }
-
- private void assertStorageManagerJobRan(int daysToRetain) {
- verify(mStorageManagementJobProvider).onStartJob(eq(mJobService), eq(mJobParameters),
- eq(daysToRetain));
- }
-
- private void assertStorageManagerJobDidNotRun() {
- verifyNoMoreInteractions(mStorageManagementJobProvider);
- }
-
- private void activateASM() {
- ContentResolver resolver = mApplication.getApplicationContext().getContentResolver();
- Settings.Secure.putInt(resolver, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, 1);
- }
-}
diff --git a/src/com/android/storagemanager/deletionhelper/AppStateUsageStatsBridge.java b/src/com/android/storagemanager/deletionhelper/AppStateUsageStatsBridge.java
index c39eec8..07ca7c1 100644
--- a/src/com/android/storagemanager/deletionhelper/AppStateUsageStatsBridge.java
+++ b/src/com/android/storagemanager/deletionhelper/AppStateUsageStatsBridge.java
@@ -24,8 +24,8 @@
import android.content.pm.PackageManager;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.text.format.DateUtils;
import android.util.Log;
-import com.android.storagemanager.deletionhelper.AppStateBaseBridge;
import com.android.storagemanager.deletionhelper.AppStateBaseBridge.Callback;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
@@ -42,12 +42,17 @@
private static final String TAG = "AppStateUsageStatsBridge";
private static final String DEBUG_APP_UNUSED_OVERRIDE = "debug.asm.app_unused_limit";
-
- private UsageStatsManager mUsageStatsManager;
- private PackageManager mPm;
public static final long NEVER_USED = Long.MAX_VALUE;
public static final long UNKNOWN_LAST_USE = -1;
public static final long UNUSED_DAYS_DELETION_THRESHOLD = 90;
+ private static final long DAYS_IN_A_TYPICAL_YEAR = 365;
+
+ private UsageStatsManager mUsageStatsManager;
+ private PackageManager mPm;
+ // This clock is used to provide the time. By default, it uses the system clock, but can be
+ // replaced for test purposes.
+ protected Clock mClock;
+
public AppStateUsageStatsBridge(Context context, ApplicationsState appState,
Callback callback) {
@@ -55,6 +60,7 @@
mUsageStatsManager =
(UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
mPm = context.getPackageManager();
+ mClock = new Clock();
}
@Override
@@ -62,8 +68,7 @@
ArrayList<AppEntry> apps = mAppSession.getAllApps();
if (apps == null) return;
- final Map<String, UsageStats> map = mUsageStatsManager.queryAndAggregateUsageStats(0,
- System.currentTimeMillis());
+ final Map<String, UsageStats> map = getAggregatedUsageStats();
for (AppEntry entry : apps) {
UsageStats usageStats = map.get(entry.info.packageName);
entry.extraInfo = new UsageStatsState(getDaysSinceLastUse(usageStats),
@@ -74,8 +79,7 @@
@Override
protected void updateExtraInfo(AppEntry app, String pkg, int uid) {
- Map<String, UsageStats> map = mUsageStatsManager.queryAndAggregateUsageStats(0,
- System.currentTimeMillis());
+ Map<String, UsageStats> map = getAggregatedUsageStats();
UsageStats usageStats = map.get(app.info.packageName);
app.extraInfo = new UsageStatsState(getDaysSinceLastUse(usageStats),
getDaysSinceInstalled(app.info.packageName),
@@ -91,7 +95,13 @@
if (lastUsed <= 0) {
return UNKNOWN_LAST_USE;
}
- return TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - lastUsed);
+
+ // Theoretically, this should be impossible, but UsageStatsService, uh, finds a way.
+ long days = (TimeUnit.MILLISECONDS.toDays(mClock.getCurrentTime() - lastUsed));
+ if (days > DAYS_IN_A_TYPICAL_YEAR) {
+ return NEVER_USED;
+ }
+ return days;
}
private long getDaysSinceInstalled(String packageName) {
@@ -105,8 +115,13 @@
if (pi == null) {
return UNKNOWN_LAST_USE;
}
+ return (TimeUnit.MILLISECONDS.toDays(mClock.getCurrentTime() - pi.firstInstallTime));
+ }
- return (TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - pi.firstInstallTime));
+ private Map<String, UsageStats> getAggregatedUsageStats() {
+ long now = mClock.getCurrentTime();
+ long startTime = now - DateUtils.YEAR_IN_MILLIS;
+ return mUsageStatsManager.queryAndAggregateUsageStats(startTime, now);
}
/**
@@ -171,4 +186,13 @@
this.userId = userId;
}
}
+
+ /**
+ * Clock provides the current time.
+ */
+ static class Clock {
+ public long getCurrentTime() {
+ return System.currentTimeMillis();
+ }
+ }
}