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