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