Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 325e2124 authored by Vadim Tryshev's avatar Vadim Tryshev Committed by Android (Google) Code Review
Browse files

Merge "Adding multiuser support to the shelf."

parents a2858716 94d469bb
Loading
Loading
Loading
Loading
+55 −16
Original line number Diff line number Diff line
@@ -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
@@ -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);
    }

    /**
@@ -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();
        }
    }
}
+116 −37
Original line number Diff line number Diff line
@@ -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>();

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

}
+129 −50
Original line number Diff line number Diff line
@@ -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);

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