diff --git a/src/com/android/settings/applications/AppCounter.java b/src/com/android/settings/applications/AppCounter.java index fb8d5809e77..64b1987b297 100644 --- a/src/com/android/settings/applications/AppCounter.java +++ b/src/com/android/settings/applications/AppCounter.java @@ -17,44 +17,36 @@ package com.android.settings.applications; import android.app.AppGlobals; import android.content.Context; import android.content.pm.ApplicationInfo; -import android.content.pm.IPackageManager; import android.content.pm.PackageManager; -import android.content.pm.ParceledListSlice; import android.content.pm.UserInfo; import android.os.AsyncTask; -import android.os.RemoteException; -import android.os.UserHandle; import android.os.UserManager; +import java.util.List; + public abstract class AppCounter extends AsyncTask { - protected final PackageManager mPm; - protected final IPackageManager mIpm; + protected final PackageManagerWrapper mPm; protected final UserManager mUm; - public AppCounter(Context context) { - mPm = context.getPackageManager(); - mIpm = AppGlobals.getPackageManager(); + public AppCounter(Context context, PackageManagerWrapper packageManager) { + mPm = packageManager; mUm = UserManager.get(context); } @Override protected Integer doInBackground(Void... params) { int count = 0; - for (UserInfo user : mUm.getProfiles(UserHandle.myUserId())) { - try { - @SuppressWarnings("unchecked") - ParceledListSlice list = - mIpm.getInstalledApplications(PackageManager.GET_DISABLED_COMPONENTS - | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS - | (user.isAdmin() ? PackageManager.GET_UNINSTALLED_PACKAGES : 0), - user.id); - for (ApplicationInfo info : list.getList()) { - if (includeInCount(info)) { - count++; - } + for (UserInfo user : getUsersToCount()) { + final List list = + mPm.getInstalledApplicationsAsUser(PackageManager.GET_DISABLED_COMPONENTS + | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS + | (user.isAdmin() ? PackageManager.GET_UNINSTALLED_PACKAGES : 0), + user.id); + for (ApplicationInfo info : list) { + if (includeInCount(info)) { + count++; } - } catch (RemoteException e) { } } return count; @@ -66,5 +58,6 @@ public abstract class AppCounter extends AsyncTask { } protected abstract void onCountComplete(int num); + protected abstract List getUsersToCount(); protected abstract boolean includeInCount(ApplicationInfo info); } diff --git a/src/com/android/settings/applications/InstalledAppCounter.java b/src/com/android/settings/applications/InstalledAppCounter.java new file mode 100644 index 00000000000..9faef7a22f9 --- /dev/null +++ b/src/com/android/settings/applications/InstalledAppCounter.java @@ -0,0 +1,52 @@ +/* + * 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.settings.applications; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; +import android.content.pm.PackageManager; +import android.os.UserHandle; + +import java.util.List; + +public abstract class InstalledAppCounter extends AppCounter { + + public InstalledAppCounter(Context context, PackageManagerWrapper packageManager) { + super(context, packageManager); + } + + @Override + protected boolean includeInCount(ApplicationInfo info) { + if ((info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { + return true; + } + if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { + return true; + } + Intent launchIntent = new Intent(Intent.ACTION_MAIN, null) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setPackage(info.packageName); + int userId = UserHandle.getUserId(info.uid); + List intents = mPm.queryIntentActivitiesAsUser( + launchIntent, + PackageManager.GET_DISABLED_COMPONENTS + | PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, + userId); + return intents != null && intents.size() != 0; + } +} diff --git a/src/com/android/settings/applications/ManageApplications.java b/src/com/android/settings/applications/ManageApplications.java index b6e3b5d41ce..0ec9f1b37bc 100644 --- a/src/com/android/settings/applications/ManageApplications.java +++ b/src/com/android/settings/applications/ManageApplications.java @@ -22,7 +22,7 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; +import android.content.pm.UserInfo; import android.icu.text.AlphabeticIndex; import android.os.Bundle; import android.os.Environment; @@ -1259,7 +1259,8 @@ public class ManageApplications extends InstrumentedFragment @Override public void setListening(boolean listening) { if (listening) { - new AppCounter(mContext) { + new InstalledAppCounter(mContext, + new PackageManagerWrapperImpl(mContext.getPackageManager())) { @Override protected void onCountComplete(int num) { mLoader.setSummary(SummaryProvider.this, @@ -1267,23 +1268,8 @@ public class ManageApplications extends InstrumentedFragment } @Override - protected boolean includeInCount(ApplicationInfo info) { - if ((info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { - return true; - } else if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { - return true; - } - Intent launchIntent = new Intent(Intent.ACTION_MAIN, null) - .addCategory(Intent.CATEGORY_LAUNCHER) - .setPackage(info.packageName); - int userId = UserHandle.getUserId(info.uid); - List intents = mPm.queryIntentActivitiesAsUser( - launchIntent, - PackageManager.GET_DISABLED_COMPONENTS - | PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, - userId); - return intents != null && intents.size() != 0; + protected List getUsersToCount() { + return mUm.getProfiles(UserHandle.myUserId()); } }.execute(); } diff --git a/src/com/android/settings/applications/NotificationApps.java b/src/com/android/settings/applications/NotificationApps.java index 0d88dc46e17..7aaa36e1ac0 100644 --- a/src/com/android/settings/applications/NotificationApps.java +++ b/src/com/android/settings/applications/NotificationApps.java @@ -17,10 +17,17 @@ package com.android.settings.applications; import android.app.Activity; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.os.UserHandle; +import android.os.UserManager; + import com.android.settings.R; import com.android.settings.dashboard.SummaryLoader; import com.android.settings.notification.NotificationBackend; +import java.util.List; + /** * Extension of ManageApplications with no changes other than having its own * SummaryProvider. @@ -42,12 +49,18 @@ public class NotificationApps extends ManageApplications { @Override public void setListening(boolean listening) { if (listening) { - new AppCounter(mContext) { + new AppCounter(mContext, + new PackageManagerWrapperImpl(mContext.getPackageManager())) { @Override protected void onCountComplete(int num) { updateSummary(num); } + @Override + protected List getUsersToCount() { + return mUm.getProfiles(UserHandle.myUserId()); + } + @Override protected boolean includeInCount(ApplicationInfo info) { return mNotificationBackend.getNotificationsBanned(info.packageName, diff --git a/src/com/android/settings/applications/PackageManagerWrapper.java b/src/com/android/settings/applications/PackageManagerWrapper.java new file mode 100644 index 00000000000..ea019f43aee --- /dev/null +++ b/src/com/android/settings/applications/PackageManagerWrapper.java @@ -0,0 +1,45 @@ +/* + * 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.settings.applications; + +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; + +import java.util.List; + +/** + * This interface replicates a subset of the android.content.pm.PackageManager (PM). The interface + * exists so that we can use a thin wrapper around the PM in production code and a mock in tests. + * We cannot directly mock or shadow the PM, because some of the methods we rely on are newer than + * the API version supported by Robolectric. + */ +public interface PackageManagerWrapper { + /** + * Calls {@code PackageManager.getInstalledApplicationsAsUser()}. + * + * @see android.content.pm.PackageManager.PackageManager#getInstalledApplicationsAsUser + */ + List getInstalledApplicationsAsUser(int flags, int userId); + + /** + * Calls {@code PackageManager.queryIntentActivitiesAsUser()}. + * + * @see android.content.pm.PackageManager.PackageManager#queryIntentActivitiesAsUser + */ + List queryIntentActivitiesAsUser(Intent intent, int flags, int userId); +} diff --git a/src/com/android/settings/applications/PackageManagerWrapperImpl.java b/src/com/android/settings/applications/PackageManagerWrapperImpl.java new file mode 100644 index 00000000000..e41983be049 --- /dev/null +++ b/src/com/android/settings/applications/PackageManagerWrapperImpl.java @@ -0,0 +1,42 @@ +/* + * 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.settings.applications; + +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; + +import java.util.List; + +public class PackageManagerWrapperImpl implements PackageManagerWrapper { + private final PackageManager mPm; + + public PackageManagerWrapperImpl(PackageManager pm) { + mPm = pm; + } + + @Override + public List getInstalledApplicationsAsUser(int flags, int userId) { + return mPm.getInstalledApplicationsAsUser(flags, userId); + } + + @Override + public List queryIntentActivitiesAsUser(Intent intent, int flags, int userId) { + return mPm.queryIntentActivitiesAsUser(intent, flags, userId); + } +} diff --git a/tests/robotests/src/com/android/settings/applications/InstalledAppCounterTest.java b/tests/robotests/src/com/android/settings/applications/InstalledAppCounterTest.java new file mode 100644 index 00000000000..0f0ada522bc --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/InstalledAppCounterTest.java @@ -0,0 +1,182 @@ +/* + * 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.settings.applications; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.UserInfo; +import android.os.UserManager; + +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.testutils.shadow.ShadowUserManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowApplication; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static com.android.settings.testutils.ApplicationTestUtils.buildInfo; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link InstalledAppCounter}. + */ +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION, + shadows = {ShadowUserManager.class}) +public final class InstalledAppCounterTest { + + private final int MAIN_USER_ID = 0; + private final int MANAGED_PROFILE_ID = 10; + + @Mock private UserManager mUserManager; + @Mock private Context mContext; + @Mock private PackageManagerWrapper mPackageManager; + private List mUsersToCount; + + private int mInstalledAppCount = -1; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); + } + + private void expectQueryIntentActivities(int userId, String packageName, boolean launchable) { + when(mPackageManager.queryIntentActivitiesAsUser( + argThat(new IsLaunchIntentFor(packageName)), + eq(PackageManager.GET_DISABLED_COMPONENTS | PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE), + eq(userId))).thenReturn(launchable ? Arrays.asList(new ResolveInfo()) + : new ArrayList()); + } + + @Test + public void testCountInstalledAppsAcrossAllUsers() { + // There are two users. + mUsersToCount = Arrays.asList( + new UserInfo(MAIN_USER_ID, "main", UserInfo.FLAG_ADMIN), + new UserInfo(MANAGED_PROFILE_ID, "managed profile", 0)); + + // The first user has four apps installed: + // * app1 is an updated system app. It should be counted. + // * app2 is a user-installed app. It should be counted. + // * app3 is a system app that provides a launcher icon. It should be counted. + // * app4 is a system app that provides no launcher icon. It should not be counted. + when(mPackageManager.getInstalledApplicationsAsUser(PackageManager.GET_DISABLED_COMPONENTS + | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS + | PackageManager.GET_UNINSTALLED_PACKAGES, + MAIN_USER_ID)).thenReturn(Arrays.asList( + buildInfo(MAIN_USER_ID, "app1", ApplicationInfo.FLAG_UPDATED_SYSTEM_APP), + buildInfo(MAIN_USER_ID, "app2", 0 /* flags */), + buildInfo(MAIN_USER_ID, "app3", ApplicationInfo.FLAG_SYSTEM), + buildInfo(MAIN_USER_ID, "app4", ApplicationInfo.FLAG_SYSTEM))); + // For system apps, InstalledAppCounter checks whether they handle the default launcher + // intent to decide whether to include them in the count of installed apps or not. + expectQueryIntentActivities(MAIN_USER_ID, "app3", true /* launchable */); + expectQueryIntentActivities(MAIN_USER_ID, "app4", false /* launchable */); + + // The second user has four apps installed: + // * app5 is a user-installed app. It should be counted. + // * app6 is a system app that provides a launcher icon. It should be counted. + when(mPackageManager.getInstalledApplicationsAsUser(PackageManager.GET_DISABLED_COMPONENTS + | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, + MANAGED_PROFILE_ID)).thenReturn(Arrays.asList( + buildInfo(MANAGED_PROFILE_ID, "app5", 0 /* flags */), + buildInfo(MANAGED_PROFILE_ID, "app6", ApplicationInfo.FLAG_SYSTEM))); + expectQueryIntentActivities(MANAGED_PROFILE_ID, "app6", true /* launchable */); + + // Count the number of apps installed. Wait for the background task to finish. + (new InstalledAppCounterTestable()).execute(); + ShadowApplication.runBackgroundTasks(); + + assertThat(mInstalledAppCount).isEqualTo(5); + + // Verify that installed packages were retrieved for the users returned by + // InstalledAppCounterTestable.getUsersToCount() only. + verify(mPackageManager).getInstalledApplicationsAsUser(anyInt(), eq(MAIN_USER_ID)); + verify(mPackageManager).getInstalledApplicationsAsUser(anyInt(), + eq(MANAGED_PROFILE_ID)); + verify(mPackageManager, atLeast(0)).queryIntentActivitiesAsUser(anyObject(), anyInt(), + anyInt()); + verifyNoMoreInteractions(mPackageManager); + } + + private class InstalledAppCounterTestable extends InstalledAppCounter { + public InstalledAppCounterTestable() { + super(mContext, mPackageManager); + } + + @Override + protected void onCountComplete(int num) { + mInstalledAppCount = num; + } + + @Override + protected List getUsersToCount() { + return mUsersToCount; + } + } + + private class IsLaunchIntentFor extends ArgumentMatcher { + private final String mPackageName; + + IsLaunchIntentFor(String packageName) { + mPackageName = packageName; + } + + @Override + public boolean matches(Object i) { + final Intent intent = (Intent) i; + if (intent == null) { + return false; + } + if (intent.getAction() != Intent.ACTION_MAIN) { + return false; + } + final Set categories = intent.getCategories(); + if (categories == null || categories.size() != 1 || + !categories.contains(Intent.CATEGORY_LAUNCHER)) { + return false; + } + if (!mPackageName.equals(intent.getPackage())) { + return false; + } + return true; + } + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/ApplicationTestUtils.java b/tests/robotests/src/com/android/settings/testutils/ApplicationTestUtils.java new file mode 100644 index 00000000000..87899282216 --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/ApplicationTestUtils.java @@ -0,0 +1,43 @@ +/* + * 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.settings.testutils; + +import android.content.pm.ApplicationInfo; +import android.os.UserHandle; + +/** + * Helper for mocking installed applications. + */ +public class ApplicationTestUtils { + /** + * Create and populate an {@link android.content.pm.ApplicationInfo} object that describes an + * installed app. + * + * @param userId The user id that this app is installed for. Typical values are 0 for the + * system user and 10, 11, 12... for secondary users. + * @param packageName The app's package name. + * @param flags Flags describing the app. See {@link android.content.pm.ApplicationInfo#flags} + * for possible values. + * + * @see android.content.pm.ApplicationInfo + */ + public static ApplicationInfo buildInfo(int userId, String packageName, int flags) { + final ApplicationInfo info = new ApplicationInfo(); + info.uid = UserHandle.getUid(userId, 1); + info.packageName = packageName; + info.flags = flags; + return info; + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java new file mode 100644 index 00000000000..c67ad36f93c --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUserManager.java @@ -0,0 +1,35 @@ +/* + * 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.settings.testutils.shadow; + +import android.content.Context; +import android.os.UserManager; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +/** + * This class provides the API 24 implementation of UserManager.get(Context). + */ +@Implements(UserManager.class) +public class ShadowUserManager { + + @Implementation + public static UserManager get(Context context) { + return (UserManager) context.getSystemService(Context.USER_SERVICE); + } +}