blob: 7a543442dd8321beef7e4fce28ef2ac920b9bef5 [file] [log] [blame]
Riddle Hsucf33f1c2019-02-18 21:20:51 +08001/*
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
17package com.android.systemui;
18
19import android.app.ActivityTaskManager;
20import android.content.Context;
21import android.content.res.ColorStateList;
22import android.graphics.Color;
23import android.graphics.PixelFormat;
24import android.graphics.drawable.Drawable;
25import android.graphics.drawable.GradientDrawable;
26import android.graphics.drawable.RippleDrawable;
27import android.hardware.display.DisplayManager;
28import android.inputmethodservice.InputMethodService;
29import android.os.IBinder;
30import android.os.RemoteException;
31import android.util.Log;
32import android.util.SparseArray;
33import android.view.Display;
34import android.view.Gravity;
35import android.view.LayoutInflater;
36import android.view.View;
37import android.view.WindowManager;
38import android.widget.Button;
39import android.widget.ImageButton;
40import android.widget.LinearLayout;
41import android.widget.PopupWindow;
42
43import com.android.internal.annotations.VisibleForTesting;
44import com.android.systemui.shared.system.ActivityManagerWrapper;
45import com.android.systemui.shared.system.TaskStackChangeListener;
46import com.android.systemui.statusbar.CommandQueue;
47
48import java.lang.ref.WeakReference;
49
50/** Shows a restart-activity button when the foreground activity is in size compatibility mode. */
51public 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
62 public SizeCompatModeActivityController() {
63 this(ActivityManagerWrapper.getInstance());
64 }
65
66 @VisibleForTesting
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
78 public void start() {
79 SysUiServiceProvider.getComponent(mContext, CommandQueue.class).addCallback(this);
80 }
81
82 @Override
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
98 public void onDisplayRemoved(int displayId) {
99 mDisplayContextCache.remove(displayId);
100 removeRestartButton(displayId);
101 }
102
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
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 restartButton.show();
133 mActiveButtons.append(displayId, restartButton);
134 }
135
136 @VisibleForTesting
137 RestartActivityButton createRestartButton(Context context) {
138 RestartActivityButton button = new RestartActivityButton(context, mHasShownHint);
139 mHasShownHint = true;
140 return button;
141 }
142
143 private Context getOrCreateDisplayContext(int displayId) {
144 if (displayId == Display.DEFAULT_DISPLAY) {
145 return mContext;
146 }
147 Context context = null;
148 WeakReference<Context> ref = mDisplayContextCache.get(displayId);
149 if (ref != null) {
150 context = ref.get();
151 }
152 if (context == null) {
153 Display display = mContext.getSystemService(DisplayManager.class).getDisplay(displayId);
154 if (display != null) {
155 context = mContext.createDisplayContext(display);
156 mDisplayContextCache.put(displayId, new WeakReference<Context>(context));
157 }
158 }
159 return context;
160 }
161
162 @VisibleForTesting
163 static class RestartActivityButton extends ImageButton implements View.OnClickListener,
164 View.OnLongClickListener {
165
166 final WindowManager.LayoutParams mWinParams;
167 final boolean mShouldShowHint;
168 IBinder mLastActivityToken;
169
170 final int mPopupOffsetX;
171 final int mPopupOffsetY;
172 PopupWindow mShowingHint;
173
174 RestartActivityButton(Context context, boolean hasShownHint) {
175 super(context);
176 mShouldShowHint = !hasShownHint;
177 Drawable drawable = context.getDrawable(R.drawable.btn_restart);
178 setImageDrawable(drawable);
179 setContentDescription(context.getString(R.string.restart_button_description));
180
181 int drawableW = drawable.getIntrinsicWidth();
182 int drawableH = drawable.getIntrinsicHeight();
183 mPopupOffsetX = drawableW / 2;
184 mPopupOffsetY = drawableH * 2;
185
186 ColorStateList color = ColorStateList.valueOf(Color.LTGRAY);
187 GradientDrawable mask = new GradientDrawable();
188 mask.setShape(GradientDrawable.OVAL);
189 mask.setColor(color);
190 setBackground(new RippleDrawable(color, null /* content */, mask));
191 setOnClickListener(this);
192 setOnLongClickListener(this);
193
194 mWinParams = new WindowManager.LayoutParams();
195 mWinParams.gravity = getGravity(getResources().getConfiguration().getLayoutDirection());
196 mWinParams.width = drawableW * 2;
197 mWinParams.height = drawableH * 2;
198 mWinParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
199 mWinParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
200 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
201 mWinParams.format = PixelFormat.TRANSLUCENT;
202 mWinParams.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
203 mWinParams.setTitle(SizeCompatModeActivityController.class.getSimpleName()
204 + context.getDisplayId());
205 }
206
207 void updateLastTargetActivity(IBinder activityToken) {
208 mLastActivityToken = activityToken;
209 }
210
211 void show() {
212 getContext().getSystemService(WindowManager.class).addView(this, mWinParams);
213 }
214
215 void remove() {
216 getContext().getSystemService(WindowManager.class).removeViewImmediate(this);
217 }
218
219 @Override
220 public void onClick(View v) {
221 try {
222 ActivityTaskManager.getService().restartActivityProcessIfVisible(
223 mLastActivityToken);
224 } catch (RemoteException e) {
225 Log.w(TAG, "Unable to restart activity", e);
226 }
227 }
228
229 @Override
230 public boolean onLongClick(View v) {
231 showHint();
232 return true;
233 }
234
235 @Override
236 protected void onAttachedToWindow() {
237 super.onAttachedToWindow();
238 if (mShouldShowHint) {
239 showHint();
240 }
241 }
242
243 @Override
244 public void setLayoutDirection(int layoutDirection) {
245 int gravity = getGravity(layoutDirection);
246 if (mWinParams.gravity != gravity) {
247 mWinParams.gravity = gravity;
248 if (mShowingHint != null) {
249 mShowingHint.dismiss();
250 showHint();
251 }
252 getContext().getSystemService(WindowManager.class).updateViewLayout(this,
253 mWinParams);
254 }
255 super.setLayoutDirection(layoutDirection);
256 }
257
258 void showHint() {
259 if (mShowingHint != null) {
260 return;
261 }
262
263 View popupView = LayoutInflater.from(getContext()).inflate(
264 R.layout.size_compat_mode_hint, null /* root */);
265 PopupWindow popupWindow = new PopupWindow(popupView,
266 LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
267 popupWindow.setElevation(getResources().getDimension(R.dimen.bubble_elevation));
268 popupWindow.setAnimationStyle(android.R.style.Animation_InputMethod);
269 popupWindow.setClippingEnabled(false);
270 popupWindow.setOnDismissListener(() -> mShowingHint = null);
271 mShowingHint = popupWindow;
272
273 Button gotItButton = popupView.findViewById(R.id.got_it);
274 gotItButton.setBackground(new RippleDrawable(ColorStateList.valueOf(Color.LTGRAY),
275 null /* content */, null /* mask */));
276 gotItButton.setOnClickListener(view -> popupWindow.dismiss());
277 popupWindow.showAtLocation(this, mWinParams.gravity, mPopupOffsetX, mPopupOffsetY);
278 }
279
280 private static int getGravity(int layoutDirection) {
281 return Gravity.BOTTOM
282 | (layoutDirection == View.LAYOUT_DIRECTION_RTL ? Gravity.START : Gravity.END);
283 }
284 }
285}