1 /*
2  * Copyright (C) 2019 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.systemui;
18 
19 import android.app.ActivityTaskManager;
20 import android.content.Context;
21 import android.content.res.ColorStateList;
22 import android.graphics.Color;
23 import android.graphics.PixelFormat;
24 import android.graphics.drawable.Drawable;
25 import android.graphics.drawable.GradientDrawable;
26 import android.graphics.drawable.RippleDrawable;
27 import android.hardware.display.DisplayManager;
28 import android.inputmethodservice.InputMethodService;
29 import android.os.IBinder;
30 import android.os.RemoteException;
31 import android.util.Log;
32 import android.util.SparseArray;
33 import android.view.Display;
34 import android.view.Gravity;
35 import android.view.LayoutInflater;
36 import android.view.View;
37 import android.view.WindowManager;
38 import android.widget.Button;
39 import android.widget.ImageButton;
40 import android.widget.LinearLayout;
41 import android.widget.PopupWindow;
42 
43 import com.android.internal.annotations.VisibleForTesting;
44 import com.android.systemui.shared.system.ActivityManagerWrapper;
45 import com.android.systemui.shared.system.TaskStackChangeListener;
46 import com.android.systemui.statusbar.CommandQueue;
47 
48 import java.lang.ref.WeakReference;
49 
50 /** Shows a restart-activity button when the foreground activity is in size compatibility mode. */
51 public class SizeCompatModeActivityController extends SystemUI implements CommandQueue.Callbacks {
52     private static final String TAG = "SizeCompatMode";
53 
54     /** The showing buttons by display id. */
55     private final SparseArray<RestartActivityButton> mActiveButtons = new SparseArray<>(1);
56     /** Avoid creating display context frequently for non-default display. */
57     private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0);
58 
59     /** Only show once automatically in the process life. */
60     private boolean mHasShownHint;
61 
SizeCompatModeActivityController()62     public SizeCompatModeActivityController() {
63         this(ActivityManagerWrapper.getInstance());
64     }
65 
66     @VisibleForTesting
SizeCompatModeActivityController(ActivityManagerWrapper am)67     SizeCompatModeActivityController(ActivityManagerWrapper am) {
68         am.registerTaskStackListener(new TaskStackChangeListener() {
69             @Override
70             public void onSizeCompatModeActivityChanged(int displayId, IBinder activityToken) {
71                 // Note the callback already runs on main thread.
72                 updateRestartButton(displayId, activityToken);
73             }
74         });
75     }
76 
77     @Override
start()78     public void start() {
79         SysUiServiceProvider.getComponent(mContext, CommandQueue.class).addCallback(this);
80     }
81 
82     @Override
setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition, boolean showImeSwitcher)83     public void setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition,
84             boolean showImeSwitcher) {
85         RestartActivityButton button = mActiveButtons.get(displayId);
86         if (button == null) {
87             return;
88         }
89         boolean imeShown = (vis & InputMethodService.IME_VISIBLE) != 0;
90         int newVisibility = imeShown ? View.GONE : View.VISIBLE;
91         // Hide the button when input method is showing.
92         if (button.getVisibility() != newVisibility) {
93             button.setVisibility(newVisibility);
94         }
95     }
96 
97     @Override
onDisplayRemoved(int displayId)98     public void onDisplayRemoved(int displayId) {
99         mDisplayContextCache.remove(displayId);
100         removeRestartButton(displayId);
101     }
102 
removeRestartButton(int displayId)103     private void removeRestartButton(int displayId) {
104         RestartActivityButton button = mActiveButtons.get(displayId);
105         if (button != null) {
106             button.remove();
107             mActiveButtons.remove(displayId);
108         }
109     }
110 
updateRestartButton(int displayId, IBinder activityToken)111     private void updateRestartButton(int displayId, IBinder activityToken) {
112         if (activityToken == null) {
113             // Null token means the current foreground activity is not in size compatibility mode.
114             removeRestartButton(displayId);
115             return;
116         }
117 
118         RestartActivityButton restartButton = mActiveButtons.get(displayId);
119         if (restartButton != null) {
120             restartButton.updateLastTargetActivity(activityToken);
121             return;
122         }
123 
124         Context context = getOrCreateDisplayContext(displayId);
125         if (context == null) {
126             Log.i(TAG, "Cannot get context for display " + displayId);
127             return;
128         }
129 
130         restartButton = createRestartButton(context);
131         restartButton.updateLastTargetActivity(activityToken);
132         if (restartButton.show()) {
133             mActiveButtons.append(displayId, restartButton);
134         } else {
135             onDisplayRemoved(displayId);
136         }
137     }
138 
139     @VisibleForTesting
createRestartButton(Context context)140     RestartActivityButton createRestartButton(Context context) {
141         RestartActivityButton button = new RestartActivityButton(context, mHasShownHint);
142         mHasShownHint = true;
143         return button;
144     }
145 
getOrCreateDisplayContext(int displayId)146     private Context getOrCreateDisplayContext(int displayId) {
147         if (displayId == Display.DEFAULT_DISPLAY) {
148             return mContext;
149         }
150         Context context = null;
151         WeakReference<Context> ref = mDisplayContextCache.get(displayId);
152         if (ref != null) {
153             context = ref.get();
154         }
155         if (context == null) {
156             Display display = mContext.getSystemService(DisplayManager.class).getDisplay(displayId);
157             if (display != null) {
158                 context = mContext.createDisplayContext(display);
159                 mDisplayContextCache.put(displayId, new WeakReference<Context>(context));
160             }
161         }
162         return context;
163     }
164 
165     @VisibleForTesting
166     static class RestartActivityButton extends ImageButton implements View.OnClickListener,
167             View.OnLongClickListener {
168 
169         final WindowManager.LayoutParams mWinParams;
170         final boolean mShouldShowHint;
171         IBinder mLastActivityToken;
172 
173         final int mPopupOffsetX;
174         final int mPopupOffsetY;
175         PopupWindow mShowingHint;
176 
RestartActivityButton(Context context, boolean hasShownHint)177         RestartActivityButton(Context context, boolean hasShownHint) {
178             super(context);
179             mShouldShowHint = !hasShownHint;
180             Drawable drawable = context.getDrawable(R.drawable.btn_restart);
181             setImageDrawable(drawable);
182             setContentDescription(context.getString(R.string.restart_button_description));
183 
184             int drawableW = drawable.getIntrinsicWidth();
185             int drawableH = drawable.getIntrinsicHeight();
186             mPopupOffsetX = drawableW / 2;
187             mPopupOffsetY = drawableH * 2;
188 
189             ColorStateList color = ColorStateList.valueOf(Color.LTGRAY);
190             GradientDrawable mask = new GradientDrawable();
191             mask.setShape(GradientDrawable.OVAL);
192             mask.setColor(color);
193             setBackground(new RippleDrawable(color, null /* content */, mask));
194             setOnClickListener(this);
195             setOnLongClickListener(this);
196 
197             mWinParams = new WindowManager.LayoutParams();
198             mWinParams.gravity = getGravity(getResources().getConfiguration().getLayoutDirection());
199             mWinParams.width = drawableW * 2;
200             mWinParams.height = drawableH * 2;
201             mWinParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
202             mWinParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
203                     | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
204             mWinParams.format = PixelFormat.TRANSLUCENT;
205             mWinParams.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
206             mWinParams.setTitle(SizeCompatModeActivityController.class.getSimpleName()
207                     + context.getDisplayId());
208         }
209 
updateLastTargetActivity(IBinder activityToken)210         void updateLastTargetActivity(IBinder activityToken) {
211             mLastActivityToken = activityToken;
212         }
213 
214         /** @return {@code false} if the target display is invalid. */
show()215         boolean show() {
216             try {
217                 getContext().getSystemService(WindowManager.class).addView(this, mWinParams);
218             } catch (WindowManager.InvalidDisplayException e) {
219                 // The target display may have been removed when the callback has just arrived.
220                 Log.w(TAG, "Cannot show on display " + getContext().getDisplayId(), e);
221                 return false;
222             }
223             return true;
224         }
225 
remove()226         void remove() {
227             getContext().getSystemService(WindowManager.class).removeViewImmediate(this);
228         }
229 
230         @Override
onClick(View v)231         public void onClick(View v) {
232             try {
233                 ActivityTaskManager.getService().restartActivityProcessIfVisible(
234                         mLastActivityToken);
235             } catch (RemoteException e) {
236                 Log.w(TAG, "Unable to restart activity", e);
237             }
238         }
239 
240         @Override
onLongClick(View v)241         public boolean onLongClick(View v) {
242             showHint();
243             return true;
244         }
245 
246         @Override
onAttachedToWindow()247         protected void onAttachedToWindow() {
248             super.onAttachedToWindow();
249             if (mShouldShowHint) {
250                 showHint();
251             }
252         }
253 
254         @Override
setLayoutDirection(int layoutDirection)255         public void setLayoutDirection(int layoutDirection) {
256             int gravity = getGravity(layoutDirection);
257             if (mWinParams.gravity != gravity) {
258                 mWinParams.gravity = gravity;
259                 if (mShowingHint != null) {
260                     mShowingHint.dismiss();
261                     showHint();
262                 }
263                 getContext().getSystemService(WindowManager.class).updateViewLayout(this,
264                         mWinParams);
265             }
266             super.setLayoutDirection(layoutDirection);
267         }
268 
showHint()269         void showHint() {
270             if (mShowingHint != null) {
271                 return;
272             }
273 
274             View popupView = LayoutInflater.from(getContext()).inflate(
275                     R.layout.size_compat_mode_hint, null /* root */);
276             PopupWindow popupWindow = new PopupWindow(popupView,
277                     LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
278             popupWindow.setElevation(getResources().getDimension(R.dimen.bubble_elevation));
279             popupWindow.setAnimationStyle(android.R.style.Animation_InputMethod);
280             popupWindow.setClippingEnabled(false);
281             popupWindow.setOnDismissListener(() -> mShowingHint = null);
282             mShowingHint = popupWindow;
283 
284             Button gotItButton = popupView.findViewById(R.id.got_it);
285             gotItButton.setBackground(new RippleDrawable(ColorStateList.valueOf(Color.LTGRAY),
286                     null /* content */, null /* mask */));
287             gotItButton.setOnClickListener(view -> popupWindow.dismiss());
288             popupWindow.showAtLocation(this, mWinParams.gravity, mPopupOffsetX, mPopupOffsetY);
289         }
290 
getGravity(int layoutDirection)291         private static int getGravity(int layoutDirection) {
292             return Gravity.BOTTOM
293                     | (layoutDirection == View.LAYOUT_DIRECTION_RTL ? Gravity.START : Gravity.END);
294         }
295     }
296 }
297