Loading packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java +55 −16 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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: Loading @@ -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 Loading Loading @@ -119,6 +135,21 @@ class NavigationBarApps extends LinearLayout { 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); } /** Loading Loading @@ -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; Loading Loading @@ -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(); } } } packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java +116 −37 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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"; Loading @@ -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>(); // Serial number of the current user. private long mCurrentUserSerialNumber = AppInfo.USER_UNSPECIFIED; public NavigationBarAppsModel(Context context) { mLauncherApps = (LauncherApps) context.getSystemService("launcherapps"); mPrefs = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); } mContext = context; mPrefs = mContext.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); @VisibleForTesting NavigationBarAppsModel(LauncherApps launcherApps, SharedPreferences prefs) { mLauncherApps = launcherApps; mPrefs = prefs; 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(); Loading Loading @@ -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(); Loading @@ -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) { Loading @@ -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)); } } packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java +129 −50 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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); mModel.initialize(); 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.setCurrentUser(22L); } /** Tests initializing the model from SharedPreferences. */ Loading @@ -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); // Initializing the model should load the installed activities. mModel.initialize(); 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)); // 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()); Loading @@ -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()); Loading @@ -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); } } Loading
packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java +55 −16 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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: Loading @@ -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 Loading Loading @@ -119,6 +135,21 @@ class NavigationBarApps extends LinearLayout { 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); } /** Loading Loading @@ -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; Loading Loading @@ -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(); } } }
packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java +116 −37 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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"; Loading @@ -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>(); // Serial number of the current user. private long mCurrentUserSerialNumber = AppInfo.USER_UNSPECIFIED; public NavigationBarAppsModel(Context context) { mLauncherApps = (LauncherApps) context.getSystemService("launcherapps"); mPrefs = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); } mContext = context; mPrefs = mContext.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); @VisibleForTesting NavigationBarAppsModel(LauncherApps launcherApps, SharedPreferences prefs) { mLauncherApps = launcherApps; mPrefs = prefs; 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(); Loading Loading @@ -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(); Loading @@ -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) { Loading @@ -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)); } }
packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java +129 −50 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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); mModel.initialize(); 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.setCurrentUser(22L); } /** Tests initializing the model from SharedPreferences. */ Loading @@ -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); // Initializing the model should load the installed activities. mModel.initialize(); 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)); // 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()); Loading @@ -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()); Loading @@ -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); } }