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
17package com.android.launcher2;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.SharedPreferences;
23import android.content.pm.ActivityInfo;
24import android.content.pm.PackageManager;
25import android.graphics.Bitmap;
26import android.graphics.BitmapFactory;
27import android.util.Base64;
28import android.util.Log;
29import android.widget.Toast;
30
31import com.android.launcher.R;
32
33import java.util.ArrayList;
34import java.util.HashSet;
35import java.util.Iterator;
36import java.util.Set;
37
38import org.json.*;
39
40public 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