Loading services/core/java/com/android/server/storage/AppCollector.java 0 → 100644 +160 −0 Original line number Diff line number Diff line /* * 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); } } } } } } } services/tests/servicestests/Android.mk +2 −1 Original line number Diff line number Diff line Loading @@ -23,7 +23,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ android-support-test \ mockito-target-minus-junit4 \ platform-test-annotations \ ShortcutManagerTestUtils ShortcutManagerTestUtils \ truth-prebuilt LOCAL_JAVA_LIBRARIES := android.test.runner Loading services/tests/servicestests/src/com/android/server/storage/AppCollectorTest.java 0 → 100644 +201 −0 Original line number Diff line number Diff line /* * 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); } } Loading
services/core/java/com/android/server/storage/AppCollector.java 0 → 100644 +160 −0 Original line number Diff line number Diff line /* * 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); } } } } } } }
services/tests/servicestests/Android.mk +2 −1 Original line number Diff line number Diff line Loading @@ -23,7 +23,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ android-support-test \ mockito-target-minus-junit4 \ platform-test-annotations \ ShortcutManagerTestUtils ShortcutManagerTestUtils \ truth-prebuilt LOCAL_JAVA_LIBRARIES := android.test.runner Loading
services/tests/servicestests/src/com/android/server/storage/AppCollectorTest.java 0 → 100644 +201 −0 Original line number Diff line number Diff line /* * 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); } }