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