Rendering the window frame with a second thread

Using a multi threaded render node to render the window frame
asynchronously from the application relayout.

Bug: 22527834
Bug: 24400680
Bug: 24459827
Bug: 24409773
Bug: 24537510
Change-Id: I1010fc6a8b6e38424178140afa3ca124433ab7e4
diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java
index dcef142..304e9c0 100644
--- a/core/java/android/view/ThreadedRenderer.java
+++ b/core/java/android/view/ThreadedRenderer.java
@@ -349,7 +349,7 @@
      * @param right The right side of the protected bounds.
      * @param bottom The bottom side of the protected bounds.
      */
-    public void setContentOverdrawProtectionBounds(int left, int top, int right, int bottom) {
+    public void setContentDrawBounds(int left, int top, int right, int bottom) {
         mStagedContentBounds.set(left, top, right, bottom);
     }
 
@@ -370,9 +370,9 @@
         // renderer.
         if (!mCurrentContentBounds.equals(mStagedContentBounds)) {
             mCurrentContentBounds.set(mStagedContentBounds);
-            nSetContentOverdrawProtectionBounds(mNativeProxy, mCurrentContentBounds.left,
-                mCurrentContentBounds.top, mCurrentContentBounds.right,
-            mCurrentContentBounds.bottom);
+            nSetContentDrawBounds(mNativeProxy, mCurrentContentBounds.left,
+                    mCurrentContentBounds.top, mCurrentContentBounds.right,
+                    mCurrentContentBounds.bottom);
         }
 
         attachInfo.mIgnoreDirtyState = false;
@@ -600,6 +600,6 @@
              boolean placeFront);
     private static native void nRemoveRenderNode(long nativeProxy, long rootRenderNode);
     private static native void nDrawRenderNode(long nativeProxy, long rootRenderNode);
-    private static native void nSetContentOverdrawProtectionBounds(long nativeProxy, int left,
+    private static native void nSetContentDrawBounds(long nativeProxy, int left,
              int top, int right, int bottom);
 }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index f6c60ed..7cf23e7 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -73,6 +73,7 @@
 import android.view.animation.Interpolator;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
+import android.view.WindowCallbacks;
 import android.widget.Scroller;
 
 import com.android.internal.R;
@@ -115,6 +116,12 @@
     private static final boolean DEBUG_INPUT_STAGES = false || LOCAL_LOGV;
 
     /**
+     * Set to false if we do not want to use the multi threaded renderer. Note that by disabling
+     * this, WindowCallbacks will not fire.
+     */
+    private static final boolean USE_MT_RENDERER = true;
+
+    /**
      * Set this system property to true to force the view hierarchy to render
      * at 60 Hz. This can be used to measure the potential framerate.
      */
@@ -132,11 +139,11 @@
 
     static final ThreadLocal<HandlerActionQueue> sRunQueues = new ThreadLocal<HandlerActionQueue>();
 
-    static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList<Runnable>();
+    static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList();
     static boolean sFirstDrawComplete = false;
+    static final ArrayList<WindowCallbacks> sWindowCallbacks = new ArrayList();
 
-    static final ArrayList<ComponentCallbacks> sConfigCallbacks
-            = new ArrayList<ComponentCallbacks>();
+    static final ArrayList<ComponentCallbacks> sConfigCallbacks = new ArrayList();
 
     final Context mContext;
     final IWindowSession mWindowSession;
@@ -417,6 +424,22 @@
         }
     }
 
+    public static void addWindowCallbacks(WindowCallbacks callback) {
+        if (USE_MT_RENDERER) {
+            synchronized (sWindowCallbacks) {
+                sWindowCallbacks.add(callback);
+            }
+        }
+    }
+
+    public static void removeWindowCallbacks(WindowCallbacks callback) {
+        if (USE_MT_RENDERER) {
+            synchronized (sWindowCallbacks) {
+                sWindowCallbacks.remove(callback);
+            }
+        }
+    }
+
     // FIXME for perf testing only
     private boolean mProfile = false;
 
@@ -1378,6 +1401,7 @@
             mAttachInfo.mWindowVisibility = viewVisibility;
             host.dispatchWindowVisibilityChanged(viewVisibility);
             if (viewVisibility != View.VISIBLE || mNewSurfaceNeeded) {
+                endDragResizing();
                 destroyHardwareResources();
             }
             if (viewVisibility == View.GONE) {
@@ -1701,14 +1725,20 @@
                 final boolean dragResizing = (relayoutResult
                         & WindowManagerGlobal.RELAYOUT_RES_DRAG_RESIZING) != 0;
                 if (mDragResizing != dragResizing) {
-                    mDragResizing = dragResizing;
-                    mFullRedrawNeeded = true;
+                    if (dragResizing) {
+                        startDragResizing(frame);
+                    } else {
+                        // We shouldn't come here, but if we come we should end the resize.
+                        endDragResizing();
+                    }
                 }
-                if (dragResizing) {
-                    mCanvasOffsetX = mWinFrame.left;
-                    mCanvasOffsetY = mWinFrame.top;
-                } else {
-                    mCanvasOffsetX = mCanvasOffsetY = 0;
+                if (!USE_MT_RENDERER) {
+                    if (dragResizing) {
+                        mCanvasOffsetX = mWinFrame.left;
+                        mCanvasOffsetY = mWinFrame.top;
+                    } else {
+                        mCanvasOffsetX = mCanvasOffsetY = 0;
+                    }
                 }
             } catch (RemoteException e) {
             }
@@ -6635,6 +6665,15 @@
                 Configuration newConfig) {
             final ViewRootImpl viewAncestor = mViewAncestor.get();
             if (viewAncestor != null) {
+                // Tell all listeners that we are resizing the window so that the chrome can get
+                // updated as fast as possible on a separate thread,
+                if (mViewAncestor.get().mDragResizing) {
+                    synchronized (sWindowCallbacks) {
+                        for (int i = sWindowCallbacks.size() - 1; i >= 0; i--) {
+                            sWindowCallbacks.get(i).onWindowSizeIsChanging(frame);
+                        }
+                    }
+                }
                 viewAncestor.dispatchResized(frame, overscanInsets, contentInsets,
                         visibleInsets, stableInsets, outsets, reportDraw, newConfig);
             }
@@ -6804,6 +6843,36 @@
     }
 
     /**
+     * Start a drag resizing which will inform all listeners that a window resize is taking place.
+     */
+    private void startDragResizing(Rect initialBounds) {
+        if (!mDragResizing) {
+            mDragResizing = true;
+            synchronized (sWindowCallbacks) {
+                for (int i = sWindowCallbacks.size() - 1; i >= 0; i--) {
+                    sWindowCallbacks.get(i).onWindowDragResizeStart(initialBounds);
+                }
+            }
+            mFullRedrawNeeded = true;
+        }
+    }
+
+    /**
+     * End a drag resize which will inform all listeners that a window resize has ended.
+     */
+    private void endDragResizing() {
+        if (mDragResizing) {
+            mDragResizing = false;
+            synchronized (sWindowCallbacks) {
+                for (int i = sWindowCallbacks.size() - 1; i >= 0; i--) {
+                    sWindowCallbacks.get(i).onWindowDragResizeEnd();
+                }
+            }
+            mFullRedrawNeeded = true;
+        }
+    }
+
+    /**
      * Class for managing the accessibility interaction connection
      * based on the global accessibility state.
      */
diff --git a/core/java/android/view/WindowCallbacks.java b/core/java/android/view/WindowCallbacks.java
new file mode 100644
index 0000000..cb6e983
--- /dev/null
+++ b/core/java/android/view/WindowCallbacks.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Rect;
+
+/**
+ * These callbacks are used to communicate window configuration changes while the user is performing
+ * window changes.
+ * @hide
+ */
+public interface WindowCallbacks {
+    /**
+     * Called by the system when the window got changed by the user, before the layouter got called.
+     * It can be used to perform a "quick and dirty" resize which should never take more then 4ms to
+     * complete.
+     *
+     * <p>At the time the layouting has not happened yet.
+     *
+     * @param newBounds The new window frame bounds.
+     */
+    void onWindowSizeIsChanging(Rect newBounds);
+
+    /**
+     * Called when a drag resize starts.
+     * @param initialBounds The initial bounds where the window will be.
+     */
+    void onWindowDragResizeStart(Rect initialBounds);
+
+    /**
+     * Called when a drag resize ends.
+     */
+    void onWindowDragResizeEnd();
+}
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index b6d7364..c9b8119 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -2615,7 +2615,7 @@
             if (action == MotionEvent.ACTION_DOWN) {
                 int y = (int)event.getY();
                 if (y >= (getHeight()-5) && !mWindow.hasChildren()) {
-                    Log.i(TAG, "Watchiing!");
+                    Log.i(TAG, "Watching!");
                     mWatchingForMenu = true;
                 }
                 return false;
diff --git a/core/java/com/android/internal/widget/NonClientDecorView.java b/core/java/com/android/internal/widget/NonClientDecorView.java
index 56cf921..6960a3b 100644
--- a/core/java/com/android/internal/widget/NonClientDecorView.java
+++ b/core/java/com/android/internal/widget/NonClientDecorView.java
@@ -17,15 +17,23 @@
 package com.android.internal.widget;
 
 import android.content.Context;
+import android.graphics.Color;
 import android.graphics.Rect;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.util.AttributeSet;
+import android.view.Choreographer;
+import android.view.DisplayListCanvas;
 import android.view.MotionEvent;
+import android.view.RenderNode;
+import android.view.ThreadedRenderer;
 import android.view.View;
+import android.view.ViewRootImpl;
 import android.widget.LinearLayout;
 import android.view.ViewGroup;
 import android.view.ViewOutlineProvider;
 import android.view.Window;
+import android.view.WindowCallbacks;
 import android.util.Log;
 import android.util.TypedValue;
 
@@ -58,7 +66,7 @@
  * This will be mitigated once b/22527834 will be addressed.
  */
 public class NonClientDecorView extends LinearLayout
-        implements View.OnClickListener, View.OnTouchListener {
+        implements View.OnClickListener, View.OnTouchListener, WindowCallbacks {
     private final static String TAG = "NonClientDecorView";
     // The height of a window which has focus in DIP.
     private final int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
@@ -67,6 +75,8 @@
     private PhoneWindow mOwner = null;
     private boolean mWindowHasShadow = false;
     private boolean mShowDecor = false;
+    // True when this object is listening for window size changes.
+    private boolean mAttachedCallbacksToRootViewImpl = false;
 
     // True if the window is being dragged.
     private boolean mDragging = false;
@@ -85,6 +95,9 @@
     // to max until the first layout command has been executed.
     private boolean mAllowUpdateElevation = false;
 
+    // The resize frame renderer.
+    private ResizeFrameThread mFrameRendererThread = null;
+
     public NonClientDecorView(Context context) {
         super(context);
     }
@@ -108,6 +121,18 @@
         // By changing the outline provider to BOUNDS, the window can remove its
         // background without removing the shadow.
         mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
+
+        if (!mAttachedCallbacksToRootViewImpl) {
+            // If there is no window callback installed there was no window set before. Set it now.
+            // Note that our ViewRootImpl object will not change.
+            getViewRootImpl().addWindowCallbacks(this);
+            mAttachedCallbacksToRootViewImpl = true;
+        } else if (mFrameRendererThread != null) {
+            // We are resizing and this call happened due to a configuration change. Tell the
+            // renderer about it.
+            mFrameRendererThread.onConfigurationChange();
+        }
+
         findViewById(R.id.maximize_window).setOnClickListener(this);
         findViewById(R.id.close_window).setOnClickListener(this);
     }
@@ -251,7 +276,9 @@
      **/
     private void updateElevation() {
         float elevation = 0;
-        if (mWindowHasShadow) {
+        // Do not use a shadow when we are in resizing mode (mRenderer not null) since the shadow
+        // is bound to the content size and not the target size.
+        if (mWindowHasShadow && mFrameRendererThread == null) {
             boolean fill = isFillingScreen();
             elevation = fill ? 0 :
                     (mWindowHasFocus ? DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP :
@@ -293,4 +320,270 @@
             }
         }
     }
+
+    @Override
+    public void onWindowDragResizeStart(Rect initialBounds) {
+        if (mOwner.isDestroyed()) {
+            // If the owner's window is gone, we should not be able to come here anymore.
+            releaseResources();
+            return;
+        }
+        if (mFrameRendererThread != null) {
+            return;
+        }
+        final ThreadedRenderer renderer =
+                (ThreadedRenderer) mOwner.getDecorView().getHardwareRenderer();
+        if (renderer != null) {
+            mFrameRendererThread = new ResizeFrameThread(renderer, initialBounds);
+            // Get rid of the shadow while we are resizing. Shadow drawing takes considerable time.
+            // If we want to get the shadow shown while resizing, we would need to elevate a new
+            // element which owns the caption and has the elevation.
+            updateElevation();
+        }
+    }
+
+    @Override
+    public void onWindowDragResizeEnd() {
+        releaseThreadedRenderer();
+    }
+
+    @Override
+    public void onWindowSizeIsChanging(Rect newBounds) {
+        if (mFrameRendererThread != null) {
+            mFrameRendererThread.setTargetRect(newBounds);
+        }
+    }
+
+    /**
+     * Release the renderer thread which is usually done when the user stops resizing.
+     */
+    private void releaseThreadedRenderer() {
+        if (mFrameRendererThread != null) {
+            mFrameRendererThread.releaseRenderer();
+            mFrameRendererThread = null;
+            // Bring the shadow back.
+            updateElevation();
+        }
+    }
+
+    /**
+     * Called when the parent window is destroyed to release all resources. Note that this will also
+     * destroy the renderer thread.
+     */
+    private void releaseResources() {
+        releaseThreadedRenderer();
+        if (mAttachedCallbacksToRootViewImpl) {
+            ViewRootImpl.removeWindowCallbacks(this);
+            mAttachedCallbacksToRootViewImpl = false;
+        }
+    }
+
+    /**
+     * The thread which draws the chrome while we are resizing.
+     * It starts with the creation and it ends once someone calls destroy().
+     * Any size changes can be passed by a call to setTargetRect will passed to the thread and
+     * executed via the Choreographer.
+     */
+    private class ResizeFrameThread extends Thread implements Choreographer.FrameCallback {
+        // This is containing the last requested size by a resize command. Note that this size might
+        // or might not have been applied to the output already.
+        private final Rect mTargetRect = new Rect();
+
+        // The render nodes for the multi threaded renderer.
+        private ThreadedRenderer mRenderer;
+        private RenderNode mFrameNode;
+        private RenderNode mBackdropNode;
+
+        private final Rect mOldTargetRect = new Rect();
+        private final Rect mNewTargetRect = new Rect();
+        private Choreographer mChoreographer;
+
+        // Cached size values from the last render for the case that the view hierarchy is gone
+        // during a configuration change.
+        private int mLastContentWidth;
+        private int mLastContentHeight;
+        private int mLastCaptionHeight;
+        private int mLastXOffset;
+        private int mLastYOffset;
+
+        ResizeFrameThread(ThreadedRenderer renderer, Rect initialBounds) {
+            mRenderer = renderer;
+
+            // Create the render nodes for our frame and backdrop which can be resized independently
+            // from the content.
+            mFrameNode = RenderNode.create("FrameNode", null);
+            mBackdropNode = RenderNode.create("BackdropNode", null);
+
+            mRenderer.addRenderNode(mFrameNode, false);
+            mRenderer.addRenderNode(mBackdropNode, true);
+
+            // Set the initial bounds and draw once so that we do not get a broken frame.
+            mTargetRect.set(initialBounds);
+            changeWindowSize(initialBounds);
+
+            // Kick off our draw thread.
+            start();
+        }
+
+        /**
+         * Call this function asynchronously when the window size has been changed. The change will
+         * be picked up once per frame and the frame will be re-rendered accordingly.
+         * @param newTargetBounds The new target bounds.
+         */
+        public void setTargetRect(Rect newTargetBounds) {
+            synchronized (this) {
+                mTargetRect.set(newTargetBounds);
+                // Notify of a bounds change.
+                pingRenderLocked();
+            }
+        }
+
+        /**
+         * The window got replaced due to a configuration change.
+         */
+        public void onConfigurationChange() {
+            if (mRenderer != null) {
+                // Enforce a window redraw.
+                mOldTargetRect.set(0, 0, 0, 0);
+                pingRenderLocked();
+            }
+        }
+
+        /**
+         * All resources of the renderer will be released. This function can be called from the
+         * the UI thread as well as the renderer thread.
+         */
+        public void releaseRenderer() {
+            synchronized (this) {
+                if (mRenderer != null) {
+                    // Invalidate the current content bounds.
+                    mRenderer.setContentDrawBounds(0, 0, 0, 0);
+
+                    // Remove the render nodes again (see comment above - better to do that only once).
+                    mRenderer.removeRenderNode(mFrameNode);
+                    mRenderer.removeRenderNode(mBackdropNode);
+
+                    mRenderer = null;
+
+                    // Exit the renderer loop.
+                    pingRenderLocked();
+                }
+            }
+        }
+
+        @Override
+        public void run() {
+            try {
+                Looper.prepare();
+                mChoreographer = Choreographer.getInstance();
+                Looper.loop();
+            } finally {
+                releaseRenderer();
+            }
+            synchronized (this) {
+                // Make sure no more messages are being sent.
+                mChoreographer = null;
+            }
+        }
+
+        /**
+         * The implementation of the FrameCallback.
+         * @param frameTimeNanos The time in nanoseconds when the frame started being rendered,
+         * in the {@link System#nanoTime()} timebase.  Divide this value by {@code 1000000}
+         */
+        @Override
+        public void doFrame(long frameTimeNanos) {
+            if (mRenderer == null) {
+                // Tell the looper to stop. We are done.
+                Looper.myLooper().quit();
+                return;
+            }
+            // Prevent someone from changing this while we are copying.
+            synchronized (this) {
+                mNewTargetRect.set(mTargetRect);
+            }
+            if (!mNewTargetRect.equals(mOldTargetRect)) {
+                mOldTargetRect.set(mNewTargetRect);
+                changeWindowSize(mNewTargetRect);
+            }
+        }
+
+        /**
+         * Resizing the frame to fit the new window size.
+         * @param newBounds The window bounds which needs to be drawn.
+         */
+        private void changeWindowSize(Rect newBounds) {
+            long startTime = System.currentTimeMillis();
+
+            // While a configuration change is taking place the view hierarchy might become
+            // inaccessible. For that case we remember the previous metrics to avoid flashes.
+            View caption = getChildAt(0);
+            View content = getChildAt(1);
+            if (content != null && caption != null) {
+                mLastContentWidth = content.getWidth();
+                mLastContentHeight = content.getHeight();
+                mLastCaptionHeight = caption.getHeight();
+
+                // Get the draw position within our surface.
+                int[] surfaceOrigin = new int[2];
+                surfaceOrigin[0] = 0;
+                surfaceOrigin[1] = 0;
+
+                // Get the shadow offsets.
+                getLocationInSurface(surfaceOrigin);
+                mLastXOffset = surfaceOrigin[0];
+                mLastYOffset = surfaceOrigin[1];
+            }
+
+            // Since the surface is spanning the entire screen, we have to add the start offset of
+            // the bounds to get to the surface location.
+            final int left = mLastXOffset + newBounds.left;
+            final int top = mLastYOffset + newBounds.top;
+            final int width = newBounds.width();
+            final int height = newBounds.height();
+
+            // Produce the draw calls.
+            // TODO(skuhne): Create a separate caption view which draws this. If the shadow should
+            // be resized while the window resizes, this hierarchy needs to have the elevation.
+            // That said - it is probably no good idea to draw the shadow every time since it costs
+            // a considerable time which we should rather spend for resizing the content and it does
+            // barely show while the entire screen is moving.
+            mFrameNode.setLeftTopRightBottom(left, top, left + width, top + mLastCaptionHeight);
+            DisplayListCanvas canvas = mFrameNode.start(width, height);
+            canvas.drawColor(Color.BLACK);
+            mFrameNode.end(canvas);
+
+            mBackdropNode.setLeftTopRightBottom(left, top + mLastCaptionHeight, left + width,
+                    top + height);
+
+            // The backdrop: clear everything with the background. Clipping is done elsewhere.
+            canvas = mBackdropNode.start(width, height - mLastCaptionHeight);
+            // TODO(skuhne): mOwner.getDecorView().mBackgroundFallback.draw(..) - or similar.
+            // Note: This might not work (calculator for example uses a transparent background).
+            canvas.drawColor(0xff808080);
+            mBackdropNode.end(canvas);
+
+            // The current content buffer is drawn here.
+            mRenderer.setContentDrawBounds(
+                    mLastXOffset,
+                    mLastYOffset + mLastCaptionHeight,
+                    mLastXOffset + mLastContentWidth,
+                    mLastYOffset + mLastCaptionHeight + mLastContentHeight);
+
+            // We need to render both rendered nodes explicitly.
+            mRenderer.drawRenderNode(mFrameNode);
+            mRenderer.drawRenderNode(mBackdropNode);
+        }
+
+        /**
+         * Sends a message to the renderer to wake up and perform the next action which can be
+         * either the next rendering or the self destruction if mRenderer is null.
+         * Note: This call must be synchronized.
+         */
+        private void pingRenderLocked() {
+            if (mChoreographer != null) {
+                mChoreographer.postFrameCallback(this);
+            }
+        }
+    }
 }