Storing and using user id for pinned shelf apps.

We need this to differentiate between starting apps for the primary and
managed users.

This in particular solves the issue when apps were executed as Owner
while we are in Guest account (see the bug).

Valid user ids for a shelf app are either primary or a managed (work)
user for the profile.
Launcher allows mixing apps for for primary and the managed users on
its home screen. Apps for the managed user are marked with a suitcase
badge.

Launcher includes a user id in the drag and drop info that it sends to
SystemUI, as “profile” extra added to the intent.

Shelf now stores this user id and uses it to start apps.

Bug: 22609426
Change-Id: Id4c6c1ac8617a83f4484f78db578955699d1a888
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e392c11..0ede25f 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1129,4 +1129,7 @@
     <!-- Dialog asking if the tuner should really be removed from settings [CHAR LIMIT=NONE]-->
     <string name="remove_from_settings_prompt">Remove System UI Tuner from Settings and stop using all of its features?"</string>
 
+    <!-- Displayed when user launches an app that was uninstalled  [CHAR LIMIT=NONE] -->
+    <string name="activity_not_found">Application is not installed on your device</string>
+
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AppInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AppInfo.java
new file mode 100644
index 0000000..67dd673
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AppInfo.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import android.content.ComponentName;
+
+/**
+ * Navigation bar app information.
+ */
+class AppInfo {
+    /**
+     * Unspecified serial number for the app's user.
+     */
+    public static final long USER_UNSPECIFIED = -1;
+
+    private final ComponentName mComponentName;
+    private final long mUserSerialNumber;
+
+    public AppInfo(ComponentName componentName, long userSerialNumber) {
+        mComponentName = componentName;
+        mUserSerialNumber = userSerialNumber;
+    }
+
+    public ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    public long getUserSerialNumber() {
+        return mUserSerialNumber;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java
index d060756..71ee4e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarApps.java
@@ -18,19 +18,26 @@
 
 import android.animation.LayoutTransition;
 import android.annotation.Nullable;
+import android.app.ActivityManager;
 import android.app.ActivityOptions;
+import android.app.AppGlobals;
 import android.content.ClipData;
 import android.content.ClipDescription;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
-import android.content.pm.LauncherApps;
+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;
+import android.util.Log;
 import android.util.Slog;
 import android.view.DragEvent;
 import android.view.LayoutInflater;
@@ -38,9 +45,12 @@
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
+import android.widget.Toast;
 
 import com.android.systemui.R;
 
+import java.util.List;
+
 /**
  * Container for application icons that appear in the navigation bar. Their appearance is similar
  * to the launcher hotseat. Clicking an icon launches the associated activity. A long click will
@@ -51,11 +61,15 @@
     private final static boolean DEBUG = false;
     private final static String TAG = "NavigationBarApps";
 
+    /**
+     * Intent extra to store user serial number.
+     */
+    static final String EXTRA_PROFILE = "profile";
+
     // There are separate NavigationBarApps view instances for landscape vs. portrait, but they
     // share the data model.
     private static NavigationBarAppsModel sAppsModel;
 
-    private final LauncherApps mLauncherApps;
     private final PackageManager mPackageManager;
     private final LayoutInflater mLayoutInflater;
 
@@ -74,7 +88,6 @@
             sAppsModel = new NavigationBarAppsModel(context);
             sAppsModel.initialize();  // Load the saved icons, if any.
         }
-        mLauncherApps = (LauncherApps) context.getSystemService("launcherapps");
         mPackageManager = context.getPackageManager();
         mLayoutInflater = LayoutInflater.from(context);
 
@@ -121,12 +134,12 @@
             ImageView button = createAppButton();
             addView(button);
 
-            ComponentName activityName = sAppsModel.getApp(i);
-            CharSequence appLabel = getAppLabel(mPackageManager, activityName);
-            button.setContentDescription(getAppLabel(mPackageManager, activityName));
+            AppInfo app = sAppsModel.getApp(i);
+            CharSequence appLabel = getAppLabel(mPackageManager, app.getComponentName());
+            button.setContentDescription(appLabel);
 
             // Load the icon asynchronously.
-            new GetActivityIconTask(mPackageManager, button).execute(activityName);
+            new GetActivityIconTask(mPackageManager, button).execute(app.getComponentName());
         }
     }
 
@@ -150,8 +163,8 @@
         public boolean onLongClick(View v) {
             mDragView = v;
             ImageView icon = (ImageView) v;
-            ComponentName activityName = sAppsModel.getApp(indexOfChild(v));
-            startAppDrag(icon, activityName);
+            AppInfo app = sAppsModel.getApp(indexOfChild(v));
+            startAppDrag(icon, app);
             return true;
         }
     }
@@ -175,9 +188,13 @@
     }
 
     /** Helper function to start dragging an app icon (either pinned or recent). */
-    static void startAppDrag(ImageView icon, ComponentName activityName) {
+    static void startAppDrag(ImageView icon, AppInfo appInfo) {
         // The drag data is an Intent to launch the activity.
-        Intent mainIntent = Intent.makeMainActivity(activityName);
+        Intent mainIntent = Intent.makeMainActivity(appInfo.getComponentName());
+        long userSerialNumber = appInfo.getUserSerialNumber();
+        if (userSerialNumber != AppInfo.USER_UNSPECIFIED) {
+            mainIntent.putExtra(EXTRA_PROFILE, userSerialNumber);
+        }
         ClipData dragData = ClipData.newIntent("", mainIntent);
         // Use the ImageView to create the shadow.
         View.DragShadowBuilder shadow = new AppIconDragShadowBuilder(icon);
@@ -294,7 +311,7 @@
         addView(mDragView, targetIndex);
 
         // Update the data model.
-        ComponentName app = sAppsModel.removeApp(dragViewIndex);
+        AppInfo app = sAppsModel.removeApp(dragViewIndex);
         sAppsModel.addApp(targetIndex, app);
     }
 
@@ -314,15 +331,15 @@
         }
 
         // The drag view was a placeholder. Unpack the drop.
-        ComponentName activityName = getActivityNameFromDragEvent(event);
-        if (activityName == null) {
+        AppInfo appInfo = getAppFromDragEvent(event);
+        if (appInfo == null) {
             // This wasn't a valid drop. Clean up the placeholder and model.
             removePlaceholderDragViewIfNeeded();
-            return true;
+            return false;
         }
 
         // The drop had valid data. Update the placeholder with a real activity and icon.
-        updateAppAt(dragViewIndex, activityName);
+        updateAppAt(dragViewIndex, appInfo);
         endDrag();
         return true;
     }
@@ -334,8 +351,8 @@
         sAppsModel.savePrefs();
     }
 
-    /** Returns an app launch Intent from a DragEvent, or null if the data wasn't valid. */
-    private ComponentName getActivityNameFromDragEvent(DragEvent event) {
+    /** Returns an app info from a DragEvent, or null if the data wasn't valid. */
+    private AppInfo getAppFromDragEvent(DragEvent event) {
         ClipData data = event.getClipData();
         if (data == null) {
             return null;
@@ -347,14 +364,16 @@
         if (intent == null) {
             return null;
         }
-        return intent.getComponent();
+
+        long userSerialNumber = intent.getLongExtra(EXTRA_PROFILE, AppInfo.USER_UNSPECIFIED);
+        return new AppInfo(intent.getComponent(), userSerialNumber);
     }
 
     /** Updates the app at a given view index. */
-    private void updateAppAt(int index, ComponentName activityName) {
-        sAppsModel.setApp(index, activityName);
+    private void updateAppAt(int index, AppInfo appInfo) {
+        sAppsModel.setApp(index, appInfo);
         ImageView button = (ImageView) getChildAt(index);
-        new GetActivityIconTask(mPackageManager, button).execute(activityName);
+        new GetActivityIconTask(mPackageManager, button).execute(appInfo.getComponentName());
     }
 
     /** Removes the empty placeholder view and cleans up the data model. */
@@ -411,11 +430,26 @@
     private class AppClickListener implements View.OnClickListener {
         @Override
         public void onClick(View v) {
-            ComponentName activityName = sAppsModel.getApp(indexOfChild(v));
+            AppInfo appInfo = sAppsModel.getApp(indexOfChild(v));
+            ComponentName component = appInfo.getComponentName();
 
-            // TODO: Support apps from multiple user profiles. The profile will need to be stored in
-            // the data model for each app shortcut.
-            UserHandle user = UserHandle.OWNER;
+            UserManager userManager =
+                    (UserManager) getContext().getSystemService(Context.USER_SERVICE);
+
+            long appUserSerialNumber = appInfo.getUserSerialNumber();
+
+            UserHandle appUser = null;
+            if (appUserSerialNumber != AppInfo.USER_UNSPECIFIED) {
+                appUser = userManager.getUserForSerialNumber(appUserSerialNumber);
+            }
+
+            int appUserId;
+            if (appUser != null) {
+                appUserId = appUser.getIdentifier();
+            } else {
+                appUserId = ActivityManager.getCurrentUser();
+                appUser = new UserHandle(appUserId);
+            }
 
             // Play a scale-up animation while launching the activity.
             // TODO: Consider playing a different animation, or no animation, if the activity is
@@ -427,8 +461,53 @@
                     ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight());
             Bundle optsBundle = opts.toBundle();
 
-            // Launch the activity.
-            mLauncherApps.startMainActivity(activityName, user, sourceBounds, optsBundle);
+            // 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);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java
index d74a522..9f80d33 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarAppsModel.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import android.app.ActivityManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -55,11 +56,14 @@
     // Preference name prefix for each app's info. The actual pref has an integer appended to it.
     private final static String APP_PREF_PREFIX = "app_";
 
+    // 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;
     private final SharedPreferences mPrefs;
 
-    // Apps are represented as an ordered list of component names.
-    private final List<ComponentName> mApps = new ArrayList<ComponentName>();
+    // 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");
@@ -97,22 +101,22 @@
     }
 
     /** Returns the app at the given index. */
-    public ComponentName getApp(int index) {
+    public AppInfo getApp(int index) {
         return mApps.get(index);
     }
 
     /** Adds the app before the given index. */
-    public void addApp(int index, ComponentName name) {
-        mApps.add(index, name);
+    public void addApp(int index, AppInfo appInfo) {
+        mApps.add(index, appInfo);
     }
 
     /** Sets the app at the given index. */
-    public void setApp(int index, ComponentName name) {
-        mApps.set(index, name);
+    public void setApp(int index, AppInfo appInfo) {
+        mApps.set(index, appInfo);
     }
 
     /** Remove the app at the given index. */
-    public ComponentName removeApp(int index) {
+    public AppInfo removeApp(int index) {
         return mApps.remove(index);
     }
 
@@ -125,8 +129,10 @@
         int appCount = mApps.size();
         edit.putInt(APP_COUNT_PREF, appCount);
         for (int i = 0; i < appCount; i++) {
-            String componentNameString = mApps.get(i).flattenToString();
+            final AppInfo appInfo = mApps.get(i);
+            String componentNameString = appInfo.getComponentName().flattenToString();
             edit.putString(prefNameForApp(i), componentNameString);
+            edit.putLong(prefUserForApp(i), appInfo.getUserSerialNumber());
         }
         // Start an asynchronous disk write.
         edit.apply();
@@ -143,7 +149,8 @@
                 continue;
             }
             ComponentName componentName = ComponentName.unflattenFromString(prefValue);
-            mApps.add(componentName);
+            long userSerialNumber = mPrefs.getLong(prefUserForApp(i), AppInfo.USER_UNSPECIFIED);
+            mApps.add(new AppInfo(componentName, userSerialNumber));
         }
     }
 
@@ -151,11 +158,12 @@
     private void addDefaultApps() {
         // Get a list of all app activities.
         List<LauncherActivityInfo> apps =
-                mLauncherApps.getActivityList(null /* packageName */, UserHandle.OWNER);
+                mLauncherApps.getActivityList(
+                        null /* packageName */, new UserHandle(ActivityManager.getCurrentUser()));
         int appCount = apps.size();
         for (int i = 0; i < NUM_INITIAL_APPS && i < appCount; i++) {
             LauncherActivityInfo activityInfo = apps.get(i);
-            mApps.add(activityInfo.getComponentName());
+            mApps.add(new AppInfo(activityInfo.getComponentName(), AppInfo.USER_UNSPECIFIED));
         }
     }
 
@@ -163,4 +171,10 @@
     private static String prefNameForApp(int index) {
         return 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);
+    }
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarRecents.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarRecents.java
index c7fae92..eaa1c20 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarRecents.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarRecents.java
@@ -232,7 +232,7 @@
             }
 
             if (DEBUG) Slog.d(TAG, "Start drag with " + intent);
-            NavigationBarApps.startAppDrag(icon, intent.getComponent());
+            NavigationBarApps.startAppDrag(icon, new AppInfo (intent.getComponent(), AppInfo.USER_UNSPECIFIED));
             return true;
         }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java
index 84aaac7..f46e76c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarAppsModelTest.java
@@ -60,8 +60,11 @@
         // 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();
     }
@@ -70,9 +73,12 @@
     public void testInitializeFromPrefs() {
         initializeModelFromPrefs();
         assertEquals(3, mModel.getAppCount());
-        assertEquals("package0/class0", mModel.getApp(0).flattenToString());
-        assertEquals("package1/class1", mModel.getApp(1).flattenToString());
-        assertEquals("package2/class2", mModel.getApp(2).flattenToString());
+        assertEquals("package0/class0", mModel.getApp(0).getComponentName().flattenToString());
+        assertEquals(-1L, mModel.getApp(0).getUserSerialNumber());
+        assertEquals("package1/class1", mModel.getApp(1).getComponentName().flattenToString());
+        assertEquals(45L, mModel.getApp(1).getUserSerialNumber());
+        assertEquals("package2/class2", mModel.getApp(2).getComponentName().flattenToString());
+        assertEquals(239L, mModel.getApp(2).getUserSerialNumber());
     }
 
     /** Tests initializing the model when the SharedPreferences aren't available. */
@@ -94,8 +100,10 @@
         // Initializing the model should load the installed activities.
         mModel.initialize();
         assertEquals(2, mModel.getAppCount());
-        assertEquals("package1/class1", mModel.getApp(0).flattenToString());
-        assertEquals("package2/class2", mModel.getApp(1).flattenToString());
+        assertEquals("package1/class1", mModel.getApp(0).getComponentName().flattenToString());
+        assertEquals(-1L, mModel.getApp(0).getUserSerialNumber());
+        assertEquals("package2/class2", mModel.getApp(1).getComponentName().flattenToString());
+        assertEquals(-1L, mModel.getApp(1).getUserSerialNumber());
     }
 
     /** Tests initializing the model if one of the prefs is missing. */
@@ -106,6 +114,7 @@
         // 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);
 
         // But assume one pref is missing.
         when(mMockPrefs.getString("app_1", null)).thenReturn(null);
@@ -113,7 +122,8 @@
         // Initializing the model should load from prefs and skip the missing one.
         mModel.initialize();
         assertEquals(1, mModel.getAppCount());
-        assertEquals("package0/class0", mModel.getApp(0).flattenToString());
+        assertEquals("package0/class0", mModel.getApp(0).getComponentName().flattenToString());
+        assertEquals(239L, mModel.getApp(0).getUserSerialNumber());
     }
 
     /** Tests saving the model to SharedPreferences. */