1 /*
2  * Copyright (C) 2023 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.wm.shell.windowdecor;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
21 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
22 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.app.ActivityManager.RunningTaskInfo;
27 import android.content.Context;
28 import android.content.res.ColorStateList;
29 import android.content.res.Resources;
30 import android.graphics.PointF;
31 import android.graphics.drawable.Drawable;
32 import android.view.MotionEvent;
33 import android.view.SurfaceControl;
34 import android.view.View;
35 import android.widget.Button;
36 import android.widget.ImageButton;
37 import android.widget.ImageView;
38 import android.widget.TextView;
39 import android.window.SurfaceSyncGroup;
40 
41 import com.android.wm.shell.R;
42 import com.android.wm.shell.desktopmode.DesktopModeStatus;
43 
44 /**
45  * Handle menu opened when the appropriate button is clicked on.
46  *
47  * Displays up to 3 pills that show the following:
48  * App Info: App name, app icon, and collapse button to close the menu.
49  * Windowing Options(Proto 2 only): Buttons to change windowing modes.
50  * Additional Options: Miscellaneous functions including screenshot and closing task.
51  */
52 class HandleMenu {
53     private static final String TAG = "HandleMenu";
54     private final Context mContext;
55     private final WindowDecoration mParentDecor;
56     private WindowDecoration.AdditionalWindow mAppInfoPill;
57     private WindowDecoration.AdditionalWindow mWindowingPill;
58     private WindowDecoration.AdditionalWindow mMoreActionsPill;
59     private final PointF mAppInfoPillPosition = new PointF();
60     private final PointF mWindowingPillPosition = new PointF();
61     private final PointF mMoreActionsPillPosition = new PointF();
62     private final boolean mShouldShowWindowingPill;
63     private final Drawable mAppIcon;
64     private final CharSequence mAppName;
65     private final View.OnClickListener mOnClickListener;
66     private final View.OnTouchListener mOnTouchListener;
67     private final RunningTaskInfo mTaskInfo;
68     private final int mLayoutResId;
69     private final int mCaptionX;
70     private final int mCaptionY;
71     private int mMarginMenuTop;
72     private int mMarginMenuStart;
73     private int mMarginMenuSpacing;
74     private int mMenuWidth;
75     private int mAppInfoPillHeight;
76     private int mWindowingPillHeight;
77     private int mMoreActionsPillHeight;
78     private int mShadowRadius;
79     private int mCornerRadius;
80 
81 
HandleMenu(WindowDecoration parentDecor, int layoutResId, int captionX, int captionY, View.OnClickListener onClickListener, View.OnTouchListener onTouchListener, Drawable appIcon, CharSequence appName, boolean shouldShowWindowingPill)82     HandleMenu(WindowDecoration parentDecor, int layoutResId, int captionX, int captionY,
83             View.OnClickListener onClickListener, View.OnTouchListener onTouchListener,
84             Drawable appIcon, CharSequence appName, boolean shouldShowWindowingPill) {
85         mParentDecor = parentDecor;
86         mContext = mParentDecor.mDecorWindowContext;
87         mTaskInfo = mParentDecor.mTaskInfo;
88         mLayoutResId = layoutResId;
89         mCaptionX = captionX;
90         mCaptionY = captionY;
91         mOnClickListener = onClickListener;
92         mOnTouchListener = onTouchListener;
93         mAppIcon = appIcon;
94         mAppName = appName;
95         mShouldShowWindowingPill = shouldShowWindowingPill;
96         loadHandleMenuDimensions();
97         updateHandleMenuPillPositions();
98     }
99 
show()100     void show() {
101         final SurfaceSyncGroup ssg = new SurfaceSyncGroup(TAG);
102         SurfaceControl.Transaction t = new SurfaceControl.Transaction();
103 
104         createAppInfoPill(t, ssg);
105         if (mShouldShowWindowingPill) {
106             createWindowingPill(t, ssg);
107         }
108         createMoreActionsPill(t, ssg);
109         ssg.addTransaction(t);
110         ssg.markSyncReady();
111         setupHandleMenu();
112     }
113 
createAppInfoPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg)114     private void createAppInfoPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg) {
115         final int x = (int) mAppInfoPillPosition.x;
116         final int y = (int) mAppInfoPillPosition.y;
117         mAppInfoPill = mParentDecor.addWindow(
118                 R.layout.desktop_mode_window_decor_handle_menu_app_info_pill,
119                 "Menu's app info pill",
120                 t, ssg, x, y, mMenuWidth, mAppInfoPillHeight, mShadowRadius, mCornerRadius);
121     }
122 
createWindowingPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg)123     private void createWindowingPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg) {
124         final int x = (int) mWindowingPillPosition.x;
125         final int y = (int) mWindowingPillPosition.y;
126         mWindowingPill = mParentDecor.addWindow(
127                 R.layout.desktop_mode_window_decor_handle_menu_windowing_pill,
128                 "Menu's windowing pill",
129                 t, ssg, x, y, mMenuWidth, mWindowingPillHeight, mShadowRadius, mCornerRadius);
130     }
131 
createMoreActionsPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg)132     private void createMoreActionsPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg) {
133         final int x = (int) mMoreActionsPillPosition.x;
134         final int y = (int) mMoreActionsPillPosition.y;
135         mMoreActionsPill = mParentDecor.addWindow(
136                 R.layout.desktop_mode_window_decor_handle_menu_more_actions_pill,
137                 "Menu's more actions pill",
138                 t, ssg, x, y, mMenuWidth, mMoreActionsPillHeight, mShadowRadius, mCornerRadius);
139     }
140 
141     /**
142      * Set up interactive elements and color of this handle menu
143      */
setupHandleMenu()144     private void setupHandleMenu() {
145         // App Info pill setup.
146         final View appInfoPillView = mAppInfoPill.mWindowViewHost.getView();
147         final ImageButton collapseBtn = appInfoPillView.findViewById(R.id.collapse_menu_button);
148         final ImageView appIcon = appInfoPillView.findViewById(R.id.application_icon);
149         final TextView appName = appInfoPillView.findViewById(R.id.application_name);
150         collapseBtn.setOnClickListener(mOnClickListener);
151         appInfoPillView.setOnTouchListener(mOnTouchListener);
152         appIcon.setImageDrawable(mAppIcon);
153         appName.setText(mAppName);
154 
155         // Windowing pill setup.
156         if (mShouldShowWindowingPill) {
157             final View windowingPillView = mWindowingPill.mWindowViewHost.getView();
158             final ImageButton fullscreenBtn = windowingPillView.findViewById(
159                     R.id.fullscreen_button);
160             final ImageButton splitscreenBtn = windowingPillView.findViewById(
161                     R.id.split_screen_button);
162             final ImageButton floatingBtn = windowingPillView.findViewById(R.id.floating_button);
163             final ImageButton desktopBtn = windowingPillView.findViewById(R.id.desktop_button);
164             fullscreenBtn.setOnClickListener(mOnClickListener);
165             splitscreenBtn.setOnClickListener(mOnClickListener);
166             floatingBtn.setOnClickListener(mOnClickListener);
167             desktopBtn.setOnClickListener(mOnClickListener);
168             // The button corresponding to the windowing mode that the task is currently in uses a
169             // different color than the others.
170             final ColorStateList activeColorStateList = ColorStateList.valueOf(
171                     mContext.getColor(R.color.desktop_mode_caption_menu_buttons_color_active));
172             final ColorStateList inActiveColorStateList = ColorStateList.valueOf(
173                     mContext.getColor(R.color.desktop_mode_caption_menu_buttons_color_inactive));
174             fullscreenBtn.setImageTintList(
175                     mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
176                             ? activeColorStateList : inActiveColorStateList);
177             splitscreenBtn.setImageTintList(
178                     mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW
179                             ? activeColorStateList : inActiveColorStateList);
180             floatingBtn.setImageTintList(mTaskInfo.getWindowingMode() == WINDOWING_MODE_PINNED
181                     ? activeColorStateList : inActiveColorStateList);
182             desktopBtn.setImageTintList(mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
183                     ? activeColorStateList : inActiveColorStateList);
184         }
185 
186         // More Actions pill setup.
187         final View moreActionsPillView = mMoreActionsPill.mWindowViewHost.getView();
188         final Button closeBtn = moreActionsPillView.findViewById(R.id.close_button);
189         closeBtn.setOnClickListener(mOnClickListener);
190         final Button selectBtn = moreActionsPillView.findViewById(R.id.select_button);
191         selectBtn.setOnClickListener(mOnClickListener);
192     }
193 
194     /**
195      * Updates the handle menu pills' position variables to reflect their next positions
196      */
updateHandleMenuPillPositions()197     private void updateHandleMenuPillPositions() {
198         final int menuX, menuY;
199         final int captionWidth = mTaskInfo.getConfiguration()
200                 .windowConfiguration.getBounds().width();
201         if (mLayoutResId
202                 == R.layout.desktop_mode_app_controls_window_decor) {
203             // Align the handle menu to the left of the caption.
204             menuX = mCaptionX + mMarginMenuStart;
205             menuY = mCaptionY + mMarginMenuTop;
206         } else {
207             // Position the handle menu at the center of the caption.
208             menuX = mCaptionX + (captionWidth / 2) - (mMenuWidth / 2);
209             menuY = mCaptionY + mMarginMenuStart;
210         }
211 
212         // App Info pill setup.
213         final int appInfoPillY = menuY;
214         mAppInfoPillPosition.set(menuX, appInfoPillY);
215 
216         final int windowingPillY, moreActionsPillY;
217         if (mShouldShowWindowingPill) {
218             windowingPillY = appInfoPillY + mAppInfoPillHeight + mMarginMenuSpacing;
219             mWindowingPillPosition.set(menuX, windowingPillY);
220             moreActionsPillY = windowingPillY + mWindowingPillHeight + mMarginMenuSpacing;
221             mMoreActionsPillPosition.set(menuX, moreActionsPillY);
222         } else {
223             // Just start after the end of the app info pill + margins.
224             moreActionsPillY = appInfoPillY + mAppInfoPillHeight + mMarginMenuSpacing;
225             mMoreActionsPillPosition.set(menuX, moreActionsPillY);
226         }
227     }
228 
229     /**
230      * Update pill layout, in case task changes have caused positioning to change.
231      * @param t
232      */
relayout(SurfaceControl.Transaction t)233     void relayout(SurfaceControl.Transaction t) {
234         if (mAppInfoPill != null) {
235             updateHandleMenuPillPositions();
236             t.setPosition(mAppInfoPill.mWindowSurface,
237                     mAppInfoPillPosition.x, mAppInfoPillPosition.y);
238             // Only show windowing buttons in proto2. Proto1 uses a system-level mode only.
239             final boolean shouldShowWindowingPill = DesktopModeStatus.isProto2Enabled();
240             if (shouldShowWindowingPill) {
241                 t.setPosition(mWindowingPill.mWindowSurface,
242                         mWindowingPillPosition.x, mWindowingPillPosition.y);
243             }
244             t.setPosition(mMoreActionsPill.mWindowSurface,
245                     mMoreActionsPillPosition.x, mMoreActionsPillPosition.y);
246         }
247     }
248     /**
249      * Check a passed MotionEvent if a click has occurred on any button on this caption
250      * Note this should only be called when a regular onClick is not possible
251      * (i.e. the button was clicked through status bar layer)
252      * @param ev the MotionEvent to compare against.
253      */
checkClickEvent(MotionEvent ev)254     void checkClickEvent(MotionEvent ev) {
255         final View appInfoPill = mAppInfoPill.mWindowViewHost.getView();
256         final ImageButton collapse = appInfoPill.findViewById(R.id.collapse_menu_button);
257         // Translate the input point from display coordinates to the same space as the collapse
258         // button, meaning its parent (app info pill view).
259         final PointF inputPoint = new PointF(ev.getX() - mAppInfoPillPosition.x,
260                 ev.getY() - mAppInfoPillPosition.y);
261         if (pointInView(collapse, inputPoint.x, inputPoint.y)) {
262             mOnClickListener.onClick(collapse);
263         }
264     }
265 
266     /**
267      * A valid menu input is one of the following:
268      * An input that happens in the menu views.
269      * Any input before the views have been laid out.
270      * @param inputPoint the input to compare against.
271      */
isValidMenuInput(PointF inputPoint)272     boolean isValidMenuInput(PointF inputPoint) {
273         if (!viewsLaidOut()) return true;
274         final boolean pointInAppInfoPill = pointInView(
275                 mAppInfoPill.mWindowViewHost.getView(),
276                 inputPoint.x - mAppInfoPillPosition.x,
277                 inputPoint.y - mAppInfoPillPosition.y);
278         boolean pointInWindowingPill = false;
279         if (mWindowingPill != null) {
280             pointInWindowingPill = pointInView(
281                     mWindowingPill.mWindowViewHost.getView(),
282                     inputPoint.x - mWindowingPillPosition.x,
283                     inputPoint.y - mWindowingPillPosition.y);
284         }
285         final boolean pointInMoreActionsPill = pointInView(
286                 mMoreActionsPill.mWindowViewHost.getView(),
287                 inputPoint.x - mMoreActionsPillPosition.x,
288                 inputPoint.y - mMoreActionsPillPosition.y);
289 
290         return pointInAppInfoPill || pointInWindowingPill || pointInMoreActionsPill;
291     }
292 
pointInView(View v, float x, float y)293     private boolean pointInView(View v, float x, float y) {
294         return v != null && v.getLeft() <= x && v.getRight() >= x
295                 && v.getTop() <= y && v.getBottom() >= y;
296     }
297 
298     /**
299      * Check if the views for handle menu can be seen.
300      * @return
301      */
viewsLaidOut()302     private boolean viewsLaidOut() {
303         return mAppInfoPill.mWindowViewHost.getView().isLaidOut();
304     }
305 
306 
loadHandleMenuDimensions()307     private void loadHandleMenuDimensions() {
308         final Resources resources = mContext.getResources();
309         mMenuWidth = loadDimensionPixelSize(resources,
310                 R.dimen.desktop_mode_handle_menu_width);
311         mMarginMenuTop = loadDimensionPixelSize(resources,
312                 R.dimen.desktop_mode_handle_menu_margin_top);
313         mMarginMenuStart = loadDimensionPixelSize(resources,
314                 R.dimen.desktop_mode_handle_menu_margin_start);
315         mMarginMenuSpacing = loadDimensionPixelSize(resources,
316                 R.dimen.desktop_mode_handle_menu_pill_spacing_margin);
317         mAppInfoPillHeight = loadDimensionPixelSize(resources,
318                 R.dimen.desktop_mode_handle_menu_app_info_pill_height);
319         mWindowingPillHeight = loadDimensionPixelSize(resources,
320                 R.dimen.desktop_mode_handle_menu_windowing_pill_height);
321         mMoreActionsPillHeight = loadDimensionPixelSize(resources,
322                 R.dimen.desktop_mode_handle_menu_more_actions_pill_height);
323         mShadowRadius = loadDimensionPixelSize(resources,
324                 R.dimen.desktop_mode_handle_menu_shadow_radius);
325         mCornerRadius = loadDimensionPixelSize(resources,
326                 R.dimen.desktop_mode_handle_menu_corner_radius);
327     }
328 
loadDimensionPixelSize(Resources resources, int resourceId)329     private int loadDimensionPixelSize(Resources resources, int resourceId) {
330         if (resourceId == Resources.ID_NULL) {
331             return 0;
332         }
333         return resources.getDimensionPixelSize(resourceId);
334     }
335 
close()336     void close() {
337         mAppInfoPill.releaseView();
338         mAppInfoPill = null;
339         if (mWindowingPill != null) {
340             mWindowingPill.releaseView();
341             mWindowingPill = null;
342         }
343         mMoreActionsPill.releaseView();
344         mMoreActionsPill = null;
345     }
346 
347     static final class Builder {
348         private final WindowDecoration mParent;
349         private CharSequence mName;
350         private Drawable mAppIcon;
351         private View.OnClickListener mOnClickListener;
352         private View.OnTouchListener mOnTouchListener;
353         private int mLayoutId;
354         private int mCaptionX;
355         private int mCaptionY;
356         private boolean mShowWindowingPill;
357 
358 
Builder(@onNull WindowDecoration parent)359         Builder(@NonNull WindowDecoration parent) {
360             mParent = parent;
361         }
362 
setAppName(@ullable CharSequence name)363         Builder setAppName(@Nullable CharSequence name) {
364             mName = name;
365             return this;
366         }
367 
setAppIcon(@ullable Drawable appIcon)368         Builder setAppIcon(@Nullable Drawable appIcon) {
369             mAppIcon = appIcon;
370             return this;
371         }
372 
setOnClickListener(@ullable View.OnClickListener onClickListener)373         Builder setOnClickListener(@Nullable View.OnClickListener onClickListener) {
374             mOnClickListener = onClickListener;
375             return this;
376         }
377 
setOnTouchListener(@ullable View.OnTouchListener onTouchListener)378         Builder setOnTouchListener(@Nullable View.OnTouchListener onTouchListener) {
379             mOnTouchListener = onTouchListener;
380             return this;
381         }
382 
setLayoutId(int layoutId)383         Builder setLayoutId(int layoutId) {
384             mLayoutId = layoutId;
385             return this;
386         }
387 
setCaptionPosition(int captionX, int captionY)388         Builder setCaptionPosition(int captionX, int captionY) {
389             mCaptionX = captionX;
390             mCaptionY = captionY;
391             return this;
392         }
393 
setWindowingButtonsVisible(boolean windowingButtonsVisible)394         Builder setWindowingButtonsVisible(boolean windowingButtonsVisible) {
395             mShowWindowingPill = windowingButtonsVisible;
396             return this;
397         }
398 
build()399         HandleMenu build() {
400             return new HandleMenu(mParent, mLayoutId, mCaptionX, mCaptionY, mOnClickListener,
401                     mOnTouchListener, mAppIcon, mName, mShowWindowingPill);
402         }
403     }
404 }
405