blob: a8d6442135ce4854a1eceb05c93662828b1ea82c [file] [log] [blame]
/*
* Copyright (C) 2017 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.quickstep.views;
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.os.Build;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.Property;
import android.view.Surface;
import android.view.View;
import androidx.annotation.RequiresApi;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.util.Themes;
import com.android.quickstep.TaskOverlayFactory;
import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
import com.android.quickstep.views.TaskView.FullscreenDrawParams;
import com.android.systemui.plugins.OverviewScreenshotActions;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
/**
* A task in the Recents view.
*/
public class TaskThumbnailView extends View implements PluginListener<OverviewScreenshotActions> {
private static final ColorMatrix COLOR_MATRIX = new ColorMatrix();
private static final ColorMatrix SATURATION_COLOR_MATRIX = new ColorMatrix();
private static final MainThreadInitializedObject<FullscreenDrawParams> TEMP_PARAMS =
new MainThreadInitializedObject<>(FullscreenDrawParams::new);
public static final Property<TaskThumbnailView, Float> DIM_ALPHA =
new FloatProperty<TaskThumbnailView>("dimAlpha") {
@Override
public void setValue(TaskThumbnailView thumbnail, float dimAlpha) {
thumbnail.setDimAlpha(dimAlpha);
}
@Override
public Float get(TaskThumbnailView thumbnailView) {
return thumbnailView.mDimAlpha;
}
};
private final BaseActivity mActivity;
private final TaskOverlay mOverlay;
private final boolean mIsDarkTextTheme;
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint mClearPaint = new Paint();
private final Paint mDimmingPaintAfterClearing = new Paint();
// Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
private final Rect mPreviewRect = new Rect();
private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper();
private TaskView.FullscreenDrawParams mFullscreenParams;
private Task mTask;
private ThumbnailData mThumbnailData;
protected BitmapShader mBitmapShader;
private float mDimAlpha = 1f;
private float mDimAlphaMultiplier = 1f;
private float mSaturation = 1f;
private boolean mOverlayEnabled;
private OverviewScreenshotActions mOverviewScreenshotActionsPlugin;
public TaskThumbnailView(Context context) {
this(context, null);
}
public TaskThumbnailView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mOverlay = TaskOverlayFactory.INSTANCE.get(context).createOverlay(this);
mPaint.setFilterBitmap(true);
mBackgroundPaint.setColor(Color.WHITE);
mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
mDimmingPaintAfterClearing.setColor(Color.BLACK);
mActivity = BaseActivity.fromContext(context);
mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText);
// Initialize with dummy value. It is overridden later by TaskView
mFullscreenParams = TEMP_PARAMS.get(context);
}
/**
* Updates the thumbnail to draw the provided task
* @param task
*/
public void bind(Task task) {
mOverlay.reset();
mTask = task;
int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000;
mPaint.setColor(color);
mBackgroundPaint.setColor(color);
}
/**
* Updates the thumbnail.
* @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately.
* In most cases, we use the {@link #setThumbnail(Task, ThumbnailData)}
* version with {@code refreshNow} is true. The only exception is
* in the live tile case that we grab a screenshot when user enters Overview
* upon swipe up so that a usable screenshot is accessible immediately when
* recents animation needs to be finished / cancelled.
*/
public void setThumbnail(Task task, ThumbnailData thumbnailData, boolean refreshNow) {
mTask = task;
mThumbnailData =
(thumbnailData != null && thumbnailData.thumbnail != null) ? thumbnailData : null;
if (refreshNow) {
refresh();
}
}
/** See {@link #setThumbnail(Task, ThumbnailData, boolean)} */
public void setThumbnail(Task task, ThumbnailData thumbnailData) {
setThumbnail(task, thumbnailData, true /* refreshNow */);
}
/** Updates the shader, paint, matrix to redraw. */
public void refresh() {
if (mThumbnailData != null && mThumbnailData.thumbnail != null) {
Bitmap bm = mThumbnailData.thumbnail;
bm.prepareToDraw();
mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mPaint.setShader(mBitmapShader);
updateThumbnailMatrix();
} else {
mBitmapShader = null;
mThumbnailData = null;
mPaint.setShader(null);
mOverlay.reset();
}
if (mOverviewScreenshotActionsPlugin != null) {
mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
}
updateThumbnailPaintFilter();
}
public void setDimAlphaMultipler(float dimAlphaMultipler) {
mDimAlphaMultiplier = dimAlphaMultipler;
setDimAlpha(mDimAlpha);
}
/**
* Sets the alpha of the dim layer on top of this view.
* <p>
* If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be black.
*/
public void setDimAlpha(float dimAlpha) {
mDimAlpha = dimAlpha;
updateThumbnailPaintFilter();
}
public TaskOverlay getTaskOverlay() {
return mOverlay;
}
public float getDimAlpha() {
return mDimAlpha;
}
public Rect getInsets(Rect fallback) {
if (mThumbnailData != null) {
return mThumbnailData.insets;
}
return fallback;
}
/**
* Get the scaled insets that are being used to draw the task view. This is a subsection of
* the full snapshot.
* @return the insets in snapshot bitmap coordinates.
*/
@RequiresApi(api = Build.VERSION_CODES.Q)
public Insets getScaledInsets() {
if (mThumbnailData == null) {
return Insets.NONE;
}
RectF bitmapRect = new RectF(
0, 0,
mThumbnailData.thumbnail.getWidth(), mThumbnailData.thumbnail.getHeight());
RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
// The position helper matrix tells us how to transform the bitmap to fit the view, the
// inverse tells us where the view would be in the bitmaps coordinates. The insets are the
// difference between the bitmap bounds and the projected view bounds.
Matrix boundsToBitmapSpace = new Matrix();
mPreviewPositionHelper.getMatrix().invert(boundsToBitmapSpace);
RectF boundsInBitmapSpace = new RectF();
boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);
return Insets.of(
Math.round(boundsInBitmapSpace.left),
Math.round(boundsInBitmapSpace.top),
Math.round(bitmapRect.right - boundsInBitmapSpace.right),
Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom));
}
public int getSysUiStatusNavFlags() {
if (mThumbnailData != null) {
int flags = 0;
flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0
? SystemUiController.FLAG_LIGHT_STATUS
: SystemUiController.FLAG_DARK_STATUS;
flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0
? SystemUiController.FLAG_LIGHT_NAV
: SystemUiController.FLAG_DARK_NAV;
return flags;
}
return 0;
}
@Override
protected void onDraw(Canvas canvas) {
RectF currentDrawnInsets = mFullscreenParams.mCurrentDrawnInsets;
canvas.save();
canvas.scale(mFullscreenParams.mScale, mFullscreenParams.mScale);
canvas.translate(currentDrawnInsets.left, currentDrawnInsets.top);
// Draw the insets if we're being drawn fullscreen (we do this for quick switch).
drawOnCanvas(canvas,
-currentDrawnInsets.left,
-currentDrawnInsets.top,
getMeasuredWidth() + currentDrawnInsets.right,
getMeasuredHeight() + currentDrawnInsets.bottom,
mFullscreenParams.mCurrentDrawnCornerRadius);
canvas.restore();
}
@Override
public void onPluginConnected(OverviewScreenshotActions overviewScreenshotActions,
Context context) {
mOverviewScreenshotActionsPlugin = overviewScreenshotActions;
mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
}
@Override
public void onPluginDisconnected(OverviewScreenshotActions plugin) {
if (mOverviewScreenshotActionsPlugin != null) {
mOverviewScreenshotActionsPlugin = null;
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
PluginManagerWrapper.INSTANCE.get(getContext())
.addPluginListener(this, OverviewScreenshotActions.class);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this);
}
public PreviewPositionHelper getPreviewPositionHelper() {
return mPreviewPositionHelper;
}
public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) {
mFullscreenParams = fullscreenParams;
invalidate();
}
public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height,
float cornerRadius) {
if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) {
canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint);
canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius,
mDimmingPaintAfterClearing);
return;
}
}
// Draw the background in all cases, except when the thumbnail data is opaque
final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null
|| mThumbnailData == null;
if (drawBackgroundOnly || mPreviewPositionHelper.mClipBottom > 0
|| mThumbnailData.isTranslucent) {
canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint);
if (drawBackgroundOnly) {
return;
}
}
if (mPreviewPositionHelper.mClipBottom > 0) {
canvas.save();
canvas.clipRect(x, y, width, mPreviewPositionHelper.mClipBottom);
canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
canvas.restore();
} else {
canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
}
}
public TaskView getTaskView() {
return (TaskView) getParent();
}
public void setOverlayEnabled(boolean overlayEnabled) {
if (mOverlayEnabled != overlayEnabled) {
mOverlayEnabled = overlayEnabled;
updateOverlay();
}
}
private void updateOverlay() {
if (mOverlayEnabled) {
mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix,
mPreviewPositionHelper.mIsOrientationChanged);
} else {
mOverlay.reset();
}
}
private void updateThumbnailPaintFilter() {
int mul = (int) ((1 - mDimAlpha * mDimAlphaMultiplier) * 255);
ColorFilter filter = getColorFilter(mul, mIsDarkTextTheme, mSaturation);
mBackgroundPaint.setColorFilter(filter);
mDimmingPaintAfterClearing.setAlpha(255 - mul);
if (mBitmapShader != null) {
mPaint.setColorFilter(filter);
} else {
mPaint.setColorFilter(null);
mPaint.setColor(Color.argb(255, mul, mul, mul));
}
invalidate();
}
private void updateThumbnailMatrix() {
mPreviewPositionHelper.mClipBottom = -1;
mPreviewPositionHelper.mIsOrientationChanged = false;
if (mBitmapShader != null && mThumbnailData != null) {
mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
mThumbnailData.thumbnail.getHeight());
int currentRotation = getTaskView().getRecentsView().getPagedViewOrientedState()
.getRecentsActivityRotation();
mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
getMeasuredWidth(), getMeasuredHeight(), mActivity.getDeviceProfile(),
currentRotation);
mBitmapShader.setLocalMatrix(mPreviewPositionHelper.mMatrix);
mPaint.setShader(mBitmapShader);
}
getTaskView().updateCurrentFullscreenParams(mPreviewPositionHelper);
invalidate();
// Update can be called from {@link #onSizeChanged} during layout, post handling of overlay
// as overlay could modify the views in the overlay as a side effect of its update.
post(this::updateOverlay);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
updateThumbnailMatrix();
}
/**
* @param intensity multiplier for color values. 0 - make black (white if shouldLighten), 255 -
* leave unchanged.
*/
private static ColorFilter getColorFilter(int intensity, boolean shouldLighten,
float saturation) {
intensity = Utilities.boundToRange(intensity, 0, 255);
if (intensity == 255 && saturation == 1) {
return null;
}
final float intensityScale = intensity / 255f;
COLOR_MATRIX.setScale(intensityScale, intensityScale, intensityScale, 1);
if (saturation != 1) {
SATURATION_COLOR_MATRIX.setSaturation(saturation);
COLOR_MATRIX.postConcat(SATURATION_COLOR_MATRIX);
}
if (shouldLighten) {
final float[] colorArray = COLOR_MATRIX.getArray();
final int colorAdd = 255 - intensity;
colorArray[4] = colorAdd;
colorArray[9] = colorAdd;
colorArray[14] = colorAdd;
}
return new ColorMatrixColorFilter(COLOR_MATRIX);
}
public Bitmap getThumbnail() {
if (mThumbnailData == null) {
return null;
}
return mThumbnailData.thumbnail;
}
/**
* Returns whether the snapshot is real.
*/
public boolean isRealSnapshot() {
if (mThumbnailData == null) {
return false;
}
return mThumbnailData.isRealSnapshot;
}
/**
* Utility class to position the thumbnail in the TaskView
*/
public static class PreviewPositionHelper {
// Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
private final RectF mClippedInsets = new RectF();
private final Matrix mMatrix = new Matrix();
private float mClipBottom = -1;
private boolean mIsOrientationChanged;
public Matrix getMatrix() {
return mMatrix;
}
/**
* Updates the matrix based on the provided parameters
*/
public void updateThumbnailMatrix(Rect thumbnailPosition, ThumbnailData thumbnailData,
int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation) {
boolean isRotated = false;
boolean isOrientationDifferent;
mClipBottom = -1;
float scale = thumbnailData.scale;
Rect activityInsets = dp.getInsets();
Rect thumbnailInsets = getBoundedInsets(activityInsets, thumbnailData.insets);
final float thumbnailWidth = thumbnailPosition.width()
- (thumbnailInsets.left + thumbnailInsets.right) * scale;
final float thumbnailHeight = thumbnailPosition.height()
- (thumbnailInsets.top + thumbnailInsets.bottom) * scale;
final float thumbnailScale;
int thumbnailRotation = thumbnailData.rotation;
int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
// Landscape vs portrait change
boolean windowingModeSupportsRotation = !dp.isMultiWindowMode
&& thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
isOrientationDifferent = isOrientationChange(deltaRotate)
&& windowingModeSupportsRotation;
if (canvasWidth == 0) {
// If we haven't measured , skip the thumbnail drawing and only draw the background
// color
thumbnailScale = 0f;
} else {
// Rotate the screenshot if not in multi-window mode
isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
// Scale the screenshot to always fit the width of the card.
thumbnailScale = isOrientationDifferent
? canvasWidth / thumbnailHeight
: canvasWidth / thumbnailWidth;
}
Rect splitScreenInsets = dp.getInsets();
if (!isRotated) {
// No Rotation
if (dp.isMultiWindowMode) {
mClippedInsets.offsetTo(splitScreenInsets.left * scale,
splitScreenInsets.top * scale);
} else {
mClippedInsets.offsetTo(thumbnailInsets.left * scale,
thumbnailInsets.top * scale);
}
mMatrix.setTranslate(
-thumbnailInsets.left * scale,
-thumbnailInsets.top * scale);
} else {
setThumbnailRotation(deltaRotate, thumbnailInsets, scale, thumbnailPosition);
}
final float widthWithInsets;
final float heightWithInsets;
if (isOrientationDifferent) {
widthWithInsets = thumbnailPosition.height() * thumbnailScale;
heightWithInsets = thumbnailPosition.width() * thumbnailScale;
} else {
widthWithInsets = thumbnailPosition.width() * thumbnailScale;
heightWithInsets = thumbnailPosition.height() * thumbnailScale;
}
mClippedInsets.left *= thumbnailScale;
mClippedInsets.top *= thumbnailScale;
if (dp.isMultiWindowMode) {
mClippedInsets.right = splitScreenInsets.right * scale * thumbnailScale;
mClippedInsets.bottom = splitScreenInsets.bottom * scale * thumbnailScale;
} else {
mClippedInsets.right = Math.max(0,
widthWithInsets - mClippedInsets.left - canvasWidth);
mClippedInsets.bottom = Math.max(0,
heightWithInsets - mClippedInsets.top - canvasHeight);
}
mMatrix.postScale(thumbnailScale, thumbnailScale);
float bitmapHeight = Math.max(0,
(isOrientationDifferent ? thumbnailWidth : thumbnailHeight) * thumbnailScale);
if (Math.round(bitmapHeight) < canvasHeight) {
mClipBottom = bitmapHeight;
}
mIsOrientationChanged = isOrientationDifferent;
}
private Rect getBoundedInsets(Rect activityInsets, Rect insets) {
return new Rect(Math.min(insets.left, activityInsets.left),
Math.min(insets.top, activityInsets.top),
Math.min(insets.right, activityInsets.right),
Math.min(insets.bottom, activityInsets.bottom));
}
private int getRotationDelta(int oldRotation, int newRotation) {
int delta = newRotation - oldRotation;
if (delta < 0) delta += 4;
return delta;
}
/**
* @param deltaRotation the number of 90 degree turns from the current orientation
* @return {@code true} if the change in rotation results in a shift from landscape to
* portrait or vice versa, {@code false} otherwise
*/
private boolean isOrientationChange(int deltaRotation) {
return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
}
private void setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale,
Rect thumbnailPosition) {
int newLeftInset = 0;
int newTopInset = 0;
int translateX = 0;
int translateY = 0;
mMatrix.setRotate(90 * deltaRotate);
switch (deltaRotate) { /* Counter-clockwise */
case Surface.ROTATION_90:
newLeftInset = thumbnailInsets.bottom;
newTopInset = thumbnailInsets.left;
translateX = thumbnailPosition.height();
break;
case Surface.ROTATION_270:
newLeftInset = thumbnailInsets.top;
newTopInset = thumbnailInsets.right;
translateY = thumbnailPosition.width();
break;
case Surface.ROTATION_180:
newLeftInset = -thumbnailInsets.top;
newTopInset = -thumbnailInsets.left;
translateX = thumbnailPosition.width();
translateY = thumbnailPosition.height();
break;
}
mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale);
mMatrix.postTranslate(translateX - mClippedInsets.left,
translateY - mClippedInsets.top);
}
/**
* Insets to used for clipping the thumbnail (in case it is drawing outside its own space)
*/
public RectF getInsetsToDrawInFullscreen() {
return mClippedInsets;
}
}
}