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.utilities.Utilities;
41 import com.android.systemui.shared.recents.model.Task;
42 import com.android.systemui.shared.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             if (mTaskViewRect.isEmpty()) {
249                 // If we haven't measured , skip the thumbnail drawing and only draw the background
250                 // color
251                 mThumbnailScale = 0f;
252             } else if (mSizeToFit) {
253                 // Make sure we fill the entire space regardless of the orientation.
254                 float viewAspectRatio = (float) mTaskViewRect.width() /
255                         (float) (mTaskViewRect.height() - mTitleBarHeight);
256                 float thumbnailAspectRatio =
257                         (float) mThumbnailRect.width() / (float) mThumbnailRect.height();
258                 if (viewAspectRatio > thumbnailAspectRatio) {
259                     mThumbnailScale =
260                             (float) mTaskViewRect.width() / (float) mThumbnailRect.width();
261                 } else {
262                     mThumbnailScale = (float) (mTaskViewRect.height() - mTitleBarHeight)
263                             / (float) mThumbnailRect.height();
264                 }
265             } else {
266                 float invThumbnailScale = 1f / mFullscreenThumbnailScale;
267                 if (mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT) {
268                     if (mThumbnailData.orientation == Configuration.ORIENTATION_PORTRAIT) {
269                         // If we are in the same orientation as the screenshot, just scale it to the
270                         // width of the task view
271                         mThumbnailScale = (float) mTaskViewRect.width() / mThumbnailRect.width();
272                     } else {
273                         // Scale the landscape thumbnail up to app size, then scale that to the task
274                         // view size to match other portrait screenshots
275                         mThumbnailScale = invThumbnailScale *
276                                 ((float) mTaskViewRect.width() / mDisplayRect.width());
277                     }
278                 } else {
279                     // Otherwise, scale the screenshot to fit 1:1 in the current orientation
280                     mThumbnailScale = invThumbnailScale;
281                 }
282             }
283             mMatrix.setTranslate(-mThumbnailData.insets.left * mFullscreenThumbnailScale,
284                     -mThumbnailData.insets.top * mFullscreenThumbnailScale);
285             mMatrix.postScale(mThumbnailScale, mThumbnailScale);
286             mBitmapShader.setLocalMatrix(mMatrix);
287         }
288         if (!mInvisible) {
289             invalidate();
290         }
291     }
292 
293     /** Sets whether the thumbnail should be resized to fit the task view in all orientations. */
setSizeToFit(boolean flag)294     public void setSizeToFit(boolean flag) {
295         mSizeToFit = flag;
296     }
297 
298     /**
299      * Sets whether the header should overlap (and hide) the action bar in the thumbnail, or
300      * be stacked just above it.
301      */
setOverlayHeaderOnThumbnailActionBar(boolean flag)302     public void setOverlayHeaderOnThumbnailActionBar(boolean flag) {
303         mOverlayHeaderOnThumbnailActionBar = flag;
304     }
305 
306     /** Updates the clip rect based on the given task bar. */
updateClipToTaskBar(View taskBar)307     void updateClipToTaskBar(View taskBar) {
308         mTaskBar = taskBar;
309         invalidate();
310     }
311 
312     /** Updates the visibility of the the thumbnail. */
updateThumbnailVisibility(int clipBottom)313     void updateThumbnailVisibility(int clipBottom) {
314         boolean invisible = mTaskBar != null && (getHeight() - clipBottom) <= mTaskBar.getHeight();
315         if (invisible != mInvisible) {
316             mInvisible = invisible;
317             if (!mInvisible) {
318                 updateThumbnailPaintFilter();
319             }
320         }
321     }
322 
323     /**
324      * Sets the dim alpha, only used when we are not using hardware layers.
325      * (see RecentsConfiguration.useHardwareLayers)
326      */
setDimAlpha(float dimAlpha)327     public void setDimAlpha(float dimAlpha) {
328         mDimAlpha = dimAlpha;
329         updateThumbnailPaintFilter();
330     }
331 
332     /**
333      * Returns the {@link Paint} used to draw a task screenshot, or {@link #mLockedPaint} if the
334      * thumbnail shouldn't be drawn because it belongs to a locked user.
335      */
getDrawPaint()336     protected Paint getDrawPaint() {
337         if (mUserLocked) {
338             return mLockedPaint;
339         }
340         return mDrawPaint;
341     }
342 
343     /**
344      * Binds the thumbnail view to the task.
345      */
bindToTask(Task t, boolean disabledInSafeMode, int displayOrientation, Rect displayRect)346     void bindToTask(Task t, boolean disabledInSafeMode, int displayOrientation, Rect displayRect) {
347         mTask = t;
348         mDisabledInSafeMode = disabledInSafeMode;
349         mDisplayOrientation = displayOrientation;
350         mDisplayRect.set(displayRect);
351         if (t.colorBackground != 0) {
352             mBgFillPaint.setColor(t.colorBackground);
353         }
354         if (t.colorPrimary != 0) {
355             mLockedPaint.setColor(t.colorPrimary);
356         }
357         mUserLocked = t.isLocked;
358         EventBus.getDefault().register(this);
359     }
360 
361     /**
362      * Called when the bound task's data has loaded and this view should update to reflect the
363      * changes.
364      */
onTaskDataLoaded(ThumbnailData thumbnailData)365     void onTaskDataLoaded(ThumbnailData thumbnailData) {
366         setThumbnail(thumbnailData);
367     }
368 
369     /** Unbinds the thumbnail view from the task */
unbindFromTask()370     void unbindFromTask() {
371         mTask = null;
372         setThumbnail(null);
373         EventBus.getDefault().unregister(this);
374     }
375 
onBusEvent(TaskSnapshotChangedEvent event)376     public final void onBusEvent(TaskSnapshotChangedEvent event) {
377         if (mTask == null || event.taskId != mTask.key.id || event.thumbnailData == null
378                 || event.thumbnailData.thumbnail == null) {
379             return;
380         }
381         setThumbnail(event.thumbnailData);
382     }
383 
dump(String prefix, PrintWriter writer)384     public void dump(String prefix, PrintWriter writer) {
385         writer.print(prefix); writer.print("TaskViewThumbnail");
386         writer.print(" mTaskViewRect="); writer.print(Utilities.dumpRect(mTaskViewRect));
387         writer.print(" mThumbnailRect="); writer.print(Utilities.dumpRect(mThumbnailRect));
388         writer.print(" mThumbnailScale="); writer.print(mThumbnailScale);
389         writer.print(" mDimAlpha="); writer.print(mDimAlpha);
390         writer.println();
391     }
392 }
393