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.LauncherSettings.Favorites.CONTAINER_ALL_APPS; 19 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT; 20 import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR; 21 import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition; 22 23 import android.content.Intent; 24 import android.content.pm.LauncherApps; 25 import android.graphics.Point; 26 import android.util.Pair; 27 import android.util.SparseArray; 28 import android.view.MotionEvent; 29 import android.view.View; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 34 import com.android.internal.logging.InstanceId; 35 import com.android.launcher3.AbstractFloatingView; 36 import com.android.launcher3.BubbleTextView; 37 import com.android.launcher3.Flags; 38 import com.android.launcher3.LauncherSettings; 39 import com.android.launcher3.R; 40 import com.android.launcher3.model.data.AppInfo; 41 import com.android.launcher3.model.data.ItemInfo; 42 import com.android.launcher3.model.data.WorkspaceItemInfo; 43 import com.android.launcher3.notification.NotificationListener; 44 import com.android.launcher3.popup.PopupContainerWithArrow; 45 import com.android.launcher3.popup.PopupDataProvider; 46 import com.android.launcher3.popup.SystemShortcut; 47 import com.android.launcher3.shortcuts.DeepShortcutView; 48 import com.android.launcher3.splitscreen.SplitShortcut; 49 import com.android.launcher3.util.ComponentKey; 50 import com.android.launcher3.util.ShortcutUtil; 51 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; 52 import com.android.launcher3.views.ActivityContext; 53 import com.android.quickstep.SystemUiProxy; 54 import com.android.quickstep.util.LogUtils; 55 import com.android.quickstep.util.SingleTask; 56 import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; 57 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; 58 59 import java.io.PrintWriter; 60 import java.util.ArrayList; 61 import java.util.Arrays; 62 import java.util.HashMap; 63 import java.util.List; 64 import java.util.Objects; 65 import java.util.stream.Collectors; 66 import java.util.stream.Stream; 67 68 /** 69 * Implements interfaces required to show and allow interacting with a PopupContainerWithArrow. 70 * Controls the long-press menu on Taskbar and AllApps icons. 71 */ 72 public class TaskbarPopupController implements TaskbarControllers.LoggableTaskbarController { 73 74 private static final SystemShortcut.Factory<BaseTaskbarContext> 75 APP_INFO = SystemShortcut.AppInfo::new; 76 77 private static final SystemShortcut.Factory<BaseTaskbarContext> 78 BUBBLE = SystemShortcut.BubbleShortcut::new; 79 80 private final TaskbarActivityContext mContext; 81 private final PopupDataProvider mPopupDataProvider; 82 83 // Initialized in init. 84 private TaskbarControllers mControllers; 85 private boolean mAllowInitialSplitSelection; 86 private AppInfo[] mAppInfosList = AppInfo.EMPTY_ARRAY; 87 // Saves the ItemInfos in the hotseat without the predicted items. 88 private SparseArray<ItemInfo> mHotseatInfosList; 89 private ManageWindowsTaskbarShortcut<BaseTaskbarContext> mManageWindowsTaskbarShortcut; 90 91 TaskbarPopupController(TaskbarActivityContext context)92 public TaskbarPopupController(TaskbarActivityContext context) { 93 mContext = context; 94 mPopupDataProvider = new PopupDataProvider(mContext); 95 } 96 init(TaskbarControllers controllers)97 public void init(TaskbarControllers controllers) { 98 mControllers = controllers; 99 100 NotificationListener.addNotificationsChangedListener(mPopupDataProvider); 101 } 102 onDestroy()103 public void onDestroy() { 104 NotificationListener.removeNotificationsChangedListener(mPopupDataProvider); 105 } 106 107 @NonNull getPopupDataProvider()108 public PopupDataProvider getPopupDataProvider() { 109 return mPopupDataProvider; 110 } 111 setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy)112 public void setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy) { 113 mPopupDataProvider.setDeepShortcutMap(deepShortcutMapCopy); 114 } 115 116 /** Closes the multi-instance menu if it is enabled and currently open. */ maybeCloseMultiInstanceMenu()117 public void maybeCloseMultiInstanceMenu() { 118 if (Flags.enableMultiInstanceMenuTaskbar() && mManageWindowsTaskbarShortcut != null) { 119 mManageWindowsTaskbarShortcut.closeMultiInstanceMenu(); 120 cleanUpMultiInstanceMenuReference(); 121 } 122 } 123 124 /** Releases the reference to the Taskbar multi-instance menu */ cleanUpMultiInstanceMenuReference()125 public void cleanUpMultiInstanceMenuReference() { 126 mManageWindowsTaskbarShortcut = null; 127 } 128 setAllowInitialSplitSelection(boolean allowInitialSplitSelection)129 public void setAllowInitialSplitSelection(boolean allowInitialSplitSelection) { 130 mAllowInitialSplitSelection = allowInitialSplitSelection; 131 } 132 133 /** 134 * Shows the notifications and deep shortcuts associated with a Taskbar {@param icon}. 135 * @return the container if shown or null. 136 */ showForIcon(BubbleTextView icon)137 public PopupContainerWithArrow<BaseTaskbarContext> showForIcon(BubbleTextView icon) { 138 BaseTaskbarContext context = ActivityContext.lookupContext(icon.getContext()); 139 if (PopupContainerWithArrow.getOpen(context) != null) { 140 // There is already an items container open, so don't open this one. 141 icon.clearFocus(); 142 return null; 143 } 144 145 ItemInfo itemInfo; 146 if (icon.getTag() instanceof ItemInfo item && ShortcutUtil.supportsShortcuts(item)) { 147 itemInfo = item; 148 } else if (icon.getTag() instanceof SingleTask task) { 149 itemInfo = SingleTask.Companion.createTaskItemInfo(task); 150 } else { 151 return null; 152 } 153 154 PopupContainerWithArrow<BaseTaskbarContext> container; 155 int deepShortcutCount = mPopupDataProvider.getShortcutCountForItem(itemInfo); 156 // TODO(b/198438631): add support for INSTALL shortcut factory 157 List<SystemShortcut> systemShortcuts = getSystemShortcuts() 158 .map(s -> s.getShortcut(context, itemInfo, icon)) 159 .filter(Objects::nonNull) 160 .collect(Collectors.toList()); 161 162 // TODO(b/375648361): Revisit to see if this can be implemented within getSystemShortcuts(). 163 if (Flags.enablePinningAppWithContextMenu()) { 164 SystemShortcut shortcut = createPinShortcut(context, itemInfo, icon); 165 if (shortcut != null) { 166 systemShortcuts.add(0, shortcut); 167 } 168 } 169 170 container = (PopupContainerWithArrow) context.getLayoutInflater().inflate( 171 R.layout.popup_container, context.getDragLayer(), false); 172 container.populateAndShowRows(icon, itemInfo, deepShortcutCount, systemShortcuts); 173 174 // TODO (b/198438631): configure for taskbar/context 175 container.setPopupItemDragHandler(new TaskbarPopupItemDragHandler()); 176 mControllers.taskbarDragController.addDragListener(container); 177 container.requestFocus(); 178 179 // Make focusable to receive back events 180 context.onPopupVisibilityChanged(true); 181 container.addOnCloseCallback(() -> { 182 context.getDragLayer().post(() -> context.onPopupVisibilityChanged(false)); 183 }); 184 185 return container; 186 } 187 188 // Create a Stream of all applicable system shortcuts getSystemShortcuts()189 private Stream<SystemShortcut.Factory> getSystemShortcuts() { 190 // append split options to APP_INFO shortcut if not in Desktop Windowing mode, the order 191 // here will reflect in the popup 192 ArrayList<SystemShortcut.Factory> shortcuts = new ArrayList<>(); 193 shortcuts.add(APP_INFO); 194 if (!mControllers.taskbarDesktopModeController 195 .isInDesktopModeAndNotInOverview(mContext.getDisplayId())) { 196 shortcuts.addAll(mControllers.uiController.getSplitMenuOptions().toList()); 197 } 198 if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) { 199 shortcuts.add(BUBBLE); 200 } 201 202 if (Flags.enableMultiInstanceMenuTaskbar() 203 && DesktopModeStatus.canEnterDesktopMode(mContext) 204 && !mControllers.taskbarStashController.isInOverview()) { 205 maybeCloseMultiInstanceMenu(); 206 shortcuts.addAll(getMultiInstanceMenuOptions().toList()); 207 } 208 return shortcuts.stream(); 209 } 210 211 @Nullable createPinShortcut(BaseTaskbarContext target, ItemInfo itemInfo, BubbleTextView originalView)212 private SystemShortcut createPinShortcut(BaseTaskbarContext target, ItemInfo itemInfo, 213 BubbleTextView originalView) { 214 // Predicted items use {@code HotseatPredictionController.PinPrediction} shortcut to pin. 215 if (itemInfo.isPredictedItem()) { 216 return null; 217 } 218 if (itemInfo.container == CONTAINER_HOTSEAT) { 219 return new PinToTaskbarShortcut<>(target, itemInfo, originalView, false, 220 mHotseatInfosList); 221 } 222 if (mHotseatInfosList.size() 223 < mContext.getTaskbarSpecsEvaluator().getNumShownHotseatIcons()) { 224 return new PinToTaskbarShortcut<>(target, itemInfo, originalView, true, 225 mHotseatInfosList); 226 } 227 228 return null; 229 } 230 231 @Override dumpLogs(String prefix, PrintWriter pw)232 public void dumpLogs(String prefix, PrintWriter pw) { 233 pw.println(prefix + "TaskbarPopupController:"); 234 235 mPopupDataProvider.dump(prefix + "\t", pw); 236 } 237 238 private class TaskbarPopupItemDragHandler implements 239 PopupContainerWithArrow.PopupItemDragHandler { 240 241 protected final Point mIconLastTouchPos = new Point(); 242 TaskbarPopupItemDragHandler()243 TaskbarPopupItemDragHandler() {} 244 245 @Override onTouch(View view, MotionEvent ev)246 public boolean onTouch(View view, MotionEvent ev) { 247 // Touched a shortcut, update where it was touched so we can drag from there on 248 // long click. 249 switch (ev.getAction()) { 250 case MotionEvent.ACTION_DOWN: 251 case MotionEvent.ACTION_MOVE: 252 mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY()); 253 break; 254 } 255 return false; 256 } 257 258 @Override onLongClick(View v)259 public boolean onLongClick(View v) { 260 // Return early if not the correct view 261 if (!(v.getParent() instanceof DeepShortcutView)) return false; 262 263 DeepShortcutView sv = (DeepShortcutView) v.getParent(); 264 sv.setWillDrawIcon(false); 265 266 // Move the icon to align with the center-top of the touch point 267 Point iconShift = new Point(); 268 iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x; 269 iconShift.y = mIconLastTouchPos.y - mContext.getDeviceProfile().taskbarIconSize; 270 271 ((TaskbarDragController) ActivityContext.lookupContext( 272 v.getContext()).getDragController()).startDragOnLongClick(sv, iconShift); 273 274 return false; 275 } 276 } 277 278 /** 279 * Creates a factory function representing a single "split position" menu item ("Split left," 280 * "Split right," or "Split top"). 281 * @param position A SplitPositionOption representing whether we are splitting top, left, or 282 * right. 283 * @return A factory function to be used in populating the long-press menu. 284 */ createSplitShortcutFactory( SplitPositionOption position)285 SystemShortcut.Factory<BaseTaskbarContext> createSplitShortcutFactory( 286 SplitPositionOption position) { 287 return (context, itemInfo, originalView) -> new TaskbarSplitShortcut(context, itemInfo, 288 originalView, position, mAllowInitialSplitSelection); 289 } 290 291 /** 292 * Set the list of AppInfos to be able to pull from later 293 */ setApps(AppInfo[] apps)294 public void setApps(AppInfo[] apps) { 295 mAppInfosList = apps; 296 } 297 298 /** 299 * Finds and returns an AppInfo object from a list, using its ComponentKey for identification. 300 * Based off of {@link com.android.launcher3.allapps.AllAppsStore#getApp(ComponentKey)} 301 * since we cannot access AllAppsStore from here. 302 */ getApp(ComponentKey key)303 public AppInfo getApp(ComponentKey key) { 304 if (key == null) { 305 return null; 306 } 307 AppInfo tempInfo = new AppInfo(); 308 tempInfo.componentName = key.componentName; 309 tempInfo.user = key.user; 310 int index = Arrays.binarySearch(mAppInfosList, tempInfo, COMPONENT_KEY_COMPARATOR); 311 return index < 0 ? null : mAppInfosList[index]; 312 } 313 setHotseatInfosList(SparseArray<ItemInfo> info)314 public void setHotseatInfosList(SparseArray<ItemInfo> info) { 315 mHotseatInfosList = info; 316 } 317 318 /** 319 * Returns a stream of Multi Instance menu options if an app supports it. 320 */ getMultiInstanceMenuOptions()321 Stream<SystemShortcut.Factory<BaseTaskbarContext>> getMultiInstanceMenuOptions() { 322 SystemShortcut.Factory<BaseTaskbarContext> f1 = createNewWindowShortcutFactory(); 323 SystemShortcut.Factory<BaseTaskbarContext> f2 = createManageWindowsShortcutFactory(); 324 return f1 != null ? Stream.of(f1, f2) : Stream.empty(); 325 } 326 327 /** 328 * Creates a factory function representing a "New Window" menu item only if the calling app 329 * supports multi-instance. 330 * @return A factory function to be used in populating the long-press menu. 331 */ createNewWindowShortcutFactory()332 SystemShortcut.Factory<BaseTaskbarContext> createNewWindowShortcutFactory() { 333 return (context, itemInfo, originalView) -> { 334 if (shouldShowMultiInstanceOptions(itemInfo)) { 335 return new NewWindowTaskbarShortcut<>(context, itemInfo, originalView); 336 } 337 return null; 338 }; 339 } 340 341 /** 342 * Creates a factory function representing a "Manage Windows" menu item only if the calling app 343 * supports multi-instance. This menu item shows the open instances of the calling app. 344 * @return A factory function to be used in populating the long-press menu. 345 */ 346 public SystemShortcut.Factory<BaseTaskbarContext> createManageWindowsShortcutFactory() { 347 return (context, itemInfo, originalView) -> { 348 if (shouldShowMultiInstanceOptions(itemInfo)) { 349 mManageWindowsTaskbarShortcut = new ManageWindowsTaskbarShortcut<>( 350 context, itemInfo, originalView, mControllers); 351 return mManageWindowsTaskbarShortcut; 352 } 353 return null; 354 }; 355 } 356 357 /** 358 * Determines whether to show multi-instance options for a given item. 359 */ 360 private boolean shouldShowMultiInstanceOptions(ItemInfo itemInfo) { 361 ComponentKey key = itemInfo.getComponentKey(); 362 AppInfo app = getApp(key); 363 return app != null && app.supportsMultiInstance() 364 && itemInfo.container != CONTAINER_ALL_APPS; 365 } 366 367 /** 368 * A single menu item ("Split left," "Split right," or "Split top") that executes a split 369 * from the taskbar, as if the user performed a drag and drop split. 370 * Includes an onClick method that initiates the actual split. 371 */ 372 private static class TaskbarSplitShortcut extends 373 SplitShortcut<BaseTaskbarContext> { 374 /** 375 * If {@code true}, clicking this shortcut will not attempt to start a split app directly, 376 * but be the first app in split selection mode 377 */ 378 private final boolean mAllowInitialSplitSelection; 379 380 TaskbarSplitShortcut(BaseTaskbarContext context, ItemInfo itemInfo, View originalView, 381 SplitPositionOption position, boolean allowInitialSplitSelection) { 382 super(position.iconResId, position.textResId, context, itemInfo, originalView, 383 position); 384 mAllowInitialSplitSelection = allowInitialSplitSelection; 385 } 386 387 @Override 388 public void onClick(View view) { 389 // Add callbacks depending on what type of Taskbar context we're in (Taskbar or AllApps) 390 mTarget.onSplitScreenMenuButtonClicked(); 391 AbstractFloatingView.closeAllOpenViews(mTarget); 392 393 // Depending on what app state we're in, we either want to initiate the split screen 394 // staging process or immediately launch a split with an existing app. 395 // - Initiate the split screen staging process 396 if (mAllowInitialSplitSelection) { 397 super.onClick(view); 398 return; 399 } 400 401 // - Immediately launch split with the running app 402 Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds = 403 LogUtils.getShellShareableInstanceId(); 404 mTarget.getStatsLogManager().logger() 405 .withItemInfo(mItemInfo) 406 .withInstanceId(instanceIds.second) 407 .log(getLogEventForPosition(getPosition().stagePosition)); 408 409 if (mItemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { 410 WorkspaceItemInfo workspaceItemInfo = (WorkspaceItemInfo) mItemInfo; 411 SystemUiProxy.INSTANCE.get(mTarget).startShortcut( 412 workspaceItemInfo.getIntent().getPackage(), 413 workspaceItemInfo.getDeepShortcutId(), 414 getPosition().stagePosition, 415 null, 416 workspaceItemInfo.user, 417 instanceIds.first); 418 } else { 419 SystemUiProxy.INSTANCE.get(mTarget).startIntent( 420 mTarget.getSystemService(LauncherApps.class).getMainActivityLaunchIntent( 421 mItemInfo.getIntent().getComponent(), 422 null, 423 mItemInfo.user), 424 mItemInfo.user.getIdentifier(), 425 new Intent(), 426 getPosition().stagePosition, 427 null, 428 instanceIds.first); 429 } 430 } 431 } 432 } 433 434