Add an app size collector.

The app collector gets a list of app sizes for packages on a given
storage volume. This information will be exposed as part of an
expansion of the diskstats dumpsys.

When the collector runs, it sets up a handler on a BackgroundThread
which asks the PackageManager for the package sizes for all apps and
all users. The call for the information is blocked using a
CompletableFuture until the call times out or until we've received
all of the package stats. After the stats are all obtained, the
future completes.

Bug: 32207207
Test: System server instrumentation tests
Change-Id: I3a27dc4410effb12ae33894b561c02a60322f7b0
diff --git a/services/core/java/com/android/server/storage/AppCollector.java b/services/core/java/com/android/server/storage/AppCollector.java
new file mode 100644
index 0000000..cf05e9f
--- /dev/null
+++ b/services/core/java/com/android/server/storage/AppCollector.java
@@ -0,0 +1,160 @@
+/*
+ * 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.server.storage;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageStatsObserver;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageStats;
+import android.content.pm.UserInfo;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserManager;
+import android.os.storage.VolumeInfo;
+import android.util.Log;
+import com.android.internal.os.BackgroundThread;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * AppCollector asynchronously collects package sizes.
+ */
+public class AppCollector {
+    private static String TAG = "AppCollector";
+
+    private CompletableFuture<List<PackageStats>> mStats;
+    private final BackgroundHandler mBackgroundHandler;
+
+    /**
+     * Constrcuts a new AppCollector which runs on the provided volume.
+     * @param context Android context used to get
+     * @param volume Volume to check for apps.
+     */
+    public AppCollector(Context context, VolumeInfo volume) {
+        mBackgroundHandler = new BackgroundHandler(BackgroundThread.get().getLooper(),
+                volume,
+                context.getPackageManager(),
+                (UserManager) context.getSystemService(Context.USER_SERVICE));
+    }
+
+    /**
+     * Returns a list of package stats for the context and volume. Note that in a multi-user
+     * environment, this may return stats for the same package multiple times. These "duplicate"
+     * entries will have the package stats for the package for a given user, not the package in
+     * aggregate.
+     * @param timeoutMillis Milliseconds before timing out and returning early with null.
+     */
+    public List<PackageStats> getPackageStats(long timeoutMillis) {
+        synchronized(this) {
+            if (mStats == null) {
+                mStats = new CompletableFuture<>();
+                mBackgroundHandler.sendEmptyMessage(BackgroundHandler.MSG_START_LOADING_SIZES);
+            }
+        }
+
+        List<PackageStats> value = null;
+        try {
+            value = mStats.get(timeoutMillis, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException | ExecutionException e) {
+            Log.e(TAG, "An exception occurred while getting app storage", e);
+        } catch (TimeoutException e) {
+            Log.e(TAG, "AppCollector timed out");
+        }
+        return value;
+    }
+
+    private class StatsObserver extends IPackageStatsObserver.Stub {
+        private AtomicInteger mCount;
+        private final ArrayList<PackageStats> mPackageStats;
+
+        public StatsObserver(int count) {
+            mCount = new AtomicInteger(count);
+            mPackageStats = new ArrayList<>(count);
+        }
+
+        @Override
+        public void onGetStatsCompleted(PackageStats packageStats, boolean succeeded)
+                throws RemoteException {
+            if (succeeded) {
+                mPackageStats.add(packageStats);
+            }
+
+            if (mCount.decrementAndGet() == 0) {
+                mStats.complete(mPackageStats);
+            }
+        }
+    }
+
+    private class BackgroundHandler extends Handler {
+        static final int MSG_START_LOADING_SIZES = 0;
+        private final VolumeInfo mVolume;
+        private final PackageManager mPm;
+        private final UserManager mUm;
+
+        BackgroundHandler(Looper looper, VolumeInfo volume, PackageManager pm, UserManager um) {
+            super(looper);
+            mVolume = volume;
+            mPm = pm;
+            mUm = um;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_START_LOADING_SIZES: {
+                    final List<ApplicationInfo> apps = mPm.getInstalledApplications(
+                            PackageManager.GET_UNINSTALLED_PACKAGES
+                                    | PackageManager.GET_DISABLED_COMPONENTS);
+
+                    final List<ApplicationInfo> volumeApps = new ArrayList<>();
+                    for (ApplicationInfo app : apps) {
+                        if (Objects.equals(app.volumeUuid, mVolume.getFsUuid())) {
+                            volumeApps.add(app);
+                        }
+                    }
+
+                    List<UserInfo> users = mUm.getUsers();
+                    final int count = users.size() * volumeApps.size();
+                    if (count == 0) {
+                        mStats.complete(new ArrayList<>());
+                    }
+
+                    // Kick off the async package size query for all apps.
+                    final StatsObserver observer = new StatsObserver(count);
+                    for (UserInfo user : users) {
+                        for (ApplicationInfo app : volumeApps) {
+                            mPm.getPackageSizeInfoAsUser(app.packageName, user.id,
+                                    observer);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/services/tests/servicestests/Android.mk b/services/tests/servicestests/Android.mk
index 50e0662..ce0a334 100644
--- a/services/tests/servicestests/Android.mk
+++ b/services/tests/servicestests/Android.mk
@@ -21,7 +21,8 @@
     guava \
     android-support-test \
     mockito-target \
-    ShortcutManagerTestUtils
+    ShortcutManagerTestUtils \
+    truth-prebuilt
 
 LOCAL_JAVA_LIBRARIES := android.test.runner
 
diff --git a/services/tests/servicestests/src/com/android/server/storage/AppCollectorTest.java b/services/tests/servicestests/src/com/android/server/storage/AppCollectorTest.java
new file mode 100644
index 0000000..da22e77
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/storage/AppCollectorTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.server.storage;
+
+import android.content.pm.UserInfo;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageStatsObserver;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageStats;
+import android.os.UserManager;
+import android.os.storage.VolumeInfo;
+import android.test.AndroidTestCase;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.when;
+
+@RunWith(JUnit4.class)
+public class AppCollectorTest extends AndroidTestCase {
+    private static final long TIMEOUT = TimeUnit.MINUTES.toMillis(1);
+    @Mock private Context mContext;
+    @Mock private PackageManager mPm;
+    @Mock private UserManager mUm;
+    private List<ApplicationInfo> mApps;
+    private List<UserInfo> mUsers;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+        mApps = new ArrayList<>();
+        when(mContext.getPackageManager()).thenReturn(mPm);
+        when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUm);
+
+        // Set up the app list.
+        when(mPm.getInstalledApplications(anyInt())).thenReturn(mApps);
+
+        // Set up the user list with a single user (0).
+        mUsers = new ArrayList<>();
+        mUsers.add(new UserInfo(0, "", 0));
+        when(mUm.getUsers()).thenReturn(mUsers);
+    }
+
+    @Test
+    public void testNoApps() throws Exception {
+        VolumeInfo volume = new VolumeInfo("testuuid", 0, null, null);
+        volume.fsUuid = "testuuid";
+        AppCollector collector = new AppCollector(mContext, volume);
+
+        assertThat(collector.getPackageStats(TIMEOUT)).isEmpty();
+    }
+
+    @Test
+    public void testAppOnExternalVolume() throws Exception {
+        addApplication("com.test.app", "differentuuid");
+        VolumeInfo volume = new VolumeInfo("testuuid", 0, null, null);
+        volume.fsUuid = "testuuid";
+        AppCollector collector = new AppCollector(mContext, volume);
+
+        assertThat(collector.getPackageStats(TIMEOUT)).isEmpty();
+    }
+
+    @Test
+    public void testOneValidApp() throws Exception {
+        addApplication("com.test.app", "testuuid");
+        VolumeInfo volume = new VolumeInfo("testuuid", 0, null, null);
+        volume.fsUuid = "testuuid";
+        AppCollector collector = new AppCollector(mContext, volume);
+        PackageStats stats = new PackageStats("com.test.app");
+
+        // Set up this to handle the asynchronous call to the PackageManager. This returns the
+        // package info for the specified package.
+        doAnswer(new Answer<Void>() {
+             @Override
+             public Void answer(InvocationOnMock invocation) {
+                 try {
+                     ((IPackageStatsObserver.Stub) invocation.getArguments()[2])
+                             .onGetStatsCompleted(stats, true);
+                 } catch (Exception e) {
+                     // We fail instead of just letting the exception fly because throwing
+                     // out of the callback like this on the background thread causes the test
+                     // runner to crash, rather than reporting the failure.
+                     fail();
+                 }
+                 return null;
+             }
+        }).when(mPm).getPackageSizeInfoAsUser(eq("com.test.app"), eq(0), any());
+
+
+        // Because getPackageStats is a blocking call, we block execution of the test until the
+        // call finishes. In order to finish the call, we need the above answer to execute.
+        List<PackageStats> myStats = new ArrayList<>();
+        CountDownLatch latch = new CountDownLatch(1);
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                myStats.addAll(collector.getPackageStats(TIMEOUT));
+                latch.countDown();
+            }
+        }).start();
+        latch.await();
+
+        assertThat(myStats).containsExactly(stats);
+    }
+
+    @Test
+    public void testMultipleUsersOneApp() throws Exception {
+        addApplication("com.test.app", "testuuid");
+        ApplicationInfo otherUsersApp = new ApplicationInfo();
+        otherUsersApp.packageName = "com.test.app";
+        otherUsersApp.volumeUuid = "testuuid";
+        otherUsersApp.uid = 1;
+        mUsers.add(new UserInfo(1, "", 0));
+
+        VolumeInfo volume = new VolumeInfo("testuuid", 0, null, null);
+        volume.fsUuid = "testuuid";
+        AppCollector collector = new AppCollector(mContext, volume);
+        PackageStats stats = new PackageStats("com.test.app");
+        PackageStats otherStats = new PackageStats("com.test.app");
+        otherStats.userHandle = 1;
+
+        // Set up this to handle the asynchronous call to the PackageManager. This returns the
+        // package info for our packages.
+        doAnswer(new Answer<Void>() {
+             @Override
+             public Void answer(InvocationOnMock invocation) {
+                 try {
+                     ((IPackageStatsObserver.Stub) invocation.getArguments()[2])
+                             .onGetStatsCompleted(stats, true);
+
+                     // Now callback for the other uid.
+                     ((IPackageStatsObserver.Stub) invocation.getArguments()[2])
+                             .onGetStatsCompleted(otherStats, true);
+                 } catch (Exception e) {
+                     // We fail instead of just letting the exception fly because throwing
+                     // out of the callback like this on the background thread causes the test
+                     // runner to crash, rather than reporting the failure.
+                     fail();
+                 }
+                 return null;
+             }
+        }).when(mPm).getPackageSizeInfoAsUser(eq("com.test.app"), eq(0), any());
+
+
+        // Because getPackageStats is a blocking call, we block execution of the test until the
+        // call finishes. In order to finish the call, we need the above answer to execute.
+        List<PackageStats> myStats = new ArrayList<>();
+        CountDownLatch latch = new CountDownLatch(1);
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                myStats.addAll(collector.getPackageStats(TIMEOUT));
+                latch.countDown();
+            }
+        }).start();
+        latch.await();
+
+        // This should
+        assertThat(myStats).containsAllOf(stats, otherStats);
+    }
+
+    private void addApplication(String packageName, String uuid) {
+        ApplicationInfo info = new ApplicationInfo();
+        info.packageName = packageName;
+        info.volumeUuid = uuid;
+        mApps.add(info);
+    }
+
+}