1 /* 2 * Copyright (C) 2018 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.model; 17 18 import static android.app.PendingIntent.FLAG_IMMUTABLE; 19 import static android.app.PendingIntent.FLAG_ONE_SHOT; 20 import static android.os.Process.myUserHandle; 21 22 import static com.android.launcher3.pm.InstallSessionHelper.getUserHandle; 23 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 24 25 import static java.util.stream.Collectors.groupingBy; 26 import static java.util.stream.Collectors.mapping; 27 28 import android.app.PendingIntent; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.pm.PackageInstaller.SessionInfo; 32 import android.os.UserHandle; 33 import android.util.Log; 34 35 import androidx.annotation.AnyThread; 36 import androidx.annotation.WorkerThread; 37 38 import com.android.launcher3.LauncherSettings; 39 import com.android.launcher3.model.data.FolderInfo; 40 import com.android.launcher3.model.data.ItemInfo; 41 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 42 import com.android.launcher3.model.data.WorkspaceItemInfo; 43 import com.android.launcher3.util.PackageUserKey; 44 45 import java.util.ArrayList; 46 import java.util.Collections; 47 import java.util.HashMap; 48 import java.util.HashSet; 49 import java.util.List; 50 import java.util.Set; 51 import java.util.stream.Collectors; 52 53 /** 54 * Helper class to send broadcasts to package installers that have: 55 * - Items on the first screen 56 * - Items with an active install session 57 * 58 * The packages are broken down by: folder items, workspace items, hotseat items, and widgets. 59 * 60 * Package installers only receive data for items that they are installing. 61 */ 62 public class FirstScreenBroadcast { 63 64 private static final String TAG = "FirstScreenBroadcast"; 65 private static final boolean DEBUG = false; 66 67 private static final String ACTION_FIRST_SCREEN_ACTIVE_INSTALLS 68 = "com.android.launcher3.action.FIRST_SCREEN_ACTIVE_INSTALLS"; 69 70 private static final String FOLDER_ITEM_EXTRA = "folderItem"; 71 private static final String WORKSPACE_ITEM_EXTRA = "workspaceItem"; 72 private static final String HOTSEAT_ITEM_EXTRA = "hotseatItem"; 73 private static final String WIDGET_ITEM_EXTRA = "widgetItem"; 74 75 private static final String VERIFICATION_TOKEN_EXTRA = "verificationToken"; 76 77 private final HashMap<PackageUserKey, SessionInfo> mSessionInfoForPackage; 78 FirstScreenBroadcast(HashMap<PackageUserKey, SessionInfo> sessionInfoForPackage)79 public FirstScreenBroadcast(HashMap<PackageUserKey, SessionInfo> sessionInfoForPackage) { 80 mSessionInfoForPackage = sessionInfoForPackage; 81 } 82 83 /** 84 * Sends a broadcast to all package installers that have items with active sessions on the users 85 * first screen. 86 */ 87 @WorkerThread sendBroadcasts(Context context, List<ItemInfo> firstScreenItems)88 public void sendBroadcasts(Context context, List<ItemInfo> firstScreenItems) { 89 UserHandle myUser = myUserHandle(); 90 mSessionInfoForPackage 91 .values() 92 .stream() 93 .filter(info -> myUser.equals(getUserHandle(info))) 94 .collect(groupingBy(SessionInfo::getInstallerPackageName, 95 mapping(SessionInfo::getAppPackageName, Collectors.toSet()))) 96 .forEach((installer, packages) -> 97 sendBroadcastToInstaller(context, installer, packages, firstScreenItems)); 98 } 99 100 /** 101 * @param installerPackageName Package name of the package installer. 102 * @param packages List of packages with active sessions for this package installer. 103 * @param firstScreenItems List of items on the first screen. 104 */ 105 @WorkerThread sendBroadcastToInstaller(Context context, String installerPackageName, Set<String> packages, List<ItemInfo> firstScreenItems)106 private void sendBroadcastToInstaller(Context context, String installerPackageName, 107 Set<String> packages, List<ItemInfo> firstScreenItems) { 108 Set<String> folderItems = new HashSet<>(); 109 Set<String> workspaceItems = new HashSet<>(); 110 Set<String> hotseatItems = new HashSet<>(); 111 Set<String> widgetItems = new HashSet<>(); 112 113 for (ItemInfo info : firstScreenItems) { 114 if (info instanceof FolderInfo) { 115 FolderInfo folderInfo = (FolderInfo) info; 116 String folderItemInfoPackage; 117 for (ItemInfo folderItemInfo : cloneOnMainThread(folderInfo.contents)) { 118 folderItemInfoPackage = getPackageName(folderItemInfo); 119 if (folderItemInfoPackage != null 120 && packages.contains(folderItemInfoPackage)) { 121 folderItems.add(folderItemInfoPackage); 122 } 123 } 124 } 125 126 String packageName = getPackageName(info); 127 if (packageName == null || !packages.contains(packageName)) { 128 continue; 129 } 130 if (info instanceof LauncherAppWidgetInfo) { 131 widgetItems.add(packageName); 132 } else if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { 133 hotseatItems.add(packageName); 134 } else if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { 135 workspaceItems.add(packageName); 136 } 137 } 138 139 if (DEBUG) { 140 printList(installerPackageName, "Folder item", folderItems); 141 printList(installerPackageName, "Workspace item", workspaceItems); 142 printList(installerPackageName, "Hotseat item", hotseatItems); 143 printList(installerPackageName, "Widget item", widgetItems); 144 } 145 146 if (folderItems.isEmpty() 147 && workspaceItems.isEmpty() 148 && hotseatItems.isEmpty() 149 && widgetItems.isEmpty()) { 150 // Avoid sending broadcast if there is nothing to send. 151 return; 152 } 153 context.sendBroadcast(new Intent(ACTION_FIRST_SCREEN_ACTIVE_INSTALLS) 154 .setPackage(installerPackageName) 155 .putStringArrayListExtra(FOLDER_ITEM_EXTRA, new ArrayList<>(folderItems)) 156 .putStringArrayListExtra(WORKSPACE_ITEM_EXTRA, new ArrayList<>(workspaceItems)) 157 .putStringArrayListExtra(HOTSEAT_ITEM_EXTRA, new ArrayList<>(hotseatItems)) 158 .putStringArrayListExtra(WIDGET_ITEM_EXTRA, new ArrayList<>(widgetItems)) 159 .putExtra(VERIFICATION_TOKEN_EXTRA, PendingIntent.getActivity(context, 0, 160 new Intent(), FLAG_ONE_SHOT | FLAG_IMMUTABLE))); 161 } 162 getPackageName(ItemInfo info)163 private static String getPackageName(ItemInfo info) { 164 String packageName = null; 165 if (info instanceof LauncherAppWidgetInfo) { 166 LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) info; 167 if (widgetInfo.providerName != null) { 168 packageName = widgetInfo.providerName.getPackageName(); 169 } 170 } else if (info.getTargetComponent() != null){ 171 packageName = info.getTargetComponent().getPackageName(); 172 } 173 return packageName; 174 } 175 printList(String packageInstaller, String label, Set<String> packages)176 private static void printList(String packageInstaller, String label, Set<String> packages) { 177 for (String pkg : packages) { 178 Log.d(TAG, packageInstaller + ":" + label + ":" + pkg); 179 } 180 } 181 182 /** 183 * Clone the provided list on UI thread. This is used for {@link FolderInfo#contents} which 184 * is always modified on UI thread. 185 */ 186 @AnyThread cloneOnMainThread(ArrayList<WorkspaceItemInfo> list)187 private static List<WorkspaceItemInfo> cloneOnMainThread(ArrayList<WorkspaceItemInfo> list) { 188 try { 189 return MAIN_EXECUTOR.submit(() -> new ArrayList(list)).get(); 190 } catch (Exception e) { 191 return Collections.emptyList(); 192 } 193 } 194 } 195