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);
+ }
+
+}