/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui; import android.app.ActivityTaskManager; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.RippleDrawable; import android.hardware.display.DisplayManager; import android.inputmethodservice.InputMethodService; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import android.util.SparseArray; import android.view.Display; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; import android.widget.Button; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.PopupWindow; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.statusbar.CommandQueue; import java.lang.ref.WeakReference; /** Shows a restart-activity button when the foreground activity is in size compatibility mode. */ public class SizeCompatModeActivityController extends SystemUI implements CommandQueue.Callbacks { private static final String TAG = "SizeCompatMode"; /** The showing buttons by display id. */ private final SparseArray mActiveButtons = new SparseArray<>(1); /** Avoid creating display context frequently for non-default display. */ private final SparseArray> mDisplayContextCache = new SparseArray<>(0); /** Only show once automatically in the process life. */ private boolean mHasShownHint; public SizeCompatModeActivityController() { this(ActivityManagerWrapper.getInstance()); } @VisibleForTesting SizeCompatModeActivityController(ActivityManagerWrapper am) { am.registerTaskStackListener(new TaskStackChangeListener() { @Override public void onSizeCompatModeActivityChanged(int displayId, IBinder activityToken) { // Note the callback already runs on main thread. updateRestartButton(displayId, activityToken); } }); } @Override public void start() { SysUiServiceProvider.getComponent(mContext, CommandQueue.class).addCallback(this); } @Override public void setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition, boolean showImeSwitcher) { RestartActivityButton button = mActiveButtons.get(displayId); if (button == null) { return; } boolean imeShown = (vis & InputMethodService.IME_VISIBLE) != 0; int newVisibility = imeShown ? View.GONE : View.VISIBLE; // Hide the button when input method is showing. if (button.getVisibility() != newVisibility) { button.setVisibility(newVisibility); } } @Override public void onDisplayRemoved(int displayId) { mDisplayContextCache.remove(displayId); removeRestartButton(displayId); } private void removeRestartButton(int displayId) { RestartActivityButton button = mActiveButtons.get(displayId); if (button != null) { button.remove(); mActiveButtons.remove(displayId); } } private void updateRestartButton(int displayId, IBinder activityToken) { if (activityToken == null) { // Null token means the current foreground activity is not in size compatibility mode. removeRestartButton(displayId); return; } RestartActivityButton restartButton = mActiveButtons.get(displayId); if (restartButton != null) { restartButton.updateLastTargetActivity(activityToken); return; } Context context = getOrCreateDisplayContext(displayId); if (context == null) { Log.i(TAG, "Cannot get context for display " + displayId); return; } restartButton = createRestartButton(context); restartButton.updateLastTargetActivity(activityToken); if (restartButton.show()) { mActiveButtons.append(displayId, restartButton); } else { onDisplayRemoved(displayId); } } @VisibleForTesting RestartActivityButton createRestartButton(Context context) { RestartActivityButton button = new RestartActivityButton(context, mHasShownHint); mHasShownHint = true; return button; } private Context getOrCreateDisplayContext(int displayId) { if (displayId == Display.DEFAULT_DISPLAY) { return mContext; } Context context = null; WeakReference ref = mDisplayContextCache.get(displayId); if (ref != null) { context = ref.get(); } if (context == null) { Display display = mContext.getSystemService(DisplayManager.class).getDisplay(displayId); if (display != null) { context = mContext.createDisplayContext(display); mDisplayContextCache.put(displayId, new WeakReference(context)); } } return context; } @VisibleForTesting static class RestartActivityButton extends ImageButton implements View.OnClickListener, View.OnLongClickListener { final WindowManager.LayoutParams mWinParams; final boolean mShouldShowHint; IBinder mLastActivityToken; final int mPopupOffsetX; final int mPopupOffsetY; PopupWindow mShowingHint; RestartActivityButton(Context context, boolean hasShownHint) { super(context); mShouldShowHint = !hasShownHint; Drawable drawable = context.getDrawable(R.drawable.btn_restart); setImageDrawable(drawable); setContentDescription(context.getString(R.string.restart_button_description)); int drawableW = drawable.getIntrinsicWidth(); int drawableH = drawable.getIntrinsicHeight(); mPopupOffsetX = drawableW / 2; mPopupOffsetY = drawableH * 2; ColorStateList color = ColorStateList.valueOf(Color.LTGRAY); GradientDrawable mask = new GradientDrawable(); mask.setShape(GradientDrawable.OVAL); mask.setColor(color); setBackground(new RippleDrawable(color, null /* content */, mask)); setOnClickListener(this); setOnLongClickListener(this); mWinParams = new WindowManager.LayoutParams(); mWinParams.gravity = getGravity(getResources().getConfiguration().getLayoutDirection()); mWinParams.width = drawableW * 2; mWinParams.height = drawableH * 2; mWinParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; mWinParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; mWinParams.format = PixelFormat.TRANSLUCENT; mWinParams.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; mWinParams.setTitle(SizeCompatModeActivityController.class.getSimpleName() + context.getDisplayId()); } void updateLastTargetActivity(IBinder activityToken) { mLastActivityToken = activityToken; } /** @return {@code false} if the target display is invalid. */ boolean show() { try { getContext().getSystemService(WindowManager.class).addView(this, mWinParams); } catch (WindowManager.InvalidDisplayException e) { // The target display may have been removed when the callback has just arrived. Log.w(TAG, "Cannot show on display " + getContext().getDisplayId(), e); return false; } return true; } void remove() { getContext().getSystemService(WindowManager.class).removeViewImmediate(this); } @Override public void onClick(View v) { try { ActivityTaskManager.getService().restartActivityProcessIfVisible( mLastActivityToken); } catch (RemoteException e) { Log.w(TAG, "Unable to restart activity", e); } } @Override public boolean onLongClick(View v) { showHint(); return true; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (mShouldShowHint) { showHint(); } } @Override public void setLayoutDirection(int layoutDirection) { int gravity = getGravity(layoutDirection); if (mWinParams.gravity != gravity) { mWinParams.gravity = gravity; if (mShowingHint != null) { mShowingHint.dismiss(); showHint(); } getContext().getSystemService(WindowManager.class).updateViewLayout(this, mWinParams); } super.setLayoutDirection(layoutDirection); } void showHint() { if (mShowingHint != null) { return; } View popupView = LayoutInflater.from(getContext()).inflate( R.layout.size_compat_mode_hint, null /* root */); PopupWindow popupWindow = new PopupWindow(popupView, LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); popupWindow.setElevation(getResources().getDimension(R.dimen.bubble_elevation)); popupWindow.setAnimationStyle(android.R.style.Animation_InputMethod); popupWindow.setClippingEnabled(false); popupWindow.setOnDismissListener(() -> mShowingHint = null); mShowingHint = popupWindow; Button gotItButton = popupView.findViewById(R.id.got_it); gotItButton.setBackground(new RippleDrawable(ColorStateList.valueOf(Color.LTGRAY), null /* content */, null /* mask */)); gotItButton.setOnClickListener(view -> popupWindow.dismiss()); popupWindow.showAtLocation(this, mWinParams.gravity, mPopupOffsetX, mPopupOffsetY); } private static int getGravity(int layoutDirection) { return Gravity.BOTTOM | (layoutDirection == View.LAYOUT_DIRECTION_RTL ? Gravity.START : Gravity.END); } } }