content: /*
   * 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 com.android.internal.policy;
  
  import android.graphics.RecordingCanvas;
  import android.graphics.Rect;
  import android.graphics.RenderNode;
  import android.graphics.drawable.ColorDrawable;
  import android.graphics.drawable.Drawable;
  import android.os.Looper;
  import android.view.Choreographer;
  import android.view.ThreadedRenderer;
  
  /**
   * The thread which draws a fill in background while the app is resizing in areas where the app
   * content draw is lagging behind the resize operation.
   * 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.
   * @hide
   */
  public class BackdropFrameRenderer extends Thread implements Choreographer.FrameCallback {
  
      private DecorView mDecorView;
  
      // 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 mFrameAndBackdropNode;
      private RenderNode mSystemBarBackgroundNode;
  
      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;
  
      // Whether to report when next frame is drawn or not.
      private boolean mReportNextDraw;
  
      private Drawable mCaptionBackgroundDrawable;
      private Drawable mUserCaptionBackgroundDrawable;
      private Drawable mResizingBackgroundDrawable;
      private ColorDrawable mStatusBarColor;
      private ColorDrawable mNavigationBarColor;
      private boolean mOldFullscreen;
      private boolean mFullscreen;
      private final Rect mOldSystemInsets = new Rect();
      private final Rect mOldStableInsets = new Rect();
      private final Rect mSystemInsets = new Rect();
      private final Rect mStableInsets = new Rect();
      private final Rect mTmpRect = new Rect();
  
      public BackdropFrameRenderer(DecorView decorView, ThreadedRenderer renderer, Rect initialBounds,
              Drawable resizingBackgroundDrawable, Drawable captionBackgroundDrawable,
              Drawable userCaptionBackgroundDrawable, int statusBarColor, int navigationBarColor,
              boolean fullscreen, Rect systemInsets, Rect stableInsets) {
          setName("ResizeFrame");
  
          mRenderer = renderer;
          onResourcesLoaded(decorView, resizingBackgroundDrawable, captionBackgroundDrawable,
                  userCaptionBackgroundDrawable, statusBarColor, navigationBarColor);
  
          // Create a render node for the content and frame backdrop
          // which can be resized independently from the content.
          mFrameAndBackdropNode = RenderNode.create("FrameAndBackdropNode", null);
  
          mRenderer.addRenderNode(mFrameAndBackdropNode, true);
  
          // Set the initial bounds and draw once so that we do not get a broken frame.
          mTargetRect.set(initialBounds);
          mFullscreen = fullscreen;
          mOldFullscreen = fullscreen;
          mSystemInsets.set(systemInsets);
          mStableInsets.set(stableInsets);
          mOldSystemInsets.set(systemInsets);
          mOldStableInsets.set(stableInsets);
  
          // Kick off our draw thread.
          start();
      }
  
      void onResourcesLoaded(DecorView decorView, Drawable resizingBackgroundDrawable,
              Drawable captionBackgroundDrawableDrawable, Drawable userCaptionBackgroundDrawable,
              int statusBarColor, int navigationBarColor) {
          synchronized (this) {
              mDecorView = decorView;
              mResizingBackgroundDrawable = resizingBackgroundDrawable != null
                      && resizingBackgroundDrawable.getConstantState() != null
                      ? resizingBackgroundDrawable.getConstantState().newDrawable()
                      : null;
              mCaptionBackgroundDrawable = captionBackgroundDrawableDrawable != null
                      && captionBackgroundDrawableDrawable.getConstantState() != null
                      ? captionBackgroundDrawableDrawable.getConstantState().newDrawable()
                      : null;
              mUserCaptionBackgroundDrawable = userCaptionBackgroundDrawable != null
                      && userCaptionBackgroundDrawable.getConstantState() != null
                      ? userCaptionBackgroundDrawable.getConstantState().newDrawable()
                      : null;
              if (mCaptionBackgroundDrawable == null) {
                  mCaptionBackgroundDrawable = mResizingBackgroundDrawable;
              }
              if (statusBarColor != 0) {
                  mStatusBarColor = new ColorDrawable(statusBarColor);
                  addSystemBarNodeIfNeeded();
              } else {
                  mStatusBarColor = null;
              }
              if (navigationBarColor != 0) {
                  mNavigationBarColor = new ColorDrawable(navigationBarColor);
                  addSystemBarNodeIfNeeded();
              } else {
                  mNavigationBarColor = null;
              }
          }
      }
  
      private void addSystemBarNodeIfNeeded() {
          if (mSystemBarBackgroundNode != null) {
              return;
          }
          mSystemBarBackgroundNode = RenderNode.create("SystemBarBackgroundNode", null);
          mRenderer.addRenderNode(mSystemBarBackgroundNode, false);
      }
  
      /**
       * Call this function asynchronously when the window size has been changed or when the insets
       * have changed or whether window switched between a fullscreen or non-fullscreen layout.
       * The change will be picked up once per frame and the frame will be re-rendered accordingly.
       *
       * @param newTargetBounds The new target bounds.
       * @param fullscreen Whether the window is currently drawing in fullscreen.
       * @param systemInsets The current visible system insets for the window.
       * @param stableInsets The stable insets for the window.
       */
      public void setTargetRect(Rect newTargetBounds, boolean fullscreen, Rect systemInsets,
              Rect stableInsets) {
          synchronized (this) {
              mFullscreen = fullscreen;
              mTargetRect.set(newTargetBounds);
              mSystemInsets.set(systemInsets);
              mStableInsets.set(stableInsets);
              // Notify of a bounds change.
              pingRenderLocked(false /* drawImmediate */);
          }
      }
  
      /**
       * The window got replaced due to a configuration change.
       */
      public void onConfigurationChange() {
          synchronized (this) {
              if (mRenderer != null) {
                  // Enforce a window redraw.
                  mOldTargetRect.set(0, 0, 0, 0);
                  pingRenderLocked(false /* drawImmediate */);
              }
          }
      }
  
      /**
       * All resources of the renderer will be released. This function can be called from the
       * the UI thread as well as the renderer thread.
       */
      void releaseRenderer() {
          synchronized (this) {
              if (mRenderer != null) {
                  // Invalidate the current content bounds.
                  mRenderer.setContentDrawBounds(0, 0, 0, 0);
  
                  // Remove the render node again
                  // (see comment above - better to do that only once).
                  mRenderer.removeRenderNode(mFrameAndBackdropNode);
                  if (mSystemBarBackgroundNode != null) {
                      mRenderer.removeRenderNode(mSystemBarBackgroundNode);
                  }
  
                  mRenderer = null;
  
                  // Exit the renderer loop.
                  pingRenderLocked(false /* drawImmediate */);
              }
          }
      }
  
      @Override
      public void run() {
          try {
              Looper.prepare();
              synchronized (this) {
                  mChoreographer = Choreographer.getInstance();
              }
              Looper.loop();
          } finally {
              releaseRenderer();
          }
          synchronized (this) {
              // Make sure no more messages are being sent.
              mChoreographer = null;
              Choreographer.releaseInstance();
          }
      }
  
      /**
       * 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) {
          synchronized (this) {
              if (mRenderer == null) {
                  reportDrawIfNeeded();
                  // Tell the looper to stop. We are done.
                  Looper.myLooper().quit();
                  return;
              }
              doFrameUncheckedLocked();
          }
      }
  
      private void doFrameUncheckedLocked() {
          mNewTargetRect.set(mTargetRect);
          if (!mNewTargetRect.equals(mOldTargetRect)
                  || mOldFullscreen != mFullscreen
                  || !mStableInsets.equals(mOldStableInsets)
                  || !mSystemInsets.equals(mOldSystemInsets)
                  || mReportNextDraw) {
              mOldFullscreen = mFullscreen;
              mOldTargetRect.set(mNewTargetRect);
              mOldSystemInsets.set(mSystemInsets);
              mOldStableInsets.set(mStableInsets);
              redrawLocked(mNewTargetRect, mFullscreen, mSystemInsets, mStableInsets);
          }
      }
  
      /**
       * The content is about to be drawn and we got the location of where it will be shown.
       * If a "redrawLocked" call has already been processed, we will re-issue the call
       * if the previous call was ignored since the size was unknown.
       * @param xOffset The x offset where the content is drawn to.
       * @param yOffset The y offset where the content is drawn to.
       * @param xSize The width size of the content. This should not be 0.
       * @param ySize The height of the content.
       * @return true if a frame should be requested after the content is drawn; false otherwise.
       */
      boolean onContentDrawn(int xOffset, int yOffset, int xSize, int ySize) {
          synchronized (this) {
              final boolean firstCall = mLastContentWidth == 0;
              // The current content buffer is drawn here.
              mLastContentWidth = xSize;
              mLastContentHeight = ySize - mLastCaptionHeight;
              mLastXOffset = xOffset;
              mLastYOffset = yOffset;
  
              // Inform the renderer of the content's new bounds
              mRenderer.setContentDrawBounds(
                      mLastXOffset,
                      mLastYOffset,
                      mLastXOffset + mLastContentWidth,
                      mLastYOffset + mLastCaptionHeight + mLastContentHeight);
  
              // If this was the first call and redrawLocked got already called prior
              // to us, we should re-issue a redrawLocked now.
              return firstCall
                      && (mLastCaptionHeight != 0 || !mDecorView.isShowingCaption());
          }
      }
  
      void onRequestDraw(boolean reportNextDraw) {
          synchronized (this) {
              mReportNextDraw = reportNextDraw;
              mOldTargetRect.set(0, 0, 0, 0);
              pingRenderLocked(true /* drawImmediate */);
          }
      }
  
      /**
       * Redraws the background, the caption and the system inset backgrounds if something changed.
       *
       * @param newBounds The window bounds which needs to be drawn.
       * @param fullscreen Whether the window is currently drawing in fullscreen.
       * @param systemInsets The current visible system insets for the window.
       * @param stableInsets The stable insets for the window.
       */
      private void redrawLocked(Rect newBounds, boolean fullscreen, Rect systemInsets,
              Rect stableInsets) {
  
          // While a configuration change is taking place the view hierarchy might become
          // inaccessible. For that case we remember the previous metrics to avoid flashes.
          // Note that even when there is no visible caption, the caption child will exist.
          final int captionHeight = mDecorView.getCaptionHeight();
  
          // The caption height will probably never dynamically change while we are resizing.
          // Once set to something other then 0 it should be kept that way.
          if (captionHeight != 0) {
              // Remember the height of the caption.
              mLastCaptionHeight = captionHeight;
          }
  
          // Make sure that the other thread has already prepared the render draw calls for the
          // content. If any size is 0, we have to wait for it to be drawn first.
          if ((mLastCaptionHeight == 0 && mDecorView.isShowingCaption()) ||
                  mLastContentWidth == 0 || mLastContentHeight == 0) {
              return;
          }
  
          // Content may not be drawn at the surface origin, so we want to keep the offset when we're
          // resizing it.
          final int left = mLastXOffset + newBounds.left;
          final int top = mLastYOffset + newBounds.top;
          final int width = newBounds.width();
          final int height = newBounds.height();
  
          mFrameAndBackdropNode.setLeftTopRightBottom(left, top, left + width, top + height);
  
          // Draw the caption and content backdrops in to our render node.
          RecordingCanvas canvas = mFrameAndBackdropNode.beginRecording(width, height);
          final Drawable drawable = mUserCaptionBackgroundDrawable != null
                  ? mUserCaptionBackgroundDrawable : mCaptionBackgroundDrawable;
  
          if (drawable != null) {
              drawable.setBounds(0, 0, left + width, top + mLastCaptionHeight);
              drawable.draw(canvas);
          }
  
          // The backdrop: clear everything with the background. Clipping is done elsewhere.
          if (mResizingBackgroundDrawable != null) {
              mResizingBackgroundDrawable.setBounds(0, mLastCaptionHeight, left + width, top + height);
              mResizingBackgroundDrawable.draw(canvas);
          }
          mFrameAndBackdropNode.endRecording();
  
          drawColorViews(left, top, width, height, fullscreen, systemInsets, stableInsets);
  
          // We need to render the node explicitly
          mRenderer.drawRenderNode(mFrameAndBackdropNode);
  
          reportDrawIfNeeded();
      }
  
      private void drawColorViews(int left, int top, int width, int height,
              boolean fullscreen, Rect systemInsets, Rect stableInsets) {
          if (mSystemBarBackgroundNode == null) {
              return;
          }
          RecordingCanvas canvas = mSystemBarBackgroundNode.beginRecording(width, height);
          mSystemBarBackgroundNode.setLeftTopRightBottom(left, top, left + width, top + height);
          final int topInset = DecorView.getColorViewTopInset(mStableInsets.top, mSystemInsets.top);
          if (mStatusBarColor != null) {
              mStatusBarColor.setBounds(0, 0, left + width, topInset);
              mStatusBarColor.draw(canvas);
          }
  
          // We only want to draw the navigation bar if our window is currently fullscreen because we
          // don't want the navigation bar background be moving around when resizing in docked mode.
          // However, we need it for the transitions into/out of docked mode.
          if (mNavigationBarColor != null && fullscreen) {
              DecorView.getNavigationBarRect(width, height, stableInsets, systemInsets, mTmpRect, 1f);
              mNavigationBarColor.setBounds(mTmpRect);
              mNavigationBarColor.draw(canvas);
          }
          mSystemBarBackgroundNode.endRecording();
          mRenderer.drawRenderNode(mSystemBarBackgroundNode);
      }
  
      /** Notify view root that a frame has been drawn by us, if it has requested so. */
      private void reportDrawIfNeeded() {
          if (mReportNextDraw) {
              if (mDecorView.isAttachedToWindow()) {
                  mDecorView.getViewRootImpl().reportDrawFinish();
              }
              mReportNextDraw = false;
          }
      }
  
      /**
       * 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.
       *
       * @param drawImmediate if we should draw immediately instead of scheduling a frame
       */
      private void pingRenderLocked(boolean drawImmediate) {
          if (mChoreographer != null && !drawImmediate) {
              mChoreographer.postFrameCallback(this);
          } else {
              doFrameUncheckedLocked();
          }
      }
  
      void setUserCaptionBackgroundDrawable(Drawable userCaptionBackgroundDrawable) {
          synchronized (this) {
              mUserCaptionBackgroundDrawable = userCaptionBackgroundDrawable;
          }
      }
  }
  