• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 
17 package com.android.quickstep;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
20 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
21 
22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP;
23 
24 import android.app.Activity;
25 import android.app.ActivityOptions;
26 import android.graphics.Bitmap;
27 import android.graphics.Color;
28 import android.graphics.Rect;
29 import android.os.Handler;
30 import android.os.Looper;
31 import android.os.RemoteException;
32 import android.os.SystemProperties;
33 import android.util.Log;
34 import android.view.View;
35 import android.view.WindowInsets;
36 import android.view.WindowManagerGlobal;
37 import android.window.SplashScreen;
38 
39 import androidx.annotation.Nullable;
40 
41 import com.android.launcher3.BaseDraggingActivity;
42 import com.android.launcher3.DeviceProfile;
43 import com.android.launcher3.R;
44 import com.android.launcher3.config.FeatureFlags;
45 import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
46 import com.android.launcher3.model.WellbeingModel;
47 import com.android.launcher3.popup.SystemShortcut;
48 import com.android.launcher3.popup.SystemShortcut.AppInfo;
49 import com.android.launcher3.touch.PagedOrientationHandler;
50 import com.android.launcher3.util.InstantAppResolver;
51 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
52 import com.android.quickstep.views.RecentsView;
53 import com.android.quickstep.views.TaskThumbnailView;
54 import com.android.quickstep.views.TaskView;
55 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
56 import com.android.systemui.shared.recents.model.Task;
57 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
58 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
59 import com.android.systemui.shared.recents.view.RecentsTransition;
60 import com.android.systemui.shared.system.ActivityManagerWrapper;
61 
62 import java.util.Collections;
63 import java.util.List;
64 import java.util.function.Function;
65 import java.util.stream.Collectors;
66 
67 /**
68  * Represents a system shortcut that can be shown for a recent task. Appears as a single entry in
69  * the dropdown menu that shows up when you tap an app icon in Overview.
70  */
71 public interface TaskShortcutFactory {
72     @Nullable
getShortcuts(BaseDraggingActivity activity, TaskIdAttributeContainer taskContainer)73     default List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
74             TaskIdAttributeContainer taskContainer) {
75         return null;
76     }
77 
showForSplitscreen()78     default boolean showForSplitscreen() {
79         return false;
80     }
81 
82     /** @return a singleton list if the provided shortcut is non-null, null otherwise */
83     @Nullable
createSingletonShortcutList(@ullable SystemShortcut shortcut)84     default List<SystemShortcut> createSingletonShortcutList(@Nullable SystemShortcut shortcut) {
85         if (shortcut != null) {
86             return Collections.singletonList(shortcut);
87         }
88         return null;
89     }
90 
91     TaskShortcutFactory APP_INFO = new TaskShortcutFactory() {
92         @Override
93         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
94                 TaskIdAttributeContainer taskContainer) {
95             TaskView taskView = taskContainer.getTaskView();
96             AppInfo.SplitAccessibilityInfo accessibilityInfo =
97                     new AppInfo.SplitAccessibilityInfo(taskView.containsMultipleTasks(),
98                             TaskUtils.getTitle(taskView.getContext(), taskContainer.getTask()),
99                             taskContainer.getA11yNodeId()
100                     );
101             return Collections.singletonList(new AppInfo(activity, taskContainer.getItemInfo(),
102                     taskView, accessibilityInfo));
103         }
104 
105         @Override
106         public boolean showForSplitscreen() {
107             return true;
108         }
109     };
110 
111     class SplitSelectSystemShortcut extends SystemShortcut {
112         private final TaskView mTaskView;
113         private final SplitPositionOption mSplitPositionOption;
114 
SplitSelectSystemShortcut(BaseDraggingActivity target, TaskView taskView, SplitPositionOption option)115         public SplitSelectSystemShortcut(BaseDraggingActivity target, TaskView taskView,
116                 SplitPositionOption option) {
117             super(option.iconResId, option.textResId, target, taskView.getItemInfo(), taskView);
118             mTaskView = taskView;
119             mSplitPositionOption = option;
120         }
121 
122         @Override
onClick(View view)123         public void onClick(View view) {
124             mTaskView.initiateSplitSelect(mSplitPositionOption);
125         }
126     }
127 
128     /**
129      * A menu item, "Save app pair", that allows the user to preserve the current app combination as
130      * a single persistent icon on the Home screen, allowing for quick split screen initialization.
131      */
132     class SaveAppPairSystemShortcut extends SystemShortcut<BaseDraggingActivity> {
133         private final TaskView mTaskView;
134 
SaveAppPairSystemShortcut(BaseDraggingActivity activity, TaskView taskView)135         public SaveAppPairSystemShortcut(BaseDraggingActivity activity, TaskView taskView) {
136             super(R.drawable.ic_save_app_pair, R.string.save_app_pair, activity,
137                     taskView.getItemInfo(), taskView);
138             mTaskView = taskView;
139         }
140 
141         @Override
onClick(View view)142         public void onClick(View view) {
143             dismissTaskMenuView(mTarget);
144             ((RecentsView) mTarget.getOverviewPanel())
145                     .getSplitSelectController().getAppPairsController().saveAppPair(mTaskView);
146         }
147     }
148 
149     class FreeformSystemShortcut extends SystemShortcut<BaseDraggingActivity> {
150         private static final String TAG = "FreeformSystemShortcut";
151 
152         private Handler mHandler;
153 
154         private final RecentsView mRecentsView;
155         private final TaskThumbnailView mThumbnailView;
156         private final TaskView mTaskView;
157         private final LauncherEvent mLauncherEvent;
158 
FreeformSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity, TaskIdAttributeContainer taskContainer, LauncherEvent launcherEvent)159         public FreeformSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity,
160                 TaskIdAttributeContainer taskContainer, LauncherEvent launcherEvent) {
161             super(iconRes, textRes, activity, taskContainer.getItemInfo(),
162                     taskContainer.getTaskView());
163             mLauncherEvent = launcherEvent;
164             mHandler = new Handler(Looper.getMainLooper());
165             mTaskView = taskContainer.getTaskView();
166             mRecentsView = activity.getOverviewPanel();
167             mThumbnailView = taskContainer.getThumbnailView();
168         }
169 
170         @Override
onClick(View view)171         public void onClick(View view) {
172             dismissTaskMenuView(mTarget);
173             RecentsView rv = mTarget.getOverviewPanel();
174             rv.switchToScreenshot(() -> {
175                 rv.finishRecentsAnimation(true /* toHome */, () -> {
176                     mTarget.returnToHomescreen();
177                     rv.getHandler().post(this::startActivity);
178                 });
179             });
180         }
181 
startActivity()182         private void startActivity() {
183             final Task.TaskKey taskKey = mTaskView.getTask().key;
184             final int taskId = taskKey.id;
185             final ActivityOptions options = makeLaunchOptions(mTarget);
186             if (options != null) {
187                 options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
188             }
189             if (options != null
190                     && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
191                             options)) {
192                 final Runnable animStartedListener = () -> {
193                     // Hide the task view and wait for the window to be resized
194                     // TODO: Consider animating in launcher and do an in-place start activity
195                     //       afterwards
196                     mRecentsView.setIgnoreResetTask(taskId);
197                     mTaskView.setAlpha(0f);
198                 };
199 
200                 final int[] position = new int[2];
201                 mThumbnailView.getLocationOnScreen(position);
202                 final int width = (int) (mThumbnailView.getWidth() * mTaskView.getScaleX());
203                 final int height = (int) (mThumbnailView.getHeight() * mTaskView.getScaleY());
204                 final Rect taskBounds = new Rect(position[0], position[1],
205                         position[0] + width, position[1] + height);
206 
207                 // Take the thumbnail of the task without a scrim and apply it back after
208                 float alpha = mThumbnailView.getDimAlpha();
209                 mThumbnailView.setDimAlpha(0);
210                 Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
211                         taskBounds.width(), taskBounds.height(), mThumbnailView, 1f,
212                         Color.BLACK);
213                 mThumbnailView.setDimAlpha(alpha);
214 
215                 AppTransitionAnimationSpecsFuture future =
216                         new AppTransitionAnimationSpecsFuture(mHandler) {
217                     @Override
218                     public List<AppTransitionAnimationSpecCompat> composeSpecs() {
219                         return Collections.singletonList(new AppTransitionAnimationSpecCompat(
220                                 taskId, thumbnail, taskBounds));
221                     }
222                 };
223                 overridePendingAppTransitionMultiThumbFuture(
224                         future, animStartedListener, mHandler, true /* scaleUp */,
225                         taskKey.displayId);
226                 mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo())
227                         .log(mLauncherEvent);
228             }
229         }
230 
231         /**
232          * Overrides a pending app transition.
233          */
overridePendingAppTransitionMultiThumbFuture( AppTransitionAnimationSpecsFuture animationSpecFuture, Runnable animStartedCallback, Handler animStartedCallbackHandler, boolean scaleUp, int displayId)234         private void overridePendingAppTransitionMultiThumbFuture(
235                 AppTransitionAnimationSpecsFuture animationSpecFuture, Runnable animStartedCallback,
236                 Handler animStartedCallbackHandler, boolean scaleUp, int displayId) {
237             try {
238                 WindowManagerGlobal.getWindowManagerService()
239                         .overridePendingAppTransitionMultiThumbFuture(
240                                 animationSpecFuture.getFuture(),
241                                 RecentsTransition.wrapStartedListener(animStartedCallbackHandler,
242                                         animStartedCallback), scaleUp, displayId);
243             } catch (RemoteException e) {
244                 Log.w(TAG, "Failed to override pending app transition (multi-thumbnail future): ",
245                         e);
246             }
247         }
248 
makeLaunchOptions(Activity activity)249         private ActivityOptions makeLaunchOptions(Activity activity) {
250             ActivityOptions activityOptions = ActivityOptions.makeBasic();
251             activityOptions.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM);
252             // Arbitrary bounds only because freeform is in dev mode right now
253             final View decorView = activity.getWindow().getDecorView();
254             final WindowInsets insets = decorView.getRootWindowInsets();
255             final Rect r = new Rect(0, 0, decorView.getWidth() / 2, decorView.getHeight() / 2);
256             r.offsetTo(insets.getSystemWindowInsetLeft() + 50,
257                     insets.getSystemWindowInsetTop() + 50);
258             activityOptions.setLaunchBounds(r);
259             return activityOptions;
260         }
261     }
262 
263     /**
264      * Does NOT add split options in the following scenarios:
265      * * The taskView to add split options is already showing split screen tasks
266      * * There aren't at least 2 tasks in overview to show split options for
267      * * Split isn't supported by the task itself (non resizable activity)
268      * * We aren't currently in multi-window
269      * * The taskView to show split options for is the focused task AND we haven't started
270      * scrolling in overview (if we haven't scrolled, there's a split overview action button so
271      * we don't need this menu option)
272      */
273     TaskShortcutFactory SPLIT_SELECT = new TaskShortcutFactory() {
274         @Override
275         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
276                 TaskIdAttributeContainer taskContainer) {
277             DeviceProfile deviceProfile = activity.getDeviceProfile();
278             final Task task  = taskContainer.getTask();
279             final int intentFlags = task.key.baseIntent.getFlags();
280             final TaskView taskView = taskContainer.getTaskView();
281             final RecentsView recentsView = taskView.getRecentsView();
282             final PagedOrientationHandler orientationHandler =
283                     recentsView.getPagedOrientationHandler();
284 
285             boolean notEnoughTasksToSplit = recentsView.getTaskViewCount() < 2;
286             boolean isFocusedTask = deviceProfile.isTablet && taskView.isFocusedTask();
287             boolean isTaskInExpectedScrollPosition =
288                     recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView));
289             boolean isTaskSplitNotSupported = !task.isDockable ||
290                     (intentFlags & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
291             boolean hideForExistingMultiWindow = activity.getDeviceProfile().isMultiWindowMode;
292 
293             if (taskView.containsMultipleTasks()
294                     || notEnoughTasksToSplit
295                     || isTaskSplitNotSupported
296                     || hideForExistingMultiWindow
297                     || (isFocusedTask && isTaskInExpectedScrollPosition)) {
298                 return null;
299             }
300 
301             return orientationHandler.getSplitPositionOptions(deviceProfile)
302                     .stream()
303                     .map((Function<SplitPositionOption, SystemShortcut>) option ->
304                             new SplitSelectSystemShortcut(activity, taskView, option))
305                     .collect(Collectors.toList());
306         }
307     };
308 
309     TaskShortcutFactory SAVE_APP_PAIR = new TaskShortcutFactory() {
310         @Nullable
311         @Override
312         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
313                 TaskIdAttributeContainer taskContainer) {
314             final TaskView taskView = taskContainer.getTaskView();
315 
316             if (!FeatureFlags.ENABLE_APP_PAIRS.get() || !taskView.containsMultipleTasks()) {
317                 return null;
318             }
319 
320             return Collections.singletonList(new SaveAppPairSystemShortcut(activity, taskView));
321         }
322 
323         @Override
324         public boolean showForSplitscreen() {
325             return true;
326         }
327     };
328 
329     TaskShortcutFactory FREE_FORM = new TaskShortcutFactory() {
330         @Override
331         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
332                 TaskIdAttributeContainer taskContainer) {
333             final Task task  = taskContainer.getTask();
334             if (!task.isDockable) {
335                 return null;
336             }
337             if (!isAvailable(activity, task.key.displayId)) {
338                 return null;
339             }
340 
341             return Collections.singletonList(new FreeformSystemShortcut(
342                     R.drawable.ic_caption_desktop_button_foreground,
343                     R.string.recent_task_option_freeform, activity, taskContainer,
344                     LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP));
345         }
346 
347         private boolean isAvailable(BaseDraggingActivity activity, int displayId) {
348             return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity)
349                     && !SystemProperties.getBoolean("persist.wm.debug.desktop_mode", false)
350                     && !SystemProperties.getBoolean("persist.wm.debug.desktop_mode_2", false);
351         }
352     };
353 
354     TaskShortcutFactory PIN = new TaskShortcutFactory() {
355         @Override
356         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
357                 TaskIdAttributeContainer taskContainer) {
358             if (!SystemUiProxy.INSTANCE.get(activity).isActive()) {
359                 return null;
360             }
361             if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) {
362                 return null;
363             }
364             if (ActivityManagerWrapper.getInstance().isLockToAppActive()) {
365                 // We shouldn't be able to pin while an app is locked.
366                 return null;
367             }
368             return Collections.singletonList(new PinSystemShortcut(activity, taskContainer));
369         }
370     };
371 
372     class PinSystemShortcut extends SystemShortcut<BaseDraggingActivity> {
373 
374         private static final String TAG = "PinSystemShortcut";
375 
376         private final TaskView mTaskView;
377 
PinSystemShortcut(BaseDraggingActivity target, TaskIdAttributeContainer taskContainer)378         public PinSystemShortcut(BaseDraggingActivity target,
379                 TaskIdAttributeContainer taskContainer) {
380             super(R.drawable.ic_pin, R.string.recent_task_option_pin, target,
381                     taskContainer.getItemInfo(), taskContainer.getTaskView());
382             mTaskView = taskContainer.getTaskView();
383         }
384 
385         @Override
onClick(View view)386         public void onClick(View view) {
387             if (mTaskView.launchTaskAnimated() != null) {
388                 SystemUiProxy.INSTANCE.get(mTarget).startScreenPinning(mTaskView.getTask().key.id);
389             }
390             dismissTaskMenuView(mTarget);
391             mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo())
392                     .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP);
393         }
394     }
395 
396     TaskShortcutFactory INSTALL = new TaskShortcutFactory() {
397         @Override
398         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
399                 TaskIdAttributeContainer taskContainer) {
400             Task t = taskContainer.getTask();
401             return InstantAppResolver.newInstance(activity).isInstantApp(
402                     t.getTopComponent().getPackageName(), t.getKey().userId)
403                     ? Collections.singletonList(new SystemShortcut.Install(activity,
404                             taskContainer.getItemInfo(), taskContainer.getTaskView()))
405                     : null;
406         }
407     };
408 
409     TaskShortcutFactory WELLBEING = new TaskShortcutFactory() {
410         @Override
411         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
412                 TaskIdAttributeContainer taskContainer) {
413             SystemShortcut<BaseDraggingActivity> wellbeingShortcut =
414                     WellbeingModel.SHORTCUT_FACTORY.getShortcut(activity,
415                             taskContainer.getItemInfo(), taskContainer.getTaskView());
416             return createSingletonShortcutList(wellbeingShortcut);
417         }
418     };
419 
420     TaskShortcutFactory SCREENSHOT = new TaskShortcutFactory() {
421         @Override
422         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
423                 TaskIdAttributeContainer taskContainer) {
424             SystemShortcut screenshotShortcut = taskContainer.getThumbnailView().getTaskOverlay()
425                     .getScreenshotShortcut(activity, taskContainer.getItemInfo(),
426                             taskContainer.getTaskView());
427             return createSingletonShortcutList(screenshotShortcut);
428         }
429     };
430 
431     TaskShortcutFactory MODAL = new TaskShortcutFactory() {
432         @Override
433         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
434                 TaskIdAttributeContainer taskContainer) {
435             SystemShortcut modalStateSystemShortcut =
436                     taskContainer.getThumbnailView().getTaskOverlay()
437                             .getModalStateSystemShortcut(
438                                     taskContainer.getItemInfo(), taskContainer.getTaskView());
439             return createSingletonShortcutList(modalStateSystemShortcut);
440         }
441     };
442 }
443