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