Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ed9f69d2 authored by Stefan Kuhne's avatar Stefan Kuhne Committed by Android (Google) Code Review
Browse files

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

parents 7439a666 61b47bb2
Loading
Loading
Loading
Loading
+129 −43
Original line number Diff line number Diff line
@@ -72,6 +72,7 @@ import com.android.internal.widget.ActionBarContextView;
import com.android.internal.widget.BackgroundFallback;
import com.android.internal.widget.DecorContentParent;
import com.android.internal.widget.FloatingToolbar;
import com.android.internal.widget.NonClientDecorView;
import com.android.internal.widget.SwipeDismissLayout;

import android.app.ActivityManager;
@@ -1270,7 +1271,7 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
     * @param st The panel being initialized.
     */
    protected boolean initializePanelDecor(PanelFeatureState st) {
        st.decorView = new DecorView(getContext(), st.featureId);
        st.decorView = generateDecor(st.featureId);
        st.gravity = Gravity.CENTER | Gravity.BOTTOM;
        st.setStyle(getContext());
        TypedArray a = getContext().obtainStyledAttributes(null,
@@ -2204,6 +2205,9 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {

        private final Rect mFrameOffsets = new Rect();

        // True if a non client area decor exists.
        private boolean mHasNonClientDecor = false;

        private boolean mChanging;

        private Drawable mMenuBackground;
@@ -3148,14 +3152,19 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
                return;
            }

            setPadding(mFramePadding.left + mBackgroundPadding.left, mFramePadding.top
                    + mBackgroundPadding.top, mFramePadding.right + mBackgroundPadding.right,
            setPadding(mFramePadding.left + mBackgroundPadding.left,
                    mFramePadding.top + mBackgroundPadding.top,
                    mFramePadding.right + mBackgroundPadding.right,
                    mFramePadding.bottom + mBackgroundPadding.bottom);
            requestLayout();
            invalidate();

            int opacity = PixelFormat.OPAQUE;
            // Note: if there is no background, we will assume opaque. The
            if (windowHasShadow()) {
                // If the window has a shadow, it must be translucent.
                opacity = PixelFormat.TRANSLUCENT;
            } else{
                // Note: If there is no background, we will assume opaque. The
                // common case seems to be that an application sets there to be
                // no background so it can draw everything itself. For that,
                // we would like to assume OPAQUE and let the app force it to
@@ -3191,9 +3200,10 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
                        opacity = PixelFormat.TRANSLUCENT;
                    }
                }

                if (false)
                    Log.v(TAG, "Background: " + bg + ", Frame: " + fg);
            }

            if (false)
                Log.v(TAG, "Selected default opacity: " + opacity);

@@ -3401,8 +3411,7 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
                        }
                    };
                } else {
                    ViewStub stub = (ViewStub) findViewById(
                            R.id.action_mode_bar_stub);
                    ViewStub stub = (ViewStub) findViewById(R.id.action_mode_bar_stub);
                    if (stub != null) {
                        mPrimaryActionModeView = (ActionBarContextView) stub.inflate();
                    }
@@ -3490,6 +3499,28 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
                .addOnPreDrawListener(mFloatingToolbarPreDrawListener);
        }

        // Set when the window is free floating and a non client decor frame was added.
        void enableNonClientDecor(boolean enable) {
            if (mHasNonClientDecor != enable) {
                mHasNonClientDecor = enable;
                if (getForeground() != null) {
                    drawableChanged();
                }
            }
        }

        // Returns true if the window has a non client decor.
        private boolean windowHasNonClientDecor() {
            return mHasNonClientDecor;
        }

        // Returns true if the Window is free floating and has a shadow. Note that non overlapping
        // windows do not have a shadow since it could not be seen anyways (a small screen / tablet
        // "tiles" the windows side by side but does not overlap them).
        private boolean windowHasShadow() {
            return windowHasNonClientDecor() && getElevation() > 0;
        }

        /**
         * Clears out internal references when the action mode is destroyed.
         */
@@ -3599,8 +3630,8 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
        }
    }

    protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    protected DecorView generateDecor(int featureId) {
        return new DecorView(getContext(), featureId);
    }

    protected void setFeatureFromAttrs(int featureId, TypedArray attrs,
@@ -3879,8 +3910,15 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {

        mDecor.startChanging();

        NonClientDecorView nonClientDecorView = createNonClientDecorView();
        View in = mLayoutInflater.inflate(layoutResource, null);
        if (nonClientDecorView != null) {
            decor.addView(nonClientDecorView,
                    new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            nonClientDecorView.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {
            decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) in;

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
@@ -3936,6 +3974,54 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
        return contentParent;
    }

    // Free floating overlapping windows require a non client decor with a caption and shadow..
    private NonClientDecorView createNonClientDecorView() {
        boolean needsDecor = true;
        NonClientDecorView nonClientDecorView = null;

        final WindowManager.LayoutParams attrs = getAttributes();
        // TODO(skuhne): Use the associated stack to figure out if the window is on the free style
        // desktop, the side by side desktop or the full screen desktop. With that informations the
        // choice is fairly easy to decide.
        // => This is only a kludge for now to suppress fullscreen windows, recents, launcher, etc..
        boolean isFullscreen =
                0 != ((mDecor.getWindowSystemUiVisibility() | mDecor.getSystemUiVisibility()) &
                        (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION));
        boolean isApplication = attrs.type != TYPE_BASE_APPLICATION &&
                attrs.type != TYPE_APPLICATION;

        // We do not show the non client decor if...
        // - this is a floating dialog (which is not a real window, e.g. it cannot be maximized).
        // - it is not an application (special windows have special functions, e.g text selector).
        // - the application is full screen, drawing everything (since the decor would be out of the
        //   screen in that case and could not be seen).
        if (isFloating() || isFullscreen || isApplication) {
            needsDecor = false;
        }

        if (needsDecor) {
            // TODO(skuhne): If running in side by side mode on a device - turn off the shadow.
            boolean windowHasShadow = true;
            // Dependent on the brightness of the used title we either use the
            // dark or the light button frame.
            TypedValue value = new TypedValue();
            getContext().getTheme().resolveAttribute(R.attr.colorPrimary, value, true);
            if (Color.brightness(value.data) < 0.5) {
                nonClientDecorView = (NonClientDecorView) mLayoutInflater.inflate(
                        R.layout.non_client_decor_dark, null);
            } else {
                nonClientDecorView = (NonClientDecorView) mLayoutInflater.inflate(
                        R.layout.non_client_decor_light, null);
            }
            nonClientDecorView.setPhoneWindow(this, windowHasShadow);
        }

        // Tell the Decor if it has a non client decor.
        mDecor.enableNonClientDecor(needsDecor);

        return nonClientDecorView;
    }

    /** @hide */
    public void alwaysReadCloseOnTouchAttr() {
        mAlwaysReadCloseOnTouchAttr = true;
@@ -3943,7 +4029,7 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {

    private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor();
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
+219 −0
Original line number Diff line number Diff line
/*
 * 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());
    }
}
+22 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_window_focused="true"
          android:drawable="@drawable/ic_decor_close_button_dark_focused" />
    <item android:drawable="@drawable/ic_decor_close_button_dark_unfocused" />
</selector>
+22 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_window_focused="true"
          android:drawable="@drawable/ic_decor_close_button_light_focused" />
    <item android:drawable="@drawable/ic_decor_close_button_light_unfocused" />
</selector>
+22 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_window_focused="true"
          android:drawable="@drawable/ic_decor_maximize_button_dark_focused" />
    <item android:drawable="@drawable/ic_decor_maximize_button_dark_unfocused" />
</selector>
Loading