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