blob: 1e1f17894605d834114f553c369938e8cfe94596 [file] [log] [blame]
/*
* 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.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;
import java.util.Collections;
import java.util.List;
/**
* 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<ComponentName, Integer> 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.first.getPackageName(), confirmActivity.second);
} 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 String 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, 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, String launcherPackage, int launcherUserId) {
final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked(
inShortcut.getPackage(), inShortcut.getUserId());
final ShortcutInfo existing = ps.findShortcutById(inShortcut.getId());
final boolean existsAlready = existing != null;
final boolean existingIsVisible = existsAlready && existing.isVisibleToPublisher();
if (DEBUG) {
Slog.d(TAG, "requestPinnedShortcut: package=" + inShortcut.getPackage()
+ " existsAlready=" + existsAlready
+ " existingIsVisible=" + existingIsVisible
+ " shortcut=" + inShortcut.toInsecureString());
}
// This is the shortcut that'll be sent to the launcher.
final ShortcutInfo shortcutForLauncher;
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<ComponentName, Integer> getRequestPinConfirmationActivity(
int callingUserId, int requestType) {
// Find the default launcher.
final int launcherUserId = mService.getParentOrSelfUserId(callingUserId);
final String defaultLauncher = mService.getDefaultLauncher(launcherUserId);
if (defaultLauncher == null) {
Log.e(TAG, "Default launcher not found.");
return null;
}
final ComponentName activity = mService.injectGetPinConfirmationActivity(
defaultLauncher, 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();
List<ShortcutInfo> changedShortcuts = null;
final ShortcutPackage ps;
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."); // This too.
}
return true;
}
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 placeholder here. It's later removed
// in deleteDynamicWithId().
if (original.getActivity() == null) {
original.setActivity(mService.getDummyMainActivity(appPackageName));
}
ps.addOrReplaceDynamicShortcut(original);
}
// Pin the shortcut.
if (DEBUG) {
Slog.d(TAG, "Pinning " + shortcutId);
}
launcher.addPinnedShortcut(appPackageName, appUserId, shortcutId,
/*forPinRequest=*/ true);
if (current == null) {
if (DEBUG) {
Slog.d(TAG, "Removing " + shortcutId + " as dynamic");
}
ps.deleteDynamicWithId(shortcutId, /*ignoreInvisible=*/ false,
/*wasPushedOut=*/ false);
}
ps.adjustRanks(); // Shouldn't be needed, but just in case.
changedShortcuts = Collections.singletonList(ps.findShortcutById(shortcutId));
}
mService.verifyStates();
mService.packageShortcutsChanged(ps, changedShortcuts, null);
return true;
}
}