blob: ed7492aae47be5e7695f6735d52d955490f6b8dd [file] [log] [blame]
/*
* 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.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.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;
import com.android.internal.R;
import com.android.internal.policy.PhoneWindow;
/**
* 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 LinearLayout
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;
// The height of a window which has not in DIP.
private final int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5;
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;
// True when the left mouse button got released while dragging.
private boolean mLeftMouseButtonReleased;
// True if this window is resizable (which is currently only true when the decor is shown).
public boolean mVisible = false;
// The current focus state of the window for updating the window elevation.
private 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.
private boolean mAllowUpdateElevation = false;
// The resize frame renderer.
private ResizeFrameThread mFrameRendererThread = null;
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);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
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();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAttachedCallbacksToRootViewImpl) {
getViewRootImpl().removeWindowCallbacks(this);
mAttachedCallbacksToRootViewImpl = false;
}
}
public void setPhoneWindow(PhoneWindow owner, boolean showDecor, boolean windowHasShadow) {
mOwner = owner;
mWindowHasShadow = windowHasShadow;
mShowDecor = showDecor;
updateCaptionVisibility();
if (mWindowHasShadow) {
initializeElevation();
}
// 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 boolean onTouch(View v, MotionEvent e) {
// Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch)
// the old input device events get cancelled first. So no need to remember the kind of
// input device we are listening to.
switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
if (!mShowDecor) {
// When there is no decor we should not react to anything.
return false;
}
// A drag action is started if we aren't dragging already and the starting event is
// either a left mouse button or any other input device.
if (!mDragging &&
(e.getToolType(e.getActionIndex()) != MotionEvent.TOOL_TYPE_MOUSE ||
(e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0)) {
mDragging = true;
mLeftMouseButtonReleased = false;
startMovingTask(e.getRawX(), e.getRawY());
}
break;
case MotionEvent.ACTION_MOVE:
if (mDragging && !mLeftMouseButtonReleased) {
if (e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE &&
(e.getButtonState() & MotionEvent.BUTTON_PRIMARY) == 0) {
// There is no separate mouse button up call and if the user mixes mouse
// button drag actions, we stop dragging once he releases the button.
mLeftMouseButtonReleased = true;
break;
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (!mDragging) {
break;
}
// Abort the ongoing dragging.
mDragging = false;
return true;
}
return mDragging;
}
/**
* The phone window configuration has changed and the decor needs to be updated.
* @param showDecor True if the decor should be shown.
* @param windowHasShadow True when the window should show a shadow.
**/
public void phoneWindowUpdated(boolean showDecor, boolean windowHasShadow) {
mShowDecor = showDecor;
updateCaptionVisibility();
if (windowHasShadow != mWindowHasShadow) {
mWindowHasShadow = windowHasShadow;
initializeElevation();
}
}
@Override
public void onClick(View view) {
if (view.getId() == R.id.maximize_window) {
maximizeWindow();
} else if (view.getId() == R.id.close_window) {
mOwner.dispatchOnWindowDismissed(true /*finishTask*/);
}
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
mWindowHasFocus = hasWindowFocus;
updateElevation();
super.onWindowFocusChanged(hasWindowFocus);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// If the application changed its SystemUI metrics, we might also have to adapt
// our shadow elevation.
updateElevation();
mAllowUpdateElevation = true;
super.onLayout(changed, left, top, right, bottom);
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
// Make sure that we never get more then one client area in our view.
if (index >= 2 || getChildCount() >= 2) {
throw new IllegalStateException("NonClientDecorView can only handle 1 client view");
}
super.addView(child, index, params);
}
/**
* Determine if the workspace is entirely covered by the window.
* @return Returns true when the window is filling the entire screen/workspace.
**/
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)));
}
/**
* Updates the visibility of the caption.
**/
private void updateCaptionVisibility() {
// Don't show the decor if the window has e.g. entered full screen.
boolean invisible = isFillingScreen() || !mShowDecor;
View caption = getChildAt(0);
caption.setVisibility(invisible ? GONE : VISIBLE);
caption.setOnTouchListener(this);
mVisible = !invisible;
}
/**
* The elevation gets set for the first time and the framework needs to be informed that
* the surface layer gets created with the shadow size in mind.
**/
private void initializeElevation() {
// TODO(skuhne): Call setMaxElevation here accordingly after b/22668382 got fixed.
mAllowUpdateElevation = false;
if (mWindowHasShadow) {
updateElevation();
} else {
mOwner.setElevation(0);
}
}
/**
* 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;
// 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 :
DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP);
// TODO(skuhne): Remove this if clause once b/22668382 got fixed.
if (!mAllowUpdateElevation && !fill) {
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);
}
}
/**
* Converts a DIP measure into physical pixels.
* @param dip The dip value.
* @return Returns the number of pixels.
*/
private float dipToPx(float dip) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip,
getResources().getDisplayMetrics());
}
/**
* Maximize the window by moving it to the maximized workspace stack.
**/
private void maximizeWindow() {
Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
if (callback != null) {
try {
callback.changeWindowStack(
android.app.ActivityManager.FULLSCREEN_WORKSPACE_STACK_ID);
} catch (RemoteException ex) {
Log.e(TAG, "Cannot change task workspace.");
}
}
}
@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 boolean onContentDrawn(int xOffset, int yOffset, int xSize, int ySize) {
if (mFrameRendererThread == null) {
return false;
}
return mFrameRendererThread.onContentDrawn(xOffset, yOffset, xSize, ySize);
}
@Override
public void onRequestDraw(boolean reportNextDraw) {
if (mFrameRendererThread != null) {
mFrameRendererThread.onRequsetDraw(reportNextDraw);
} else if (reportNextDraw) {
// If render thread is gone, just report immediately.
if (isAttachedToWindow()) {
getViewRootImpl().reportDrawFinish();
}
}
}
@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();
}
/**
* 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.
* TODO(b/24810450): Separate functionality from non-client-decor so that it can be used
* independently.
*/
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;
// Whether to report when next frame is drawn or not.
private boolean mReportNextDraw;
ResizeFrameThread(ThreadedRenderer renderer, Rect initialBounds) {
setName("ResizeFrame");
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);
synchronized (this) {
changeWindowSizeLocked(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() {
synchronized (this) {
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) {
synchronized (this) {
if (mRenderer == null) {
reportDrawIfNeeded();
// Tell the looper to stop. We are done.
Looper.myLooper().quit();
return;
}
mNewTargetRect.set(mTargetRect);
if (!mNewTargetRect.equals(mOldTargetRect) || mReportNextDraw) {
mOldTargetRect.set(mNewTargetRect);
changeWindowSizeLocked(mNewTargetRect);
}
}
}
/**
* The content is about to be drawn and we got the location of where it will be shown.
* If a "changeWindowSizeLocked" 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.
*/
public 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;
mRenderer.setContentDrawBounds(
mLastXOffset,
mLastYOffset + mLastCaptionHeight,
mLastXOffset + mLastContentWidth,
mLastYOffset + mLastCaptionHeight + mLastContentHeight);
// If this was the first call and changeWindowSizeLocked got already called prior
// to us, we should re-issue a changeWindowSizeLocked now.
return firstCall && (mLastCaptionHeight != 0 || !mShowDecor);
}
}
public void onRequsetDraw(boolean reportNextDraw) {
synchronized (this) {
mReportNextDraw = reportNextDraw;
mOldTargetRect.set(0, 0, 0, 0);
pingRenderLocked();
}
}
/**
* Resizing the frame to fit the new window size.
* @param newBounds The window bounds which needs to be drawn.
*/
private void changeWindowSizeLocked(Rect newBounds) {
// 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.
View caption = getChildAt(0);
if (caption != null) {
final int captionHeight = caption.getHeight();
// 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 && mShowDecor) ||
mLastContentWidth == 0 || mLastContentHeight == 0) {
return;
}
// 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);
// We need to render both rendered nodes explicitly.
mRenderer.drawRenderNode(mFrameNode);
mRenderer.drawRenderNode(mBackdropNode);
reportDrawIfNeeded();
}
/**
* Notify view root that a frame has been drawn by us, if it has requested so.
*/
private void reportDrawIfNeeded() {
if (mReportNextDraw) {
if (isAttachedToWindow()) {
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.
*/
private void pingRenderLocked() {
if (mChoreographer != null) {
mChoreographer.postFrameCallback(this);
}
}
}
}