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