1 /* 2 * Copyright (C) 2021 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.taskbar; 17 18 import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition; 19 20 import android.content.Intent; 21 import android.content.pm.LauncherApps; 22 import android.graphics.Point; 23 import android.util.Pair; 24 import android.view.MotionEvent; 25 import android.view.View; 26 27 import androidx.annotation.NonNull; 28 29 import com.android.internal.logging.InstanceId; 30 import com.android.launcher3.AbstractFloatingView; 31 import com.android.launcher3.BubbleTextView; 32 import com.android.launcher3.LauncherSettings; 33 import com.android.launcher3.R; 34 import com.android.launcher3.dot.FolderDotInfo; 35 import com.android.launcher3.folder.Folder; 36 import com.android.launcher3.folder.FolderIcon; 37 import com.android.launcher3.model.data.FolderInfo; 38 import com.android.launcher3.model.data.ItemInfo; 39 import com.android.launcher3.model.data.WorkspaceItemInfo; 40 import com.android.launcher3.notification.NotificationListener; 41 import com.android.launcher3.popup.PopupContainerWithArrow; 42 import com.android.launcher3.popup.PopupDataProvider; 43 import com.android.launcher3.popup.SystemShortcut; 44 import com.android.launcher3.shortcuts.DeepShortcutView; 45 import com.android.launcher3.splitscreen.SplitShortcut; 46 import com.android.launcher3.util.ComponentKey; 47 import com.android.launcher3.util.LauncherBindableItemsContainer; 48 import com.android.launcher3.util.PackageUserKey; 49 import com.android.launcher3.util.ShortcutUtil; 50 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; 51 import com.android.launcher3.views.ActivityContext; 52 import com.android.quickstep.SystemUiProxy; 53 import com.android.quickstep.util.LogUtils; 54 55 import java.io.PrintWriter; 56 import java.util.HashMap; 57 import java.util.List; 58 import java.util.Objects; 59 import java.util.function.Predicate; 60 import java.util.stream.Collectors; 61 import java.util.stream.Stream; 62 63 /** 64 * Implements interfaces required to show and allow interacting with a PopupContainerWithArrow. 65 * Controls the long-press menu on Taskbar and AllApps icons. 66 */ 67 public class TaskbarPopupController implements TaskbarControllers.LoggableTaskbarController { 68 69 private static final SystemShortcut.Factory<BaseTaskbarContext> 70 APP_INFO = SystemShortcut.AppInfo::new; 71 72 private final TaskbarActivityContext mContext; 73 private final PopupDataProvider mPopupDataProvider; 74 75 // Initialized in init. 76 private TaskbarControllers mControllers; 77 private boolean mAllowInitialSplitSelection; 78 TaskbarPopupController(TaskbarActivityContext context)79 public TaskbarPopupController(TaskbarActivityContext context) { 80 mContext = context; 81 mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots); 82 } 83 init(TaskbarControllers controllers)84 public void init(TaskbarControllers controllers) { 85 mControllers = controllers; 86 87 NotificationListener.addNotificationsChangedListener(mPopupDataProvider); 88 } 89 onDestroy()90 public void onDestroy() { 91 NotificationListener.removeNotificationsChangedListener(mPopupDataProvider); 92 } 93 94 @NonNull getPopupDataProvider()95 public PopupDataProvider getPopupDataProvider() { 96 return mPopupDataProvider; 97 } 98 setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy)99 public void setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy) { 100 mPopupDataProvider.setDeepShortcutMap(deepShortcutMapCopy); 101 } 102 setAllowInitialSplitSelection(boolean allowInitialSplitSelection)103 public void setAllowInitialSplitSelection(boolean allowInitialSplitSelection) { 104 mAllowInitialSplitSelection = allowInitialSplitSelection; 105 } 106 updateNotificationDots(Predicate<PackageUserKey> updatedDots)107 private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) { 108 final PackageUserKey packageUserKey = new PackageUserKey(null, null); 109 Predicate<ItemInfo> matcher = info -> !packageUserKey.updateFromItemInfo(info) 110 || updatedDots.test(packageUserKey); 111 112 LauncherBindableItemsContainer.ItemOperator op = (info, v) -> { 113 if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) { 114 if (matcher.test(info)) { 115 ((BubbleTextView) v).applyDotState(info, true /* animate */); 116 } 117 } else if (info instanceof FolderInfo && v instanceof FolderIcon) { 118 FolderInfo fi = (FolderInfo) info; 119 if (fi.anyMatch(matcher)) { 120 FolderDotInfo folderDotInfo = new FolderDotInfo(); 121 for (ItemInfo si : fi.getContents()) { 122 folderDotInfo.addDotInfo(mPopupDataProvider.getDotInfoForItem(si)); 123 } 124 ((FolderIcon) v).setDotInfo(folderDotInfo); 125 } 126 } 127 128 // process all the shortcuts 129 return false; 130 }; 131 132 mControllers.taskbarViewController.mapOverItems(op); 133 Folder folder = Folder.getOpen(mContext); 134 if (folder != null) { 135 folder.iterateOverItems(op); 136 } 137 mControllers.taskbarAllAppsController.updateNotificationDots(updatedDots); 138 } 139 140 /** 141 * Shows the notifications and deep shortcuts associated with a Taskbar {@param icon}. 142 * @return the container if shown or null. 143 */ showForIcon(BubbleTextView icon)144 public PopupContainerWithArrow<BaseTaskbarContext> showForIcon(BubbleTextView icon) { 145 BaseTaskbarContext context = ActivityContext.lookupContext(icon.getContext()); 146 if (PopupContainerWithArrow.getOpen(context) != null) { 147 // There is already an items container open, so don't open this one. 148 icon.clearFocus(); 149 return null; 150 } 151 ItemInfo item = (ItemInfo) icon.getTag(); 152 if (!ShortcutUtil.supportsShortcuts(item)) { 153 return null; 154 } 155 156 PopupContainerWithArrow<BaseTaskbarContext> container; 157 int deepShortcutCount = mPopupDataProvider.getShortcutCountForItem(item); 158 // TODO(b/198438631): add support for INSTALL shortcut factory 159 List<SystemShortcut> systemShortcuts = getSystemShortcuts() 160 .map(s -> s.getShortcut(context, item, icon)) 161 .filter(Objects::nonNull) 162 .collect(Collectors.toList()); 163 164 container = (PopupContainerWithArrow) context.getLayoutInflater().inflate( 165 R.layout.popup_container, context.getDragLayer(), false); 166 container.populateAndShowRows(icon, deepShortcutCount, systemShortcuts); 167 168 // TODO (b/198438631): configure for taskbar/context 169 container.setPopupItemDragHandler(new TaskbarPopupItemDragHandler()); 170 mControllers.taskbarDragController.addDragListener(container); 171 container.requestFocus(); 172 173 // Make focusable to receive back events 174 context.onPopupVisibilityChanged(true); 175 container.addOnCloseCallback(() -> { 176 context.getDragLayer().post(() -> context.onPopupVisibilityChanged(false)); 177 }); 178 179 return container; 180 } 181 182 // Create a Stream of all applicable system shortcuts getSystemShortcuts()183 private Stream<SystemShortcut.Factory> getSystemShortcuts() { 184 // append split options to APP_INFO shortcut, the order here will reflect in the popup 185 return Stream.concat( 186 Stream.of(APP_INFO), 187 mControllers.uiController.getSplitMenuOptions() 188 ); 189 } 190 191 @Override dumpLogs(String prefix, PrintWriter pw)192 public void dumpLogs(String prefix, PrintWriter pw) { 193 pw.println(prefix + "TaskbarPopupController:"); 194 195 mPopupDataProvider.dump(prefix + "\t", pw); 196 } 197 198 private class TaskbarPopupItemDragHandler implements 199 PopupContainerWithArrow.PopupItemDragHandler { 200 201 protected final Point mIconLastTouchPos = new Point(); 202 TaskbarPopupItemDragHandler()203 TaskbarPopupItemDragHandler() {} 204 205 @Override onTouch(View view, MotionEvent ev)206 public boolean onTouch(View view, MotionEvent ev) { 207 // Touched a shortcut, update where it was touched so we can drag from there on 208 // long click. 209 switch (ev.getAction()) { 210 case MotionEvent.ACTION_DOWN: 211 case MotionEvent.ACTION_MOVE: 212 mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY()); 213 break; 214 } 215 return false; 216 } 217 218 @Override onLongClick(View v)219 public boolean onLongClick(View v) { 220 // Return early if not the correct view 221 if (!(v.getParent() instanceof DeepShortcutView)) return false; 222 223 DeepShortcutView sv = (DeepShortcutView) v.getParent(); 224 sv.setWillDrawIcon(false); 225 226 // Move the icon to align with the center-top of the touch point 227 Point iconShift = new Point(); 228 iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x; 229 iconShift.y = mIconLastTouchPos.y - mContext.getDeviceProfile().taskbarIconSize; 230 231 ((TaskbarDragController) ActivityContext.lookupContext( 232 v.getContext()).getDragController()).startDragOnLongClick(sv, iconShift); 233 234 return false; 235 } 236 } 237 238 /** 239 * Creates a factory function representing a single "split position" menu item ("Split left," 240 * "Split right," or "Split top"). 241 * @param position A SplitPositionOption representing whether we are splitting top, left, or 242 * right. 243 * @return A factory function to be used in populating the long-press menu. 244 */ createSplitShortcutFactory( SplitPositionOption position)245 SystemShortcut.Factory<BaseTaskbarContext> createSplitShortcutFactory( 246 SplitPositionOption position) { 247 return (context, itemInfo, originalView) -> new TaskbarSplitShortcut(context, itemInfo, 248 originalView, position, mAllowInitialSplitSelection); 249 } 250 251 /** 252 * A single menu item ("Split left," "Split right," or "Split top") that executes a split 253 * from the taskbar, as if the user performed a drag and drop split. 254 * Includes an onClick method that initiates the actual split. 255 */ 256 private static class TaskbarSplitShortcut extends 257 SplitShortcut<BaseTaskbarContext> { 258 /** 259 * If {@code true}, clicking this shortcut will not attempt to start a split app directly, 260 * but be the first app in split selection mode 261 */ 262 private final boolean mAllowInitialSplitSelection; 263 TaskbarSplitShortcut(BaseTaskbarContext context, ItemInfo itemInfo, View originalView, SplitPositionOption position, boolean allowInitialSplitSelection)264 TaskbarSplitShortcut(BaseTaskbarContext context, ItemInfo itemInfo, View originalView, 265 SplitPositionOption position, boolean allowInitialSplitSelection) { 266 super(position.iconResId, position.textResId, context, itemInfo, originalView, 267 position); 268 mAllowInitialSplitSelection = allowInitialSplitSelection; 269 } 270 271 @Override onClick(View view)272 public void onClick(View view) { 273 // Add callbacks depending on what type of Taskbar context we're in (Taskbar or AllApps) 274 mTarget.onSplitScreenMenuButtonClicked(); 275 AbstractFloatingView.closeAllOpenViews(mTarget); 276 277 // Depending on what app state we're in, we either want to initiate the split screen 278 // staging process or immediately launch a split with an existing app. 279 // - Initiate the split screen staging process 280 if (mAllowInitialSplitSelection) { 281 super.onClick(view); 282 return; 283 } 284 285 // - Immediately launch split with the running app 286 Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds = 287 LogUtils.getShellShareableInstanceId(); 288 mTarget.getStatsLogManager().logger() 289 .withItemInfo(mItemInfo) 290 .withInstanceId(instanceIds.second) 291 .log(getLogEventForPosition(getPosition().stagePosition)); 292 293 if (mItemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { 294 WorkspaceItemInfo workspaceItemInfo = (WorkspaceItemInfo) mItemInfo; 295 SystemUiProxy.INSTANCE.get(mTarget).startShortcut( 296 workspaceItemInfo.getIntent().getPackage(), 297 workspaceItemInfo.getDeepShortcutId(), 298 getPosition().stagePosition, 299 null, 300 workspaceItemInfo.user, 301 instanceIds.first); 302 } else { 303 SystemUiProxy.INSTANCE.get(mTarget).startIntent( 304 mTarget.getSystemService(LauncherApps.class).getMainActivityLaunchIntent( 305 mItemInfo.getIntent().getComponent(), 306 null, 307 mItemInfo.user), 308 mItemInfo.user.getIdentifier(), 309 new Intent(), 310 getPosition().stagePosition, 311 null, 312 instanceIds.first); 313 } 314 } 315 } 316 } 317 318