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