Loading packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java +84 −52 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ import android.animation.LayoutTransition; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.AppGlobals; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipDescription; Loading @@ -29,13 +28,10 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.ActivityInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Rect; import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.util.AttributeSet; Loading @@ -49,6 +45,7 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Toast; import com.android.internal.content.PackageMonitor; import com.android.systemui.R; import java.util.List; Loading @@ -75,6 +72,8 @@ class NavigationBarApps extends LinearLayout { private final PackageManager mPackageManager; private final UserManager mUserManager; private final LayoutInflater mLayoutInflater; private final AppPackageMonitor mAppPackageMonitor; // This view has two roles: // 1) If the drag started outside the pinned apps list, it is a placeholder icon with a null Loading Loading @@ -106,6 +105,7 @@ class NavigationBarApps extends LinearLayout { mPackageManager = context.getPackageManager(); mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE); mLayoutInflater = LayoutInflater.from(context); mAppPackageMonitor = new AppPackageMonitor(); // Dragging an icon removes and adds back the dragged icon. Use the layout transitions to // trigger animation. By default all transitions animate, so turn off the unneeded ones. Loading @@ -121,6 +121,76 @@ class NavigationBarApps extends LinearLayout { setLayoutTransition(transition); } // Monitor that catches events like "app uninstalled". private class AppPackageMonitor extends PackageMonitor { @Override public void onPackageRemoved(String packageName, int uid) { postRemoveIfUnlauncheable(packageName, new UserHandle(getChangingUserId())); super.onPackageRemoved(packageName, uid); } @Override public void onPackageModified(String packageName) { postRemoveIfUnlauncheable(packageName, new UserHandle(getChangingUserId())); super.onPackageModified(packageName); } @Override public void onPackagesAvailable(String[] packages) { if (isReplacing()) { UserHandle user = new UserHandle(getChangingUserId()); for (String packageName : packages) { postRemoveIfUnlauncheable(packageName, user); } } super.onPackagesAvailable(packages); } @Override public void onPackagesUnavailable(String[] packages) { if (!isReplacing()) { UserHandle user = new UserHandle(getChangingUserId()); for (String packageName : packages) { postRemoveIfUnlauncheable(packageName, user); } } super.onPackagesUnavailable(packages); } } private void postRemoveIfUnlauncheable(final String packageName, final UserHandle user) { // This method doesn't necessarily get called in the main thread. Redirect the call into // the main thread. post(new Runnable() { @Override public void run() { if (!isAttachedToWindow()) return; removeIfUnlauncheable(packageName, user); } }); } private void removeIfUnlauncheable(String packageName, UserHandle user) { long appUserSerialNumber = mUserManager.getSerialNumberForUser(user); // Remove icons for all apps that match a package that perhaps became unlauncheable. for(int i = sAppsModel.getAppCount() - 1; i >= 0; --i) { AppInfo appInfo = sAppsModel.getApp(i); if (appInfo.getUserSerialNumber() != appUserSerialNumber) continue; ComponentName appComponentName = appInfo.getComponentName(); if (!appComponentName.getPackageName().equals(packageName)) continue; if (sAppsModel.buildAppLaunchIntent(appComponentName, user) != null) continue; removeViewAt(i); sAppsModel.removeApp(i); sAppsModel.savePrefs(); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); Loading @@ -145,12 +215,15 @@ class NavigationBarApps extends LinearLayout { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_SWITCHED); mContext.registerReceiver(mBroadcastReceiver, filter); mAppPackageMonitor.register(mContext, null, UserHandle.ALL, true); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mContext.unregisterReceiver(mBroadcastReceiver); mAppPackageMonitor.unregister(); } /** Loading Loading @@ -470,7 +543,6 @@ class NavigationBarApps extends LinearLayout { ComponentName component = appInfo.getComponentName(); long appUserSerialNumber = appInfo.getUserSerialNumber(); UserHandle appUser = mUserManager.getUserForSerialNumber(appUserSerialNumber); if (appUser == null) { Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); Loading @@ -478,7 +550,12 @@ class NavigationBarApps extends LinearLayout { " because its user doesn't exist."); return; } int appUserId = appUser.getIdentifier(); Intent launchIntent = sAppsModel.buildAppLaunchIntent(component, appUser); if (launchIntent == null) { Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); return; } // Play a scale-up animation while launching the activity. // TODO: Consider playing a different animation, or no animation, if the activity is Loading @@ -489,54 +566,9 @@ class NavigationBarApps extends LinearLayout { ActivityOptions opts = ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight()); Bundle optsBundle = opts.toBundle(); // Launch the activity. This code is based on LauncherAppsService.startActivityAsUser code. Intent launchIntent = new Intent(Intent.ACTION_MAIN); launchIntent.addCategory(Intent.CATEGORY_LAUNCHER); launchIntent.setSourceBounds(sourceBounds); launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); launchIntent.setPackage(component.getPackageName()); IPackageManager pm = AppGlobals.getPackageManager(); try { ActivityInfo info = pm.getActivityInfo(component, 0, appUserId); if (info == null) { Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); Log.e(TAG, "Can't start activity " + component + " because it's not installed."); return; } if (!info.exported) { Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); Log.e(TAG, "Can't start activity " + component + " because it doesn't have 'exported' attribute."); return; } } catch (RemoteException e) { Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); Log.e(TAG, "Failed to get activity info for " + component, e); return; } // Check that the component actually has Intent.CATEGORY_LAUCNCHER // as calling startActivityAsUser ignores the category and just // resolves based on the component if present. List<ResolveInfo> apps = getContext().getPackageManager().queryIntentActivitiesAsUser(launchIntent, 0 /* flags */, appUserId); final int size = apps.size(); for (int i = 0; i < size; ++i) { ActivityInfo activityInfo = apps.get(i).activityInfo; if (activityInfo.packageName.equals(component.getPackageName()) && activityInfo.name.equals(component.getClassName())) { // Found an activity with category launcher that matches // this component so ok to launch. launchIntent.setComponent(component); mContext.startActivityAsUser(launchIntent, optsBundle, appUser); return; } } Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); Log.e(TAG, "Attempt to launch activity without category Intent.CATEGORY_LAUNCHER " + component); } } Loading packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java +71 −1 Original line number Diff line number Diff line Loading @@ -16,16 +16,23 @@ package com.android.systemui.statusbar.phone; import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.IPackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.HashSet; import java.util.List; Loading Loading @@ -94,6 +101,58 @@ class NavigationBarAppsModel { } } @VisibleForTesting protected IPackageManager getPackageManager() { return AppGlobals.getPackageManager(); } // Returns a launch intent for a given component, or null if the component is unlauncheable. public Intent buildAppLaunchIntent(ComponentName component, UserHandle appUser) { int appUserId = appUser.getIdentifier(); // This code is based on LauncherAppsService.startActivityAsUser code. Intent launchIntent = new Intent(Intent.ACTION_MAIN); launchIntent.addCategory(Intent.CATEGORY_LAUNCHER); launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); launchIntent.setPackage(component.getPackageName()); try { ActivityInfo info = getPackageManager().getActivityInfo(component, 0, appUserId); if (info == null) { Log.e(TAG, "Activity " + component + " is not installed."); return null; } if (!info.exported) { Log.e(TAG, "Activity " + component + " doesn't have 'exported' attribute."); return null; } } catch (RemoteException e) { Log.e(TAG, "Failed to get activity info for " + component, e); return null; } // Check that the component actually has Intent.CATEGORY_LAUNCHER // as calling startActivityAsUser ignores the category and just // resolves based on the component if present. List<ResolveInfo> apps = mContext.getPackageManager().queryIntentActivitiesAsUser(launchIntent, 0 /* flags */, appUserId); final int size = apps.size(); for (int i = 0; i < size; ++i) { ActivityInfo activityInfo = apps.get(i).activityInfo; if (activityInfo.packageName.equals(component.getPackageName()) && activityInfo.name.equals(component.getClassName())) { // Found an activity with category launcher that matches // this component so ok to launch. launchIntent.setComponent(component); return launchIntent; } } Log.e(TAG, "Activity doesn't have category Intent.CATEGORY_LAUNCHER " + component); return null; } /** * Reinitializes the model for a new user. */ Loading Loading @@ -199,6 +258,10 @@ class NavigationBarAppsModel { /** Loads the list of apps from SharedPreferences. */ private void loadAppsFromPrefs() { UserManager mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); boolean hadUnlauncheableApps = false; int appCount = mPrefs.getInt(userPrefixed(APP_COUNT_PREF), -1); for (int i = 0; i < appCount; i++) { String prefValue = mPrefs.getString(prefNameForApp(i), null); Loading @@ -214,10 +277,17 @@ class NavigationBarAppsModel { // Couldn't find the saved state. Just skip this item. continue; } UserHandle appUser = mUserManager.getUserForSerialNumber(userSerialNumber); if (appUser != null && buildAppLaunchIntent(componentName, appUser) != null) { mApps.add(new AppInfo(componentName, userSerialNumber)); } else { hadUnlauncheableApps = true; } } if (hadUnlauncheableApps) savePrefs(); } /** Adds the first few apps from the owner profile. Used for demo purposes. */ private void addDefaultApps() { // Get a list of all app activities. Loading packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java +174 −1 Original line number Diff line number Diff line Loading @@ -16,21 +16,26 @@ package com.android.systemui.statusbar.phone; import org.mockito.InOrder; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.test.AndroidTestCase; Loading @@ -45,6 +50,7 @@ import java.util.Map; /** Tests for the data model for the navigation bar app icons. */ public class NavigationBarAppsModelTest extends AndroidTestCase { private PackageManager mMockPackageManager; private IPackageManager mMockIPackageManager; private SharedPreferences mMockPrefs; private SharedPreferences.Editor mMockEdit; private UserManager mMockUserManager; Loading @@ -61,6 +67,7 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { final Context context = mock(Context.class); mMockPackageManager = mock(PackageManager.class); mMockIPackageManager = mock(IPackageManager.class); mMockPrefs = mock(SharedPreferences.class); mMockEdit = mock(SharedPreferences.Editor.class); mMockUserManager = mock(UserManager.class); Loading @@ -78,8 +85,71 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { when(mMockPrefs.edit()).thenReturn(mMockEdit); when(mMockUserManager.getSerialNumberForUser(new UserHandle(2))).thenReturn(22L); when(mMockUserManager.getUserForSerialNumber(45L)).thenReturn(new UserHandle(4)); when(mMockUserManager.getUserForSerialNumber(239L)).thenReturn(new UserHandle(5)); mModel = new NavigationBarAppsModel(context); mModel = new NavigationBarAppsModel(context) { @Override protected IPackageManager getPackageManager() { return mMockIPackageManager; } }; } /** Tests buildAppLaunchIntent(). */ public void testBuildAppLaunchIntent() { ActivityInfo mockNonExportedActivityInfo = new ActivityInfo(); mockNonExportedActivityInfo.exported = false; ActivityInfo mockExportedActivityInfo = new ActivityInfo(); mockExportedActivityInfo.exported = true; try { when(mMockIPackageManager.getActivityInfo( new ComponentName("package1", "class1"), 0, 4)). thenReturn(mockNonExportedActivityInfo); when(mMockIPackageManager.getActivityInfo( new ComponentName("package2", "class2"), 0, 5)). thenThrow(new RemoteException()); when(mMockIPackageManager.getActivityInfo( new ComponentName("package3", "class3"), 0, 6)). thenReturn(mockExportedActivityInfo); when(mMockIPackageManager.getActivityInfo( new ComponentName("package4", "class4"), 0, 7)). thenReturn(mockExportedActivityInfo); } catch (RemoteException e) { fail("RemoteException can't happen in the test, but it happened."); } // Assume some installed activities. ActivityInfo ai0 = new ActivityInfo(); ai0.packageName = "package0"; ai0.name = "class0"; ActivityInfo ai1 = new ActivityInfo(); ai1.packageName = "package4"; ai1.name = "class4"; ResolveInfo ri0 = new ResolveInfo(); ri0.activityInfo = ai0; ResolveInfo ri1 = new ResolveInfo(); ri1.activityInfo = ai1; when(mMockPackageManager .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class))) .thenReturn(Arrays.asList(ri0, ri1)); // Unlauncheable (for various reasons) apps. assertEquals(null, mModel.buildAppLaunchIntent( new ComponentName("package0", "class0"), new UserHandle(3))); assertEquals(null, mModel.buildAppLaunchIntent( new ComponentName("package1", "class1"), new UserHandle(4))); assertEquals(null, mModel.buildAppLaunchIntent( new ComponentName("package2", "class2"), new UserHandle(5))); assertEquals(null, mModel.buildAppLaunchIntent( new ComponentName("package3", "class3"), new UserHandle(6))); // A launcheable app. Intent intent = mModel.buildAppLaunchIntent( new ComponentName("package4", "class4"), new UserHandle(7)); assertNotNull(intent); assertEquals(new ComponentName("package4", "class4"), intent.getComponent()); assertEquals("package4", intent.getPackage()); } /** Initializes the model from SharedPreferences for a few app activites. */ Loading @@ -93,6 +163,39 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { when(mMockPrefs.getString("22|app_2", null)).thenReturn("package2/class2"); when(mMockPrefs.getLong("22|app_user_2", -1)).thenReturn(239L); ActivityInfo mockActivityInfo = new ActivityInfo(); mockActivityInfo.exported = true; try { when(mMockIPackageManager.getActivityInfo( new ComponentName("package0", "class0"), 0, 5)).thenReturn(mockActivityInfo); when(mMockIPackageManager.getActivityInfo( new ComponentName("package1", "class1"), 0, 4)).thenReturn(mockActivityInfo); when(mMockIPackageManager.getActivityInfo( new ComponentName("package2", "class2"), 0, 5)).thenReturn(mockActivityInfo); } catch (RemoteException e) { fail("RemoteException can't happen in the test, but it happened."); } // Assume some installed activities. ActivityInfo ai0 = new ActivityInfo(); ai0.packageName = "package0"; ai0.name = "class0"; ActivityInfo ai1 = new ActivityInfo(); ai1.packageName = "package1"; ai1.name = "class1"; ActivityInfo ai2 = new ActivityInfo(); ai2.packageName = "package2"; ai2.name = "class2"; ResolveInfo ri0 = new ResolveInfo(); ri0.activityInfo = ai0; ResolveInfo ri1 = new ResolveInfo(); ri1.activityInfo = ai1; ResolveInfo ri2 = new ResolveInfo(); ri2.activityInfo = ai2; when(mMockPackageManager .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class))) .thenReturn(Arrays.asList(ri0, ri1, ri2)); mModel.setCurrentUser(2); } Loading Loading @@ -133,6 +236,15 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { assertEquals(22L, mModel.getApp(0).getUserSerialNumber()); assertEquals("package2/class2", mModel.getApp(1).getComponentName().flattenToString()); assertEquals(22L, mModel.getApp(1).getUserSerialNumber()); InOrder order = inOrder(mMockEdit); order.verify(mMockEdit).apply(); order.verify(mMockEdit).putInt("22|app_count", 2); order.verify(mMockEdit).putString("22|app_0", "package1/class1"); order.verify(mMockEdit).putLong("22|app_user_0", 22L); order.verify(mMockEdit).putString("22|app_1", "package2/class2"); order.verify(mMockEdit).putLong("22|app_user_1", 22L); order.verify(mMockEdit).apply(); verifyNoMoreInteractions(mMockEdit); } /** Tests initializing the model if one of the prefs is missing. */ Loading @@ -145,11 +257,72 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { // But assume one pref is missing. when(mMockPrefs.getString("22|app_1", null)).thenReturn(null); ActivityInfo mockActivityInfo = new ActivityInfo(); mockActivityInfo.exported = true; try { when(mMockIPackageManager.getActivityInfo( new ComponentName("package0", "class0"), 0, 5)).thenReturn(mockActivityInfo); } catch (RemoteException e) { fail("RemoteException can't happen in the test, but it happened."); } ActivityInfo ai0 = new ActivityInfo(); ai0.packageName = "package0"; ai0.name = "class0"; ResolveInfo ri0 = new ResolveInfo(); ri0.activityInfo = ai0; when(mMockPackageManager .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class))) .thenReturn(Arrays.asList(ri0)); // Initializing the model should load from prefs and skip the missing one. mModel.setCurrentUser(2); assertEquals(1, mModel.getAppCount()); assertEquals("package0/class0", mModel.getApp(0).getComponentName().flattenToString()); assertEquals(239L, mModel.getApp(0).getUserSerialNumber()); verifyNoMoreInteractions(mMockEdit); } /** Tests initializing the model if one of the apps is unlauncheable. */ public void testInitializeWithUnlauncheableApp() { // Assume two apps are nominally stored. when(mMockPrefs.getInt("22|app_count", -1)).thenReturn(2); when(mMockPrefs.getString("22|app_0", null)).thenReturn("package0/class0"); when(mMockPrefs.getLong("22|app_user_0", -1)).thenReturn(239L); when(mMockPrefs.getString("22|app_1", null)).thenReturn("package1/class1"); when(mMockPrefs.getLong("22|app_user_1", -1)).thenReturn(45L); ActivityInfo mockActivityInfo = new ActivityInfo(); mockActivityInfo.exported = true; try { when(mMockIPackageManager.getActivityInfo( new ComponentName("package0", "class0"), 0, 5)).thenReturn(mockActivityInfo); } catch (RemoteException e) { fail("RemoteException can't happen in the test, but it happened."); } ActivityInfo ai0 = new ActivityInfo(); ai0.packageName = "package0"; ai0.name = "class0"; ResolveInfo ri0 = new ResolveInfo(); ri0.activityInfo = ai0; when(mMockPackageManager .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class))) .thenReturn(Arrays.asList(ri0)); // Initializing the model should load from prefs and skip the unlauncheable one. mModel.setCurrentUser(2); assertEquals(1, mModel.getAppCount()); assertEquals("package0/class0", mModel.getApp(0).getComponentName().flattenToString()); assertEquals(239L, mModel.getApp(0).getUserSerialNumber()); // Once an unlauncheable app is detected, the model should save all apps excluding the // unlauncheable one. verify(mMockEdit).putInt("22|app_count", 1); verify(mMockEdit).putString("22|app_0", "package0/class0"); verify(mMockEdit).putLong("22|app_user_0", 239L); verify(mMockEdit).apply(); verifyNoMoreInteractions(mMockEdit); } /** Tests saving the model to SharedPreferences. */ Loading Loading
packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java +84 −52 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ import android.animation.LayoutTransition; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.AppGlobals; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipDescription; Loading @@ -29,13 +28,10 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.ActivityInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Rect; import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.util.AttributeSet; Loading @@ -49,6 +45,7 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Toast; import com.android.internal.content.PackageMonitor; import com.android.systemui.R; import java.util.List; Loading @@ -75,6 +72,8 @@ class NavigationBarApps extends LinearLayout { private final PackageManager mPackageManager; private final UserManager mUserManager; private final LayoutInflater mLayoutInflater; private final AppPackageMonitor mAppPackageMonitor; // This view has two roles: // 1) If the drag started outside the pinned apps list, it is a placeholder icon with a null Loading Loading @@ -106,6 +105,7 @@ class NavigationBarApps extends LinearLayout { mPackageManager = context.getPackageManager(); mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE); mLayoutInflater = LayoutInflater.from(context); mAppPackageMonitor = new AppPackageMonitor(); // Dragging an icon removes and adds back the dragged icon. Use the layout transitions to // trigger animation. By default all transitions animate, so turn off the unneeded ones. Loading @@ -121,6 +121,76 @@ class NavigationBarApps extends LinearLayout { setLayoutTransition(transition); } // Monitor that catches events like "app uninstalled". private class AppPackageMonitor extends PackageMonitor { @Override public void onPackageRemoved(String packageName, int uid) { postRemoveIfUnlauncheable(packageName, new UserHandle(getChangingUserId())); super.onPackageRemoved(packageName, uid); } @Override public void onPackageModified(String packageName) { postRemoveIfUnlauncheable(packageName, new UserHandle(getChangingUserId())); super.onPackageModified(packageName); } @Override public void onPackagesAvailable(String[] packages) { if (isReplacing()) { UserHandle user = new UserHandle(getChangingUserId()); for (String packageName : packages) { postRemoveIfUnlauncheable(packageName, user); } } super.onPackagesAvailable(packages); } @Override public void onPackagesUnavailable(String[] packages) { if (!isReplacing()) { UserHandle user = new UserHandle(getChangingUserId()); for (String packageName : packages) { postRemoveIfUnlauncheable(packageName, user); } } super.onPackagesUnavailable(packages); } } private void postRemoveIfUnlauncheable(final String packageName, final UserHandle user) { // This method doesn't necessarily get called in the main thread. Redirect the call into // the main thread. post(new Runnable() { @Override public void run() { if (!isAttachedToWindow()) return; removeIfUnlauncheable(packageName, user); } }); } private void removeIfUnlauncheable(String packageName, UserHandle user) { long appUserSerialNumber = mUserManager.getSerialNumberForUser(user); // Remove icons for all apps that match a package that perhaps became unlauncheable. for(int i = sAppsModel.getAppCount() - 1; i >= 0; --i) { AppInfo appInfo = sAppsModel.getApp(i); if (appInfo.getUserSerialNumber() != appUserSerialNumber) continue; ComponentName appComponentName = appInfo.getComponentName(); if (!appComponentName.getPackageName().equals(packageName)) continue; if (sAppsModel.buildAppLaunchIntent(appComponentName, user) != null) continue; removeViewAt(i); sAppsModel.removeApp(i); sAppsModel.savePrefs(); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); Loading @@ -145,12 +215,15 @@ class NavigationBarApps extends LinearLayout { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_SWITCHED); mContext.registerReceiver(mBroadcastReceiver, filter); mAppPackageMonitor.register(mContext, null, UserHandle.ALL, true); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mContext.unregisterReceiver(mBroadcastReceiver); mAppPackageMonitor.unregister(); } /** Loading Loading @@ -470,7 +543,6 @@ class NavigationBarApps extends LinearLayout { ComponentName component = appInfo.getComponentName(); long appUserSerialNumber = appInfo.getUserSerialNumber(); UserHandle appUser = mUserManager.getUserForSerialNumber(appUserSerialNumber); if (appUser == null) { Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); Loading @@ -478,7 +550,12 @@ class NavigationBarApps extends LinearLayout { " because its user doesn't exist."); return; } int appUserId = appUser.getIdentifier(); Intent launchIntent = sAppsModel.buildAppLaunchIntent(component, appUser); if (launchIntent == null) { Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); return; } // Play a scale-up animation while launching the activity. // TODO: Consider playing a different animation, or no animation, if the activity is Loading @@ -489,54 +566,9 @@ class NavigationBarApps extends LinearLayout { ActivityOptions opts = ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight()); Bundle optsBundle = opts.toBundle(); // Launch the activity. This code is based on LauncherAppsService.startActivityAsUser code. Intent launchIntent = new Intent(Intent.ACTION_MAIN); launchIntent.addCategory(Intent.CATEGORY_LAUNCHER); launchIntent.setSourceBounds(sourceBounds); launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); launchIntent.setPackage(component.getPackageName()); IPackageManager pm = AppGlobals.getPackageManager(); try { ActivityInfo info = pm.getActivityInfo(component, 0, appUserId); if (info == null) { Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); Log.e(TAG, "Can't start activity " + component + " because it's not installed."); return; } if (!info.exported) { Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); Log.e(TAG, "Can't start activity " + component + " because it doesn't have 'exported' attribute."); return; } } catch (RemoteException e) { Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); Log.e(TAG, "Failed to get activity info for " + component, e); return; } // Check that the component actually has Intent.CATEGORY_LAUCNCHER // as calling startActivityAsUser ignores the category and just // resolves based on the component if present. List<ResolveInfo> apps = getContext().getPackageManager().queryIntentActivitiesAsUser(launchIntent, 0 /* flags */, appUserId); final int size = apps.size(); for (int i = 0; i < size; ++i) { ActivityInfo activityInfo = apps.get(i).activityInfo; if (activityInfo.packageName.equals(component.getPackageName()) && activityInfo.name.equals(component.getClassName())) { // Found an activity with category launcher that matches // this component so ok to launch. launchIntent.setComponent(component); mContext.startActivityAsUser(launchIntent, optsBundle, appUser); return; } } Toast.makeText(getContext(), R.string.activity_not_found, Toast.LENGTH_SHORT).show(); Log.e(TAG, "Attempt to launch activity without category Intent.CATEGORY_LAUNCHER " + component); } } Loading
packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java +71 −1 Original line number Diff line number Diff line Loading @@ -16,16 +16,23 @@ package com.android.systemui.statusbar.phone; import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.IPackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.HashSet; import java.util.List; Loading Loading @@ -94,6 +101,58 @@ class NavigationBarAppsModel { } } @VisibleForTesting protected IPackageManager getPackageManager() { return AppGlobals.getPackageManager(); } // Returns a launch intent for a given component, or null if the component is unlauncheable. public Intent buildAppLaunchIntent(ComponentName component, UserHandle appUser) { int appUserId = appUser.getIdentifier(); // This code is based on LauncherAppsService.startActivityAsUser code. Intent launchIntent = new Intent(Intent.ACTION_MAIN); launchIntent.addCategory(Intent.CATEGORY_LAUNCHER); launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); launchIntent.setPackage(component.getPackageName()); try { ActivityInfo info = getPackageManager().getActivityInfo(component, 0, appUserId); if (info == null) { Log.e(TAG, "Activity " + component + " is not installed."); return null; } if (!info.exported) { Log.e(TAG, "Activity " + component + " doesn't have 'exported' attribute."); return null; } } catch (RemoteException e) { Log.e(TAG, "Failed to get activity info for " + component, e); return null; } // Check that the component actually has Intent.CATEGORY_LAUNCHER // as calling startActivityAsUser ignores the category and just // resolves based on the component if present. List<ResolveInfo> apps = mContext.getPackageManager().queryIntentActivitiesAsUser(launchIntent, 0 /* flags */, appUserId); final int size = apps.size(); for (int i = 0; i < size; ++i) { ActivityInfo activityInfo = apps.get(i).activityInfo; if (activityInfo.packageName.equals(component.getPackageName()) && activityInfo.name.equals(component.getClassName())) { // Found an activity with category launcher that matches // this component so ok to launch. launchIntent.setComponent(component); return launchIntent; } } Log.e(TAG, "Activity doesn't have category Intent.CATEGORY_LAUNCHER " + component); return null; } /** * Reinitializes the model for a new user. */ Loading Loading @@ -199,6 +258,10 @@ class NavigationBarAppsModel { /** Loads the list of apps from SharedPreferences. */ private void loadAppsFromPrefs() { UserManager mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); boolean hadUnlauncheableApps = false; int appCount = mPrefs.getInt(userPrefixed(APP_COUNT_PREF), -1); for (int i = 0; i < appCount; i++) { String prefValue = mPrefs.getString(prefNameForApp(i), null); Loading @@ -214,10 +277,17 @@ class NavigationBarAppsModel { // Couldn't find the saved state. Just skip this item. continue; } UserHandle appUser = mUserManager.getUserForSerialNumber(userSerialNumber); if (appUser != null && buildAppLaunchIntent(componentName, appUser) != null) { mApps.add(new AppInfo(componentName, userSerialNumber)); } else { hadUnlauncheableApps = true; } } if (hadUnlauncheableApps) savePrefs(); } /** Adds the first few apps from the owner profile. Used for demo purposes. */ private void addDefaultApps() { // Get a list of all app activities. Loading
packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java +174 −1 Original line number Diff line number Diff line Loading @@ -16,21 +16,26 @@ package com.android.systemui.statusbar.phone; import org.mockito.InOrder; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.test.AndroidTestCase; Loading @@ -45,6 +50,7 @@ import java.util.Map; /** Tests for the data model for the navigation bar app icons. */ public class NavigationBarAppsModelTest extends AndroidTestCase { private PackageManager mMockPackageManager; private IPackageManager mMockIPackageManager; private SharedPreferences mMockPrefs; private SharedPreferences.Editor mMockEdit; private UserManager mMockUserManager; Loading @@ -61,6 +67,7 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { final Context context = mock(Context.class); mMockPackageManager = mock(PackageManager.class); mMockIPackageManager = mock(IPackageManager.class); mMockPrefs = mock(SharedPreferences.class); mMockEdit = mock(SharedPreferences.Editor.class); mMockUserManager = mock(UserManager.class); Loading @@ -78,8 +85,71 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { when(mMockPrefs.edit()).thenReturn(mMockEdit); when(mMockUserManager.getSerialNumberForUser(new UserHandle(2))).thenReturn(22L); when(mMockUserManager.getUserForSerialNumber(45L)).thenReturn(new UserHandle(4)); when(mMockUserManager.getUserForSerialNumber(239L)).thenReturn(new UserHandle(5)); mModel = new NavigationBarAppsModel(context); mModel = new NavigationBarAppsModel(context) { @Override protected IPackageManager getPackageManager() { return mMockIPackageManager; } }; } /** Tests buildAppLaunchIntent(). */ public void testBuildAppLaunchIntent() { ActivityInfo mockNonExportedActivityInfo = new ActivityInfo(); mockNonExportedActivityInfo.exported = false; ActivityInfo mockExportedActivityInfo = new ActivityInfo(); mockExportedActivityInfo.exported = true; try { when(mMockIPackageManager.getActivityInfo( new ComponentName("package1", "class1"), 0, 4)). thenReturn(mockNonExportedActivityInfo); when(mMockIPackageManager.getActivityInfo( new ComponentName("package2", "class2"), 0, 5)). thenThrow(new RemoteException()); when(mMockIPackageManager.getActivityInfo( new ComponentName("package3", "class3"), 0, 6)). thenReturn(mockExportedActivityInfo); when(mMockIPackageManager.getActivityInfo( new ComponentName("package4", "class4"), 0, 7)). thenReturn(mockExportedActivityInfo); } catch (RemoteException e) { fail("RemoteException can't happen in the test, but it happened."); } // Assume some installed activities. ActivityInfo ai0 = new ActivityInfo(); ai0.packageName = "package0"; ai0.name = "class0"; ActivityInfo ai1 = new ActivityInfo(); ai1.packageName = "package4"; ai1.name = "class4"; ResolveInfo ri0 = new ResolveInfo(); ri0.activityInfo = ai0; ResolveInfo ri1 = new ResolveInfo(); ri1.activityInfo = ai1; when(mMockPackageManager .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class))) .thenReturn(Arrays.asList(ri0, ri1)); // Unlauncheable (for various reasons) apps. assertEquals(null, mModel.buildAppLaunchIntent( new ComponentName("package0", "class0"), new UserHandle(3))); assertEquals(null, mModel.buildAppLaunchIntent( new ComponentName("package1", "class1"), new UserHandle(4))); assertEquals(null, mModel.buildAppLaunchIntent( new ComponentName("package2", "class2"), new UserHandle(5))); assertEquals(null, mModel.buildAppLaunchIntent( new ComponentName("package3", "class3"), new UserHandle(6))); // A launcheable app. Intent intent = mModel.buildAppLaunchIntent( new ComponentName("package4", "class4"), new UserHandle(7)); assertNotNull(intent); assertEquals(new ComponentName("package4", "class4"), intent.getComponent()); assertEquals("package4", intent.getPackage()); } /** Initializes the model from SharedPreferences for a few app activites. */ Loading @@ -93,6 +163,39 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { when(mMockPrefs.getString("22|app_2", null)).thenReturn("package2/class2"); when(mMockPrefs.getLong("22|app_user_2", -1)).thenReturn(239L); ActivityInfo mockActivityInfo = new ActivityInfo(); mockActivityInfo.exported = true; try { when(mMockIPackageManager.getActivityInfo( new ComponentName("package0", "class0"), 0, 5)).thenReturn(mockActivityInfo); when(mMockIPackageManager.getActivityInfo( new ComponentName("package1", "class1"), 0, 4)).thenReturn(mockActivityInfo); when(mMockIPackageManager.getActivityInfo( new ComponentName("package2", "class2"), 0, 5)).thenReturn(mockActivityInfo); } catch (RemoteException e) { fail("RemoteException can't happen in the test, but it happened."); } // Assume some installed activities. ActivityInfo ai0 = new ActivityInfo(); ai0.packageName = "package0"; ai0.name = "class0"; ActivityInfo ai1 = new ActivityInfo(); ai1.packageName = "package1"; ai1.name = "class1"; ActivityInfo ai2 = new ActivityInfo(); ai2.packageName = "package2"; ai2.name = "class2"; ResolveInfo ri0 = new ResolveInfo(); ri0.activityInfo = ai0; ResolveInfo ri1 = new ResolveInfo(); ri1.activityInfo = ai1; ResolveInfo ri2 = new ResolveInfo(); ri2.activityInfo = ai2; when(mMockPackageManager .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class))) .thenReturn(Arrays.asList(ri0, ri1, ri2)); mModel.setCurrentUser(2); } Loading Loading @@ -133,6 +236,15 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { assertEquals(22L, mModel.getApp(0).getUserSerialNumber()); assertEquals("package2/class2", mModel.getApp(1).getComponentName().flattenToString()); assertEquals(22L, mModel.getApp(1).getUserSerialNumber()); InOrder order = inOrder(mMockEdit); order.verify(mMockEdit).apply(); order.verify(mMockEdit).putInt("22|app_count", 2); order.verify(mMockEdit).putString("22|app_0", "package1/class1"); order.verify(mMockEdit).putLong("22|app_user_0", 22L); order.verify(mMockEdit).putString("22|app_1", "package2/class2"); order.verify(mMockEdit).putLong("22|app_user_1", 22L); order.verify(mMockEdit).apply(); verifyNoMoreInteractions(mMockEdit); } /** Tests initializing the model if one of the prefs is missing. */ Loading @@ -145,11 +257,72 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { // But assume one pref is missing. when(mMockPrefs.getString("22|app_1", null)).thenReturn(null); ActivityInfo mockActivityInfo = new ActivityInfo(); mockActivityInfo.exported = true; try { when(mMockIPackageManager.getActivityInfo( new ComponentName("package0", "class0"), 0, 5)).thenReturn(mockActivityInfo); } catch (RemoteException e) { fail("RemoteException can't happen in the test, but it happened."); } ActivityInfo ai0 = new ActivityInfo(); ai0.packageName = "package0"; ai0.name = "class0"; ResolveInfo ri0 = new ResolveInfo(); ri0.activityInfo = ai0; when(mMockPackageManager .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class))) .thenReturn(Arrays.asList(ri0)); // Initializing the model should load from prefs and skip the missing one. mModel.setCurrentUser(2); assertEquals(1, mModel.getAppCount()); assertEquals("package0/class0", mModel.getApp(0).getComponentName().flattenToString()); assertEquals(239L, mModel.getApp(0).getUserSerialNumber()); verifyNoMoreInteractions(mMockEdit); } /** Tests initializing the model if one of the apps is unlauncheable. */ public void testInitializeWithUnlauncheableApp() { // Assume two apps are nominally stored. when(mMockPrefs.getInt("22|app_count", -1)).thenReturn(2); when(mMockPrefs.getString("22|app_0", null)).thenReturn("package0/class0"); when(mMockPrefs.getLong("22|app_user_0", -1)).thenReturn(239L); when(mMockPrefs.getString("22|app_1", null)).thenReturn("package1/class1"); when(mMockPrefs.getLong("22|app_user_1", -1)).thenReturn(45L); ActivityInfo mockActivityInfo = new ActivityInfo(); mockActivityInfo.exported = true; try { when(mMockIPackageManager.getActivityInfo( new ComponentName("package0", "class0"), 0, 5)).thenReturn(mockActivityInfo); } catch (RemoteException e) { fail("RemoteException can't happen in the test, but it happened."); } ActivityInfo ai0 = new ActivityInfo(); ai0.packageName = "package0"; ai0.name = "class0"; ResolveInfo ri0 = new ResolveInfo(); ri0.activityInfo = ai0; when(mMockPackageManager .queryIntentActivitiesAsUser(any(Intent.class), eq(0), any(int.class))) .thenReturn(Arrays.asList(ri0)); // Initializing the model should load from prefs and skip the unlauncheable one. mModel.setCurrentUser(2); assertEquals(1, mModel.getAppCount()); assertEquals("package0/class0", mModel.getApp(0).getComponentName().flattenToString()); assertEquals(239L, mModel.getApp(0).getUserSerialNumber()); // Once an unlauncheable app is detected, the model should save all apps excluding the // unlauncheable one. verify(mMockEdit).putInt("22|app_count", 1); verify(mMockEdit).putString("22|app_0", "package0/class0"); verify(mMockEdit).putLong("22|app_user_0", 239L); verify(mMockEdit).apply(); verifyNoMoreInteractions(mMockEdit); } /** Tests saving the model to SharedPreferences. */ Loading