/*
|
* 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<RestartActivityButton> mActiveButtons = new SparseArray<>(1);
|
/** Avoid creating display context frequently for non-default display. */
|
private final SparseArray<WeakReference<Context>> 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<Context> 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>(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);
|
}
|
}
|
}
|