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.launcher3; 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 java.util.ArrayList; 32 import java.util.HashSet; 33 import java.util.Iterator; 34 import java.util.Set; 35 36 import org.json.*; 37 38 public class InstallShortcutReceiver extends BroadcastReceiver { 39 private static final String TAG = "InstallShortcutReceiver"; 40 private static final boolean DBG = false; 41 42 public static final String ACTION_INSTALL_SHORTCUT = 43 "com.android.launcher.action.INSTALL_SHORTCUT"; 44 45 public static final String DATA_INTENT_KEY = "intent.data"; 46 public static final String LAUNCH_INTENT_KEY = "intent.launch"; 47 public static final String NAME_KEY = "name"; 48 public static final String ICON_KEY = "icon"; 49 public static final String ICON_RESOURCE_NAME_KEY = "iconResource"; 50 public static final String ICON_RESOURCE_PACKAGE_NAME_KEY = "iconResourcePackage"; 51 // The set of shortcuts that are pending install 52 public static final String APPS_PENDING_INSTALL = "apps_to_install"; 53 54 public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450; 55 public static final int NEW_SHORTCUT_STAGGER_DELAY = 85; 56 57 private static final int INSTALL_SHORTCUT_SUCCESSFUL = 0; 58 private static final int INSTALL_SHORTCUT_IS_DUPLICATE = -1; 59 60 // A mime-type representing shortcut data 61 public static final String SHORTCUT_MIMETYPE = 62 "com.android.launcher3/shortcut"; 63 64 private static Object sLock = new Object(); 65 66 private static void addToStringSet(SharedPreferences sharedPrefs, 67 SharedPreferences.Editor editor, String key, String value) { 68 Set<String> strings = sharedPrefs.getStringSet(key, null); 69 if (strings == null) { 70 strings = new HashSet<String>(0); 71 } else { 72 strings = new HashSet<String>(strings); 73 } 74 strings.add(value); 75 editor.putStringSet(key, strings); 76 } 77 78 private static void addToInstallQueue( 79 SharedPreferences sharedPrefs, PendingInstallShortcutInfo info) { 80 synchronized(sLock) { 81 try { 82 JSONStringer json = new JSONStringer() 83 .object() 84 .key(DATA_INTENT_KEY).value(info.data.toUri(0)) 85 .key(LAUNCH_INTENT_KEY).value(info.launchIntent.toUri(0)) 86 .key(NAME_KEY).value(info.name); 87 if (info.icon != null) { 88 byte[] iconByteArray = ItemInfo.flattenBitmap(info.icon); 89 json = json.key(ICON_KEY).value( 90 Base64.encodeToString( 91 iconByteArray, 0, iconByteArray.length, Base64.DEFAULT)); 92 } 93 if (info.iconResource != null) { 94 json = json.key(ICON_RESOURCE_NAME_KEY).value(info.iconResource.resourceName); 95 json = json.key(ICON_RESOURCE_PACKAGE_NAME_KEY) 96 .value(info.iconResource.packageName); 97 } 98 json = json.endObject(); 99 SharedPreferences.Editor editor = sharedPrefs.edit(); 100 if (DBG) Log.d(TAG, "Adding to APPS_PENDING_INSTALL: " + json); 101 addToStringSet(sharedPrefs, editor, APPS_PENDING_INSTALL, json.toString()); 102 editor.commit(); 103 } catch (org.json.JSONException e) { 104 Log.d(TAG, "Exception when adding shortcut: " + e); 105 } 106 } 107 } 108 109 public static void removeFromInstallQueue(SharedPreferences sharedPrefs, 110 ArrayList<String> packageNames) { 111 synchronized(sLock) { 112 Set<String> strings = sharedPrefs.getStringSet(APPS_PENDING_INSTALL, null); 113 if (DBG) { 114 Log.d(TAG, "APPS_PENDING_INSTALL: " + strings 115 + ", removing packages: " + packageNames); 116 } 117 if (strings != null) { 118 Set<String> newStrings = new HashSet<String>(strings); 119 Iterator<String> newStringsIter = newStrings.iterator(); 120 while (newStringsIter.hasNext()) { 121 String json = newStringsIter.next(); 122 try { 123 JSONObject object = (JSONObject) new JSONTokener(json).nextValue(); 124 Intent launchIntent = Intent.parseUri(object.getString(LAUNCH_INTENT_KEY), 0); 125 String pn = launchIntent.getPackage(); 126 if (pn == null) { 127 pn = launchIntent.getComponent().getPackageName(); 128 } 129 if (packageNames.contains(pn)) { 130 newStringsIter.remove(); 131 } 132 } catch (org.json.JSONException e) { 133 Log.d(TAG, "Exception reading shortcut to remove: " + e); 134 } catch (java.net.URISyntaxException e) { 135 Log.d(TAG, "Exception reading shortcut to remove: " + e); 136 } 137 } 138 sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, 139 new HashSet<String>(newStrings)).commit(); 140 } 141 } 142 } 143 144 private static ArrayList<PendingInstallShortcutInfo> getAndClearInstallQueue( 145 SharedPreferences sharedPrefs) { 146 synchronized(sLock) { 147 Set<String> strings = sharedPrefs.getStringSet(APPS_PENDING_INSTALL, null); 148 if (DBG) Log.d(TAG, "Getting and clearing APPS_PENDING_INSTALL: " + strings); 149 if (strings == null) { 150 return new ArrayList<PendingInstallShortcutInfo>(); 151 } 152 ArrayList<PendingInstallShortcutInfo> infos = 153 new ArrayList<PendingInstallShortcutInfo>(); 154 for (String json : strings) { 155 try { 156 JSONObject object = (JSONObject) new JSONTokener(json).nextValue(); 157 Intent data = Intent.parseUri(object.getString(DATA_INTENT_KEY), 0); 158 Intent launchIntent = 159 Intent.parseUri(object.getString(LAUNCH_INTENT_KEY), 0); 160 String name = object.getString(NAME_KEY); 161 String iconBase64 = object.optString(ICON_KEY); 162 String iconResourceName = object.optString(ICON_RESOURCE_NAME_KEY); 163 String iconResourcePackageName = 164 object.optString(ICON_RESOURCE_PACKAGE_NAME_KEY); 165 if (iconBase64 != null && !iconBase64.isEmpty()) { 166 byte[] iconArray = Base64.decode(iconBase64, Base64.DEFAULT); 167 Bitmap b = BitmapFactory.decodeByteArray(iconArray, 0, iconArray.length); 168 data.putExtra(Intent.EXTRA_SHORTCUT_ICON, b); 169 } else if (iconResourceName != null && !iconResourceName.isEmpty()) { 170 Intent.ShortcutIconResource iconResource = 171 new Intent.ShortcutIconResource(); 172 iconResource.resourceName = iconResourceName; 173 iconResource.packageName = iconResourcePackageName; 174 data.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource); 175 } 176 data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launchIntent); 177 PendingInstallShortcutInfo info = 178 new PendingInstallShortcutInfo(data, name, launchIntent); 179 infos.add(info); 180 } catch (org.json.JSONException e) { 181 Log.d(TAG, "Exception reading shortcut to add: " + e); 182 } catch (java.net.URISyntaxException e) { 183 Log.d(TAG, "Exception reading shortcut to add: " + e); 184 } 185 } 186 sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, new HashSet<String>()).commit(); 187 return infos; 188 } 189 } 190 191 // Determines whether to defer installing shortcuts immediately until 192 // processAllPendingInstalls() is called. 193 private static boolean mUseInstallQueue = false; 194 195 private static class PendingInstallShortcutInfo { 196 Intent data; 197 Intent launchIntent; 198 String name; 199 Bitmap icon; 200 Intent.ShortcutIconResource iconResource; 201 202 public PendingInstallShortcutInfo(Intent rawData, String shortcutName, 203 Intent shortcutIntent) { 204 data = rawData; 205 name = shortcutName; 206 launchIntent = shortcutIntent; 207 } 208 } 209 210 public void onReceive(Context context, Intent data) { 211 if (!ACTION_INSTALL_SHORTCUT.equals(data.getAction())) { 212 return; 213 } 214 215 if (DBG) Log.d(TAG, "Got INSTALL_SHORTCUT: " + data.toUri(0)); 216 217 Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); 218 if (intent == null) { 219 return; 220 } 221 // This name is only used for comparisons and notifications, so fall back to activity name 222 // if not supplied 223 String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); 224 if (name == null) { 225 try { 226 PackageManager pm = context.getPackageManager(); 227 ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0); 228 name = info.loadLabel(pm).toString(); 229 } catch (PackageManager.NameNotFoundException nnfe) { 230 return; 231 } 232 } 233 Bitmap icon = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON); 234 Intent.ShortcutIconResource iconResource = 235 data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE); 236 237 // Queue the item up for adding if launcher has not loaded properly yet 238 LauncherAppState.setApplicationContext(context.getApplicationContext()); 239 LauncherAppState app = LauncherAppState.getInstance(); 240 boolean launcherNotLoaded = (app.getDynamicGrid() == null); 241 242 PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, name, intent); 243 info.icon = icon; 244 info.iconResource = iconResource; 245 246 String spKey = LauncherAppState.getSharedPreferencesKey(); 247 SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); 248 addToInstallQueue(sp, info); 249 if (!mUseInstallQueue && !launcherNotLoaded) { 250 flushInstallQueue(context); 251 } 252 } 253 254 static void enableInstallQueue() { 255 mUseInstallQueue = true; 256 } 257 static void disableAndFlushInstallQueue(Context context) { 258 mUseInstallQueue = false; 259 flushInstallQueue(context); 260 } 261 static void flushInstallQueue(Context context) { 262 String spKey = LauncherAppState.getSharedPreferencesKey(); 263 SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); 264 ArrayList<PendingInstallShortcutInfo> installQueue = getAndClearInstallQueue(sp); 265 if (!installQueue.isEmpty()) { 266 Iterator<PendingInstallShortcutInfo> iter = installQueue.iterator(); 267 ArrayList<ItemInfo> addShortcuts = new ArrayList<ItemInfo>(); 268 int result = INSTALL_SHORTCUT_SUCCESSFUL; 269 String duplicateName = ""; 270 while (iter.hasNext()) { 271 final PendingInstallShortcutInfo pendingInfo = iter.next(); 272 //final Intent data = pendingInfo.data; 273 final Intent intent = pendingInfo.launchIntent; 274 final String name = pendingInfo.name; 275 final boolean exists = LauncherModel.shortcutExists(context, name, intent); 276 //final boolean allowDuplicate = data.getBooleanExtra(Launcher.EXTRA_SHORTCUT_DUPLICATE, true); 277 278 // TODO-XXX: Disable duplicates for now 279 if (!exists /* && allowDuplicate */) { 280 // Generate a shortcut info to add into the model 281 ShortcutInfo info = getShortcutInfo(context, pendingInfo.data, 282 pendingInfo.launchIntent); 283 addShortcuts.add(info); 284 } 285 /* 286 else if (exists && !allowDuplicate) { 287 result = INSTALL_SHORTCUT_IS_DUPLICATE; 288 duplicateName = name; 289 } 290 */ 291 } 292 293 // Notify the user once if we weren't able to place any duplicates 294 if (result == INSTALL_SHORTCUT_IS_DUPLICATE) { 295 Toast.makeText(context, context.getString(R.string.shortcut_duplicate, 296 duplicateName), Toast.LENGTH_SHORT).show(); 297 } 298 299 // Add the new apps to the model and bind them 300 if (!addShortcuts.isEmpty()) { 301 LauncherAppState app = LauncherAppState.getInstance(); 302 app.getModel().addAndBindAddedApps(context, addShortcuts, null); 303 } 304 } 305 } 306 307 private static ShortcutInfo getShortcutInfo(Context context, Intent data, 308 Intent launchIntent) { 309 if (launchIntent.getAction() == null) { 310 launchIntent.setAction(Intent.ACTION_VIEW); 311 } else if (launchIntent.getAction().equals(Intent.ACTION_MAIN) && 312 launchIntent.getCategories() != null && 313 launchIntent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { 314 launchIntent.addFlags( 315 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 316 } 317 LauncherAppState app = LauncherAppState.getInstance(); 318 return app.getModel().infoFromShortcutIntent(context, data, null); 319 } 320 } 321