1 /*
2  * Copyright (C) 2014 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.systemui.recents.views;
18 
19 import android.content.Context;
20 import android.content.res.Configuration;
21 import android.content.res.Resources;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapShader;
24 import android.graphics.Canvas;
25 import android.graphics.Color;
26 import android.graphics.ColorMatrix;
27 import android.graphics.ColorMatrixColorFilter;
28 import android.graphics.LightingColorFilter;
29 import android.graphics.Matrix;
30 import android.graphics.Paint;
31 import android.graphics.Rect;
32 import android.graphics.Shader;
33 import android.util.AttributeSet;
34 import android.view.View;
35 import android.view.ViewDebug;
36 
37 import com.android.systemui.R;
38 import com.android.systemui.recents.events.EventBus;
39 import com.android.systemui.recents.events.ui.TaskSnapshotChangedEvent;
40 import com.android.systemui.recents.misc.Utilities;
41 import com.android.systemui.recents.model.Task;
42 import com.android.systemui.recents.model.ThumbnailData;
43 import java.io.PrintWriter;
44 
45 
46 /**
47  * The task thumbnail view.  It implements an image view that allows for animating the dim and
48  * alpha of the thumbnail image.
49  */
50 public class TaskViewThumbnail extends View {
51 
52     private static final ColorMatrix TMP_FILTER_COLOR_MATRIX = new ColorMatrix();
53     private static final ColorMatrix TMP_BRIGHTNESS_COLOR_MATRIX = new ColorMatrix();
54 
55     private Task mTask;
56 
57     private int mDisplayOrientation = Configuration.ORIENTATION_UNDEFINED;
58     private Rect mDisplayRect = new Rect();
59 
60     // Drawing
61     @ViewDebug.ExportedProperty(category="recents")
62     protected Rect mTaskViewRect = new Rect();
63     @ViewDebug.ExportedProperty(category="recents")
64     protected Rect mThumbnailRect = new Rect();
65     @ViewDebug.ExportedProperty(category="recents")
66     protected float mThumbnailScale;
67     private float mFullscreenThumbnailScale = 1f;
68     /** The height, in pixels, of the task view's title bar. */
69     private int mTitleBarHeight;
70     private boolean mSizeToFit = false;
71     private boolean mOverlayHeaderOnThumbnailActionBar = true;
72     private ThumbnailData mThumbnailData;
73 
74     protected int mCornerRadius;
75     @ViewDebug.ExportedProperty(category="recents")
76     private float mDimAlpha;
77     private Matrix mMatrix = new Matrix();
78     private Paint mDrawPaint = new Paint();
79     protected Paint mLockedPaint = new Paint();
80     protected Paint mBgFillPaint = new Paint();
81     protected BitmapShader mBitmapShader;
82     protected boolean mUserLocked = false;
83     private LightingColorFilter mLightingColorFilter = new LightingColorFilter(0xffffffff, 0);
84 
85     // Clip the top of the thumbnail against the opaque header bar that overlaps this view
86     private View mTaskBar;
87 
88     // Visibility optimization, if the thumbnail height is less than the height of the header
89     // bar for the task view, then just mark this thumbnail view as invisible
90     @ViewDebug.ExportedProperty(category="recents")
91     private boolean mInvisible;
92 
93     @ViewDebug.ExportedProperty(category="recents")
94     private boolean mDisabledInSafeMode;
95 
TaskViewThumbnail(Context context)96     public TaskViewThumbnail(Context context) {
97         this(context, null);
98     }
99 
TaskViewThumbnail(Context context, AttributeSet attrs)100     public TaskViewThumbnail(Context context, AttributeSet attrs) {
101         this(context, attrs, 0);
102     }
103 
TaskViewThumbnail(Context context, AttributeSet attrs, int defStyleAttr)104     public TaskViewThumbnail(Context context, AttributeSet attrs, int defStyleAttr) {
105         this(context, attrs, defStyleAttr, 0);
106     }
107 
TaskViewThumbnail(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)108     public TaskViewThumbnail(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
109         super(context, attrs, defStyleAttr, defStyleRes);
110         mDrawPaint.setColorFilter(mLightingColorFilter);
111         mDrawPaint.setFilterBitmap(true);
112         mDrawPaint.setAntiAlias(true);
113         Resources res = getResources();
114         mCornerRadius = res.getDimensionPixelSize(R.dimen.recents_task_view_rounded_corners_radius);
115         mBgFillPaint.setColor(Color.WHITE);
116         mLockedPaint.setColor(Color.WHITE);
117         mTitleBarHeight = res.getDimensionPixelSize(R.dimen.recents_grid_task_view_header_height);
118     }
119 
120     /**
121      * Called when the task view frame changes, allowing us to move the contents of the header
122      * to match the frame changes.
123      */
onTaskViewSizeChanged(int width, int height)124     public void onTaskViewSizeChanged(int width, int height) {
125         // Return early if the bounds have not changed
126         if (mTaskViewRect.width() == width && mTaskViewRect.height() == height) {
127             return;
128         }
129 
130         mTaskViewRect.set(0, 0, width, height);
131         setLeftTopRightBottom(0, 0, width, height);
132         updateThumbnailMatrix();
133     }
134 
135     @Override
onDraw(Canvas canvas)136     protected void onDraw(Canvas canvas) {
137         if (mInvisible) {
138             return;
139         }
140 
141         int viewWidth = mTaskViewRect.width();
142         int viewHeight = mTaskViewRect.height();
143         int thumbnailWidth = Math.min(viewWidth,
144                 (int) (mThumbnailRect.width() * mThumbnailScale));
145         int thumbnailHeight = Math.min(viewHeight,
146                 (int) (mThumbnailRect.height() * mThumbnailScale));
147 
148         if (mUserLocked) {
149             canvas.drawRoundRect(0, 0, viewWidth, viewHeight, mCornerRadius, mCornerRadius,
150                     mLockedPaint);
151         } else if (mBitmapShader != null && thumbnailWidth > 0 && thumbnailHeight > 0) {
152             int topOffset = 0;
153             if (mTaskBar != null && mOverlayHeaderOnThumbnailActionBar) {
154                 topOffset = mTaskBar.getHeight() - mCornerRadius;
155             }
156 
157             // Draw the background, there will be some small overdraw with the thumbnail
158             if (thumbnailWidth < viewWidth) {
159                 // Portrait thumbnail on a landscape task view
160                 canvas.drawRoundRect(Math.max(0, thumbnailWidth - mCornerRadius), topOffset,
161                         viewWidth, viewHeight,
162                         mCornerRadius, mCornerRadius, mBgFillPaint);
163             }
164             if (thumbnailHeight < viewHeight) {
165                 // Landscape thumbnail on a portrait task view
166                 canvas.drawRoundRect(0, Math.max(topOffset, thumbnailHeight - mCornerRadius),
167                         viewWidth, viewHeight,
168                         mCornerRadius, mCornerRadius, mBgFillPaint);
169             }
170 
171             // Draw the thumbnail
172             canvas.drawRoundRect(0, topOffset, thumbnailWidth, thumbnailHeight,
173                     mCornerRadius, mCornerRadius, mDrawPaint);
174         } else {
175             canvas.drawRoundRect(0, 0, viewWidth, viewHeight, mCornerRadius, mCornerRadius,
176                     mBgFillPaint);
177         }
178     }
179 
180     /** Sets the thumbnail to a given bitmap. */
setThumbnail(ThumbnailData thumbnailData)181     void setThumbnail(ThumbnailData thumbnailData) {
182         if (thumbnailData != null && thumbnailData.thumbnail != null) {
183             Bitmap bm = thumbnailData.thumbnail;
184             bm.prepareToDraw();
185             mFullscreenThumbnailScale = thumbnailData.scale;
186             mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
187             mDrawPaint.setShader(mBitmapShader);
188             mThumbnailRect.set(0, 0,
189                     bm.getWidth() - thumbnailData.insets.left - thumbnailData.insets.right,
190                     bm.getHeight() - thumbnailData.insets.top - thumbnailData.insets.bottom);
191             mThumbnailData = thumbnailData;
192             updateThumbnailMatrix();
193             updateThumbnailPaintFilter();
194         } else {
195             mBitmapShader = null;
196             mDrawPaint.setShader(null);
197             mThumbnailRect.setEmpty();
198             mThumbnailData = null;
199         }
200     }
201 
202     /** Updates the paint to draw the thumbnail. */
updateThumbnailPaintFilter()203     void updateThumbnailPaintFilter() {
204         if (mInvisible) {
205             return;
206         }
207         int mul = (int) ((1.0f - mDimAlpha) * 255);
208         if (mBitmapShader != null) {
209             if (mDisabledInSafeMode) {
210                 // Brightness: C-new = C-old*(1-amount) + amount
211                 TMP_FILTER_COLOR_MATRIX.setSaturation(0);
212                 float scale = 1f - mDimAlpha;
213                 float[] mat = TMP_BRIGHTNESS_COLOR_MATRIX.getArray();
214                 mat[0] = scale;
215                 mat[6] = scale;
216                 mat[12] = scale;
217                 mat[4] = mDimAlpha * 255f;
218                 mat[9] = mDimAlpha * 255f;
219                 mat[14] = mDimAlpha * 255f;
220                 TMP_FILTER_COLOR_MATRIX.preConcat(TMP_BRIGHTNESS_COLOR_MATRIX);
221                 ColorMatrixColorFilter filter = new ColorMatrixColorFilter(TMP_FILTER_COLOR_MATRIX);
222                 mDrawPaint.setColorFilter(filter);
223                 mBgFillPaint.setColorFilter(filter);
224                 mLockedPaint.setColorFilter(filter);
225             } else {
226                 mLightingColorFilter.setColorMultiply(Color.argb(255, mul, mul, mul));
227                 mDrawPaint.setColorFilter(mLightingColorFilter);
228                 mDrawPaint.setColor(0xFFffffff);
229                 mBgFillPaint.setColorFilter(mLightingColorFilter);
230                 mLockedPaint.setColorFilter(mLightingColorFilter);
231             }
232         } else {
233             int grey = mul;
234             mDrawPaint.setColorFilter(null);
235             mDrawPaint.setColor(Color.argb(255, grey, grey, grey));
236         }
237         if (!mInvisible) {
238             invalidate();
239         }
240     }
241 
242     /**
243      * Updates the scale of the bitmap relative to this view.
244      */
updateThumbnailMatrix()245     public void updateThumbnailMatrix() {
246         mThumbnailScale = 1f;
247         if (mBitmapShader != null && mThumbnailData != null) {
248             // We consider this a stack task if it is not freeform (ie. has no bounds) or has been
249             // dragged into the stack from the freeform workspace
250             boolean isStackTask = !mTask.isFreeformTask() || mTask.bounds == null;
251             int xOffset, yOffset = 0;
252             if (mTaskViewRect.isEmpty()) {
253                 // If we haven't measured , skip the thumbnail drawing and only draw the background
254                 // color
255                 mThumbnailScale = 0f;
256             } else if (mSizeToFit) {
257                 // Make sure we fill the entire space regardless of the orientation.
258                 float viewAspectRatio = (float) mTaskViewRect.width() /
259                         (float) (mTaskViewRect.height() - mTitleBarHeight);
260                 float thumbnailAspectRatio =
261                         (float) mThumbnailRect.width() / (float) mThumbnailRect.height();
262                 if (viewAspectRatio > thumbnailAspectRatio) {
263                     mThumbnailScale =
264                             (float) mTaskViewRect.width() / (float) mThumbnailRect.width();
265                 } else {
266                     mThumbnailScale = (float) (mTaskViewRect.height() - mTitleBarHeight)
267                             / (float) mThumbnailRect.height();
268                 }
269             } else if (isStackTask) {
270                 float invThumbnailScale = 1f / mFullscreenThumbnailScale;
271                 if (mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT) {
272                     if (mThumbnailData.orientation == Configuration.ORIENTATION_PORTRAIT) {
273                         // If we are in the same orientation as the screenshot, just scale it to the
274                         // width of the task view
275                         mThumbnailScale = (float) mTaskViewRect.width() / mThumbnailRect.width();
276                     } else {
277                         // Scale the landscape thumbnail up to app size, then scale that to the task
278                         // view size to match other portrait screenshots
279                         mThumbnailScale = invThumbnailScale *
280                                 ((float) mTaskViewRect.width() / mDisplayRect.width());
281                     }
282                 } else {
283                     // Otherwise, scale the screenshot to fit 1:1 in the current orientation
284                     mThumbnailScale = invThumbnailScale;
285                 }
286             } else {
287                 // Otherwise, if this is a freeform task with task bounds, then scale the thumbnail
288                 // to fit the entire bitmap into the task bounds
289                 mThumbnailScale = Math.min(
290                         (float) mTaskViewRect.width() / mThumbnailRect.width(),
291                         (float) mTaskViewRect.height() / mThumbnailRect.height());
292             }
293             mMatrix.setTranslate(-mThumbnailData.insets.left * mFullscreenThumbnailScale,
294                     -mThumbnailData.insets.top * mFullscreenThumbnailScale);
295             mMatrix.postScale(mThumbnailScale, mThumbnailScale);
296             mBitmapShader.setLocalMatrix(mMatrix);
297         }
298         if (!mInvisible) {
299             invalidate();
300         }
301     }
302 
303     /** Sets whether the thumbnail should be resized to fit the task view in all orientations. */
setSizeToFit(boolean flag)304     public void setSizeToFit(boolean flag) {
305         mSizeToFit = flag;
306     }
307 
308     /**
309      * Sets whether the header should overlap (and hide) the action bar in the thumbnail, or
310      * be stacked just above it.
311      */
setOverlayHeaderOnThumbnailActionBar(boolean flag)312     public void setOverlayHeaderOnThumbnailActionBar(boolean flag) {
313         mOverlayHeaderOnThumbnailActionBar = flag;
314     }
315 
316     /** Updates the clip rect based on the given task bar. */
updateClipToTaskBar(View taskBar)317     void updateClipToTaskBar(View taskBar) {
318         mTaskBar = taskBar;
319         invalidate();
320     }
321 
322     /** Updates the visibility of the the thumbnail. */
updateThumbnailVisibility(int clipBottom)323     void updateThumbnailVisibility(int clipBottom) {
324         boolean invisible = mTaskBar != null && (getHeight() - clipBottom) <= mTaskBar.getHeight();
325         if (invisible != mInvisible) {
326             mInvisible = invisible;
327             if (!mInvisible) {
328                 updateThumbnailPaintFilter();
329             }
330         }
331     }
332 
333     /**
334      * Sets the dim alpha, only used when we are not using hardware layers.
335      * (see RecentsConfiguration.useHardwareLayers)
336      */
setDimAlpha(float dimAlpha)337     public void setDimAlpha(float dimAlpha) {
338         mDimAlpha = dimAlpha;
339         updateThumbnailPaintFilter();
340     }
341 
342     /**
343      * Returns the {@link Paint} used to draw a task screenshot, or {@link #mLockedPaint} if the
344      * thumbnail shouldn't be drawn because it belongs to a locked user.
345      */
getDrawPaint()346     protected Paint getDrawPaint() {
347         if (mUserLocked) {
348             return mLockedPaint;
349         }
350         return mDrawPaint;
351     }
352 
353     /**
354      * Binds the thumbnail view to the task.
355      */
bindToTask(Task t, boolean disabledInSafeMode, int displayOrientation, Rect displayRect)356     void bindToTask(Task t, boolean disabledInSafeMode, int displayOrientation, Rect displayRect) {
357         mTask = t;
358         mDisabledInSafeMode = disabledInSafeMode;
359         mDisplayOrientation = displayOrientation;
360         mDisplayRect.set(displayRect);
361         if (t.colorBackground != 0) {
362             mBgFillPaint.setColor(t.colorBackground);
363         }
364         if (t.colorPrimary != 0) {
365             mLockedPaint.setColor(t.colorPrimary);
366         }
367         mUserLocked = t.isLocked;
368         EventBus.getDefault().register(this);
369     }
370 
371     /**
372      * Called when the bound task's data has loaded and this view should update to reflect the
373      * changes.
374      */
onTaskDataLoaded(ThumbnailData thumbnailData)375     void onTaskDataLoaded(ThumbnailData thumbnailData) {
376         setThumbnail(thumbnailData);
377     }
378 
379     /** Unbinds the thumbnail view from the task */
unbindFromTask()380     void unbindFromTask() {
381         mTask = null;
382         setThumbnail(null);
383         EventBus.getDefault().unregister(this);
384     }
385 
onBusEvent(TaskSnapshotChangedEvent event)386     public final void onBusEvent(TaskSnapshotChangedEvent event) {
387         if (mTask == null || event.taskId != mTask.key.id || event.thumbnailData == null
388                 || event.thumbnailData.thumbnail == null) {
389             return;
390         }
391         setThumbnail(event.thumbnailData);
392     }
393 
dump(String prefix, PrintWriter writer)394     public void dump(String prefix, PrintWriter writer) {
395         writer.print(prefix); writer.print("TaskViewThumbnail");
396         writer.print(" mTaskViewRect="); writer.print(Utilities.dumpRect(mTaskViewRect));
397         writer.print(" mThumbnailRect="); writer.print(Utilities.dumpRect(mThumbnailRect));
398         writer.print(" mThumbnailScale="); writer.print(mThumbnailScale);
399         writer.print(" mDimAlpha="); writer.print(mDimAlpha);
400         writer.println();
401     }
402 }
403