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