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.view.Display.DEFAULT_DISPLAY;
20 import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
21 
22 import android.app.Activity;
23 import android.app.ActivityOptions;
24 import android.content.ComponentName;
25 import android.content.Intent;
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.UserHandle;
33 import android.util.Log;
34 import android.view.View;
35 
36 import com.android.launcher3.BaseDraggingActivity;
37 import com.android.launcher3.DeviceProfile;
38 import com.android.launcher3.ItemInfo;
39 import com.android.launcher3.R;
40 import com.android.launcher3.WorkspaceItemInfo;
41 import com.android.launcher3.popup.SystemShortcut;
42 import com.android.launcher3.userevent.nano.LauncherLogProto;
43 import com.android.launcher3.util.InstantAppResolver;
44 import com.android.quickstep.views.RecentsView;
45 import com.android.quickstep.views.TaskThumbnailView;
46 import com.android.quickstep.views.TaskView;
47 import com.android.systemui.shared.recents.ISystemUiProxy;
48 import com.android.systemui.shared.recents.model.Task;
49 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
50 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
51 import com.android.systemui.shared.recents.view.RecentsTransition;
52 import com.android.systemui.shared.system.ActivityCompat;
53 import com.android.systemui.shared.system.ActivityManagerWrapper;
54 import com.android.systemui.shared.system.ActivityOptionsCompat;
55 import com.android.systemui.shared.system.WindowManagerWrapper;
56 
57 import java.util.Collections;
58 import java.util.List;
59 import java.util.function.Consumer;
60 
61 /**
62  * Represents a system shortcut that can be shown for a recent task.
63  */
64 public class TaskSystemShortcut<T extends SystemShortcut> extends SystemShortcut {
65 
66     private static final String TAG = "TaskSystemShortcut";
67 
68     protected T mSystemShortcut;
69 
TaskSystemShortcut(T systemShortcut)70     public TaskSystemShortcut(T systemShortcut) {
71         super(systemShortcut);
72         mSystemShortcut = systemShortcut;
73     }
74 
TaskSystemShortcut(int iconResId, int labelResId)75     protected TaskSystemShortcut(int iconResId, int labelResId) {
76         super(iconResId, labelResId);
77     }
78 
79     @Override
getOnClickListener( BaseDraggingActivity activity, ItemInfo itemInfo)80     public View.OnClickListener getOnClickListener(
81             BaseDraggingActivity activity, ItemInfo itemInfo) {
82         return null;
83     }
84 
getOnClickListener(BaseDraggingActivity activity, TaskView view)85     public View.OnClickListener getOnClickListener(BaseDraggingActivity activity, TaskView view) {
86         Task task = view.getTask();
87 
88         WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
89         dummyInfo.intent = new Intent();
90         ComponentName component = task.getTopComponent();
91         dummyInfo.intent.setComponent(component);
92         dummyInfo.user = UserHandle.of(task.key.userId);
93         dummyInfo.title = TaskUtils.getTitle(activity, task);
94 
95         return getOnClickListenerForTask(activity, task, dummyInfo);
96     }
97 
getOnClickListenerForTask( BaseDraggingActivity activity, Task task, ItemInfo dummyInfo)98     protected View.OnClickListener getOnClickListenerForTask(
99             BaseDraggingActivity activity, Task task, ItemInfo dummyInfo) {
100         return mSystemShortcut.getOnClickListener(activity, dummyInfo);
101     }
102 
103     public static class AppInfo extends TaskSystemShortcut<SystemShortcut.AppInfo> {
AppInfo()104         public AppInfo() {
105             super(new SystemShortcut.AppInfo());
106         }
107     }
108 
109     public static abstract class MultiWindow extends TaskSystemShortcut {
110 
111         private Handler mHandler;
112 
MultiWindow(int iconRes, int textRes)113         public MultiWindow(int iconRes, int textRes) {
114             super(iconRes, textRes);
115             mHandler = new Handler(Looper.getMainLooper());
116         }
117 
isAvailable(BaseDraggingActivity activity, int displayId)118         protected abstract boolean isAvailable(BaseDraggingActivity activity, int displayId);
makeLaunchOptions(Activity activity)119         protected abstract ActivityOptions makeLaunchOptions(Activity activity);
onActivityStarted(BaseDraggingActivity activity)120         protected abstract boolean onActivityStarted(BaseDraggingActivity activity);
121 
122         @Override
getOnClickListener( BaseDraggingActivity activity, TaskView taskView)123         public View.OnClickListener getOnClickListener(
124                 BaseDraggingActivity activity, TaskView taskView) {
125             final Task task  = taskView.getTask();
126             final int taskId = task.key.id;
127             final int displayId = task.key.displayId;
128             if (!task.isDockable) {
129                 return null;
130             }
131             if (!isAvailable(activity, displayId)) {
132                 return null;
133             }
134             final RecentsView recentsView = activity.getOverviewPanel();
135 
136             final TaskThumbnailView thumbnailView = taskView.getThumbnail();
137             return (v -> {
138                 final View.OnLayoutChangeListener onLayoutChangeListener =
139                         new View.OnLayoutChangeListener() {
140                             @Override
141                             public void onLayoutChange(View v, int l, int t, int r, int b,
142                                     int oldL, int oldT, int oldR, int oldB) {
143                                 taskView.getRootView().removeOnLayoutChangeListener(this);
144                                 recentsView.clearIgnoreResetTask(taskId);
145 
146                                 // Start animating in the side pages once launcher has been resized
147                                 recentsView.dismissTask(taskView, false, false);
148                             }
149                         };
150 
151                 final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener =
152                         new DeviceProfile.OnDeviceProfileChangeListener() {
153                             @Override
154                             public void onDeviceProfileChanged(DeviceProfile dp) {
155                                 activity.removeOnDeviceProfileChangeListener(this);
156                                 if (dp.isMultiWindowMode) {
157                                     taskView.getRootView().addOnLayoutChangeListener(
158                                             onLayoutChangeListener);
159                                 }
160                             }
161                         };
162 
163                 dismissTaskMenuView(activity);
164 
165                 ActivityOptions options = makeLaunchOptions(activity);
166                 if (options != null
167                         && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
168                                 options)) {
169                     if (!onActivityStarted(activity)) {
170                         return;
171                     }
172                     // Add a device profile change listener to kick off animating the side tasks
173                     // once we enter multiwindow mode and relayout
174                     activity.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener);
175 
176                     final Runnable animStartedListener = () -> {
177                         // Hide the task view and wait for the window to be resized
178                         // TODO: Consider animating in launcher and do an in-place start activity
179                         //       afterwards
180                         recentsView.setIgnoreResetTask(taskId);
181                         taskView.setAlpha(0f);
182                     };
183 
184                     final int[] position = new int[2];
185                     thumbnailView.getLocationOnScreen(position);
186                     final int width = (int) (thumbnailView.getWidth() * taskView.getScaleX());
187                     final int height = (int) (thumbnailView.getHeight() * taskView.getScaleY());
188                     final Rect taskBounds = new Rect(position[0], position[1],
189                             position[0] + width, position[1] + height);
190 
191                     // Take the thumbnail of the task without a scrim and apply it back after
192                     float alpha = thumbnailView.getDimAlpha();
193                     thumbnailView.setDimAlpha(0);
194                     Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
195                             taskBounds.width(), taskBounds.height(), thumbnailView, 1f,
196                             Color.BLACK);
197                     thumbnailView.setDimAlpha(alpha);
198 
199                     AppTransitionAnimationSpecsFuture future =
200                             new AppTransitionAnimationSpecsFuture(mHandler) {
201                         @Override
202                         public List<AppTransitionAnimationSpecCompat> composeSpecs() {
203                             return Collections.singletonList(new AppTransitionAnimationSpecCompat(
204                                     taskId, thumbnail, taskBounds));
205                         }
206                     };
207                     WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture(
208                             future, animStartedListener, mHandler, true /* scaleUp */, displayId);
209                 }
210             });
211         }
212     }
213 
214     public static class SplitScreen extends MultiWindow {
215         public SplitScreen() {
216             super(R.drawable.ic_split_screen, R.string.recent_task_option_split_screen);
217         }
218 
219         @Override
220         protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
221             // Don't show menu-item if already in multi-window and the task is from
222             // the secondary display.
223             // TODO(b/118266305): Temporarily disable splitscreen for secondary display while new
224             // implementation is enabled
225             return !activity.getDeviceProfile().isMultiWindowMode
226                     && (displayId == -1 || displayId == DEFAULT_DISPLAY);
227         }
228 
229         @Override
230         protected ActivityOptions makeLaunchOptions(Activity activity) {
231             final ActivityCompat act = new ActivityCompat(activity);
232             final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(
233                     act.getDisplayId());
234             if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) {
235                 return null;
236             }
237             boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT;
238             return ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft);
239         }
240 
241         @Override
242         protected boolean onActivityStarted(BaseDraggingActivity activity) {
243             ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy();
244             try {
245                 sysUiProxy.onSplitScreenInvoked();
246             } catch (RemoteException e) {
247                 Log.w(TAG, "Failed to notify SysUI of split screen: ", e);
248                 return false;
249             }
250             activity.getUserEventDispatcher().logActionOnControl(TAP,
251                     LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET);
252             return true;
253         }
254     }
255 
256     public static class Freeform extends MultiWindow {
257         public Freeform() {
258             super(R.drawable.ic_split_screen, R.string.recent_task_option_freeform);
259         }
260 
261         @Override
262         protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
263             return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity);
264         }
265 
266         @Override
267         protected ActivityOptions makeLaunchOptions(Activity activity) {
268             ActivityOptions activityOptions = ActivityOptionsCompat.makeFreeformOptions();
269             // Arbitrary bounds only because freeform is in dev mode right now
270             Rect r = new Rect(50, 50, 200, 200);
271             activityOptions.setLaunchBounds(r);
272             return activityOptions;
273         }
274 
275         @Override
276         protected boolean onActivityStarted(BaseDraggingActivity activity) {
277             activity.returnToHomescreen();
278             return true;
279         }
280     }
281 
282     public static class Pin extends TaskSystemShortcut {
283 
284         private static final String TAG = Pin.class.getSimpleName();
285 
286         private Handler mHandler;
287 
288         public Pin() {
289             super(R.drawable.ic_pin, R.string.recent_task_option_pin);
290             mHandler = new Handler(Looper.getMainLooper());
291         }
292 
293         @Override
294         public View.OnClickListener getOnClickListener(
295                 BaseDraggingActivity activity, TaskView taskView) {
296             ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy();
297             if (sysUiProxy == null) {
298                 return null;
299             }
300             if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) {
301                 return null;
302             }
303             if (ActivityManagerWrapper.getInstance().isLockToAppActive()) {
304                 // We shouldn't be able to pin while an app is locked.
305                 return null;
306             }
307             return view -> {
308                 Consumer<Boolean> resultCallback = success -> {
309                     if (success) {
310                         try {
311                             sysUiProxy.startScreenPinning(taskView.getTask().key.id);
312                         } catch (RemoteException e) {
313                             Log.w(TAG, "Failed to start screen pinning: ", e);
314                         }
315                     } else {
316                         taskView.notifyTaskLaunchFailed(TAG);
317                     }
318                 };
319                 taskView.launchTask(true, resultCallback, mHandler);
320                 dismissTaskMenuView(activity);
321             };
322         }
323     }
324 
325     public static class Install extends TaskSystemShortcut<SystemShortcut.Install> {
326         public Install() {
327             super(new SystemShortcut.Install());
328         }
329 
330         @Override
331         protected View.OnClickListener getOnClickListenerForTask(
332                 BaseDraggingActivity activity, Task task, ItemInfo itemInfo) {
333             if (InstantAppResolver.newInstance(activity).isInstantApp(activity,
334                         task.getTopComponent().getPackageName())) {
335                 return mSystemShortcut.createOnClickListener(activity, itemInfo);
336             }
337             return null;
338         }
339     }
340 }
341