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