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.NonNull;
19import android.annotation.Nullable;
20import android.appwidget.AppWidgetProviderInfo;
21import android.content.ComponentName;
22import android.content.Intent;
23import android.content.IntentSender;
24import android.content.pm.IPinItemRequest;
25import android.content.pm.LauncherApps;
26import android.content.pm.LauncherApps.PinItemRequest;
27import android.content.pm.ShortcutInfo;
28import android.os.Bundle;
29import android.os.RemoteException;
30import android.os.UserHandle;
31import android.util.Log;
32import android.util.Pair;
33import android.util.Slog;
34
35import com.android.internal.annotations.GuardedBy;
36import com.android.internal.annotations.VisibleForTesting;
37import com.android.internal.util.Preconditions;
38
39/**
40 * Handles {@link android.content.pm.ShortcutManager#requestPinShortcut} related tasks.
41 */
42class 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