Home | History | Annotate | Download | only in launcher2
      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.launcher2;
     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.util.Base64;
     28 import android.util.Log;
     29 import android.widget.Toast;
     30 
     31 import com.android.launcher.R;
     32 
     33 import java.util.ArrayList;
     34 import java.util.HashSet;
     35 import java.util.Iterator;
     36 import java.util.Set;
     37 
     38 import org.json.*;
     39 
     40 public class InstallShortcutReceiver extends BroadcastReceiver {
     41     public static final String ACTION_INSTALL_SHORTCUT =
     42             "com.android.launcher.action.INSTALL_SHORTCUT";
     43     public static final String NEW_APPS_PAGE_KEY = "apps.new.page";
     44     public static final String NEW_APPS_LIST_KEY = "apps.new.list";
     45 
     46     public static final String DATA_INTENT_KEY = "intent.data";
     47     public static final String LAUNCH_INTENT_KEY = "intent.launch";
     48     public static final String NAME_KEY = "name";
     49     public static final String ICON_KEY = "icon";
     50     public static final String ICON_RESOURCE_NAME_KEY = "iconResource";
     51     public static final String ICON_RESOURCE_PACKAGE_NAME_KEY = "iconResourcePackage";
     52     // The set of shortcuts that are pending install
     53     public static final String APPS_PENDING_INSTALL = "apps_to_install";
     54 
     55     public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450;
     56     public static final int NEW_SHORTCUT_STAGGER_DELAY = 75;
     57 
     58     private static final int INSTALL_SHORTCUT_SUCCESSFUL = 0;
     59     private static final int INSTALL_SHORTCUT_IS_DUPLICATE = -1;
     60     private static final int INSTALL_SHORTCUT_NO_SPACE = -2;
     61 
     62     // A mime-type representing shortcut data
     63     public static final String SHORTCUT_MIMETYPE =
     64             "com.android.launcher/shortcut";
     65 
     66     private static Object sLock = new Object();
     67 
     68     private static void addToStringSet(SharedPreferences sharedPrefs,
     69             SharedPreferences.Editor editor, String key, String value) {
     70         Set<String> strings = sharedPrefs.getStringSet(key, null);
     71         if (strings == null) {
     72             strings = new HashSet<String>(0);
     73         } else {
     74             strings = new HashSet<String>(strings);
     75         }
     76         strings.add(value);
     77         editor.putStringSet(key, strings);
     78     }
     79 
     80     private static void addToInstallQueue(
     81             SharedPreferences sharedPrefs, PendingInstallShortcutInfo info) {
     82         synchronized(sLock) {
     83             try {
     84                 JSONStringer json = new JSONStringer()
     85                     .object()
     86                     .key(DATA_INTENT_KEY).value(info.data.toUri(0))
     87                     .key(LAUNCH_INTENT_KEY).value(info.launchIntent.toUri(0))
     88                     .key(NAME_KEY).value(info.name);
     89                 if (info.icon != null) {
     90                     byte[] iconByteArray = ItemInfo.flattenBitmap(info.icon);
     91                     json = json.key(ICON_KEY).value(
     92                         Base64.encodeToString(
     93                             iconByteArray, 0, iconByteArray.length, Base64.DEFAULT));
     94                 }
     95                 if (info.iconResource != null) {
     96                     json = json.key(ICON_RESOURCE_NAME_KEY).value(info.iconResource.resourceName);
     97                     json = json.key(ICON_RESOURCE_PACKAGE_NAME_KEY)
     98                         .value(info.iconResource.packageName);
     99                 }
    100                 json = json.endObject();
    101                 SharedPreferences.Editor editor = sharedPrefs.edit();
    102                 addToStringSet(sharedPrefs, editor, APPS_PENDING_INSTALL, json.toString());
    103                 editor.commit();
    104             } catch (org.json.JSONException e) {
    105                 Log.d("InstallShortcutReceiver", "Exception when adding shortcut: " + e);
    106             }
    107         }
    108     }
    109 
    110     private static ArrayList<PendingInstallShortcutInfo> getAndClearInstallQueue(
    111             SharedPreferences sharedPrefs) {
    112         synchronized(sLock) {
    113             Set<String> strings = sharedPrefs.getStringSet(APPS_PENDING_INSTALL, null);
    114             if (strings == null) {
    115                 return new ArrayList<PendingInstallShortcutInfo>();
    116             }
    117             ArrayList<PendingInstallShortcutInfo> infos =
    118                 new ArrayList<PendingInstallShortcutInfo>();
    119             for (String json : strings) {
    120                 try {
    121                     JSONObject object = (JSONObject) new JSONTokener(json).nextValue();
    122                     Intent data = Intent.parseUri(object.getString(DATA_INTENT_KEY), 0);
    123                     Intent launchIntent = Intent.parseUri(object.getString(LAUNCH_INTENT_KEY), 0);
    124                     String name = object.getString(NAME_KEY);
    125                     String iconBase64 = object.optString(ICON_KEY);
    126                     String iconResourceName = object.optString(ICON_RESOURCE_NAME_KEY);
    127                     String iconResourcePackageName =
    128                         object.optString(ICON_RESOURCE_PACKAGE_NAME_KEY);
    129                     if (iconBase64 != null && !iconBase64.isEmpty()) {
    130                         byte[] iconArray = Base64.decode(iconBase64, Base64.DEFAULT);
    131                         Bitmap b = BitmapFactory.decodeByteArray(iconArray, 0, iconArray.length);
    132                         data.putExtra(Intent.EXTRA_SHORTCUT_ICON, b);
    133                     } else if (iconResourceName != null && !iconResourceName.isEmpty()) {
    134                         Intent.ShortcutIconResource iconResource =
    135                             new Intent.ShortcutIconResource();
    136                         iconResource.resourceName = iconResourceName;
    137                         iconResource.packageName = iconResourcePackageName;
    138                         data.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource);
    139                     }
    140                     data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launchIntent);
    141                     PendingInstallShortcutInfo info =
    142                         new PendingInstallShortcutInfo(data, name, launchIntent);
    143                     infos.add(info);
    144                 } catch (org.json.JSONException e) {
    145                     Log.d("InstallShortcutReceiver", "Exception reading shortcut to add: " + e);
    146                 } catch (java.net.URISyntaxException e) {
    147                     Log.d("InstallShortcutReceiver", "Exception reading shortcut to add: " + e);
    148                 }
    149             }
    150             sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, new HashSet<String>()).commit();
    151             return infos;
    152         }
    153     }
    154 
    155     // Determines whether to defer installing shortcuts immediately until
    156     // processAllPendingInstalls() is called.
    157     private static boolean mUseInstallQueue = false;
    158 
    159     private static class PendingInstallShortcutInfo {
    160         Intent data;
    161         Intent launchIntent;
    162         String name;
    163         Bitmap icon;
    164         Intent.ShortcutIconResource iconResource;
    165 
    166         public PendingInstallShortcutInfo(Intent rawData, String shortcutName,
    167                 Intent shortcutIntent) {
    168             data = rawData;
    169             name = shortcutName;
    170             launchIntent = shortcutIntent;
    171         }
    172     }
    173 
    174     public void onReceive(Context context, Intent data) {
    175         if (!ACTION_INSTALL_SHORTCUT.equals(data.getAction())) {
    176             return;
    177         }
    178 
    179         Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
    180         if (intent == null) {
    181             return;
    182         }
    183         // This name is only used for comparisons and notifications, so fall back to activity name
    184         // if not supplied
    185         String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
    186         if (name == null) {
    187             try {
    188                 PackageManager pm = context.getPackageManager();
    189                 ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0);
    190                 name = info.loadLabel(pm).toString();
    191             } catch (PackageManager.NameNotFoundException nnfe) {
    192                 return;
    193             }
    194         }
    195         Bitmap icon = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
    196         Intent.ShortcutIconResource iconResource =
    197             data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
    198 
    199         // Queue the item up for adding if launcher has not loaded properly yet
    200         boolean launcherNotLoaded = LauncherModel.getCellCountX() <= 0 ||
    201                 LauncherModel.getCellCountY() <= 0;
    202 
    203         PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, name, intent);
    204         info.icon = icon;
    205         info.iconResource = iconResource;
    206         if (mUseInstallQueue || launcherNotLoaded) {
    207             String spKey = LauncherApplication.getSharedPreferencesKey();
    208             SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE);
    209             addToInstallQueue(sp, info);
    210         } else {
    211             processInstallShortcut(context, info);
    212         }
    213     }
    214 
    215     static void enableInstallQueue() {
    216         mUseInstallQueue = true;
    217     }
    218     static void disableAndFlushInstallQueue(Context context) {
    219         mUseInstallQueue = false;
    220         flushInstallQueue(context);
    221     }
    222     static void flushInstallQueue(Context context) {
    223         String spKey = LauncherApplication.getSharedPreferencesKey();
    224         SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE);
    225         ArrayList<PendingInstallShortcutInfo> installQueue = getAndClearInstallQueue(sp);
    226         Iterator<PendingInstallShortcutInfo> iter = installQueue.iterator();
    227         while (iter.hasNext()) {
    228             processInstallShortcut(context, iter.next());
    229         }
    230     }
    231 
    232     private static void processInstallShortcut(Context context,
    233             PendingInstallShortcutInfo pendingInfo) {
    234         String spKey = LauncherApplication.getSharedPreferencesKey();
    235         SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE);
    236 
    237         final Intent data = pendingInfo.data;
    238         final Intent intent = pendingInfo.launchIntent;
    239         final String name = pendingInfo.name;
    240 
    241         // Lock on the app so that we don't try and get the items while apps are being added
    242         LauncherApplication app = (LauncherApplication) context.getApplicationContext();
    243         final int[] result = {INSTALL_SHORTCUT_SUCCESSFUL};
    244         boolean found = false;
    245         synchronized (app) {
    246             // Flush the LauncherModel worker thread, so that if we just did another
    247             // processInstallShortcut, we give it time for its shortcut to get added to the
    248             // database (getItemsInLocalCoordinates reads the database)
    249             app.getModel().flushWorkerThread();
    250             final ArrayList<ItemInfo> items = LauncherModel.getItemsInLocalCoordinates(context);
    251             final boolean exists = LauncherModel.shortcutExists(context, name, intent);
    252 
    253             // Try adding to the workspace screens incrementally, starting at the default or center
    254             // screen and alternating between +1, -1, +2, -2, etc. (using ~ ceil(i/2f)*(-1)^(i-1))
    255             final int screen = Launcher.DEFAULT_SCREEN;
    256             for (int i = 0; i < (2 * Launcher.SCREEN_COUNT) + 1 && !found; ++i) {
    257                 int si = screen + (int) ((i / 2f) + 0.5f) * ((i % 2 == 1) ? 1 : -1);
    258                 if (0 <= si && si < Launcher.SCREEN_COUNT) {
    259                     found = installShortcut(context, data, items, name, intent, si, exists, sp,
    260                             result);
    261                 }
    262             }
    263         }
    264 
    265         // We only report error messages (duplicate shortcut or out of space) as the add-animation
    266         // will provide feedback otherwise
    267         if (!found) {
    268             if (result[0] == INSTALL_SHORTCUT_NO_SPACE) {
    269                 Toast.makeText(context, context.getString(R.string.completely_out_of_space),
    270                         Toast.LENGTH_SHORT).show();
    271             } else if (result[0] == INSTALL_SHORTCUT_IS_DUPLICATE) {
    272                 Toast.makeText(context, context.getString(R.string.shortcut_duplicate, name),
    273                         Toast.LENGTH_SHORT).show();
    274             }
    275         }
    276     }
    277 
    278     private static boolean installShortcut(Context context, Intent data, ArrayList<ItemInfo> items,
    279             String name, final Intent intent, final int screen, boolean shortcutExists,
    280             final SharedPreferences sharedPrefs, int[] result) {
    281         int[] tmpCoordinates = new int[2];
    282         if (findEmptyCell(context, items, tmpCoordinates, screen)) {
    283             if (intent != null) {
    284                 if (intent.getAction() == null) {
    285                     intent.setAction(Intent.ACTION_VIEW);
    286                 } else if (intent.getAction().equals(Intent.ACTION_MAIN) &&
    287                         intent.getCategories() != null &&
    288                         intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) {
    289                     intent.addFlags(
    290                         Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
    291                 }
    292 
    293                 // By default, we allow for duplicate entries (located in
    294                 // different places)
    295                 boolean duplicate = data.getBooleanExtra(Launcher.EXTRA_SHORTCUT_DUPLICATE, true);
    296                 if (duplicate || !shortcutExists) {
    297                     new Thread("setNewAppsThread") {
    298                         public void run() {
    299                             synchronized (sLock) {
    300                                 // If the new app is going to fall into the same page as before,
    301                                 // then just continue adding to the current page
    302                                 final int newAppsScreen = sharedPrefs.getInt(
    303                                         NEW_APPS_PAGE_KEY, screen);
    304                                 SharedPreferences.Editor editor = sharedPrefs.edit();
    305                                 if (newAppsScreen == -1 || newAppsScreen == screen) {
    306                                     addToStringSet(sharedPrefs,
    307                                         editor, NEW_APPS_LIST_KEY, intent.toUri(0));
    308                                 }
    309                                 editor.putInt(NEW_APPS_PAGE_KEY, screen);
    310                                 editor.commit();
    311                             }
    312                         }
    313                     }.start();
    314 
    315                     // Update the Launcher db
    316                     LauncherApplication app = (LauncherApplication) context.getApplicationContext();
    317                     ShortcutInfo info = app.getModel().addShortcut(context, data,
    318                             LauncherSettings.Favorites.CONTAINER_DESKTOP, screen,
    319                             tmpCoordinates[0], tmpCoordinates[1], true);
    320                     if (info == null) {
    321                         return false;
    322                     }
    323                 } else {
    324                     result[0] = INSTALL_SHORTCUT_IS_DUPLICATE;
    325                 }
    326 
    327                 return true;
    328             }
    329         } else {
    330             result[0] = INSTALL_SHORTCUT_NO_SPACE;
    331         }
    332 
    333         return false;
    334     }
    335 
    336     private static boolean findEmptyCell(Context context, ArrayList<ItemInfo> items, int[] xy,
    337             int screen) {
    338         final int xCount = LauncherModel.getCellCountX();
    339         final int yCount = LauncherModel.getCellCountY();
    340         boolean[][] occupied = new boolean[xCount][yCount];
    341 
    342         ItemInfo item = null;
    343         int cellX, cellY, spanX, spanY;
    344         for (int i = 0; i < items.size(); ++i) {
    345             item = items.get(i);
    346             if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
    347                 if (item.screen == screen) {
    348                     cellX = item.cellX;
    349                     cellY = item.cellY;
    350                     spanX = item.spanX;
    351                     spanY = item.spanY;
    352                     for (int x = cellX; 0 <= x && x < cellX + spanX && x < xCount; x++) {
    353                         for (int y = cellY; 0 <= y && y < cellY + spanY && y < yCount; y++) {
    354                             occupied[x][y] = true;
    355                         }
    356                     }
    357                 }
    358             }
    359         }
    360 
    361         return CellLayout.findVacantCell(xy, 1, 1, xCount, yCount, occupied);
    362     }
    363 }
    364