/* * Copyright (C) 2016 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.server.pm; import android.annotation.NonNull; import android.annotation.Nullable; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Intent; import android.content.IntentSender; import android.content.pm.IPinItemRequest; import android.content.pm.LauncherApps; import android.content.pm.LauncherApps.PinItemRequest; import android.content.pm.ShortcutInfo; import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; import android.util.Pair; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; /** * Handles {@link android.content.pm.ShortcutManager#requestPinShortcut} related tasks. */ class ShortcutRequestPinProcessor { private static final String TAG = ShortcutService.TAG; private static final boolean DEBUG = ShortcutService.DEBUG; private final ShortcutService mService; private final Object mLock; /** * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks. */ private abstract static class PinItemRequestInner extends IPinItemRequest.Stub { protected final ShortcutRequestPinProcessor mProcessor; private final IntentSender mResultIntent; private final int mLauncherUid; @GuardedBy("this") private boolean mAccepted; private PinItemRequestInner(ShortcutRequestPinProcessor processor, IntentSender resultIntent, int launcherUid) { mProcessor = processor; mResultIntent = resultIntent; mLauncherUid = launcherUid; } @Override public ShortcutInfo getShortcutInfo() { return null; } @Override public AppWidgetProviderInfo getAppWidgetProviderInfo() { return null; } @Override public Bundle getExtras() { return null; } /** * Returns true if the caller is same as the default launcher app when this request * object was created. */ private boolean isCallerValid() { return mProcessor.isCallerUid(mLauncherUid); } @Override public boolean isValid() { if (!isCallerValid()) { return false; } // TODO When an app calls requestPinShortcut(), all pending requests should be // invalidated. synchronized (this) { return !mAccepted; } } /** * Called when the launcher calls {@link PinItemRequest#accept}. */ @Override public boolean accept(Bundle options) { // Make sure the options are unparcellable by the FW. (e.g. not containing unknown // classes.) if (!isCallerValid()) { throw new SecurityException("Calling uid mismatch"); } Intent extras = null; if (options != null) { try { options.size(); extras = new Intent().putExtras(options); } catch (RuntimeException e) { throw new IllegalArgumentException("options cannot be unparceled", e); } } synchronized (this) { if (mAccepted) { throw new IllegalStateException("accept() called already"); } mAccepted = true; } // Pin it and send the result intent. if (tryAccept()) { mProcessor.sendResultIntent(mResultIntent, extras); return true; } else { return false; } } protected boolean tryAccept() { return true; } } /** * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks. */ private static class PinAppWidgetRequestInner extends PinItemRequestInner { final AppWidgetProviderInfo mAppWidgetProviderInfo; final Bundle mExtras; private PinAppWidgetRequestInner(ShortcutRequestPinProcessor processor, IntentSender resultIntent, int launcherUid, AppWidgetProviderInfo appWidgetProviderInfo, Bundle extras) { super(processor, resultIntent, launcherUid); mAppWidgetProviderInfo = appWidgetProviderInfo; mExtras = extras; } @Override public AppWidgetProviderInfo getAppWidgetProviderInfo() { return mAppWidgetProviderInfo; } @Override public Bundle getExtras() { return mExtras; } } /** * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks. */ private static class PinShortcutRequestInner extends PinItemRequestInner { /** Original shortcut passed by the app. */ public final ShortcutInfo shortcutOriginal; /** * Cloned shortcut that's passed to the launcher. The notable difference from * {@link #shortcutOriginal} is it must not have the intent. */ public final ShortcutInfo shortcutForLauncher; public final String launcherPackage; public final int launcherUserId; public final boolean preExisting; private PinShortcutRequestInner(ShortcutRequestPinProcessor processor, ShortcutInfo shortcutOriginal, ShortcutInfo shortcutForLauncher, IntentSender resultIntent, String launcherPackage, int launcherUserId, int launcherUid, boolean preExisting) { super(processor, resultIntent, launcherUid); this.shortcutOriginal = shortcutOriginal; this.shortcutForLauncher = shortcutForLauncher; this.launcherPackage = launcherPackage; this.launcherUserId = launcherUserId; this.preExisting = preExisting; } @Override public ShortcutInfo getShortcutInfo() { return shortcutForLauncher; } @Override protected boolean tryAccept() { if (DEBUG) { Slog.d(TAG, "Launcher accepted shortcut. ID=" + shortcutOriginal.getId() + " package=" + shortcutOriginal.getPackage()); } return mProcessor.directPinShortcut(this); } } public ShortcutRequestPinProcessor(ShortcutService service, Object lock) { mService = service; mLock = lock; } public boolean isRequestPinItemSupported(int callingUserId, int requestType) { return getRequestPinConfirmationActivity(callingUserId, requestType) != null; } /** * Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)} and * {@link android.appwidget.AppWidgetManager#requestPinAppWidget}. * In this flow the PinItemRequest is delivered directly to the default launcher app. * One of {@param inShortcut} and {@param inAppWidget} is always non-null and the other is * always null. */ public boolean requestPinItemLocked(ShortcutInfo inShortcut, AppWidgetProviderInfo inAppWidget, Bundle extras, int userId, IntentSender resultIntent) { // First, make sure the launcher supports it. // Find the confirmation activity in the default launcher. final int requestType = inShortcut != null ? PinItemRequest.REQUEST_TYPE_SHORTCUT : PinItemRequest.REQUEST_TYPE_APPWIDGET; final Pair confirmActivity = getRequestPinConfirmationActivity(userId, requestType); // If the launcher doesn't support it, just return a rejected result and finish. if (confirmActivity == null) { Log.w(TAG, "Launcher doesn't support requestPinnedShortcut(). Shortcut not created."); return false; } final int launcherUserId = confirmActivity.second; // Make sure the launcher user is unlocked. (it's always the parent profile, so should // really be unlocked here though.) mService.throwIfUserLockedL(launcherUserId); // Next, validate the incoming shortcut, etc. final PinItemRequest request; if (inShortcut != null) { request = requestPinShortcutLocked(inShortcut, resultIntent, confirmActivity); } else { int launcherUid = mService.injectGetPackageUid( confirmActivity.first.getPackageName(), launcherUserId); request = new PinItemRequest( new PinAppWidgetRequestInner(this, resultIntent, launcherUid, inAppWidget, extras), PinItemRequest.REQUEST_TYPE_APPWIDGET); } return startRequestConfirmActivity(confirmActivity.first, launcherUserId, request, requestType); } /** * Handle {@link android.content.pm.ShortcutManager#createShortcutResultIntent(ShortcutInfo)}. * In this flow the PinItemRequest is delivered to the caller app. Its the app's responsibility * to send it to the Launcher app (via {@link android.app.Activity#setResult(int, Intent)}). */ public Intent createShortcutResultIntent(@NonNull ShortcutInfo inShortcut, int userId) { // Find the default launcher activity final int launcherUserId = mService.getParentOrSelfUserId(userId); final ComponentName defaultLauncher = mService.getDefaultLauncher(launcherUserId); if (defaultLauncher == null) { Log.e(TAG, "Default launcher not found."); return null; } // Make sure the launcher user is unlocked. (it's always the parent profile, so should // really be unlocked here though.) mService.throwIfUserLockedL(launcherUserId); // Next, validate the incoming shortcut, etc. final PinItemRequest request = requestPinShortcutLocked(inShortcut, null, Pair.create(defaultLauncher, launcherUserId)); return new Intent().putExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST, request); } /** * Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)}. */ @NonNull private PinItemRequest requestPinShortcutLocked(ShortcutInfo inShortcut, IntentSender resultIntentOriginal, Pair confirmActivity) { final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked( inShortcut.getPackage(), inShortcut.getUserId()); final ShortcutInfo existing = ps.findShortcutById(inShortcut.getId()); final boolean existsAlready = existing != null; if (DEBUG) { Slog.d(TAG, "requestPinnedShortcut: package=" + inShortcut.getPackage() + " existsAlready=" + existsAlready + " shortcut=" + inShortcut.toInsecureString()); } // This is the shortcut that'll be sent to the launcher. final ShortcutInfo shortcutForLauncher; final String launcherPackage = confirmActivity.first.getPackageName(); final int launcherUserId = confirmActivity.second; IntentSender resultIntentToSend = resultIntentOriginal; if (existsAlready) { validateExistingShortcut(existing); final boolean isAlreadyPinned = mService.getLauncherShortcutsLocked( launcherPackage, existing.getUserId(), launcherUserId).hasPinned(existing); if (isAlreadyPinned) { // When the shortcut is already pinned by this launcher, the request will always // succeed, so just send the result at this point. sendResultIntent(resultIntentOriginal, null); // So, do not send the intent again. resultIntentToSend = null; } // Pass a clone, not the original. // Note this will remove the intent and icons. shortcutForLauncher = existing.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); if (!isAlreadyPinned) { // FLAG_PINNED may still be set, if it's pinned by other launchers. shortcutForLauncher.clearFlags(ShortcutInfo.FLAG_PINNED); } } else { // If the shortcut has no default activity, try to set the main activity. // But in the request-pin case, it's optional, so it's okay even if the caller // has no default activity. if (inShortcut.getActivity() == null) { inShortcut.setActivity(mService.injectGetDefaultMainActivity( inShortcut.getPackage(), inShortcut.getUserId())); } // It doesn't exist, so it must have all mandatory fields. mService.validateShortcutForPinRequest(inShortcut); // Initialize the ShortcutInfo for pending approval. inShortcut.resolveResourceStrings(mService.injectGetResourcesForApplicationAsUser( inShortcut.getPackage(), inShortcut.getUserId())); if (DEBUG) { Slog.d(TAG, "Resolved shortcut=" + inShortcut.toInsecureString()); } // We should strip out the intent, but should preserve the icon. shortcutForLauncher = inShortcut.clone( ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER_APPROVAL); } if (DEBUG) { Slog.d(TAG, "Sending to launcher=" + shortcutForLauncher.toInsecureString()); } // Create a request object. final PinShortcutRequestInner inner = new PinShortcutRequestInner(this, inShortcut, shortcutForLauncher, resultIntentToSend, launcherPackage, launcherUserId, mService.injectGetPackageUid(launcherPackage, launcherUserId), existsAlready); return new PinItemRequest(inner, PinItemRequest.REQUEST_TYPE_SHORTCUT); } private void validateExistingShortcut(ShortcutInfo shortcutInfo) { // Make sure it's enabled. // (Because we can't always force enable it automatically as it may be a stale // manifest shortcut.) Preconditions.checkArgument(shortcutInfo.isEnabled(), "Shortcut ID=" + shortcutInfo + " already exists but disabled."); } private boolean startRequestConfirmActivity(ComponentName activity, int launcherUserId, PinItemRequest request, int requestType) { final String action = requestType == LauncherApps.PinItemRequest.REQUEST_TYPE_SHORTCUT ? LauncherApps.ACTION_CONFIRM_PIN_SHORTCUT : LauncherApps.ACTION_CONFIRM_PIN_APPWIDGET; // Start the activity. final Intent confirmIntent = new Intent(action); confirmIntent.setComponent(activity); confirmIntent.putExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST, request); confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); final long token = mService.injectClearCallingIdentity(); try { mService.mContext.startActivityAsUser( confirmIntent, UserHandle.of(launcherUserId)); } catch (RuntimeException e) { // ActivityNotFoundException, etc. Log.e(TAG, "Unable to start activity " + activity, e); return false; } finally { mService.injectRestoreCallingIdentity(token); } return true; } /** * Find the activity that handles {@link LauncherApps#ACTION_CONFIRM_PIN_SHORTCUT} in the * default launcher. */ @Nullable @VisibleForTesting Pair getRequestPinConfirmationActivity( int callingUserId, int requestType) { // Find the default launcher. final int launcherUserId = mService.getParentOrSelfUserId(callingUserId); final ComponentName defaultLauncher = mService.getDefaultLauncher(launcherUserId); if (defaultLauncher == null) { Log.e(TAG, "Default launcher not found."); return null; } final ComponentName activity = mService.injectGetPinConfirmationActivity( defaultLauncher.getPackageName(), launcherUserId, requestType); return (activity == null) ? null : Pair.create(activity, launcherUserId); } public void sendResultIntent(@Nullable IntentSender intent, @Nullable Intent extras) { if (DEBUG) { Slog.d(TAG, "Sending result intent."); } mService.injectSendIntentSender(intent, extras); } public boolean isCallerUid(int uid) { return uid == mService.injectBinderCallingUid(); } /** * The last step of the "request pin shortcut" flow. Called when the launcher accepted a * request. */ public boolean directPinShortcut(PinShortcutRequestInner request) { final ShortcutInfo original = request.shortcutOriginal; final int appUserId = original.getUserId(); final String appPackageName = original.getPackage(); final int launcherUserId = request.launcherUserId; final String launcherPackage = request.launcherPackage; final String shortcutId = original.getId(); synchronized (mLock) { if (!(mService.isUserUnlockedL(appUserId) && mService.isUserUnlockedL(request.launcherUserId))) { Log.w(TAG, "User is locked now."); return false; } final ShortcutLauncher launcher = mService.getLauncherShortcutsLocked( launcherPackage, appUserId, launcherUserId); launcher.attemptToRestoreIfNeededAndSave(); if (launcher.hasPinned(original)) { if (DEBUG) { Slog.d(TAG, "Shortcut " + original + " already pinned."); } return true; } final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked( appPackageName, appUserId); final ShortcutInfo current = ps.findShortcutById(shortcutId); // The shortcut might have been changed, so we need to do the same validation again. try { if (current == null) { // It doesn't exist, so it must have all necessary fields. mService.validateShortcutForPinRequest(original); } else { validateExistingShortcut(current); } } catch (RuntimeException e) { Log.w(TAG, "Unable to pin shortcut: " + e.getMessage()); return false; } // If the shortcut doesn't exist, need to create it. // First, create it as a dynamic shortcut. if (current == null) { if (DEBUG) { Slog.d(TAG, "Temporarily adding " + shortcutId + " as dynamic"); } // Add as a dynamic shortcut. In order for a shortcut to be dynamic, it must // have a target activity, so we set a dummy here. It's later removed // in deleteDynamicWithId(). if (original.getActivity() == null) { original.setActivity(mService.getDummyMainActivity(appPackageName)); } ps.addOrUpdateDynamicShortcut(original); } // Pin the shortcut. if (DEBUG) { Slog.d(TAG, "Pinning " + shortcutId); } launcher.addPinnedShortcut(appPackageName, appUserId, shortcutId); if (current == null) { if (DEBUG) { Slog.d(TAG, "Removing " + shortcutId + " as dynamic"); } ps.deleteDynamicWithId(shortcutId); } ps.adjustRanks(); // Shouldn't be needed, but just in case. } mService.verifyStates(); mService.packageShortcutsChanged(appPackageName, appUserId); return true; } }