ShortcutRequestPinProcessor.java revision 255461f676f09db9be3876c156d571f4e08981ee
1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package com.android.server.pm; 17 18import android.annotation.Nullable; 19import android.appwidget.AppWidgetProviderInfo; 20import android.content.ComponentName; 21import android.content.Intent; 22import android.content.IntentSender; 23import android.content.pm.IPinItemRequest; 24import android.content.pm.LauncherApps; 25import android.content.pm.LauncherApps.PinItemRequest; 26import android.content.pm.ShortcutInfo; 27import android.os.Bundle; 28import android.os.UserHandle; 29import android.util.Log; 30import android.util.Pair; 31import android.util.Slog; 32 33import com.android.internal.annotations.GuardedBy; 34import com.android.internal.annotations.VisibleForTesting; 35import com.android.internal.util.Preconditions; 36 37/** 38 * Handles {@link android.content.pm.ShortcutManager#requestPinShortcut} related tasks. 39 */ 40class ShortcutRequestPinProcessor { 41 private static final String TAG = ShortcutService.TAG; 42 private static final boolean DEBUG = ShortcutService.DEBUG; 43 44 private final ShortcutService mService; 45 private final Object mLock; 46 47 /** 48 * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks. 49 */ 50 private static class PinItemRequestInner extends IPinItemRequest.Stub { 51 protected final ShortcutRequestPinProcessor mProcessor; 52 private final IntentSender mResultIntent; 53 54 @GuardedBy("this") 55 private boolean mAccepted; 56 57 private PinItemRequestInner(ShortcutRequestPinProcessor processor, 58 IntentSender resultIntent) { 59 mProcessor = processor; 60 mResultIntent = resultIntent; 61 } 62 63 @Override 64 public boolean isValid() { 65 // TODO When an app calls requestPinShortcut(), all pending requests should be 66 // invalidated. 67 synchronized (this) { 68 return !mAccepted; 69 } 70 } 71 72 /** 73 * Called when the launcher calls {@link PinItemRequest#accept}. 74 */ 75 @Override 76 public boolean accept(Bundle options) { 77 // Make sure the options are unparcellable by the FW. (e.g. not containing unknown 78 // classes.) 79 Intent extras = null; 80 if (options != null) { 81 try { 82 options.size(); 83 extras = new Intent().putExtras(options); 84 } catch (RuntimeException e) { 85 throw new IllegalArgumentException("options cannot be unparceled", e); 86 } 87 } 88 synchronized (this) { 89 if (mAccepted) { 90 throw new IllegalStateException("accept() called already"); 91 } 92 mAccepted = true; 93 } 94 95 // Pin it and send the result intent. 96 if (tryAccept()) { 97 mProcessor.sendResultIntent(mResultIntent, extras); 98 return true; 99 } else { 100 return false; 101 } 102 } 103 104 protected boolean tryAccept() { 105 return true; 106 } 107 } 108 109 /** 110 * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks. 111 */ 112 private static class PinShortcutRequestInner extends PinItemRequestInner { 113 /** Original shortcut passed by the app. */ 114 public final ShortcutInfo shortcutOriginal; 115 116 /** 117 * Cloned shortcut that's passed to the launcher. The notable difference from 118 * {@link #shortcutOriginal} is it must not have the intent. 119 */ 120 public final ShortcutInfo shortcutForLauncher; 121 122 public final String launcherPackage; 123 public final int launcherUserId; 124 public final boolean preExisting; 125 126 private PinShortcutRequestInner(ShortcutRequestPinProcessor processor, 127 ShortcutInfo shortcutOriginal, ShortcutInfo shortcutForLauncher, 128 IntentSender resultIntent, 129 String launcherPackage, int launcherUserId, boolean preExisting) { 130 super(processor, resultIntent); 131 this.shortcutOriginal = shortcutOriginal; 132 this.shortcutForLauncher = shortcutForLauncher; 133 this.launcherPackage = launcherPackage; 134 this.launcherUserId = launcherUserId; 135 this.preExisting = preExisting; 136 } 137 138 @Override 139 protected boolean tryAccept() { 140 if (DEBUG) { 141 Slog.d(TAG, "Launcher accepted shortcut. ID=" + shortcutOriginal.getId() 142 + " package=" + shortcutOriginal.getPackage()); 143 } 144 return mProcessor.directPinShortcut(this); 145 } 146 } 147 148 public ShortcutRequestPinProcessor(ShortcutService service, Object lock) { 149 mService = service; 150 mLock = lock; 151 } 152 153 public boolean isRequestPinnedShortcutSupported(int callingUserId) { 154 return getRequestPinShortcutConfirmationActivity(callingUserId) != null; 155 } 156 157 /** 158 * Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)} and 159 * {@link android.appwidget.AppWidgetManager#requestPinAppWidget}. 160 * One of {@param inShortcut} and {@param inAppWidget} is always non-null and the other is 161 * always null. 162 */ 163 public boolean requestPinItemLocked(ShortcutInfo inShortcut, AppWidgetProviderInfo inAppWidget, 164 int userId, IntentSender resultIntent) { 165 166 // First, make sure the launcher supports it. 167 168 // Find the confirmation activity in the default launcher. 169 final Pair<ComponentName, Integer> confirmActivity = 170 getRequestPinShortcutConfirmationActivity(userId); 171 172 // If the launcher doesn't support it, just return a rejected result and finish. 173 if (confirmActivity == null) { 174 Log.w(TAG, "Launcher doesn't support requestPinnedShortcut(). Shortcut not created."); 175 return false; 176 } 177 178 final int launcherUserId = confirmActivity.second; 179 180 // Make sure the launcher user is unlocked. (it's always the parent profile, so should 181 // really be unlocked here though.) 182 mService.throwIfUserLockedL(launcherUserId); 183 184 // Next, validate the incoming shortcut, etc. 185 final PinItemRequest request; 186 if (inShortcut != null) { 187 request = requestPinShortcutLocked(inShortcut, resultIntent, confirmActivity); 188 } else { 189 request = new PinItemRequest(inAppWidget, new PinItemRequestInner(this, resultIntent)); 190 } 191 192 if (request == null) { 193 sendResultIntent(resultIntent, null); 194 return true; 195 } 196 return startRequestConfirmActivity(confirmActivity.first, launcherUserId, request); 197 } 198 199 /** 200 * Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)}. 201 */ 202 private PinItemRequest requestPinShortcutLocked(ShortcutInfo inShortcut, 203 IntentSender resultIntent, Pair<ComponentName, Integer> confirmActivity) { 204 final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked( 205 inShortcut.getPackage(), inShortcut.getUserId()); 206 207 final ShortcutInfo existing = ps.findShortcutById(inShortcut.getId()); 208 final boolean existsAlready = existing != null; 209 210 if (DEBUG) { 211 Slog.d(TAG, "requestPinnedShortcut: package=" + inShortcut.getPackage() 212 + " existsAlready=" + existsAlready 213 + " shortcut=" + inShortcut.toInsecureString()); 214 } 215 216 // This is the shortcut that'll be sent to the launcher. 217 final ShortcutInfo shortcutForLauncher; 218 final String launcherPackage = confirmActivity.first.getPackageName(); 219 final int launcherUserId = confirmActivity.second; 220 221 if (existsAlready) { 222 validateExistingShortcut(existing); 223 224 // See if it's already pinned. 225 if (mService.getLauncherShortcutsLocked( 226 launcherPackage, existing.getUserId(), launcherUserId).hasPinned(existing)) { 227 Log.i(TAG, "Launcher's already pinning shortcut " + existing.getId() 228 + " for package " + existing.getPackage()); 229 return null; 230 } 231 232 // Pass a clone, not the original. 233 // Note this will remove the intent and icons. 234 shortcutForLauncher = existing.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); 235 236 // FLAG_PINNED is still set, if it's pinned by other launchers. 237 shortcutForLauncher.clearFlags(ShortcutInfo.FLAG_PINNED); 238 } else { 239 // If the shortcut has no default activity, try to set the main activity. 240 // But in the request-pin case, it's optional, so it's okay even if the caller 241 // has no default activity. 242 if (inShortcut.getActivity() == null) { 243 inShortcut.setActivity(mService.injectGetDefaultMainActivity( 244 inShortcut.getPackage(), inShortcut.getUserId())); 245 } 246 247 // It doesn't exist, so it must have all mandatory fields. 248 mService.validateShortcutForPinRequest(inShortcut); 249 250 // Initialize the ShortcutInfo for pending approval. 251 inShortcut.resolveResourceStrings(mService.injectGetResourcesForApplicationAsUser( 252 inShortcut.getPackage(), inShortcut.getUserId())); 253 if (DEBUG) { 254 Slog.d(TAG, "Resolved shortcut=" + inShortcut.toInsecureString()); 255 } 256 // We should strip out the intent, but should preserve the icon. 257 shortcutForLauncher = inShortcut.clone( 258 ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER_APPROVAL); 259 } 260 if (DEBUG) { 261 Slog.d(TAG, "Sending to launcher=" + shortcutForLauncher.toInsecureString()); 262 } 263 264 // Create a request object. 265 final PinShortcutRequestInner inner = 266 new PinShortcutRequestInner(this, inShortcut, shortcutForLauncher, resultIntent, 267 launcherPackage, launcherUserId, existsAlready); 268 269 return new PinItemRequest(shortcutForLauncher, inner); 270 } 271 272 private void validateExistingShortcut(ShortcutInfo shortcutInfo) { 273 // Make sure it's enabled. 274 // (Because we can't always force enable it automatically as it may be a stale 275 // manifest shortcut.) 276 Preconditions.checkArgument(shortcutInfo.isEnabled(), 277 "Shortcut ID=" + shortcutInfo + " already exists but disabled."); 278 279 } 280 281 private boolean startRequestConfirmActivity(ComponentName activity, int launcherUserId, 282 PinItemRequest request) { 283 // Start the activity. 284 final Intent confirmIntent = new Intent(LauncherApps.ACTION_CONFIRM_PIN_ITEM); 285 confirmIntent.setComponent(activity); 286 confirmIntent.putExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST, request); 287 confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 288 289 final long token = mService.injectClearCallingIdentity(); 290 try { 291 mService.mContext.startActivityAsUser( 292 confirmIntent, UserHandle.of(launcherUserId)); 293 } catch (RuntimeException e) { // ActivityNotFoundException, etc. 294 Log.e(TAG, "Unable to start activity " + activity, e); 295 return false; 296 } finally { 297 mService.injectRestoreCallingIdentity(token); 298 } 299 return true; 300 } 301 302 /** 303 * Find the activity that handles {@link LauncherApps#ACTION_CONFIRM_PIN_ITEM} in the 304 * default launcher. 305 */ 306 @Nullable 307 @VisibleForTesting 308 Pair<ComponentName, Integer> getRequestPinShortcutConfirmationActivity( 309 int callingUserId) { 310 // Find the default launcher. 311 final int launcherUserId = mService.getParentOrSelfUserId(callingUserId); 312 final ComponentName defaultLauncher = mService.getDefaultLauncher(launcherUserId); 313 314 if (defaultLauncher == null) { 315 Log.e(TAG, "Default launcher not found."); 316 return null; 317 } 318 final ComponentName activity = mService.injectGetPinConfirmationActivity( 319 defaultLauncher.getPackageName(), launcherUserId); 320 return (activity == null) ? null : Pair.create(activity, launcherUserId); 321 } 322 323 public void sendResultIntent(@Nullable IntentSender intent, @Nullable Intent extras) { 324 if (DEBUG) { 325 Slog.d(TAG, "Sending result intent."); 326 } 327 mService.injectSendIntentSender(intent, extras); 328 } 329 330 /** 331 * The last step of the "request pin shortcut" flow. Called when the launcher accepted a 332 * request. 333 */ 334 public boolean directPinShortcut(PinShortcutRequestInner request) { 335 336 final ShortcutInfo original = request.shortcutOriginal; 337 final int appUserId = original.getUserId(); 338 final String appPackageName = original.getPackage(); 339 final int launcherUserId = request.launcherUserId; 340 final String launcherPackage = request.launcherPackage; 341 final String shortcutId = original.getId(); 342 343 synchronized (mLock) { 344 if (!(mService.isUserUnlockedL(appUserId) 345 && mService.isUserUnlockedL(request.launcherUserId))) { 346 Log.w(TAG, "User is locked now."); 347 return false; 348 } 349 350 final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked( 351 appPackageName, appUserId); 352 final ShortcutInfo current = ps.findShortcutById(shortcutId); 353 354 // The shortcut might have been changed, so we need to do the same validation again. 355 try { 356 if (current == null) { 357 // It doesn't exist, so it must have all necessary fields. 358 mService.validateShortcutForPinRequest(original); 359 } else { 360 validateExistingShortcut(current); 361 } 362 } catch (RuntimeException e) { 363 Log.w(TAG, "Unable to pin shortcut: " + e.getMessage()); 364 return false; 365 } 366 367 // If the shortcut doesn't exist, need to create it. 368 // First, create it as a dynamic shortcut. 369 if (current == null) { 370 if (DEBUG) { 371 Slog.d(TAG, "Temporarily adding " + shortcutId + " as dynamic"); 372 } 373 // Add as a dynamic shortcut. In order for a shortcut to be dynamic, it must 374 // have a target activity, so we set a dummy here. It's later removed 375 // in deleteDynamicWithId(). 376 if (original.getActivity() == null) { 377 original.setActivity(mService.getDummyMainActivity(appPackageName)); 378 } 379 ps.addOrUpdateDynamicShortcut(original); 380 } 381 382 // Pin the shortcut. 383 if (DEBUG) { 384 Slog.d(TAG, "Pinning " + shortcutId); 385 } 386 387 final ShortcutLauncher launcher = mService.getLauncherShortcutsLocked( 388 launcherPackage, appUserId, launcherUserId); 389 launcher.attemptToRestoreIfNeededAndSave(); 390 launcher.addPinnedShortcut(appPackageName, appUserId, shortcutId); 391 392 if (current == null) { 393 if (DEBUG) { 394 Slog.d(TAG, "Removing " + shortcutId + " as dynamic"); 395 } 396 ps.deleteDynamicWithId(shortcutId); 397 } 398 399 ps.adjustRanks(); // Shouldn't be needed, but just in case. 400 } 401 402 mService.verifyStates(); 403 mService.packageShortcutsChanged(appPackageName, appUserId); 404 405 return true; 406 } 407} 408