Merge "Checking for unlauncheable apps."

This commit is contained in:
Vadim Tryshev
2015-08-19 22:38:40 +00:00
committed by Android (Google) Code Review
3 changed files with 329 additions and 54 deletions

View File

@@ -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);
}
}

View File

@@ -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. */

View File

@@ -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);
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. */
@@ -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. */