Render window decoration in WM shell

There are still several things missing in this CL:

1. Still need to wire up freeform resizing. This is done in a separate
   CL.
2. Check if a11y works as intended.
3. Wire up fullscreen and window decorations for freeform displays.
4. Immersive mode work.

Bug: 165794880
Bug: 165794636
Test: Caption shows up.
Change-Id: I7c0e1d55d6c9d03332a58d31ade88aef06a84048
This commit is contained in:
Garfield Tan
2021-08-04 14:17:31 -07:00
parent 3b628d7c69
commit 2d3382a8b4
20 changed files with 954 additions and 76 deletions

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 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"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item app:state_task_focused="true" android:color="#FF000000" />
<item android:color="#33000000" />
</selector>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 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"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item app:state_task_focused="true" android:color="#FFFFFFFF" />
<item android:color="#33FFFFFF" />
</selector>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 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"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- Fading the to 85% blackness -->
<item app:state_task_focused="true" android:color="#D8D8D8" />
<!-- Fading the to 95% blackness -->
<item android:color="#F2F2F2" />
</selector>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 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.
-->
<shape android:shape="rectangle"
android:tintMode="multiply"
android:tint="@color/decor_caption_title_color"
xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?android:attr/colorPrimary" />
</shape>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32.0dp"
android:height="32.0dp"
android:viewportWidth="32.0"
android:viewportHeight="32.0"
android:tint="@color/decor_button_dark_color"
>
<group android:scaleX="0.5"
android:scaleY="0.5"
android:translateX="8.0"
android:translateY="8.0" >
<path
android:fillColor="@android:color/white"
android:pathData="M6.9,4.0l-2.9,2.9 9.1,9.1 -9.1,9.200001 2.9,2.799999 9.1,-9.1 9.1,9.1 2.9,-2.799999 -9.1,-9.200001 9.1,-9.1 -2.9,-2.9 -9.1,9.2z"/>
</group>
</vector>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32.0dp"
android:height="32.0dp"
android:viewportWidth="32.0"
android:viewportHeight="32.0"
android:tint="@color/decor_button_dark_color">
<group android:scaleX="0.5"
android:scaleY="0.5"
android:translateX="8.0"
android:translateY="8.0" >
<path
android:fillColor="@android:color/white"
android:pathData="M2.0,4.0l0.0,16.0l28.0,0.0L30.0,4.0L2.0,4.0zM26.0,16.0L6.0,16.0L6.0,8.0l20.0,0.0L26.0,16.0z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M2.0,24.0l28.0,0.0l0.0,4.0l-28.0,0.0z"/>
</group>
</vector>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 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.
-->
<com.android.wm.shell.windowdecor.WindowDecorLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/caption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:background="@drawable/decor_caption_title">
<Button
android:id="@+id/maximize_window"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="5dp"
android:padding="4dp"
android:layout_gravity="center_vertical|end"
android:contentDescription="@string/maximize_button_text"
android:background="@drawable/decor_maximize_button_dark"
android:duplicateParentState="true"/>
<Button
android:id="@+id/close_window"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="5dp"
android:padding="4dp"
android:layout_gravity="center_vertical|end"
android:contentDescription="@string/close_button_text"
android:background="@drawable/decor_close_button_dark"
android:duplicateParentState="true"/>
</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>

View File

@@ -19,4 +19,8 @@
<attr name="icon" format="reference" />
<attr name="text" format="string" />
</declare-styleable>
<declare-styleable name="MessageState">
<attr name="state_task_focused" format="boolean"/>
</declare-styleable>
</resources>

View File

@@ -186,4 +186,12 @@
<!-- Button text for dismissing the letterbox education dialog. [CHAR LIMIT=20] -->
<string name="letterbox_education_got_it">Got it</string>
<!-- Accessibility description of the letterbox education toast expand to dialog button. [CHAR LIMIT=NONE] -->
<string name="letterbox_education_expand_button_description">Expand for more information.</string>
<!-- Freeform window caption strings -->
<!-- Accessibility text for the maximize window button [CHAR LIMIT=NONE] -->
<string name="maximize_button_text">Maximize</string>
<!-- Accessibility text for the close window button [CHAR LIMIT=NONE] -->
<string name="close_button_text">Close</string>
</resources>

View File

@@ -57,7 +57,7 @@ public class ShellInitImpl {
private final FullscreenTaskListener mFullscreenTaskListener;
private final Optional<FullscreenUnfoldController> mFullscreenUnfoldController;
private final Optional<UnfoldTransitionHandler> mUnfoldTransitionHandler;
private final Optional<FreeformTaskListener> mFreeformTaskListenerOptional;
private final Optional<FreeformTaskListener<?>> mFreeformTaskListenerOptional;
private final ShellExecutor mMainExecutor;
private final Transitions mTransitions;
private final StartingWindowController mStartingWindow;
@@ -78,7 +78,7 @@ public class ShellInitImpl {
FullscreenTaskListener fullscreenTaskListener,
Optional<FullscreenUnfoldController> fullscreenUnfoldTransitionController,
Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
Optional<FreeformTaskListener> freeformTaskListenerOptional,
Optional<FreeformTaskListener<?>> freeformTaskListenerOptional,
Optional<RecentTasksController> recentTasks,
Transitions transitions,
StartingWindowController startingWindow,

View File

@@ -347,12 +347,12 @@ public abstract class WMShellBaseModule {
// Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
@BindsOptionalOf
@DynamicOverride
abstract FreeformTaskListener optionalFreeformTaskListener();
abstract FreeformTaskListener<?> optionalFreeformTaskListener();
@WMSingleton
@Provides
static Optional<FreeformTaskListener> provideFreeformTaskListener(
@DynamicOverride Optional<FreeformTaskListener> freeformTaskListener,
static Optional<FreeformTaskListener<?>> provideFreeformTaskListener(
@DynamicOverride Optional<FreeformTaskListener<?>> freeformTaskListener,
Context context) {
if (FreeformTaskListener.isFreeformEnabled(context)) {
return freeformTaskListener;
@@ -642,7 +642,7 @@ public abstract class WMShellBaseModule {
FullscreenTaskListener fullscreenTaskListener,
Optional<FullscreenUnfoldController> appUnfoldTransitionController,
Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
Optional<FreeformTaskListener> freeformTaskListener,
Optional<FreeformTaskListener<?>> freeformTaskListener,
Optional<RecentTasksController> recentTasksOptional,
Transitions transitions,
StartingWindowController startingWindow,

View File

@@ -74,6 +74,8 @@ import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
import com.android.wm.shell.unfold.UnfoldBackgroundController;
import com.android.wm.shell.unfold.UnfoldTransitionHandler;
import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator;
import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import java.util.Optional;
@@ -127,6 +129,24 @@ public class WMShellModule {
taskViewTransitions, syncQueue);
}
//
// Window decoration
//
@WMSingleton
@Provides
static WindowDecorViewModel<?> provideWindowDecorViewModel(
Context context,
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
SyncTransactionQueue syncQueue) {
return new CaptionWindowDecorViewModel(
context,
taskOrganizer,
displayController,
syncQueue);
}
//
// Freeform
//
@@ -134,9 +154,10 @@ public class WMShellModule {
@WMSingleton
@Provides
@DynamicOverride
static FreeformTaskListener provideFreeformTaskListener(
static FreeformTaskListener<?> provideFreeformTaskListener(
WindowDecorViewModel<?> windowDecorViewModel,
SyncTransactionQueue syncQueue) {
return new FreeformTaskListener(syncQueue);
return new FreeformTaskListener<>(windowDecorViewModel, syncQueue);
}
//

View File

@@ -21,8 +21,6 @@ import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDO
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.provider.Settings;
import android.util.Slog;
import android.util.SparseArray;
@@ -32,26 +30,35 @@ import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import java.io.PrintWriter;
/**
* {@link ShellTaskOrganizer.TaskListener} for {@link
* ShellTaskOrganizer#TASK_LISTENER_TYPE_FREEFORM}.
*
* @param <T> the type of window decoration instance
*/
public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener {
public class FreeformTaskListener<T extends AutoCloseable>
implements ShellTaskOrganizer.TaskListener {
private static final String TAG = "FreeformTaskListener";
private final WindowDecorViewModel<T> mWindowDecorationViewModel;
private final SyncTransactionQueue mSyncQueue;
private final SparseArray<State> mTasks = new SparseArray<>();
private final SparseArray<State<T>> mTasks = new SparseArray<>();
private static class State {
private static class State<T extends AutoCloseable> {
RunningTaskInfo mTaskInfo;
SurfaceControl mLeash;
T mWindowDecoration;
}
public FreeformTaskListener(SyncTransactionQueue syncQueue) {
public FreeformTaskListener(
WindowDecorViewModel<T> windowDecorationViewModel,
SyncTransactionQueue syncQueue) {
mWindowDecorationViewModel = windowDecorationViewModel;
mSyncQueue = syncQueue;
}
@@ -62,23 +69,17 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener {
}
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Appeared: #%d",
taskInfo.taskId);
final State state = new State();
final State<T> state = new State<>();
state.mTaskInfo = taskInfo;
state.mLeash = leash;
state.mWindowDecoration =
mWindowDecorationViewModel.createWindowDecoration(taskInfo, leash);
mTasks.put(taskInfo.taskId, state);
final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds();
mSyncQueue.runInSync(t -> {
Point taskPosition = taskInfo.positionInParent;
t.setPosition(leash, taskPosition.x, taskPosition.y)
.setWindowCrop(leash, taskBounds.width(), taskBounds.height())
.show(leash);
});
}
@Override
public void onTaskVanished(RunningTaskInfo taskInfo) {
State state = mTasks.get(taskInfo.taskId);
State<T> state = mTasks.get(taskInfo.taskId);
if (state == null) {
Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId);
return;
@@ -86,11 +87,17 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Vanished: #%d",
taskInfo.taskId);
mTasks.remove(taskInfo.taskId);
try {
state.mWindowDecoration.close();
} catch (Exception e) {
Slog.e(TAG, "Failed to release window decoration.", e);
}
}
@Override
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
State state = mTasks.get(taskInfo.taskId);
State<T> state = mTasks.get(taskInfo.taskId);
if (state == null) {
throw new RuntimeException(
"Task info changed before appearing: #" + taskInfo.taskId);
@@ -98,15 +105,9 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Info Changed: #%d",
taskInfo.taskId);
state.mTaskInfo = taskInfo;
final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds();
final SurfaceControl leash = state.mLeash;
mSyncQueue.runInSync(t -> {
Point taskPosition = taskInfo.positionInParent;
t.setPosition(leash, taskPosition.x, taskPosition.y)
.setWindowCrop(leash, taskBounds.width(), taskBounds.height())
.show(leash);
});
mWindowDecorationViewModel.onTaskInfoChanged(state.mTaskInfo, state.mWindowDecoration);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Info Changed: #%d",
taskInfo.taskId);
}
@Override

View File

@@ -0,0 +1,136 @@
/*
* Copyright (C) 2022 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.wm.shell.windowdecor;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityTaskManager;
import android.content.Context;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.View;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
/**
* View model for the window decoration with a caption and shadows. Works with
* {@link CaptionWindowDecoration}.
*/
public class CaptionWindowDecorViewModel implements WindowDecorViewModel<CaptionWindowDecoration> {
private final ActivityTaskManager mActivityTaskManager;
private final ShellTaskOrganizer mTaskOrganizer;
private final Context mContext;
private final DisplayController mDisplayController;
private final SyncTransactionQueue mSyncQueue;
public CaptionWindowDecorViewModel(
Context context,
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
SyncTransactionQueue syncQueue) {
mContext = context;
mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
mTaskOrganizer = taskOrganizer;
mDisplayController = displayController;
mSyncQueue = syncQueue;
}
@Override
public CaptionWindowDecoration createWindowDecoration(
ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface) {
final CaptionWindowDecoration windowDecoration = new CaptionWindowDecoration(
mContext,
mDisplayController,
mTaskOrganizer,
taskInfo,
taskSurface,
mSyncQueue);
CaptionTouchEventListener touchEventListener = new CaptionTouchEventListener(taskInfo);
windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
onTaskInfoChanged(taskInfo, windowDecoration);
return windowDecoration;
}
@Override
public void onTaskInfoChanged(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) {
decoration.relayout(taskInfo);
int statusBarColor = taskInfo.taskDescription.getStatusBarColor();
decoration.setCaptionColor(statusBarColor);
}
private class CaptionTouchEventListener implements
View.OnClickListener, View.OnTouchListener {
private final int mTaskId;
private final WindowContainerToken mTaskToken;
private CaptionTouchEventListener(RunningTaskInfo taskInfo) {
mTaskId = taskInfo.taskId;
mTaskToken = taskInfo.token;
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.close_window) {
mActivityTaskManager.removeTask(mTaskId);
} else if (id == R.id.maximize_window) {
WindowContainerTransaction wct = new WindowContainerTransaction();
RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
int targetWindowingMode = taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN
? WINDOWING_MODE_FULLSCREEN : WINDOWING_MODE_FREEFORM;
int displayWindowingMode =
taskInfo.configuration.windowConfiguration.getDisplayWindowingMode();
wct.setWindowingMode(mTaskToken,
targetWindowingMode == displayWindowingMode
? WINDOWING_MODE_UNDEFINED : targetWindowingMode);
if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) {
wct.setBounds(mTaskToken, null);
}
mSyncQueue.queue(wct);
}
}
@Override
public boolean onTouch(View v, MotionEvent e) {
if (v.getId() != R.id.caption) {
return false;
}
if (e.getAction() != MotionEvent.ACTION_DOWN) {
return false;
}
RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
if (taskInfo.isFocused) {
return false;
}
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reorder(mTaskToken, true /* onTop */);
mSyncQueue.queue(wct);
return true;
}
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2022 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.wm.shell.windowdecor;
import android.app.ActivityManager;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.VectorDrawable;
import android.view.SurfaceControl;
import android.view.View;
import android.window.WindowContainerTransaction;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
/**
* Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with
* {@link CaptionWindowDecorViewModel}. The caption bar contains maximize and close buttons.
*
* {@link CaptionWindowDecorViewModel} can change the color of the caption bar based on the foremost
* app's request through {@link #setCaptionColor(int)}, in which it changes the foreground color of
* caption buttons according to the luminance of the background.
*
* The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't.
*/
public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
// The thickness of shadows of a window that has focus in DIP.
private static final int DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP = 20;
// The thickness of shadows of a window that doesn't have focus in DIP.
private static final int DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP = 5;
private static final int DECOR_CAPTION_HEIGHT_IN_DIP = 42;
private static final Rect EMPTY_OUTSET = new Rect();
private final SyncTransactionQueue mSyncQueue;
private View.OnClickListener mOnCaptionButtonClickListener;
private View.OnTouchListener mOnCaptionTouchListener;
private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
new WindowDecoration.RelayoutResult<>();
CaptionWindowDecoration(
Context context,
DisplayController displayController,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
SyncTransactionQueue syncQueue) {
super(context, displayController, taskOrganizer, taskInfo, taskSurface);
mSyncQueue = syncQueue;
}
void setCaptionListeners(
View.OnClickListener onCaptionButtonClickListener,
View.OnTouchListener onCaptionTouchListener) {
mOnCaptionButtonClickListener = onCaptionButtonClickListener;
mOnCaptionTouchListener = onCaptionTouchListener;
}
void relayout(ActivityManager.RunningTaskInfo taskInfo) {
final int shadowRadiusDp = taskInfo.isFocused
? DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP : DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP;
WindowDecorLinearLayout oldRootView = mResult.mRootView;
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
final WindowContainerTransaction wct = new WindowContainerTransaction();
relayout(taskInfo, R.layout.caption_window_decoration, oldRootView,
DECOR_CAPTION_HEIGHT_IN_DIP, EMPTY_OUTSET, shadowRadiusDp, t, wct, mResult);
taskInfo = null; // Clear it just in case we use it accidentally
mSyncQueue.runInSync(transaction -> {
transaction.merge(t);
t.close();
mTaskOrganizer.applyTransaction(wct);
});
if (mResult.mRootView == null) {
return;
}
if (oldRootView != mResult.mRootView) {
setupRootView();
}
}
/**
* Sets up listeners when a new root view is created.
*/
private void setupRootView() {
View caption = mResult.mRootView.findViewById(R.id.caption);
caption.setOnTouchListener(mOnCaptionTouchListener);
View maximize = caption.findViewById(R.id.maximize_window);
maximize.setOnClickListener(mOnCaptionButtonClickListener);
View close = caption.findViewById(R.id.close_window);
close.setOnClickListener(mOnCaptionButtonClickListener);
}
void setCaptionColor(int captionColor) {
if (mResult.mRootView == null) {
return;
}
View caption = mResult.mRootView.findViewById(R.id.caption);
GradientDrawable captionDrawable = (GradientDrawable) caption.getBackground();
captionDrawable.setColor(captionColor);
int buttonTintColorRes =
Color.valueOf(captionColor).luminance() < 0.5
? R.color.decor_button_light_color
: R.color.decor_button_dark_color;
ColorStateList buttonTintColor =
caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */);
View maximize = caption.findViewById(R.id.maximize_window);
VectorDrawable maximizeBackground = (VectorDrawable) maximize.getBackground();
maximizeBackground.setTintList(buttonTintColor);
View close = caption.findViewById(R.id.close_window);
VectorDrawable closeBackground = (VectorDrawable) close.getBackground();
closeBackground.setTintList(buttonTintColor);
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (C) 2022 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.wm.shell.windowdecor;
interface TaskFocusStateConsumer {
void setTaskFocusState(boolean focused);
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2022 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.wm.shell.windowdecor;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import com.android.wm.shell.R;
/**
* A {@link LinearLayout} that takes an additional task focused drawable state. The new state is
* used to select the correct background color for views in the window decoration.
*/
public class WindowDecorLinearLayout extends LinearLayout implements TaskFocusStateConsumer {
private static final int[] TASK_FOCUSED_STATE = { R.attr.state_task_focused };
private boolean mIsTaskFocused;
public WindowDecorLinearLayout(Context context) {
super(context);
}
public WindowDecorLinearLayout(Context context,
@Nullable AttributeSet attrs) {
super(context, attrs);
}
public WindowDecorLinearLayout(Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public WindowDecorLinearLayout(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void setTaskFocusState(boolean focused) {
mIsTaskFocused = focused;
refreshDrawableState();
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
if (!mIsTaskFocused) {
return super.onCreateDrawableState(extraSpace);
}
final int[] states = super.onCreateDrawableState(extraSpace + 1);
mergeDrawableStates(states, TASK_FOCUSED_STATE);
return states;
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2022 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.wm.shell.windowdecor;
import android.app.ActivityManager;
import android.view.SurfaceControl;
/**
* The interface used by some {@link com.android.wm.shell.ShellTaskOrganizer.TaskListener} to help
* customize {@link WindowDecoration}. Its implementations are responsible to interpret user's
* interactions with UI widgets in window decorations and send corresponding requests to system
* servers.
*
* @param <T> The actual decoration type
*/
public interface WindowDecorViewModel<T extends AutoCloseable> {
/**
* Creates a window decoration for the given task.
*
* @param taskInfo the initial task info of the task
* @param taskSurface the surface of the task
* @return the window decoration object
*/
T createWindowDecoration(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface);
/**
* Notifies a task info update on the given task, with the window decoration created previously
* for this task by {@link #createWindowDecoration}.
*
* @param taskInfo the new task info of the task
* @param windowDecoration the window decoration created for the task
*/
void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo, T windowDecoration);
}

View File

@@ -0,0 +1,265 @@
/*
* Copyright (C) 2022 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.wm.shell.windowdecor;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.InsetsState;
import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
import android.window.WindowContainerTransaction;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
/**
* Manages a container surface and a windowless window to show window decoration. Responsible to
* update window decoration window state and layout parameters on task info changes and so that
* window decoration is in correct state and bounds.
*
* The container surface is a child of the task display area in the same display, so that window
* decorations can be drawn out of the task bounds and receive input events from out of the task
* bounds to support drag resizing.
*
* The windowless window that hosts window decoration is positioned in front of all activities, to
* allow the foreground activity to draw its own background behind window decorations, such as
* the window captions.
*
* @param <T> The type of the root view
*/
public class WindowDecoration<T extends View & TaskFocusStateConsumer> implements AutoCloseable {
private static final int[] CAPTION_INSETS_TYPES = { InsetsState.ITYPE_CAPTION_BAR };
/**
* System-wide context. Only used to create context with overridden configurations.
*/
final Context mContext;
final DisplayController mDisplayController;
final ShellTaskOrganizer mTaskOrganizer;
RunningTaskInfo mTaskInfo;
final SurfaceControl mTaskSurface;
Display mDisplay;
Context mDecorWindowContext;
SurfaceControl mDecorationContainerSurface;
SurfaceControl mTaskBackgroundSurface;
private CaptionWindowManager mCaptionWindowManager;
private SurfaceControlViewHost mViewHost;
private final Rect mCaptionInsetsRect = new Rect();
private final Rect mTaskSurfaceCrop = new Rect();
private final float[] mTmpColor = new float[3];
WindowDecoration(
Context context,
DisplayController displayController,
ShellTaskOrganizer taskOrganizer,
RunningTaskInfo taskInfo,
SurfaceControl taskSurface) {
mContext = context;
mDisplayController = displayController;
mTaskOrganizer = taskOrganizer;
mTaskInfo = taskInfo;
mTaskSurface = taskSurface;
mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId);
mDecorWindowContext = mContext.createConfigurationContext(mTaskInfo.getConfiguration());
// Put caption under task surface because ViewRootImpl sets the destination frame of
// windowless window layers and BLASTBufferQueue#update() doesn't support offset.
mCaptionWindowManager =
new CaptionWindowManager(mTaskInfo.getConfiguration(), mTaskSurface);
}
void relayout(RunningTaskInfo taskInfo, int layoutResId, T rootView, float captionHeightDp,
Rect outsetsDp, float shadowRadiusDp, SurfaceControl.Transaction t,
WindowContainerTransaction wct, RelayoutResult<T> outResult) {
outResult.reset();
final Configuration oldTaskConfig = mTaskInfo.getConfiguration();
if (taskInfo != null) {
mTaskInfo = taskInfo;
}
if (!mTaskInfo.isVisible) {
close();
t.hide(mTaskSurface);
return;
}
if (rootView == null && layoutResId == 0) {
throw new IllegalArgumentException("layoutResId and rootView can't both be invalid.");
}
outResult.mRootView = rootView;
rootView = null; // Clear it just in case we use it accidentally
final Configuration taskConfig = mTaskInfo.getConfiguration();
if (oldTaskConfig.densityDpi != taskConfig.densityDpi
|| mDisplay.getDisplayId() != mTaskInfo.displayId) {
close();
mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId);
mDecorWindowContext = mContext.createConfigurationContext(taskConfig);
if (layoutResId != 0) {
outResult.mRootView =
(T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null);
}
}
if (outResult.mRootView == null) {
outResult.mRootView =
(T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null);
}
// DecorationContainerSurface
if (mDecorationContainerSurface == null) {
final SurfaceControl.Builder builder = new SurfaceControl.Builder();
mDecorationContainerSurface = builder
.setName("Decor container of Task=" + mTaskInfo.taskId)
.setContainerLayer()
.setParent(mTaskSurface)
.build();
t.setTrustedOverlay(mDecorationContainerSurface, true);
}
final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
outResult.mDensity = taskConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
final int decorContainerOffsetX = -(int) (outsetsDp.left * outResult.mDensity);
final int decorContainerOffsetY = -(int) (outsetsDp.top * outResult.mDensity);
outResult.mWidth = taskBounds.width()
+ (int) (outsetsDp.right * outResult.mDensity)
- decorContainerOffsetX;
outResult.mHeight = taskBounds.height()
+ (int) (outsetsDp.bottom * outResult.mDensity)
- decorContainerOffsetY;
t.setPosition(mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY)
.setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight)
.setLayer(mDecorationContainerSurface, mTaskInfo.numActivities + 1)
.show(mDecorationContainerSurface);
// TaskBackgroundSurface
if (mTaskBackgroundSurface == null) {
final SurfaceControl.Builder builder = new SurfaceControl.Builder();
mTaskBackgroundSurface = builder
.setName("Background of Task=" + mTaskInfo.taskId)
.setEffectLayer()
.setParent(mTaskSurface)
.build();
}
float shadowRadius = outResult.mDensity * shadowRadiusDp;
int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor();
mTmpColor[0] = Color.red(backgroundColorInt);
mTmpColor[1] = Color.green(backgroundColorInt);
mTmpColor[2] = Color.blue(backgroundColorInt);
t.setCrop(mTaskBackgroundSurface, taskBounds)
.setShadowRadius(mTaskBackgroundSurface, shadowRadius)
.setColor(mTaskBackgroundSurface, mTmpColor);
// Caption view
mCaptionWindowManager.setConfiguration(taskConfig);
final int captionHeight = (int) Math.ceil(captionHeightDp * outResult.mDensity);
final WindowManager.LayoutParams lp =
new WindowManager.LayoutParams(taskBounds.width(), captionHeight,
WindowManager.LayoutParams.TYPE_APPLICATION,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
lp.setTitle("Caption of Task=" + mTaskInfo.taskId);
lp.setTrustedOverlay();
if (mViewHost == null) {
mViewHost = new SurfaceControlViewHost(mDecorWindowContext, mDisplay,
mCaptionWindowManager, true);
mViewHost.setView(outResult.mRootView, lp);
} else {
mViewHost.relayout(lp);
}
outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused);
// Caption insets
mCaptionInsetsRect.set(taskBounds);
mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + captionHeight;
wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, CAPTION_INSETS_TYPES);
// Task surface itself
Point taskPosition = mTaskInfo.positionInParent;
mTaskSurfaceCrop.set(
decorContainerOffsetX,
decorContainerOffsetY,
outResult.mWidth + decorContainerOffsetX,
outResult.mHeight + decorContainerOffsetY);
t.setPosition(mTaskSurface, taskPosition.x, taskPosition.y)
.setCrop(mTaskSurface, mTaskSurfaceCrop)
.show(mTaskSurface);
}
@Override
public void close() {
if (mViewHost != null) {
mViewHost.release();
mViewHost = null;
}
if (mDecorationContainerSurface != null) {
mDecorationContainerSurface.release();
mDecorationContainerSurface = null;
}
if (mTaskBackgroundSurface != null) {
mTaskBackgroundSurface.release();
mTaskBackgroundSurface = null;
}
}
static class RelayoutResult<T extends View & TaskFocusStateConsumer> {
int mWidth;
int mHeight;
float mDensity;
T mRootView;
void reset() {
mWidth = 0;
mHeight = 0;
mDensity = 0;
mRootView = null;
}
}
private static class CaptionWindowManager extends WindowlessWindowManager {
CaptionWindowManager(Configuration config, SurfaceControl rootSurface) {
super(config, rootSurface, null /* hostInputToken */);
}
@Override
public void setConfiguration(Configuration configuration) {
super.setConfiguration(configuration);
}
}
}

View File

@@ -69,8 +69,6 @@ import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
import static com.android.internal.policy.DecorView.DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP;
import static com.android.internal.policy.DecorView.DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BACK_PREVIEW;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_LOCKTASK;
@@ -131,7 +129,6 @@ import static com.android.server.wm.WindowContainerChildProto.TASK;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ROOT_TASK;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_MOVEMENT;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.WindowManagerService.dipToPixel;
import static java.lang.Integer.MAX_VALUE;
@@ -3277,7 +3274,6 @@ class Task extends TaskFragment {
}
final SurfaceControl.Transaction t = getSyncTransaction();
updateShadowsRadius(isFocused(), t);
if (mDimmer.updateDims(t, mTmpDimBoundsRect)) {
scheduleAnimation();
@@ -4248,49 +4244,11 @@ class Task extends TaskFragment {
return getActivity(ActivityRecord::isVisible) != null;
}
/**
* @return the desired shadow radius in pixels for the current task.
*/
private float getShadowRadius(boolean taskIsFocused) {
int elevation = 0;
// Get elevation for a specific windowing mode.
if (inFreeformWindowingMode()) {
elevation = taskIsFocused
? DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP : DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP;
} else {
// For all other windowing modes, do not draw a shadow.
return 0;
}
// If the task has no visible children, do not draw a shadow.
if (!hasVisibleChildren()) {
return 0;
}
return dipToPixel(elevation, getDisplayContent().getDisplayMetrics());
}
/**
* Update the length of the shadow if needed based on windowing mode and task focus state.
*/
private void updateShadowsRadius(boolean taskIsFocused,
SurfaceControl.Transaction pendingTransaction) {
if (!isRootTask()) return;
final float newShadowRadius = getShadowRadius(taskIsFocused);
if (mShadowRadius != newShadowRadius) {
mShadowRadius = newShadowRadius;
pendingTransaction.setShadowRadius(getSurfaceControl(), mShadowRadius);
}
}
/**
* Called on the task when it gained or lost focus.
* @param hasFocus
*/
void onAppFocusChanged(boolean hasFocus) {
updateShadowsRadius(hasFocus, getSyncTransaction());
dispatchTaskInfoChangedIfNeeded(false /* force */);
}