diff options
3 files changed, 329 insertions, 54 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java index d74c5b01307b..cb5c1257a831 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java @@ -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; @@ -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; @@ -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; @@ -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 @@ -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. @@ -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(); @@ -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(); } /** @@ -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(); @@ -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 @@ -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); + mContext.startActivityAsUser(launchIntent, optsBundle, appUser); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java index b8764cf40efb..c4c31fd6bf80 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java @@ -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; @@ -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. */ @@ -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); @@ -214,8 +277,15 @@ class NavigationBarAppsModel { // Couldn't find the saved state. Just skip this item. continue; } - mApps.add(new AppInfo(componentName, userSerialNumber)); + 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. */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java index 62213ab51703..4d0e28bb6d98 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java @@ -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; @@ -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; @@ -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); @@ -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) { + @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."); + } - mModel = new NavigationBarAppsModel(context); + // 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. */ @@ -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); } @@ -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. */ @@ -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. */ |