Home | History | Annotate | Download | only in pm
      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 
     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