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