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