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