1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.quickstep.views;
18 
19 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
20 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
21 
22 import android.content.Context;
23 import android.content.res.Configuration;
24 import android.graphics.Bitmap;
25 import android.graphics.BitmapShader;
26 import android.graphics.Canvas;
27 import android.graphics.Color;
28 import android.graphics.ColorFilter;
29 import android.graphics.ColorMatrix;
30 import android.graphics.ColorMatrixColorFilter;
31 import android.graphics.Matrix;
32 import android.graphics.Paint;
33 import android.graphics.PorterDuff;
34 import android.graphics.PorterDuffXfermode;
35 import android.graphics.Rect;
36 import android.graphics.RectF;
37 import android.graphics.Shader;
38 import android.util.AttributeSet;
39 import android.util.FloatProperty;
40 import android.util.Property;
41 import android.view.View;
42 import android.view.ViewGroup;
43 
44 import com.android.launcher3.BaseActivity;
45 import com.android.launcher3.DeviceProfile;
46 import com.android.launcher3.R;
47 import com.android.launcher3.Utilities;
48 import com.android.launcher3.config.FeatureFlags;
49 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
50 import com.android.launcher3.util.SystemUiController;
51 import com.android.launcher3.util.Themes;
52 import com.android.quickstep.TaskOverlayFactory;
53 import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
54 import com.android.quickstep.util.TaskCornerRadius;
55 import com.android.systemui.plugins.OverviewScreenshotActions;
56 import com.android.systemui.plugins.PluginListener;
57 import com.android.systemui.shared.recents.model.Task;
58 import com.android.systemui.shared.recents.model.ThumbnailData;
59 
60 /**
61  * A task in the Recents view.
62  */
63 public class TaskThumbnailView extends View implements PluginListener<OverviewScreenshotActions> {
64 
65     private final static ColorMatrix COLOR_MATRIX = new ColorMatrix();
66     private final static ColorMatrix SATURATION_COLOR_MATRIX = new ColorMatrix();
67     private final static RectF EMPTY_RECT_F = new RectF();
68 
69     public static final Property<TaskThumbnailView, Float> DIM_ALPHA =
70             new FloatProperty<TaskThumbnailView>("dimAlpha") {
71                 @Override
72                 public void setValue(TaskThumbnailView thumbnail, float dimAlpha) {
73                     thumbnail.setDimAlpha(dimAlpha);
74                 }
75 
76                 @Override
77                 public Float get(TaskThumbnailView thumbnailView) {
78                     return thumbnailView.mDimAlpha;
79                 }
80             };
81 
82     private final BaseActivity mActivity;
83     private final TaskOverlay mOverlay;
84     private final boolean mIsDarkTextTheme;
85     private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
86     private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
87     private final Paint mClearPaint = new Paint();
88     private final Paint mDimmingPaintAfterClearing = new Paint();
89 
90     private final Matrix mMatrix = new Matrix();
91 
92     private float mClipBottom = -1;
93     // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
94     private RectF mClippedInsets = new RectF();
95     private TaskView.FullscreenDrawParams mFullscreenParams;
96 
97     private Task mTask;
98     private ThumbnailData mThumbnailData;
99     protected BitmapShader mBitmapShader;
100 
101     private float mDimAlpha = 1f;
102     private float mDimAlphaMultiplier = 1f;
103     private float mSaturation = 1f;
104 
105     private boolean mOverlayEnabled;
106     private boolean mRotated;
107     private OverviewScreenshotActions mOverviewScreenshotActionsPlugin;
108 
TaskThumbnailView(Context context)109     public TaskThumbnailView(Context context) {
110         this(context, null);
111     }
112 
TaskThumbnailView(Context context, AttributeSet attrs)113     public TaskThumbnailView(Context context, AttributeSet attrs) {
114         this(context, attrs, 0);
115     }
116 
TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr)117     public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) {
118         super(context, attrs, defStyleAttr);
119         mOverlay = TaskOverlayFactory.INSTANCE.get(context).createOverlay(this);
120         mPaint.setFilterBitmap(true);
121         mBackgroundPaint.setColor(Color.WHITE);
122         mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
123         mDimmingPaintAfterClearing.setColor(Color.BLACK);
124         mActivity = BaseActivity.fromContext(context);
125         mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText);
126         mFullscreenParams = new TaskView.FullscreenDrawParams(TaskCornerRadius.get(context));
127     }
128 
bind(Task task)129     public void bind(Task task) {
130         mOverlay.reset();
131         mTask = task;
132         int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000;
133         mPaint.setColor(color);
134         mBackgroundPaint.setColor(color);
135     }
136 
137     /**
138      * Updates this thumbnail.
139      */
setThumbnail(Task task, ThumbnailData thumbnailData)140     public void setThumbnail(Task task, ThumbnailData thumbnailData) {
141         mTask = task;
142         if (thumbnailData != null && thumbnailData.thumbnail != null) {
143             Bitmap bm = thumbnailData.thumbnail;
144             bm.prepareToDraw();
145             mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
146             mPaint.setShader(mBitmapShader);
147             mThumbnailData = thumbnailData;
148             updateThumbnailMatrix();
149         } else {
150             mBitmapShader = null;
151             mThumbnailData = null;
152             mPaint.setShader(null);
153             mOverlay.reset();
154         }
155 
156         if (mOverviewScreenshotActionsPlugin != null) {
157             mOverviewScreenshotActionsPlugin
158                 .setupActions((ViewGroup) getTaskView(), getThumbnail(), mActivity);
159         }
160         updateThumbnailPaintFilter();
161     }
162 
setDimAlphaMultipler(float dimAlphaMultipler)163     public void setDimAlphaMultipler(float dimAlphaMultipler) {
164         mDimAlphaMultiplier = dimAlphaMultipler;
165         setDimAlpha(mDimAlpha);
166     }
167 
168     /**
169      * Sets the alpha of the dim layer on top of this view.
170      * <p>
171      * If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be black.
172      */
setDimAlpha(float dimAlpha)173     public void setDimAlpha(float dimAlpha) {
174         mDimAlpha = dimAlpha;
175         updateThumbnailPaintFilter();
176     }
177 
setSaturation(float saturation)178     public void setSaturation(float saturation) {
179         mSaturation = saturation;
180         updateThumbnailPaintFilter();
181     }
182 
getDimAlpha()183     public float getDimAlpha() {
184         return mDimAlpha;
185     }
186 
getInsets(Rect fallback)187     public Rect getInsets(Rect fallback) {
188         if (mThumbnailData != null) {
189             return mThumbnailData.insets;
190         }
191         return fallback;
192     }
193 
getSysUiStatusNavFlags()194     public int getSysUiStatusNavFlags() {
195         if (mThumbnailData != null) {
196             int flags = 0;
197             flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0
198                     ? SystemUiController.FLAG_LIGHT_STATUS
199                     : SystemUiController.FLAG_DARK_STATUS;
200             flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0
201                     ? SystemUiController.FLAG_LIGHT_NAV
202                     : SystemUiController.FLAG_DARK_NAV;
203             return flags;
204         }
205         return 0;
206     }
207 
208     @Override
onDraw(Canvas canvas)209     protected void onDraw(Canvas canvas) {
210         RectF currentDrawnInsets = mFullscreenParams.mCurrentDrawnInsets;
211         canvas.save();
212         canvas.translate(currentDrawnInsets.left, currentDrawnInsets.top);
213         canvas.scale(mFullscreenParams.mScale, mFullscreenParams.mScale);
214         // Draw the insets if we're being drawn fullscreen (we do this for quick switch).
215         drawOnCanvas(canvas,
216                 -currentDrawnInsets.left,
217                 -currentDrawnInsets.top,
218                 getMeasuredWidth() + currentDrawnInsets.right,
219                 getMeasuredHeight() + currentDrawnInsets.bottom,
220                 mFullscreenParams.mCurrentDrawnCornerRadius);
221         canvas.restore();
222     }
223 
224     @Override
onPluginConnected(OverviewScreenshotActions overviewScreenshotActions, Context context)225     public void onPluginConnected(OverviewScreenshotActions overviewScreenshotActions,
226             Context context) {
227         mOverviewScreenshotActionsPlugin = overviewScreenshotActions;
228         mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
229     }
230 
231     @Override
onPluginDisconnected(OverviewScreenshotActions plugin)232     public void onPluginDisconnected(OverviewScreenshotActions plugin) {
233         if (mOverviewScreenshotActionsPlugin != null) {
234             mOverviewScreenshotActionsPlugin = null;
235         }
236     }
237 
238     @Override
onAttachedToWindow()239     protected void onAttachedToWindow() {
240         super.onAttachedToWindow();
241         PluginManagerWrapper.INSTANCE.get(getContext())
242             .addPluginListener(this, OverviewScreenshotActions.class);
243     }
244 
245     @Override
onDetachedFromWindow()246     protected void onDetachedFromWindow() {
247         super.onDetachedFromWindow();
248         PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this);
249     }
250 
getInsetsToDrawInFullscreen(boolean isMultiWindowMode)251     public RectF getInsetsToDrawInFullscreen(boolean isMultiWindowMode) {
252         // Don't show insets in multi window mode.
253         return isMultiWindowMode ? EMPTY_RECT_F : mClippedInsets;
254     }
255 
setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams)256     public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) {
257         mFullscreenParams = fullscreenParams;
258         invalidate();
259     }
260 
drawOnCanvas(Canvas canvas, float x, float y, float width, float height, float cornerRadius)261     public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height,
262             float cornerRadius) {
263         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
264             if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) {
265                 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint);
266                 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius,
267                         mDimmingPaintAfterClearing);
268                 return;
269             }
270         }
271 
272         // Draw the background in all cases, except when the thumbnail data is opaque
273         final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null
274                 || mThumbnailData == null;
275         if (drawBackgroundOnly || mClipBottom > 0 || mThumbnailData.isTranslucent) {
276             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint);
277             if (drawBackgroundOnly) {
278                 return;
279             }
280         }
281 
282         if (mClipBottom > 0) {
283             canvas.save();
284             canvas.clipRect(x, y, width, mClipBottom);
285             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
286             canvas.restore();
287         } else {
288             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
289         }
290     }
291 
getTaskView()292     public TaskView getTaskView() {
293         return (TaskView) getParent();
294     }
295 
setOverlayEnabled(boolean overlayEnabled)296     public void setOverlayEnabled(boolean overlayEnabled) {
297         if (mOverlayEnabled != overlayEnabled) {
298             mOverlayEnabled = overlayEnabled;
299             updateOverlay();
300         }
301     }
302 
updateOverlay()303     private void updateOverlay() {
304         // The overlay doesn't really work when the screenshot is rotated, so don't add it.
305         if (mOverlayEnabled && !mRotated && mBitmapShader != null && mThumbnailData != null) {
306             mOverlay.initOverlay(mTask, mThumbnailData, mMatrix);
307         } else {
308             mOverlay.reset();
309         }
310     }
311 
updateThumbnailPaintFilter()312     private void updateThumbnailPaintFilter() {
313         int mul = (int) ((1 - mDimAlpha * mDimAlphaMultiplier) * 255);
314         ColorFilter filter = getColorFilter(mul, mIsDarkTextTheme, mSaturation);
315         mBackgroundPaint.setColorFilter(filter);
316         mDimmingPaintAfterClearing.setAlpha(255 - mul);
317         if (mBitmapShader != null) {
318             mPaint.setColorFilter(filter);
319         } else {
320             mPaint.setColorFilter(null);
321             mPaint.setColor(Color.argb(255, mul, mul, mul));
322         }
323         invalidate();
324     }
325 
updateThumbnailMatrix()326     private void updateThumbnailMatrix() {
327         boolean isRotated = false;
328         mClipBottom = -1;
329         if (mBitmapShader != null && mThumbnailData != null) {
330             float scale = mThumbnailData.scale;
331             Rect thumbnailInsets = mThumbnailData.insets;
332             final float thumbnailWidth = mThumbnailData.thumbnail.getWidth() -
333                     (thumbnailInsets.left + thumbnailInsets.right) * scale;
334             final float thumbnailHeight = mThumbnailData.thumbnail.getHeight() -
335                     (thumbnailInsets.top + thumbnailInsets.bottom) * scale;
336 
337             final float thumbnailScale;
338             final DeviceProfile profile = mActivity.getDeviceProfile();
339 
340             if (getMeasuredWidth() == 0) {
341                 // If we haven't measured , skip the thumbnail drawing and only draw the background
342                 // color
343                 thumbnailScale = 0f;
344             } else {
345                 final Configuration configuration =
346                         getContext().getResources().getConfiguration();
347                 // Rotate the screenshot if not in multi-window mode
348                 isRotated = FeatureFlags.OVERVIEW_USE_SCREENSHOT_ORIENTATION &&
349                         configuration.orientation != mThumbnailData.orientation &&
350                         !mActivity.isInMultiWindowMode() &&
351                         mThumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
352                 // Scale the screenshot to always fit the width of the card.
353                 thumbnailScale = isRotated
354                         ? getMeasuredWidth() / thumbnailHeight
355                         : getMeasuredWidth() / thumbnailWidth;
356             }
357 
358             if (isRotated) {
359                 int rotationDir = profile.isVerticalBarLayout() && !profile.isSeascape() ? -1 : 1;
360                 mMatrix.setRotate(90 * rotationDir);
361                 int newLeftInset = rotationDir == 1 ? thumbnailInsets.bottom : thumbnailInsets.top;
362                 int newTopInset = rotationDir == 1 ? thumbnailInsets.left : thumbnailInsets.right;
363                 mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale);
364                 if (rotationDir == -1) {
365                     // Crop the right/bottom side of the screenshot rather than left/top
366                     float excessHeight = thumbnailWidth * thumbnailScale - getMeasuredHeight();
367                     mClippedInsets.offset(0, excessHeight);
368                 }
369                 mMatrix.postTranslate(-mClippedInsets.left, -mClippedInsets.top);
370                 // Move the screenshot to the thumbnail window (rotation moved it out).
371                 if (rotationDir == 1) {
372                     mMatrix.postTranslate(mThumbnailData.thumbnail.getHeight(), 0);
373                 } else {
374                     mMatrix.postTranslate(0, mThumbnailData.thumbnail.getWidth());
375                 }
376             } else {
377                 mClippedInsets.offsetTo(thumbnailInsets.left * scale, thumbnailInsets.top * scale);
378                 mMatrix.setTranslate(-mClippedInsets.left, -mClippedInsets.top);
379             }
380 
381             final float widthWithInsets;
382             final float heightWithInsets;
383             if (isRotated) {
384                 widthWithInsets = mThumbnailData.thumbnail.getHeight() * thumbnailScale;
385                 heightWithInsets = mThumbnailData.thumbnail.getWidth() * thumbnailScale;
386             } else {
387                 widthWithInsets = mThumbnailData.thumbnail.getWidth() * thumbnailScale;
388                 heightWithInsets = mThumbnailData.thumbnail.getHeight() * thumbnailScale;
389             }
390             mClippedInsets.left *= thumbnailScale;
391             mClippedInsets.top *= thumbnailScale;
392             mClippedInsets.right = widthWithInsets - mClippedInsets.left - getMeasuredWidth();
393             mClippedInsets.bottom = heightWithInsets - mClippedInsets.top - getMeasuredHeight();
394 
395             mMatrix.postScale(thumbnailScale, thumbnailScale);
396             mBitmapShader.setLocalMatrix(mMatrix);
397 
398             float bitmapHeight = Math.max((isRotated ? thumbnailWidth : thumbnailHeight)
399                     * thumbnailScale, 0);
400             if (Math.round(bitmapHeight) < getMeasuredHeight()) {
401                 mClipBottom = bitmapHeight;
402             }
403             mPaint.setShader(mBitmapShader);
404         }
405 
406         mRotated = isRotated;
407         invalidate();
408 
409         // Update can be called from {@link #onSizeChanged} during layout, post handling of overlay
410         // as overlay could modify the views in the overlay as a side effect of its update.
411         post(this::updateOverlay);
412     }
413 
414     @Override
onSizeChanged(int w, int h, int oldw, int oldh)415     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
416         super.onSizeChanged(w, h, oldw, oldh);
417         updateThumbnailMatrix();
418     }
419 
420     /**
421      * @param intensity multiplier for color values. 0 - make black (white if shouldLighten), 255 -
422      *                  leave unchanged.
423      */
getColorFilter(int intensity, boolean shouldLighten, float saturation)424     private static ColorFilter getColorFilter(int intensity, boolean shouldLighten,
425             float saturation) {
426         intensity = Utilities.boundToRange(intensity, 0, 255);
427 
428         if (intensity == 255 && saturation == 1) {
429             return null;
430         }
431 
432         final float intensityScale = intensity / 255f;
433         COLOR_MATRIX.setScale(intensityScale, intensityScale, intensityScale, 1);
434 
435         if (saturation != 1) {
436             SATURATION_COLOR_MATRIX.setSaturation(saturation);
437             COLOR_MATRIX.postConcat(SATURATION_COLOR_MATRIX);
438         }
439 
440         if (shouldLighten) {
441             final float[] colorArray = COLOR_MATRIX.getArray();
442             final int colorAdd = 255 - intensity;
443             colorArray[4] = colorAdd;
444             colorArray[9] = colorAdd;
445             colorArray[14] = colorAdd;
446         }
447 
448         return new ColorMatrixColorFilter(COLOR_MATRIX);
449     }
450 
getThumbnail()451     public Bitmap getThumbnail() {
452         if (mThumbnailData == null) {
453             return null;
454         }
455         return mThumbnailData.thumbnail;
456     }
457 }
458