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.widget.Toast; 26 27 import com.android.launcher.R; 28 29 import java.util.ArrayList; 30 import java.util.HashSet; 31 import java.util.Iterator; 32 import java.util.Set; 33 34 public 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