1 /**
2  * Copyright (C) 2019 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 package com.android.launcher3.appprediction;
17 
18 import static android.content.pm.PackageManager.MATCH_INSTANT;
19 
20 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
21 import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
22 
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.ApplicationInfo;
26 import android.content.pm.PackageManager;
27 import android.content.pm.ResolveInfo;
28 import android.content.pm.ShortcutInfo;
29 import android.net.Uri;
30 import android.os.Handler;
31 import android.os.Looper;
32 import android.os.Message;
33 import android.util.ArrayMap;
34 import android.util.Log;
35 
36 import androidx.annotation.MainThread;
37 import androidx.annotation.Nullable;
38 import androidx.annotation.UiThread;
39 import androidx.annotation.WorkerThread;
40 
41 import com.android.launcher3.LauncherAppState;
42 import com.android.launcher3.allapps.AllAppsStore;
43 import com.android.launcher3.icons.IconCache;
44 import com.android.launcher3.model.data.AppInfo;
45 import com.android.launcher3.model.data.WorkspaceItemInfo;
46 import com.android.launcher3.shortcuts.ShortcutKey;
47 import com.android.launcher3.shortcuts.ShortcutRequest;
48 import com.android.launcher3.util.ComponentKey;
49 import com.android.launcher3.util.InstantAppResolver;
50 
51 import java.util.ArrayList;
52 import java.util.Collections;
53 import java.util.HashMap;
54 import java.util.List;
55 import java.util.Map;
56 
57 /**
58  * Utility class which loads and caches predicted items like instant apps and shortcuts, before
59  * they can be displayed on the UI
60  */
61 public class DynamicItemCache {
62 
63     private static final String TAG = "DynamicItemCache";
64     private static final boolean DEBUG = false;
65     private static final String DEFAULT_URL = "default-url";
66 
67     private static final int BG_MSG_LOAD_SHORTCUTS = 1;
68     private static final int BG_MSG_LOAD_INSTANT_APPS = 2;
69 
70     private static final int UI_MSG_UPDATE_SHORTCUTS = 1;
71     private static final int UI_MSG_UPDATE_INSTANT_APPS = 2;
72 
73     private final Context mContext;
74     private final Handler mWorker;
75     private final Handler mUiHandler;
76     private final InstantAppResolver mInstantAppResolver;
77     private final Runnable mOnUpdateCallback;
78     private final IconCache mIconCache;
79 
80     private final Map<ComponentKey, WorkspaceItemInfo> mShortcuts;
81     private final Map<String, InstantAppItemInfo> mInstantApps;
82 
DynamicItemCache(Context context, Runnable onUpdateCallback)83     public DynamicItemCache(Context context, Runnable onUpdateCallback) {
84         mContext = context;
85         mWorker = new Handler(MODEL_EXECUTOR.getLooper(), this::handleWorkerMessage);
86         mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage);
87         mInstantAppResolver = InstantAppResolver.newInstance(context);
88         mOnUpdateCallback = onUpdateCallback;
89         mIconCache = LauncherAppState.getInstance(mContext).getIconCache();
90 
91         mShortcuts = new HashMap<>();
92         mInstantApps = new HashMap<>();
93     }
94 
cacheItems(List<ShortcutKey> shortcutKeys, List<String> pkgNames)95     public void cacheItems(List<ShortcutKey> shortcutKeys, List<String> pkgNames) {
96         if (!shortcutKeys.isEmpty()) {
97             mWorker.removeMessages(BG_MSG_LOAD_SHORTCUTS);
98             Message.obtain(mWorker, BG_MSG_LOAD_SHORTCUTS, shortcutKeys).sendToTarget();
99         }
100         if (!pkgNames.isEmpty()) {
101             mWorker.removeMessages(BG_MSG_LOAD_INSTANT_APPS);
102             Message.obtain(mWorker, BG_MSG_LOAD_INSTANT_APPS, pkgNames).sendToTarget();
103         }
104     }
105 
handleWorkerMessage(Message msg)106     private boolean handleWorkerMessage(Message msg) {
107         switch (msg.what) {
108             case BG_MSG_LOAD_SHORTCUTS: {
109                 List<ShortcutKey> shortcutKeys = msg.obj != null ?
110                         (List<ShortcutKey>) msg.obj : Collections.EMPTY_LIST;
111                 Map<ShortcutKey, WorkspaceItemInfo> shortcutKeyAndInfos = new ArrayMap<>();
112                 for (ShortcutKey shortcutKey : shortcutKeys) {
113                     WorkspaceItemInfo workspaceItemInfo = loadShortcutWorker(shortcutKey);
114                     if (workspaceItemInfo != null) {
115                         shortcutKeyAndInfos.put(shortcutKey, workspaceItemInfo);
116                     }
117                 }
118                 Message.obtain(mUiHandler, UI_MSG_UPDATE_SHORTCUTS, shortcutKeyAndInfos)
119                         .sendToTarget();
120                 return true;
121             }
122             case BG_MSG_LOAD_INSTANT_APPS: {
123                 List<String> pkgNames = msg.obj != null ?
124                         (List<String>) msg.obj : Collections.EMPTY_LIST;
125                 List<InstantAppItemInfo> instantAppItemInfos = new ArrayList<>();
126                 for (String pkgName : pkgNames) {
127                     InstantAppItemInfo instantAppItemInfo = loadInstantApp(pkgName);
128                     if (instantAppItemInfo != null) {
129                         instantAppItemInfos.add(instantAppItemInfo);
130                     }
131                 }
132                 Message.obtain(mUiHandler, UI_MSG_UPDATE_INSTANT_APPS, instantAppItemInfos)
133                         .sendToTarget();
134                 return true;
135             }
136         }
137 
138         return false;
139     }
140 
handleUiMessage(Message msg)141     private boolean handleUiMessage(Message msg) {
142         switch (msg.what) {
143             case UI_MSG_UPDATE_SHORTCUTS: {
144                 mShortcuts.clear();
145                 mShortcuts.putAll((Map<ShortcutKey, WorkspaceItemInfo>) msg.obj);
146                 mOnUpdateCallback.run();
147                 return true;
148             }
149             case UI_MSG_UPDATE_INSTANT_APPS: {
150                 List<InstantAppItemInfo> instantAppItemInfos = (List<InstantAppItemInfo>) msg.obj;
151                 mInstantApps.clear();
152                 for (InstantAppItemInfo instantAppItemInfo : instantAppItemInfos) {
153                     mInstantApps.put(instantAppItemInfo.getTargetComponent().getPackageName(),
154                             instantAppItemInfo);
155                 }
156                 mOnUpdateCallback.run();
157                 if (DEBUG) {
158                     Log.d(TAG, String.format("Cache size: %d, Cache: %s",
159                             mInstantApps.size(), mInstantApps.toString()));
160                 }
161                 return true;
162             }
163         }
164 
165         return false;
166     }
167 
168     @WorkerThread
loadShortcutWorker(ShortcutKey shortcutKey)169     private WorkspaceItemInfo loadShortcutWorker(ShortcutKey shortcutKey) {
170         List<ShortcutInfo> details = shortcutKey.buildRequest(mContext).query(ShortcutRequest.ALL);
171         if (!details.isEmpty()) {
172             WorkspaceItemInfo si = new WorkspaceItemInfo(details.get(0), mContext);
173             mIconCache.getShortcutIcon(si, details.get(0));
174             return si;
175         }
176         if (DEBUG) {
177             Log.d(TAG, "No shortcut found: " + shortcutKey.toString());
178         }
179         return null;
180     }
181 
loadInstantApp(String pkgName)182     private InstantAppItemInfo loadInstantApp(String pkgName) {
183         PackageManager pm = mContext.getPackageManager();
184 
185         try {
186             ApplicationInfo ai = pm.getApplicationInfo(pkgName, 0);
187             if (!mInstantAppResolver.isInstantApp(ai)) {
188                 return null;
189             }
190         } catch (PackageManager.NameNotFoundException e) {
191             return null;
192         }
193 
194         String url = retrieveDefaultUrl(pkgName, pm);
195         if (url == null) {
196             Log.w(TAG, "no default-url available for pkg " + pkgName);
197             return null;
198         }
199 
200         Intent intent = new Intent(Intent.ACTION_VIEW)
201                 .addCategory(Intent.CATEGORY_BROWSABLE)
202                 .setData(Uri.parse(url));
203         InstantAppItemInfo info = new InstantAppItemInfo(intent, pkgName);
204         IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
205         iconCache.getTitleAndIcon(info, false);
206         if (info.bitmap.icon == null || iconCache.isDefaultIcon(info.bitmap, info.user)) {
207             return null;
208         }
209         return info;
210     }
211 
212     @Nullable
retrieveDefaultUrl(String pkgName, PackageManager pm)213     public static String retrieveDefaultUrl(String pkgName, PackageManager pm) {
214         Intent mainIntent = new Intent().setAction(Intent.ACTION_MAIN)
215                 .addCategory(Intent.CATEGORY_LAUNCHER).setPackage(pkgName);
216         List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
217                 mainIntent, MATCH_INSTANT | PackageManager.GET_META_DATA);
218         String url = null;
219         for (ResolveInfo resolveInfo : resolveInfos) {
220             if (resolveInfo.activityInfo.metaData != null
221                     && resolveInfo.activityInfo.metaData.containsKey(DEFAULT_URL)) {
222                 url = resolveInfo.activityInfo.metaData.getString(DEFAULT_URL);
223             }
224         }
225         return url;
226     }
227 
228     @UiThread
getInstantApp(String pkgName)229     public InstantAppItemInfo getInstantApp(String pkgName) {
230         return mInstantApps.get(pkgName);
231     }
232 
233     @MainThread
getShortcutInfo(ComponentKey key)234     public WorkspaceItemInfo getShortcutInfo(ComponentKey key) {
235         return mShortcuts.get(key);
236     }
237 
238     /**
239      * requests and caches icons for app targets
240      */
updateDependencies(List<ComponentKeyMapper> componentKeyMappers, AllAppsStore appsStore, IconCache.ItemInfoUpdateReceiver callback, int itemCount)241     public void updateDependencies(List<ComponentKeyMapper> componentKeyMappers,
242             AllAppsStore appsStore, IconCache.ItemInfoUpdateReceiver callback, int itemCount) {
243         List<String> instantAppsToLoad = new ArrayList<>();
244         List<ShortcutKey> shortcutsToLoad = new ArrayList<>();
245         int total = componentKeyMappers.size();
246         for (int i = 0, count = 0; i < total && count < itemCount; i++) {
247             ComponentKeyMapper mapper = componentKeyMappers.get(i);
248             // Update instant apps
249             if (COMPONENT_CLASS_MARKER.equals(mapper.getComponentClass())) {
250                 instantAppsToLoad.add(mapper.getPackage());
251                 count++;
252             } else if (mapper.getComponentKey() instanceof ShortcutKey) {
253                 shortcutsToLoad.add((ShortcutKey) mapper.getComponentKey());
254                 count++;
255             } else {
256                 // Reload high res icon
257                 AppInfo info = (AppInfo) mapper.getApp(appsStore);
258                 if (info != null) {
259                     if (info.usingLowResIcon()) {
260                         mIconCache.updateIconInBackground(callback, info);
261                     }
262                     count++;
263                 }
264             }
265         }
266         cacheItems(shortcutsToLoad, instantAppsToLoad);
267     }
268 }
269