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 */ 16 package com.android.server.pm; 17 18 import android.annotation.NonNull; 19 import android.annotation.Nullable; 20 import android.appwidget.AppWidgetProviderInfo; 21 import android.content.ComponentName; 22 import android.content.Intent; 23 import android.content.IntentSender; 24 import android.content.pm.IPinItemRequest; 25 import android.content.pm.LauncherApps; 26 import android.content.pm.LauncherApps.PinItemRequest; 27 import android.content.pm.ShortcutInfo; 28 import android.os.Bundle; 29 import android.os.RemoteException; 30 import android.os.UserHandle; 31 import android.util.Log; 32 import android.util.Pair; 33 import android.util.Slog; 34 35 import com.android.internal.annotations.GuardedBy; 36 import com.android.internal.annotations.VisibleForTesting; 37 import com.android.internal.util.Preconditions; 38 39 /** 40 * Handles {@link android.content.pm.ShortcutManager#requestPinShortcut} related tasks. 41 */ 42 class ShortcutRequestPinProcessor { 43 private static final String TAG = ShortcutService.TAG; 44 private static final boolean DEBUG = ShortcutService.DEBUG; 45 46 private final ShortcutService mService; 47 private final Object mLock; 48 49 /** 50 * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks. 51 */ 52 private abstract static class PinItemRequestInner extends IPinItemRequest.Stub { 53 protected final ShortcutRequestPinProcessor mProcessor; 54 private final IntentSender mResultIntent; 55 private final int mLauncherUid; 56 57 @GuardedBy("this") 58 private boolean mAccepted; 59 PinItemRequestInner(ShortcutRequestPinProcessor processor, IntentSender resultIntent, int launcherUid)60 private PinItemRequestInner(ShortcutRequestPinProcessor processor, 61 IntentSender resultIntent, int launcherUid) { 62 mProcessor = processor; 63 mResultIntent = resultIntent; 64 mLauncherUid = launcherUid; 65 } 66 67 @Override getShortcutInfo()68 public ShortcutInfo getShortcutInfo() { 69 return null; 70 } 71 72 @Override getAppWidgetProviderInfo()73 public AppWidgetProviderInfo getAppWidgetProviderInfo() { 74 return null; 75 } 76 77 @Override getExtras()78 public Bundle getExtras() { 79 return null; 80 } 81 82 /** 83 * Returns true if the caller is same as the default launcher app when this request 84 * object was created. 85 */ isCallerValid()86 private boolean isCallerValid() { 87 return mProcessor.isCallerUid(mLauncherUid); 88 } 89 90 @Override isValid()91 public boolean isValid() { 92 if (!isCallerValid()) { 93 return false; 94 } 95 // TODO When an app calls requestPinShortcut(), all pending requests should be 96 // invalidated. 97 synchronized (this) { 98 return !mAccepted; 99 } 100 } 101 102 /** 103 * Called when the launcher calls {@link PinItemRequest#accept}. 104 */ 105 @Override accept(Bundle options)106 public boolean accept(Bundle options) { 107 // Make sure the options are unparcellable by the FW. (e.g. not containing unknown 108 // classes.) 109 if (!isCallerValid()) { 110 throw new SecurityException("Calling uid mismatch"); 111 } 112 Intent extras = null; 113 if (options != null) { 114 try { 115 options.size(); 116 extras = new Intent().putExtras(options); 117 } catch (RuntimeException e) { 118 throw new IllegalArgumentException("options cannot be unparceled", e); 119 } 120 } 121 synchronized (this) { 122 if (mAccepted) { 123 throw new IllegalStateException("accept() called already"); 124 } 125 mAccepted = true; 126 } 127 128 // Pin it and send the result intent. 129 if (tryAccept()) { 130 mProcessor.sendResultIntent(mResultIntent, extras); 131 return true; 132 } else { 133 return false; 134 } 135 } 136 tryAccept()137 protected boolean tryAccept() { 138 return true; 139 } 140 } 141 142 /** 143 * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks. 144 */ 145 private static class PinAppWidgetRequestInner extends PinItemRequestInner { 146 final AppWidgetProviderInfo mAppWidgetProviderInfo; 147 final Bundle mExtras; 148 PinAppWidgetRequestInner(ShortcutRequestPinProcessor processor, IntentSender resultIntent, int launcherUid, AppWidgetProviderInfo appWidgetProviderInfo, Bundle extras)149 private PinAppWidgetRequestInner(ShortcutRequestPinProcessor processor, 150 IntentSender resultIntent, int launcherUid, 151 AppWidgetProviderInfo appWidgetProviderInfo, Bundle extras) { 152 super(processor, resultIntent, launcherUid); 153 154 mAppWidgetProviderInfo = appWidgetProviderInfo; 155 mExtras = extras; 156 } 157 158 @Override getAppWidgetProviderInfo()159 public AppWidgetProviderInfo getAppWidgetProviderInfo() { 160 return mAppWidgetProviderInfo; 161 } 162 163 @Override getExtras()164 public Bundle getExtras() { 165 return mExtras; 166 } 167 } 168 169 /** 170 * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks. 171 */ 172 private static class PinShortcutRequestInner extends PinItemRequestInner { 173 /** Original shortcut passed by the app. */ 174 public final ShortcutInfo shortcutOriginal; 175 176 /** 177 * Cloned shortcut that's passed to the launcher. The notable difference from 178 * {@link #shortcutOriginal} is it must not have the intent. 179 */ 180 public final ShortcutInfo shortcutForLauncher; 181 182 public final String launcherPackage; 183 public final int launcherUserId; 184 public final boolean preExisting; 185 PinShortcutRequestInner(ShortcutRequestPinProcessor processor, ShortcutInfo shortcutOriginal, ShortcutInfo shortcutForLauncher, IntentSender resultIntent, String launcherPackage, int launcherUserId, int launcherUid, boolean preExisting)186 private PinShortcutRequestInner(ShortcutRequestPinProcessor processor, 187 ShortcutInfo shortcutOriginal, ShortcutInfo shortcutForLauncher, 188 IntentSender resultIntent, 189 String launcherPackage, int launcherUserId, int launcherUid, boolean preExisting) { 190 super(processor, resultIntent, launcherUid); 191 this.shortcutOriginal = shortcutOriginal; 192 this.shortcutForLauncher = shortcutForLauncher; 193 this.launcherPackage = launcherPackage; 194 this.launcherUserId = launcherUserId; 195 this.preExisting = preExisting; 196 } 197 198 @Override getShortcutInfo()199 public ShortcutInfo getShortcutInfo() { 200 return shortcutForLauncher; 201 } 202 203 @Override tryAccept()204 protected boolean tryAccept() { 205 if (DEBUG) { 206 Slog.d(TAG, "Launcher accepted shortcut. ID=" + shortcutOriginal.getId() 207 + " package=" + shortcutOriginal.getPackage()); 208 } 209 return mProcessor.directPinShortcut(this); 210 } 211 } 212 ShortcutRequestPinProcessor(ShortcutService service, Object lock)213 public ShortcutRequestPinProcessor(ShortcutService service, Object lock) { 214 mService = service; 215 mLock = lock; 216 } 217 isRequestPinItemSupported(int callingUserId, int requestType)218 public boolean isRequestPinItemSupported(int callingUserId, int requestType) { 219 return getRequestPinConfirmationActivity(callingUserId, requestType) != null; 220 } 221 222 /** 223 * Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)} and 224 * {@link android.appwidget.AppWidgetManager#requestPinAppWidget}. 225 * In this flow the PinItemRequest is delivered directly to the default launcher app. 226 * One of {@param inShortcut} and {@param inAppWidget} is always non-null and the other is 227 * always null. 228 */ requestPinItemLocked(ShortcutInfo inShortcut, AppWidgetProviderInfo inAppWidget, Bundle extras, int userId, IntentSender resultIntent)229 public boolean requestPinItemLocked(ShortcutInfo inShortcut, AppWidgetProviderInfo inAppWidget, 230 Bundle extras, int userId, IntentSender resultIntent) { 231 232 // First, make sure the launcher supports it. 233 234 // Find the confirmation activity in the default launcher. 235 final int requestType = inShortcut != null ? 236 PinItemRequest.REQUEST_TYPE_SHORTCUT : PinItemRequest.REQUEST_TYPE_APPWIDGET; 237 final Pair<ComponentName, Integer> confirmActivity = 238 getRequestPinConfirmationActivity(userId, requestType); 239 240 // If the launcher doesn't support it, just return a rejected result and finish. 241 if (confirmActivity == null) { 242 Log.w(TAG, "Launcher doesn't support requestPinnedShortcut(). Shortcut not created."); 243 return false; 244 } 245 246 final int launcherUserId = confirmActivity.second; 247 248 // Make sure the launcher user is unlocked. (it's always the parent profile, so should 249 // really be unlocked here though.) 250 mService.throwIfUserLockedL(launcherUserId); 251 252 // Next, validate the incoming shortcut, etc. 253 final PinItemRequest request; 254 if (inShortcut != null) { 255 request = requestPinShortcutLocked(inShortcut, resultIntent, confirmActivity); 256 } else { 257 int launcherUid = mService.injectGetPackageUid( 258 confirmActivity.first.getPackageName(), launcherUserId); 259 request = new PinItemRequest( 260 new PinAppWidgetRequestInner(this, resultIntent, launcherUid, inAppWidget, 261 extras), 262 PinItemRequest.REQUEST_TYPE_APPWIDGET); 263 } 264 return startRequestConfirmActivity(confirmActivity.first, launcherUserId, request, 265 requestType); 266 } 267 268 /** 269 * Handle {@link android.content.pm.ShortcutManager#createShortcutResultIntent(ShortcutInfo)}. 270 * In this flow the PinItemRequest is delivered to the caller app. Its the app's responsibility 271 * to send it to the Launcher app (via {@link android.app.Activity#setResult(int, Intent)}). 272 */ createShortcutResultIntent(@onNull ShortcutInfo inShortcut, int userId)273 public Intent createShortcutResultIntent(@NonNull ShortcutInfo inShortcut, int userId) { 274 // Find the default launcher activity 275 final int launcherUserId = mService.getParentOrSelfUserId(userId); 276 final ComponentName defaultLauncher = mService.getDefaultLauncher(launcherUserId); 277 if (defaultLauncher == null) { 278 Log.e(TAG, "Default launcher not found."); 279 return null; 280 } 281 282 // Make sure the launcher user is unlocked. (it's always the parent profile, so should 283 // really be unlocked here though.) 284 mService.throwIfUserLockedL(launcherUserId); 285 286 // Next, validate the incoming shortcut, etc. 287 final PinItemRequest request = requestPinShortcutLocked(inShortcut, null, 288 Pair.create(defaultLauncher, launcherUserId)); 289 return new Intent().putExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST, request); 290 } 291 292 /** 293 * Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)}. 294 */ 295 @NonNull requestPinShortcutLocked(ShortcutInfo inShortcut, IntentSender resultIntentOriginal, Pair<ComponentName, Integer> confirmActivity)296 private PinItemRequest requestPinShortcutLocked(ShortcutInfo inShortcut, 297 IntentSender resultIntentOriginal, Pair<ComponentName, Integer> confirmActivity) { 298 final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked( 299 inShortcut.getPackage(), inShortcut.getUserId()); 300 301 final ShortcutInfo existing = ps.findShortcutById(inShortcut.getId()); 302 final boolean existsAlready = existing != null; 303 final boolean existingIsVisible = existsAlready && existing.isVisibleToPublisher(); 304 305 if (DEBUG) { 306 Slog.d(TAG, "requestPinnedShortcut: package=" + inShortcut.getPackage() 307 + " existsAlready=" + existsAlready 308 + " existingIsVisible=" + existingIsVisible 309 + " shortcut=" + inShortcut.toInsecureString()); 310 } 311 312 // This is the shortcut that'll be sent to the launcher. 313 final ShortcutInfo shortcutForLauncher; 314 final String launcherPackage = confirmActivity.first.getPackageName(); 315 final int launcherUserId = confirmActivity.second; 316 317 IntentSender resultIntentToSend = resultIntentOriginal; 318 319 if (existsAlready) { 320 validateExistingShortcut(existing); 321 322 final boolean isAlreadyPinned = mService.getLauncherShortcutsLocked( 323 launcherPackage, existing.getUserId(), launcherUserId).hasPinned(existing); 324 if (isAlreadyPinned) { 325 // When the shortcut is already pinned by this launcher, the request will always 326 // succeed, so just send the result at this point. 327 sendResultIntent(resultIntentOriginal, null); 328 329 // So, do not send the intent again. 330 resultIntentToSend = null; 331 } 332 333 // Pass a clone, not the original. 334 // Note this will remove the intent and icons. 335 shortcutForLauncher = existing.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); 336 337 if (!isAlreadyPinned) { 338 // FLAG_PINNED may still be set, if it's pinned by other launchers. 339 shortcutForLauncher.clearFlags(ShortcutInfo.FLAG_PINNED); 340 } 341 } else { 342 // If the shortcut has no default activity, try to set the main activity. 343 // But in the request-pin case, it's optional, so it's okay even if the caller 344 // has no default activity. 345 if (inShortcut.getActivity() == null) { 346 inShortcut.setActivity(mService.injectGetDefaultMainActivity( 347 inShortcut.getPackage(), inShortcut.getUserId())); 348 } 349 350 // It doesn't exist, so it must have all mandatory fields. 351 mService.validateShortcutForPinRequest(inShortcut); 352 353 // Initialize the ShortcutInfo for pending approval. 354 inShortcut.resolveResourceStrings(mService.injectGetResourcesForApplicationAsUser( 355 inShortcut.getPackage(), inShortcut.getUserId())); 356 if (DEBUG) { 357 Slog.d(TAG, "Resolved shortcut=" + inShortcut.toInsecureString()); 358 } 359 // We should strip out the intent, but should preserve the icon. 360 shortcutForLauncher = inShortcut.clone( 361 ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER_APPROVAL); 362 } 363 if (DEBUG) { 364 Slog.d(TAG, "Sending to launcher=" + shortcutForLauncher.toInsecureString()); 365 } 366 367 // Create a request object. 368 final PinShortcutRequestInner inner = 369 new PinShortcutRequestInner(this, inShortcut, shortcutForLauncher, 370 resultIntentToSend, launcherPackage, launcherUserId, 371 mService.injectGetPackageUid(launcherPackage, launcherUserId), 372 existsAlready); 373 374 return new PinItemRequest(inner, PinItemRequest.REQUEST_TYPE_SHORTCUT); 375 } 376 validateExistingShortcut(ShortcutInfo shortcutInfo)377 private void validateExistingShortcut(ShortcutInfo shortcutInfo) { 378 // Make sure it's enabled. 379 // (Because we can't always force enable it automatically as it may be a stale 380 // manifest shortcut.) 381 Preconditions.checkArgument(shortcutInfo.isEnabled(), 382 "Shortcut ID=" + shortcutInfo + " already exists but disabled."); 383 } 384 startRequestConfirmActivity(ComponentName activity, int launcherUserId, PinItemRequest request, int requestType)385 private boolean startRequestConfirmActivity(ComponentName activity, int launcherUserId, 386 PinItemRequest request, int requestType) { 387 final String action = requestType == LauncherApps.PinItemRequest.REQUEST_TYPE_SHORTCUT ? 388 LauncherApps.ACTION_CONFIRM_PIN_SHORTCUT : 389 LauncherApps.ACTION_CONFIRM_PIN_APPWIDGET; 390 391 // Start the activity. 392 final Intent confirmIntent = new Intent(action); 393 confirmIntent.setComponent(activity); 394 confirmIntent.putExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST, request); 395 confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 396 397 final long token = mService.injectClearCallingIdentity(); 398 try { 399 mService.mContext.startActivityAsUser( 400 confirmIntent, UserHandle.of(launcherUserId)); 401 } catch (RuntimeException e) { // ActivityNotFoundException, etc. 402 Log.e(TAG, "Unable to start activity " + activity, e); 403 return false; 404 } finally { 405 mService.injectRestoreCallingIdentity(token); 406 } 407 return true; 408 } 409 410 /** 411 * Find the activity that handles {@link LauncherApps#ACTION_CONFIRM_PIN_SHORTCUT} in the 412 * default launcher. 413 */ 414 @Nullable 415 @VisibleForTesting getRequestPinConfirmationActivity( int callingUserId, int requestType)416 Pair<ComponentName, Integer> getRequestPinConfirmationActivity( 417 int callingUserId, int requestType) { 418 // Find the default launcher. 419 final int launcherUserId = mService.getParentOrSelfUserId(callingUserId); 420 final ComponentName defaultLauncher = mService.getDefaultLauncher(launcherUserId); 421 422 if (defaultLauncher == null) { 423 Log.e(TAG, "Default launcher not found."); 424 return null; 425 } 426 final ComponentName activity = mService.injectGetPinConfirmationActivity( 427 defaultLauncher.getPackageName(), launcherUserId, requestType); 428 return (activity == null) ? null : Pair.create(activity, launcherUserId); 429 } 430 sendResultIntent(@ullable IntentSender intent, @Nullable Intent extras)431 public void sendResultIntent(@Nullable IntentSender intent, @Nullable Intent extras) { 432 if (DEBUG) { 433 Slog.d(TAG, "Sending result intent."); 434 } 435 mService.injectSendIntentSender(intent, extras); 436 } 437 isCallerUid(int uid)438 public boolean isCallerUid(int uid) { 439 return uid == mService.injectBinderCallingUid(); 440 } 441 442 /** 443 * The last step of the "request pin shortcut" flow. Called when the launcher accepted a 444 * request. 445 */ directPinShortcut(PinShortcutRequestInner request)446 public boolean directPinShortcut(PinShortcutRequestInner request) { 447 448 final ShortcutInfo original = request.shortcutOriginal; 449 final int appUserId = original.getUserId(); 450 final String appPackageName = original.getPackage(); 451 final int launcherUserId = request.launcherUserId; 452 final String launcherPackage = request.launcherPackage; 453 final String shortcutId = original.getId(); 454 455 synchronized (mLock) { 456 if (!(mService.isUserUnlockedL(appUserId) 457 && mService.isUserUnlockedL(request.launcherUserId))) { 458 Log.w(TAG, "User is locked now."); 459 return false; 460 } 461 462 final ShortcutLauncher launcher = mService.getLauncherShortcutsLocked( 463 launcherPackage, appUserId, launcherUserId); 464 launcher.attemptToRestoreIfNeededAndSave(); 465 if (launcher.hasPinned(original)) { 466 if (DEBUG) { 467 Slog.d(TAG, "Shortcut " + original + " already pinned."); // This too. 468 } 469 return true; 470 } 471 472 final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked( 473 appPackageName, appUserId); 474 final ShortcutInfo current = ps.findShortcutById(shortcutId); 475 476 // The shortcut might have been changed, so we need to do the same validation again. 477 try { 478 if (current == null) { 479 // It doesn't exist, so it must have all necessary fields. 480 mService.validateShortcutForPinRequest(original); 481 } else { 482 validateExistingShortcut(current); 483 } 484 } catch (RuntimeException e) { 485 Log.w(TAG, "Unable to pin shortcut: " + e.getMessage()); 486 return false; 487 } 488 489 // If the shortcut doesn't exist, need to create it. 490 // First, create it as a dynamic shortcut. 491 if (current == null) { 492 if (DEBUG) { 493 Slog.d(TAG, "Temporarily adding " + shortcutId + " as dynamic"); 494 } 495 // Add as a dynamic shortcut. In order for a shortcut to be dynamic, it must 496 // have a target activity, so we set a dummy here. It's later removed 497 // in deleteDynamicWithId(). 498 if (original.getActivity() == null) { 499 original.setActivity(mService.getDummyMainActivity(appPackageName)); 500 } 501 ps.addOrReplaceDynamicShortcut(original); 502 } 503 504 // Pin the shortcut. 505 if (DEBUG) { 506 Slog.d(TAG, "Pinning " + shortcutId); 507 } 508 509 launcher.addPinnedShortcut(appPackageName, appUserId, shortcutId, 510 /*forPinRequest=*/ true); 511 512 if (current == null) { 513 if (DEBUG) { 514 Slog.d(TAG, "Removing " + shortcutId + " as dynamic"); 515 } 516 ps.deleteDynamicWithId(shortcutId, /*ignoreInvisible=*/ false); 517 } 518 519 ps.adjustRanks(); // Shouldn't be needed, but just in case. 520 } 521 522 mService.verifyStates(); 523 mService.packageShortcutsChanged(appPackageName, appUserId); 524 525 return true; 526 } 527 } 528