blob: 72a40303529480bfe7aaa9935059e95f48313bb5 [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
Dave Mankoffbcaca8a2019-10-31 18:04:08 -040050import javax.inject.Inject;
51import javax.inject.Singleton;
52
Riddle Hsucf33f1c2019-02-18 21:20:51 +080053/** Shows a restart-activity button when the foreground activity is in size compatibility mode. */
Dave Mankoffbcaca8a2019-10-31 18:04:08 -040054@Singleton
Riddle Hsucf33f1c2019-02-18 21:20:51 +080055public 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);
Dave Mankoffbcaca8a2019-10-31 18:04:08 -040062 private final CommandQueue mCommandQueue;
Riddle Hsucf33f1c2019-02-18 21:20:51 +080063
64 /** Only show once automatically in the process life. */
65 private boolean mHasShownHint;
66
Riddle Hsucf33f1c2019-02-18 21:20:51 +080067 @VisibleForTesting
Dave Mankoffbcaca8a2019-10-31 18:04:08 -040068 @Inject
69 SizeCompatModeActivityController(Context context, ActivityManagerWrapper am,
70 CommandQueue commandQueue) {
Dave Mankoffa5d8a392019-10-10 12:21:09 -040071 super(context);
Dave Mankoffbcaca8a2019-10-31 18:04:08 -040072 mCommandQueue = commandQueue;
Riddle Hsucf33f1c2019-02-18 21:20:51 +080073 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
83 public void start() {
Dave Mankoffbcaca8a2019-10-31 18:04:08 -040084 mCommandQueue.addCallback(this);
Riddle Hsucf33f1c2019-02-18 21:20:51 +080085 }
86
87 @Override
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
103 public void onDisplayRemoved(int displayId) {
104 mDisplayContextCache.remove(displayId);
105 removeRestartButton(displayId);
106 }
107
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
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);
Riddle Hsu74826262019-04-17 14:57:42 +0800137 if (restartButton.show()) {
138 mActiveButtons.append(displayId, restartButton);
139 } else {
140 onDisplayRemoved(displayId);
141 }
Riddle Hsucf33f1c2019-02-18 21:20:51 +0800142 }
143
144 @VisibleForTesting
145 RestartActivityButton createRestartButton(Context context) {
146 RestartActivityButton button = new RestartActivityButton(context, mHasShownHint);
147 mHasShownHint = true;
148 return button;
149 }
150
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
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;
Roshan Piusa3f89c62019-10-11 08:50:53 -0700210 mWinParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
Riddle Hsucf33f1c2019-02-18 21:20:51 +0800211 mWinParams.setTitle(SizeCompatModeActivityController.class.getSimpleName()
212 + context.getDisplayId());
213 }
214
215 void updateLastTargetActivity(IBinder activityToken) {
216 mLastActivityToken = activityToken;
217 }
218
Riddle Hsu74826262019-04-17 14:57:42 +0800219 /** @return {@code false} if the target display is invalid. */
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;
Riddle Hsucf33f1c2019-02-18 21:20:51 +0800229 }
230
231 void remove() {
232 getContext().getSystemService(WindowManager.class).removeViewImmediate(this);
233 }
234
235 @Override
236 public void onClick(View v) {
237 try {
238 ActivityTaskManager.getService().restartActivityProcessIfVisible(
239 mLastActivityToken);
240 } catch (RemoteException e) {
241 Log.w(TAG, "Unable to restart activity", e);
242 }
243 }
244
245 @Override
246 public boolean onLongClick(View v) {
247 showHint();
248 return true;
249 }
250
251 @Override
252 protected void onAttachedToWindow() {
253 super.onAttachedToWindow();
254 if (mShouldShowHint) {
255 showHint();
256 }
257 }
258
259 @Override
260 public void setLayoutDirection(int layoutDirection) {
261 int gravity = getGravity(layoutDirection);
262 if (mWinParams.gravity != gravity) {
263 mWinParams.gravity = gravity;
264 if (mShowingHint != null) {
265 mShowingHint.dismiss();
266 showHint();
267 }
268 getContext().getSystemService(WindowManager.class).updateViewLayout(this,
269 mWinParams);
270 }
271 super.setLayoutDirection(layoutDirection);
272 }
273
274 void showHint() {
275 if (mShowingHint != null) {
276 return;
277 }
278
279 View popupView = LayoutInflater.from(getContext()).inflate(
280 R.layout.size_compat_mode_hint, null /* root */);
281 PopupWindow popupWindow = new PopupWindow(popupView,
282 LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
283 popupWindow.setElevation(getResources().getDimension(R.dimen.bubble_elevation));
284 popupWindow.setAnimationStyle(android.R.style.Animation_InputMethod);
285 popupWindow.setClippingEnabled(false);
286 popupWindow.setOnDismissListener(() -> mShowingHint = null);
287 mShowingHint = popupWindow;
288
289 Button gotItButton = popupView.findViewById(R.id.got_it);
290 gotItButton.setBackground(new RippleDrawable(ColorStateList.valueOf(Color.LTGRAY),
291 null /* content */, null /* mask */));
292 gotItButton.setOnClickListener(view -> popupWindow.dismiss());
293 popupWindow.showAtLocation(this, mWinParams.gravity, mPopupOffsetX, mPopupOffsetY);
294 }
295
296 private static int getGravity(int layoutDirection) {
297 return Gravity.BOTTOM
298 | (layoutDirection == View.LAYOUT_DIRECTION_RTL ? Gravity.START : Gravity.END);
299 }
300 }
301}