diff options
| author | 2016-12-21 02:14:51 +0000 | |
|---|---|---|
| committer | 2016-12-21 02:14:54 +0000 | |
| commit | 8fd55702b1b39dc28033d38c06d1711eea4ffe21 (patch) | |
| tree | 41e19aeecc431663ad3d7e62148b4bea6bf92e95 | |
| parent | 5153e46897f8441b55dcace2530c613a4bd9f99a (diff) | |
| parent | c7d9de59bfa083472a5c8fcfc02c2363b33e204f (diff) | |
Merge "Add an app size collector." into nyc-mr2-dev
3 files changed, 363 insertions, 1 deletions
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 000000000000..cf05e9f73d19 --- /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 50e0662ad4d4..ce0a33453e3e 100644 --- a/services/tests/servicestests/Android.mk +++ b/services/tests/servicestests/Android.mk @@ -21,7 +21,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ 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 000000000000..da22e77956a1 --- /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); + } + +} |