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