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 android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
20 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
21 
22 import static com.android.launcher3.Utilities.comp;
23 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
24 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
25 
26 import android.content.Context;
27 import android.graphics.Bitmap;
28 import android.graphics.BitmapShader;
29 import android.graphics.Canvas;
30 import android.graphics.Color;
31 import android.graphics.ColorFilter;
32 import android.graphics.Insets;
33 import android.graphics.Matrix;
34 import android.graphics.Paint;
35 import android.graphics.PorterDuff;
36 import android.graphics.PorterDuffXfermode;
37 import android.graphics.Rect;
38 import android.graphics.RectF;
39 import android.graphics.Shader;
40 import android.os.Build;
41 import android.util.AttributeSet;
42 import android.util.FloatProperty;
43 import android.util.Property;
44 import android.view.Surface;
45 import android.view.View;
46 
47 import androidx.annotation.Nullable;
48 import androidx.annotation.RequiresApi;
49 import androidx.core.graphics.ColorUtils;
50 
51 import com.android.launcher3.BaseActivity;
52 import com.android.launcher3.DeviceProfile;
53 import com.android.launcher3.Utilities;
54 import com.android.launcher3.util.MainThreadInitializedObject;
55 import com.android.launcher3.util.SystemUiController;
56 import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
57 import com.android.quickstep.views.TaskView.FullscreenDrawParams;
58 import com.android.systemui.shared.recents.model.Task;
59 import com.android.systemui.shared.recents.model.ThumbnailData;
60 
61 /**
62  * A task in the Recents view.
63  */
64 public class TaskThumbnailView extends View {
65     private static final MainThreadInitializedObject<FullscreenDrawParams> TEMP_PARAMS =
66             new MainThreadInitializedObject<>(FullscreenDrawParams::new);
67 
68     public static final Property<TaskThumbnailView, Float> DIM_ALPHA =
69             new FloatProperty<TaskThumbnailView>("dimAlpha") {
70                 @Override
71                 public void setValue(TaskThumbnailView thumbnail, float dimAlpha) {
72                     thumbnail.setDimAlpha(dimAlpha);
73                 }
74 
75                 @Override
76                 public Float get(TaskThumbnailView thumbnailView) {
77                     return thumbnailView.mDimAlpha;
78                 }
79             };
80 
81     private final BaseActivity mActivity;
82     @Nullable
83     private TaskOverlay mOverlay;
84     private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
85     private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
86     private final Paint mClearPaint = new Paint();
87     private final Paint mDimmingPaintAfterClearing = new Paint();
88     private final int mDimColor;
89 
90     // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
91     private final Rect mPreviewRect = new Rect();
92     private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper();
93     private TaskView.FullscreenDrawParams mFullscreenParams;
94 
95     @Nullable
96     private Task mTask;
97     @Nullable
98     private ThumbnailData mThumbnailData;
99     @Nullable
100     protected BitmapShader mBitmapShader;
101 
102     /** How much this thumbnail is dimmed, 0 not dimmed at all, 1 totally dimmed. */
103     private float mDimAlpha = 0f;
104 
105     private boolean mOverlayEnabled;
106 
TaskThumbnailView(Context context)107     public TaskThumbnailView(Context context) {
108         this(context, null);
109     }
110 
TaskThumbnailView(Context context, @Nullable AttributeSet attrs)111     public TaskThumbnailView(Context context, @Nullable AttributeSet attrs) {
112         this(context, attrs, 0);
113     }
114 
TaskThumbnailView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)115     public TaskThumbnailView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
116         super(context, attrs, defStyleAttr);
117         mPaint.setFilterBitmap(true);
118         mBackgroundPaint.setColor(Color.WHITE);
119         mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
120         mActivity = BaseActivity.fromContext(context);
121         // Initialize with placeholder value. It is overridden later by TaskView
122         mFullscreenParams = TEMP_PARAMS.get(context);
123 
124         mDimColor = RecentsView.getForegroundScrimDimColor(context);
125         mDimmingPaintAfterClearing.setColor(mDimColor);
126     }
127 
128     /**
129      * Updates the thumbnail to draw the provided task
130      * @param task
131      */
bind(Task task)132     public void bind(Task task) {
133         getTaskOverlay().reset();
134         mTask = task;
135         int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000;
136         mPaint.setColor(color);
137         mBackgroundPaint.setColor(color);
138     }
139 
140     /**
141      * Updates the thumbnail.
142      * @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately.
143      *                   In most cases, we use the {@link #setThumbnail(Task, ThumbnailData)}
144      *                   version with {@code refreshNow} is true. The only exception is
145      *                   in the live tile case that we grab a screenshot when user enters Overview
146      *                   upon swipe up so that a usable screenshot is accessible immediately when
147      *                   recents animation needs to be finished / cancelled.
148      */
setThumbnail(@ullable Task task, @Nullable ThumbnailData thumbnailData, boolean refreshNow)149     public void setThumbnail(@Nullable Task task, @Nullable ThumbnailData thumbnailData,
150             boolean refreshNow) {
151         mTask = task;
152         boolean thumbnailWasNull = mThumbnailData == null;
153         mThumbnailData =
154                 (thumbnailData != null && thumbnailData.thumbnail != null) ? thumbnailData : null;
155         if (refreshNow) {
156             refresh(thumbnailWasNull && mThumbnailData != null);
157         }
158     }
159 
160     /** See {@link #setThumbnail(Task, ThumbnailData, boolean)} */
setThumbnail(@ullable Task task, @Nullable ThumbnailData thumbnailData)161     public void setThumbnail(@Nullable Task task, @Nullable ThumbnailData thumbnailData) {
162         setThumbnail(task, thumbnailData, true /* refreshNow */);
163     }
164 
165     /** Updates the shader, paint, matrix to redraw. */
refresh()166     public void refresh() {
167         refresh(false);
168     }
169 
170     /**
171      * Updates the shader, paint, matrix to redraw.
172      * @param shouldRefreshOverlay whether to re-initialize overlay
173      */
refresh(boolean shouldRefreshOverlay)174     private void refresh(boolean shouldRefreshOverlay) {
175         if (mThumbnailData != null && mThumbnailData.thumbnail != null) {
176             Bitmap bm = mThumbnailData.thumbnail;
177             bm.prepareToDraw();
178             mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
179             mPaint.setShader(mBitmapShader);
180             updateThumbnailMatrix();
181             if (shouldRefreshOverlay) {
182                 refreshOverlay();
183             }
184         } else {
185             mBitmapShader = null;
186             mThumbnailData = null;
187             mPaint.setShader(null);
188             getTaskOverlay().reset();
189         }
190         updateThumbnailPaintFilter();
191     }
192 
193     /**
194      * Sets the alpha of the dim layer on top of this view.
195      * <p>
196      * If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be the
197      * extracted background color.
198      *
199      */
setDimAlpha(float dimAlpha)200     public void setDimAlpha(float dimAlpha) {
201         mDimAlpha = dimAlpha;
202         updateThumbnailPaintFilter();
203     }
204 
getTaskOverlay()205     public TaskOverlay getTaskOverlay() {
206         if (mOverlay == null) {
207             mOverlay = getTaskView().getRecentsView().getTaskOverlayFactory().createOverlay(this);
208         }
209         return mOverlay;
210     }
211 
getDimAlpha()212     public float getDimAlpha() {
213         return mDimAlpha;
214     }
215 
216     /**
217      * Get the scaled insets that are being used to draw the task view. This is a subsection of
218      * the full snapshot.
219      * @return the insets in snapshot bitmap coordinates.
220      */
221     @RequiresApi(api = Build.VERSION_CODES.Q)
getScaledInsets()222     public Insets getScaledInsets() {
223         if (mThumbnailData == null) {
224             return Insets.NONE;
225         }
226 
227         RectF bitmapRect = new RectF(
228                 0, 0,
229                 mThumbnailData.thumbnail.getWidth(), mThumbnailData.thumbnail.getHeight());
230         RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
231 
232         // The position helper matrix tells us how to transform the bitmap to fit the view, the
233         // inverse tells us where the view would be in the bitmaps coordinates. The insets are the
234         // difference between the bitmap bounds and the projected view bounds.
235         Matrix boundsToBitmapSpace = new Matrix();
236         mPreviewPositionHelper.getMatrix().invert(boundsToBitmapSpace);
237         RectF boundsInBitmapSpace = new RectF();
238         boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);
239 
240         DeviceProfile dp = mActivity.getDeviceProfile();
241         int leftInset = TaskView.clipLeft(dp) ? Math.round(boundsInBitmapSpace.left) : 0;
242         int topInset = TaskView.clipTop(dp) ? Math.round(boundsInBitmapSpace.top) : 0;
243         int rightInset = TaskView.clipRight(dp) ? Math.round(
244                 bitmapRect.right - boundsInBitmapSpace.right) : 0;
245         int bottomInset = TaskView.clipBottom(dp)
246                 ? Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom) : 0;
247         return Insets.of(leftInset, topInset, rightInset, bottomInset);
248     }
249 
250 
getSysUiStatusNavFlags()251     public int getSysUiStatusNavFlags() {
252         if (mThumbnailData != null) {
253             int flags = 0;
254             flags |= (mThumbnailData.appearance & APPEARANCE_LIGHT_STATUS_BARS) != 0
255                     ? SystemUiController.FLAG_LIGHT_STATUS
256                     : SystemUiController.FLAG_DARK_STATUS;
257             flags |= (mThumbnailData.appearance & APPEARANCE_LIGHT_NAVIGATION_BARS) != 0
258                     ? SystemUiController.FLAG_LIGHT_NAV
259                     : SystemUiController.FLAG_DARK_NAV;
260             return flags;
261         }
262         return 0;
263     }
264 
265     @Override
onDraw(Canvas canvas)266     protected void onDraw(Canvas canvas) {
267         RectF currentDrawnInsets = mFullscreenParams.mCurrentDrawnInsets;
268         canvas.save();
269         canvas.scale(mFullscreenParams.mScale, mFullscreenParams.mScale);
270         canvas.translate(currentDrawnInsets.left, currentDrawnInsets.top);
271         // Draw the insets if we're being drawn fullscreen (we do this for quick switch).
272         drawOnCanvas(canvas,
273                 -currentDrawnInsets.left,
274                 -currentDrawnInsets.top,
275                 getMeasuredWidth() + currentDrawnInsets.right,
276                 getMeasuredHeight() + currentDrawnInsets.bottom,
277                 mFullscreenParams.mCurrentDrawnCornerRadius);
278         canvas.restore();
279     }
280 
getPreviewPositionHelper()281     public PreviewPositionHelper getPreviewPositionHelper() {
282         return mPreviewPositionHelper;
283     }
284 
setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams)285     public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) {
286         mFullscreenParams = fullscreenParams;
287         invalidate();
288     }
289 
drawOnCanvas(Canvas canvas, float x, float y, float width, float height, float cornerRadius)290     public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height,
291             float cornerRadius) {
292         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
293             if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) {
294                 // TODO(b/189265196): Temporary fix to align the surface with the cutout perfectly.
295                 // Round up only when the live tile task is displayed in Overview.
296                 float rounding = comp(mFullscreenParams.mFullscreenProgress);
297                 float left = x + rounding / 2;
298                 float top = y + rounding / 2;
299                 float right = width - rounding;
300                 float bottom = height - rounding;
301 
302                 canvas.drawRoundRect(left, top, right, bottom, cornerRadius, cornerRadius,
303                         mClearPaint);
304                 canvas.drawRoundRect(left, top, right, bottom, cornerRadius, cornerRadius,
305                         mDimmingPaintAfterClearing);
306                 return;
307             }
308         }
309 
310         // Always draw the background since the snapshots might be translucent or partially empty
311         // (For example, tasks been reparented out of dismissing split root when drag-to-dismiss
312         // split screen).
313         canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint);
314 
315         final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null
316                 || mThumbnailData == null;
317         if (drawBackgroundOnly) {
318             return;
319         }
320 
321         canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
322     }
323 
getTaskView()324     public TaskView getTaskView() {
325         return (TaskView) getParent();
326     }
327 
setOverlayEnabled(boolean overlayEnabled)328     public void setOverlayEnabled(boolean overlayEnabled) {
329         if (mOverlayEnabled != overlayEnabled) {
330             mOverlayEnabled = overlayEnabled;
331 
332             refreshOverlay();
333         }
334     }
335 
336     /**
337      * Potentially re-init the task overlay. Be cautious when calling this as the overlay may
338      * do processing on initialization.
339      */
refreshOverlay()340     private void refreshOverlay() {
341         if (mOverlayEnabled) {
342             getTaskOverlay().initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix,
343                     mPreviewPositionHelper.mIsOrientationChanged);
344         } else {
345             getTaskOverlay().reset();
346         }
347     }
348 
updateThumbnailPaintFilter()349     private void updateThumbnailPaintFilter() {
350         ColorFilter filter = getColorFilter(mDimAlpha);
351         mBackgroundPaint.setColorFilter(filter);
352         int alpha = (int) (mDimAlpha * 255);
353         mDimmingPaintAfterClearing.setAlpha(alpha);
354         if (mBitmapShader != null) {
355             mPaint.setColorFilter(filter);
356         } else {
357             mPaint.setColorFilter(null);
358             mPaint.setColor(ColorUtils.blendARGB(Color.BLACK, mDimColor, alpha));
359         }
360         invalidate();
361     }
362 
updateThumbnailMatrix()363     private void updateThumbnailMatrix() {
364         mPreviewPositionHelper.mIsOrientationChanged = false;
365         if (mBitmapShader != null && mThumbnailData != null) {
366             mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
367                     mThumbnailData.thumbnail.getHeight());
368             int currentRotation = getTaskView().getRecentsView().getPagedViewOrientedState()
369                     .getRecentsActivityRotation();
370             boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
371             mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
372                     getMeasuredWidth(), getMeasuredHeight(), mActivity.getDeviceProfile(),
373                     currentRotation, isRtl);
374 
375             mBitmapShader.setLocalMatrix(mPreviewPositionHelper.mMatrix);
376             mPaint.setShader(mBitmapShader);
377         }
378         getTaskView().updateCurrentFullscreenParams(mPreviewPositionHelper);
379         invalidate();
380     }
381 
382     @Override
onSizeChanged(int w, int h, int oldw, int oldh)383     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
384         super.onSizeChanged(w, h, oldw, oldh);
385         updateThumbnailMatrix();
386 
387         refreshOverlay();
388     }
389 
getColorFilter(float dimAmount)390     private ColorFilter getColorFilter(float dimAmount) {
391         return Utilities.makeColorTintingColorFilter(mDimColor, dimAmount);
392     }
393 
394     /**
395      * Returns current thumbnail or null if none is set.
396      */
397     @Nullable
getThumbnail()398     public Bitmap getThumbnail() {
399         if (mThumbnailData == null) {
400             return null;
401         }
402         return mThumbnailData.thumbnail;
403     }
404 
405     /**
406      * Returns whether the snapshot is real. If the device is locked for the user of the task,
407      * the snapshot used will be an app-theme generated snapshot instead of a real snapshot.
408      */
isRealSnapshot()409     public boolean isRealSnapshot() {
410         if (mThumbnailData == null) {
411             return false;
412         }
413         return mThumbnailData.isRealSnapshot && !mTask.isLocked;
414     }
415 
416     /**
417      * Utility class to position the thumbnail in the TaskView
418      */
419     public static class PreviewPositionHelper {
420 
421         private static final RectF EMPTY_RECT_F = new RectF();
422 
423         // Contains the portion of the thumbnail that is unclipped when fullscreen progress = 1.
424         private final RectF mClippedInsets = new RectF();
425         private final Matrix mMatrix = new Matrix();
426         private boolean mIsOrientationChanged;
427 
getMatrix()428         public Matrix getMatrix() {
429             return mMatrix;
430         }
431 
432         /**
433          * Updates the matrix based on the provided parameters
434          */
updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData, int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation, boolean isRtl)435         public void updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData,
436                 int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation,
437                 boolean isRtl) {
438             boolean isRotated = false;
439             boolean isOrientationDifferent;
440 
441             int thumbnailRotation = thumbnailData.rotation;
442             int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
443             RectF thumbnailClipHint = new RectF();
444             if (TaskView.clipLeft(dp)) {
445                 thumbnailClipHint.left = thumbnailData.insets.left;
446             }
447             if (TaskView.clipRight(dp)) {
448                 thumbnailClipHint.right = thumbnailData.insets.right;
449             }
450             if (TaskView.clipTop(dp)) {
451                 thumbnailClipHint.top = thumbnailData.insets.top;
452             }
453             if (TaskView.clipBottom(dp)) {
454                 thumbnailClipHint.bottom = thumbnailData.insets.bottom;
455             }
456 
457             float scale = thumbnailData.scale;
458             final float thumbnailScale;
459 
460             // Landscape vs portrait change.
461             // Note: Disable rotation in grid layout.
462             boolean windowingModeSupportsRotation = !dp.isMultiWindowMode
463                     && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN
464                     && !dp.overviewShowAsGrid;
465             isOrientationDifferent = isOrientationChange(deltaRotate)
466                     && windowingModeSupportsRotation;
467             if (canvasWidth == 0 || canvasHeight == 0 || scale == 0) {
468                 // If we haven't measured , skip the thumbnail drawing and only draw the background
469                 // color
470                 thumbnailScale = 0f;
471             } else {
472                 // Rotate the screenshot if not in multi-window mode
473                 isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
474 
475                 float surfaceWidth = thumbnailBounds.width() / scale;
476                 float surfaceHeight = thumbnailBounds.height() / scale;
477                 float availableWidth = surfaceWidth
478                         - (thumbnailClipHint.left + thumbnailClipHint.right);
479                 float availableHeight = surfaceHeight
480                         - (thumbnailClipHint.top + thumbnailClipHint.bottom);
481 
482                 float canvasAspect = canvasWidth / (float) canvasHeight;
483                 float availableAspect = isRotated
484                         ? availableHeight / availableWidth
485                         : availableWidth / availableHeight;
486                 boolean isAspectLargelyDifferent = Utilities.isRelativePercentDifferenceGreaterThan(
487                         canvasAspect, availableAspect, 0.1f);
488                 if (isRotated && isAspectLargelyDifferent) {
489                     // Do not rotate thumbnail if it would not improve fit
490                     isRotated = false;
491                     isOrientationDifferent = false;
492                 }
493 
494                 if (isAspectLargelyDifferent) {
495                     // Crop letterbox insets if insets isn't already clipped
496                     if (!TaskView.clipLeft(dp)) {
497                         thumbnailClipHint.left = thumbnailData.letterboxInsets.left;
498                     }
499                     if (!TaskView.clipRight(dp)) {
500                         thumbnailClipHint.right = thumbnailData.letterboxInsets.right;
501                     }
502                     if (!TaskView.clipTop(dp)) {
503                         thumbnailClipHint.top = thumbnailData.letterboxInsets.top;
504                     }
505                     if (!TaskView.clipBottom(dp)) {
506                         thumbnailClipHint.bottom = thumbnailData.letterboxInsets.bottom;
507                     }
508                     availableWidth = surfaceWidth
509                             - (thumbnailClipHint.left + thumbnailClipHint.right);
510                     availableHeight = surfaceHeight
511                             - (thumbnailClipHint.top + thumbnailClipHint.bottom);
512                 }
513 
514                 final float targetW, targetH;
515                 if (isOrientationDifferent) {
516                     targetW = canvasHeight;
517                     targetH = canvasWidth;
518                 } else {
519                     targetW = canvasWidth;
520                     targetH = canvasHeight;
521                 }
522                 float targetAspect = targetW / targetH;
523 
524                 // Update the clipHint such that
525                 //   > the final clipped position has same aspect ratio as requested by canvas
526                 //   > first fit the width and crop the extra height
527                 //   > if that will leave empty space, fit the height and crop the width instead
528                 float croppedWidth = availableWidth;
529                 float croppedHeight = croppedWidth / targetAspect;
530                 if (croppedHeight > availableHeight) {
531                     croppedHeight = availableHeight;
532                     if (croppedHeight < targetH) {
533                         croppedHeight = Math.min(targetH, surfaceHeight);
534                     }
535                     croppedWidth = croppedHeight * targetAspect;
536 
537                     // One last check in case the task aspect radio messed up something
538                     if (croppedWidth > surfaceWidth) {
539                         croppedWidth = surfaceWidth;
540                         croppedHeight = croppedWidth / targetAspect;
541                     }
542                 }
543 
544                 // Update the clip hints. Align to 0,0, crop the remaining.
545                 if (isRtl) {
546                     thumbnailClipHint.left += availableWidth - croppedWidth;
547                     if (thumbnailClipHint.right < 0) {
548                         thumbnailClipHint.left += thumbnailClipHint.right;
549                         thumbnailClipHint.right = 0;
550                     }
551                 } else {
552                     thumbnailClipHint.right += availableWidth - croppedWidth;
553                     if (thumbnailClipHint.left < 0) {
554                         thumbnailClipHint.right += thumbnailClipHint.left;
555                         thumbnailClipHint.left = 0;
556                     }
557                 }
558                 thumbnailClipHint.bottom += availableHeight - croppedHeight;
559                 if (thumbnailClipHint.top < 0) {
560                     thumbnailClipHint.bottom += thumbnailClipHint.top;
561                     thumbnailClipHint.top = 0;
562                 } else if (thumbnailClipHint.bottom < 0) {
563                     thumbnailClipHint.top += thumbnailClipHint.bottom;
564                     thumbnailClipHint.bottom = 0;
565                 }
566 
567                 thumbnailScale = targetW / (croppedWidth * scale);
568             }
569 
570             Rect splitScreenInsets = dp.getInsets();
571             if (!isRotated) {
572                 // No Rotation
573                 if (dp.isMultiWindowMode) {
574                     mClippedInsets.offsetTo(splitScreenInsets.left * scale,
575                             splitScreenInsets.top * scale);
576                 } else {
577                     mClippedInsets.offsetTo(thumbnailClipHint.left * scale,
578                             thumbnailClipHint.top * scale);
579                 }
580                 mMatrix.setTranslate(
581                         -thumbnailClipHint.left * scale,
582                         -thumbnailClipHint.top * scale);
583             } else {
584                 setThumbnailRotation(deltaRotate, thumbnailClipHint, scale, thumbnailBounds, dp);
585             }
586 
587             final float widthWithInsets;
588             final float heightWithInsets;
589             if (isOrientationDifferent) {
590                 widthWithInsets = thumbnailBounds.height() * thumbnailScale;
591                 heightWithInsets = thumbnailBounds.width() * thumbnailScale;
592             } else {
593                 widthWithInsets = thumbnailBounds.width() * thumbnailScale;
594                 heightWithInsets = thumbnailBounds.height() * thumbnailScale;
595             }
596             mClippedInsets.left *= thumbnailScale;
597             mClippedInsets.top *= thumbnailScale;
598 
599             if (dp.isMultiWindowMode) {
600                 mClippedInsets.right = splitScreenInsets.right * scale * thumbnailScale;
601                 mClippedInsets.bottom = splitScreenInsets.bottom * scale * thumbnailScale;
602             } else {
603                 mClippedInsets.right = Math.max(0,
604                         widthWithInsets - mClippedInsets.left - canvasWidth);
605                 mClippedInsets.bottom = Math.max(0,
606                         heightWithInsets - mClippedInsets.top - canvasHeight);
607             }
608 
609             mMatrix.postScale(thumbnailScale, thumbnailScale);
610             mIsOrientationChanged = isOrientationDifferent;
611         }
612 
getRotationDelta(int oldRotation, int newRotation)613         private int getRotationDelta(int oldRotation, int newRotation) {
614             int delta = newRotation - oldRotation;
615             if (delta < 0) delta += 4;
616             return delta;
617         }
618 
619         /**
620          * @param deltaRotation the number of 90 degree turns from the current orientation
621          * @return {@code true} if the change in rotation results in a shift from landscape to
622          * portrait or vice versa, {@code false} otherwise
623          */
isOrientationChange(int deltaRotation)624         private boolean isOrientationChange(int deltaRotation) {
625             return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
626         }
627 
setThumbnailRotation(int deltaRotate, RectF thumbnailInsets, float scale, Rect thumbnailPosition, DeviceProfile dp)628         private void setThumbnailRotation(int deltaRotate, RectF thumbnailInsets, float scale,
629                 Rect thumbnailPosition, DeviceProfile dp) {
630             float newLeftInset = 0;
631             float newTopInset = 0;
632             float translateX = 0;
633             float translateY = 0;
634 
635             mMatrix.setRotate(90 * deltaRotate);
636             switch (deltaRotate) { /* Counter-clockwise */
637                 case Surface.ROTATION_90:
638                     newLeftInset = thumbnailInsets.bottom;
639                     newTopInset = thumbnailInsets.left;
640                     translateX = thumbnailPosition.height();
641                     break;
642                 case Surface.ROTATION_270:
643                     newLeftInset = thumbnailInsets.top;
644                     newTopInset = thumbnailInsets.right;
645                     translateY = thumbnailPosition.width();
646                     break;
647                 case Surface.ROTATION_180:
648                     newLeftInset = -thumbnailInsets.top;
649                     newTopInset = -thumbnailInsets.left;
650                     translateX = thumbnailPosition.width();
651                     translateY = thumbnailPosition.height();
652                     break;
653             }
654             mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale);
655             mMatrix.postTranslate(translateX, translateY);
656             if (TaskView.useFullThumbnail(dp)) {
657                 mMatrix.postTranslate(-mClippedInsets.left, -mClippedInsets.top);
658             }
659         }
660 
661         /**
662          * Insets to used for clipping the thumbnail (in case it is drawing outside its own space)
663          */
getInsetsToDrawInFullscreen(DeviceProfile dp)664         public RectF getInsetsToDrawInFullscreen(DeviceProfile dp) {
665             return TaskView.useFullThumbnail(dp) ? mClippedInsets : EMPTY_RECT_F;
666         }
667     }
668 }
669