| /* |
| * 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); |
| } |
| } |
| } |