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
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, int resizeMode) {
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        mResizeMode = resizeMode;
103
104        // Kick off our draw thread.
105        start();
106    }
107
108    void onResourcesLoaded(DecorView decorView, Drawable resizingBackgroundDrawable,
109            Drawable captionBackgroundDrawableDrawable, Drawable userCaptionBackgroundDrawable,
110            int statusBarColor, int navigationBarColor) {
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    private void addSystemBarNodeIfNeeded() {
142        if (mSystemBarBackgroundNode != null) {
143            return;
144        }
145        mSystemBarBackgroundNode = RenderNode.create("SystemBarBackgroundNode", null);
146        mRenderer.addRenderNode(mSystemBarBackgroundNode, false);
147    }
148
149    /**
150     * Call this function asynchronously when the window size has been changed or when the insets
151     * have changed or whether window switched between a fullscreen or non-fullscreen layout.
152     * The change will be picked up once per frame and the frame will be re-rendered accordingly.
153     *
154     * @param newTargetBounds The new target bounds.
155     * @param fullscreen Whether the window is currently drawing in fullscreen.
156     * @param systemInsets The current visible system insets for the window.
157     * @param stableInsets The stable insets for the window.
158     */
159    public void setTargetRect(Rect newTargetBounds, boolean fullscreen, Rect systemInsets,
160            Rect stableInsets) {
161        synchronized (this) {
162            mFullscreen = fullscreen;
163            mTargetRect.set(newTargetBounds);
164            mSystemInsets.set(systemInsets);
165            mStableInsets.set(stableInsets);
166            // Notify of a bounds change.
167            pingRenderLocked(false /* drawImmediate */);
168        }
169    }
170
171    /**
172     * The window got replaced due to a configuration change.
173     */
174    public void onConfigurationChange() {
175        synchronized (this) {
176            if (mRenderer != null) {
177                // Enforce a window redraw.
178                mOldTargetRect.set(0, 0, 0, 0);
179                pingRenderLocked(false /* drawImmediate */);
180            }
181        }
182    }
183
184    /**
185     * All resources of the renderer will be released. This function can be called from the
186     * the UI thread as well as the renderer thread.
187     */
188    public void releaseRenderer() {
189        synchronized (this) {
190            if (mRenderer != null) {
191                // Invalidate the current content bounds.
192                mRenderer.setContentDrawBounds(0, 0, 0, 0);
193
194                // Remove the render node again
195                // (see comment above - better to do that only once).
196                mRenderer.removeRenderNode(mFrameAndBackdropNode);
197                if (mSystemBarBackgroundNode != null) {
198                    mRenderer.removeRenderNode(mSystemBarBackgroundNode);
199                }
200
201                mRenderer = null;
202
203                // Exit the renderer loop.
204                pingRenderLocked(false /* drawImmediate */);
205            }
206        }
207    }
208
209    @Override
210    public void run() {
211        try {
212            Looper.prepare();
213            synchronized (this) {
214                mChoreographer = Choreographer.getInstance();
215            }
216            Looper.loop();
217        } finally {
218            releaseRenderer();
219        }
220        synchronized (this) {
221            // Make sure no more messages are being sent.
222            mChoreographer = null;
223            Choreographer.releaseInstance();
224        }
225    }
226
227    /**
228     * The implementation of the FrameCallback.
229     * @param frameTimeNanos The time in nanoseconds when the frame started being rendered,
230     * in the {@link System#nanoTime()} timebase.  Divide this value by {@code 1000000}
231     */
232    @Override
233    public void doFrame(long frameTimeNanos) {
234        synchronized (this) {
235            if (mRenderer == null) {
236                reportDrawIfNeeded();
237                // Tell the looper to stop. We are done.
238                Looper.myLooper().quit();
239                return;
240            }
241            doFrameUncheckedLocked();
242        }
243    }
244
245    private void doFrameUncheckedLocked() {
246        mNewTargetRect.set(mTargetRect);
247        if (!mNewTargetRect.equals(mOldTargetRect)
248                || mOldFullscreen != mFullscreen
249                || !mStableInsets.equals(mOldStableInsets)
250                || !mSystemInsets.equals(mOldSystemInsets)
251                || mReportNextDraw) {
252            mOldFullscreen = mFullscreen;
253            mOldTargetRect.set(mNewTargetRect);
254            mOldSystemInsets.set(mSystemInsets);
255            mOldStableInsets.set(mStableInsets);
256            redrawLocked(mNewTargetRect, mFullscreen, mSystemInsets, mStableInsets);
257        }
258    }
259
260    /**
261     * The content is about to be drawn and we got the location of where it will be shown.
262     * If a "redrawLocked" call has already been processed, we will re-issue the call
263     * if the previous call was ignored since the size was unknown.
264     * @param xOffset The x offset where the content is drawn to.
265     * @param yOffset The y offset where the content is drawn to.
266     * @param xSize The width size of the content. This should not be 0.
267     * @param ySize The height of the content.
268     * @return true if a frame should be requested after the content is drawn; false otherwise.
269     */
270    public boolean onContentDrawn(int xOffset, int yOffset, int xSize, int ySize) {
271        synchronized (this) {
272            final boolean firstCall = mLastContentWidth == 0;
273            // The current content buffer is drawn here.
274            mLastContentWidth = xSize;
275            mLastContentHeight = ySize - mLastCaptionHeight;
276            mLastXOffset = xOffset;
277            mLastYOffset = yOffset;
278
279            // Inform the renderer of the content's new bounds
280            mRenderer.setContentDrawBounds(
281                    mLastXOffset,
282                    mLastYOffset,
283                    mLastXOffset + mLastContentWidth,
284                    mLastYOffset + mLastCaptionHeight + mLastContentHeight);
285
286            // If this was the first call and redrawLocked got already called prior
287            // to us, we should re-issue a redrawLocked now.
288            return firstCall
289                    && (mLastCaptionHeight != 0 || !mDecorView.isShowingCaption());
290        }
291    }
292
293    public void onRequestDraw(boolean reportNextDraw) {
294        synchronized (this) {
295            mReportNextDraw = reportNextDraw;
296            mOldTargetRect.set(0, 0, 0, 0);
297            pingRenderLocked(true /* drawImmediate */);
298        }
299    }
300
301    /**
302     * Redraws the background, the caption and the system inset backgrounds if something changed.
303     *
304     * @param newBounds The window bounds which needs to be drawn.
305     * @param fullscreen Whether the window is currently drawing in fullscreen.
306     * @param systemInsets The current visible system insets for the window.
307     * @param stableInsets The stable insets for the window.
308     */
309    private void redrawLocked(Rect newBounds, boolean fullscreen, Rect systemInsets,
310            Rect stableInsets) {
311
312        // While a configuration change is taking place the view hierarchy might become
313        // inaccessible. For that case we remember the previous metrics to avoid flashes.
314        // Note that even when there is no visible caption, the caption child will exist.
315        final int captionHeight = mDecorView.getCaptionHeight();
316
317        // The caption height will probably never dynamically change while we are resizing.
318        // Once set to something other then 0 it should be kept that way.
319        if (captionHeight != 0) {
320            // Remember the height of the caption.
321            mLastCaptionHeight = captionHeight;
322        }
323
324        // Make sure that the other thread has already prepared the render draw calls for the
325        // content. If any size is 0, we have to wait for it to be drawn first.
326        if ((mLastCaptionHeight == 0 && mDecorView.isShowingCaption()) ||
327                mLastContentWidth == 0 || mLastContentHeight == 0) {
328            return;
329        }
330
331        // Since the surface is spanning the entire screen, we have to add the start offset of
332        // the bounds to get to the surface location.
333        final int left = mLastXOffset + newBounds.left;
334        final int top = mLastYOffset + newBounds.top;
335        final int width = newBounds.width();
336        final int height = newBounds.height();
337
338        mFrameAndBackdropNode.setLeftTopRightBottom(left, top, left + width, top + height);
339
340        // Draw the caption and content backdrops in to our render node.
341        DisplayListCanvas canvas = mFrameAndBackdropNode.start(width, height);
342        final Drawable drawable = mUserCaptionBackgroundDrawable != null
343                ? mUserCaptionBackgroundDrawable : mCaptionBackgroundDrawable;
344
345        if (drawable != null) {
346            drawable.setBounds(0, 0, left + width, top + mLastCaptionHeight);
347            drawable.draw(canvas);
348        }
349
350        // The backdrop: clear everything with the background. Clipping is done elsewhere.
351        if (mResizingBackgroundDrawable != null) {
352            mResizingBackgroundDrawable.setBounds(0, mLastCaptionHeight, left + width, top + height);
353            mResizingBackgroundDrawable.draw(canvas);
354        }
355        mFrameAndBackdropNode.end(canvas);
356
357        drawColorViews(left, top, width, height, fullscreen, systemInsets, stableInsets);
358
359        // We need to render the node explicitly
360        mRenderer.drawRenderNode(mFrameAndBackdropNode);
361
362        reportDrawIfNeeded();
363    }
364
365    private void drawColorViews(int left, int top, int width, int height,
366            boolean fullscreen, Rect systemInsets, Rect stableInsets) {
367        if (mSystemBarBackgroundNode == null) {
368            return;
369        }
370        DisplayListCanvas canvas = mSystemBarBackgroundNode.start(width, height);
371        mSystemBarBackgroundNode.setLeftTopRightBottom(left, top, left + width, top + height);
372        final int topInset = DecorView.getColorViewTopInset(mStableInsets.top, mSystemInsets.top);
373        final int bottomInset = DecorView.getColorViewBottomInset(stableInsets.bottom,
374                systemInsets.bottom);
375        final int rightInset = DecorView.getColorViewRightInset(stableInsets.right,
376                systemInsets.right);
377        final int leftInset = DecorView.getColorViewLeftInset(stableInsets.left,
378                systemInsets.left);
379        if (mStatusBarColor != null) {
380            mStatusBarColor.setBounds(0, 0, left + width, topInset);
381            mStatusBarColor.draw(canvas);
382        }
383
384        // We only want to draw the navigation bar if our window is currently fullscreen because we
385        // don't want the navigation bar background be moving around when resizing in docked mode.
386        // However, we need it for the transitions into/out of docked mode.
387        if (mNavigationBarColor != null && fullscreen) {
388            final int size = DecorView.getNavBarSize(bottomInset, rightInset, leftInset);
389            if (DecorView.isNavBarToRightEdge(bottomInset, rightInset)) {
390                mNavigationBarColor.setBounds(width - size, 0, width, height);
391            } else if (DecorView.isNavBarToLeftEdge(bottomInset, leftInset)) {
392                mNavigationBarColor.setBounds(0, 0, size, height);
393            } else {
394                mNavigationBarColor.setBounds(0, height - size, width, height);
395            }
396            mNavigationBarColor.draw(canvas);
397        }
398        mSystemBarBackgroundNode.end(canvas);
399        mRenderer.drawRenderNode(mSystemBarBackgroundNode);
400    }
401
402    /** Notify view root that a frame has been drawn by us, if it has requested so. */
403    private void reportDrawIfNeeded() {
404        if (mReportNextDraw) {
405            if (mDecorView.isAttachedToWindow()) {
406                mDecorView.getViewRootImpl().reportDrawFinish();
407            }
408            mReportNextDraw = false;
409        }
410    }
411
412    /**
413     * Sends a message to the renderer to wake up and perform the next action which can be
414     * either the next rendering or the self destruction if mRenderer is null.
415     * Note: This call must be synchronized.
416     *
417     * @param drawImmediate if we should draw immediately instead of scheduling a frame
418     */
419    private void pingRenderLocked(boolean drawImmediate) {
420        if (mChoreographer != null && !drawImmediate) {
421            mChoreographer.postFrameCallback(this);
422        } else {
423            doFrameUncheckedLocked();
424        }
425    }
426
427    void setUserCaptionBackgroundDrawable(Drawable userCaptionBackgroundDrawable) {
428        mUserCaptionBackgroundDrawable = userCaptionBackgroundDrawable;
429    }
430}
431