1 /* 2 * Copyright (C) 2017 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.popup; 18 19 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_SHORTCUTS; 20 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.pm.ShortcutInfo; 24 import android.os.Handler; 25 import android.os.UserHandle; 26 27 import androidx.annotation.Nullable; 28 import androidx.annotation.VisibleForTesting; 29 30 import com.android.launcher3.LauncherAppState; 31 import com.android.launcher3.icons.IconCache; 32 import com.android.launcher3.model.data.ItemInfo; 33 import com.android.launcher3.model.data.WorkspaceItemInfo; 34 import com.android.launcher3.notification.NotificationInfo; 35 import com.android.launcher3.notification.NotificationKeyData; 36 import com.android.launcher3.notification.NotificationListener; 37 import com.android.launcher3.shortcuts.DeepShortcutView; 38 import com.android.launcher3.shortcuts.ShortcutRequest; 39 import com.android.launcher3.views.ActivityContext; 40 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.Comparator; 44 import java.util.Iterator; 45 import java.util.List; 46 import java.util.stream.Collectors; 47 48 /** 49 * Contains logic relevant to populating a {@link PopupContainerWithArrow}. In particular, 50 * this class determines which items appear in the container, and in what order. 51 */ 52 public class PopupPopulator { 53 54 public static final int MAX_SHORTCUTS = 4; 55 @VisibleForTesting static final int NUM_DYNAMIC = 2; 56 public static final int MAX_SHORTCUTS_IF_NOTIFICATIONS = 2; 57 58 /** 59 * Sorts shortcuts in rank order, with manifest shortcuts coming before dynamic shortcuts. 60 */ 61 private static final Comparator<ShortcutInfo> SHORTCUT_RANK_COMPARATOR 62 = new Comparator<ShortcutInfo>() { 63 @Override 64 public int compare(ShortcutInfo a, ShortcutInfo b) { 65 if (a.isDeclaredInManifest() && !b.isDeclaredInManifest()) { 66 return -1; 67 } 68 if (!a.isDeclaredInManifest() && b.isDeclaredInManifest()) { 69 return 1; 70 } 71 return Integer.compare(a.getRank(), b.getRank()); 72 } 73 }; 74 75 /** 76 * Filters the shortcuts so that only MAX_SHORTCUTS or fewer shortcuts are retained. 77 * We want the filter to include both static and dynamic shortcuts, so we always 78 * include NUM_DYNAMIC dynamic shortcuts, if at least that many are present. 79 * 80 * @param shortcutIdToRemoveFirst An id that should be filtered out first, if any. 81 * @return a subset of shortcuts, in sorted order, with size <= MAX_SHORTCUTS. 82 */ sortAndFilterShortcuts( List<ShortcutInfo> shortcuts, @Nullable String shortcutIdToRemoveFirst)83 public static List<ShortcutInfo> sortAndFilterShortcuts( 84 List<ShortcutInfo> shortcuts, @Nullable String shortcutIdToRemoveFirst) { 85 // Remove up to one specific shortcut before sorting and doing somewhat fancy filtering. 86 if (shortcutIdToRemoveFirst != null) { 87 Iterator<ShortcutInfo> shortcutIterator = shortcuts.iterator(); 88 while (shortcutIterator.hasNext()) { 89 if (shortcutIterator.next().getId().equals(shortcutIdToRemoveFirst)) { 90 shortcutIterator.remove(); 91 break; 92 } 93 } 94 } 95 96 Collections.sort(shortcuts, SHORTCUT_RANK_COMPARATOR); 97 if (shortcuts.size() <= MAX_SHORTCUTS) { 98 return shortcuts; 99 } 100 101 // The list of shortcuts is now sorted with static shortcuts followed by dynamic 102 // shortcuts. We want to preserve this order, but only keep MAX_SHORTCUTS. 103 List<ShortcutInfo> filteredShortcuts = new ArrayList<>(MAX_SHORTCUTS); 104 int numDynamic = 0; 105 int size = shortcuts.size(); 106 for (int i = 0; i < size; i++) { 107 ShortcutInfo shortcut = shortcuts.get(i); 108 int filteredSize = filteredShortcuts.size(); 109 if (filteredSize < MAX_SHORTCUTS) { 110 // Always add the first MAX_SHORTCUTS to the filtered list. 111 filteredShortcuts.add(shortcut); 112 if (shortcut.isDynamic()) { 113 numDynamic++; 114 } 115 continue; 116 } 117 // At this point, we have MAX_SHORTCUTS already, but they may all be static. 118 // If there are dynamic shortcuts, remove static shortcuts to add them. 119 if (shortcut.isDynamic() && numDynamic < NUM_DYNAMIC) { 120 numDynamic++; 121 int lastStaticIndex = filteredSize - numDynamic; 122 filteredShortcuts.remove(lastStaticIndex); 123 filteredShortcuts.add(shortcut); 124 } 125 } 126 return filteredShortcuts; 127 } 128 129 /** 130 * Returns a runnable to update the provided shortcuts and notifications 131 */ createUpdateRunnable( final T context, final ItemInfo originalInfo, final Handler uiHandler, final PopupContainerWithArrow container, final List<DeepShortcutView> shortcutViews, final List<NotificationKeyData> notificationKeys)132 public static <T extends Context & ActivityContext> Runnable createUpdateRunnable( 133 final T context, 134 final ItemInfo originalInfo, 135 final Handler uiHandler, final PopupContainerWithArrow container, 136 final List<DeepShortcutView> shortcutViews, 137 final List<NotificationKeyData> notificationKeys) { 138 final ComponentName activity = originalInfo.getTargetComponent(); 139 final UserHandle user = originalInfo.user; 140 return () -> { 141 if (!notificationKeys.isEmpty()) { 142 NotificationListener notificationListener = 143 NotificationListener.getInstanceIfConnected(); 144 final List<NotificationInfo> infos; 145 if (notificationListener == null) { 146 infos = Collections.emptyList(); 147 } else { 148 infos = notificationListener.getNotificationsForKeys(notificationKeys).stream() 149 .map(sbn -> new NotificationInfo(context, sbn, originalInfo)) 150 .collect(Collectors.toList()); 151 } 152 uiHandler.post(() -> container.applyNotificationInfos(infos)); 153 } 154 155 List<ShortcutInfo> shortcuts = new ShortcutRequest(context, user) 156 .withContainer(activity) 157 .query(ShortcutRequest.PUBLISHED); 158 String shortcutIdToDeDupe = notificationKeys.isEmpty() ? null 159 : notificationKeys.get(0).shortcutId; 160 shortcuts = PopupPopulator.sortAndFilterShortcuts(shortcuts, shortcutIdToDeDupe); 161 IconCache cache = LauncherAppState.getInstance(context).getIconCache(); 162 for (int i = 0; i < shortcuts.size() && i < shortcutViews.size(); i++) { 163 final ShortcutInfo shortcut = shortcuts.get(i); 164 final WorkspaceItemInfo si = new WorkspaceItemInfo(shortcut, context); 165 cache.getUnbadgedShortcutIcon(si, shortcut); 166 si.rank = i; 167 si.container = CONTAINER_SHORTCUTS; 168 169 final DeepShortcutView view = shortcutViews.get(i); 170 uiHandler.post(() -> view.applyShortcutInfo(si, shortcut, container)); 171 } 172 }; 173 } 174 } 175