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 
addToStringSet(SharedPreferences sharedPrefs, SharedPreferences.Editor editor, String key, String value)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 
addToInstallQueue( SharedPreferences sharedPrefs, PendingInstallShortcutInfo info)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 
getAndClearInstallQueue( SharedPreferences sharedPrefs)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 
PendingInstallShortcutInfo(Intent rawData, String shortcutName, Intent shortcutIntent)166         public PendingInstallShortcutInfo(Intent rawData, String shortcutName,
167                 Intent shortcutIntent) {
168             data = rawData;
169             name = shortcutName;
170             launchIntent = shortcutIntent;
171         }
172     }
173 
onReceive(Context context, Intent data)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 
enableInstallQueue()215     static void enableInstallQueue() {
216         mUseInstallQueue = true;
217     }
disableAndFlushInstallQueue(Context context)218     static void disableAndFlushInstallQueue(Context context) {
219         mUseInstallQueue = false;
220         flushInstallQueue(context);
221     }
flushInstallQueue(Context context)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 
processInstallShortcut(Context context, PendingInstallShortcutInfo pendingInfo)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 
installShortcut(Context context, Intent data, ArrayList<ItemInfo> items, String name, final Intent intent, final int screen, boolean shortcutExists, final SharedPreferences sharedPrefs, int[] result)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 
findEmptyCell(Context context, ArrayList<ItemInfo> items, int[] xy, int screen)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