blob: ce11b2604559b25e6ff1bf2ff3aa444bec1b323f [file] [log] [blame]
/*
* Copyright (C) 2022 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.wm.shell.windowdecor;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import android.view.Choreographer;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewConfiguration;
import android.window.WindowContainerTransaction;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.windowdecor.viewholder.DesktopModeAppControlsWindowDecorationViewHolder;
import com.android.wm.shell.windowdecor.viewholder.DesktopModeFocusedWindowDecorationViewHolder;
import com.android.wm.shell.windowdecor.viewholder.DesktopModeWindowDecorationViewHolder;
import java.util.HashSet;
import java.util.Set;
/**
* Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with
* {@link DesktopModeWindowDecorViewModel}.
*
* The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't.
*/
public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
private static final String TAG = "DesktopModeWindowDecoration";
private final Handler mHandler;
private final Choreographer mChoreographer;
private final SyncTransactionQueue mSyncQueue;
private DesktopModeWindowDecorationViewHolder mWindowDecorViewHolder;
private View.OnClickListener mOnCaptionButtonClickListener;
private View.OnTouchListener mOnCaptionTouchListener;
private DragPositioningCallback mDragPositioningCallback;
private DragResizeInputListener mDragResizeListener;
private DragDetector mDragDetector;
private RelayoutParams mRelayoutParams = new RelayoutParams();
private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
new WindowDecoration.RelayoutResult<>();
private final Point mPositionInParent = new Point();
private HandleMenu mHandleMenu;
private ResizeVeil mResizeVeil;
private Drawable mAppIcon;
private CharSequence mAppName;
private TaskCornersListener mCornersListener;
private final Set<IBinder> mTransitionsPausingRelayout = new HashSet<>();
private int mRelayoutBlock;
DesktopModeWindowDecoration(
Context context,
DisplayController displayController,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
Handler handler,
Choreographer choreographer,
SyncTransactionQueue syncQueue) {
super(context, displayController, taskOrganizer, taskInfo, taskSurface);
mHandler = handler;
mChoreographer = choreographer;
mSyncQueue = syncQueue;
loadAppInfo();
}
@Override
protected Configuration getConfigurationWithOverrides(
ActivityManager.RunningTaskInfo taskInfo) {
Configuration configuration = taskInfo.getConfiguration();
if (DesktopTasksController.isDesktopDensityOverrideSet()) {
// Density is overridden for desktop tasks. Keep system density for window decoration.
configuration.densityDpi = mContext.getResources().getConfiguration().densityDpi;
}
return configuration;
}
void setCaptionListeners(
View.OnClickListener onCaptionButtonClickListener,
View.OnTouchListener onCaptionTouchListener) {
mOnCaptionButtonClickListener = onCaptionButtonClickListener;
mOnCaptionTouchListener = onCaptionTouchListener;
}
void setCornersListener(TaskCornersListener cornersListener) {
mCornersListener = cornersListener;
}
void setDragPositioningCallback(DragPositioningCallback dragPositioningCallback) {
mDragPositioningCallback = dragPositioningCallback;
}
void setDragDetector(DragDetector dragDetector) {
mDragDetector = dragDetector;
mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop());
}
@Override
void relayout(ActivityManager.RunningTaskInfo taskInfo) {
// TaskListener callbacks and shell transitions aren't synchronized, so starting a shell
// transition can trigger an onTaskInfoChanged call that updates the task's SurfaceControl
// and interferes with the transition animation that is playing at the same time.
if (mRelayoutBlock > 0) {
return;
}
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
// Use |applyStartTransactionOnDraw| so that the transaction (that applies task crop) is
// synced with the buffer transaction (that draws the View). Both will be shown on screen
// at the same, whereas applying them independently causes flickering. See b/270202228.
relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */);
}
void relayout(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
boolean applyStartTransactionOnDraw) {
final int shadowRadiusID = taskInfo.isFocused
? R.dimen.freeform_decor_shadow_focused_thickness
: R.dimen.freeform_decor_shadow_unfocused_thickness;
final boolean isFreeform =
taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM;
final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
if (isHandleMenuActive()) {
mHandleMenu.relayout(startT);
}
final WindowDecorLinearLayout oldRootView = mResult.mRootView;
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
final WindowContainerTransaction wct = new WindowContainerTransaction();
final int windowDecorLayoutId = getDesktopModeWindowDecorLayoutId(
taskInfo.getWindowingMode());
mRelayoutParams.reset();
mRelayoutParams.mRunningTaskInfo = taskInfo;
mRelayoutParams.mLayoutResId = windowDecorLayoutId;
mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
mRelayoutParams.mShadowRadiusId = shadowRadiusID;
mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
final TypedArray ta = mContext.obtainStyledAttributes(
new int[]{android.R.attr.dialogCornerRadius});
mRelayoutParams.mCornerRadius = ta.getDimensionPixelSize(0, 0);
ta.recycle();
relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
// After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
mTaskOrganizer.applyTransaction(wct);
if (mResult.mRootView == null) {
// This means something blocks the window decor from showing, e.g. the task is hidden.
// Nothing is set up in this case including the decoration surface.
return;
}
if (oldRootView != mResult.mRootView) {
if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_focused_window_decor) {
mWindowDecorViewHolder = new DesktopModeFocusedWindowDecorationViewHolder(
mResult.mRootView,
mOnCaptionTouchListener,
mOnCaptionButtonClickListener
);
} else if (mRelayoutParams.mLayoutResId
== R.layout.desktop_mode_app_controls_window_decor) {
mWindowDecorViewHolder = new DesktopModeAppControlsWindowDecorationViewHolder(
mResult.mRootView,
mOnCaptionTouchListener,
mOnCaptionButtonClickListener,
mAppName,
mAppIcon
);
} else {
throw new IllegalArgumentException("Unexpected layout resource id");
}
}
mWindowDecorViewHolder.bindData(mTaskInfo);
if (!mTaskInfo.isFocused) {
closeHandleMenu();
}
if (!isDragResizeable) {
closeDragResizeListener();
return;
}
if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) {
closeDragResizeListener();
mDragResizeListener = new DragResizeInputListener(
mContext,
mHandler,
mChoreographer,
mDisplay.getDisplayId(),
mDecorationContainerSurface,
mDragPositioningCallback);
}
final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext())
.getScaledTouchSlop();
mDragDetector.setTouchSlop(touchSlop);
final int resize_handle = mResult.mRootView.getResources()
.getDimensionPixelSize(R.dimen.freeform_resize_handle);
final int resize_corner = mResult.mRootView.getResources()
.getDimensionPixelSize(R.dimen.freeform_resize_corner);
// If either task geometry or position have changed, update this task's cornersListener
if (mDragResizeListener.setGeometry(
mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop)
|| !mTaskInfo.positionInParent.equals(mPositionInParent)) {
mCornersListener.onTaskCornersChanged(mTaskInfo.taskId, getGlobalCornersRegion());
}
mPositionInParent.set(mTaskInfo.positionInParent);
}
boolean isHandleMenuActive() {
return mHandleMenu != null;
}
private void loadAppInfo() {
String packageName = mTaskInfo.realActivity.getPackageName();
PackageManager pm = mContext.getApplicationContext().getPackageManager();
try {
IconProvider provider = new IconProvider(mContext);
mAppIcon = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity,
PackageManager.ComponentInfoFlags.of(0)));
ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName,
PackageManager.ApplicationInfoFlags.of(0));
mAppName = pm.getApplicationLabel(applicationInfo);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Package not found: " + packageName, e);
}
}
private void closeDragResizeListener() {
if (mDragResizeListener == null) {
return;
}
mDragResizeListener.close();
mDragResizeListener = null;
}
/**
* Create the resize veil for this task. Note the veil's visibility is View.GONE by default
* until a resize event calls showResizeVeil below.
*/
void createResizeVeil() {
mResizeVeil = new ResizeVeil(mContext, mAppIcon, mTaskInfo,
mSurfaceControlBuilderSupplier, mDisplay, mSurfaceControlTransactionSupplier);
}
/**
* Fade in the resize veil
*/
void showResizeVeil(Rect taskBounds) {
mResizeVeil.showVeil(mTaskSurface, taskBounds);
}
/**
* Set new bounds for the resize veil
*/
void updateResizeVeil(Rect newBounds) {
mResizeVeil.updateResizeVeil(newBounds);
}
/**
* Fade the resize veil out.
*/
void hideResizeVeil() {
mResizeVeil.hideVeil();
}
private void disposeResizeVeil() {
if (mResizeVeil == null) return;
mResizeVeil.dispose();
mResizeVeil = null;
}
/**
* Create and display handle menu window
*/
void createHandleMenu() {
mHandleMenu = new HandleMenu.Builder(this)
.setAppIcon(mAppIcon)
.setAppName(mAppName)
.setOnClickListener(mOnCaptionButtonClickListener)
.setOnTouchListener(mOnCaptionTouchListener)
.setLayoutId(mRelayoutParams.mLayoutResId)
.setCaptionPosition(mRelayoutParams.mCaptionX, mRelayoutParams.mCaptionY)
.setWindowingButtonsVisible(DesktopModeStatus.isProto2Enabled())
.build();
mHandleMenu.show();
}
/**
* Close the handle menu window
*/
void closeHandleMenu() {
if (!isHandleMenuActive()) return;
mHandleMenu.close();
mHandleMenu = null;
}
@Override
void releaseViews() {
closeHandleMenu();
super.releaseViews();
}
/**
* Close an open handle menu if input is outside of menu coordinates
*
* @param ev the tapped point to compare against
*/
void closeHandleMenuIfNeeded(MotionEvent ev) {
if (!isHandleMenuActive()) return;
PointF inputPoint = offsetCaptionLocation(ev);
// If this is called before open_menu_button's onClick, we don't want to close
// the menu since it will just reopen in onClick.
final boolean pointInOpenMenuButton = pointInView(
mResult.mRootView.findViewById(R.id.open_menu_button),
inputPoint.x,
inputPoint.y);
if (!mHandleMenu.isValidMenuInput(inputPoint) && !pointInOpenMenuButton) {
closeHandleMenu();
}
}
boolean isFocused() {
return mTaskInfo.isFocused;
}
/**
* Offset the coordinates of a {@link MotionEvent} to be in the same coordinate space as caption
*
* @param ev the {@link MotionEvent} to offset
* @return the point of the input in local space
*/
private PointF offsetCaptionLocation(MotionEvent ev) {
final PointF result = new PointF(ev.getX(), ev.getY());
final Point positionInParent = mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId)
.positionInParent;
result.offset(-mRelayoutParams.mCaptionX, -mRelayoutParams.mCaptionY);
result.offset(-positionInParent.x, -positionInParent.y);
return result;
}
/**
* Determine if a passed MotionEvent is in a view in caption
*
* @param ev the {@link MotionEvent} to check
* @param layoutId the id of the view
* @return {@code true} if event is inside the specified view, {@code false} if not
*/
private boolean checkEventInCaptionView(MotionEvent ev, int layoutId) {
if (mResult.mRootView == null) return false;
final PointF inputPoint = offsetCaptionLocation(ev);
final View view = mResult.mRootView.findViewById(layoutId);
return view != null && pointInView(view, inputPoint.x, inputPoint.y);
}
boolean checkTouchEventInHandle(MotionEvent ev) {
if (isHandleMenuActive()) return false;
return checkEventInCaptionView(ev, R.id.caption_handle);
}
/**
* Check a passed MotionEvent if a click has occurred on any button on this caption
* Note this should only be called when a regular onClick is not possible
* (i.e. the button was clicked through status bar layer)
*
* @param ev the MotionEvent to compare
*/
void checkClickEvent(MotionEvent ev) {
if (mResult.mRootView == null) return;
if (!isHandleMenuActive()) {
final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
final View handle = caption.findViewById(R.id.caption_handle);
clickIfPointInView(new PointF(ev.getX(), ev.getY()), handle);
} else {
mHandleMenu.checkClickEvent(ev);
}
}
private boolean clickIfPointInView(PointF inputPoint, View v) {
if (pointInView(v, inputPoint.x, inputPoint.y)) {
mOnCaptionButtonClickListener.onClick(v);
return true;
}
return false;
}
boolean pointInView(View v, float x, float y) {
return v != null && v.getLeft() <= x && v.getRight() >= x
&& v.getTop() <= y && v.getBottom() >= y;
}
@Override
public void close() {
closeDragResizeListener();
closeHandleMenu();
mCornersListener.onTaskCornersRemoved(mTaskInfo.taskId);
disposeResizeVeil();
super.close();
}
private int getDesktopModeWindowDecorLayoutId(int windowingMode) {
if (DesktopModeStatus.isProto1Enabled()) {
return R.layout.desktop_mode_app_controls_window_decor;
}
return windowingMode == WINDOWING_MODE_FREEFORM
? R.layout.desktop_mode_app_controls_window_decor
: R.layout.desktop_mode_focused_window_decor;
}
/**
* Create a new region out of the corner rects of this task.
*/
Region getGlobalCornersRegion() {
Region cornersRegion = mDragResizeListener.getCornersRegion();
cornersRegion.translate(mPositionInParent.x, mPositionInParent.y);
return cornersRegion;
}
/**
* If transition exists in mTransitionsPausingRelayout, remove the transition and decrement
* mRelayoutBlock
*/
void removeTransitionPausingRelayout(IBinder transition) {
if (mTransitionsPausingRelayout.remove(transition)) {
mRelayoutBlock--;
}
}
/**
* Add transition to mTransitionsPausingRelayout
*/
void addTransitionPausingRelayout(IBinder transition) {
mTransitionsPausingRelayout.add(transition);
}
/**
* If two transitions merge and the merged transition is in mTransitionsPausingRelayout,
* remove the merged transition from the set and add the transition it was merged into.
*/
public void mergeTransitionPausingRelayout(IBinder merged, IBinder playing) {
if (mTransitionsPausingRelayout.remove(merged)) {
mTransitionsPausingRelayout.add(playing);
}
}
/**
* Increase mRelayoutBlock, stopping relayout if mRelayoutBlock is now greater than 0.
*/
public void incrementRelayoutBlock() {
mRelayoutBlock++;
}
static class Factory {
DesktopModeWindowDecoration create(
Context context,
DisplayController displayController,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
Handler handler,
Choreographer choreographer,
SyncTransactionQueue syncQueue) {
return new DesktopModeWindowDecoration(
context,
displayController,
taskOrganizer,
taskInfo,
taskSurface,
handler,
choreographer,
syncQueue);
}
}
interface TaskCornersListener {
/** Inform the implementing class of this task's change in corner resize handles */
void onTaskCornersChanged(int taskId, Region corner);
/** Inform the implementing class that this task no longer needs its corners tracked,
* likely due to it closing. */
void onTaskCornersRemoved(int taskId);
}
}