• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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