Remaining APIs for freeform window caption.

The APIs are:
* setting a listener for the area of the caption where we display
control buttons; this is intended for apps that overlay the caption on
their content, so they don't display the content in that area and clash
with the buttons;
* specifying the color pallete of controls in the caption; by default we
try to infer the best color from the theme (dark buttons for light theme
and vice versa), but in case we get it wrong, we allow the developer to
overwrite it;
* specifying the caption background during the resizing; by default we
try to infer a good color from the theme, but in case we do it wrong, we
allow the developer to overwrite this.

The CL also includes merging of layouts decor_caption_dark and
decor_caption_light into a single one. Not only it reduces code
duplication, but also is necessary for allowing adjustment of the
controls color pallete after the layout was created.

Bug: 25486369
Bug: 25818398

Change-Id: Ib87fe849b07df341893ec7873982bf7ab932f6d5
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 7a359e7..53490b4 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -29,6 +29,7 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.PixelFormat;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.media.session.MediaController;
 import android.net.Uri;
@@ -247,12 +248,30 @@
 
     private static final String PROPERTY_HARDWARE_UI = "persist.sys.ui.hw";
 
+    /**
+     * Flag for letting the theme drive the color of the window caption controls. Use with
+     * {@link #setDecorCaptionShade(int)}. This is the default value.
+     */
+    public static final int DECOR_CAPTION_SHADE_AUTO = 0;
+    /**
+     * Flag for setting light-color controls on the window caption. Use with
+     * {@link #setDecorCaptionShade(int)}.
+     */
+    public static final int DECOR_CAPTION_SHADE_LIGHT = 1;
+    /**
+     * Flag for setting dark-color controls on the window caption. Use with
+     * {@link #setDecorCaptionShade(int)}.
+     */
+    public static final int DECOR_CAPTION_SHADE_DARK = 2;
+
     private final Context mContext;
 
     private TypedArray mWindowStyle;
     private Callback mCallback;
     private OnWindowDismissedCallback mOnWindowDismissedCallback;
     private WindowControllerCallback mWindowControllerCallback;
+    private RestrictedCaptionAreaListener mRestrictedCaptionAreaListener;
+    private Rect mRestrictedCaptionAreaRect;
     private WindowManager mWindowManager;
     private IBinder mAppToken;
     private String mAppName;
@@ -565,6 +584,18 @@
         int getWindowStackId() throws RemoteException;
     }
 
+    /**
+     * Callback for clients that want to be aware of where caption draws content.
+     */
+    public interface RestrictedCaptionAreaListener {
+        /**
+         * Called when the area where caption draws content changes.
+         *
+         * @param rect The area where caption content is positioned, relative to the top view.
+         */
+        void onRestrictedCaptionAreaChanged(Rect rect);
+    }
+
     public Window(Context context) {
         mContext = context;
         mFeatures = mLocalFeatures = getDefaultFeatures(context);
@@ -778,6 +809,16 @@
     }
 
     /**
+     * Set a callback for changes of area where caption will draw its content.
+     *
+     * @param listener Callback that will be called when the area changes.
+     */
+    public final void setRestrictedCaptionAreaListener(RestrictedCaptionAreaListener listener) {
+        mRestrictedCaptionAreaListener = listener;
+        mRestrictedCaptionAreaRect = listener != null ? new Rect() : null;
+    }
+
+    /**
      * Take ownership of this window's surface.  The window's view hierarchy
      * will no longer draw into the surface, though it will otherwise continue
      * to operate (such as for receiving input events).  The given SurfaceHolder
@@ -2040,5 +2081,29 @@
         return mOverlayWithDecorCaption;
     }
 
+    /** @hide */
+    public void notifyRestrictedCaptionAreaCallback(int left, int top, int right, int bottom) {
+        if (mRestrictedCaptionAreaListener != null) {
+            mRestrictedCaptionAreaRect.set(left, top, right, bottom);
+            mRestrictedCaptionAreaListener.onRestrictedCaptionAreaChanged(
+                    mRestrictedCaptionAreaRect);
+        }
+    }
 
+    /**
+     * Set what color should the caption controls be. By default the system will try to determine
+     * the color from the theme. You can overwrite this by using {@link #DECOR_CAPTION_SHADE_DARK}
+     * or {@link #DECOR_CAPTION_SHADE_DARK}.
+     */
+    public abstract void setDecorCaptionShade(int decorCaptionShade);
+
+    /**
+     * Set the drawable that is drawn underneath the caption during the resizing.
+     *
+     * During the resizing the caption might not be drawn fast enough to match the new dimensions.
+     * There is a second caption drawn underneath it that will be fast enough. By default the
+     * caption is constructed from the theme. You can provide a drawable, that will be drawn instead
+     * to better match your application.
+     */
+    public abstract void setResizingCaptionDrawable(Drawable drawable);
 }
diff --git a/core/java/com/android/internal/policy/BackdropFrameRenderer.java b/core/java/com/android/internal/policy/BackdropFrameRenderer.java
index 75ca639..1b44ff3 100644
--- a/core/java/com/android/internal/policy/BackdropFrameRenderer.java
+++ b/core/java/com/android/internal/policy/BackdropFrameRenderer.java
@@ -63,17 +63,18 @@
     private boolean mReportNextDraw;
 
     private Drawable mCaptionBackgroundDrawable;
+    private Drawable mUserCaptionBackgroundDrawable;
     private Drawable mResizingBackgroundDrawable;
     private ColorDrawable mStatusBarColor;
 
     public BackdropFrameRenderer(DecorView decorView, ThreadedRenderer renderer, Rect initialBounds,
             Drawable resizingBackgroundDrawable, Drawable captionBackgroundDrawable,
-            int statusBarColor) {
+            Drawable userCaptionBackgroundDrawable, int statusBarColor) {
         setName("ResizeFrame");
 
         mRenderer = renderer;
         onResourcesLoaded(decorView, resizingBackgroundDrawable, captionBackgroundDrawable,
-                statusBarColor);
+                userCaptionBackgroundDrawable, statusBarColor);
 
         // Create a render node for the content and frame backdrop
         // which can be resized independently from the content.
@@ -92,10 +93,12 @@
     }
 
     void onResourcesLoaded(DecorView decorView, Drawable resizingBackgroundDrawable,
-            Drawable captionBackgroundDrawableDrawable, int statusBarColor) {
+            Drawable captionBackgroundDrawableDrawable, Drawable userCaptionBackgroundDrawable,
+            int statusBarColor) {
         mDecorView = decorView;
         mResizingBackgroundDrawable = resizingBackgroundDrawable;
         mCaptionBackgroundDrawable = captionBackgroundDrawableDrawable;
+        mUserCaptionBackgroundDrawable = userCaptionBackgroundDrawable;
         if (statusBarColor != 0) {
             mStatusBarColor = new ColorDrawable(statusBarColor);
             addSystemBarNodeIfNeeded();
@@ -281,8 +284,10 @@
 
         // Draw the caption and content backdrops in to our render node.
         DisplayListCanvas canvas = mFrameAndBackdropNode.start(width, height);
-        mCaptionBackgroundDrawable.setBounds(0, 0, left + width, top + mLastCaptionHeight);
-        mCaptionBackgroundDrawable.draw(canvas);
+        final Drawable drawable = mUserCaptionBackgroundDrawable != null
+                ? mUserCaptionBackgroundDrawable : mCaptionBackgroundDrawable;
+        drawable.setBounds(0, 0, left + width, top + mLastCaptionHeight);
+        drawable.draw(canvas);
 
         // The backdrop: clear everything with the background. Clipping is done elsewhere.
         mResizingBackgroundDrawable.setBounds(0, mLastCaptionHeight, left + width, top + height);
@@ -324,4 +329,8 @@
             mChoreographer.postFrameCallback(this);
         }
     }
+
+    void setUserCaptionBackgroundDrawable(Drawable userCaptionBackgroundDrawable) {
+        mUserCaptionBackgroundDrawable = userCaptionBackgroundDrawable;
+    }
 }
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 531ba2f..e405564 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -74,6 +74,8 @@
 import static android.view.View.MeasureSpec.getMode;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.Window.DECOR_CAPTION_SHADE_DARK;
+import static android.view.Window.DECOR_CAPTION_SHADE_LIGHT;
 import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
 import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN;
 import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
@@ -184,9 +186,10 @@
 
     private boolean mWindowResizeCallbacksAdded = false;
 
-    BackdropFrameRenderer mBackdropFrameRenderer = null;
+    private BackdropFrameRenderer mBackdropFrameRenderer = null;
     private Drawable mResizingBackgroundDrawable;
     private Drawable mCaptionBackgroundDrawable;
+    private Drawable mUserCaptionBackgroundDrawable;
 
     DecorView(Context context, int featureId, PhoneWindow window) {
         super(context);
@@ -1580,18 +1583,20 @@
         initializeElevation();
     }
 
-    View onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
+    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
         mStackId = getStackId();
 
         mResizingBackgroundDrawable = getResizingBackgroundDrawable(
                 mWindow.mBackgroundResource, mWindow.mBackgroundFallbackResource);
-        mCaptionBackgroundDrawable =
-                getContext().getDrawable(R.drawable.decor_caption_title_focused);
+        if (mCaptionBackgroundDrawable == null) {
+            mCaptionBackgroundDrawable = getContext().getDrawable(
+                    R.drawable.decor_caption_title_focused);
+        }
 
         if (mBackdropFrameRenderer != null) {
             mBackdropFrameRenderer.onResourcesLoaded(
                     this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
-                    getCurrentColor(mStatusColorViewState));
+                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState));
         }
 
         mDecorCaptionView = createDecorCaptionView(inflater);
@@ -1608,17 +1613,16 @@
         }
         mContentRoot = (ViewGroup) root;
         initializeElevation();
-        return root;
     }
 
     // Free floating overlapping windows require a caption.
     private DecorCaptionView createDecorCaptionView(LayoutInflater inflater) {
-        DecorCaptionView DecorCaptionView = null;
-        for (int i = getChildCount() - 1; i >= 0 && DecorCaptionView == null; i--) {
+        DecorCaptionView decorCaptionView = null;
+        for (int i = getChildCount() - 1; i >= 0 && decorCaptionView == null; i--) {
             View view = getChildAt(i);
             if (view instanceof DecorCaptionView) {
                 // The decor was most likely saved from a relaunch - so reuse it.
-                DecorCaptionView = (DecorCaptionView) view;
+                decorCaptionView = (DecorCaptionView) view;
                 removeViewAt(i);
             }
         }
@@ -1630,27 +1634,72 @@
                 && ActivityManager.StackId.hasWindowDecor(mStackId)) {
             // Dependent on the brightness of the used title we either use the
             // dark or the light button frame.
-            if (DecorCaptionView == null) {
-                Context context = getContext();
-                TypedValue value = new TypedValue();
-                context.getTheme().resolveAttribute(R.attr.colorPrimary, value, true);
-                inflater = inflater.from(context);
-                if (Color.luminance(value.data) < 0.5) {
-                    DecorCaptionView = (DecorCaptionView) inflater.inflate(
-                            R.layout.decor_caption_dark, null);
-                } else {
-                    DecorCaptionView = (DecorCaptionView) inflater.inflate(
-                            R.layout.decor_caption_light, null);
-                }
+            if (decorCaptionView == null) {
+                decorCaptionView = inflateDecorCaptionView(inflater);
             }
-            DecorCaptionView.setPhoneWindow(mWindow, true /*showDecor*/);
+            decorCaptionView.setPhoneWindow(mWindow, true /*showDecor*/);
         } else {
-            DecorCaptionView = null;
+            decorCaptionView = null;
         }
 
         // Tell the decor if it has a visible caption.
-        enableCaption(DecorCaptionView != null);
-        return DecorCaptionView;
+        enableCaption(decorCaptionView != null);
+        return decorCaptionView;
+    }
+
+    private DecorCaptionView inflateDecorCaptionView(LayoutInflater inflater) {
+        final Context context = getContext();
+        // We make a copy of the inflater, so it has the right context associated with it.
+        inflater = inflater.from(context);
+        final DecorCaptionView view = (DecorCaptionView) inflater.inflate(R.layout.decor_caption,
+                null);
+        setDecorCaptionShade(context, view);
+        return view;
+    }
+
+    private void setDecorCaptionShade(Context context, DecorCaptionView view) {
+        final int shade = mWindow.getDecorCaptionShade();
+        switch (shade) {
+            case DECOR_CAPTION_SHADE_LIGHT:
+                setLightDecorCaptionShade(view);
+                break;
+            case DECOR_CAPTION_SHADE_DARK:
+                setDarkDecorCaptionShade(view);
+                break;
+            default: {
+                TypedValue value = new TypedValue();
+                context.getTheme().resolveAttribute(R.attr.colorPrimary, value, true);
+                // We invert the shade depending on brightness of the theme. Dark shade for light
+                // theme and vice versa. Thanks to this the buttons should be visible on the
+                // background.
+                if (Color.luminance(value.data) < 0.5) {
+                    setLightDecorCaptionShade(view);
+                } else {
+                    setDarkDecorCaptionShade(view);
+                }
+                break;
+            }
+        }
+    }
+
+    void updateDecorCaptionShade() {
+        if (mDecorCaptionView != null) {
+            setDecorCaptionShade(getContext(), mDecorCaptionView);
+        }
+    }
+
+    private void setLightDecorCaptionShade(DecorCaptionView view) {
+        view.findViewById(R.id.maximize_window).setBackgroundResource(
+                R.drawable.decor_maximize_button_light);
+        view.findViewById(R.id.close_window).setBackgroundResource(
+                R.drawable.decor_close_button_light);
+    }
+
+    private void setDarkDecorCaptionShade(DecorCaptionView view) {
+        view.findViewById(R.id.maximize_window).setBackgroundResource(
+                R.drawable.decor_maximize_button_dark);
+        view.findViewById(R.id.close_window).setBackgroundResource(
+                R.drawable.decor_close_button_dark);
     }
 
     /**
@@ -1735,11 +1784,11 @@
         if (mBackdropFrameRenderer != null) {
             return;
         }
-        final ThreadedRenderer renderer = (ThreadedRenderer) getHardwareRenderer();
+        final ThreadedRenderer renderer = getHardwareRenderer();
         if (renderer != null) {
             mBackdropFrameRenderer = new BackdropFrameRenderer(this, renderer,
                     initialBounds, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
-                    getCurrentColor(mStatusColorViewState));
+                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState));
 
             // 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
@@ -1849,6 +1898,16 @@
                 getResources().getDisplayMetrics());
     }
 
+    /**
+     * Provide an override of the caption background drawable.
+     */
+    void setUserCaptionBackgroundDrawable(Drawable drawable) {
+        mUserCaptionBackgroundDrawable = drawable;
+        if (mBackdropFrameRenderer != null) {
+            mBackdropFrameRenderer.setUserCaptionBackgroundDrawable(drawable);
+        }
+    }
+
     private static class ColorViewState {
         View view = null;
         int targetVisibility = View.INVISIBLE;
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 337bb69..86bd782 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -94,7 +94,6 @@
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
 import android.widget.FrameLayout;
-import android.widget.LinearLayout;
 import android.widget.ImageView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
@@ -273,6 +272,8 @@
     private boolean mIsStartingWindow;
     private int mTheme = -1;
 
+    private int mDecorCaptionShade = DECOR_CAPTION_SHADE_AUTO;
+
     static class WindowManagerHolder {
         static final IWindowManager sWindowManager = IWindowManager.Stub.asInterface(
                 ServiceManager.getService("window"));
@@ -2527,7 +2528,7 @@
         }
 
         mDecor.startChanging();
-        final View in = mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
+        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
 
         ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
         if (contentParent == null) {
@@ -3720,4 +3721,21 @@
             }
         }
     }
+
+    @Override
+    public void setResizingCaptionDrawable(Drawable drawable) {
+        mDecor.setUserCaptionBackgroundDrawable(drawable);
+    }
+
+    @Override
+    public void setDecorCaptionShade(int decorCaptionShade) {
+        mDecorCaptionShade = decorCaptionShade;
+        if (mDecor != null) {
+            mDecor.updateDecorCaptionShade();
+        }
+    }
+
+    int getDecorCaptionShade() {
+        return mDecorCaptionShade;
+    }
 }
diff --git a/core/java/com/android/internal/widget/DecorCaptionView.java b/core/java/com/android/internal/widget/DecorCaptionView.java
index d747686..c3fe9e7 100644
--- a/core/java/com/android/internal/widget/DecorCaptionView.java
+++ b/core/java/com/android/internal/widget/DecorCaptionView.java
@@ -319,6 +319,10 @@
                         captionHeight + mContent.getMeasuredHeight());
             }
         }
+
+        // This assumes that the caption bar is at the top.
+        mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(),
+                mClose.getRight(), mClose.getBottom());
     }
     /**
      * Determine if the workspace is entirely covered by the window.