Home | History | Annotate | Download | only in launcher3
      1 /*
      2  * Copyright (C) 2008 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 
     17 package com.android.launcher3;
     18 
     19 import android.content.BroadcastReceiver;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.SharedPreferences;
     23 import android.content.pm.ActivityInfo;
     24 import android.content.pm.PackageManager;
     25 import android.content.pm.ResolveInfo;
     26 import android.graphics.Bitmap;
     27 import android.graphics.BitmapFactory;
     28 import android.text.TextUtils;
     29 import android.util.Base64;
     30 import android.util.Log;
     31 
     32 import com.android.launcher3.compat.LauncherActivityInfoCompat;
     33 import com.android.launcher3.compat.LauncherAppsCompat;
     34 import com.android.launcher3.compat.UserHandleCompat;
     35 import com.android.launcher3.compat.UserManagerCompat;
     36 import com.android.launcher3.util.Thunk;
     37 
     38 import org.json.JSONException;
     39 import org.json.JSONObject;
     40 import org.json.JSONStringer;
     41 import org.json.JSONTokener;
     42 
     43 import java.net.URISyntaxException;
     44 import java.util.ArrayList;
     45 import java.util.HashSet;
     46 import java.util.Iterator;
     47 import java.util.Set;
     48 
     49 public class InstallShortcutReceiver extends BroadcastReceiver {
     50     private static final String TAG = "InstallShortcutReceiver";
     51     private static final boolean DBG = false;
     52 
     53     private static final String ACTION_INSTALL_SHORTCUT =
     54             "com.android.launcher.action.INSTALL_SHORTCUT";
     55 
     56     private static final String LAUNCH_INTENT_KEY = "intent.launch";
     57     private static final String NAME_KEY = "name";
     58     private static final String ICON_KEY = "icon";
     59     private static final String ICON_RESOURCE_NAME_KEY = "iconResource";
     60     private static final String ICON_RESOURCE_PACKAGE_NAME_KEY = "iconResourcePackage";
     61 
     62     private static final String APP_SHORTCUT_TYPE_KEY = "isAppShortcut";
     63     private static final String USER_HANDLE_KEY = "userHandle";
     64 
     65     // The set of shortcuts that are pending install
     66     private static final String APPS_PENDING_INSTALL = "apps_to_install";
     67 
     68     public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450;
     69     public static final int NEW_SHORTCUT_STAGGER_DELAY = 85;
     70 
     71     private static final Object sLock = new Object();
     72 
     73     private static void addToInstallQueue(
     74             SharedPreferences sharedPrefs, PendingInstallShortcutInfo info) {
     75         synchronized(sLock) {
     76             String encoded = info.encodeToString();
     77             if (encoded != null) {
     78                 Set<String> strings = sharedPrefs.getStringSet(APPS_PENDING_INSTALL, null);
     79                 if (strings == null) {
     80                     strings = new HashSet<String>(1);
     81                 } else {
     82                     strings = new HashSet<String>(strings);
     83                 }
     84                 strings.add(encoded);
     85                 sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, strings).apply();
     86             }
     87         }
     88     }
     89 
     90     public static void removeFromInstallQueue(Context context, HashSet<String> packageNames,
     91             UserHandleCompat user) {
     92         if (packageNames.isEmpty()) {
     93             return;
     94         }
     95         SharedPreferences sp = Utilities.getPrefs(context);
     96         synchronized(sLock) {
     97             Set<String> strings = sp.getStringSet(APPS_PENDING_INSTALL, null);
     98             if (DBG) {
     99                 Log.d(TAG, "APPS_PENDING_INSTALL: " + strings
    100                         + ", removing packages: " + packageNames);
    101             }
    102             if (strings != null) {
    103                 Set<String> newStrings = new HashSet<String>(strings);
    104                 Iterator<String> newStringsIter = newStrings.iterator();
    105                 while (newStringsIter.hasNext()) {
    106                     String encoded = newStringsIter.next();
    107                     PendingInstallShortcutInfo info = decode(encoded, context);
    108                     if (info == null || (packageNames.contains(info.getTargetPackage())
    109                             && user.equals(info.user))) {
    110                         newStringsIter.remove();
    111                     }
    112                 }
    113                 sp.edit().putStringSet(APPS_PENDING_INSTALL, newStrings).apply();
    114             }
    115         }
    116     }
    117 
    118     private static ArrayList<PendingInstallShortcutInfo> getAndClearInstallQueue(
    119             SharedPreferences sharedPrefs, Context context) {
    120         synchronized(sLock) {
    121             Set<String> strings = sharedPrefs.getStringSet(APPS_PENDING_INSTALL, null);
    122             if (DBG) Log.d(TAG, "Getting and clearing APPS_PENDING_INSTALL: " + strings);
    123             if (strings == null) {
    124                 return new ArrayList<PendingInstallShortcutInfo>();
    125             }
    126             ArrayList<PendingInstallShortcutInfo> infos =
    127                 new ArrayList<PendingInstallShortcutInfo>();
    128             for (String encoded : strings) {
    129                 PendingInstallShortcutInfo info = decode(encoded, context);
    130                 if (info != null) {
    131                     infos.add(info);
    132                 }
    133             }
    134             sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, new HashSet<String>()).apply();
    135             return infos;
    136         }
    137     }
    138 
    139     // Determines whether to defer installing shortcuts immediately until
    140     // processAllPendingInstalls() is called.
    141     private static boolean mUseInstallQueue = false;
    142 
    143     public void onReceive(Context context, Intent data) {
    144         if (!ACTION_INSTALL_SHORTCUT.equals(data.getAction())) {
    145             return;
    146         }
    147         PendingInstallShortcutInfo info = createPendingInfo(context, data);
    148         if (info != null) {
    149             queuePendingShortcutInfo(info, context);
    150         }
    151     }
    152 
    153     /**
    154      * @return true is the extra is either null or is of type {@param type}
    155      */
    156     private static boolean isValidExtraType(Intent intent, String key, Class type) {
    157         Object extra = intent.getParcelableExtra(key);
    158         return extra == null || type.isInstance(extra);
    159     }
    160 
    161     /**
    162      * Verifies the intent and creates a {@link PendingInstallShortcutInfo}
    163      */
    164     private static PendingInstallShortcutInfo createPendingInfo(Context context, Intent data) {
    165         if (!isValidExtraType(data, Intent.EXTRA_SHORTCUT_INTENT, Intent.class) ||
    166                 !(isValidExtraType(data, Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
    167                         Intent.ShortcutIconResource.class)) ||
    168                 !(isValidExtraType(data, Intent.EXTRA_SHORTCUT_ICON, Bitmap.class))) {
    169 
    170             if (DBG) Log.e(TAG, "Invalid install shortcut intent");
    171             return null;
    172         }
    173 
    174         PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context);
    175         if (info.launchIntent == null || info.label == null) {
    176             if (DBG) Log.e(TAG, "Invalid install shortcut intent");
    177             return null;
    178         }
    179 
    180         return convertToLauncherActivityIfPossible(info);
    181     }
    182 
    183     public static ShortcutInfo fromShortcutIntent(Context context, Intent data) {
    184         PendingInstallShortcutInfo info = createPendingInfo(context, data);
    185         return info == null ? null : info.getShortcutInfo();
    186     }
    187 
    188     private static void queuePendingShortcutInfo(PendingInstallShortcutInfo info, Context context) {
    189         // Queue the item up for adding if launcher has not loaded properly yet
    190         LauncherAppState app = LauncherAppState.getInstance();
    191         boolean launcherNotLoaded = app.getModel().getCallback() == null;
    192 
    193         addToInstallQueue(Utilities.getPrefs(context), info);
    194         if (!mUseInstallQueue && !launcherNotLoaded) {
    195             flushInstallQueue(context);
    196         }
    197     }
    198 
    199     static void enableInstallQueue() {
    200         mUseInstallQueue = true;
    201     }
    202     static void disableAndFlushInstallQueue(Context context) {
    203         mUseInstallQueue = false;
    204         flushInstallQueue(context);
    205     }
    206     static void flushInstallQueue(Context context) {
    207         SharedPreferences sp = Utilities.getPrefs(context);
    208         ArrayList<PendingInstallShortcutInfo> installQueue = getAndClearInstallQueue(sp, context);
    209         if (!installQueue.isEmpty()) {
    210             Iterator<PendingInstallShortcutInfo> iter = installQueue.iterator();
    211             ArrayList<ItemInfo> addShortcuts = new ArrayList<ItemInfo>();
    212             while (iter.hasNext()) {
    213                 final PendingInstallShortcutInfo pendingInfo = iter.next();
    214 
    215                 // If the intent specifies a package, make sure the package exists
    216                 String packageName = pendingInfo.getTargetPackage();
    217                 if (!TextUtils.isEmpty(packageName)) {
    218                     UserHandleCompat myUserHandle = UserHandleCompat.myUserHandle();
    219                     if (!LauncherModel.isValidPackage(context, packageName, myUserHandle)) {
    220                         if (DBG) Log.d(TAG, "Ignoring shortcut for absent package: "
    221                                 + pendingInfo.launchIntent);
    222                         continue;
    223                     }
    224                 }
    225 
    226                 // Generate a shortcut info to add into the model
    227                 addShortcuts.add(pendingInfo.getShortcutInfo());
    228             }
    229 
    230             // Add the new apps to the model and bind them
    231             if (!addShortcuts.isEmpty()) {
    232                 LauncherAppState app = LauncherAppState.getInstance();
    233                 app.getModel().addAndBindAddedWorkspaceItems(context, addShortcuts);
    234             }
    235         }
    236     }
    237 
    238     /**
    239      * Ensures that we have a valid, non-null name.  If the provided name is null, we will return
    240      * the application name instead.
    241      */
    242     @Thunk static CharSequence ensureValidName(Context context, Intent intent, CharSequence name) {
    243         if (name == null) {
    244             try {
    245                 PackageManager pm = context.getPackageManager();
    246                 ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0);
    247                 name = info.loadLabel(pm);
    248             } catch (PackageManager.NameNotFoundException nnfe) {
    249                 return "";
    250             }
    251         }
    252         return name;
    253     }
    254 
    255     private static class PendingInstallShortcutInfo {
    256 
    257         final LauncherActivityInfoCompat activityInfo;
    258 
    259         final Intent data;
    260         final Context mContext;
    261         final Intent launchIntent;
    262         final String label;
    263         final UserHandleCompat user;
    264 
    265         /**
    266          * Initializes a PendingInstallShortcutInfo received from a different app.
    267          */
    268         public PendingInstallShortcutInfo(Intent data, Context context) {
    269             this.data = data;
    270             mContext = context;
    271 
    272             launchIntent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
    273             label = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
    274             user = UserHandleCompat.myUserHandle();
    275             activityInfo = null;
    276         }
    277 
    278         /**
    279          * Initializes a PendingInstallShortcutInfo to represent a launcher target.
    280          */
    281         public PendingInstallShortcutInfo(LauncherActivityInfoCompat info, Context context) {
    282             this.data = null;
    283             mContext = context;
    284             activityInfo = info;
    285             user = info.getUser();
    286 
    287             launchIntent = AppInfo.makeLaunchIntent(context, info, user);
    288             label = info.getLabel().toString();
    289         }
    290 
    291         public String encodeToString() {
    292             if (activityInfo != null) {
    293                 try {
    294                     // If it a launcher target, we only need component name, and user to
    295                     // recreate this.
    296                     return new JSONStringer()
    297                         .object()
    298                         .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0))
    299                         .key(APP_SHORTCUT_TYPE_KEY).value(true)
    300                         .key(USER_HANDLE_KEY).value(UserManagerCompat.getInstance(mContext)
    301                                 .getSerialNumberForUser(user))
    302                         .endObject().toString();
    303                 } catch (JSONException e) {
    304                     Log.d(TAG, "Exception when adding shortcut: " + e);
    305                     return null;
    306                 }
    307             }
    308 
    309             if (launchIntent.getAction() == null) {
    310                 launchIntent.setAction(Intent.ACTION_VIEW);
    311             } else if (launchIntent.getAction().equals(Intent.ACTION_MAIN) &&
    312                     launchIntent.getCategories() != null &&
    313                     launchIntent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) {
    314                 launchIntent.addFlags(
    315                         Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
    316             }
    317 
    318             // This name is only used for comparisons and notifications, so fall back to activity
    319             // name if not supplied
    320             String name = ensureValidName(mContext, launchIntent, label).toString();
    321             Bitmap icon = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
    322             Intent.ShortcutIconResource iconResource =
    323                 data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
    324 
    325             // Only encode the parameters which are supported by the API.
    326             try {
    327                 JSONStringer json = new JSONStringer()
    328                     .object()
    329                     .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0))
    330                     .key(NAME_KEY).value(name);
    331                 if (icon != null) {
    332                     byte[] iconByteArray = Utilities.flattenBitmap(icon);
    333                     json = json.key(ICON_KEY).value(
    334                             Base64.encodeToString(
    335                                     iconByteArray, 0, iconByteArray.length, Base64.DEFAULT));
    336                 }
    337                 if (iconResource != null) {
    338                     json = json.key(ICON_RESOURCE_NAME_KEY).value(iconResource.resourceName);
    339                     json = json.key(ICON_RESOURCE_PACKAGE_NAME_KEY)
    340                             .value(iconResource.packageName);
    341                 }
    342                 return json.endObject().toString();
    343             } catch (JSONException e) {
    344                 Log.d(TAG, "Exception when adding shortcut: " + e);
    345             }
    346             return null;
    347         }
    348 
    349         public ShortcutInfo getShortcutInfo() {
    350             if (activityInfo != null) {
    351                 return ShortcutInfo.fromActivityInfo(activityInfo, mContext);
    352             } else {
    353                 return LauncherAppState.getInstance().getModel().infoFromShortcutIntent(mContext, data);
    354             }
    355         }
    356 
    357         public String getTargetPackage() {
    358             String packageName = launchIntent.getPackage();
    359             if (packageName == null) {
    360                 packageName = launchIntent.getComponent() == null ? null :
    361                     launchIntent.getComponent().getPackageName();
    362             }
    363             return packageName;
    364         }
    365 
    366         public boolean isLauncherActivity() {
    367             return activityInfo != null;
    368         }
    369     }
    370 
    371     private static PendingInstallShortcutInfo decode(String encoded, Context context) {
    372         try {
    373             JSONObject object = (JSONObject) new JSONTokener(encoded).nextValue();
    374             Intent launcherIntent = Intent.parseUri(object.getString(LAUNCH_INTENT_KEY), 0);
    375 
    376             if (object.optBoolean(APP_SHORTCUT_TYPE_KEY)) {
    377                 // The is an internal launcher target shortcut.
    378                 UserHandleCompat user = UserManagerCompat.getInstance(context)
    379                         .getUserForSerialNumber(object.getLong(USER_HANDLE_KEY));
    380                 if (user == null) {
    381                     return null;
    382                 }
    383 
    384                 LauncherActivityInfoCompat info = LauncherAppsCompat.getInstance(context)
    385                         .resolveActivity(launcherIntent, user);
    386                 return info == null ? null : new PendingInstallShortcutInfo(info, context);
    387             }
    388 
    389             Intent data = new Intent();
    390             data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launcherIntent);
    391             data.putExtra(Intent.EXTRA_SHORTCUT_NAME, object.getString(NAME_KEY));
    392 
    393             String iconBase64 = object.optString(ICON_KEY);
    394             String iconResourceName = object.optString(ICON_RESOURCE_NAME_KEY);
    395             String iconResourcePackageName = object.optString(ICON_RESOURCE_PACKAGE_NAME_KEY);
    396             if (iconBase64 != null && !iconBase64.isEmpty()) {
    397                 byte[] iconArray = Base64.decode(iconBase64, Base64.DEFAULT);
    398                 Bitmap b = BitmapFactory.decodeByteArray(iconArray, 0, iconArray.length);
    399                 data.putExtra(Intent.EXTRA_SHORTCUT_ICON, b);
    400             } else if (iconResourceName != null && !iconResourceName.isEmpty()) {
    401                 Intent.ShortcutIconResource iconResource =
    402                     new Intent.ShortcutIconResource();
    403                 iconResource.resourceName = iconResourceName;
    404                 iconResource.packageName = iconResourcePackageName;
    405                 data.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource);
    406             }
    407 
    408             return new PendingInstallShortcutInfo(data, context);
    409         } catch (JSONException | URISyntaxException e) {
    410             Log.d(TAG, "Exception reading shortcut to add: " + e);
    411         }
    412         return null;
    413     }
    414 
    415     /**
    416      * Tries to create a new PendingInstallShortcutInfo which represents the same target,
    417      * but is an app target and not a shortcut.
    418      * @return the newly created info or the original one.
    419      */
    420     private static PendingInstallShortcutInfo convertToLauncherActivityIfPossible(
    421             PendingInstallShortcutInfo original) {
    422         if (original.isLauncherActivity()) {
    423             // Already an activity target
    424             return original;
    425         }
    426         if (!Utilities.isLauncherAppTarget(original.launchIntent)
    427                 || !original.user.equals(UserHandleCompat.myUserHandle())) {
    428             // We can only convert shortcuts which point to a main activity in the current user.
    429             return original;
    430         }
    431 
    432         PackageManager pm = original.mContext.getPackageManager();
    433         ResolveInfo info = pm.resolveActivity(original.launchIntent, 0);
    434 
    435         if (info == null) {
    436             return original;
    437         }
    438 
    439         // Ignore any conflicts in the label name, as that can change based on locale.
    440         LauncherActivityInfoCompat launcherInfo = LauncherActivityInfoCompat
    441                 .fromResolveInfo(info, original.mContext);
    442         return new PendingInstallShortcutInfo(launcherInfo, original.mContext);
    443     }
    444 }
    445