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.widget.Toast;
26
27import com.android.launcher.R;
28
29import java.util.ArrayList;
30import java.util.HashSet;
31import java.util.Iterator;
32import java.util.Set;
33
34public class InstallShortcutReceiver extends BroadcastReceiver {
35    public static final String ACTION_INSTALL_SHORTCUT =
36            "com.android.launcher.action.INSTALL_SHORTCUT";
37    public static final String NEW_APPS_PAGE_KEY = "apps.new.page";
38    public static final String NEW_APPS_LIST_KEY = "apps.new.list";
39
40    public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450;
41    public static final int NEW_SHORTCUT_STAGGER_DELAY = 75;
42
43    private static final int INSTALL_SHORTCUT_SUCCESSFUL = 0;
44    private static final int INSTALL_SHORTCUT_IS_DUPLICATE = -1;
45    private static final int INSTALL_SHORTCUT_NO_SPACE = -2;
46
47    // A mime-type representing shortcut data
48    public static final String SHORTCUT_MIMETYPE =
49            "com.android.launcher/shortcut";
50
51    // The set of shortcuts that are pending install
52    private static ArrayList<PendingInstallShortcutInfo> mInstallQueue =
53            new ArrayList<PendingInstallShortcutInfo>();
54
55    // Determines whether to defer installing shortcuts immediately until
56    // processAllPendingInstalls() is called.
57    private static boolean mUseInstallQueue = false;
58
59    private static class PendingInstallShortcutInfo {
60        Intent data;
61        Intent launchIntent;
62        String name;
63
64        public PendingInstallShortcutInfo(Intent rawData, String shortcutName,
65                Intent shortcutIntent) {
66            data = rawData;
67            name = shortcutName;
68            launchIntent = shortcutIntent;
69        }
70    }
71
72    public void onReceive(Context context, Intent data) {
73        if (!ACTION_INSTALL_SHORTCUT.equals(data.getAction())) {
74            return;
75        }
76
77        Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
78        if (intent == null) {
79            return;
80        }
81        // This name is only used for comparisons and notifications, so fall back to activity name
82        // if not supplied
83        String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
84        if (name == null) {
85            try {
86                PackageManager pm = context.getPackageManager();
87                ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0);
88                name = info.loadLabel(pm).toString();
89            } catch (PackageManager.NameNotFoundException nnfe) {
90                return;
91            }
92        }
93        // Queue the item up for adding if launcher has not loaded properly yet
94        boolean launcherNotLoaded = LauncherModel.getCellCountX() <= 0 ||
95                LauncherModel.getCellCountY() <= 0;
96
97        PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, name, intent);
98        if (mUseInstallQueue || launcherNotLoaded) {
99            mInstallQueue.add(info);
100        } else {
101            processInstallShortcut(context, info);
102        }
103    }
104
105    static void enableInstallQueue() {
106        mUseInstallQueue = true;
107    }
108    static void disableAndFlushInstallQueue(Context context) {
109        mUseInstallQueue = false;
110        flushInstallQueue(context);
111    }
112    static void flushInstallQueue(Context context) {
113        Iterator<PendingInstallShortcutInfo> iter = mInstallQueue.iterator();
114        while (iter.hasNext()) {
115            processInstallShortcut(context, iter.next());
116            iter.remove();
117        }
118    }
119
120    private static void processInstallShortcut(Context context,
121            PendingInstallShortcutInfo pendingInfo) {
122        String spKey = LauncherApplication.getSharedPreferencesKey();
123        SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE);
124
125        final Intent data = pendingInfo.data;
126        final Intent intent = pendingInfo.launchIntent;
127        final String name = pendingInfo.name;
128
129        // Lock on the app so that we don't try and get the items while apps are being added
130        LauncherApplication app = (LauncherApplication) context.getApplicationContext();
131        final int[] result = {INSTALL_SHORTCUT_SUCCESSFUL};
132        boolean found = false;
133        synchronized (app) {
134            final ArrayList<ItemInfo> items = LauncherModel.getItemsInLocalCoordinates(context);
135            final boolean exists = LauncherModel.shortcutExists(context, name, intent);
136
137            // Try adding to the workspace screens incrementally, starting at the default or center
138            // screen and alternating between +1, -1, +2, -2, etc. (using ~ ceil(i/2f)*(-1)^(i-1))
139            final int screen = Launcher.DEFAULT_SCREEN;
140            for (int i = 0; i < (2 * Launcher.SCREEN_COUNT) + 1 && !found; ++i) {
141                int si = screen + (int) ((i / 2f) + 0.5f) * ((i % 2 == 1) ? 1 : -1);
142                if (0 <= si && si < Launcher.SCREEN_COUNT) {
143                    found = installShortcut(context, data, items, name, intent, si, exists, sp,
144                            result);
145                }
146            }
147        }
148
149        // We only report error messages (duplicate shortcut or out of space) as the add-animation
150        // will provide feedback otherwise
151        if (!found) {
152            if (result[0] == INSTALL_SHORTCUT_NO_SPACE) {
153                Toast.makeText(context, context.getString(R.string.completely_out_of_space),
154                        Toast.LENGTH_SHORT).show();
155            } else if (result[0] == INSTALL_SHORTCUT_IS_DUPLICATE) {
156                Toast.makeText(context, context.getString(R.string.shortcut_duplicate, name),
157                        Toast.LENGTH_SHORT).show();
158            }
159        }
160    }
161
162    private static boolean installShortcut(Context context, Intent data, ArrayList<ItemInfo> items,
163            String name, Intent intent, final int screen, boolean shortcutExists,
164            final SharedPreferences sharedPrefs, int[] result) {
165        int[] tmpCoordinates = new int[2];
166        if (findEmptyCell(context, items, tmpCoordinates, screen)) {
167            if (intent != null) {
168                if (intent.getAction() == null) {
169                    intent.setAction(Intent.ACTION_VIEW);
170                } else if (intent.getAction().equals(Intent.ACTION_MAIN) &&
171                        intent.getCategories() != null &&
172                        intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) {
173                    intent.addFlags(
174                        Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
175                }
176
177                // By default, we allow for duplicate entries (located in
178                // different places)
179                boolean duplicate = data.getBooleanExtra(Launcher.EXTRA_SHORTCUT_DUPLICATE, true);
180                if (duplicate || !shortcutExists) {
181                    // If the new app is going to fall into the same page as before, then just
182                    // continue adding to the current page
183                    int newAppsScreen = sharedPrefs.getInt(NEW_APPS_PAGE_KEY, screen);
184                    Set<String> newApps = new HashSet<String>();
185                    if (newAppsScreen == screen) {
186                        newApps = sharedPrefs.getStringSet(NEW_APPS_LIST_KEY, newApps);
187                    }
188                    synchronized (newApps) {
189                        newApps.add(intent.toUri(0).toString());
190                    }
191                    final Set<String> savedNewApps = newApps;
192                    new Thread("setNewAppsThread") {
193                        public void run() {
194                            synchronized (savedNewApps) {
195                                sharedPrefs.edit()
196                                           .putInt(NEW_APPS_PAGE_KEY, screen)
197                                           .putStringSet(NEW_APPS_LIST_KEY, savedNewApps)
198                                           .commit();
199                            }
200                        }
201                    }.start();
202
203                    // Update the Launcher db
204                    LauncherApplication app = (LauncherApplication) context.getApplicationContext();
205                    ShortcutInfo info = app.getModel().addShortcut(context, data,
206                            LauncherSettings.Favorites.CONTAINER_DESKTOP, screen,
207                            tmpCoordinates[0], tmpCoordinates[1], true);
208                    if (info == null) {
209                        return false;
210                    }
211                } else {
212                    result[0] = INSTALL_SHORTCUT_IS_DUPLICATE;
213                }
214
215                return true;
216            }
217        } else {
218            result[0] = INSTALL_SHORTCUT_NO_SPACE;
219        }
220
221        return false;
222    }
223
224    private static boolean findEmptyCell(Context context, ArrayList<ItemInfo> items, int[] xy,
225            int screen) {
226        final int xCount = LauncherModel.getCellCountX();
227        final int yCount = LauncherModel.getCellCountY();
228        boolean[][] occupied = new boolean[xCount][yCount];
229
230        ItemInfo item = null;
231        int cellX, cellY, spanX, spanY;
232        for (int i = 0; i < items.size(); ++i) {
233            item = items.get(i);
234            if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
235                if (item.screen == screen) {
236                    cellX = item.cellX;
237                    cellY = item.cellY;
238                    spanX = item.spanX;
239                    spanY = item.spanY;
240                    for (int x = cellX; 0 <= x && x < cellX + spanX && x < xCount; x++) {
241                        for (int y = cellY; 0 <= y && y < cellY + spanY && y < yCount; y++) {
242                            occupied[x][y] = true;
243                        }
244                    }
245                }
246            }
247        }
248
249        return CellLayout.findVacantCell(xy, 1, 1, xCount, yCount, occupied);
250    }
251}
252