diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index e392c1134d421..0ede25f34b163 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1129,4 +1129,7 @@ Remove System UI Tuner from Settings and stop using all of its features?" + + Application is not installed on your device + 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 0000000000000..67dd6736a9f62 --- /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 d0607562fcd71..71ee4e42f2e4b 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 @@ package com.android.systemui.statusbar.phone; 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.View; 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 @@ class NavigationBarApps extends LinearLayout { 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 @@ class NavigationBarApps extends LinearLayout { 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 @@ class NavigationBarApps extends LinearLayout { 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 @@ class NavigationBarApps extends LinearLayout { 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 @@ class NavigationBarApps extends LinearLayout { } /** 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 @@ class NavigationBarApps extends LinearLayout { 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 @@ class NavigationBarApps extends LinearLayout { } // 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 @@ class NavigationBarApps extends LinearLayout { 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 @@ class NavigationBarApps extends LinearLayout { 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 @@ class NavigationBarApps extends LinearLayout { 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 @@ class NavigationBarApps extends LinearLayout { 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 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 d74a5223283d7..9f80d33aed889 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 @@ class NavigationBarAppsModel { // 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 mApps = new ArrayList(); + // Apps are represented as an ordered list of app infos. + private final List mApps = new ArrayList(); public NavigationBarAppsModel(Context context) { mLauncherApps = (LauncherApps) context.getSystemService("launcherapps"); @@ -97,22 +101,22 @@ class NavigationBarAppsModel { } /** 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 @@ class NavigationBarAppsModel { 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 @@ class NavigationBarAppsModel { 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 @@ class NavigationBarAppsModel { private void addDefaultApps() { // Get a list of all app activities. List 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 @@ class NavigationBarAppsModel { 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 c7fae9207647b..eaa1c20c9b0cc 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 @@ class NavigationBarRecents extends LinearLayout { } 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 84aaac7f4c664..f46e76c701754 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 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { // 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 class NavigationBarAppsModelTest extends AndroidTestCase { 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 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { // 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 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { // 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 @@ public class NavigationBarAppsModelTest extends AndroidTestCase { // 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. */