ShortcutRequestPinProcessor.java revision 2d895c3efd625e09e9f2cc4d0c7131b34f52f154
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.content.ComponentName; 20import android.content.Intent; 21import android.content.IntentSender; 22import android.content.pm.IPinItemRequest; 23import android.content.pm.LauncherApps; 24import android.content.pm.LauncherApps.PinItemRequest; 25import android.content.pm.ShortcutInfo; 26import android.os.Bundle; 27import android.os.UserHandle; 28import android.util.Log; 29import android.util.Pair; 30import android.util.Slog; 31 32import com.android.internal.annotations.GuardedBy; 33import com.android.internal.annotations.VisibleForTesting; 34import com.android.internal.util.Preconditions; 35 36/** 37 * Handles {@link android.content.pm.ShortcutManager#requestPinShortcut} related tasks. 38 */ 39class ShortcutRequestPinProcessor { 40 private static final String TAG = ShortcutService.TAG; 41 private static final boolean DEBUG = ShortcutService.DEBUG; 42 43 private final ShortcutService mService; 44 private final Object mLock; 45 46 /** 47 * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks. 48 */ 49 private static class PinShortcutRequestInner extends IPinItemRequest.Stub { 50 private final ShortcutRequestPinProcessor mProcessor; 51 public final ShortcutInfo shortcut; 52 private final IntentSender mResultIntent; 53 54 public final String launcherPackage; 55 public final int launcherUserId; 56 public final boolean preExisting; 57 58 @GuardedBy("this") 59 private boolean mAccepted; 60 61 private PinShortcutRequestInner(ShortcutRequestPinProcessor processor, 62 ShortcutInfo shortcut, IntentSender resultIntent, 63 String launcherPackage, int launcherUserId, boolean preExisting) { 64 mProcessor = processor; 65 this.shortcut = shortcut; 66 mResultIntent = resultIntent; 67 this.launcherPackage = launcherPackage; 68 this.launcherUserId = launcherUserId; 69 this.preExisting = preExisting; 70 } 71 72 @Override 73 public boolean isValid() { 74 // TODO When an app calls requestPinShortcut(), all pending requests should be 75 // invalidated. 76 synchronized (this) { 77 return !mAccepted; 78 } 79 } 80 81 /** 82 * Called when the launcher calls {@link PinItemRequest#accept}. 83 */ 84 @Override 85 public boolean accept(Bundle options) { 86 // Make sure the options are unparcellable by the FW. (e.g. not containing unknown 87 // classes.) 88 if (options != null) { 89 try { 90 options.size(); 91 } catch (RuntimeException e) { 92 throw new IllegalArgumentException("options cannot be unparceled", e); 93 } 94 } 95 synchronized (this) { 96 if (mAccepted) { 97 throw new IllegalStateException("accept() called already"); 98 } 99 mAccepted = true; 100 } 101 if (DEBUG) { 102 Slog.d(TAG, "Launcher accepted shortcut. ID=" + shortcut.getId() 103 + " package=" + shortcut.getPackage() 104 + " options=" + options); 105 } 106 107 // Pin it and send the result intent. 108 if (mProcessor.directPinShortcut(this)) { 109 mProcessor.sendResultIntent(mResultIntent); 110 return true; 111 } else { 112 return false; 113 } 114 } 115 } 116 117 public ShortcutRequestPinProcessor(ShortcutService service, Object lock) { 118 mService = service; 119 mLock = lock; 120 } 121 122 public boolean isRequestPinnedShortcutSupported(int callingUserId) { 123 return getRequestPinShortcutConfirmationActivity(callingUserId) != null; 124 } 125 126 /** 127 * Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)}. 128 */ 129 public boolean requestPinShortcutLocked(ShortcutInfo inShortcut, IntentSender resultIntent) { 130 131 // First, make sure the launcher supports it. 132 133 // Find the confirmation activity in the default launcher. 134 final Pair<ComponentName, Integer> confirmActivity = 135 getRequestPinShortcutConfirmationActivity(inShortcut.getUserId()); 136 137 // If the launcher doesn't support it, just return a rejected result and finish. 138 if (confirmActivity == null) { 139 Log.w(TAG, "Launcher doesn't support requestPinnedShortcut(). Shortcut not created."); 140 return false; 141 } 142 143 final ComponentName launcherComponent = confirmActivity.first; 144 final String launcherPackage = confirmActivity.first.getPackageName(); 145 final int launcherUserId = confirmActivity.second; 146 147 // Make sure the launcher user is unlocked. (it's always the parent profile, so should 148 // really be unlocked here though.) 149 mService.throwIfUserLockedL(launcherUserId); 150 151 // Next, validate the incoming shortcut, etc. 152 153 final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked( 154 inShortcut.getPackage(), inShortcut.getUserId()); 155 156 final ShortcutInfo existing = ps.findShortcutById(inShortcut.getId()); 157 final boolean existsAlready = existing != null; 158 159 if (DEBUG) { 160 Slog.d(TAG, "requestPinnedShortcut package=" + inShortcut.getPackage() 161 + " existsAlready=" + existsAlready 162 + " shortcut=" + inShortcut.toInsecureString()); 163 } 164 165 // This is the shortcut that'll be sent to the launcher. 166 final ShortcutInfo shortcutToSend; 167 168 if (existsAlready) { 169 validateExistingShortcut(existing); 170 171 // See if it's already pinned. 172 if (mService.getLauncherShortcutsLocked( 173 launcherPackage, existing.getUserId(), launcherUserId).hasPinned(existing)) { 174 Log.i(TAG, "Launcher's already pinning shortcut " + existing.getId() 175 + " for package " + existing.getPackage()); 176 sendResultIntent(resultIntent); 177 return true; 178 } 179 180 // Pass a clone, not the original. 181 // Note this will remove the intent and icons. 182 shortcutToSend = existing.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER); 183 shortcutToSend.clearFlags(ShortcutInfo.FLAG_PINNED); 184 } else { 185 // It doesn't exist, so it must have all mandatory fields. 186 mService.validateShortcutForPinRequest(inShortcut); 187 188 // Initialize the ShortcutInfo for pending approval. 189 inShortcut.resolveResourceStrings(mService.injectGetResourcesForApplicationAsUser( 190 inShortcut.getPackage(), inShortcut.getUserId())); 191 if (DEBUG) { 192 Slog.d(TAG, "resolved shortcut=" + inShortcut.toInsecureString()); 193 } 194 // TODO Remove the intent here -- don't pass shortcut intents to the launcher. 195 shortcutToSend = inShortcut; 196 } 197 198 // Create a request object. 199 final PinShortcutRequestInner inner = 200 new PinShortcutRequestInner(this, shortcutToSend, resultIntent, 201 launcherPackage, launcherUserId, existsAlready); 202 203 final PinItemRequest outer = new PinItemRequest(PinItemRequest.REQUEST_TYPE_SHORTCUT, 204 shortcutToSend, inner); 205 206 return startRequestConfirmActivity(launcherComponent, launcherUserId, outer); 207 } 208 209 private void validateExistingShortcut(ShortcutInfo shortcutInfo) { 210 // Make sure it's enabled. 211 // (Because we can't always force enable it automatically as it may be a stale 212 // manifest shortcut.) 213 Preconditions.checkState(shortcutInfo.isEnabled(), 214 "Shortcut ID=" + shortcutInfo + " already exists but disabled."); 215 216 } 217 218 private boolean startRequestConfirmActivity(ComponentName activity, int launcherUserId, 219 PinItemRequest request) { 220 // Start the activity. 221 final Intent confirmIntent = new Intent(LauncherApps.ACTION_CONFIRM_PIN_ITEM); 222 confirmIntent.setComponent(activity); 223 confirmIntent.putExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST, request); 224 confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 225 226 final long token = mService.injectClearCallingIdentity(); 227 try { 228 mService.mContext.startActivityAsUser( 229 confirmIntent, UserHandle.of(launcherUserId)); 230 } catch (RuntimeException e) { // ActivityNotFoundException, etc. 231 Log.e(TAG, "Unable to start activity " + activity, e); 232 return false; 233 } finally { 234 mService.injectRestoreCallingIdentity(token); 235 } 236 return true; 237 } 238 239 /** 240 * Find the activity that handles {@link LauncherApps#ACTION_CONFIRM_PIN_ITEM} in the 241 * default launcher. 242 */ 243 @Nullable 244 @VisibleForTesting 245 Pair<ComponentName, Integer> getRequestPinShortcutConfirmationActivity( 246 int callingUserId) { 247 // Find the default launcher. 248 final int launcherUserId = mService.getParentOrSelfUserId(callingUserId); 249 final ComponentName defaultLauncher = mService.getDefaultLauncher(launcherUserId); 250 251 if (defaultLauncher == null) { 252 Log.e(TAG, "Default launcher not found."); 253 return null; 254 } 255 final ComponentName activity = mService.injectGetPinConfirmationActivity( 256 defaultLauncher.getPackageName(), launcherUserId); 257 return (activity == null) ? null : Pair.create(activity, launcherUserId); 258 } 259 260 public void sendResultIntent(@Nullable IntentSender intent) { 261 if (DEBUG) { 262 Slog.d(TAG, "Sending result intent."); 263 } 264 mService.injectSendIntentSender(intent); 265 } 266 267 /** 268 * The last step of the "request pin shortcut" flow. Called when the launcher accepted a 269 * request. 270 */ 271 public boolean directPinShortcut(PinShortcutRequestInner request) { 272 273 final ShortcutInfo original = request.shortcut; 274 final int appUserId = original.getUserId(); 275 final String appPackageName = original.getPackage(); 276 final int launcherUserId = request.launcherUserId; 277 final String launcherPackage = request.launcherPackage; 278 final String shortcutId = original.getId(); 279 280 synchronized (mLock) { 281 if (!(mService.isUserUnlockedL(appUserId) 282 && mService.isUserUnlockedL(request.launcherUserId))) { 283 Log.w(TAG, "User is locked now."); 284 return false; 285 } 286 287 final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked( 288 appPackageName, appUserId); 289 final ShortcutInfo current = ps.findShortcutById(shortcutId); 290 291 // The shortcut might have been changed, so we need to do the same validation again. 292 try { 293 if (current == null) { 294 // It doesn't exist, so it must have all necessary fields. 295 mService.validateShortcutForPinRequest(request.shortcut); 296 } else { 297 validateExistingShortcut(current); 298 } 299 } catch (RuntimeException e) { 300 Log.w(TAG, "Unable to pin shortcut: " + e.getMessage()); 301 return false; 302 } 303 304 // If the shortcut doesn't exist, need to create it. 305 // First, create it as a dynamic shortcut. 306 if (current == null) { 307 if (DEBUG) { 308 Slog.d(TAG, "Temporarily adding " + shortcutId + " as dynamic"); 309 } 310 // Add as a dynamic shortcut. 311 if (original.getActivity() == null) { 312 original.setActivity(mService.getDummyMainActivity(appPackageName)); 313 } 314 ps.addOrUpdateDynamicShortcut(original); 315 } 316 317 // Pin the shortcut. 318 if (DEBUG) { 319 Slog.d(TAG, "Pinning " + shortcutId); 320 } 321 322 final ShortcutLauncher launcher = mService.getLauncherShortcutsLocked( 323 launcherPackage, appUserId, launcherUserId); 324 launcher.attemptToRestoreIfNeededAndSave(); 325 launcher.addPinnedShortcut(appPackageName, appUserId, shortcutId); 326 327 if (current == null) { 328 if (DEBUG) { 329 Slog.d(TAG, "Removing " + shortcutId + " as dynamic"); 330 } 331 ps.deleteDynamicWithId(shortcutId); 332 } 333 334 ps.adjustRanks(); // Shouldn't be needed, but just in case. 335 } 336 337 mService.verifyStates(); 338 mService.packageShortcutsChanged(appPackageName, appUserId); 339 340 return true; 341 } 342} 343