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