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