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 
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 
validateExistingShortcut(ShortcutInfo shortcutInfo)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 
startRequestConfirmActivity(ComponentName activity, int launcherUserId, PinItemRequest request, int requestType)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
getRequestPinConfirmationActivity( int callingUserId, int requestType)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 
sendResultIntent(@ullable IntentSender intent, @Nullable Intent extras)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 
isCallerUid(int uid)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      */
directPinShortcut(PinShortcutRequestInner request)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