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 60 private PinItemRequestInner(ShortcutRequestPinProcessor processor, 61 IntentSender resultIntent, int launcherUid) { 62 mProcessor = processor; 63 mResultIntent = resultIntent; 64 mLauncherUid = launcherUid; 65 } 66 67 @Override 68 public ShortcutInfo getShortcutInfo() { 69 return null; 70 } 71 72 @Override 73 public AppWidgetProviderInfo getAppWidgetProviderInfo() { 74 return null; 75 } 76 77 @Override 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 */ 86 private boolean isCallerValid() { 87 return mProcessor.isCallerUid(mLauncherUid); 88 } 89 90 @Override 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 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 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 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 159 public AppWidgetProviderInfo getAppWidgetProviderInfo() { 160 return mAppWidgetProviderInfo; 161 } 162 163 @Override 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 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 199 public ShortcutInfo getShortcutInfo() { 200 return shortcutForLauncher; 201 } 202 203 @Override 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 213 public ShortcutRequestPinProcessor(ShortcutService service, Object lock) { 214 mService = service; 215 mLock = lock; 216 } 217 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 */ 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 */ 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 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 304 if (DEBUG) { 305 Slog.d(TAG, "requestPinnedShortcut: package=" + inShortcut.getPackage() 306 + " existsAlready=" + existsAlready 307 + " shortcut=" + inShortcut.toInsecureString()); 308 } 309 310 // This is the shortcut that'll be sent to the launcher. 311 final ShortcutInfo shortcutForLauncher; 312 final String launcherPackage = confirmActivity.first.getPackageName(); 313 final int launcherUserId = confirmActivity.second; 314 315 IntentSender resultIntentToSend = resultIntentOriginal; 316 317 if (existsAlready) { 318 validateExistingShortcut(existing); 319 320 final boolean isAlreadyPinned = mService.getLauncherShortcutsLocked( 321 launcherPackage, existing.getUserId(), launcherUserId).hasPinned(existing); 322 if (isAlreadyPinned) { 323 // When the shortcut is already pinned by this launcher, the request will always 324 // succeed, so just send the result at this point. 325 sendResultIntent(resultIntentOriginal, null); 326 327 // So, do not send the intent again. 328 resultIntentToSend = null; 329 } 330 331 // Pass a clone, not the original. 332 // Note this will remove the intent and icons. 333 shortcutForLauncher = existing.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); 334 335 if (!isAlreadyPinned) { 336 // FLAG_PINNED may still be set, if it's pinned by other launchers. 337 shortcutForLauncher.clearFlags(ShortcutInfo.FLAG_PINNED); 338 } 339 } else { 340 // If the shortcut has no default activity, try to set the main activity. 341 // But in the request-pin case, it's optional, so it's okay even if the caller 342 // has no default activity. 343 if (inShortcut.getActivity() == null) { 344 inShortcut.setActivity(mService.injectGetDefaultMainActivity( 345 inShortcut.getPackage(), inShortcut.getUserId())); 346 } 347 348 // It doesn't exist, so it must have all mandatory fields. 349 mService.validateShortcutForPinRequest(inShortcut); 350 351 // Initialize the ShortcutInfo for pending approval. 352 inShortcut.resolveResourceStrings(mService.injectGetResourcesForApplicationAsUser( 353 inShortcut.getPackage(), inShortcut.getUserId())); 354 if (DEBUG) { 355 Slog.d(TAG, "Resolved shortcut=" + inShortcut.toInsecureString()); 356 } 357 // We should strip out the intent, but should preserve the icon. 358 shortcutForLauncher = inShortcut.clone( 359 ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER_APPROVAL); 360 } 361 if (DEBUG) { 362 Slog.d(TAG, "Sending to launcher=" + shortcutForLauncher.toInsecureString()); 363 } 364 365 // Create a request object. 366 final PinShortcutRequestInner inner = 367 new PinShortcutRequestInner(this, inShortcut, shortcutForLauncher, 368 resultIntentToSend, launcherPackage, launcherUserId, 369 mService.injectGetPackageUid(launcherPackage, launcherUserId), 370 existsAlready); 371 372 return new PinItemRequest(inner, PinItemRequest.REQUEST_TYPE_SHORTCUT); 373 } 374 375 private void validateExistingShortcut(ShortcutInfo shortcutInfo) { 376 // Make sure it's enabled. 377 // (Because we can't always force enable it automatically as it may be a stale 378 // manifest shortcut.) 379 Preconditions.checkArgument(shortcutInfo.isEnabled(), 380 "Shortcut ID=" + shortcutInfo + " already exists but disabled."); 381 382 } 383 384 private boolean startRequestConfirmActivity(ComponentName activity, int launcherUserId, 385 PinItemRequest request, int requestType) { 386 final String action = requestType == LauncherApps.PinItemRequest.REQUEST_TYPE_SHORTCUT ? 387 LauncherApps.ACTION_CONFIRM_PIN_SHORTCUT : 388 LauncherApps.ACTION_CONFIRM_PIN_APPWIDGET; 389 390 // Start the activity. 391 final Intent confirmIntent = new Intent(action); 392 confirmIntent.setComponent(activity); 393 confirmIntent.putExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST, request); 394 confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 395 396 final long token = mService.injectClearCallingIdentity(); 397 try { 398 mService.mContext.startActivityAsUser( 399 confirmIntent, UserHandle.of(launcherUserId)); 400 } catch (RuntimeException e) { // ActivityNotFoundException, etc. 401 Log.e(TAG, "Unable to start activity " + activity, e); 402 return false; 403 } finally { 404 mService.injectRestoreCallingIdentity(token); 405 } 406 return true; 407 } 408 409 /** 410 * Find the activity that handles {@link LauncherApps#ACTION_CONFIRM_PIN_SHORTCUT} in the 411 * default launcher. 412 */ 413 @Nullable 414 @VisibleForTesting 415 Pair<ComponentName, Integer> getRequestPinConfirmationActivity( 416 int callingUserId, int requestType) { 417 // Find the default launcher. 418 final int launcherUserId = mService.getParentOrSelfUserId(callingUserId); 419 final ComponentName defaultLauncher = mService.getDefaultLauncher(launcherUserId); 420 421 if (defaultLauncher == null) { 422 Log.e(TAG, "Default launcher not found."); 423 return null; 424 } 425 final ComponentName activity = mService.injectGetPinConfirmationActivity( 426 defaultLauncher.getPackageName(), launcherUserId, requestType); 427 return (activity == null) ? null : Pair.create(activity, launcherUserId); 428 } 429 430 public void sendResultIntent(@Nullable IntentSender intent, @Nullable Intent extras) { 431 if (DEBUG) { 432 Slog.d(TAG, "Sending result intent."); 433 } 434 mService.injectSendIntentSender(intent, extras); 435 } 436 437 public boolean isCallerUid(int uid) { 438 return uid == mService.injectBinderCallingUid(); 439 } 440 441 /** 442 * The last step of the "request pin shortcut" flow. Called when the launcher accepted a 443 * request. 444 */ 445 public boolean directPinShortcut(PinShortcutRequestInner request) { 446 447 final ShortcutInfo original = request.shortcutOriginal; 448 final int appUserId = original.getUserId(); 449 final String appPackageName = original.getPackage(); 450 final int launcherUserId = request.launcherUserId; 451 final String launcherPackage = request.launcherPackage; 452 final String shortcutId = original.getId(); 453 454 synchronized (mLock) { 455 if (!(mService.isUserUnlockedL(appUserId) 456 && mService.isUserUnlockedL(request.launcherUserId))) { 457 Log.w(TAG, "User is locked now."); 458 return false; 459 } 460 461 final ShortcutLauncher launcher = mService.getLauncherShortcutsLocked( 462 launcherPackage, appUserId, launcherUserId); 463 launcher.attemptToRestoreIfNeededAndSave(); 464 if (launcher.hasPinned(original)) { 465 if (DEBUG) { 466 Slog.d(TAG, "Shortcut " + original + " already pinned."); 467 } 468 return true; 469 } 470 471 final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked( 472 appPackageName, appUserId); 473 final ShortcutInfo current = ps.findShortcutById(shortcutId); 474 475 // The shortcut might have been changed, so we need to do the same validation again. 476 try { 477 if (current == null) { 478 // It doesn't exist, so it must have all necessary fields. 479 mService.validateShortcutForPinRequest(original); 480 } else { 481 validateExistingShortcut(current); 482 } 483 } catch (RuntimeException e) { 484 Log.w(TAG, "Unable to pin shortcut: " + e.getMessage()); 485 return false; 486 } 487 488 // If the shortcut doesn't exist, need to create it. 489 // First, create it as a dynamic shortcut. 490 if (current == null) { 491 if (DEBUG) { 492 Slog.d(TAG, "Temporarily adding " + shortcutId + " as dynamic"); 493 } 494 // Add as a dynamic shortcut. In order for a shortcut to be dynamic, it must 495 // have a target activity, so we set a dummy here. It's later removed 496 // in deleteDynamicWithId(). 497 if (original.getActivity() == null) { 498 original.setActivity(mService.getDummyMainActivity(appPackageName)); 499 } 500 ps.addOrUpdateDynamicShortcut(original); 501 } 502 503 // Pin the shortcut. 504 if (DEBUG) { 505 Slog.d(TAG, "Pinning " + shortcutId); 506 } 507 508 launcher.addPinnedShortcut(appPackageName, appUserId, shortcutId); 509 510 if (current == null) { 511 if (DEBUG) { 512 Slog.d(TAG, "Removing " + shortcutId + " as dynamic"); 513 } 514 ps.deleteDynamicWithId(shortcutId); 515 } 516 517 ps.adjustRanks(); // Shouldn't be needed, but just in case. 518 } 519 520 mService.verifyStates(); 521 mService.packageShortcutsChanged(appPackageName, appUserId); 522 523 return true; 524 } 525 } 526