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
17package com.android.internal.policy;
18
19import android.graphics.Rect;
20import android.graphics.drawable.ColorDrawable;
21import android.graphics.drawable.Drawable;
22import android.os.Looper;
23import android.view.Choreographer;
24import android.view.DisplayListCanvas;
25import android.view.RenderNode;
26import 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 */
36public 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 int mResizeMode;
73    private final Rect mOldSystemInsets = new Rect();
74    private final Rect mOldStableInsets = new Rect();
75    private final Rect mSystemInsets = new Rect();
76    private final Rect mStableInsets = new Rect();
77    private final Rect mTmpRect = new Rect();
78
79    public BackdropFrameRenderer(DecorView decorView, ThreadedRenderer renderer, Rect initialBounds,
80            Drawable resizingBackgroundDrawable, Drawable captionBackgroundDrawable,
81            Drawable userCaptionBackgroundDrawable, int statusBarColor, int navigationBarColor,
82            boolean fullscreen, Rect systemInsets, Rect stableInsets, int resizeMode) {
83        setName("ResizeFrame");
84
85        mRenderer = renderer;
86        onResourcesLoaded(decorView, resizingBackgroundDrawable, captionBackgroundDrawable,
87                userCaptionBackgroundDrawable, statusBarColor, navigationBarColor);
88
89        // Create a render node for the content and frame backdrop
90        // which can be resized independently from the content.
91        mFrameAndBackdropNode = RenderNode.create("FrameAndBackdropNode", null);
92
93        mRenderer.addRenderNode(mFrameAndBackdropNode, true);
94
95        // Set the initial bounds and draw once so that we do not get a broken frame.
96        mTargetRect.set(initialBounds);
97        mFullscreen = fullscreen;
98        mOldFullscreen = fullscreen;
99        mSystemInsets.set(systemInsets);
100        mStableInsets.set(stableInsets);
101        mOldSystemInsets.set(systemInsets);
102        mOldStableInsets.set(stableInsets);
103        mResizeMode = resizeMode;
104
105        // Kick off our draw thread.
106        start();
107    }
108
109    void onResourcesLoaded(DecorView decorView, Drawable resizingBackgroundDrawable,
110            Drawable captionBackgroundDrawableDrawable, Drawable userCaptionBackgroundDrawable,
111            int statusBarColor, int navigationBarColor) {
112        mDecorView = decorView;
113        mResizingBackgroundDrawable = resizingBackgroundDrawable != null
114                        && resizingBackgroundDrawable.getConstantState() != null
115                ? resizingBackgroundDrawable.getConstantState().newDrawable()
116                : null;
117        mCaptionBackgroundDrawable = captionBackgroundDrawableDrawable != null
118                        && captionBackgroundDrawableDrawable.getConstantState() != null
119                ? captionBackgroundDrawableDrawable.getConstantState().newDrawable()
120                : null;
121        mUserCaptionBackgroundDrawable = userCaptionBackgroundDrawable != null
122                        && userCaptionBackgroundDrawable.getConstantState() != null
123                ? userCaptionBackgroundDrawable.getConstantState().newDrawable()
124                : null;
125        if (mCaptionBackgroundDrawable == null) {
126            mCaptionBackgroundDrawable = mResizingBackgroundDrawable;
127        }
128        if (statusBarColor != 0) {
129            mStatusBarColor = new ColorDrawable(statusBarColor);
130            addSystemBarNodeIfNeeded();
131        } else {
132            mStatusBarColor = null;
133        }
134        if (navigationBarColor != 0) {
135            mNavigationBarColor = new ColorDrawable(navigationBarColor);
136            addSystemBarNodeIfNeeded();
137        } else {
138            mNavigationBarColor = null;
139        }
140    }
141
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     */
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     */
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     */
189    public 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
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
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
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     */
271    public 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
294    public 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     */
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        // Since the surface is spanning the entire screen, we have to add the start offset of
333        // the bounds to get to the surface location.
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        DisplayListCanvas canvas = mFrameAndBackdropNode.start(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.end(canvas);
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
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        DisplayListCanvas canvas = mSystemBarBackgroundNode.start(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);
384            mNavigationBarColor.setBounds(mTmpRect);
385            mNavigationBarColor.draw(canvas);
386        }
387        mSystemBarBackgroundNode.end(canvas);
388        mRenderer.drawRenderNode(mSystemBarBackgroundNode);
389    }
390
391    /** Notify view root that a frame has been drawn by us, if it has requested so. */
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     */
408    private void pingRenderLocked(boolean drawImmediate) {
409        if (mChoreographer != null && !drawImmediate) {
410            mChoreographer.postFrameCallback(this);
411        } else {
412            doFrameUncheckedLocked();
413        }
414    }
415
416    void setUserCaptionBackgroundDrawable(Drawable userCaptionBackgroundDrawable) {
417        mUserCaptionBackgroundDrawable = userCaptionBackgroundDrawable;
418    }
419}
420