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