Adding the 'non client decor view' to free floating windows

This patch adds a new free floating window non client decor view which
has:
- Two buttons (maximize and close) as it meets our current UX design
- A Shadow which changes size with focus
- It's own view as preparation for the thread distributed drawing and
  user interaction handling (resize handles, etc)

Missing is at this point:
- The button functionality itself.
- Changing/removing decors based on window type (once the needed
  patches land.

bug: 21738666
bug: 21739152
Change-Id: I6e41f2a7470256cee7bae35029d7e8c43fb235dd
diff --git a/core/java/com/android/internal/widget/NonClientDecorView.java b/core/java/com/android/internal/widget/NonClientDecorView.java
new file mode 100644
index 0000000..57039b7
--- /dev/null
+++ b/core/java/com/android/internal/widget/NonClientDecorView.java
@@ -0,0 +1,219 @@
+/*
+ * 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.widget;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ActivityManagerNative;
+import android.content.Context;
+import android.os.RemoteException;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.util.Log;
+import android.util.TypedValue;
+
+import android.view.ViewOutlineProvider;
+import android.view.WindowInsets;
+import com.android.internal.R;
+import com.android.internal.policy.PhoneWindow;
+
+import java.util.List;
+
+/**
+ * This class represents the special screen elements to control a window on free form
+ * environment. All thse screen elements are added in the "non client area" which is the area of
+ * the window which is handled by the OS and not the application.
+ * As such this class handles the following things:
+ * <ul>
+ * <li>The caption, containing the system buttons like maximize, close and such as well as
+ * allowing the user to drag the window around.</li>
+ * <li>The shadow - which is changing dependent on the window focus.</li>
+ * <li>The border around the client area (if there is one).</li>
+ * <li>The resize handles which allow to resize the window.</li>
+ * </ul>
+ * After creating the view, the function
+ * {@link #setPhoneWindow(PhoneWindow owner, boolean windowHasShadow)} needs to be called to make
+ * the connection to it's owning PhoneWindow.
+ * Note: At this time the application can change various attributes of the DecorView which
+ * will break things (in settle/unexpected ways):
+ * <ul>
+ * <li>setElevation</li>
+ * <li>setOutlineProvider</li>
+ * <li>setSurfaceFormat</li>
+ * <li>..</li>
+ * </ul>
+ * This will be mitigated once b/22527834 will be addressed.
+ */
+public class NonClientDecorView extends ViewGroup implements View.OnClickListener {
+    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;
+    // The height of a window which has not in DIP.
+    private final int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5;
+
+    private PhoneWindow mOwner = null;
+    boolean mWindowHasShadow = false;
+
+    // The current focus state of the window for updating the window elevation.
+    boolean mWindowHasFocus = true;
+
+    // Cludge to address b/22668382: Set the shadow size to the maximum so that the layer
+    // size calculation takes the shadow size into account. We set the elevation currently
+    // to max until the first layout command has been executed.
+    boolean mAllowUpdateElevation = false;
+
+    public NonClientDecorView(Context context) {
+        super(context);
+    }
+
+    public NonClientDecorView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public NonClientDecorView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public void setPhoneWindow(PhoneWindow owner, boolean windowHasShadow) {
+        mOwner = owner;
+        mWindowHasShadow = windowHasShadow;
+        if (mWindowHasShadow) {
+            // TODO(skuhne): Call setMaxElevation here as soon as b/22668382 got fixed.
+            updateElevation();
+        }
+        // By changing the outline provider to BOUNDS, the window can remove its
+        // background without removing the shadow.
+        mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
+        findViewById(R.id.maximize_window).setOnClickListener(this);
+        findViewById(R.id.close_window).setOnClickListener(this);
+    }
+
+    @Override
+    public void onClick(View view) {
+        if (view.getId() == R.id.maximize_window) {
+            // TODO(skuhne): Add code to maximize window.
+        } else if (view.getId() == R.id.close_window) {
+            // TODO(skuhne): This is not the right way to kill an app and we should add a high level
+            // function for it.
+            final ActivityManager m =
+                    (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
+            List<RunningTaskInfo> runningTaskInfoList =  m.getRunningTasks(1);
+            if (!runningTaskInfoList.isEmpty()) {
+                try {
+                    ActivityManagerNative.getDefault().removeTask(runningTaskInfoList.get(0).id);
+                } catch (RemoteException ex) {
+                    Log.e(TAG, "Couldn't close task with the close button.");
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        mWindowHasFocus = hasWindowFocus;
+        updateElevation();
+        super.onWindowFocusChanged(hasWindowFocus);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        measureChildren(widthMeasureSpec, heightMeasureSpec);
+        final int width = MeasureSpec.getSize(widthMeasureSpec);
+        final int height = MeasureSpec.getSize(heightMeasureSpec);
+        setMeasuredDimension(width, height);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        // The system inset needs only to be applied to the caption. The client area of
+        // the window will automatically be adjusted by the the DecorView.
+        WindowInsets insets = getRootWindowInsets();
+        int systemMargin = insets.getSystemWindowInsetTop();
+
+        final int leftPos = getPaddingLeft();
+        final int rightPos = right - left - getPaddingRight();
+        final int topPos = getPaddingTop();
+        final int bottomPos = bottom - top - getPaddingBottom();
+
+        // On top we have the caption which has to fill left to right with a fixed height.
+        final int width = rightPos - leftPos;
+        final View caption = getChildAt(0);
+
+        // If the application changed its SystemUI metrics, we might also have to adapt
+        // our shadow elevation.
+        updateElevation();
+        mAllowUpdateElevation = true;
+
+        // Remove the decor temporarily if the window entered a full screen/immersive mode.
+        final int captionHeight = isFillingScreen() ? 0 : caption.getMeasuredHeight();
+        caption.layout(leftPos, topPos + systemMargin, leftPos + width,
+                topPos + systemMargin + captionHeight);
+
+        // Note: We should never have more then 1 additional item in here.
+        if (getChildCount() > 1) {
+            getChildAt(1).layout(leftPos, topPos + captionHeight, leftPos + width, bottomPos);
+        }
+    }
+
+    // Make sure that we never get more then one client area in our view.
+    @Override
+    public void addView(View child, int index, LayoutParams params) {
+        if (index >= 2 || getChildCount() >= 2) {
+            throw new IllegalStateException("NonClientDecorView can only handle 1 client view");
+        }
+        super.addView(child, index, params);
+    }
+
+    // Returns true when the window is filling the entire screen and the non client area
+    // should not be shown.
+    private boolean isFillingScreen() {
+        return (0 != ((getWindowSystemUiVisibility() | getSystemUiVisibility()) &
+                (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+                        View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LOW_PROFILE)));
+    }
+
+    // The shadow height gets controlled by the focus to visualize highlighted windows.
+    // Note: This will overwrite application elevation properties.
+    // Note: Windows which have (temporarily) changed their attributes to cover the SystemUI
+    //       will get no shadow as they are expected to be "full screen".
+    private void updateElevation() {
+        float elevation = 0;
+        if (mWindowHasShadow) {
+            boolean fill = isFillingScreen();
+            elevation = fill ? 0 :
+                    (mWindowHasFocus ? DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP :
+                            DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP);
+            if (!mAllowUpdateElevation && !fill) {
+                // TODO(skuhne): Change this to setMaxElevation as soon as b/22668382 got fixed
+                // and remove this cludge.
+                elevation = DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP;
+            }
+            // Convert the DP elevation into physical pixels.
+            elevation = dipToPx(elevation);
+        }
+        // Don't change the elevation if it didn't change since it can require some time.
+        if (mOwner.getDecorView().getElevation() != elevation) {
+            mOwner.setElevation(elevation);
+        }
+    }
+
+    private float dipToPx(float dip) {
+        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip,
+                getResources().getDisplayMetrics());
+    }
+}