Fix a bug where multiple private volumes caused inaccuracies.

Settings and the Storage Manager used slightly different calculations
for the total amount of space and the amount of space remaining. By
moving to a unified calculation, the ASM will run at the expected %
shown in storage Settings.

Bug: 30895163
Test: m RunStorageManagerRoboTests
Change-Id: I81aa15be953c2950692b5d7a7c7ec2943c18edb5
diff --git a/robotests/src/com/android/storagemanager/automatic/AutomaticStorageManagementJobServiceTest.java b/robotests/src/com/android/storagemanager/automatic/AutomaticStorageManagementJobServiceTest.java
new file mode 100644
index 0000000..c0e1cd4
--- /dev/null
+++ b/robotests/src/com/android/storagemanager/automatic/AutomaticStorageManagementJobServiceTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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/automatic/AutomaticStorageManagementJobService.java b/src/com/android/storagemanager/automatic/AutomaticStorageManagementJobService.java
index 9a8b1f7..9954813 100644
--- a/src/com/android/storagemanager/automatic/AutomaticStorageManagementJobService.java
+++ b/src/com/android/storagemanager/automatic/AutomaticStorageManagementJobService.java
@@ -21,14 +21,14 @@
 import android.content.Context;
 import android.os.BatteryManager;
 import android.os.storage.StorageManager;
-import android.os.storage.VolumeInfo;
 import android.provider.Settings;
 import android.util.Log;
+import com.android.settingslib.deviceinfo.PrivateStorageInfo;
+import com.android.settingslib.deviceinfo.StorageManagerVolumeProvider;
+import com.android.settingslib.deviceinfo.StorageVolumeProvider;
 import com.android.storagemanager.overlay.FeatureFactory;
 import com.android.storagemanager.overlay.StorageManagementJobProvider;
 
-import java.io.File;
-
 /**
  * {@link JobService} class to start automatic storage clearing jobs to free up space. The job only
  * starts if the device is under a certain percent of free storage.
@@ -39,6 +39,7 @@
     private static final long DEFAULT_LOW_FREE_PERCENT = 15;
 
     private StorageManagementJobProvider mProvider;
+    private StorageVolumeProvider mVolumeProvider;
 
     @Override
     public boolean onStartJob(JobParameters args) {
@@ -51,10 +52,7 @@
             return false;
         }
 
-        StorageManager manager = getSystemService(StorageManager.class);
-        VolumeInfo internalVolume = manager.findVolumeById(VolumeInfo.ID_PRIVATE_INTERNAL);
-        final File dataPath = internalVolume.getPath();
-        if (!volumeNeedsManagement(dataPath)) {
+        if (!volumeNeedsManagement()) {
             Log.i(TAG, "Skipping automatic storage management.");
             Settings.Secure.putLong(getContentResolver(),
                     Settings.Secure.AUTOMATIC_STORAGE_MANAGER_LAST_RUN,
@@ -90,15 +88,26 @@
         return false;
     }
 
+    void setStorageVolumeProvider(StorageVolumeProvider storageProvider) {
+        mVolumeProvider = storageProvider;
+    }
+
     private int getDaysToRetain() {
         return Settings.Secure.getInt(getContentResolver(),
                 Settings.Secure.AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN,
                 Settings.Secure.AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN_DEFAULT);
     }
 
-    private boolean volumeNeedsManagement(final File dataPath) {
-        long lowStorageThreshold = (dataPath.getTotalSpace() * DEFAULT_LOW_FREE_PERCENT) / 100;
-        return dataPath.getFreeSpace() < lowStorageThreshold;
+    private boolean volumeNeedsManagement() {
+        if (mVolumeProvider == null) {
+            mVolumeProvider = new StorageManagerVolumeProvider(
+                    getSystemService(StorageManager.class));
+        }
+
+        PrivateStorageInfo info = PrivateStorageInfo.getPrivateStorageInfo(mVolumeProvider);
+
+        long lowStorageThreshold = (info.totalBytes * DEFAULT_LOW_FREE_PERCENT) / 100;
+        return info.freeBytes < lowStorageThreshold;
     }
 
     private boolean preconditionsFulfilled() {