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.graphics.Bitmap;
24 import android.graphics.BitmapShader;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.ColorFilter;
28 import android.graphics.ColorMatrix;
29 import android.graphics.ColorMatrixColorFilter;
30 import android.graphics.Insets;
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.os.Build;
39 import android.util.AttributeSet;
40 import android.util.FloatProperty;
41 import android.util.Property;
42 import android.view.Surface;
43 import android.view.View;
44 
45 import androidx.annotation.RequiresApi;
46 
47 import com.android.launcher3.BaseActivity;
48 import com.android.launcher3.DeviceProfile;
49 import com.android.launcher3.R;
50 import com.android.launcher3.Utilities;
51 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
52 import com.android.launcher3.util.MainThreadInitializedObject;
53 import com.android.launcher3.util.SystemUiController;
54 import com.android.launcher3.util.Themes;
55 import com.android.quickstep.TaskOverlayFactory;
56 import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
57 import com.android.quickstep.views.TaskView.FullscreenDrawParams;
58 import com.android.systemui.plugins.OverviewScreenshotActions;
59 import com.android.systemui.plugins.PluginListener;
60 import com.android.systemui.shared.recents.model.Task;
61 import com.android.systemui.shared.recents.model.ThumbnailData;
62 import com.android.systemui.shared.system.ConfigurationCompat;
63 
64 /**
65  * A task in the Recents view.
66  */
67 public class TaskThumbnailView extends View implements PluginListener<OverviewScreenshotActions> {
68 
69     private static final ColorMatrix COLOR_MATRIX = new ColorMatrix();
70     private static final ColorMatrix SATURATION_COLOR_MATRIX = new ColorMatrix();
71 
72     private static final MainThreadInitializedObject<FullscreenDrawParams> TEMP_PARAMS =
73             new MainThreadInitializedObject<>(FullscreenDrawParams::new);
74 
75     public static final Property<TaskThumbnailView, Float> DIM_ALPHA =
76             new FloatProperty<TaskThumbnailView>("dimAlpha") {
77                 @Override
78                 public void setValue(TaskThumbnailView thumbnail, float dimAlpha) {
79                     thumbnail.setDimAlpha(dimAlpha);
80                 }
81 
82                 @Override
83                 public Float get(TaskThumbnailView thumbnailView) {
84                     return thumbnailView.mDimAlpha;
85                 }
86             };
87 
88     private final BaseActivity mActivity;
89     private final TaskOverlay mOverlay;
90     private final boolean mIsDarkTextTheme;
91     private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
92     private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
93     private final Paint mClearPaint = new Paint();
94     private final Paint mDimmingPaintAfterClearing = new Paint();
95 
96     // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
97     private final Rect mPreviewRect = new Rect();
98     private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper();
99     private TaskView.FullscreenDrawParams mFullscreenParams;
100 
101     private Task mTask;
102     private ThumbnailData mThumbnailData;
103     protected BitmapShader mBitmapShader;
104 
105     private float mDimAlpha = 1f;
106     private float mDimAlphaMultiplier = 1f;
107     private float mSaturation = 1f;
108 
109     private boolean mOverlayEnabled;
110     private OverviewScreenshotActions mOverviewScreenshotActionsPlugin;
111 
TaskThumbnailView(Context context)112     public TaskThumbnailView(Context context) {
113         this(context, null);
114     }
115 
TaskThumbnailView(Context context, AttributeSet attrs)116     public TaskThumbnailView(Context context, AttributeSet attrs) {
117         this(context, attrs, 0);
118     }
119 
TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr)120     public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) {
121         super(context, attrs, defStyleAttr);
122         mOverlay = TaskOverlayFactory.INSTANCE.get(context).createOverlay(this);
123         mPaint.setFilterBitmap(true);
124         mBackgroundPaint.setColor(Color.WHITE);
125         mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
126         mDimmingPaintAfterClearing.setColor(Color.BLACK);
127         mActivity = BaseActivity.fromContext(context);
128         mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText);
129         // Initialize with dummy value. It is overridden later by TaskView
130         mFullscreenParams = TEMP_PARAMS.get(context);
131     }
132 
133     /**
134      * Updates the thumbnail to draw the provided task
135      * @param task
136      */
bind(Task task)137     public void bind(Task task) {
138         mOverlay.reset();
139         mTask = task;
140         int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000;
141         mPaint.setColor(color);
142         mBackgroundPaint.setColor(color);
143     }
144 
145     /**
146      * Updates the thumbnail.
147      * @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately.
148      *                   In most cases, we use the {@link #setThumbnail(Task, ThumbnailData)}
149      *                   version with {@code refreshNow} is true. The only exception is
150      *                   in the live tile case that we grab a screenshot when user enters Overview
151      *                   upon swipe up so that a usable screenshot is accessible immediately when
152      *                   recents animation needs to be finished / cancelled.
153      */
setThumbnail(Task task, ThumbnailData thumbnailData, boolean refreshNow)154     public void setThumbnail(Task task, ThumbnailData thumbnailData, boolean refreshNow) {
155         mTask = task;
156         mThumbnailData =
157                 (thumbnailData != null && thumbnailData.thumbnail != null) ? thumbnailData : null;
158         if (refreshNow) {
159             refresh();
160         }
161     }
162 
163     /** See {@link #setThumbnail(Task, ThumbnailData, boolean)} */
setThumbnail(Task task, ThumbnailData thumbnailData)164     public void setThumbnail(Task task, ThumbnailData thumbnailData) {
165         setThumbnail(task, thumbnailData, true /* refreshNow */);
166     }
167 
168     /** Updates the shader, paint, matrix to redraw. */
refresh()169     public void refresh() {
170         if (mThumbnailData != null && mThumbnailData.thumbnail != null) {
171             Bitmap bm = mThumbnailData.thumbnail;
172             bm.prepareToDraw();
173             mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
174             mPaint.setShader(mBitmapShader);
175             updateThumbnailMatrix();
176         } else {
177             mBitmapShader = null;
178             mThumbnailData = null;
179             mPaint.setShader(null);
180             mOverlay.reset();
181         }
182         if (mOverviewScreenshotActionsPlugin != null) {
183             mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
184         }
185         updateThumbnailPaintFilter();
186     }
187 
setDimAlphaMultipler(float dimAlphaMultipler)188     public void setDimAlphaMultipler(float dimAlphaMultipler) {
189         mDimAlphaMultiplier = dimAlphaMultipler;
190         setDimAlpha(mDimAlpha);
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 black.
197      */
setDimAlpha(float dimAlpha)198     public void setDimAlpha(float dimAlpha) {
199         mDimAlpha = dimAlpha;
200         updateThumbnailPaintFilter();
201     }
202 
getTaskOverlay()203     public TaskOverlay getTaskOverlay() {
204         return mOverlay;
205     }
206 
getDimAlpha()207     public float getDimAlpha() {
208         return mDimAlpha;
209     }
210 
getInsets(Rect fallback)211     public Rect getInsets(Rect fallback) {
212         if (mThumbnailData != null) {
213             return mThumbnailData.insets;
214         }
215         return fallback;
216     }
217 
218     /**
219      * Get the scaled insets that are being used to draw the task view. This is a subsection of
220      * the full snapshot.
221      * @return the insets in snapshot bitmap coordinates.
222      */
223     @RequiresApi(api = Build.VERSION_CODES.Q)
getScaledInsets()224     public Insets getScaledInsets() {
225         if (mThumbnailData == null) {
226             return Insets.NONE;
227         }
228 
229         RectF bitmapRect = new RectF(
230                 0, 0,
231                 mThumbnailData.thumbnail.getWidth(), mThumbnailData.thumbnail.getHeight());
232         RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
233 
234         // The position helper matrix tells us how to transform the bitmap to fit the view, the
235         // inverse tells us where the view would be in the bitmaps coordinates. The insets are the
236         // difference between the bitmap bounds and the projected view bounds.
237         Matrix boundsToBitmapSpace = new Matrix();
238         mPreviewPositionHelper.getMatrix().invert(boundsToBitmapSpace);
239         RectF boundsInBitmapSpace = new RectF();
240         boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);
241 
242         return Insets.of(
243             Math.round(boundsInBitmapSpace.left),
244             Math.round(boundsInBitmapSpace.top),
245             Math.round(bitmapRect.right - boundsInBitmapSpace.right),
246             Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom));
247     }
248 
249 
getSysUiStatusNavFlags()250     public int getSysUiStatusNavFlags() {
251         if (mThumbnailData != null) {
252             int flags = 0;
253             flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0
254                     ? SystemUiController.FLAG_LIGHT_STATUS
255                     : SystemUiController.FLAG_DARK_STATUS;
256             flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0
257                     ? SystemUiController.FLAG_LIGHT_NAV
258                     : SystemUiController.FLAG_DARK_NAV;
259             return flags;
260         }
261         return 0;
262     }
263 
264     @Override
onDraw(Canvas canvas)265     protected void onDraw(Canvas canvas) {
266         RectF currentDrawnInsets = mFullscreenParams.mCurrentDrawnInsets;
267         canvas.save();
268         canvas.scale(mFullscreenParams.mScale, mFullscreenParams.mScale);
269         canvas.translate(currentDrawnInsets.left, currentDrawnInsets.top);
270         // Draw the insets if we're being drawn fullscreen (we do this for quick switch).
271         drawOnCanvas(canvas,
272                 -currentDrawnInsets.left,
273                 -currentDrawnInsets.top,
274                 getMeasuredWidth() + currentDrawnInsets.right,
275                 getMeasuredHeight() + currentDrawnInsets.bottom,
276                 mFullscreenParams.mCurrentDrawnCornerRadius);
277         canvas.restore();
278     }
279 
280     @Override
onPluginConnected(OverviewScreenshotActions overviewScreenshotActions, Context context)281     public void onPluginConnected(OverviewScreenshotActions overviewScreenshotActions,
282             Context context) {
283         mOverviewScreenshotActionsPlugin = overviewScreenshotActions;
284         mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
285     }
286 
287     @Override
onPluginDisconnected(OverviewScreenshotActions plugin)288     public void onPluginDisconnected(OverviewScreenshotActions plugin) {
289         if (mOverviewScreenshotActionsPlugin != null) {
290             mOverviewScreenshotActionsPlugin = null;
291         }
292     }
293 
294     @Override
onAttachedToWindow()295     protected void onAttachedToWindow() {
296         super.onAttachedToWindow();
297         PluginManagerWrapper.INSTANCE.get(getContext())
298             .addPluginListener(this, OverviewScreenshotActions.class);
299     }
300 
301     @Override
onDetachedFromWindow()302     protected void onDetachedFromWindow() {
303         super.onDetachedFromWindow();
304         PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this);
305     }
306 
getPreviewPositionHelper()307     public PreviewPositionHelper getPreviewPositionHelper() {
308         return mPreviewPositionHelper;
309     }
310 
setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams)311     public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) {
312         mFullscreenParams = fullscreenParams;
313         invalidate();
314     }
315 
drawOnCanvas(Canvas canvas, float x, float y, float width, float height, float cornerRadius)316     public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height,
317             float cornerRadius) {
318         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
319             if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) {
320                 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint);
321                 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius,
322                         mDimmingPaintAfterClearing);
323                 return;
324             }
325         }
326 
327         // Draw the background in all cases, except when the thumbnail data is opaque
328         final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null
329                 || mThumbnailData == null;
330         if (drawBackgroundOnly || mPreviewPositionHelper.mClipBottom > 0
331                 || mThumbnailData.isTranslucent) {
332             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint);
333             if (drawBackgroundOnly) {
334                 return;
335             }
336         }
337 
338         if (mPreviewPositionHelper.mClipBottom > 0) {
339             canvas.save();
340             canvas.clipRect(x, y, width, mPreviewPositionHelper.mClipBottom);
341             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
342             canvas.restore();
343         } else {
344             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
345         }
346     }
347 
getTaskView()348     public TaskView getTaskView() {
349         return (TaskView) getParent();
350     }
351 
setOverlayEnabled(boolean overlayEnabled)352     public void setOverlayEnabled(boolean overlayEnabled) {
353         if (mOverlayEnabled != overlayEnabled) {
354             mOverlayEnabled = overlayEnabled;
355             updateOverlay();
356         }
357     }
358 
updateOverlay()359     private void updateOverlay() {
360         if (mOverlayEnabled && mBitmapShader != null && mThumbnailData != null) {
361             mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix,
362                     mPreviewPositionHelper.mIsOrientationChanged);
363         } else {
364             mOverlay.reset();
365         }
366     }
367 
updateThumbnailPaintFilter()368     private void updateThumbnailPaintFilter() {
369         int mul = (int) ((1 - mDimAlpha * mDimAlphaMultiplier) * 255);
370         ColorFilter filter = getColorFilter(mul, mIsDarkTextTheme, mSaturation);
371         mBackgroundPaint.setColorFilter(filter);
372         mDimmingPaintAfterClearing.setAlpha(255 - mul);
373         if (mBitmapShader != null) {
374             mPaint.setColorFilter(filter);
375         } else {
376             mPaint.setColorFilter(null);
377             mPaint.setColor(Color.argb(255, mul, mul, mul));
378         }
379         invalidate();
380     }
381 
updateThumbnailMatrix()382     private void updateThumbnailMatrix() {
383         mPreviewPositionHelper.mClipBottom = -1;
384         mPreviewPositionHelper.mIsOrientationChanged = false;
385         if (mBitmapShader != null && mThumbnailData != null) {
386             mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
387                     mThumbnailData.thumbnail.getHeight());
388             int currentRotation = ConfigurationCompat.getWindowConfigurationRotation(
389                     mActivity.getResources().getConfiguration());
390             mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
391                     getMeasuredWidth(), getMeasuredHeight(), mActivity.getDeviceProfile(),
392                     currentRotation);
393 
394             mBitmapShader.setLocalMatrix(mPreviewPositionHelper.mMatrix);
395             mPaint.setShader(mBitmapShader);
396         }
397         getTaskView().updateCurrentFullscreenParams(mPreviewPositionHelper);
398         invalidate();
399 
400         // Update can be called from {@link #onSizeChanged} during layout, post handling of overlay
401         // as overlay could modify the views in the overlay as a side effect of its update.
402         post(this::updateOverlay);
403     }
404 
405     @Override
onSizeChanged(int w, int h, int oldw, int oldh)406     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
407         super.onSizeChanged(w, h, oldw, oldh);
408         updateThumbnailMatrix();
409     }
410 
411     /**
412      * @param intensity multiplier for color values. 0 - make black (white if shouldLighten), 255 -
413      *                  leave unchanged.
414      */
getColorFilter(int intensity, boolean shouldLighten, float saturation)415     private static ColorFilter getColorFilter(int intensity, boolean shouldLighten,
416             float saturation) {
417         intensity = Utilities.boundToRange(intensity, 0, 255);
418 
419         if (intensity == 255 && saturation == 1) {
420             return null;
421         }
422 
423         final float intensityScale = intensity / 255f;
424         COLOR_MATRIX.setScale(intensityScale, intensityScale, intensityScale, 1);
425 
426         if (saturation != 1) {
427             SATURATION_COLOR_MATRIX.setSaturation(saturation);
428             COLOR_MATRIX.postConcat(SATURATION_COLOR_MATRIX);
429         }
430 
431         if (shouldLighten) {
432             final float[] colorArray = COLOR_MATRIX.getArray();
433             final int colorAdd = 255 - intensity;
434             colorArray[4] = colorAdd;
435             colorArray[9] = colorAdd;
436             colorArray[14] = colorAdd;
437         }
438 
439         return new ColorMatrixColorFilter(COLOR_MATRIX);
440     }
441 
getThumbnail()442     public Bitmap getThumbnail() {
443         if (mThumbnailData == null) {
444             return null;
445         }
446         return mThumbnailData.thumbnail;
447     }
448 
449     /**
450      * Returns whether the snapshot is real.
451      */
isRealSnapshot()452     public boolean isRealSnapshot() {
453         if (mThumbnailData == null) {
454             return false;
455         }
456         return mThumbnailData.isRealSnapshot;
457     }
458 
459     /**
460      * Utility class to position the thumbnail in the TaskView
461      */
462     public static class PreviewPositionHelper {
463 
464         // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
465         private final RectF mClippedInsets = new RectF();
466         private final Matrix mMatrix = new Matrix();
467         private float mClipBottom = -1;
468         private boolean mIsOrientationChanged;
469 
getMatrix()470         public Matrix getMatrix() {
471             return mMatrix;
472         }
473 
474         /**
475          * Updates the matrix based on the provided parameters
476          */
updateThumbnailMatrix(Rect thumbnailPosition, ThumbnailData thumbnailData, int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation)477         public void updateThumbnailMatrix(Rect thumbnailPosition, ThumbnailData thumbnailData,
478                 int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation) {
479             boolean isRotated = false;
480             boolean isOrientationDifferent;
481             mClipBottom = -1;
482 
483             float scale = thumbnailData.scale;
484             Rect activityInsets = dp.getInsets();
485             Rect thumbnailInsets = getBoundedInsets(activityInsets, thumbnailData.insets);
486             final float thumbnailWidth = thumbnailPosition.width()
487                     - (thumbnailInsets.left + thumbnailInsets.right) * scale;
488             final float thumbnailHeight = thumbnailPosition.height()
489                     - (thumbnailInsets.top + thumbnailInsets.bottom) * scale;
490 
491             final float thumbnailScale;
492             int thumbnailRotation = thumbnailData.rotation;
493             int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
494 
495             // Landscape vs portrait change
496             boolean windowingModeSupportsRotation = !dp.isMultiWindowMode
497                     && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
498             isOrientationDifferent = isOrientationChange(deltaRotate)
499                     && windowingModeSupportsRotation;
500             if (canvasWidth == 0) {
501                 // If we haven't measured , skip the thumbnail drawing and only draw the background
502                 // color
503                 thumbnailScale = 0f;
504             } else {
505                 // Rotate the screenshot if not in multi-window mode
506                 isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
507                 // Scale the screenshot to always fit the width of the card.
508                 thumbnailScale = isOrientationDifferent
509                         ? canvasWidth / thumbnailHeight
510                         : canvasWidth / thumbnailWidth;
511             }
512 
513             Rect splitScreenInsets = dp.getInsets();
514             if (!isRotated) {
515                 // No Rotation
516                 if (dp.isMultiWindowMode) {
517                     mClippedInsets.offsetTo(splitScreenInsets.left * scale,
518                             splitScreenInsets.top * scale);
519                 } else {
520                     mClippedInsets.offsetTo(thumbnailInsets.left * scale,
521                             thumbnailInsets.top * scale);
522                 }
523                 mMatrix.setTranslate(
524                         -thumbnailInsets.left * scale,
525                         -thumbnailInsets.top * scale);
526             } else {
527                 setThumbnailRotation(deltaRotate, thumbnailInsets, scale, thumbnailPosition);
528             }
529 
530             final float widthWithInsets;
531             final float heightWithInsets;
532             if (isOrientationDifferent) {
533                 widthWithInsets = thumbnailPosition.height() * thumbnailScale;
534                 heightWithInsets = thumbnailPosition.width() * thumbnailScale;
535             } else {
536                 widthWithInsets = thumbnailPosition.width() * thumbnailScale;
537                 heightWithInsets = thumbnailPosition.height() * thumbnailScale;
538             }
539             mClippedInsets.left *= thumbnailScale;
540             mClippedInsets.top *= thumbnailScale;
541 
542             if (dp.isMultiWindowMode) {
543                 mClippedInsets.right = splitScreenInsets.right * scale * thumbnailScale;
544                 mClippedInsets.bottom = splitScreenInsets.bottom * scale * thumbnailScale;
545             } else {
546                 mClippedInsets.right = Math.max(0,
547                         widthWithInsets - mClippedInsets.left - canvasWidth);
548                 mClippedInsets.bottom = Math.max(0,
549                         heightWithInsets - mClippedInsets.top - canvasHeight);
550             }
551 
552             mMatrix.postScale(thumbnailScale, thumbnailScale);
553 
554             float bitmapHeight = Math.max(0,
555                     (isOrientationDifferent ? thumbnailWidth : thumbnailHeight) * thumbnailScale);
556             if (Math.round(bitmapHeight) < canvasHeight) {
557                 mClipBottom = bitmapHeight;
558             }
559             mIsOrientationChanged = isOrientationDifferent;
560         }
561 
getBoundedInsets(Rect activityInsets, Rect insets)562         private Rect getBoundedInsets(Rect activityInsets, Rect insets) {
563             return new Rect(Math.min(insets.left, activityInsets.left),
564                     Math.min(insets.top, activityInsets.top),
565                     Math.min(insets.right, activityInsets.right),
566                     Math.min(insets.bottom, activityInsets.bottom));
567         }
568 
getRotationDelta(int oldRotation, int newRotation)569         private int getRotationDelta(int oldRotation, int newRotation) {
570             int delta = newRotation - oldRotation;
571             if (delta < 0) delta += 4;
572             return delta;
573         }
574 
575         /**
576          * @param deltaRotation the number of 90 degree turns from the current orientation
577          * @return {@code true} if the change in rotation results in a shift from landscape to
578          * portrait or vice versa, {@code false} otherwise
579          */
isOrientationChange(int deltaRotation)580         private boolean isOrientationChange(int deltaRotation) {
581             return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
582         }
583 
setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale, Rect thumbnailPosition)584         private void setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale,
585                 Rect thumbnailPosition) {
586             int newLeftInset = 0;
587             int newTopInset = 0;
588             int translateX = 0;
589             int translateY = 0;
590 
591             mMatrix.setRotate(90 * deltaRotate);
592             switch (deltaRotate) { /* Counter-clockwise */
593                 case Surface.ROTATION_90:
594                     newLeftInset = thumbnailInsets.bottom;
595                     newTopInset = thumbnailInsets.left;
596                     translateX = thumbnailPosition.height();
597                     break;
598                 case Surface.ROTATION_270:
599                     newLeftInset = thumbnailInsets.top;
600                     newTopInset = thumbnailInsets.right;
601                     translateY = thumbnailPosition.width();
602                     break;
603                 case Surface.ROTATION_180:
604                     newLeftInset = -thumbnailInsets.top;
605                     newTopInset = -thumbnailInsets.left;
606                     translateX = thumbnailPosition.width();
607                     translateY = thumbnailPosition.height();
608                     break;
609             }
610             mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale);
611             mMatrix.postTranslate(translateX - mClippedInsets.left,
612                     translateY - mClippedInsets.top);
613         }
614 
615         /**
616          * Insets to used for clipping the thumbnail (in case it is drawing outside its own space)
617          */
getInsetsToDrawInFullscreen()618         public RectF getInsetsToDrawInFullscreen() {
619             return mClippedInsets;
620         }
621     }
622 }
623