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.text.TextUtils;
28 import android.util.Base64;
29 import android.util.Log;
30 
31 import com.android.launcher3.compat.LauncherActivityInfoCompat;
32 import com.android.launcher3.compat.LauncherAppsCompat;
33 import com.android.launcher3.compat.UserHandleCompat;
34 import com.android.launcher3.compat.UserManagerCompat;
35 
36 import org.json.JSONException;
37 import org.json.JSONObject;
38 import org.json.JSONStringer;
39 import org.json.JSONTokener;
40 
41 import java.net.URISyntaxException;
42 import java.util.ArrayList;
43 import java.util.HashSet;
44 import java.util.Iterator;
45 import java.util.Set;
46 
47 public class InstallShortcutReceiver extends BroadcastReceiver {
48     private static final String TAG = "InstallShortcutReceiver";
49     private static final boolean DBG = false;
50 
51     private static final String ACTION_INSTALL_SHORTCUT =
52             "com.android.launcher.action.INSTALL_SHORTCUT";
53 
54     private static final String LAUNCH_INTENT_KEY = "intent.launch";
55     private static final String NAME_KEY = "name";
56     private static final String ICON_KEY = "icon";
57     private static final String ICON_RESOURCE_NAME_KEY = "iconResource";
58     private static final String ICON_RESOURCE_PACKAGE_NAME_KEY = "iconResourcePackage";
59 
60     private static final String APP_SHORTCUT_TYPE_KEY = "isAppShortcut";
61     private static final String USER_HANDLE_KEY = "userHandle";
62 
63     // The set of shortcuts that are pending install
64     private static final String APPS_PENDING_INSTALL = "apps_to_install";
65 
66     public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450;
67     public static final int NEW_SHORTCUT_STAGGER_DELAY = 85;
68 
69     private static final Object sLock = new Object();
70 
addToInstallQueue( SharedPreferences sharedPrefs, PendingInstallShortcutInfo info)71     private static void addToInstallQueue(
72             SharedPreferences sharedPrefs, PendingInstallShortcutInfo info) {
73         synchronized(sLock) {
74             String encoded = info.encodeToString();
75             if (encoded != null) {
76                 Set<String> strings = sharedPrefs.getStringSet(APPS_PENDING_INSTALL, null);
77                 if (strings == null) {
78                     strings = new HashSet<String>(1);
79                 } else {
80                     strings = new HashSet<String>(strings);
81                 }
82                 strings.add(encoded);
83                 sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, strings).commit();
84             }
85         }
86     }
87 
removeFromInstallQueue(Context context, ArrayList<String> packageNames, UserHandleCompat user)88     public static void removeFromInstallQueue(Context context, ArrayList<String> packageNames,
89             UserHandleCompat user) {
90         if (packageNames.isEmpty()) {
91             return;
92         }
93         String spKey = LauncherAppState.getSharedPreferencesKey();
94         SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE);
95         synchronized(sLock) {
96             Set<String> strings = sp.getStringSet(APPS_PENDING_INSTALL, null);
97             if (DBG) {
98                 Log.d(TAG, "APPS_PENDING_INSTALL: " + strings
99                         + ", removing packages: " + packageNames);
100             }
101             if (strings != null) {
102                 Set<String> newStrings = new HashSet<String>(strings);
103                 Iterator<String> newStringsIter = newStrings.iterator();
104                 while (newStringsIter.hasNext()) {
105                     String encoded = newStringsIter.next();
106                     PendingInstallShortcutInfo info = decode(encoded, context);
107                     if (info == null || (packageNames.contains(info.getTargetPackage())
108                             && user.equals(info.user))) {
109                         newStringsIter.remove();
110                     }
111                 }
112                 sp.edit().putStringSet(APPS_PENDING_INSTALL, newStrings).commit();
113             }
114         }
115     }
116 
getAndClearInstallQueue( SharedPreferences sharedPrefs, Context context)117     private static ArrayList<PendingInstallShortcutInfo> getAndClearInstallQueue(
118             SharedPreferences sharedPrefs, Context context) {
119         synchronized(sLock) {
120             Set<String> strings = sharedPrefs.getStringSet(APPS_PENDING_INSTALL, null);
121             if (DBG) Log.d(TAG, "Getting and clearing APPS_PENDING_INSTALL: " + strings);
122             if (strings == null) {
123                 return new ArrayList<PendingInstallShortcutInfo>();
124             }
125             ArrayList<PendingInstallShortcutInfo> infos =
126                 new ArrayList<PendingInstallShortcutInfo>();
127             for (String encoded : strings) {
128                 PendingInstallShortcutInfo info = decode(encoded, context);
129                 if (info != null) {
130                     infos.add(info);
131                 }
132             }
133             sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, new HashSet<String>()).commit();
134             return infos;
135         }
136     }
137 
138     // Determines whether to defer installing shortcuts immediately until
139     // processAllPendingInstalls() is called.
140     private static boolean mUseInstallQueue = false;
141 
onReceive(Context context, Intent data)142     public void onReceive(Context context, Intent data) {
143         if (!ACTION_INSTALL_SHORTCUT.equals(data.getAction())) {
144             return;
145         }
146 
147         if (DBG) Log.d(TAG, "Got INSTALL_SHORTCUT: " + data.toUri(0));
148         PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context);
149 
150         queuePendingShortcutInfo(info, context);
151     }
152 
queueInstallShortcut(LauncherActivityInfoCompat info, Context context)153     static void queueInstallShortcut(LauncherActivityInfoCompat info, Context context) {
154         queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, context), context);
155     }
156 
queuePendingShortcutInfo(PendingInstallShortcutInfo info, Context context)157     private static void queuePendingShortcutInfo(PendingInstallShortcutInfo info, Context context) {
158         // Queue the item up for adding if launcher has not loaded properly yet
159         LauncherAppState.setApplicationContext(context.getApplicationContext());
160         LauncherAppState app = LauncherAppState.getInstance();
161         boolean launcherNotLoaded = app.getModel().getCallback() == null;
162 
163         String spKey = LauncherAppState.getSharedPreferencesKey();
164         SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE);
165         addToInstallQueue(sp, info);
166         if (!mUseInstallQueue && !launcherNotLoaded) {
167             flushInstallQueue(context);
168         }
169     }
170 
enableInstallQueue()171     static void enableInstallQueue() {
172         mUseInstallQueue = true;
173     }
disableAndFlushInstallQueue(Context context)174     static void disableAndFlushInstallQueue(Context context) {
175         mUseInstallQueue = false;
176         flushInstallQueue(context);
177     }
flushInstallQueue(Context context)178     static void flushInstallQueue(Context context) {
179         String spKey = LauncherAppState.getSharedPreferencesKey();
180         SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE);
181         ArrayList<PendingInstallShortcutInfo> installQueue = getAndClearInstallQueue(sp, context);
182         if (!installQueue.isEmpty()) {
183             Iterator<PendingInstallShortcutInfo> iter = installQueue.iterator();
184             ArrayList<ItemInfo> addShortcuts = new ArrayList<ItemInfo>();
185             while (iter.hasNext()) {
186                 final PendingInstallShortcutInfo pendingInfo = iter.next();
187                 final Intent intent = pendingInfo.launchIntent;
188 
189                 if (LauncherAppState.isDisableAllApps() && !isValidShortcutLaunchIntent(intent)) {
190                     if (DBG) Log.d(TAG, "Ignoring shortcut with launchIntent:" + intent);
191                     continue;
192                 }
193 
194                 // If the intent specifies a package, make sure the package exists
195                 String packageName = pendingInfo.getTargetPackage();
196                 if (!TextUtils.isEmpty(packageName)) {
197                     UserHandleCompat myUserHandle = UserHandleCompat.myUserHandle();
198                     if (!LauncherModel.isValidPackage(context, packageName, myUserHandle)) {
199                         if (DBG) Log.d(TAG, "Ignoring shortcut for absent package:" + intent);
200                         continue;
201                     }
202                 }
203 
204                 final boolean exists = LauncherModel.shortcutExists(context, pendingInfo.label,
205                         intent, pendingInfo.user);
206                 if (!exists) {
207                     // Generate a shortcut info to add into the model
208                     addShortcuts.add(pendingInfo.getShortcutInfo());
209                 }
210             }
211 
212             // Add the new apps to the model and bind them
213             if (!addShortcuts.isEmpty()) {
214                 LauncherAppState app = LauncherAppState.getInstance();
215                 app.getModel().addAndBindAddedWorkspaceApps(context, addShortcuts);
216             }
217         }
218     }
219 
220     /**
221      * Returns true if the intent is a valid launch intent for a shortcut.
222      * This is used to identify shortcuts which are different from the ones exposed by the
223      * applications' manifest file.
224      *
225      * When DISABLE_ALL_APPS is true, shortcuts exposed via the app's manifest should never be
226      * duplicated or removed(unless the app is un-installed).
227      *
228      * @param launchIntent The intent that will be launched when the shortcut is clicked.
229      */
isValidShortcutLaunchIntent(Intent launchIntent)230     static boolean isValidShortcutLaunchIntent(Intent launchIntent) {
231         if (launchIntent != null
232                 && Intent.ACTION_MAIN.equals(launchIntent.getAction())
233                 && launchIntent.getComponent() != null
234                 && launchIntent.getCategories() != null
235                 && launchIntent.getCategories().size() == 1
236                 && launchIntent.hasCategory(Intent.CATEGORY_LAUNCHER)
237                 && launchIntent.getExtras() == null
238                 && TextUtils.isEmpty(launchIntent.getDataString())) {
239             return false;
240         }
241         return true;
242     }
243 
244     /**
245      * Ensures that we have a valid, non-null name.  If the provided name is null, we will return
246      * the application name instead.
247      */
ensureValidName(Context context, Intent intent, CharSequence name)248     private static CharSequence ensureValidName(Context context, Intent intent, CharSequence name) {
249         if (name == null) {
250             try {
251                 PackageManager pm = context.getPackageManager();
252                 ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0);
253                 name = info.loadLabel(pm).toString();
254             } catch (PackageManager.NameNotFoundException nnfe) {
255                 return "";
256             }
257         }
258         return name;
259     }
260 
261     private static class PendingInstallShortcutInfo {
262 
263         final LauncherActivityInfoCompat activityInfo;
264 
265         final Intent data;
266         final Context mContext;
267         final Intent launchIntent;
268         final String label;
269         final UserHandleCompat user;
270 
271         /**
272          * Initializes a PendingInstallShortcutInfo received from a different app.
273          */
PendingInstallShortcutInfo(Intent data, Context context)274         public PendingInstallShortcutInfo(Intent data, Context context) {
275             this.data = data;
276             mContext = context;
277 
278             launchIntent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
279             label = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
280             user = UserHandleCompat.myUserHandle();
281             activityInfo = null;
282         }
283 
284         /**
285          * Initializes a PendingInstallShortcutInfo to represent a launcher target.
286          */
PendingInstallShortcutInfo(LauncherActivityInfoCompat info, Context context)287         public PendingInstallShortcutInfo(LauncherActivityInfoCompat info, Context context) {
288             this.data = null;
289             mContext = context;
290             activityInfo = info;
291             user = info.getUser();
292 
293             launchIntent = AppInfo.makeLaunchIntent(context, info, user);
294             label = info.getLabel().toString();
295         }
296 
encodeToString()297         public String encodeToString() {
298             if (activityInfo != null) {
299                 try {
300                     // If it a launcher target, we only need component name, and user to
301                     // recreate this.
302                     return new JSONStringer()
303                         .object()
304                         .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0))
305                         .key(APP_SHORTCUT_TYPE_KEY).value(true)
306                         .key(USER_HANDLE_KEY).value(UserManagerCompat.getInstance(mContext)
307                                 .getSerialNumberForUser(user))
308                         .endObject().toString();
309                 } catch (JSONException e) {
310                     Log.d(TAG, "Exception when adding shortcut: " + e);
311                     return null;
312                 }
313             }
314 
315             if (launchIntent.getAction() == null) {
316                 launchIntent.setAction(Intent.ACTION_VIEW);
317             } else if (launchIntent.getAction().equals(Intent.ACTION_MAIN) &&
318                     launchIntent.getCategories() != null &&
319                     launchIntent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) {
320                 launchIntent.addFlags(
321                         Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
322             }
323 
324             // This name is only used for comparisons and notifications, so fall back to activity
325             // name if not supplied
326             String name = ensureValidName(mContext, launchIntent, label).toString();
327             Bitmap icon = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
328             Intent.ShortcutIconResource iconResource =
329                 data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
330 
331             // Only encode the parameters which are supported by the API.
332             try {
333                 JSONStringer json = new JSONStringer()
334                     .object()
335                     .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0))
336                     .key(NAME_KEY).value(name);
337                 if (icon != null) {
338                     byte[] iconByteArray = ItemInfo.flattenBitmap(icon);
339                     json = json.key(ICON_KEY).value(
340                             Base64.encodeToString(
341                                     iconByteArray, 0, iconByteArray.length, Base64.DEFAULT));
342                 }
343                 if (iconResource != null) {
344                     json = json.key(ICON_RESOURCE_NAME_KEY).value(iconResource.resourceName);
345                     json = json.key(ICON_RESOURCE_PACKAGE_NAME_KEY)
346                             .value(iconResource.packageName);
347                 }
348                 return json.endObject().toString();
349             } catch (JSONException e) {
350                 Log.d(TAG, "Exception when adding shortcut: " + e);
351             }
352             return null;
353         }
354 
getShortcutInfo()355         public ShortcutInfo getShortcutInfo() {
356             if (activityInfo != null) {
357                 final ShortcutInfo info = new ShortcutInfo();
358                 info.user = user;
359                 info.title = label;
360                 info.contentDescription = label;
361                 info.customIcon = false;
362                 info.intent = launchIntent;
363                 info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
364                 info.flags = AppInfo.initFlags(activityInfo);
365                 info.firstInstallTime = activityInfo.getFirstInstallTime();
366                 return info;
367             } else {
368                 return LauncherAppState.getInstance().getModel().infoFromShortcutIntent(mContext, data);
369             }
370         }
371 
getTargetPackage()372         public String getTargetPackage() {
373             String packageName = launchIntent.getPackage();
374             if (packageName == null) {
375                 packageName = launchIntent.getComponent() == null ? null :
376                     launchIntent.getComponent().getPackageName();
377             }
378             return packageName;
379         }
380     }
381 
decode(String encoded, Context context)382     private static PendingInstallShortcutInfo decode(String encoded, Context context) {
383         try {
384             JSONObject object = (JSONObject) new JSONTokener(encoded).nextValue();
385             Intent launcherIntent = Intent.parseUri(object.getString(LAUNCH_INTENT_KEY), 0);
386 
387             if (object.optBoolean(APP_SHORTCUT_TYPE_KEY)) {
388                 // The is an internal launcher target shortcut.
389                 UserHandleCompat user = UserManagerCompat.getInstance(context)
390                         .getUserForSerialNumber(object.getLong(USER_HANDLE_KEY));
391                 if (user == null) {
392                     return null;
393                 }
394 
395                 LauncherActivityInfoCompat info = LauncherAppsCompat.getInstance(context)
396                         .resolveActivity(launcherIntent, user);
397                 return info == null ? null : new PendingInstallShortcutInfo(info, context);
398             }
399 
400             Intent data = new Intent();
401             data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launcherIntent);
402             data.putExtra(Intent.EXTRA_SHORTCUT_NAME, object.getString(NAME_KEY));
403 
404             String iconBase64 = object.optString(ICON_KEY);
405             String iconResourceName = object.optString(ICON_RESOURCE_NAME_KEY);
406             String iconResourcePackageName = object.optString(ICON_RESOURCE_PACKAGE_NAME_KEY);
407             if (iconBase64 != null && !iconBase64.isEmpty()) {
408                 byte[] iconArray = Base64.decode(iconBase64, Base64.DEFAULT);
409                 Bitmap b = BitmapFactory.decodeByteArray(iconArray, 0, iconArray.length);
410                 data.putExtra(Intent.EXTRA_SHORTCUT_ICON, b);
411             } else if (iconResourceName != null && !iconResourceName.isEmpty()) {
412                 Intent.ShortcutIconResource iconResource =
413                     new Intent.ShortcutIconResource();
414                 iconResource.resourceName = iconResourceName;
415                 iconResource.packageName = iconResourcePackageName;
416                 data.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource);
417             }
418 
419             return new PendingInstallShortcutInfo(data, context);
420         } catch (JSONException e) {
421             Log.d(TAG, "Exception reading shortcut to add: " + e);
422         } catch (URISyntaxException e) {
423             Log.d(TAG, "Exception reading shortcut to add: " + e);
424         }
425         return null;
426     }
427 }
428