1 /*
2  * Copyright (C) 2015 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.internal.policy;
18 
19 import android.graphics.RecordingCanvas;
20 import android.graphics.Rect;
21 import android.graphics.RenderNode;
22 import android.graphics.drawable.ColorDrawable;
23 import android.graphics.drawable.Drawable;
24 import android.os.Looper;
25 import android.view.Choreographer;
26 import android.view.ThreadedRenderer;
27 
28 /**
29  * The thread which draws a fill in background while the app is resizing in areas where the app
30  * content draw is lagging behind the resize operation.
31  * It starts with the creation and it ends once someone calls destroy().
32  * Any size changes can be passed by a call to setTargetRect will passed to the thread and
33  * executed via the Choreographer.
34  * @hide
35  */
36 public class BackdropFrameRenderer extends Thread implements Choreographer.FrameCallback {
37 
38     private DecorView mDecorView;
39 
40     // This is containing the last requested size by a resize command. Note that this size might
41     // or might not have been applied to the output already.
42     private final Rect mTargetRect = new Rect();
43 
44     // The render nodes for the multi threaded renderer.
45     private ThreadedRenderer mRenderer;
46     private RenderNode mFrameAndBackdropNode;
47     private RenderNode mSystemBarBackgroundNode;
48 
49     private final Rect mOldTargetRect = new Rect();
50     private final Rect mNewTargetRect = new Rect();
51 
52     private Choreographer mChoreographer;
53 
54     // Cached size values from the last render for the case that the view hierarchy is gone
55     // during a configuration change.
56     private int mLastContentWidth;
57     private int mLastContentHeight;
58     private int mLastCaptionHeight;
59     private int mLastXOffset;
60     private int mLastYOffset;
61 
62     // Whether to report when next frame is drawn or not.
63     private boolean mReportNextDraw;
64 
65     private Drawable mCaptionBackgroundDrawable;
66     private Drawable mUserCaptionBackgroundDrawable;
67     private Drawable mResizingBackgroundDrawable;
68     private ColorDrawable mStatusBarColor;
69     private ColorDrawable mNavigationBarColor;
70     private boolean mOldFullscreen;
71     private boolean mFullscreen;
72     private final Rect mOldSystemInsets = new Rect();
73     private final Rect mOldStableInsets = new Rect();
74     private final Rect mSystemInsets = new Rect();
75     private final Rect mStableInsets = new Rect();
76     private final Rect mTmpRect = new Rect();
77 
BackdropFrameRenderer(DecorView decorView, ThreadedRenderer renderer, Rect initialBounds, Drawable resizingBackgroundDrawable, Drawable captionBackgroundDrawable, Drawable userCaptionBackgroundDrawable, int statusBarColor, int navigationBarColor, boolean fullscreen, Rect systemInsets, Rect stableInsets)78     public BackdropFrameRenderer(DecorView decorView, ThreadedRenderer renderer, Rect initialBounds,
79             Drawable resizingBackgroundDrawable, Drawable captionBackgroundDrawable,
80             Drawable userCaptionBackgroundDrawable, int statusBarColor, int navigationBarColor,
81             boolean fullscreen, Rect systemInsets, Rect stableInsets) {
82         setName("ResizeFrame");
83 
84         mRenderer = renderer;
85         onResourcesLoaded(decorView, resizingBackgroundDrawable, captionBackgroundDrawable,
86                 userCaptionBackgroundDrawable, statusBarColor, navigationBarColor);
87 
88         // Create a render node for the content and frame backdrop
89         // which can be resized independently from the content.
90         mFrameAndBackdropNode = RenderNode.create("FrameAndBackdropNode", null);
91 
92         mRenderer.addRenderNode(mFrameAndBackdropNode, true);
93 
94         // Set the initial bounds and draw once so that we do not get a broken frame.
95         mTargetRect.set(initialBounds);
96         mFullscreen = fullscreen;
97         mOldFullscreen = fullscreen;
98         mSystemInsets.set(systemInsets);
99         mStableInsets.set(stableInsets);
100         mOldSystemInsets.set(systemInsets);
101         mOldStableInsets.set(stableInsets);
102 
103         // Kick off our draw thread.
104         start();
105     }
106 
onResourcesLoaded(DecorView decorView, Drawable resizingBackgroundDrawable, Drawable captionBackgroundDrawableDrawable, Drawable userCaptionBackgroundDrawable, int statusBarColor, int navigationBarColor)107     void onResourcesLoaded(DecorView decorView, Drawable resizingBackgroundDrawable,
108             Drawable captionBackgroundDrawableDrawable, Drawable userCaptionBackgroundDrawable,
109             int statusBarColor, int navigationBarColor) {
110         synchronized (this) {
111             mDecorView = decorView;
112             mResizingBackgroundDrawable = resizingBackgroundDrawable != null
113                     && resizingBackgroundDrawable.getConstantState() != null
114                     ? resizingBackgroundDrawable.getConstantState().newDrawable()
115                     : null;
116             mCaptionBackgroundDrawable = captionBackgroundDrawableDrawable != null
117                     && captionBackgroundDrawableDrawable.getConstantState() != null
118                     ? captionBackgroundDrawableDrawable.getConstantState().newDrawable()
119                     : null;
120             mUserCaptionBackgroundDrawable = userCaptionBackgroundDrawable != null
121                     && userCaptionBackgroundDrawable.getConstantState() != null
122                     ? userCaptionBackgroundDrawable.getConstantState().newDrawable()
123                     : null;
124             if (mCaptionBackgroundDrawable == null) {
125                 mCaptionBackgroundDrawable = mResizingBackgroundDrawable;
126             }
127             if (statusBarColor != 0) {
128                 mStatusBarColor = new ColorDrawable(statusBarColor);
129                 addSystemBarNodeIfNeeded();
130             } else {
131                 mStatusBarColor = null;
132             }
133             if (navigationBarColor != 0) {
134                 mNavigationBarColor = new ColorDrawable(navigationBarColor);
135                 addSystemBarNodeIfNeeded();
136             } else {
137                 mNavigationBarColor = null;
138             }
139         }
140     }
141 
addSystemBarNodeIfNeeded()142     private void addSystemBarNodeIfNeeded() {
143         if (mSystemBarBackgroundNode != null) {
144             return;
145         }
146         mSystemBarBackgroundNode = RenderNode.create("SystemBarBackgroundNode", null);
147         mRenderer.addRenderNode(mSystemBarBackgroundNode, false);
148     }
149 
150     /**
151      * Call this function asynchronously when the window size has been changed or when the insets
152      * have changed or whether window switched between a fullscreen or non-fullscreen layout.
153      * The change will be picked up once per frame and the frame will be re-rendered accordingly.
154      *
155      * @param newTargetBounds The new target bounds.
156      * @param fullscreen Whether the window is currently drawing in fullscreen.
157      * @param systemInsets The current visible system insets for the window.
158      * @param stableInsets The stable insets for the window.
159      */
setTargetRect(Rect newTargetBounds, boolean fullscreen, Rect systemInsets, Rect stableInsets)160     public void setTargetRect(Rect newTargetBounds, boolean fullscreen, Rect systemInsets,
161             Rect stableInsets) {
162         synchronized (this) {
163             mFullscreen = fullscreen;
164             mTargetRect.set(newTargetBounds);
165             mSystemInsets.set(systemInsets);
166             mStableInsets.set(stableInsets);
167             // Notify of a bounds change.
168             pingRenderLocked(false /* drawImmediate */);
169         }
170     }
171 
172     /**
173      * The window got replaced due to a configuration change.
174      */
onConfigurationChange()175     public void onConfigurationChange() {
176         synchronized (this) {
177             if (mRenderer != null) {
178                 // Enforce a window redraw.
179                 mOldTargetRect.set(0, 0, 0, 0);
180                 pingRenderLocked(false /* drawImmediate */);
181             }
182         }
183     }
184 
185     /**
186      * All resources of the renderer will be released. This function can be called from the
187      * the UI thread as well as the renderer thread.
188      */
releaseRenderer()189     void releaseRenderer() {
190         synchronized (this) {
191             if (mRenderer != null) {
192                 // Invalidate the current content bounds.
193                 mRenderer.setContentDrawBounds(0, 0, 0, 0);
194 
195                 // Remove the render node again
196                 // (see comment above - better to do that only once).
197                 mRenderer.removeRenderNode(mFrameAndBackdropNode);
198                 if (mSystemBarBackgroundNode != null) {
199                     mRenderer.removeRenderNode(mSystemBarBackgroundNode);
200                 }
201 
202                 mRenderer = null;
203 
204                 // Exit the renderer loop.
205                 pingRenderLocked(false /* drawImmediate */);
206             }
207         }
208     }
209 
210     @Override
run()211     public void run() {
212         try {
213             Looper.prepare();
214             synchronized (this) {
215                 mChoreographer = Choreographer.getInstance();
216             }
217             Looper.loop();
218         } finally {
219             releaseRenderer();
220         }
221         synchronized (this) {
222             // Make sure no more messages are being sent.
223             mChoreographer = null;
224             Choreographer.releaseInstance();
225         }
226     }
227 
228     /**
229      * The implementation of the FrameCallback.
230      * @param frameTimeNanos The time in nanoseconds when the frame started being rendered,
231      * in the {@link System#nanoTime()} timebase.  Divide this value by {@code 1000000}
232      */
233     @Override
doFrame(long frameTimeNanos)234     public void doFrame(long frameTimeNanos) {
235         synchronized (this) {
236             if (mRenderer == null) {
237                 reportDrawIfNeeded();
238                 // Tell the looper to stop. We are done.
239                 Looper.myLooper().quit();
240                 return;
241             }
242             doFrameUncheckedLocked();
243         }
244     }
245 
doFrameUncheckedLocked()246     private void doFrameUncheckedLocked() {
247         mNewTargetRect.set(mTargetRect);
248         if (!mNewTargetRect.equals(mOldTargetRect)
249                 || mOldFullscreen != mFullscreen
250                 || !mStableInsets.equals(mOldStableInsets)
251                 || !mSystemInsets.equals(mOldSystemInsets)
252                 || mReportNextDraw) {
253             mOldFullscreen = mFullscreen;
254             mOldTargetRect.set(mNewTargetRect);
255             mOldSystemInsets.set(mSystemInsets);
256             mOldStableInsets.set(mStableInsets);
257             redrawLocked(mNewTargetRect, mFullscreen, mSystemInsets, mStableInsets);
258         }
259     }
260 
261     /**
262      * The content is about to be drawn and we got the location of where it will be shown.
263      * If a "redrawLocked" call has already been processed, we will re-issue the call
264      * if the previous call was ignored since the size was unknown.
265      * @param xOffset The x offset where the content is drawn to.
266      * @param yOffset The y offset where the content is drawn to.
267      * @param xSize The width size of the content. This should not be 0.
268      * @param ySize The height of the content.
269      * @return true if a frame should be requested after the content is drawn; false otherwise.
270      */
onContentDrawn(int xOffset, int yOffset, int xSize, int ySize)271     boolean onContentDrawn(int xOffset, int yOffset, int xSize, int ySize) {
272         synchronized (this) {
273             final boolean firstCall = mLastContentWidth == 0;
274             // The current content buffer is drawn here.
275             mLastContentWidth = xSize;
276             mLastContentHeight = ySize - mLastCaptionHeight;
277             mLastXOffset = xOffset;
278             mLastYOffset = yOffset;
279 
280             // Inform the renderer of the content's new bounds
281             mRenderer.setContentDrawBounds(
282                     mLastXOffset,
283                     mLastYOffset,
284                     mLastXOffset + mLastContentWidth,
285                     mLastYOffset + mLastCaptionHeight + mLastContentHeight);
286 
287             // If this was the first call and redrawLocked got already called prior
288             // to us, we should re-issue a redrawLocked now.
289             return firstCall
290                     && (mLastCaptionHeight != 0 || !mDecorView.isShowingCaption());
291         }
292     }
293 
onRequestDraw(boolean reportNextDraw)294     void onRequestDraw(boolean reportNextDraw) {
295         synchronized (this) {
296             mReportNextDraw = reportNextDraw;
297             mOldTargetRect.set(0, 0, 0, 0);
298             pingRenderLocked(true /* drawImmediate */);
299         }
300     }
301 
302     /**
303      * Redraws the background, the caption and the system inset backgrounds if something changed.
304      *
305      * @param newBounds The window bounds which needs to be drawn.
306      * @param fullscreen Whether the window is currently drawing in fullscreen.
307      * @param systemInsets The current visible system insets for the window.
308      * @param stableInsets The stable insets for the window.
309      */
redrawLocked(Rect newBounds, boolean fullscreen, Rect systemInsets, Rect stableInsets)310     private void redrawLocked(Rect newBounds, boolean fullscreen, Rect systemInsets,
311             Rect stableInsets) {
312 
313         // While a configuration change is taking place the view hierarchy might become
314         // inaccessible. For that case we remember the previous metrics to avoid flashes.
315         // Note that even when there is no visible caption, the caption child will exist.
316         final int captionHeight = mDecorView.getCaptionHeight();
317 
318         // The caption height will probably never dynamically change while we are resizing.
319         // Once set to something other then 0 it should be kept that way.
320         if (captionHeight != 0) {
321             // Remember the height of the caption.
322             mLastCaptionHeight = captionHeight;
323         }
324 
325         // Make sure that the other thread has already prepared the render draw calls for the
326         // content. If any size is 0, we have to wait for it to be drawn first.
327         if ((mLastCaptionHeight == 0 && mDecorView.isShowingCaption()) ||
328                 mLastContentWidth == 0 || mLastContentHeight == 0) {
329             return;
330         }
331 
332         // Content may not be drawn at the surface origin, so we want to keep the offset when we're
333         // resizing it.
334         final int left = mLastXOffset + newBounds.left;
335         final int top = mLastYOffset + newBounds.top;
336         final int width = newBounds.width();
337         final int height = newBounds.height();
338 
339         mFrameAndBackdropNode.setLeftTopRightBottom(left, top, left + width, top + height);
340 
341         // Draw the caption and content backdrops in to our render node.
342         RecordingCanvas canvas = mFrameAndBackdropNode.beginRecording(width, height);
343         final Drawable drawable = mUserCaptionBackgroundDrawable != null
344                 ? mUserCaptionBackgroundDrawable : mCaptionBackgroundDrawable;
345 
346         if (drawable != null) {
347             drawable.setBounds(0, 0, left + width, top + mLastCaptionHeight);
348             drawable.draw(canvas);
349         }
350 
351         // The backdrop: clear everything with the background. Clipping is done elsewhere.
352         if (mResizingBackgroundDrawable != null) {
353             mResizingBackgroundDrawable.setBounds(0, mLastCaptionHeight, left + width, top + height);
354             mResizingBackgroundDrawable.draw(canvas);
355         }
356         mFrameAndBackdropNode.endRecording();
357 
358         drawColorViews(left, top, width, height, fullscreen, systemInsets, stableInsets);
359 
360         // We need to render the node explicitly
361         mRenderer.drawRenderNode(mFrameAndBackdropNode);
362 
363         reportDrawIfNeeded();
364     }
365 
drawColorViews(int left, int top, int width, int height, boolean fullscreen, Rect systemInsets, Rect stableInsets)366     private void drawColorViews(int left, int top, int width, int height,
367             boolean fullscreen, Rect systemInsets, Rect stableInsets) {
368         if (mSystemBarBackgroundNode == null) {
369             return;
370         }
371         RecordingCanvas canvas = mSystemBarBackgroundNode.beginRecording(width, height);
372         mSystemBarBackgroundNode.setLeftTopRightBottom(left, top, left + width, top + height);
373         final int topInset = DecorView.getColorViewTopInset(mStableInsets.top, mSystemInsets.top);
374         if (mStatusBarColor != null) {
375             mStatusBarColor.setBounds(0, 0, left + width, topInset);
376             mStatusBarColor.draw(canvas);
377         }
378 
379         // We only want to draw the navigation bar if our window is currently fullscreen because we
380         // don't want the navigation bar background be moving around when resizing in docked mode.
381         // However, we need it for the transitions into/out of docked mode.
382         if (mNavigationBarColor != null && fullscreen) {
383             DecorView.getNavigationBarRect(width, height, stableInsets, systemInsets, mTmpRect, 1f);
384             mNavigationBarColor.setBounds(mTmpRect);
385             mNavigationBarColor.draw(canvas);
386         }
387         mSystemBarBackgroundNode.endRecording();
388         mRenderer.drawRenderNode(mSystemBarBackgroundNode);
389     }
390 
391     /** Notify view root that a frame has been drawn by us, if it has requested so. */
reportDrawIfNeeded()392     private void reportDrawIfNeeded() {
393         if (mReportNextDraw) {
394             if (mDecorView.isAttachedToWindow()) {
395                 mDecorView.getViewRootImpl().reportDrawFinish();
396             }
397             mReportNextDraw = false;
398         }
399     }
400 
401     /**
402      * Sends a message to the renderer to wake up and perform the next action which can be
403      * either the next rendering or the self destruction if mRenderer is null.
404      * Note: This call must be synchronized.
405      *
406      * @param drawImmediate if we should draw immediately instead of scheduling a frame
407      */
pingRenderLocked(boolean drawImmediate)408     private void pingRenderLocked(boolean drawImmediate) {
409         if (mChoreographer != null && !drawImmediate) {
410             mChoreographer.postFrameCallback(this);
411         } else {
412             doFrameUncheckedLocked();
413         }
414     }
415 
setUserCaptionBackgroundDrawable(Drawable userCaptionBackgroundDrawable)416     void setUserCaptionBackgroundDrawable(Drawable userCaptionBackgroundDrawable) {
417         synchronized (this) {
418             mUserCaptionBackgroundDrawable = userCaptionBackgroundDrawable;
419         }
420     }
421 }
422