Merge "Adding multiuser support to the shelf."

This commit is contained in:
Vadim Tryshev
2015-08-06 19:56:24 +00:00
committed by Android (Google) Code Review
3 changed files with 299 additions and 102 deletions

View File

@@ -21,11 +21,13 @@ 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;
import android.content.ComponentName;
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;
@@ -71,6 +73,7 @@ class NavigationBarApps extends LinearLayout {
private static NavigationBarAppsModel sAppsModel;
private final PackageManager mPackageManager;
private final UserManager mUserManager;
private final LayoutInflater mLayoutInflater;
// This view has two roles:
@@ -82,13 +85,26 @@ class NavigationBarApps extends LinearLayout {
// When the user is not dragging this member is null.
private View mDragView;
private long mCurrentUserSerialNumber = -1;
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (Intent.ACTION_USER_SWITCHED.equals(action)) {
int currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
onUserSwitched(currentUserId);
}
}
};
public NavigationBarApps(Context context, AttributeSet attrs) {
super(context, attrs);
if (sAppsModel == null) {
sAppsModel = new NavigationBarAppsModel(context);
sAppsModel.initialize(); // Load the saved icons, if any.
}
mPackageManager = context.getPackageManager();
mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
mLayoutInflater = LayoutInflater.from(context);
// Dragging an icon removes and adds back the dragged icon. Use the layout transitions to
@@ -108,17 +124,32 @@ class NavigationBarApps extends LinearLayout {
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// When an icon is dragged out of the pinned area this view's width changes, which causes
// the parent container's layout to change and the divider and recents icons to shift left.
// Animate the parent's CHANGING transition.
ViewGroup parent = (ViewGroup) getParent();
LayoutTransition transition = new LayoutTransition();
transition.disableTransitionType(LayoutTransition.APPEARING);
transition.disableTransitionType(LayoutTransition.DISAPPEARING);
transition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
transition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
transition.enableTransitionType(LayoutTransition.CHANGING);
parent.setLayoutTransition(transition);
// When an icon is dragged out of the pinned area this view's width changes, which causes
// the parent container's layout to change and the divider and recents icons to shift left.
// Animate the parent's CHANGING transition.
ViewGroup parent = (ViewGroup) getParent();
LayoutTransition transition = new LayoutTransition();
transition.disableTransitionType(LayoutTransition.APPEARING);
transition.disableTransitionType(LayoutTransition.DISAPPEARING);
transition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
transition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
transition.enableTransitionType(LayoutTransition.CHANGING);
parent.setLayoutTransition(transition);
mCurrentUserSerialNumber = mUserManager.getSerialNumberForUser(
new UserHandle(ActivityManager.getCurrentUser()));
sAppsModel.setCurrentUser(mCurrentUserSerialNumber);
recreateAppButtons();
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_USER_SWITCHED);
mContext.registerReceiver(mBroadcastReceiver, filter);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mContext.unregisterReceiver(mBroadcastReceiver);
}
/**
@@ -433,14 +464,11 @@ class NavigationBarApps extends LinearLayout {
AppInfo appInfo = sAppsModel.getApp(indexOfChild(v));
ComponentName component = appInfo.getComponentName();
UserManager userManager =
(UserManager) getContext().getSystemService(Context.USER_SERVICE);
long appUserSerialNumber = appInfo.getUserSerialNumber();
UserHandle appUser = null;
if (appUserSerialNumber != AppInfo.USER_UNSPECIFIED) {
appUser = userManager.getUserForSerialNumber(appUserSerialNumber);
appUser = mUserManager.getUserForSerialNumber(appUserSerialNumber);
}
int appUserId;
@@ -510,4 +538,15 @@ class NavigationBarApps extends LinearLayout {
Log.e(TAG, "Attempt to launch activity without category Intent.CATEGORY_LAUNCHER " + component);
}
}
private void onUserSwitched(int currentUserId) {
final long newUserSerialNumber =
mUserManager.getSerialNumberForUser(new UserHandle(currentUserId));
if (newUserSerialNumber != mCurrentUserSerialNumber) {
mCurrentUserSerialNumber = newUserSerialNumber;
sAppsModel.setCurrentUser(newUserSerialNumber);
recreateAppButtons();
}
}
}

View File

@@ -19,16 +19,20 @@ package com.android.systemui.statusbar.phone;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.os.UserHandle;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.os.UserManager;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Data model and controller for app icons appearing in the navigation bar. The data is stored on
@@ -48,7 +52,7 @@ class NavigationBarAppsModel {
private final static String VERSION_PREF = "version";
// Current version number for preferences.
private final static int CURRENT_VERSION = 1;
private final static int CURRENT_VERSION = 2;
// Preference name for the number of app icons.
private final static String APP_COUNT_PREF = "app_count";
@@ -59,42 +63,99 @@ class NavigationBarAppsModel {
// User serial number prefix for each app's info. The actual pref has an integer appended to it.
private final static String APP_USER_PREFIX = "app_user_";
private final LauncherApps mLauncherApps;
// Character separating current user serial number from the user-specific part of a pref.
// Example "22|app_user_2" - when logged as user with serial 22, we'll use this pref for the
// user serial of the third app of the logged-in user.
private final static char USER_SEPARATOR = '|';
final Context mContext;
private final SharedPreferences mPrefs;
// Apps are represented as an ordered list of app infos.
private final List<AppInfo> mApps = new ArrayList<AppInfo>();
public NavigationBarAppsModel(Context context) {
mLauncherApps = (LauncherApps) context.getSystemService("launcherapps");
mPrefs = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
}
// Serial number of the current user.
private long mCurrentUserSerialNumber = AppInfo.USER_UNSPECIFIED;
@VisibleForTesting
NavigationBarAppsModel(LauncherApps launcherApps, SharedPreferences prefs) {
mLauncherApps = launcherApps;
mPrefs = prefs;
public NavigationBarAppsModel(Context context) {
mContext = context;
mPrefs = mContext.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
int version = mPrefs.getInt(VERSION_PREF, -1);
if (version != CURRENT_VERSION) {
// Since the data format changed, clean everything.
SharedPreferences.Editor edit = mPrefs.edit();
edit.clear();
edit.putInt(VERSION_PREF, CURRENT_VERSION);
edit.apply();
}
}
/**
* Initializes the model with a list of apps, either by loading it off disk or by supplying
* a default list.
* Reinitializes the model for a new user.
*/
public void initialize() {
if (mApps.size() > 0) {
Slog.e(TAG, "Model already initialized");
return;
}
public void setCurrentUser(long userSerialNumber) {
mCurrentUserSerialNumber = userSerialNumber;
// Check for an existing list of apps.
int version = mPrefs.getInt(VERSION_PREF, -1);
if (version == CURRENT_VERSION) {
mApps.clear();
int appCount = mPrefs.getInt(userPrefixed(APP_COUNT_PREF), -1);
if (appCount >= 0) {
loadAppsFromPrefs();
} else {
// We switched to this user for the first time ever. This is a good opportunity to clean
// prefs for users deleted in the past.
removePrefsForDeletedUsers();
addDefaultApps();
}
}
/**
* Removes prefs for users that don't exist on the device.
*/
private void removePrefsForDeletedUsers() {
UserManager userManager =
(UserManager) mContext.getSystemService(Context.USER_SERVICE);
// Build a set of string representations of serial numbers of the device users.
final List<UserInfo> users = userManager.getUsers();
final int userCount = users.size();
final Set<String> userSerials = new HashSet<String> ();
for (int i = 0; i < userCount; ++i) {
userSerials.add(Long.toString(users.get(i).serialNumber));
}
// Walk though all prefs and delete ones which user is not in the string set.
final Map<String, ?> allPrefs = mPrefs.getAll();
final SharedPreferences.Editor edit = mPrefs.edit();
for (Map.Entry<String, ?> pref : allPrefs.entrySet()) {
final String key = pref.getKey();
if (key.equals(VERSION_PREF)) continue;
final int userSeparatorPos = key.indexOf(USER_SEPARATOR);
if (userSeparatorPos < 0) {
// Removing anomalous pref with no user.
edit.remove(key);
continue;
}
final String prefUserSerial = key.substring(0, userSeparatorPos);
if (!userSerials.contains(prefUserSerial)) {
// Removes pref for a not existing user.
edit.remove(key);
continue;
}
}
edit.apply();
}
/** Returns the number of apps. */
public int getAppCount() {
return mApps.size();
@@ -123,11 +184,8 @@ class NavigationBarAppsModel {
/** Saves the current model to disk. */
public void savePrefs() {
SharedPreferences.Editor edit = mPrefs.edit();
// The user might have removed icons, so clear all the old prefs.
edit.clear();
edit.putInt(VERSION_PREF, CURRENT_VERSION);
int appCount = mApps.size();
edit.putInt(APP_COUNT_PREF, appCount);
edit.putInt(userPrefixed(APP_COUNT_PREF), appCount);
for (int i = 0; i < appCount; i++) {
final AppInfo appInfo = mApps.get(i);
String componentNameString = appInfo.getComponentName().flattenToString();
@@ -140,7 +198,7 @@ class NavigationBarAppsModel {
/** Loads the list of apps from SharedPreferences. */
private void loadAppsFromPrefs() {
int appCount = mPrefs.getInt(APP_COUNT_PREF, -1);
int appCount = mPrefs.getInt(userPrefixed(APP_COUNT_PREF), -1);
for (int i = 0; i < appCount; i++) {
String prefValue = mPrefs.getString(prefNameForApp(i), null);
if (prefValue == null) {
@@ -154,27 +212,48 @@ class NavigationBarAppsModel {
}
}
@VisibleForTesting
protected int getCurrentUser() {
return ActivityManager.getCurrentUser();
}
/** Adds the first few apps from the owner profile. Used for demo purposes. */
private void addDefaultApps() {
// Get a list of all app activities.
List<LauncherActivityInfo> apps =
mLauncherApps.getActivityList(
null /* packageName */, new UserHandle(ActivityManager.getCurrentUser()));
int appCount = apps.size();
final Intent queryIntent = new Intent(Intent.ACTION_MAIN, null);
queryIntent.addCategory(Intent.CATEGORY_LAUNCHER);
final int currentUser = getCurrentUser();
final List<ResolveInfo> apps = mContext.getPackageManager().queryIntentActivitiesAsUser(
queryIntent, 0 /* flags */, currentUser);
final int appCount = apps.size();
for (int i = 0; i < NUM_INITIAL_APPS && i < appCount; i++) {
LauncherActivityInfo activityInfo = apps.get(i);
mApps.add(new AppInfo(activityInfo.getComponentName(), AppInfo.USER_UNSPECIFIED));
ResolveInfo ri = apps.get(i);
ComponentName componentName = new ComponentName(
ri.activityInfo.packageName, ri.activityInfo.name);
mApps.add(new AppInfo(componentName, AppInfo.USER_UNSPECIFIED));
}
savePrefs();
}
/** Returns a pref prefixed with the serial number of the current user. */
private String userPrefixed(String pref) {
if (mCurrentUserSerialNumber == AppInfo.USER_UNSPECIFIED) {
throw new RuntimeException("Current user is not yet set");
}
return Long.toString(mCurrentUserSerialNumber) + USER_SEPARATOR + pref;
}
/** Returns the pref name for the app at a given index. */
private static String prefNameForApp(int index) {
return APP_PREF_PREFIX + Integer.toString(index);
private String prefNameForApp(int index) {
return userPrefixed(APP_PREF_PREFIX + Integer.toString(index));
}
/** Returns the pref name for the app's user at a given index. */
private static String prefUserForApp(int index) {
return APP_USER_PREFIX + Integer.toString(index);
private String prefUserForApp(int index) {
return userPrefixed(APP_USER_PREFIX + Integer.toString(index));
}
}

View File

@@ -18,24 +18,35 @@ package com.android.systemui.statusbar.phone;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
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.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.os.UserHandle;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.os.UserManager;
import android.test.AndroidTestCase;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Tests for the data model for the navigation bar app icons. */
public class NavigationBarAppsModelTest extends AndroidTestCase {
private LauncherApps mMockLauncherApps;
private PackageManager mMockPackageManager;
private SharedPreferences mMockPrefs;
private SharedPreferences.Editor mMockEdit;
private UserManager mMockUserManager;
private NavigationBarAppsModel mModel;
@@ -47,26 +58,44 @@ public class NavigationBarAppsModelTest extends AndroidTestCase {
System.setProperty("dexmaker.dexcache", mContext.getCacheDir().getPath());
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
mMockLauncherApps = mock(LauncherApps.class);
final Context context = mock(Context.class);
mMockPackageManager = mock(PackageManager.class);
mMockPrefs = mock(SharedPreferences.class);
mModel = new NavigationBarAppsModel(mMockLauncherApps, mMockPrefs);
mMockEdit = mock(SharedPreferences.Editor.class);
mMockUserManager = mock(UserManager.class);
when (context.getSharedPreferences(
"com.android.systemui.navbarapps", Context.MODE_PRIVATE)).thenReturn(mMockPrefs);
when(context.getSystemService(Context.USER_SERVICE)).thenReturn(mMockUserManager);
when(context.getPackageManager()).thenReturn(mMockPackageManager);
setContext(context);
when(mMockUserManager.getUsers()).thenReturn(new ArrayList<UserInfo>());
// Assume the version pref is present and equal to the current version.
when(mMockPrefs.getInt("version", -1)).thenReturn(2);
when(mMockPrefs.edit()).thenReturn(mMockEdit);
mModel = new NavigationBarAppsModel(context) {
@Override
protected int getCurrentUser() {
return 0;
}
};
}
/** Initializes the model from SharedPreferences for a few app activites. */
private void initializeModelFromPrefs() {
// Assume the version pref is present.
when(mMockPrefs.getInt("version", -1)).thenReturn(1);
// Assume several apps are stored.
when(mMockPrefs.getInt("app_count", -1)).thenReturn(3);
when(mMockPrefs.getString("app_0", null)).thenReturn("package0/class0");
when(mMockPrefs.getLong("app_user_0", -1)).thenReturn(-1L);
when(mMockPrefs.getString("app_1", null)).thenReturn("package1/class1");
when(mMockPrefs.getLong("app_user_1", -1)).thenReturn(45L);
when(mMockPrefs.getString("app_2", null)).thenReturn("package2/class2");
when(mMockPrefs.getLong("app_user_2", -1)).thenReturn(239L);
when(mMockPrefs.getInt("22|app_count", -1)).thenReturn(3);
when(mMockPrefs.getString("22|app_0", null)).thenReturn("package0/class0");
when(mMockPrefs.getLong("22|app_user_0", -1)).thenReturn(-1L);
when(mMockPrefs.getString("22|app_1", null)).thenReturn("package1/class1");
when(mMockPrefs.getLong("22|app_user_1", -1)).thenReturn(45L);
when(mMockPrefs.getString("22|app_2", null)).thenReturn("package2/class2");
when(mMockPrefs.getLong("22|app_user_2", -1)).thenReturn(239L);
mModel.initialize();
mModel.setCurrentUser(22L);
}
/** Tests initializing the model from SharedPreferences. */
@@ -83,22 +112,26 @@ public class NavigationBarAppsModelTest extends AndroidTestCase {
/** Tests initializing the model when the SharedPreferences aren't available. */
public void testInitializeDefaultApps() {
// Assume the version pref isn't available.
when(mMockPrefs.getInt("version", -1)).thenReturn(-1);
// Assume the user's app count pref isn't available.
when(mMockPrefs.getInt("0|app_count", -1)).thenReturn(-1);
// Assume some installed activities.
LauncherActivityInfo activity1 = mock(LauncherActivityInfo.class);
when(activity1.getComponentName()).thenReturn(new ComponentName("package1", "class1"));
LauncherActivityInfo activity2 = mock(LauncherActivityInfo.class);
when(activity2.getComponentName()).thenReturn(new ComponentName("package2", "class2"));
List<LauncherActivityInfo> apps = new ArrayList<LauncherActivityInfo>();
apps.add(activity1);
apps.add(activity2);
when(mMockLauncherApps.getActivityList(anyString(), any(UserHandle.class)))
.thenReturn(apps);
ActivityInfo ai1 = new ActivityInfo();
ai1.packageName = "package1";
ai1.name = "class1";
ActivityInfo ai2 = new ActivityInfo();
ai2.packageName = "package2";
ai2.name = "class2";
ResolveInfo ri1 = new ResolveInfo();
ri1.activityInfo = ai1;
ResolveInfo ri2 = new ResolveInfo();
ri2.activityInfo = ai2;
when(mMockPackageManager
.queryIntentActivitiesAsUser(any(Intent.class), eq(0), eq(0)))
.thenReturn(Arrays.asList(ri1, ri2));
// Initializing the model should load the installed activities.
mModel.initialize();
// Setting the user should load the installed activities.
mModel.setCurrentUser(0L);
assertEquals(2, mModel.getAppCount());
assertEquals("package1/class1", mModel.getApp(0).getComponentName().flattenToString());
assertEquals(-1L, mModel.getApp(0).getUserSerialNumber());
@@ -108,19 +141,16 @@ public class NavigationBarAppsModelTest extends AndroidTestCase {
/** Tests initializing the model if one of the prefs is missing. */
public void testInitializeWithMissingPref() {
// Assume the version pref is present.
when(mMockPrefs.getInt("version", -1)).thenReturn(1);
// Assume two apps are nominally stored.
when(mMockPrefs.getInt("app_count", -1)).thenReturn(2);
when(mMockPrefs.getString("app_0", null)).thenReturn("package0/class0");
when(mMockPrefs.getLong("app_user_0", -1)).thenReturn(239L);
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);
// But assume one pref is missing.
when(mMockPrefs.getString("app_1", null)).thenReturn(null);
when(mMockPrefs.getString("22|app_1", null)).thenReturn(null);
// Initializing the model should load from prefs and skip the missing one.
mModel.initialize();
mModel.setCurrentUser(22L);
assertEquals(1, mModel.getAppCount());
assertEquals("package0/class0", mModel.getApp(0).getComponentName().flattenToString());
assertEquals(239L, mModel.getApp(0).getUserSerialNumber());
@@ -130,15 +160,64 @@ public class NavigationBarAppsModelTest extends AndroidTestCase {
public void testSavePrefs() {
initializeModelFromPrefs();
SharedPreferences.Editor mockEdit = mock(SharedPreferences.Editor.class);
when(mMockPrefs.edit()).thenReturn(mockEdit);
mModel.savePrefs();
verify(mockEdit).clear(); // Old prefs were removed.
verify(mockEdit).putInt("version", 1);
verify(mockEdit).putInt("app_count", 3);
verify(mockEdit).putString("app_0", "package0/class0");
verify(mockEdit).putString("app_1", "package1/class1");
verify(mockEdit).putString("app_2", "package2/class2");
verify(mMockEdit).putInt("22|app_count", 3);
verify(mMockEdit).putString("22|app_0", "package0/class0");
verify(mMockEdit).putLong("22|app_user_0", -1L);
verify(mMockEdit).putString("22|app_1", "package1/class1");
verify(mMockEdit).putLong("22|app_user_1", 45L);
verify(mMockEdit).putString("22|app_2", "package2/class2");
verify(mMockEdit).putLong("22|app_user_2", 239L);
verify(mMockEdit).apply();
verifyNoMoreInteractions(mMockEdit);
}
/** Tests cleaning all prefs on a version change. */
public void testVersionChange() {
// Assume the version pref changed.
when(mMockPrefs.getInt("version", -1)).thenReturn(1);
new NavigationBarAppsModel(getContext());
verify(mMockEdit).clear();
verify(mMockEdit).putInt("version", 2);
verify(mMockEdit).apply();
verifyNoMoreInteractions(mMockEdit);
}
/** Tests cleaning prefs for deleted users. */
public void testCleaningDeletedUsers() {
// Users on the device.
final UserInfo user1 = new UserInfo(11, "", 0);
user1.serialNumber = 1111;
final UserInfo user2 = new UserInfo(13, "", 0);
user2.serialNumber = 1313;
when(mMockUserManager.getUsers()).thenReturn(Arrays.asList(user1, user2));
when(mMockPrefs.edit()).
thenReturn(mMockEdit).
thenReturn(mock(SharedPreferences.Editor.class));
// Assume the user's app count pref isn't available. This will trigger clearing deleted
// users' prefs.
when(mMockPrefs.getInt("0|app_count", -1)).thenReturn(-1);
final Map allPrefs = new HashMap<String, Object>();
allPrefs.put("version", null);
allPrefs.put("some_strange_pref", null);
allPrefs.put("", null);
allPrefs.put("|", null);
allPrefs.put("1313|app_count", null);
allPrefs.put("1212|app_count", null);
when(mMockPrefs.getAll()).thenReturn(allPrefs);
// Setting the user should remove prefs for deleted users.
mModel.setCurrentUser(0L);
verify(mMockEdit).remove("some_strange_pref");
verify(mMockEdit).remove("");
verify(mMockEdit).remove("|");
verify(mMockEdit).remove("1212|app_count");
verify(mMockEdit).apply();
verifyNoMoreInteractions(mMockEdit);
}
}