ShortcutRequestPinProcessor.java revision 255461f676f09db9be3876c156d571f4e08981ee
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.Nullable;
19import android.appwidget.AppWidgetProviderInfo;
20import android.content.ComponentName;
21import android.content.Intent;
22import android.content.IntentSender;
23import android.content.pm.IPinItemRequest;
24import android.content.pm.LauncherApps;
25import android.content.pm.LauncherApps.PinItemRequest;
26import android.content.pm.ShortcutInfo;
27import android.os.Bundle;
28import android.os.UserHandle;
29import android.util.Log;
30import android.util.Pair;
31import android.util.Slog;
32
33import com.android.internal.annotations.GuardedBy;
34import com.android.internal.annotations.VisibleForTesting;
35import com.android.internal.util.Preconditions;
36
37/**
38 * Handles {@link android.content.pm.ShortcutManager#requestPinShortcut} related tasks.
39 */
40class ShortcutRequestPinProcessor {
41    private static final String TAG = ShortcutService.TAG;
42    private static final boolean DEBUG = ShortcutService.DEBUG;
43
44    private final ShortcutService mService;
45    private final Object mLock;
46
47    /**
48     * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks.
49     */
50    private static class PinItemRequestInner extends IPinItemRequest.Stub {
51        protected final ShortcutRequestPinProcessor mProcessor;
52        private final IntentSender mResultIntent;
53
54        @GuardedBy("this")
55        private boolean mAccepted;
56
57        private PinItemRequestInner(ShortcutRequestPinProcessor processor,
58                IntentSender resultIntent) {
59            mProcessor = processor;
60            mResultIntent = resultIntent;
61        }
62
63        @Override
64        public boolean isValid() {
65            // TODO When an app calls requestPinShortcut(), all pending requests should be
66            // invalidated.
67            synchronized (this) {
68                return !mAccepted;
69            }
70        }
71
72        /**
73         * Called when the launcher calls {@link PinItemRequest#accept}.
74         */
75        @Override
76        public boolean accept(Bundle options) {
77            // Make sure the options are unparcellable by the FW. (e.g. not containing unknown
78            // classes.)
79            Intent extras = null;
80            if (options != null) {
81                try {
82                    options.size();
83                    extras = new Intent().putExtras(options);
84                } catch (RuntimeException e) {
85                    throw new IllegalArgumentException("options cannot be unparceled", e);
86                }
87            }
88            synchronized (this) {
89                if (mAccepted) {
90                    throw new IllegalStateException("accept() called already");
91                }
92                mAccepted = true;
93            }
94
95            // Pin it and send the result intent.
96            if (tryAccept()) {
97                mProcessor.sendResultIntent(mResultIntent, extras);
98                return true;
99            } else {
100                return false;
101            }
102        }
103
104        protected boolean tryAccept() {
105            return true;
106        }
107    }
108
109    /**
110     * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks.
111     */
112    private static class PinShortcutRequestInner extends PinItemRequestInner {
113        /** Original shortcut passed by the app. */
114        public final ShortcutInfo shortcutOriginal;
115
116        /**
117         * Cloned shortcut that's passed to the launcher.  The notable difference from
118         * {@link #shortcutOriginal} is it must not have the intent.
119         */
120        public final ShortcutInfo shortcutForLauncher;
121
122        public final String launcherPackage;
123        public final int launcherUserId;
124        public final boolean preExisting;
125
126        private PinShortcutRequestInner(ShortcutRequestPinProcessor processor,
127                ShortcutInfo shortcutOriginal, ShortcutInfo shortcutForLauncher,
128                IntentSender resultIntent,
129                String launcherPackage, int launcherUserId, boolean preExisting) {
130            super(processor, resultIntent);
131            this.shortcutOriginal = shortcutOriginal;
132            this.shortcutForLauncher = shortcutForLauncher;
133            this.launcherPackage = launcherPackage;
134            this.launcherUserId = launcherUserId;
135            this.preExisting = preExisting;
136        }
137
138        @Override
139        protected boolean tryAccept() {
140            if (DEBUG) {
141                Slog.d(TAG, "Launcher accepted shortcut. ID=" + shortcutOriginal.getId()
142                    + " package=" + shortcutOriginal.getPackage());
143            }
144            return mProcessor.directPinShortcut(this);
145        }
146    }
147
148    public ShortcutRequestPinProcessor(ShortcutService service, Object lock) {
149        mService = service;
150        mLock = lock;
151    }
152
153    public boolean isRequestPinnedShortcutSupported(int callingUserId) {
154        return getRequestPinShortcutConfirmationActivity(callingUserId) != null;
155    }
156
157    /**
158     * Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)} and
159     * {@link android.appwidget.AppWidgetManager#requestPinAppWidget}.
160     * One of {@param inShortcut} and {@param inAppWidget} is always non-null and the other is
161     * always null.
162     */
163    public boolean requestPinItemLocked(ShortcutInfo inShortcut, AppWidgetProviderInfo inAppWidget,
164        int userId, IntentSender resultIntent) {
165
166        // First, make sure the launcher supports it.
167
168        // Find the confirmation activity in the default launcher.
169        final Pair<ComponentName, Integer> confirmActivity =
170                getRequestPinShortcutConfirmationActivity(userId);
171
172        // If the launcher doesn't support it, just return a rejected result and finish.
173        if (confirmActivity == null) {
174            Log.w(TAG, "Launcher doesn't support requestPinnedShortcut(). Shortcut not created.");
175            return false;
176        }
177
178        final int launcherUserId = confirmActivity.second;
179
180        // Make sure the launcher user is unlocked. (it's always the parent profile, so should
181        // really be unlocked here though.)
182        mService.throwIfUserLockedL(launcherUserId);
183
184        // Next, validate the incoming shortcut, etc.
185        final PinItemRequest request;
186        if (inShortcut != null) {
187            request = requestPinShortcutLocked(inShortcut, resultIntent, confirmActivity);
188        } else {
189            request = new PinItemRequest(inAppWidget, new PinItemRequestInner(this, resultIntent));
190        }
191
192        if (request == null) {
193            sendResultIntent(resultIntent, null);
194            return true;
195        }
196        return startRequestConfirmActivity(confirmActivity.first, launcherUserId, request);
197    }
198
199    /**
200     * Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)}.
201     */
202    private PinItemRequest requestPinShortcutLocked(ShortcutInfo inShortcut,
203            IntentSender resultIntent, Pair<ComponentName, Integer> confirmActivity) {
204        final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked(
205                inShortcut.getPackage(), inShortcut.getUserId());
206
207        final ShortcutInfo existing = ps.findShortcutById(inShortcut.getId());
208        final boolean existsAlready = existing != null;
209
210        if (DEBUG) {
211            Slog.d(TAG, "requestPinnedShortcut: package=" + inShortcut.getPackage()
212                    + " existsAlready=" + existsAlready
213                    + " shortcut=" + inShortcut.toInsecureString());
214        }
215
216        // This is the shortcut that'll be sent to the launcher.
217        final ShortcutInfo shortcutForLauncher;
218        final String launcherPackage = confirmActivity.first.getPackageName();
219        final int launcherUserId = confirmActivity.second;
220
221        if (existsAlready) {
222            validateExistingShortcut(existing);
223
224            // See if it's already pinned.
225            if (mService.getLauncherShortcutsLocked(
226                    launcherPackage, existing.getUserId(), launcherUserId).hasPinned(existing)) {
227                Log.i(TAG, "Launcher's already pinning shortcut " + existing.getId()
228                        + " for package " + existing.getPackage());
229                return null;
230            }
231
232            // Pass a clone, not the original.
233            // Note this will remove the intent and icons.
234            shortcutForLauncher = existing.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
235
236            // FLAG_PINNED is still set, if it's pinned by other launchers.
237            shortcutForLauncher.clearFlags(ShortcutInfo.FLAG_PINNED);
238        } else {
239            // If the shortcut has no default activity, try to set the main activity.
240            // But in the request-pin case, it's optional, so it's okay even if the caller
241            // has no default activity.
242            if (inShortcut.getActivity() == null) {
243                inShortcut.setActivity(mService.injectGetDefaultMainActivity(
244                        inShortcut.getPackage(), inShortcut.getUserId()));
245            }
246
247            // It doesn't exist, so it must have all mandatory fields.
248            mService.validateShortcutForPinRequest(inShortcut);
249
250            // Initialize the ShortcutInfo for pending approval.
251            inShortcut.resolveResourceStrings(mService.injectGetResourcesForApplicationAsUser(
252                    inShortcut.getPackage(), inShortcut.getUserId()));
253            if (DEBUG) {
254                Slog.d(TAG, "Resolved shortcut=" + inShortcut.toInsecureString());
255            }
256            // We should strip out the intent, but should preserve the icon.
257            shortcutForLauncher = inShortcut.clone(
258                    ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER_APPROVAL);
259        }
260        if (DEBUG) {
261            Slog.d(TAG, "Sending to launcher=" + shortcutForLauncher.toInsecureString());
262        }
263
264        // Create a request object.
265        final PinShortcutRequestInner inner =
266                new PinShortcutRequestInner(this, inShortcut, shortcutForLauncher, resultIntent,
267                        launcherPackage, launcherUserId, existsAlready);
268
269        return new PinItemRequest(shortcutForLauncher, inner);
270    }
271
272    private void validateExistingShortcut(ShortcutInfo shortcutInfo) {
273        // Make sure it's enabled.
274        // (Because we can't always force enable it automatically as it may be a stale
275        // manifest shortcut.)
276        Preconditions.checkArgument(shortcutInfo.isEnabled(),
277                "Shortcut ID=" + shortcutInfo + " already exists but disabled.");
278
279    }
280
281    private boolean startRequestConfirmActivity(ComponentName activity, int launcherUserId,
282            PinItemRequest request) {
283        // Start the activity.
284        final Intent confirmIntent = new Intent(LauncherApps.ACTION_CONFIRM_PIN_ITEM);
285        confirmIntent.setComponent(activity);
286        confirmIntent.putExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST, request);
287        confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
288
289        final long token = mService.injectClearCallingIdentity();
290        try {
291            mService.mContext.startActivityAsUser(
292                    confirmIntent, UserHandle.of(launcherUserId));
293        } catch (RuntimeException e) { // ActivityNotFoundException, etc.
294            Log.e(TAG, "Unable to start activity " + activity, e);
295            return false;
296        } finally {
297            mService.injectRestoreCallingIdentity(token);
298        }
299        return true;
300    }
301
302    /**
303     * Find the activity that handles {@link LauncherApps#ACTION_CONFIRM_PIN_ITEM} in the
304     * default launcher.
305     */
306    @Nullable
307    @VisibleForTesting
308    Pair<ComponentName, Integer> getRequestPinShortcutConfirmationActivity(
309            int callingUserId) {
310        // Find the default launcher.
311        final int launcherUserId = mService.getParentOrSelfUserId(callingUserId);
312        final ComponentName defaultLauncher = mService.getDefaultLauncher(launcherUserId);
313
314        if (defaultLauncher == null) {
315            Log.e(TAG, "Default launcher not found.");
316            return null;
317        }
318        final ComponentName activity = mService.injectGetPinConfirmationActivity(
319                defaultLauncher.getPackageName(), launcherUserId);
320        return (activity == null) ? null : Pair.create(activity, launcherUserId);
321    }
322
323    public void sendResultIntent(@Nullable IntentSender intent, @Nullable Intent extras) {
324        if (DEBUG) {
325            Slog.d(TAG, "Sending result intent.");
326        }
327        mService.injectSendIntentSender(intent, extras);
328    }
329
330    /**
331     * The last step of the "request pin shortcut" flow.  Called when the launcher accepted a
332     * request.
333     */
334    public boolean directPinShortcut(PinShortcutRequestInner request) {
335
336        final ShortcutInfo original = request.shortcutOriginal;
337        final int appUserId = original.getUserId();
338        final String appPackageName = original.getPackage();
339        final int launcherUserId = request.launcherUserId;
340        final String launcherPackage = request.launcherPackage;
341        final String shortcutId = original.getId();
342
343        synchronized (mLock) {
344            if (!(mService.isUserUnlockedL(appUserId)
345                    && mService.isUserUnlockedL(request.launcherUserId))) {
346                Log.w(TAG, "User is locked now.");
347                return false;
348            }
349
350            final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked(
351                    appPackageName, appUserId);
352            final ShortcutInfo current = ps.findShortcutById(shortcutId);
353
354            // The shortcut might have been changed, so we need to do the same validation again.
355            try {
356                if (current == null) {
357                    // It doesn't exist, so it must have all necessary fields.
358                    mService.validateShortcutForPinRequest(original);
359                } else {
360                    validateExistingShortcut(current);
361                }
362            } catch (RuntimeException e) {
363                Log.w(TAG, "Unable to pin shortcut: " + e.getMessage());
364                return false;
365            }
366
367            // If the shortcut doesn't exist, need to create it.
368            // First, create it as a dynamic shortcut.
369            if (current == null) {
370                if (DEBUG) {
371                    Slog.d(TAG, "Temporarily adding " + shortcutId + " as dynamic");
372                }
373                // Add as a dynamic shortcut.  In order for a shortcut to be dynamic, it must
374                // have a target activity, so we set a dummy here.  It's later removed
375                // in deleteDynamicWithId().
376                if (original.getActivity() == null) {
377                    original.setActivity(mService.getDummyMainActivity(appPackageName));
378                }
379                ps.addOrUpdateDynamicShortcut(original);
380            }
381
382            // Pin the shortcut.
383            if (DEBUG) {
384                Slog.d(TAG, "Pinning " + shortcutId);
385            }
386
387            final ShortcutLauncher launcher = mService.getLauncherShortcutsLocked(
388                    launcherPackage, appUserId, launcherUserId);
389            launcher.attemptToRestoreIfNeededAndSave();
390            launcher.addPinnedShortcut(appPackageName, appUserId, shortcutId);
391
392            if (current == null) {
393                if (DEBUG) {
394                    Slog.d(TAG, "Removing " + shortcutId + " as dynamic");
395                }
396                ps.deleteDynamicWithId(shortcutId);
397            }
398
399            ps.adjustRanks(); // Shouldn't be needed, but just in case.
400        }
401
402        mService.verifyStates();
403        mService.packageShortcutsChanged(appPackageName, appUserId);
404
405        return true;
406    }
407}
408