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        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
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
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
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
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
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     */
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