mirror of
https://github.com/alesimula/Murine-launcher.git
synced 2026-10-28 09:43:42 +00:00
update
This commit is contained in:
1 parent
40f4d97dc0
commit
7dc4595b5c
91 files changed
+10180
No files matched your search
@@ -0,0 +1,95 @@
|
|||||||
|
// Copyright (C) 2024 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 {
|
||||||
|
// See: http://go/android-license-faq
|
||||||
|
// A large-scale-change added 'default_applicable_licenses' to import
|
||||||
|
// all of the 'license_kinds' from "frameworks_base_license"
|
||||||
|
// to get the below license kinds:
|
||||||
|
// SPDX-license-identifier-Apache-2.0
|
||||||
|
default_applicable_licenses: ["frameworks_base_license"],
|
||||||
|
default_team: "trendy_team_multitasking_windowing",
|
||||||
|
}
|
||||||
|
|
||||||
|
java_library_static {
|
||||||
|
name: "wm_shell-shared-utils",
|
||||||
|
srcs: [
|
||||||
|
"src/com/android/wm/shell/shared/TransitionUtil.java",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
filegroup {
|
||||||
|
name: "wm_shell-shared-aidls",
|
||||||
|
|
||||||
|
srcs: [
|
||||||
|
"**/*.aidl",
|
||||||
|
],
|
||||||
|
|
||||||
|
path: "src",
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This shared lib is built with various apps and should not
|
||||||
|
// contain resources that can be overlaid, as they would need
|
||||||
|
// to be overlaid in each app individually.
|
||||||
|
android_library {
|
||||||
|
name: "WindowManager-Shell-shared",
|
||||||
|
|
||||||
|
resource_dirs: [
|
||||||
|
"res",
|
||||||
|
],
|
||||||
|
srcs: [
|
||||||
|
"**/*.java",
|
||||||
|
"**/*.kt",
|
||||||
|
":wm_shell-shared-aidls",
|
||||||
|
],
|
||||||
|
static_libs: [
|
||||||
|
"androidx.core_core-animation",
|
||||||
|
"androidx.dynamicanimation_dynamicanimation",
|
||||||
|
"com_android_wm_shell_flags_lib",
|
||||||
|
"jsr330",
|
||||||
|
],
|
||||||
|
kotlincflags: ["-Xjvm-default=all"],
|
||||||
|
use_resource_processor: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This shared lib is built with various apps and should not
|
||||||
|
// contain resources that can be overlaid, as they would need
|
||||||
|
// to be overlaid in each app individually.
|
||||||
|
java_library {
|
||||||
|
name: "WindowManager-Shell-shared-desktopMode",
|
||||||
|
|
||||||
|
srcs: [
|
||||||
|
"**/desktopmode/*.java",
|
||||||
|
"**/desktopmode/*.kt",
|
||||||
|
],
|
||||||
|
static_libs: [
|
||||||
|
"WindowManager-Shell-shared-AOSP",
|
||||||
|
"com.android.window.flags.window-aconfig-java",
|
||||||
|
"wm_shell-shared-utils",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Things that can be shared with launcher3
|
||||||
|
java_library {
|
||||||
|
name: "WindowManager-Shell-shared-AOSP",
|
||||||
|
|
||||||
|
sdk_version: "current",
|
||||||
|
|
||||||
|
srcs: [
|
||||||
|
"src/com/android/wm/shell/shared/bubbles/BubbleAnythingFlagHelper.java",
|
||||||
|
],
|
||||||
|
static_libs: [
|
||||||
|
"com_android_wm_shell_flags_lib",
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2024 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.
|
||||||
|
-->
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.android.wm.shell.shared">
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("kotlin-android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.android.wm.shell.shared"
|
||||||
|
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 26
|
||||||
|
targetSdk = 36
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
}
|
||||||
|
sourceSets {
|
||||||
|
named("main") {
|
||||||
|
java.setSrcDirs(listOf("src"))
|
||||||
|
manifest.srcFile("AndroidManifest.xml")
|
||||||
|
res.setSrcDirs(listOf("res"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)//1.8(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("androidx.core:core:1.16.0")
|
||||||
|
implementation("androidx.annotation:annotation:1.9.1")
|
||||||
|
implementation("androidx.core:core-animation:1.0.0")
|
||||||
|
implementation("androidx.annotation:annotation:1.7.0")
|
||||||
|
implementation("androidx.dynamicanimation:dynamicanimation-ktx:1.1.0")
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright (C) 2025 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:androidprv="http://schemas.android.com/apk/prv/res/android">
|
||||||
|
<item android:alpha="0.35" android:color="@androidprv:color/materialColorPrimaryContainer" />
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright (C) 2025 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 xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:radius="28dp" />
|
||||||
|
<solid android:color="@color/bubble_drop_target_background_color" />
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="@androidprv:color/materialColorPrimaryContainer" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (C) 2024 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
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="@android:color/system_primary_fixed" />
|
||||||
|
|
||||||
|
<solid android:color="@android:color/system_primary_fixed" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (C) 2024 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="32dp"
|
||||||
|
android:height="32dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z"
|
||||||
|
android:fillColor="@android:color/system_on_primary_fixed"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2024 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.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright (C) 2024 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
<dimen name="floating_dismiss_icon_size">32dp</dimen>
|
||||||
|
<dimen name="floating_dismiss_background_size">96dp</dimen>
|
||||||
|
|
||||||
|
<!-- Bubble drag zone dimensions -->
|
||||||
|
<dimen name="drag_zone_dismiss_fold">140dp</dimen>
|
||||||
|
<dimen name="drag_zone_dismiss_tablet">200dp</dimen>
|
||||||
|
<dimen name="drag_zone_bubble_fold">140dp</dimen>
|
||||||
|
<dimen name="drag_zone_bubble_tablet">200dp</dimen>
|
||||||
|
<dimen name="drag_zone_full_screen_width">512dp</dimen>
|
||||||
|
<dimen name="drag_zone_full_screen_height">44dp</dimen>
|
||||||
|
<dimen name="drag_zone_desktop_window_width">880dp</dimen>
|
||||||
|
<dimen name="drag_zone_desktop_window_height">300dp</dimen>
|
||||||
|
<dimen name="drag_zone_desktop_window_expanded_view_width">200dp</dimen>
|
||||||
|
<dimen name="drag_zone_desktop_window_expanded_view_height">350dp</dimen>
|
||||||
|
<dimen name="drag_zone_split_from_bubble_height">100dp</dimen>
|
||||||
|
<dimen name="drag_zone_split_from_bubble_width">60dp</dimen>
|
||||||
|
<dimen name="drag_zone_h_split_from_app_width_fold">140dp</dimen>
|
||||||
|
<dimen name="drag_zone_h_split_from_expanded_view_width">60dp</dimen>
|
||||||
|
<dimen name="drag_zone_v_split_from_expanded_view_width">200dp</dimen>
|
||||||
|
<dimen name="drag_zone_v_split_from_expanded_view_height_tablet">285dp</dimen>
|
||||||
|
<dimen name="drag_zone_v_split_from_expanded_view_height_fold_tall">150dp</dimen>
|
||||||
|
<dimen name="drag_zone_v_split_from_expanded_view_height_fold_short">100dp</dimen>
|
||||||
|
|
||||||
|
<!-- Bubble drop target dimensions -->
|
||||||
|
<dimen name="drop_target_elevation">2dp</dimen>
|
||||||
|
<dimen name="drop_target_radius">28dp</dimen>
|
||||||
|
<dimen name="drop_target_stroke">2dp</dimen>
|
||||||
|
<dimen name="drop_target_full_screen_padding">20dp</dimen>
|
||||||
|
<dimen name="drop_target_desktop_window_padding_small">100dp</dimen>
|
||||||
|
<dimen name="drop_target_desktop_window_padding_large">130dp</dimen>
|
||||||
|
<dimen name="drop_target_expanded_view_width">330</dimen>
|
||||||
|
<dimen name="drop_target_expanded_view_height">578</dimen>
|
||||||
|
<dimen name="drop_target_expanded_view_padding_bottom">108</dimen>
|
||||||
|
<dimen name="drop_target_expanded_view_padding_horizontal">24</dimen>
|
||||||
|
|
||||||
|
<!-- The corner radius of freeform tasks in desktop windowing. -->
|
||||||
|
<dimen name="desktop_windowing_freeform_rounded_corner_radius">16dp</dimen>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2024 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.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<!-- Accessibility text for the icons generated by the Manage Windows submenu
|
||||||
|
in desktop mode and taskbar. [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="manage_windows_icon_text">Open Window %1$d</string>
|
||||||
|
</resources>
|
||||||
File renamed without changes.
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 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.shared;
|
||||||
|
|
||||||
|
import android.graphics.Point;
|
||||||
|
import android.util.RotationUtils;
|
||||||
|
import android.view.SurfaceControl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class that takes care of rotating unchanging child-surfaces to match the parent rotation
|
||||||
|
* during a transition animation. This gives the illusion that the child surfaces haven't rotated
|
||||||
|
* relative to the screen.
|
||||||
|
*/
|
||||||
|
public class CounterRotator {
|
||||||
|
private SurfaceControl mSurface = null;
|
||||||
|
|
||||||
|
/** Gets the surface with the counter-rotation. */
|
||||||
|
public SurfaceControl getSurface() {
|
||||||
|
return mSurface;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up this rotator.
|
||||||
|
*
|
||||||
|
* @param rotateDelta is the forward rotation change (the rotation the display is making).
|
||||||
|
* @param parentW (and H) Is the size of the rotating parent after the rotation.
|
||||||
|
*/
|
||||||
|
public void setup(SurfaceControl.Transaction t, SurfaceControl parent, int rotateDelta,
|
||||||
|
float parentW, float parentH) {
|
||||||
|
if (rotateDelta == 0) return;
|
||||||
|
mSurface = new SurfaceControl.Builder()
|
||||||
|
.setName("Transition Unrotate")
|
||||||
|
.setContainerLayer()
|
||||||
|
.setParent(parent)
|
||||||
|
.build();
|
||||||
|
// Rotate forward to match the new rotation (rotateDelta is the forward rotation the parent
|
||||||
|
// already took). Child surfaces will be in the old rotation relative to the new parent
|
||||||
|
// rotation, so we need to forward-rotate the child surfaces to match.
|
||||||
|
RotationUtils.rotateSurface(t, mSurface, rotateDelta);
|
||||||
|
final Point tmpPt = new Point(0, 0);
|
||||||
|
// parentW/H are the size in the END rotation, the rotation utilities expect the starting
|
||||||
|
// size. So swap them if necessary
|
||||||
|
if ((rotateDelta % 2) != 0) {
|
||||||
|
final float w = parentW;
|
||||||
|
parentW = parentH;
|
||||||
|
parentH = w;
|
||||||
|
}
|
||||||
|
RotationUtils.rotatePoint(tmpPt, rotateDelta, (int) parentW, (int) parentH);
|
||||||
|
t.setPosition(mSurface, tmpPt.x, tmpPt.y);
|
||||||
|
t.show(mSurface);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a surface that needs to be counter-rotate.
|
||||||
|
*/
|
||||||
|
public void addChild(SurfaceControl.Transaction t, SurfaceControl child) {
|
||||||
|
if (mSurface == null) return;
|
||||||
|
t.reparent(child, mSurface);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean-up. Since finishTransaction should reset all change leashes, we only need to remove the
|
||||||
|
* counter rotation surface.
|
||||||
|
*/
|
||||||
|
public void cleanUp(SurfaceControl.Transaction finishTransaction) {
|
||||||
|
if (mSurface == null) return;
|
||||||
|
finishTransaction.remove(mSurface);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared;
|
||||||
|
|
||||||
|
import com.android.wm.shell.shared.annotations.ExternalThread;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener to get focus-related transition callbacks.
|
||||||
|
*/
|
||||||
|
@ExternalThread
|
||||||
|
public interface FocusTransitionListener {
|
||||||
|
/**
|
||||||
|
* Called when a transition changes the top, focused display.
|
||||||
|
*/
|
||||||
|
default void onFocusedDisplayChanged(int displayId) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the per-app or system-wide focus state has changed for a task.
|
||||||
|
*/
|
||||||
|
default void onFocusedTaskChanged(int taskId, boolean isFocusedOnDisplay,
|
||||||
|
boolean isFocusedGlobally) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared;
|
||||||
|
|
||||||
|
parcelable GroupedTaskInfo;
|
||||||
@@ -0,0 +1,446 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared;
|
||||||
|
|
||||||
|
import static android.app.WindowConfiguration.windowingModeToString;
|
||||||
|
import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
|
||||||
|
import static android.view.Display.INVALID_DISPLAY;
|
||||||
|
|
||||||
|
import android.annotation.IntDef;
|
||||||
|
import android.app.ActivityManager.RecentTaskInfo;
|
||||||
|
import android.app.TaskInfo;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.android.wm.shell.shared.split.SplitBounds;
|
||||||
|
|
||||||
|
import kotlin.collections.CollectionsKt;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple container for recent tasks which should be presented as a single task within the
|
||||||
|
* Overview UI.
|
||||||
|
*/
|
||||||
|
public class GroupedTaskInfo implements Parcelable {
|
||||||
|
|
||||||
|
public static final int TYPE_FULLSCREEN = 1;
|
||||||
|
public static final int TYPE_SPLIT = 2;
|
||||||
|
public static final int TYPE_DESK = 3;
|
||||||
|
public static final int TYPE_MIXED = 4;
|
||||||
|
|
||||||
|
@IntDef(prefix = {"TYPE_"}, value = {
|
||||||
|
TYPE_FULLSCREEN,
|
||||||
|
TYPE_SPLIT,
|
||||||
|
TYPE_DESK,
|
||||||
|
TYPE_MIXED
|
||||||
|
})
|
||||||
|
public @interface GroupType {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the desk that this `GroupedTaskInfo` represents (when the type is `TYPE_DESK`). The
|
||||||
|
* value is -1 if this is not a desk.
|
||||||
|
*/
|
||||||
|
private final int mDeskId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the display that desk with [mDeskId] is in.
|
||||||
|
*/
|
||||||
|
private final int mDeskDisplayId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of this particular task info, can be one of TYPE_FULLSCREEN, TYPE_SPLIT or
|
||||||
|
* TYPE_DESK.
|
||||||
|
*/
|
||||||
|
@GroupType
|
||||||
|
protected final int mType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of tasks associated with this single recent task info.
|
||||||
|
* TYPE_FULLSCREEN: Contains the stack of tasks associated with a single "task" in overview
|
||||||
|
* TYPE_SPLIT: Contains the two split roots of each side
|
||||||
|
* TYPE_DESK: Contains the set of tasks currently in freeform mode contained in desk.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected final List<TaskInfo> mTasks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only set for TYPE_SPLIT.
|
||||||
|
*
|
||||||
|
* Information about the split bounds.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected final SplitBounds mSplitBounds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only set for TYPE_DESK.
|
||||||
|
*
|
||||||
|
* TODO(b/348332802): move isMinimized inside each Task object instead once we have a
|
||||||
|
* replacement for RecentTaskInfo
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected final int[] mMinimizedTaskIds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only set for TYPE_MIXED.
|
||||||
|
*
|
||||||
|
* The mixed set of task infos in this group.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected final List<GroupedTaskInfo> mGroupedTasks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new for a stack of fullscreen tasks
|
||||||
|
*/
|
||||||
|
public static GroupedTaskInfo forFullscreenTasks(@NonNull TaskInfo task) {
|
||||||
|
return new GroupedTaskInfo(/* deskId = */ -1, /* displayId = */ INVALID_DISPLAY,
|
||||||
|
List.of(task), null,
|
||||||
|
TYPE_FULLSCREEN, /* minimizedFreeformTaskIds = */ null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new for a pair of tasks in split screen
|
||||||
|
*/
|
||||||
|
public static GroupedTaskInfo forSplitTasks(@NonNull TaskInfo task1,
|
||||||
|
@NonNull TaskInfo task2, @Nullable SplitBounds splitBounds) {
|
||||||
|
return new GroupedTaskInfo(/* deskId = */ -1, /* displayId = */ INVALID_DISPLAY,
|
||||||
|
List.of(task1, task2),
|
||||||
|
splitBounds, TYPE_SPLIT, /* minimizedFreeformTaskIds = */ null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new for a group of freeform tasks that belong to a single desk.
|
||||||
|
*/
|
||||||
|
public static GroupedTaskInfo forDeskTasks(
|
||||||
|
int deskId,
|
||||||
|
int deskDisplayId,
|
||||||
|
@NonNull List<TaskInfo> tasks,
|
||||||
|
@NonNull Set<Integer> minimizedFreeformTaskIds) {
|
||||||
|
return new GroupedTaskInfo(deskId, deskDisplayId, tasks, /* splitBounds = */ null,
|
||||||
|
TYPE_DESK,
|
||||||
|
minimizedFreeformTaskIds.stream().mapToInt(i -> i).toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new for a group of grouped task infos, those grouped task infos may not be mixed
|
||||||
|
* themselves (ie. multiple depths of mixed grouped task infos are not allowed).
|
||||||
|
*/
|
||||||
|
public static GroupedTaskInfo forMixed(@NonNull List<GroupedTaskInfo> groupedTasks) {
|
||||||
|
if (groupedTasks.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Expected non-empty grouped task list");
|
||||||
|
}
|
||||||
|
if (groupedTasks.stream().anyMatch(task -> task.mType == TYPE_MIXED)) {
|
||||||
|
throw new IllegalArgumentException("Unexpected grouped task list");
|
||||||
|
}
|
||||||
|
return new GroupedTaskInfo(groupedTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupedTaskInfo(
|
||||||
|
int deskId,
|
||||||
|
int deskDisplayId,
|
||||||
|
@NonNull List<TaskInfo> tasks,
|
||||||
|
@Nullable SplitBounds splitBounds,
|
||||||
|
@GroupType int type,
|
||||||
|
@Nullable int[] minimizedFreeformTaskIds) {
|
||||||
|
mDeskId = deskId;
|
||||||
|
mDeskDisplayId = deskDisplayId;
|
||||||
|
mTasks = tasks;
|
||||||
|
mGroupedTasks = null;
|
||||||
|
mSplitBounds = splitBounds;
|
||||||
|
mType = type;
|
||||||
|
mMinimizedTaskIds = minimizedFreeformTaskIds;
|
||||||
|
ensureAllMinimizedIdsPresent(tasks, minimizedFreeformTaskIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupedTaskInfo(@NonNull List<GroupedTaskInfo> groupedTasks) {
|
||||||
|
mDeskId = -1;
|
||||||
|
mDeskDisplayId = INVALID_DISPLAY;
|
||||||
|
mTasks = null;
|
||||||
|
mGroupedTasks = groupedTasks;
|
||||||
|
mSplitBounds = null;
|
||||||
|
mType = TYPE_MIXED;
|
||||||
|
mMinimizedTaskIds = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureAllMinimizedIdsPresent(
|
||||||
|
@NonNull List<TaskInfo> tasks,
|
||||||
|
@Nullable int[] minimizedFreeformTaskIds) {
|
||||||
|
if (minimizedFreeformTaskIds == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Arrays.stream(minimizedFreeformTaskIds).allMatch(
|
||||||
|
taskId -> tasks.stream().anyMatch(task -> task.taskId == taskId))) {
|
||||||
|
throw new IllegalArgumentException("Minimized task IDs contain non-existent Task ID.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected GroupedTaskInfo(@NonNull Parcel parcel) {
|
||||||
|
mDeskId = parcel.readInt();
|
||||||
|
mDeskDisplayId = parcel.readInt();
|
||||||
|
mTasks = new ArrayList();
|
||||||
|
final int numTasks = parcel.readInt();
|
||||||
|
for (int i = 0; i < numTasks; i++) {
|
||||||
|
mTasks.add(new TaskInfo(parcel));
|
||||||
|
}
|
||||||
|
mGroupedTasks = parcel.createTypedArrayList(GroupedTaskInfo.CREATOR);
|
||||||
|
mSplitBounds = parcel.readTypedObject(SplitBounds.CREATOR);
|
||||||
|
mType = parcel.readInt();
|
||||||
|
mMinimizedTaskIds = parcel.createIntArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If TYPE_MIXED, returns the root of the grouped tasks
|
||||||
|
* For all other types, returns this task itself
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public GroupedTaskInfo getBaseGroupedTask() {
|
||||||
|
if (mType == TYPE_MIXED) {
|
||||||
|
return mGroupedTasks.getFirst();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get primary {@link TaskInfo}.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if the group is TYPE_MIXED.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public TaskInfo getTaskInfo1() {
|
||||||
|
if (mType == TYPE_MIXED) {
|
||||||
|
throw new IllegalStateException("No indexed tasks for a mixed task");
|
||||||
|
}
|
||||||
|
return mTasks.getFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get secondary {@link TaskInfo}, used primarily for TYPE_SPLIT.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if the group is TYPE_MIXED.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public TaskInfo getTaskInfo2() {
|
||||||
|
if (mType == TYPE_MIXED) {
|
||||||
|
throw new IllegalStateException("No indexed tasks for a mixed task");
|
||||||
|
}
|
||||||
|
if (mTasks.size() > 1) {
|
||||||
|
return mTasks.get(1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The task info for the task in this group with the given {@code taskId}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public TaskInfo getTaskById(int taskId) {
|
||||||
|
return CollectionsKt.firstOrNull(getTaskInfoList(), taskInfo -> taskInfo.taskId == taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all {@link RecentTaskInfo}s grouped together.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public List<TaskInfo> getTaskInfoList() {
|
||||||
|
if (mType == TYPE_MIXED) {
|
||||||
|
return CollectionsKt.flatMap(mGroupedTasks, groupedTaskInfo -> groupedTaskInfo.mTasks);
|
||||||
|
} else {
|
||||||
|
return mTasks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Whether this grouped task contains a task with the given {@code taskId}.
|
||||||
|
*/
|
||||||
|
public boolean containsTask(int taskId) {
|
||||||
|
return getTaskById(taskId) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the group is of the given type, if this is a TYPE_MIXED group, then returns
|
||||||
|
* whether the root task info is of the given type.
|
||||||
|
*/
|
||||||
|
public boolean isBaseType(@GroupType int type) {
|
||||||
|
return getBaseGroupedTask().mType == type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@link SplitBounds} if this is a split screen entry or {@code null}. Only valid for
|
||||||
|
* TYPE_SPLIT.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public SplitBounds getSplitBounds() {
|
||||||
|
if (mType == TYPE_MIXED) {
|
||||||
|
throw new IllegalStateException("No split bounds for a mixed task");
|
||||||
|
}
|
||||||
|
return mSplitBounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ID of the desk represented by `this` if the type is `TYPE_DESK`, or -1 otherwise.
|
||||||
|
*/
|
||||||
|
public int getDeskId() {
|
||||||
|
if (mType == TYPE_MIXED) {
|
||||||
|
throw new IllegalStateException("No desk ID for a mixed task");
|
||||||
|
}
|
||||||
|
return mDeskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ID of the display that hosts the desk represented by [mDeskId].
|
||||||
|
*/
|
||||||
|
public int getDeskDisplayId() {
|
||||||
|
if (mType != TYPE_DESK) {
|
||||||
|
throw new IllegalStateException("No display ID for non desktop task");
|
||||||
|
}
|
||||||
|
return mDeskDisplayId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get type of this recents entry. One of {@link GroupType}.
|
||||||
|
* Note: This is deprecated, callers should use `isBaseType()` and not make assumptions about
|
||||||
|
* specific group types
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
@GroupType
|
||||||
|
public int getType() {
|
||||||
|
return mType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the set of minimized task ids, only valid for TYPE_DESK.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public int[] getMinimizedTaskIds() {
|
||||||
|
if (mType == TYPE_MIXED) {
|
||||||
|
throw new IllegalStateException("No minimized task ids for a mixed task");
|
||||||
|
}
|
||||||
|
return mMinimizedTaskIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (!(obj instanceof GroupedTaskInfo)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
GroupedTaskInfo other = (GroupedTaskInfo) obj;
|
||||||
|
return mDeskId == other.mDeskId
|
||||||
|
&& mDeskDisplayId == other.mDeskDisplayId
|
||||||
|
&& mType == other.mType
|
||||||
|
&& Objects.equals(mTasks, other.mTasks)
|
||||||
|
&& Objects.equals(mGroupedTasks, other.mGroupedTasks)
|
||||||
|
&& Objects.equals(mSplitBounds, other.mSplitBounds)
|
||||||
|
&& Arrays.equals(mMinimizedTaskIds, other.mMinimizedTaskIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(mDeskId, mDeskDisplayId, mType, mTasks, mGroupedTasks, mSplitBounds,
|
||||||
|
Arrays.hashCode(mMinimizedTaskIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder taskString = new StringBuilder();
|
||||||
|
if (mType == TYPE_MIXED) {
|
||||||
|
taskString.append("GroupedTasks=" + mGroupedTasks.stream()
|
||||||
|
.map(GroupedTaskInfo::toString)
|
||||||
|
.collect(Collectors.joining(",\n\t", "[\n\t", "\n]")));
|
||||||
|
} else {
|
||||||
|
taskString.append("Desk ID= ").append(mDeskId).append(", ");
|
||||||
|
taskString.append("Desk Display ID=").append(mDeskDisplayId).append(", ");
|
||||||
|
taskString.append("Tasks=" + mTasks.stream()
|
||||||
|
.map(taskInfo -> getTaskInfoDumpString(taskInfo))
|
||||||
|
.collect(Collectors.joining(", ", "[", "]")));
|
||||||
|
if (mSplitBounds != null) {
|
||||||
|
taskString.append(", SplitBounds=").append(mSplitBounds);
|
||||||
|
}
|
||||||
|
taskString.append(", Type=" + typeToString(mType));
|
||||||
|
taskString.append(", Minimized Task IDs=" + Arrays.toString(mMinimizedTaskIds));
|
||||||
|
}
|
||||||
|
return taskString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTaskInfoDumpString(TaskInfo taskInfo) {
|
||||||
|
if (taskInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final boolean isExcluded = (taskInfo.baseIntent.getFlags()
|
||||||
|
& FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
|
||||||
|
return "id=" + taskInfo.taskId
|
||||||
|
+ " winMode=" + windowingModeToString(taskInfo.getWindowingMode())
|
||||||
|
+ " visReq=" + taskInfo.isVisibleRequested
|
||||||
|
+ " vis=" + taskInfo.isVisible
|
||||||
|
+ " excluded=" + isExcluded
|
||||||
|
+ " baseIntent="
|
||||||
|
+ (taskInfo.baseIntent != null && taskInfo.baseIntent.getComponent() != null
|
||||||
|
? taskInfo.baseIntent.getComponent().flattenToShortString()
|
||||||
|
: "null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel parcel, int flags) {
|
||||||
|
parcel.writeInt(mDeskId);
|
||||||
|
parcel.writeInt(mDeskDisplayId);
|
||||||
|
// We don't use the parcel list methods because we want to only write the TaskInfo state
|
||||||
|
// and not the subclasses (Recents/RunningTaskInfo) whose fields are all deprecated
|
||||||
|
final int tasksSize = mTasks != null ? mTasks.size() : 0;
|
||||||
|
parcel.writeInt(tasksSize);
|
||||||
|
for (int i = 0; i < tasksSize; i++) {
|
||||||
|
mTasks.get(i).writeTaskToParcel(parcel, flags);
|
||||||
|
}
|
||||||
|
parcel.writeTypedList(mGroupedTasks);
|
||||||
|
parcel.writeTypedObject(mSplitBounds, flags);
|
||||||
|
parcel.writeInt(mType);
|
||||||
|
parcel.writeIntArray(mMinimizedTaskIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String typeToString(@GroupType int type) {
|
||||||
|
return switch (type) {
|
||||||
|
case TYPE_FULLSCREEN -> "FULLSCREEN";
|
||||||
|
case TYPE_SPLIT -> "SPLIT";
|
||||||
|
case TYPE_DESK -> "DESK";
|
||||||
|
case TYPE_MIXED -> "MIXED";
|
||||||
|
default -> "UNKNOWN";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<GroupedTaskInfo> CREATOR = new Creator() {
|
||||||
|
@Override
|
||||||
|
public GroupedTaskInfo createFromParcel(Parcel in) {
|
||||||
|
return new GroupedTaskInfo(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GroupedTaskInfo[] newArray(int size) {
|
||||||
|
return new GroupedTaskInfo[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener interface that to get focus-related transition callbacks.
|
||||||
|
*/
|
||||||
|
oneway interface IFocusTransitionListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a transition changes the top, focused display.
|
||||||
|
*/
|
||||||
|
void onFocusedDisplayChanged(int displayId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared;
|
||||||
|
|
||||||
|
import android.window.RemoteTransition;
|
||||||
|
import android.window.TransitionFilter;
|
||||||
|
import android.view.InsetsState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener interface that Launcher attaches to SystemUI to get home activity transition callbacks
|
||||||
|
* on the default display.
|
||||||
|
*/
|
||||||
|
oneway interface IHomeTransitionListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a transition changes the visibility of the home activity on the default display.
|
||||||
|
*/
|
||||||
|
void onHomeVisibilityChanged(in boolean isVisible);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the insets at display-level change.
|
||||||
|
*/
|
||||||
|
void onDisplayInsetsChanged(in InsetsState insets);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 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.shared;
|
||||||
|
|
||||||
|
import android.view.SurfaceControl;
|
||||||
|
import android.window.RemoteTransition;
|
||||||
|
import android.window.TransitionFilter;
|
||||||
|
|
||||||
|
import com.android.wm.shell.shared.IFocusTransitionListener;
|
||||||
|
import com.android.wm.shell.shared.IHomeTransitionListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that is exposed to remote callers to manipulate the transitions feature.
|
||||||
|
*/
|
||||||
|
interface IShellTransitions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a remote transition handler for all operations excluding takeovers (see
|
||||||
|
* registerRemoteForTakeover()).
|
||||||
|
*/
|
||||||
|
oneway void registerRemote(in TransitionFilter filter,
|
||||||
|
in RemoteTransition remoteTransition) = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters a remote transition handler for all operations.
|
||||||
|
*/
|
||||||
|
oneway void unregisterRemote(in RemoteTransition remoteTransition) = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the apply-token used by transactions in Shell
|
||||||
|
*/
|
||||||
|
IBinder getShellApplyToken() = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set listener that will receive callbacks about transitions involving home activity.
|
||||||
|
*/
|
||||||
|
oneway void setHomeTransitionListener(in IHomeTransitionListener listener) = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a container surface for the home root task.
|
||||||
|
*/
|
||||||
|
SurfaceControl getHomeTaskOverlayContainer() = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a remote transition for takeover operations only.
|
||||||
|
*/
|
||||||
|
oneway void registerRemoteForTakeover(in TransitionFilter filter,
|
||||||
|
in RemoteTransition remoteTransition) = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set listener that will receive callbacks about transitions involving focus switch.
|
||||||
|
*/
|
||||||
|
oneway void setFocusTransitionListener(in IFocusTransitionListener listener) = 7;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General shell-related constants that are shared with users of the library.
|
||||||
|
*/
|
||||||
|
public class ShellSharedConstants {
|
||||||
|
public static final String KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION =
|
||||||
|
"extra_shell_can_hand_off_animation";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the max screen width or height in dp for a device to be considered a small tablet.
|
||||||
|
*
|
||||||
|
* @see android.view.WindowManager#LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP
|
||||||
|
*/
|
||||||
|
public static final int SMALL_TABLET_MAX_EDGE_DP = 960;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared;
|
||||||
|
|
||||||
|
import android.annotation.NonNull;
|
||||||
|
import android.window.RemoteTransition;
|
||||||
|
import android.window.TransitionFilter;
|
||||||
|
|
||||||
|
import com.android.wm.shell.shared.annotations.ExternalThread;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to manage remote transitions.
|
||||||
|
*/
|
||||||
|
@ExternalThread
|
||||||
|
public interface ShellTransitions {
|
||||||
|
/**
|
||||||
|
* Registers a remote transition for all operations excluding takeovers (see
|
||||||
|
* {@link ShellTransitions#registerRemoteForTakeover(TransitionFilter, RemoteTransition)}).
|
||||||
|
*/
|
||||||
|
default void registerRemote(@NonNull TransitionFilter filter,
|
||||||
|
@NonNull RemoteTransition remoteTransition) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a remote transition for takeover operations only.
|
||||||
|
*/
|
||||||
|
default void registerRemoteForTakeover(@NonNull TransitionFilter filter,
|
||||||
|
@NonNull RemoteTransition remoteTransition) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters a remote transition for all operations.
|
||||||
|
*/
|
||||||
|
default void unregisterRemote(@NonNull RemoteTransition remoteTransition) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets listener that will receive callbacks about transitions involving focus switch.
|
||||||
|
*/
|
||||||
|
default void setFocusTransitionListener(@NonNull FocusTransitionListener listener,
|
||||||
|
Executor executor) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsets listener that will receive callbacks about transitions involving focus switch.
|
||||||
|
*/
|
||||||
|
default void unsetFocusTransitionListener(@NonNull FocusTransitionListener listener) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared;
|
||||||
|
|
||||||
|
import android.util.Pools;
|
||||||
|
import android.view.SurfaceControl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a synchronized pool of {@link SurfaceControl.Transaction}s to minimize allocations.
|
||||||
|
*/
|
||||||
|
public class TransactionPool {
|
||||||
|
private final Pools.SynchronizedPool<SurfaceControl.Transaction> mTransactionPool =
|
||||||
|
new Pools.SynchronizedPool<>(4);
|
||||||
|
|
||||||
|
public TransactionPool() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets a transaction from the pool. */
|
||||||
|
public SurfaceControl.Transaction acquire() {
|
||||||
|
SurfaceControl.Transaction t = mTransactionPool.acquire();
|
||||||
|
if (t == null) {
|
||||||
|
return new SurfaceControl.Transaction();
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a transaction to the pool. DO NOT call {@link SurfaceControl.Transaction#close()} if
|
||||||
|
* returning to pool.
|
||||||
|
*/
|
||||||
|
public void release(SurfaceControl.Transaction t) {
|
||||||
|
if (!mTransactionPool.release(t)) {
|
||||||
|
t.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,504 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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.shared;
|
||||||
|
|
||||||
|
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
|
||||||
|
import static android.view.RemoteAnimationTarget.MODE_CHANGING;
|
||||||
|
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
|
||||||
|
import static android.view.RemoteAnimationTarget.MODE_OPENING;
|
||||||
|
import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
|
||||||
|
import static android.view.WindowManager.LayoutParams.LAST_SYSTEM_WINDOW;
|
||||||
|
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
|
||||||
|
import static android.view.WindowManager.TRANSIT_CHANGE;
|
||||||
|
import static android.view.WindowManager.TRANSIT_CLOSE;
|
||||||
|
import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
|
||||||
|
import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION;
|
||||||
|
import static android.view.WindowManager.TRANSIT_OPEN;
|
||||||
|
import static android.view.WindowManager.TRANSIT_TO_BACK;
|
||||||
|
import static android.view.WindowManager.TRANSIT_TO_FRONT;
|
||||||
|
import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
|
||||||
|
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
|
||||||
|
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
|
||||||
|
import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
|
||||||
|
import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
|
||||||
|
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
|
||||||
|
|
||||||
|
import android.annotation.NonNull;
|
||||||
|
import android.annotation.Nullable;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.ActivityManager;
|
||||||
|
import android.app.WindowConfiguration;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.util.ArrayMap;
|
||||||
|
import android.util.SparseBooleanArray;
|
||||||
|
import android.view.RemoteAnimationTarget;
|
||||||
|
import android.view.SurfaceControl;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.window.TransitionInfo;
|
||||||
|
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
/** Various utility functions for transitions. */
|
||||||
|
public class TransitionUtil {
|
||||||
|
/** Flag applied to a transition change to identify it as a divider bar for animation. */
|
||||||
|
public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
|
||||||
|
public static final int FLAG_IS_DIM_LAYER = FLAG_FIRST_CUSTOM << 1;
|
||||||
|
|
||||||
|
/** Flag applied to a transition change to identify it as a desktop wallpaper activity. */
|
||||||
|
public static final int FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY = FLAG_FIRST_CUSTOM << 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applied to a {@link RemoteAnimationTarget} to identify dim layers for animation in Launcher.
|
||||||
|
*/
|
||||||
|
public static final int TYPE_SPLIT_SCREEN_DIM_LAYER = LAST_SYSTEM_WINDOW + 1;
|
||||||
|
|
||||||
|
/** @return true if the transition was triggered by opening something vs closing something */
|
||||||
|
public static boolean isOpeningType(@WindowManager.TransitionType int type) {
|
||||||
|
return type == TRANSIT_OPEN
|
||||||
|
|| type == TRANSIT_TO_FRONT
|
||||||
|
|| type == TRANSIT_KEYGUARD_GOING_AWAY
|
||||||
|
|| type == TRANSIT_PREPARE_BACK_NAVIGATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return true if the transition was triggered by closing something vs opening something */
|
||||||
|
public static boolean isClosingType(@WindowManager.TransitionType int type) {
|
||||||
|
return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns {@code true} if the transition is opening or closing mode. */
|
||||||
|
public static boolean isOpenOrCloseMode(@TransitionInfo.TransitionMode int mode) {
|
||||||
|
return isOpeningMode(mode) || isClosingMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns {@code true} if the transition is opening mode. */
|
||||||
|
public static boolean isOpeningMode(@TransitionInfo.TransitionMode int mode) {
|
||||||
|
return mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns {@code true} if the transition is closing mode. */
|
||||||
|
public static boolean isClosingMode(@TransitionInfo.TransitionMode int mode) {
|
||||||
|
return mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns {@code true} if the transition has a display change. */
|
||||||
|
public static boolean hasDisplayChange(@NonNull TransitionInfo info) {
|
||||||
|
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
|
||||||
|
final TransitionInfo.Change change = info.getChanges().get(i);
|
||||||
|
if (change.getMode() == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns `true` if `change` is a wallpaper. */
|
||||||
|
public static boolean isWallpaper(TransitionInfo.Change change) {
|
||||||
|
return (change.getTaskInfo() == null)
|
||||||
|
&& change.hasFlags(FLAG_IS_WALLPAPER)
|
||||||
|
&& !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns `true` if `change` is not an app window or wallpaper. */
|
||||||
|
public static boolean isNonApp(TransitionInfo.Change change) {
|
||||||
|
return (change.getTaskInfo() == null)
|
||||||
|
&& !change.hasFlags(FLAG_IS_WALLPAPER)
|
||||||
|
&& !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns `true` if `change` is the divider. */
|
||||||
|
public static boolean isDividerBar(TransitionInfo.Change change) {
|
||||||
|
return isNonApp(change) && change.hasFlags(FLAG_IS_DIVIDER_BAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns `true` if `change` is an app's dim layer. */
|
||||||
|
public static boolean isDimLayer(TransitionInfo.Change change) {
|
||||||
|
return isNonApp(change) && change.hasFlags(FLAG_IS_DIM_LAYER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns `true` if `change` is only re-ordering. */
|
||||||
|
public static boolean isOrderOnly(TransitionInfo.Change change) {
|
||||||
|
return change.getMode() == TRANSIT_CHANGE
|
||||||
|
&& (change.getFlags() & FLAG_MOVED_TO_TOP) != 0
|
||||||
|
&& change.getStartAbsBounds().equals(change.getEndAbsBounds())
|
||||||
|
&& (change.getLastParent() == null
|
||||||
|
|| change.getLastParent().equals(change.getParent()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if all changes in this transition are only ordering changes. If so, we won't animate.
|
||||||
|
*/
|
||||||
|
public static boolean isAllOrderOnly(TransitionInfo info) {
|
||||||
|
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
|
||||||
|
if (!isOrderOnly(info.getChanges().get(i))) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look through a transition and see if all non-closing changes are no-animation. If so, no
|
||||||
|
* animation should play.
|
||||||
|
*/
|
||||||
|
public static boolean isAllNoAnimation(TransitionInfo info) {
|
||||||
|
if (isClosingType(info.getType())) {
|
||||||
|
// no-animation is only relevant for launching (open) activities.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean hasNoAnimation = false;
|
||||||
|
final int changeSize = info.getChanges().size();
|
||||||
|
for (int i = changeSize - 1; i >= 0; --i) {
|
||||||
|
final TransitionInfo.Change change = info.getChanges().get(i);
|
||||||
|
if (isClosingType(change.getMode())) {
|
||||||
|
// ignore closing apps since they are a side-effect of the transition and don't
|
||||||
|
// animate.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (change.hasFlags(TransitionInfo.FLAG_NO_ANIMATION)) {
|
||||||
|
hasNoAnimation = true;
|
||||||
|
} else if (!isOrderOnly(change) && !change.hasFlags(TransitionInfo.FLAG_IS_OCCLUDED)) {
|
||||||
|
// Ignore the order only or occluded changes since they shouldn't be visible during
|
||||||
|
// animation. For anything else, we need to animate if at-least one relevant
|
||||||
|
// participant *is* animated,
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasNoAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter that selects leaf-tasks only. THIS IS ORDER-DEPENDENT! For it to work properly, you
|
||||||
|
* MUST call `test` in the same order that the changes appear in the TransitionInfo.
|
||||||
|
*/
|
||||||
|
public static class LeafTaskFilter implements Predicate<TransitionInfo.Change> {
|
||||||
|
private final SparseBooleanArray mChildTaskTargets = new SparseBooleanArray();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(TransitionInfo.Change change) {
|
||||||
|
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
|
||||||
|
if (taskInfo == null) return false;
|
||||||
|
// Children always come before parent since changes are in top-to-bottom z-order.
|
||||||
|
boolean hasChildren = mChildTaskTargets.get(taskInfo.taskId);
|
||||||
|
if (taskInfo.hasParentTask()) {
|
||||||
|
mChildTaskTargets.put(taskInfo.parentTaskId, true);
|
||||||
|
}
|
||||||
|
// If it has children, it's not a leaf.
|
||||||
|
return !hasChildren;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static int newModeToLegacyMode(int newMode) {
|
||||||
|
switch (newMode) {
|
||||||
|
case WindowManager.TRANSIT_OPEN:
|
||||||
|
case WindowManager.TRANSIT_TO_FRONT:
|
||||||
|
return MODE_OPENING;
|
||||||
|
case WindowManager.TRANSIT_CLOSE:
|
||||||
|
case WindowManager.TRANSIT_TO_BACK:
|
||||||
|
return MODE_CLOSING;
|
||||||
|
default:
|
||||||
|
return MODE_CHANGING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Very similar to Transitions#setupAnimHierarchy but specialized for leashes.
|
||||||
|
*/
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
private static void setupLeash(@NonNull SurfaceControl leash,
|
||||||
|
@NonNull TransitionInfo.Change change, int layer,
|
||||||
|
@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
|
||||||
|
final boolean isOpening = TransitionUtil.isOpeningType(info.getType());
|
||||||
|
// Put animating stuff above this line and put static stuff below it.
|
||||||
|
int zSplitLine = info.getChanges().size();
|
||||||
|
// changes should be ordered top-to-bottom in z
|
||||||
|
final int mode = change.getMode();
|
||||||
|
|
||||||
|
final int rootIdx = TransitionUtil.rootIndexFor(change, info);
|
||||||
|
t.reparent(leash, info.getRoot(rootIdx).getLeash());
|
||||||
|
final Rect absBounds =
|
||||||
|
(mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds();
|
||||||
|
t.setPosition(leash, absBounds.left - info.getRoot(rootIdx).getOffset().x,
|
||||||
|
absBounds.top - info.getRoot(rootIdx).getOffset().y);
|
||||||
|
|
||||||
|
if (isDividerBar(change)) {
|
||||||
|
if (isOpeningType(mode)) {
|
||||||
|
t.setAlpha(leash, 0.f);
|
||||||
|
}
|
||||||
|
// Set the transition leash position to 0 in case the divider leash position being
|
||||||
|
// taking down.
|
||||||
|
t.setPosition(leash, 0, 0);
|
||||||
|
t.setLayer(leash, Integer.MAX_VALUE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isDimLayer(change)) {
|
||||||
|
// When a dim layer gets reparented onto the transition root, we need to zero out its
|
||||||
|
// position so that it's in line with everything else on the transition root. Also,
|
||||||
|
// we need to set a crop because we don't want it applying MATCH_PARENT on the whole
|
||||||
|
// root surface.
|
||||||
|
t.setPosition(leash, 0, 0);
|
||||||
|
t.setCrop(leash, change.getEndAbsBounds());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put all the OPEN/SHOW on top
|
||||||
|
if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
|
||||||
|
// Wallpaper is always at the bottom, opening wallpaper on top of closing one.
|
||||||
|
if (mode == WindowManager.TRANSIT_OPEN || mode == WindowManager.TRANSIT_TO_FRONT) {
|
||||||
|
t.setLayer(leash, -zSplitLine + info.getChanges().size() - layer);
|
||||||
|
} else {
|
||||||
|
t.setLayer(leash, -zSplitLine - layer);
|
||||||
|
}
|
||||||
|
} else if (TransitionUtil.isOpeningType(mode)) {
|
||||||
|
if (isOpening) {
|
||||||
|
t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
|
||||||
|
if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) == 0) {
|
||||||
|
// if transferred, it should be left visible.
|
||||||
|
t.setAlpha(leash, 0.f);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// put on bottom and leave it visible
|
||||||
|
t.setLayer(leash, zSplitLine - layer);
|
||||||
|
}
|
||||||
|
} else if (TransitionUtil.isClosingType(mode)) {
|
||||||
|
if (isOpening) {
|
||||||
|
// put on bottom and leave visible
|
||||||
|
t.setLayer(leash, zSplitLine - layer);
|
||||||
|
} else {
|
||||||
|
// put on top
|
||||||
|
t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
|
||||||
|
}
|
||||||
|
} else { // CHANGE
|
||||||
|
t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
private static SurfaceControl createLeash(TransitionInfo info, TransitionInfo.Change change,
|
||||||
|
int order, SurfaceControl.Transaction t) {
|
||||||
|
// TODO: once we can properly sync transactions across process, then get rid of this leash.
|
||||||
|
if (change.getParent() != null && (change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
|
||||||
|
// Special case for wallpaper atm. Normally these are left alone; but, a quirk of
|
||||||
|
// making leashes means we have to handle them specially.
|
||||||
|
return change.getLeash();
|
||||||
|
}
|
||||||
|
final int rootIdx = TransitionUtil.rootIndexFor(change, info);
|
||||||
|
SurfaceControl leashSurface = new SurfaceControl.Builder()
|
||||||
|
.setName(change.getLeash().toString() + "_transition-leash")
|
||||||
|
.setContainerLayer()
|
||||||
|
// Initial the surface visible to respect the visibility of the original surface.
|
||||||
|
.setHidden(false)
|
||||||
|
.setParent(info.getRoot(rootIdx).getLeash())
|
||||||
|
.build();
|
||||||
|
// Copied Transitions setup code (which expects bottom-to-top order, so we swap here)
|
||||||
|
setupLeash(leashSurface, change, info.getChanges().size() - order, info, t);
|
||||||
|
t.reparent(change.getLeash(), leashSurface);
|
||||||
|
if (!isDimLayer(change)) {
|
||||||
|
// Most leashes going onto the transition root should have their alpha set here to make
|
||||||
|
// them visible. But dim layers should be left untouched (their alpha value is their
|
||||||
|
// actual dim value).
|
||||||
|
t.setAlpha(change.getLeash(), 1.0f);
|
||||||
|
}
|
||||||
|
if (!isDividerBar(change)) {
|
||||||
|
// For divider, don't modify its inner leash position when creating the outer leash
|
||||||
|
// for the transition. In case the position being wrong after the transition finished.
|
||||||
|
t.setPosition(change.getLeash(), 0, 0);
|
||||||
|
}
|
||||||
|
t.setLayer(change.getLeash(), 0);
|
||||||
|
t.show(change.getLeash());
|
||||||
|
return leashSurface;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new RemoteAnimationTarget from the provided change info
|
||||||
|
*/
|
||||||
|
public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
|
||||||
|
TransitionInfo info, SurfaceControl.Transaction t,
|
||||||
|
@Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
|
||||||
|
return newTarget(change, order, false /* forceTranslucent */, info, t, leashMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new RemoteAnimationTarget from the provided change info
|
||||||
|
*/
|
||||||
|
public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
|
||||||
|
boolean forceTranslucent, TransitionInfo info, SurfaceControl.Transaction t,
|
||||||
|
@Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
|
||||||
|
final SurfaceControl leash = createLeash(info, change, order, t);
|
||||||
|
if (leashMap != null) {
|
||||||
|
leashMap.put(change.getLeash(), leash);
|
||||||
|
}
|
||||||
|
return newTarget(change, order, leash, forceTranslucent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new RemoteAnimationTarget from the provided change and leash
|
||||||
|
*/
|
||||||
|
public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
|
||||||
|
SurfaceControl leash) {
|
||||||
|
return newTarget(change, order, leash, false /* forceTranslucent */);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new RemoteAnimationTarget from the provided change and leash
|
||||||
|
*/
|
||||||
|
public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
|
||||||
|
SurfaceControl leash, boolean forceTranslucent) {
|
||||||
|
if (isDividerBar(change)) {
|
||||||
|
return getDividerTarget(change, leash);
|
||||||
|
}
|
||||||
|
if (isDimLayer(change)) {
|
||||||
|
return getDimLayerTarget(change, leash);
|
||||||
|
}
|
||||||
|
|
||||||
|
int taskId;
|
||||||
|
boolean isNotInRecents;
|
||||||
|
ActivityManager.RunningTaskInfo taskInfo;
|
||||||
|
WindowConfiguration windowConfiguration;
|
||||||
|
|
||||||
|
taskInfo = change.getTaskInfo();
|
||||||
|
if (taskInfo != null) {
|
||||||
|
taskId = taskInfo.taskId;
|
||||||
|
isNotInRecents = !taskInfo.isRunning;
|
||||||
|
windowConfiguration = taskInfo.configuration.windowConfiguration;
|
||||||
|
} else {
|
||||||
|
taskId = INVALID_TASK_ID;
|
||||||
|
isNotInRecents = true;
|
||||||
|
windowConfiguration = new WindowConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect localBounds = new Rect(change.getEndAbsBounds());
|
||||||
|
localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y);
|
||||||
|
|
||||||
|
RemoteAnimationTarget target = new RemoteAnimationTarget(
|
||||||
|
taskId,
|
||||||
|
newModeToLegacyMode(change.getMode()),
|
||||||
|
// TODO: once we can properly sync transactions across process,
|
||||||
|
// then get rid of this leash.
|
||||||
|
leash,
|
||||||
|
forceTranslucent || (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0,
|
||||||
|
null,
|
||||||
|
// TODO(shell-transitions): we need to send content insets? evaluate how its used.
|
||||||
|
new Rect(0, 0, 0, 0),
|
||||||
|
order,
|
||||||
|
null,
|
||||||
|
localBounds,
|
||||||
|
new Rect(change.getEndAbsBounds()),
|
||||||
|
windowConfiguration,
|
||||||
|
isNotInRecents,
|
||||||
|
null,
|
||||||
|
new Rect(change.getStartAbsBounds()),
|
||||||
|
taskInfo,
|
||||||
|
change.isAllowEnterPip(),
|
||||||
|
INVALID_WINDOW_TYPE
|
||||||
|
);
|
||||||
|
target.setWillShowImeOnTarget(
|
||||||
|
(change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0);
|
||||||
|
target.setRotationChange(change.getEndRotation() - change.getStartRotation());
|
||||||
|
target.backgroundColor = change.getBackgroundColor();
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new RemoteAnimationTarget from the provided change and leash
|
||||||
|
*/
|
||||||
|
public static RemoteAnimationTarget newSyntheticTarget(ActivityManager.RunningTaskInfo taskInfo,
|
||||||
|
SurfaceControl leash, @TransitionInfo.TransitionMode int mode, int order,
|
||||||
|
boolean isTranslucent) {
|
||||||
|
int taskId;
|
||||||
|
boolean isNotInRecents;
|
||||||
|
WindowConfiguration windowConfiguration;
|
||||||
|
|
||||||
|
if (taskInfo != null) {
|
||||||
|
taskId = taskInfo.taskId;
|
||||||
|
isNotInRecents = !taskInfo.isRunning;
|
||||||
|
windowConfiguration = taskInfo.configuration.windowConfiguration;
|
||||||
|
} else {
|
||||||
|
taskId = INVALID_TASK_ID;
|
||||||
|
isNotInRecents = true;
|
||||||
|
windowConfiguration = new WindowConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect bounds = windowConfiguration.getBounds();
|
||||||
|
RemoteAnimationTarget target = new RemoteAnimationTarget(
|
||||||
|
taskId,
|
||||||
|
newModeToLegacyMode(mode),
|
||||||
|
// TODO: once we can properly sync transactions across process,
|
||||||
|
// then get rid of this leash.
|
||||||
|
leash,
|
||||||
|
isTranslucent,
|
||||||
|
null,
|
||||||
|
// TODO(shell-transitions): we need to send content insets? evaluate how its used.
|
||||||
|
new Rect(0, 0, 0, 0),
|
||||||
|
order,
|
||||||
|
null,
|
||||||
|
bounds,
|
||||||
|
bounds,
|
||||||
|
windowConfiguration,
|
||||||
|
isNotInRecents,
|
||||||
|
null,
|
||||||
|
bounds,
|
||||||
|
taskInfo,
|
||||||
|
false,
|
||||||
|
INVALID_WINDOW_TYPE
|
||||||
|
);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RemoteAnimationTarget getDividerTarget(TransitionInfo.Change change,
|
||||||
|
SurfaceControl leash) {
|
||||||
|
return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()),
|
||||||
|
leash, false /* isTranslucent */, null /* clipRect */,
|
||||||
|
null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
|
||||||
|
new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(),
|
||||||
|
change.getStartAbsBounds(), new WindowConfiguration(), true, null /* startLeash */,
|
||||||
|
null /* startBounds */, null /* taskInfo */, false /* allowEnterPip */,
|
||||||
|
TYPE_DOCK_DIVIDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RemoteAnimationTarget getDimLayerTarget(TransitionInfo.Change change,
|
||||||
|
SurfaceControl leash) {
|
||||||
|
return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()),
|
||||||
|
leash, false /* isTranslucent */, null /* clipRect */,
|
||||||
|
null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
|
||||||
|
new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(),
|
||||||
|
change.getStartAbsBounds(), new WindowConfiguration(), true, null /* startLeash */,
|
||||||
|
null /* startBounds */, null /* taskInfo */, false /* allowEnterPip */,
|
||||||
|
TYPE_SPLIT_SCREEN_DIM_LAYER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the "correct" root idx for a change. The change's end display is prioritized, then
|
||||||
|
* the start display. If there is no display, it will fallback on the 0th root in the
|
||||||
|
* transition. There MUST be at-least 1 root in the transition (ie. it's not a no-op).
|
||||||
|
*/
|
||||||
|
public static int rootIndexFor(@NonNull TransitionInfo.Change change,
|
||||||
|
@NonNull TransitionInfo info) {
|
||||||
|
int rootIdx = info.findRootIndex(change.getEndDisplayId());
|
||||||
|
if (rootIdx >= 0) return rootIdx;
|
||||||
|
rootIdx = info.findRootIndex(change.getStartDisplayId());
|
||||||
|
if (rootIdx >= 0) return rootIdx;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the {@link TransitionInfo.Root} for the given {@link TransitionInfo.Change}.
|
||||||
|
* @see #rootIndexFor(TransitionInfo.Change, TransitionInfo)
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static TransitionInfo.Root getRootFor(@NonNull TransitionInfo.Change change,
|
||||||
|
@NonNull TransitionInfo info) {
|
||||||
|
return info.getRoot(rootIndexFor(change, info));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared;
|
||||||
|
|
||||||
|
import android.graphics.Outline;
|
||||||
|
import android.graphics.Path;
|
||||||
|
import android.graphics.drawable.shapes.PathShape;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around {@link PathShape}
|
||||||
|
* that creates a shape with a triangular path (pointing up or down).
|
||||||
|
*
|
||||||
|
* This is the copy from SystemUI/recents.
|
||||||
|
*/
|
||||||
|
public class TriangleShape extends PathShape {
|
||||||
|
private Path mTriangularPath;
|
||||||
|
|
||||||
|
public TriangleShape(Path path, float stdWidth, float stdHeight) {
|
||||||
|
super(path, stdWidth, stdHeight);
|
||||||
|
mTriangularPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TriangleShape create(float width, float height, boolean isPointingUp) {
|
||||||
|
Path triangularPath = new Path();
|
||||||
|
if (isPointingUp) {
|
||||||
|
triangularPath.moveTo(0, height);
|
||||||
|
triangularPath.lineTo(width, height);
|
||||||
|
triangularPath.lineTo(width / 2, 0);
|
||||||
|
triangularPath.close();
|
||||||
|
} else {
|
||||||
|
triangularPath.moveTo(0, 0);
|
||||||
|
triangularPath.lineTo(width / 2, height);
|
||||||
|
triangularPath.lineTo(width, 0);
|
||||||
|
triangularPath.close();
|
||||||
|
}
|
||||||
|
return new TriangleShape(triangularPath, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create an arrow TriangleShape that points to the left or the right */
|
||||||
|
public static TriangleShape createHorizontal(
|
||||||
|
float width, float height, boolean isPointingLeft) {
|
||||||
|
Path triangularPath = new Path();
|
||||||
|
if (isPointingLeft) {
|
||||||
|
triangularPath.moveTo(0, height / 2);
|
||||||
|
triangularPath.lineTo(width, height);
|
||||||
|
triangularPath.lineTo(width, 0);
|
||||||
|
triangularPath.close();
|
||||||
|
} else {
|
||||||
|
triangularPath.moveTo(0, height);
|
||||||
|
triangularPath.lineTo(width, height / 2);
|
||||||
|
triangularPath.lineTo(0, 0);
|
||||||
|
triangularPath.close();
|
||||||
|
}
|
||||||
|
return new TriangleShape(triangularPath, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void getOutline(@NonNull Outline outline) {
|
||||||
|
outline.setPath(mTriangularPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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.shared
|
||||||
|
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.android.wm.shell.Flags
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class to apply a specified typeface to a [TextView].
|
||||||
|
*
|
||||||
|
* This class provides a method, [setTypeface],
|
||||||
|
* to easily set a pre-defined font family and style to a given [TextView].
|
||||||
|
*/
|
||||||
|
class TypefaceUtils {
|
||||||
|
|
||||||
|
enum class FontFamily(val value: String) {
|
||||||
|
GSF_DISPLAY_LARGE("variable-display-large"),
|
||||||
|
GSF_DISPLAY_MEDIUM("variable-display-medium"),
|
||||||
|
GSF_DISPLAY_SMALL("variable-display-small"),
|
||||||
|
GSF_HEADLINE_LARGE("variable-headline-large"),
|
||||||
|
GSF_HEADLINE_MEDIUM("variable-headline-medium"),
|
||||||
|
GSF_HEADLINE_SMALL("variable-headline-small"),
|
||||||
|
GSF_TITLE_LARGE("variable-title-large"),
|
||||||
|
GSF_TITLE_MEDIUM("variable-title-medium"),
|
||||||
|
GSF_TITLE_SMALL("variable-title-small"),
|
||||||
|
GSF_LABEL_LARGE("variable-label-large"),
|
||||||
|
GSF_LABEL_MEDIUM("variable-label-medium"),
|
||||||
|
GSF_LABEL_SMALL("variable-label-small"),
|
||||||
|
GSF_BODY_LARGE("variable-body-large"),
|
||||||
|
GSF_BODY_MEDIUM("variable-body-medium"),
|
||||||
|
GSF_BODY_SMALL("variable-body-small"),
|
||||||
|
GSF_DISPLAY_LARGE_EMPHASIZED("variable-display-large-emphasized"),
|
||||||
|
GSF_DISPLAY_MEDIUM_EMPHASIZED("variable-display-medium-emphasized"),
|
||||||
|
GSF_DISPLAY_SMALL_EMPHASIZED("variable-display-small-emphasized"),
|
||||||
|
GSF_HEADLINE_LARGE_EMPHASIZED("variable-headline-large-emphasized"),
|
||||||
|
GSF_HEADLINE_MEDIUM_EMPHASIZED("variable-headline-medium-emphasized"),
|
||||||
|
GSF_HEADLINE_SMALL_EMPHASIZED("variable-headline-small-emphasized"),
|
||||||
|
GSF_TITLE_LARGE_EMPHASIZED("variable-title-large-emphasized"),
|
||||||
|
GSF_TITLE_MEDIUM_EMPHASIZED("variable-title-medium-emphasized"),
|
||||||
|
GSF_TITLE_SMALL_EMPHASIZED("variable-title-small-emphasized"),
|
||||||
|
GSF_LABEL_LARGE_EMPHASIZED("variable-label-large-emphasized"),
|
||||||
|
GSF_LABEL_MEDIUM_EMPHASIZED("variable-label-medium-emphasized"),
|
||||||
|
GSF_LABEL_SMALL_EMPHASIZED("variable-label-small-emphasized"),
|
||||||
|
GSF_BODY_LARGE_EMPHASIZED("variable-body-large-emphasized"),
|
||||||
|
GSF_BODY_MEDIUM_EMPHASIZED("variable-body-medium-emphasized"),
|
||||||
|
GSF_BODY_SMALL_EMPHASIZED("variable-body-small-emphasized"),
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Sets the typeface of the provided [textView] to the specified [fontFamily] and [fontStyle].
|
||||||
|
*
|
||||||
|
* The typeface is only applied to the [TextView] when [Flags.enableGsf] is `true`.
|
||||||
|
* If [Flags.enableGsf] is `false`, this method has no effect.
|
||||||
|
*
|
||||||
|
* @param textView The [TextView] to which the typeface should be applied. If `null`, this method does nothing.
|
||||||
|
* @param fontFamily The desired [FontFamily] for the [TextView].
|
||||||
|
* @param fontStyle The desired font style (e.g., [Typeface.NORMAL], [Typeface.BOLD], [Typeface.ITALIC]). Defaults to [Typeface.NORMAL].
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
@JvmOverloads
|
||||||
|
fun setTypeface(
|
||||||
|
textView: TextView?,
|
||||||
|
fontFamily: FontFamily,
|
||||||
|
fontStyle: Int = Typeface.NORMAL,
|
||||||
|
) {
|
||||||
|
if (!Flags.enableGsf()) return
|
||||||
|
textView?.typeface = Typeface.create(fontFamily.value, fontStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.animation;
|
||||||
|
|
||||||
|
import android.graphics.Path;
|
||||||
|
import android.view.animation.BackGestureInterpolator;
|
||||||
|
import android.view.animation.Interpolator;
|
||||||
|
import android.view.animation.LinearInterpolator;
|
||||||
|
import android.view.animation.PathInterpolator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common interpolators used in wm shell library.
|
||||||
|
*/
|
||||||
|
public class Interpolators {
|
||||||
|
|
||||||
|
public static final Interpolator LINEAR = new LinearInterpolator();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpolator for alpha in animation.
|
||||||
|
*/
|
||||||
|
public static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpolator for alpha out animation.
|
||||||
|
*/
|
||||||
|
public static final Interpolator ALPHA_OUT = new PathInterpolator(0f, 0f, 0.8f, 1f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpolator for fast out linear in animation.
|
||||||
|
*/
|
||||||
|
public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpolator for fast out slow in animation.
|
||||||
|
*/
|
||||||
|
public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpolator for linear out slow in animation.
|
||||||
|
*/
|
||||||
|
public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default emphasized interpolator. Used for hero / emphasized movement of content.
|
||||||
|
*/
|
||||||
|
public static final Interpolator EMPHASIZED = createEmphasizedInterpolator();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The accelerated emphasized interpolator. Used for hero / emphasized movement of content that
|
||||||
|
* is disappearing e.g. when moving off screen.
|
||||||
|
*/
|
||||||
|
public static final Interpolator EMPHASIZED_ACCELERATE = new PathInterpolator(
|
||||||
|
0.3f, 0f, 0.8f, 0.15f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The decelerating emphasized interpolator. Used for hero / emphasized movement of content that
|
||||||
|
* is appearing e.g. when coming from off screen
|
||||||
|
*/
|
||||||
|
public static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator(
|
||||||
|
0.05f, 0.7f, 0.1f, 1f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The standard accelerating interpolator that should be used on every regular movement of
|
||||||
|
* content that is disappearing e.g. when moving off screen.
|
||||||
|
*/
|
||||||
|
public static final Interpolator STANDARD_ACCELERATE = new PathInterpolator(0.3f, 0f, 1f, 1f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The standard decelerating interpolator that should be used on every regular movement of
|
||||||
|
* content that is appearing e.g. when coming from off screen.
|
||||||
|
*/
|
||||||
|
public static final Interpolator STANDARD_DECELERATE = new PathInterpolator(0f, 0f, 0f, 1f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpolator to be used when animating a move based on a click. Pair with enough duration.
|
||||||
|
*/
|
||||||
|
public static final Interpolator TOUCH_RESPONSE = new PathInterpolator(0.3f, 0f, 0.1f, 1f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpolator to be used when animating a panel closing.
|
||||||
|
*/
|
||||||
|
public static final Interpolator PANEL_CLOSE_ACCELERATED =
|
||||||
|
new PathInterpolator(0.3f, 0, 0.5f, 1);
|
||||||
|
|
||||||
|
public static final PathInterpolator SLOWDOWN_INTERPOLATOR =
|
||||||
|
new PathInterpolator(0.5f, 1f, 0.5f, 1f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interpolator used for dimming a task as it travels offscreen, or towards a distant dismiss
|
||||||
|
* point. A sharp rise, followed by a steady middle, and ending with another sharp rise.
|
||||||
|
*/
|
||||||
|
public static final PathInterpolator DIM_INTERPOLATOR =
|
||||||
|
new PathInterpolator(.23f, .87f, .52f, -0.11f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interpolator used for dimming a task very quickly. Roughly approximates one of the "sharp
|
||||||
|
* rises" of {@link #DIM_INTERPOLATOR}.
|
||||||
|
*/
|
||||||
|
public static final PathInterpolator FAST_DIM_INTERPOLATOR =
|
||||||
|
new PathInterpolator(0.23f, 0.87f, 0.83f, 0.83f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this interpolator for animating progress values coming from the back callback to get
|
||||||
|
* the predictive-back-typical decelerate motion.
|
||||||
|
*
|
||||||
|
* This interpolator is similar to {@link Interpolators#STANDARD_DECELERATE} but has a slight
|
||||||
|
* acceleration phase at the start.
|
||||||
|
*/
|
||||||
|
public static final Interpolator BACK_GESTURE = new BackGestureInterpolator();
|
||||||
|
|
||||||
|
// Create the default emphasized interpolator
|
||||||
|
private static PathInterpolator createEmphasizedInterpolator() {
|
||||||
|
Path path = new Path();
|
||||||
|
// Doing the same as fast_out_extra_slow_in
|
||||||
|
path.moveTo(0f, 0f);
|
||||||
|
path.cubicTo(0.05f, 0f, 0.133333f, 0.06f, 0.166666f, 0.4f);
|
||||||
|
path.cubicTo(0.208333f, 0.82f, 0.25f, 1f, 1f, 1f);
|
||||||
|
return new PathInterpolator(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.animation
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorSet
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Handler
|
||||||
|
import android.view.Choreographer
|
||||||
|
import android.view.SurfaceControl.Transaction
|
||||||
|
import android.window.TransitionInfo.Change
|
||||||
|
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_MINIMIZE_WINDOW
|
||||||
|
import com.android.internal.jank.InteractionJankMonitor
|
||||||
|
|
||||||
|
/** Creates minimization animation */
|
||||||
|
object MinimizeAnimator {
|
||||||
|
|
||||||
|
private const val MINIMIZE_ANIM_ALPHA_DURATION_MS = 100L
|
||||||
|
|
||||||
|
private val minimizeBoundsAnimationDef =
|
||||||
|
WindowAnimator.BoundsAnimationParams(
|
||||||
|
durationMs = 200,
|
||||||
|
endOffsetYDp = 12f,
|
||||||
|
endScale = 0.97f,
|
||||||
|
interpolator = Interpolators.STANDARD_ACCELERATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a minimize animator for given task [Change].
|
||||||
|
*
|
||||||
|
* @param onAnimFinish finish-callback for the animation, note that this is called on the same
|
||||||
|
* thread as the animation itself.
|
||||||
|
* @param animationHandler the Handler that the animation is running on.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun create(
|
||||||
|
context: Context,
|
||||||
|
change: Change,
|
||||||
|
transaction: Transaction,
|
||||||
|
onAnimFinish: (Animator) -> Unit,
|
||||||
|
interactionJankMonitor: InteractionJankMonitor,
|
||||||
|
animationHandler: Handler,
|
||||||
|
): Animator {
|
||||||
|
val boundsAnimator = WindowAnimator.createBoundsAnimator(
|
||||||
|
context.resources.displayMetrics,
|
||||||
|
minimizeBoundsAnimationDef,
|
||||||
|
change,
|
||||||
|
transaction,
|
||||||
|
)
|
||||||
|
val alphaAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
|
||||||
|
duration = MINIMIZE_ANIM_ALPHA_DURATION_MS
|
||||||
|
interpolator = Interpolators.LINEAR
|
||||||
|
addUpdateListener { animation ->
|
||||||
|
transaction
|
||||||
|
.setAlpha(change.leash, animation.animatedValue as Float)
|
||||||
|
.setFrameTimeline(Choreographer.getInstance().vsyncId)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val listener = object : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationStart(animator: Animator) {
|
||||||
|
interactionJankMonitor.begin(
|
||||||
|
change.leash,
|
||||||
|
context,
|
||||||
|
animationHandler,
|
||||||
|
CUJ_DESKTOP_MODE_MINIMIZE_WINDOW,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override fun onAnimationCancel(animator: Animator) {
|
||||||
|
interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)
|
||||||
|
}
|
||||||
|
override fun onAnimationRepeat(animator: Animator) = Unit
|
||||||
|
override fun onAnimationEnd(animator: Animator) {
|
||||||
|
interactionJankMonitor.end(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)
|
||||||
|
onAnimFinish(animator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AnimatorSet().apply {
|
||||||
|
playTogether(boundsAnimator, alphaAnimator)
|
||||||
|
addListener(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
+494
@@ -0,0 +1,494 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.shared.animation
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.ArrayMap
|
||||||
|
import androidx.dynamicanimation.animation.FloatPropertyCompat
|
||||||
|
import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils.prepareForTest
|
||||||
|
import java.util.ArrayDeque
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
import kotlin.collections.HashMap
|
||||||
|
import kotlin.collections.HashSet
|
||||||
|
import kotlin.collections.Set
|
||||||
|
import kotlin.collections.component1
|
||||||
|
import kotlin.collections.component2
|
||||||
|
import kotlin.collections.drop
|
||||||
|
import kotlin.collections.forEach
|
||||||
|
import kotlin.collections.getOrPut
|
||||||
|
import kotlin.collections.set
|
||||||
|
import kotlin.collections.toList
|
||||||
|
import kotlin.collections.toTypedArray
|
||||||
|
|
||||||
|
typealias UpdateMatcher = (PhysicsAnimator.AnimationUpdate) -> Boolean
|
||||||
|
typealias UpdateFramesPerProperty<T> =
|
||||||
|
ArrayMap<FloatPropertyCompat<in T>, ArrayList<PhysicsAnimator.AnimationUpdate>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for testing code that uses [PhysicsAnimator].
|
||||||
|
*
|
||||||
|
* Start by calling [prepareForTest] at the beginning of each test - this will modify the behavior
|
||||||
|
* of all PhysicsAnimator instances so that they post animations to the main thread (so they don't
|
||||||
|
* crash). It'll also enable the use of the other static helper methods in this class, which you can
|
||||||
|
* use to do things like block the test until animations complete (so you can test end states), or
|
||||||
|
* verify keyframes.
|
||||||
|
*/
|
||||||
|
object PhysicsAnimatorTestUtils {
|
||||||
|
var timeoutMs: Long = 2000
|
||||||
|
private var startBlocksUntilAnimationsEnd = false
|
||||||
|
private val animationThreadHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val allAnimatedObjects = HashSet<Any>()
|
||||||
|
private val animatorTestHelpers = HashMap<PhysicsAnimator<*>, AnimatorTestHelper<*>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies the behavior of all [PhysicsAnimator] instances so that they post animations to the
|
||||||
|
* main thread, and report all of their
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun prepareForTest() {
|
||||||
|
PhysicsAnimator.onAnimatorCreated = { animator, target ->
|
||||||
|
allAnimatedObjects.add(target)
|
||||||
|
animatorTestHelpers[animator] = AnimatorTestHelper(animator)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutMs = 2000
|
||||||
|
startBlocksUntilAnimationsEnd = false
|
||||||
|
allAnimatedObjects.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun tearDown() {
|
||||||
|
if (Looper.myLooper() == animationThreadHandler.looper) {
|
||||||
|
animatorTestHelpers.keys.forEach { it.cancel() }
|
||||||
|
} else {
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
animationThreadHandler.post {
|
||||||
|
animatorTestHelpers.keys.forEach { it.cancel() }
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
latch.await(5, TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
animatorTestHelpers.clear()
|
||||||
|
animators.clear()
|
||||||
|
allAnimatedObjects.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum time (in milliseconds) to block the test thread while waiting for animations
|
||||||
|
* before throwing an exception.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun setBlockTimeout(timeoutMs: Long) {
|
||||||
|
PhysicsAnimatorTestUtils.timeoutMs = timeoutMs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether all animations should block the test thread until they end. This is typically
|
||||||
|
* the desired behavior, since you can invoke code that runs an animation and then assert things
|
||||||
|
* about its end state.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun setAllAnimationsBlock(block: Boolean) {
|
||||||
|
startBlocksUntilAnimationsEnd = block
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks the calling thread until animations of the given property on the target object end.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
@Throws(InterruptedException::class)
|
||||||
|
fun <T : Any> blockUntilAnimationsEnd(
|
||||||
|
animator: PhysicsAnimator<T>,
|
||||||
|
vararg properties: FloatPropertyCompat<in T>
|
||||||
|
) {
|
||||||
|
val animatingProperties = HashSet<FloatPropertyCompat<in T>>()
|
||||||
|
for (property in properties) {
|
||||||
|
if (animator.isPropertyAnimating(property)) {
|
||||||
|
animatingProperties.add(property)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (animatingProperties.size > 0) {
|
||||||
|
val latch = CountDownLatch(animatingProperties.size)
|
||||||
|
getAnimationTestHelper(animator).addTestEndListener(
|
||||||
|
object : PhysicsAnimator.EndListener<T> {
|
||||||
|
override fun onAnimationEnd(
|
||||||
|
target: T,
|
||||||
|
property: FloatPropertyCompat<in T>,
|
||||||
|
wasFling: Boolean,
|
||||||
|
canceled: Boolean,
|
||||||
|
finalValue: Float,
|
||||||
|
finalVelocity: Float,
|
||||||
|
allRelevantPropertyAnimsEnded: Boolean
|
||||||
|
) {
|
||||||
|
if (animatingProperties.contains(property)) {
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
latch.await(timeoutMs, TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks the calling thread until all animations of the given property (on all target objects)
|
||||||
|
* have ended. Useful when you don't have access to the objects being animated, but still need
|
||||||
|
* to wait for them to end so that other testable side effects occur (such as update/end
|
||||||
|
* listeners).
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
@Throws(InterruptedException::class)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <T : Any> blockUntilAnimationsEnd(
|
||||||
|
vararg properties: FloatPropertyCompat<in T>
|
||||||
|
) {
|
||||||
|
for (target in allAnimatedObjects) {
|
||||||
|
try {
|
||||||
|
blockUntilAnimationsEnd(
|
||||||
|
PhysicsAnimator.getInstance(target) as PhysicsAnimator<T>, *properties)
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
// Keep checking the other objects for ones whose types match the provided
|
||||||
|
// properties.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether any animation is currently running. */
|
||||||
|
@JvmStatic
|
||||||
|
fun isAnyAnimationRunning(): Boolean {
|
||||||
|
for (target in allAnimatedObjects) {
|
||||||
|
val animator = PhysicsAnimator.getInstance(target)
|
||||||
|
if (animator.isRunning()) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks the calling thread until the first animation frame in which predicate returns true. If
|
||||||
|
* the given object isn't animating, returns without blocking.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
@Throws(InterruptedException::class)
|
||||||
|
fun <T : Any> blockUntilFirstAnimationFrameWhereTrue(
|
||||||
|
animator: PhysicsAnimator<T>,
|
||||||
|
predicate: (T) -> Boolean
|
||||||
|
) {
|
||||||
|
if (animator.isRunning()) {
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
getAnimationTestHelper(animator).addTestUpdateListener(object : PhysicsAnimator
|
||||||
|
.UpdateListener<T> {
|
||||||
|
override fun onAnimationUpdateForProperty(
|
||||||
|
target: T,
|
||||||
|
values: UpdateMap<T>
|
||||||
|
) {
|
||||||
|
if (predicate(target)) {
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
latch.await(timeoutMs, TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the animator reported animation frame values to update listeners that satisfy
|
||||||
|
* the given matchers, in order. Not all frames need to satisfy a matcher - we'll run through
|
||||||
|
* all animation frames, and check them against the current predicate. If it returns false, we
|
||||||
|
* continue through the frames until it returns true, and then move on to the next matcher.
|
||||||
|
* Verification fails if we run out of frames while unsatisfied matchers remain.
|
||||||
|
*
|
||||||
|
* If verification is successful, all frames to this point are considered 'verified' and will be
|
||||||
|
* cleared. Subsequent calls to this method will start verification at the next animation frame.
|
||||||
|
*
|
||||||
|
* Example: Verify that an animation surpassed x = 50f before going negative.
|
||||||
|
* verifyAnimationUpdateFrames(
|
||||||
|
* animator, TRANSLATION_X,
|
||||||
|
* { u -> u.value > 50f },
|
||||||
|
* { u -> u.value < 0f })
|
||||||
|
*
|
||||||
|
* Example: verify that an animation went backwards at some point while still being on-screen.
|
||||||
|
* verifyAnimationUpdateFrames(
|
||||||
|
* animator, TRANSLATION_X,
|
||||||
|
* { u -> u.velocity < 0f && u.value >= 0f })
|
||||||
|
*
|
||||||
|
* This method is intended to help you test longer, more complicated animations where it's
|
||||||
|
* critical that certain values were reached. Using this method to test short animations can
|
||||||
|
* fail due to the animation having fewer frames than provided matchers. For example, an
|
||||||
|
* animation from x = 1f to x = 5f might only have two frames, at x = 3f and x = 5f. The
|
||||||
|
* following would then fail despite it seeming logically sound:
|
||||||
|
*
|
||||||
|
* verifyAnimationUpdateFrames(
|
||||||
|
* animator, TRANSLATION_X,
|
||||||
|
* { u -> u.value > 1f },
|
||||||
|
* { u -> u.value > 2f },
|
||||||
|
* { u -> u.value > 3f })
|
||||||
|
*
|
||||||
|
* Tests might also fail if your matchers are too granular, such as this example test after an
|
||||||
|
* animation from x = 0f to x = 100f. It's unlikely there was a frame specifically between 2f
|
||||||
|
* and 3f.
|
||||||
|
*
|
||||||
|
* verifyAnimationUpdateFrames(
|
||||||
|
* animator, TRANSLATION_X,
|
||||||
|
* { u -> u.value > 2f && u.value < 3f },
|
||||||
|
* { u -> u.value >= 50f })
|
||||||
|
*
|
||||||
|
* Failures will print a helpful log of all animation frames so you can see what caused the test
|
||||||
|
* to fail.
|
||||||
|
*/
|
||||||
|
fun <T : Any> verifyAnimationUpdateFrames(
|
||||||
|
animator: PhysicsAnimator<T>,
|
||||||
|
property: FloatPropertyCompat<in T>,
|
||||||
|
firstUpdateMatcher: UpdateMatcher,
|
||||||
|
vararg additionalUpdateMatchers: UpdateMatcher
|
||||||
|
) {
|
||||||
|
val updateFrames: UpdateFramesPerProperty<T> = getAnimationUpdateFrames(animator)
|
||||||
|
|
||||||
|
if (!updateFrames.containsKey(property)) {
|
||||||
|
error("No frames for given target object and property.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the frames to avoid a ConcurrentModificationException if the animation update
|
||||||
|
// listeners attempt to add a new frame while we're verifying these.
|
||||||
|
val framesForProperty = ArrayList(updateFrames[property]!!)
|
||||||
|
val matchers = ArrayDeque<UpdateMatcher>(
|
||||||
|
additionalUpdateMatchers.toList())
|
||||||
|
val frameTraceMessage = StringBuilder()
|
||||||
|
|
||||||
|
var curMatcher = firstUpdateMatcher
|
||||||
|
|
||||||
|
// Loop through the updates from the testable animator.
|
||||||
|
for (update in framesForProperty) {
|
||||||
|
// Check whether this frame satisfies the current matcher.
|
||||||
|
if (curMatcher(update)) {
|
||||||
|
// If that was the last unsatisfied matcher, we're good here. 'Verify' all remaining
|
||||||
|
// frames and return without failing.
|
||||||
|
if (matchers.size == 0) {
|
||||||
|
getAnimationUpdateFrames(animator).remove(property)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
frameTraceMessage.append("$update\t(satisfied matcher)\n")
|
||||||
|
curMatcher = matchers.pop() // Get the next matcher and keep going.
|
||||||
|
} else {
|
||||||
|
frameTraceMessage.append("${update}\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val readablePropertyName = PhysicsAnimator.getReadablePropertyName(property)
|
||||||
|
getAnimationUpdateFrames(animator).remove(property)
|
||||||
|
|
||||||
|
throw RuntimeException(
|
||||||
|
"Failed to verify animation frames for property $readablePropertyName: " +
|
||||||
|
"Provided ${additionalUpdateMatchers.size + 1} matchers, " +
|
||||||
|
"however ${matchers.size + 1} remained unsatisfied.\n\n" +
|
||||||
|
"All frames:\n$frameTraceMessage")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overload of [verifyAnimationUpdateFrames] that builds matchers for you, from given float
|
||||||
|
* values. For example, to verify that an animations passed from 0f to 50f to 100f back to 50f:
|
||||||
|
*
|
||||||
|
* verifyAnimationUpdateFrames(animator, TRANSLATION_X, 0f, 50f, 100f, 50f)
|
||||||
|
*
|
||||||
|
* This verifies that update frames were received with values of >= 0f, >= 50f, >= 100f, and
|
||||||
|
* <= 50f.
|
||||||
|
*
|
||||||
|
* The same caveats apply: short animations might not have enough frames to satisfy all of the
|
||||||
|
* matchers, and overly specific calls (such as 0f, 1f, 2f, 3f, etc. for an animation from
|
||||||
|
* x = 0f to x = 100f) might fail as the animation only had frames at 0f, 25f, 50f, 75f, and
|
||||||
|
* 100f. As with [verifyAnimationUpdateFrames], failures will print a helpful log of all frames
|
||||||
|
* so you can see what caused the test to fail.
|
||||||
|
*/
|
||||||
|
fun <T : Any> verifyAnimationUpdateFrames(
|
||||||
|
animator: PhysicsAnimator<T>,
|
||||||
|
property: FloatPropertyCompat<in T>,
|
||||||
|
startValue: Float,
|
||||||
|
firstTargetValue: Float,
|
||||||
|
vararg additionalTargetValues: Float
|
||||||
|
) {
|
||||||
|
val matchers = ArrayList<UpdateMatcher>()
|
||||||
|
|
||||||
|
val values = ArrayList<Float>().also {
|
||||||
|
it.add(firstTargetValue)
|
||||||
|
it.addAll(additionalTargetValues.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevVal = startValue
|
||||||
|
for (value in values) {
|
||||||
|
if (value > prevVal) {
|
||||||
|
matchers.add { update -> update.value >= value }
|
||||||
|
} else {
|
||||||
|
matchers.add { update -> update.value <= value }
|
||||||
|
}
|
||||||
|
|
||||||
|
prevVal = value
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyAnimationUpdateFrames(
|
||||||
|
animator, property, matchers[0], *matchers.drop(0).toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all of the values that have ever been reported to update listeners, per property.
|
||||||
|
*/
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <T : Any> getAnimationUpdateFrames(
|
||||||
|
animator: PhysicsAnimator<T>
|
||||||
|
): UpdateFramesPerProperty<T> {
|
||||||
|
return animatorTestHelpers[animator]?.getUpdates() as UpdateFramesPerProperty<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears animation frame updates from the given animator so they aren't used the next time its
|
||||||
|
* passed to [verifyAnimationUpdateFrames].
|
||||||
|
*/
|
||||||
|
fun <T : Any> clearAnimationUpdateFrames(animator: PhysicsAnimator<T>) {
|
||||||
|
animatorTestHelpers[animator]?.clearUpdates()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun <T> getAnimationTestHelper(animator: PhysicsAnimator<T>): AnimatorTestHelper<T> {
|
||||||
|
return animatorTestHelpers[animator] as AnimatorTestHelper<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for testing an animator. This replaces the animator's start action with
|
||||||
|
* [startForTest] and adds test listeners to enable other test utility behaviors. We build one
|
||||||
|
* these for each Animator and keep them around so we can access the updates.
|
||||||
|
*/
|
||||||
|
class AnimatorTestHelper<T> (private val animator: PhysicsAnimator<T>) {
|
||||||
|
|
||||||
|
/** All updates received for each property animation. */
|
||||||
|
private val allUpdates =
|
||||||
|
ArrayMap<FloatPropertyCompat<in T>, ArrayList<PhysicsAnimator.AnimationUpdate>>()
|
||||||
|
|
||||||
|
private val testEndListeners = ArrayList<PhysicsAnimator.EndListener<T>>()
|
||||||
|
private val testUpdateListeners = ArrayList<PhysicsAnimator.UpdateListener<T>>()
|
||||||
|
|
||||||
|
/** Whether we're currently in the middle of executing startInternal(). */
|
||||||
|
private var currentlyRunningStartInternal = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
animator.startAction = ::startForTest
|
||||||
|
animator.cancelAction = ::cancelForTest
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun addTestEndListener(listener: PhysicsAnimator.EndListener<T>) {
|
||||||
|
testEndListeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun addTestUpdateListener(listener: PhysicsAnimator.UpdateListener<T>) {
|
||||||
|
testUpdateListeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun getUpdates(): UpdateFramesPerProperty<T> {
|
||||||
|
return allUpdates
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun clearUpdates() {
|
||||||
|
allUpdates.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startForTest() {
|
||||||
|
// The testable animator needs to block the main thread until super.start() has been
|
||||||
|
// called, since callers expect .start() to be synchronous but we're posting it to a
|
||||||
|
// handler here. We may also continue blocking until all animations end, if
|
||||||
|
// startBlocksUntilAnimationsEnd = true.
|
||||||
|
val unblockLatch = CountDownLatch(if (startBlocksUntilAnimationsEnd) 2 else 1)
|
||||||
|
|
||||||
|
animationThreadHandler.post {
|
||||||
|
// Add an update listener that dispatches to any test update listeners added by
|
||||||
|
// tests.
|
||||||
|
animator.addUpdateListener(object : PhysicsAnimator.UpdateListener<T> {
|
||||||
|
override fun onAnimationUpdateForProperty(
|
||||||
|
target: T,
|
||||||
|
values: ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate>
|
||||||
|
) {
|
||||||
|
values.forEach { (property, value) ->
|
||||||
|
allUpdates.getOrPut(property, { ArrayList() }).add(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (listener in testUpdateListeners) {
|
||||||
|
listener.onAnimationUpdateForProperty(target, values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add an end listener that dispatches to any test end listeners added by tests, and
|
||||||
|
// unblocks the main thread if required.
|
||||||
|
animator.addEndListener(object : PhysicsAnimator.EndListener<T> {
|
||||||
|
override fun onAnimationEnd(
|
||||||
|
target: T,
|
||||||
|
property: FloatPropertyCompat<in T>,
|
||||||
|
wasFling: Boolean,
|
||||||
|
canceled: Boolean,
|
||||||
|
finalValue: Float,
|
||||||
|
finalVelocity: Float,
|
||||||
|
allRelevantPropertyAnimsEnded: Boolean
|
||||||
|
) {
|
||||||
|
for (listener in testEndListeners) {
|
||||||
|
listener.onAnimationEnd(
|
||||||
|
target, property, wasFling, canceled, finalValue, finalVelocity,
|
||||||
|
allRelevantPropertyAnimsEnded)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allRelevantPropertyAnimsEnded) {
|
||||||
|
testEndListeners.clear()
|
||||||
|
testUpdateListeners.clear()
|
||||||
|
|
||||||
|
if (startBlocksUntilAnimationsEnd) {
|
||||||
|
unblockLatch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
currentlyRunningStartInternal = true
|
||||||
|
animator.startInternal()
|
||||||
|
currentlyRunningStartInternal = false
|
||||||
|
unblockLatch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
unblockLatch.await(timeoutMs, TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelForTest(properties: Set<FloatPropertyCompat<in T>>) {
|
||||||
|
// If this was called from startInternal, we are already on the animation thread, and
|
||||||
|
// should just call cancelInternal rather than posting it. If we post it, the
|
||||||
|
// cancellation will occur after the rest of startInternal() and we'll immediately
|
||||||
|
// cancel the animation we worked so hard to start!
|
||||||
|
if (currentlyRunningStartInternal) {
|
||||||
|
animator.cancelInternal(properties)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val unblockLatch = CountDownLatch(1)
|
||||||
|
|
||||||
|
animationThreadHandler.post {
|
||||||
|
animator.cancelInternal(properties)
|
||||||
|
unblockLatch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
unblockLatch.await(timeoutMs, TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.animation
|
||||||
|
|
||||||
|
import android.animation.PointFEvaluator
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.graphics.PointF
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.util.DisplayMetrics
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.Choreographer
|
||||||
|
import android.view.SurfaceControl
|
||||||
|
import android.view.animation.Interpolator
|
||||||
|
import android.window.TransitionInfo
|
||||||
|
|
||||||
|
/** Creates animations that can be applied to windows/surfaces. */
|
||||||
|
object WindowAnimator {
|
||||||
|
|
||||||
|
/** Parameters defining a window bounds animation. */
|
||||||
|
data class BoundsAnimationParams(
|
||||||
|
val durationMs: Long,
|
||||||
|
val startOffsetYDp: Float = 0f,
|
||||||
|
val endOffsetYDp: Float = 0f,
|
||||||
|
val startScale: Float = 1f,
|
||||||
|
val endScale: Float = 1f,
|
||||||
|
val interpolator: Interpolator,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an animator to reposition and scale the bounds of the leash of the given change.
|
||||||
|
*
|
||||||
|
* @param displayMetrics the metrics of the display where the animation plays in
|
||||||
|
* @param boundsAnimDef the parameters for the animation itself (duration, scale, position)
|
||||||
|
* @param change the change to which the animation should be applied
|
||||||
|
* @param transaction the transaction to apply the animation to
|
||||||
|
*/
|
||||||
|
fun createBoundsAnimator(
|
||||||
|
displayMetrics: DisplayMetrics,
|
||||||
|
boundsAnimDef: BoundsAnimationParams,
|
||||||
|
change: TransitionInfo.Change,
|
||||||
|
transaction: SurfaceControl.Transaction,
|
||||||
|
): ValueAnimator {
|
||||||
|
val startPos =
|
||||||
|
getPosition(
|
||||||
|
displayMetrics,
|
||||||
|
change.endAbsBounds,
|
||||||
|
boundsAnimDef.startScale,
|
||||||
|
boundsAnimDef.startOffsetYDp,
|
||||||
|
)
|
||||||
|
val leash = change.leash
|
||||||
|
val endPos =
|
||||||
|
getPosition(
|
||||||
|
displayMetrics,
|
||||||
|
change.endAbsBounds,
|
||||||
|
boundsAnimDef.endScale,
|
||||||
|
boundsAnimDef.endOffsetYDp,
|
||||||
|
)
|
||||||
|
return ValueAnimator.ofObject(PointFEvaluator(), startPos, endPos).apply {
|
||||||
|
duration = boundsAnimDef.durationMs
|
||||||
|
interpolator = boundsAnimDef.interpolator
|
||||||
|
addUpdateListener { animation ->
|
||||||
|
val animPos = animation.animatedValue as PointF
|
||||||
|
val animScale =
|
||||||
|
interpolate(
|
||||||
|
boundsAnimDef.startScale,
|
||||||
|
boundsAnimDef.endScale,
|
||||||
|
animation.animatedFraction
|
||||||
|
)
|
||||||
|
transaction
|
||||||
|
.setPosition(leash, animPos.x, animPos.y)
|
||||||
|
.setScale(leash, animScale, animScale)
|
||||||
|
.setFrameTimeline(Choreographer.getInstance().vsyncId)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun interpolate(startVal: Float, endVal: Float, fraction: Float): Float {
|
||||||
|
require(fraction in 0.0f..1.0f)
|
||||||
|
return startVal + (endVal - startVal) * fraction
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPosition(
|
||||||
|
displayMetrics: DisplayMetrics,
|
||||||
|
bounds: Rect,
|
||||||
|
scale: Float,
|
||||||
|
offsetYDp: Float
|
||||||
|
) = PointF(bounds.left.toFloat(), bounds.top.toFloat()).apply {
|
||||||
|
check(scale in 0.0f..1.0f)
|
||||||
|
// Scale the bounds down with an anchor in the center
|
||||||
|
offset(
|
||||||
|
(bounds.width().toFloat() * (1 - scale) / 2),
|
||||||
|
(bounds.height().toFloat() * (1 - scale) / 2),
|
||||||
|
)
|
||||||
|
val offsetYPx =
|
||||||
|
TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
offsetYDp,
|
||||||
|
displayMetrics,
|
||||||
|
)
|
||||||
|
.toInt()
|
||||||
|
offset(/* dx= */ 0f, offsetYPx.toFloat())
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.annotations;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Inherited;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotates a method that or qualifies a provider runs aligned to the Choreographer SF vsync
|
||||||
|
* instead of the app vsync.
|
||||||
|
*/
|
||||||
|
@Documented
|
||||||
|
@Inherited
|
||||||
|
@Qualifier
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface ChoreographerSfVsync {}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.annotations;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotates a method or qualifies a provider that runs on the main-thread of the process using
|
||||||
|
* this library.
|
||||||
|
*/
|
||||||
|
@Qualifier
|
||||||
|
@Documented
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
public @interface ExternalMainThread {
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.annotations;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Inherited;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
|
||||||
|
/** Annotates a method or class that is called from an external thread to the Shell threads. */
|
||||||
|
@Documented
|
||||||
|
@Inherited
|
||||||
|
@Qualifier
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface ExternalThread {}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.annotations;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Inherited;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
|
||||||
|
/** Annotates a method or qualifies a provider that runs on the Shell animation-thread */
|
||||||
|
@Documented
|
||||||
|
@Inherited
|
||||||
|
@Qualifier
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface ShellAnimationThread {}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.annotations;
|
||||||
|
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Inherited;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
|
||||||
|
/** Annotates a method or qualifies a provider that runs on the shared background thread */
|
||||||
|
@Documented
|
||||||
|
@Inherited
|
||||||
|
@Qualifier
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface ShellBackgroundThread {
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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.shared.annotations;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Inherited;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
|
||||||
|
/** Annotates a method or qualifies a provider that runs on the Shell desktop thread */
|
||||||
|
@Documented
|
||||||
|
@Inherited
|
||||||
|
@Qualifier
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface ShellDesktopThread {
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.annotations;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Inherited;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
|
||||||
|
/** Annotates a method or qualifies a provider that runs on the Shell main-thread */
|
||||||
|
@Documented
|
||||||
|
@Inherited
|
||||||
|
@Qualifier
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface ShellMainThread {}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.annotations;
|
||||||
|
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Inherited;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
|
||||||
|
/** Annotates a method or qualifies a provider that runs on the Shell splashscreen-thread */
|
||||||
|
@Documented
|
||||||
|
@Inherited
|
||||||
|
@Qualifier
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface ShellSplashscreenThread {
|
||||||
|
}
|
||||||
+219
@@ -0,0 +1,219 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.graphics.Point
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.core.animation.Animator
|
||||||
|
import androidx.core.animation.AnimatorListenerAdapter
|
||||||
|
import androidx.core.animation.ObjectAnimator
|
||||||
|
import com.android.wm.shell.shared.bubbles.BaseBubblePinController.LocationChangeListener
|
||||||
|
import com.android.wm.shell.shared.bubbles.BubbleBarLocation.LEFT
|
||||||
|
import com.android.wm.shell.shared.bubbles.BubbleBarLocation.RIGHT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for common logic shared between different bubble views to support pinning bubble bar
|
||||||
|
* to left or right edge of screen.
|
||||||
|
*
|
||||||
|
* Handles drag events and allows a [LocationChangeListener] to be registered that is notified when
|
||||||
|
* location of the bubble bar should change.
|
||||||
|
*
|
||||||
|
* Shows a drop target when releasing a view would update the [BubbleBarLocation].
|
||||||
|
*/
|
||||||
|
abstract class BaseBubblePinController(private val screenSizeProvider: () -> Point) {
|
||||||
|
|
||||||
|
private var initialLocationOnLeft = false
|
||||||
|
private var onLeft = false
|
||||||
|
private var dismissZone: RectF? = null
|
||||||
|
private var stuckToDismissTarget = false
|
||||||
|
private var screenCenterX = 0
|
||||||
|
private var listener: LocationChangeListener? = null
|
||||||
|
private var dropTargetAnimator: ObjectAnimator? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal the controller that dragging interaction has started.
|
||||||
|
*
|
||||||
|
* @param initialLocationOnLeft side of the screen where bubble bar is pinned to
|
||||||
|
*/
|
||||||
|
fun onDragStart(initialLocationOnLeft: Boolean) {
|
||||||
|
this.initialLocationOnLeft = initialLocationOnLeft
|
||||||
|
onLeft = initialLocationOnLeft
|
||||||
|
screenCenterX = screenSizeProvider.invoke().x / 2
|
||||||
|
dismissZone = getExclusionRect()
|
||||||
|
listener?.onStart(if (initialLocationOnLeft) LEFT else RIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** View has moved to [x] and [y] screen coordinates */
|
||||||
|
fun onDragUpdate(x: Float, y: Float) {
|
||||||
|
if (dismissZone?.contains(x, y) == true) return
|
||||||
|
|
||||||
|
val wasOnLeft = onLeft
|
||||||
|
onLeft = x < screenCenterX
|
||||||
|
if (wasOnLeft != onLeft) {
|
||||||
|
onLocationChange(if (onLeft) LEFT else RIGHT)
|
||||||
|
} else if (stuckToDismissTarget) {
|
||||||
|
// Moved out of the dismiss view back to initial side, if we have a drop target, show it
|
||||||
|
getDropTargetView()?.apply { animateIn() }
|
||||||
|
}
|
||||||
|
// Make sure this gets cleared
|
||||||
|
stuckToDismissTarget = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Signal the controller that view has been dragged to dismiss view. */
|
||||||
|
fun onStuckToDismissTarget() {
|
||||||
|
stuckToDismissTarget = true
|
||||||
|
// Notify that location may be reset
|
||||||
|
val shouldResetLocation = onLeft != initialLocationOnLeft
|
||||||
|
if (shouldResetLocation) {
|
||||||
|
onLeft = initialLocationOnLeft
|
||||||
|
listener?.onChange(if (onLeft) LEFT else RIGHT)
|
||||||
|
}
|
||||||
|
getDropTargetView()?.apply {
|
||||||
|
animateOut {
|
||||||
|
if (shouldResetLocation) {
|
||||||
|
updateLocation(if (onLeft) LEFT else RIGHT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Signal the controller that dragging interaction has finished. */
|
||||||
|
fun onDragEnd() {
|
||||||
|
hideDropTarget()
|
||||||
|
dismissZone = null
|
||||||
|
listener?.onRelease(if (onLeft) LEFT else RIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [LocationChangeListener] that is notified when dragging interaction has resulted in bubble
|
||||||
|
* bar to be pinned on the other edge
|
||||||
|
*/
|
||||||
|
fun setListener(listener: LocationChangeListener?) {
|
||||||
|
this.listener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get width for exclusion rect where dismiss takes over drag */
|
||||||
|
protected abstract fun getExclusionRectWidth(): Float
|
||||||
|
|
||||||
|
/** Get height for exclusion rect where dismiss takes over drag */
|
||||||
|
protected abstract fun getExclusionRectHeight(): Float
|
||||||
|
|
||||||
|
/** Create the drop target view and attach it to the parent */
|
||||||
|
protected abstract fun createDropTargetView(): View
|
||||||
|
|
||||||
|
/** Get the drop target view if it exists */
|
||||||
|
protected abstract fun getDropTargetView(): View?
|
||||||
|
|
||||||
|
/** Remove the drop target view */
|
||||||
|
protected abstract fun removeDropTargetView(view: View)
|
||||||
|
|
||||||
|
/** Update size and location of the drop target view */
|
||||||
|
protected abstract fun updateLocation(location: BubbleBarLocation)
|
||||||
|
|
||||||
|
private fun onLocationChange(location: BubbleBarLocation) {
|
||||||
|
showDropTarget(location)
|
||||||
|
listener?.onChange(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getExclusionRect(): RectF {
|
||||||
|
val rect = RectF(0f, 0f, getExclusionRectWidth(), getExclusionRectHeight())
|
||||||
|
// Center it around the bottom center of the screen
|
||||||
|
val screenBottom = screenSizeProvider.invoke().y
|
||||||
|
rect.offsetTo(screenCenterX - rect.width() / 2, screenBottom - rect.height())
|
||||||
|
return rect
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showDropTarget(location: BubbleBarLocation) {
|
||||||
|
val targetView = getDropTargetView() ?: createDropTargetView().apply { alpha = 0f }
|
||||||
|
if (targetView.alpha > 0) {
|
||||||
|
targetView.animateOut {
|
||||||
|
updateLocation(location)
|
||||||
|
targetView.animateIn()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateLocation(location)
|
||||||
|
targetView.animateIn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideDropTarget() {
|
||||||
|
getDropTargetView()?.let { view -> view.animateOut { removeDropTargetView(view) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun View.animateIn() {
|
||||||
|
dropTargetAnimator?.cancel()
|
||||||
|
dropTargetAnimator =
|
||||||
|
ObjectAnimator.ofFloat(this, View.ALPHA, 1f)
|
||||||
|
.setDuration(DROP_TARGET_ALPHA_IN_DURATION)
|
||||||
|
.addEndAction { dropTargetAnimator = null }
|
||||||
|
dropTargetAnimator?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun View.animateOut(endAction: Runnable? = null) {
|
||||||
|
dropTargetAnimator?.cancel()
|
||||||
|
dropTargetAnimator =
|
||||||
|
ObjectAnimator.ofFloat(this, View.ALPHA, 0f)
|
||||||
|
.setDuration(DROP_TARGET_ALPHA_OUT_DURATION)
|
||||||
|
.addEndAction {
|
||||||
|
endAction?.run()
|
||||||
|
dropTargetAnimator = null
|
||||||
|
}
|
||||||
|
dropTargetAnimator?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T : Animator> T.addEndAction(runnable: Runnable): T {
|
||||||
|
addListener(
|
||||||
|
object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
runnable.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Receive updates on location changes */
|
||||||
|
interface LocationChangeListener {
|
||||||
|
/** Bubble bar dragging has started. Includes the initial location of the bar */
|
||||||
|
fun onStart(location: BubbleBarLocation) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bubble bar has been dragged to a new [BubbleBarLocation]. And the drag is still in
|
||||||
|
* progress.
|
||||||
|
*
|
||||||
|
* Triggered when drag gesture passes the middle of the screen and before touch up. Can be
|
||||||
|
* triggered multiple times per gesture.
|
||||||
|
*
|
||||||
|
* @param location new location as a result of the ongoing drag operation
|
||||||
|
*/
|
||||||
|
fun onChange(location: BubbleBarLocation) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bubble bar has been released in the [BubbleBarLocation].
|
||||||
|
*
|
||||||
|
* @param location final location of the bubble bar once drag is released
|
||||||
|
*/
|
||||||
|
fun onRelease(location: BubbleBarLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@VisibleForTesting const val DROP_TARGET_ALPHA_IN_DURATION = 150L
|
||||||
|
@VisibleForTesting const val DROP_TARGET_ALPHA_OUT_DURATION = 100L
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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.shared.bubbles;
|
||||||
|
|
||||||
|
import com.android.wm.shell.Flags;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bubble anything has some dependent flags, this class simplifies the checks.
|
||||||
|
* (TODO: b/389737359 - remove this when the feature is launched).
|
||||||
|
*/
|
||||||
|
public class BubbleAnythingFlagHelper {
|
||||||
|
|
||||||
|
private BubbleAnythingFlagHelper() {}
|
||||||
|
|
||||||
|
/** Whether creating any bubble or the overall bubble anything feature is enabled. */
|
||||||
|
public static boolean enableCreateAnyBubble() {
|
||||||
|
return enableBubbleAnything() || Flags.enableCreateAnyBubble();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether creating any bubble and transforming to fullscreen, or the overall bubble anything
|
||||||
|
* feature is enabled.
|
||||||
|
*/
|
||||||
|
public static boolean enableBubbleToFullscreen() {
|
||||||
|
return enableBubbleAnything()
|
||||||
|
|| (Flags.enableBubbleToFullscreen()
|
||||||
|
&& Flags.enableCreateAnyBubble());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the overall bubble anything feature is enabled. */
|
||||||
|
public static boolean enableBubbleAnything() {
|
||||||
|
return Flags.enableBubbleAnything();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.bubbles;
|
||||||
|
|
||||||
|
parcelable BubbleBarLocation;
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.annotation.IntDef
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The location of the bubble bar.
|
||||||
|
*/
|
||||||
|
enum class BubbleBarLocation : Parcelable {
|
||||||
|
/**
|
||||||
|
* Place bubble bar at the default location for the chosen system language.
|
||||||
|
* If an RTL language is used, it is on the left. Otherwise on the right.
|
||||||
|
*/
|
||||||
|
DEFAULT,
|
||||||
|
/** Default bubble bar location is overridden. Place bubble bar on the left. */
|
||||||
|
LEFT,
|
||||||
|
/** Default bubble bar location is overridden. Place bubble bar on the right. */
|
||||||
|
RIGHT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether bubble bar is pinned to the left edge or right edge.
|
||||||
|
*/
|
||||||
|
fun isOnLeft(isRtl: Boolean): Boolean {
|
||||||
|
if (this == DEFAULT) {
|
||||||
|
return isRtl
|
||||||
|
}
|
||||||
|
return this == LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
dest.writeString(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmField
|
||||||
|
val CREATOR = object : Parcelable.Creator<BubbleBarLocation> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): BubbleBarLocation {
|
||||||
|
return parcel.readString()?.let { valueOf(it) } ?: DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int) = arrayOfNulls<BubbleBarLocation>(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Define set of constants that allow to determine why location changed. */
|
||||||
|
@IntDef(
|
||||||
|
UpdateSource.DRAG_BAR,
|
||||||
|
UpdateSource.DRAG_BUBBLE,
|
||||||
|
UpdateSource.DRAG_EXP_VIEW,
|
||||||
|
UpdateSource.A11Y_ACTION_BAR,
|
||||||
|
UpdateSource.A11Y_ACTION_BUBBLE,
|
||||||
|
UpdateSource.A11Y_ACTION_EXP_VIEW,
|
||||||
|
UpdateSource.APP_ICON_DRAG,
|
||||||
|
UpdateSource.DRAG_TASK,
|
||||||
|
)
|
||||||
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
annotation class UpdateSource {
|
||||||
|
companion object {
|
||||||
|
/** Location changed from dragging the bar */
|
||||||
|
const val DRAG_BAR = 1
|
||||||
|
|
||||||
|
/** Location changed from dragging the bubble */
|
||||||
|
const val DRAG_BUBBLE = 2
|
||||||
|
|
||||||
|
/** Location changed from dragging the expanded view */
|
||||||
|
const val DRAG_EXP_VIEW = 3
|
||||||
|
|
||||||
|
/** Location changed via a11y action on the bar */
|
||||||
|
const val A11Y_ACTION_BAR = 4
|
||||||
|
|
||||||
|
/** Location changed via a11y action on the bubble */
|
||||||
|
const val A11Y_ACTION_BUBBLE = 5
|
||||||
|
|
||||||
|
/** Location changed via a11y action on the expanded view */
|
||||||
|
const val A11Y_ACTION_EXP_VIEW = 6
|
||||||
|
|
||||||
|
/** Location changed from dragging the application icon to the bubble bar */
|
||||||
|
const val APP_ICON_DRAG = 7
|
||||||
|
|
||||||
|
/** Location changed from dragging a running task to the bubble bar */
|
||||||
|
const val DRAG_TASK = 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.bubbles;
|
||||||
|
|
||||||
|
import android.annotation.NonNull;
|
||||||
|
import android.annotation.Nullable;
|
||||||
|
import android.graphics.Point;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an update to bubbles state. This is passed through
|
||||||
|
* {@link com.android.wm.shell.bubbles.IBubblesListener} to launcher so that taskbar may render
|
||||||
|
* bubbles. This should be kept this as minimal as possible in terms of data.
|
||||||
|
*/
|
||||||
|
public class BubbleBarUpdate implements Parcelable {
|
||||||
|
|
||||||
|
public static final String BUNDLE_KEY = "update";
|
||||||
|
|
||||||
|
public final boolean initialState;
|
||||||
|
public boolean expandedChanged;
|
||||||
|
public boolean expanded;
|
||||||
|
public boolean shouldShowEducation;
|
||||||
|
@Nullable
|
||||||
|
public String selectedBubbleKey;
|
||||||
|
@Nullable
|
||||||
|
public BubbleInfo addedBubble;
|
||||||
|
@Nullable
|
||||||
|
public BubbleInfo updatedBubble;
|
||||||
|
@Nullable
|
||||||
|
public String suppressedBubbleKey;
|
||||||
|
@Nullable
|
||||||
|
public String unsupressedBubbleKey;
|
||||||
|
@Nullable
|
||||||
|
public BubbleBarLocation bubbleBarLocation;
|
||||||
|
@Nullable
|
||||||
|
public Point expandedViewDropTargetSize;
|
||||||
|
public boolean showOverflowChanged;
|
||||||
|
public boolean showOverflow;
|
||||||
|
|
||||||
|
// This is only populated if bubbles have been removed.
|
||||||
|
public List<RemovedBubble> removedBubbles = new ArrayList<>();
|
||||||
|
|
||||||
|
// This is only populated if the order of the bubbles has changed.
|
||||||
|
public List<String> bubbleKeysInOrder = new ArrayList<>();
|
||||||
|
|
||||||
|
// This is only populated the first time a listener is connected so it gets the current state.
|
||||||
|
public List<BubbleInfo> currentBubbleList = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
|
public BubbleBarUpdate() {
|
||||||
|
this(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BubbleBarUpdate(boolean initialState) {
|
||||||
|
this.initialState = initialState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BubbleBarUpdate(Parcel parcel) {
|
||||||
|
initialState = parcel.readBoolean();
|
||||||
|
expandedChanged = parcel.readBoolean();
|
||||||
|
expanded = parcel.readBoolean();
|
||||||
|
shouldShowEducation = parcel.readBoolean();
|
||||||
|
selectedBubbleKey = parcel.readString();
|
||||||
|
addedBubble = parcel.readParcelable(BubbleInfo.class.getClassLoader(),
|
||||||
|
BubbleInfo.class);
|
||||||
|
updatedBubble = parcel.readParcelable(BubbleInfo.class.getClassLoader(),
|
||||||
|
BubbleInfo.class);
|
||||||
|
suppressedBubbleKey = parcel.readString();
|
||||||
|
unsupressedBubbleKey = parcel.readString();
|
||||||
|
removedBubbles = parcel.readParcelableList(new ArrayList<>(),
|
||||||
|
RemovedBubble.class.getClassLoader(), RemovedBubble.class);
|
||||||
|
parcel.readStringList(bubbleKeysInOrder);
|
||||||
|
currentBubbleList = parcel.readParcelableList(new ArrayList<>(),
|
||||||
|
BubbleInfo.class.getClassLoader(), BubbleInfo.class);
|
||||||
|
bubbleBarLocation = parcel.readParcelable(BubbleBarLocation.class.getClassLoader(),
|
||||||
|
BubbleBarLocation.class);
|
||||||
|
expandedViewDropTargetSize = parcel.readParcelable(Point.class.getClassLoader(),
|
||||||
|
Point.class);
|
||||||
|
showOverflowChanged = parcel.readBoolean();
|
||||||
|
showOverflow = parcel.readBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether anything has changed in this update.
|
||||||
|
*/
|
||||||
|
public boolean anythingChanged() {
|
||||||
|
return expandedChanged
|
||||||
|
|| selectedBubbleKey != null
|
||||||
|
|| addedBubble != null
|
||||||
|
|| updatedBubble != null
|
||||||
|
|| !removedBubbles.isEmpty()
|
||||||
|
|| !bubbleKeysInOrder.isEmpty()
|
||||||
|
|| suppressedBubbleKey != null
|
||||||
|
|| unsupressedBubbleKey != null
|
||||||
|
|| !currentBubbleList.isEmpty()
|
||||||
|
|| bubbleBarLocation != null
|
||||||
|
|| showOverflowChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "BubbleBarUpdate{"
|
||||||
|
+ " initialState=" + initialState
|
||||||
|
+ " expandedChanged=" + expandedChanged
|
||||||
|
+ " expanded=" + expanded
|
||||||
|
+ " selectedBubbleKey=" + selectedBubbleKey
|
||||||
|
+ " shouldShowEducation=" + shouldShowEducation
|
||||||
|
+ " addedBubble=" + addedBubble
|
||||||
|
+ " updatedBubble=" + updatedBubble
|
||||||
|
+ " suppressedBubbleKey=" + suppressedBubbleKey
|
||||||
|
+ " unsuppressedBubbleKey=" + unsupressedBubbleKey
|
||||||
|
+ " removedBubbles=" + removedBubbles
|
||||||
|
+ " bubbles=" + bubbleKeysInOrder
|
||||||
|
+ " currentBubbleList=" + currentBubbleList
|
||||||
|
+ " bubbleBarLocation=" + bubbleBarLocation
|
||||||
|
+ " expandedViewDropTargetSize=" + expandedViewDropTargetSize
|
||||||
|
+ " showOverflowChanged=" + showOverflowChanged
|
||||||
|
+ " showOverflow=" + showOverflow
|
||||||
|
+ " }";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel parcel, int flags) {
|
||||||
|
parcel.writeBoolean(initialState);
|
||||||
|
parcel.writeBoolean(expandedChanged);
|
||||||
|
parcel.writeBoolean(expanded);
|
||||||
|
parcel.writeBoolean(shouldShowEducation);
|
||||||
|
parcel.writeString(selectedBubbleKey);
|
||||||
|
parcel.writeParcelable(addedBubble, flags);
|
||||||
|
parcel.writeParcelable(updatedBubble, flags);
|
||||||
|
parcel.writeString(suppressedBubbleKey);
|
||||||
|
parcel.writeString(unsupressedBubbleKey);
|
||||||
|
parcel.writeParcelableList(removedBubbles, flags);
|
||||||
|
parcel.writeStringList(bubbleKeysInOrder);
|
||||||
|
parcel.writeParcelableList(currentBubbleList, flags);
|
||||||
|
parcel.writeParcelable(bubbleBarLocation, flags);
|
||||||
|
parcel.writeParcelable(expandedViewDropTargetSize, flags);
|
||||||
|
parcel.writeBoolean(showOverflowChanged);
|
||||||
|
parcel.writeBoolean(showOverflow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create update for initial set of values.
|
||||||
|
* <p>
|
||||||
|
* Used when bubble bar is newly created.
|
||||||
|
*/
|
||||||
|
public static BubbleBarUpdate createInitialState() {
|
||||||
|
return new BubbleBarUpdate(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final Creator<BubbleBarUpdate> CREATOR =
|
||||||
|
new Creator<>() {
|
||||||
|
public BubbleBarUpdate createFromParcel(Parcel source) {
|
||||||
|
return new BubbleBarUpdate(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BubbleBarUpdate[] newArray(int size) {
|
||||||
|
return new BubbleBarUpdate[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.bubbles;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants shared between bubbles in shell & things we have to do for bubbles in launcher.
|
||||||
|
*/
|
||||||
|
public class BubbleConstants {
|
||||||
|
|
||||||
|
/** The alpha for the scrim shown when bubbles are expanded. */
|
||||||
|
public static float BUBBLE_EXPANDED_SCRIM_ALPHA = .32f;
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide bounds for Bubbles drop targets that are shown when dragging over drag zones
|
||||||
|
*/
|
||||||
|
interface BubbleDropTargetBoundsProvider {
|
||||||
|
/**
|
||||||
|
* Get bubble bar expanded view visual drop target bounds on screen
|
||||||
|
*/
|
||||||
|
fun getBubbleBarExpandedViewDropTargetBounds(onLeft: Boolean): Rect
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the bar visual drop target bounds on screen
|
||||||
|
*/
|
||||||
|
fun getBarDropTargetBounds(onLeft: Boolean): Rect
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.bubbles;
|
||||||
|
|
||||||
|
import android.annotation.NonNull;
|
||||||
|
import android.annotation.Nullable;
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.graphics.drawable.Icon;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains information necessary to present a bubble.
|
||||||
|
*/
|
||||||
|
public class BubbleInfo implements Parcelable {
|
||||||
|
|
||||||
|
private final String mKey; // Same key as the Notification
|
||||||
|
private int mFlags; // Flags from BubbleMetadata
|
||||||
|
@Nullable
|
||||||
|
private final String mShortcutId;
|
||||||
|
private final int mUserId;
|
||||||
|
private final String mPackageName;
|
||||||
|
/**
|
||||||
|
* All notification bubbles require a shortcut to be set on the notification, however, the
|
||||||
|
* app could still specify an Icon and PendingIntent to use for the bubble. In that case
|
||||||
|
* this icon will be populated. If the bubble is entirely shortcut based, this will be null.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private final Icon mIcon;
|
||||||
|
@Nullable
|
||||||
|
private final String mTitle;
|
||||||
|
@Nullable
|
||||||
|
private final String mAppName;
|
||||||
|
private final boolean mIsImportantConversation;
|
||||||
|
private final boolean mShowAppBadge;
|
||||||
|
@Nullable
|
||||||
|
private final ParcelableFlyoutMessage mParcelableFlyoutMessage;
|
||||||
|
|
||||||
|
public BubbleInfo(String key, int flags, @Nullable String shortcutId, @Nullable Icon icon,
|
||||||
|
int userId, String packageName, @Nullable String title, @Nullable String appName,
|
||||||
|
boolean isImportantConversation, boolean showAppBadge,
|
||||||
|
@Nullable ParcelableFlyoutMessage flyoutMessage) {
|
||||||
|
mKey = key;
|
||||||
|
mFlags = flags;
|
||||||
|
mShortcutId = shortcutId;
|
||||||
|
mIcon = icon;
|
||||||
|
mUserId = userId;
|
||||||
|
mPackageName = packageName;
|
||||||
|
mTitle = title;
|
||||||
|
mAppName = appName;
|
||||||
|
mIsImportantConversation = isImportantConversation;
|
||||||
|
mShowAppBadge = showAppBadge;
|
||||||
|
mParcelableFlyoutMessage = flyoutMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BubbleInfo(Parcel source) {
|
||||||
|
mKey = source.readString();
|
||||||
|
mFlags = source.readInt();
|
||||||
|
mShortcutId = source.readString();
|
||||||
|
mIcon = source.readTypedObject(Icon.CREATOR);
|
||||||
|
mUserId = source.readInt();
|
||||||
|
mPackageName = source.readString();
|
||||||
|
mTitle = source.readString();
|
||||||
|
mAppName = source.readString();
|
||||||
|
mIsImportantConversation = source.readBoolean();
|
||||||
|
mShowAppBadge = source.readBoolean();
|
||||||
|
mParcelableFlyoutMessage = source.readParcelable(
|
||||||
|
ParcelableFlyoutMessage.class.getClassLoader(), ParcelableFlyoutMessage.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return mKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getShortcutId() {
|
||||||
|
return mShortcutId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Icon getIcon() {
|
||||||
|
return mIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFlags() {
|
||||||
|
return mFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getUserId() {
|
||||||
|
return mUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPackageName() {
|
||||||
|
return mPackageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getTitle() {
|
||||||
|
return mTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getAppName() {
|
||||||
|
return mAppName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isImportantConversation() {
|
||||||
|
return mIsImportantConversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean showAppBadge() {
|
||||||
|
return mShowAppBadge;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public ParcelableFlyoutMessage getParcelableFlyoutMessage() {
|
||||||
|
return mParcelableFlyoutMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this bubble is currently being hidden from the stack.
|
||||||
|
*/
|
||||||
|
public boolean isBubbleSuppressed() {
|
||||||
|
return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this bubble is able to be suppressed (i.e. has the developer opted into the API
|
||||||
|
* to
|
||||||
|
* hide the bubble when in the same content).
|
||||||
|
*/
|
||||||
|
public boolean isBubbleSuppressable() {
|
||||||
|
return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the notification for this bubble is hidden from the shade.
|
||||||
|
*/
|
||||||
|
public boolean isNotificationSuppressed() {
|
||||||
|
return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the flags for this bubble. */
|
||||||
|
public void setFlags(int flags) {
|
||||||
|
mFlags = flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof BubbleInfo)) return false;
|
||||||
|
BubbleInfo bubble = (BubbleInfo) o;
|
||||||
|
return Objects.equals(mKey, bubble.mKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return mKey.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel parcel, int flags) {
|
||||||
|
parcel.writeString(mKey);
|
||||||
|
parcel.writeInt(mFlags);
|
||||||
|
parcel.writeString(mShortcutId);
|
||||||
|
parcel.writeTypedObject(mIcon, flags);
|
||||||
|
parcel.writeInt(mUserId);
|
||||||
|
parcel.writeString(mPackageName);
|
||||||
|
parcel.writeString(mTitle);
|
||||||
|
parcel.writeString(mAppName);
|
||||||
|
parcel.writeBoolean(mIsImportantConversation);
|
||||||
|
parcel.writeBoolean(mShowAppBadge);
|
||||||
|
parcel.writeParcelable(mParcelableFlyoutMessage, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final Creator<BubbleInfo> CREATOR =
|
||||||
|
new Creator<>() {
|
||||||
|
public BubbleInfo createFromParcel(Parcel source) {
|
||||||
|
return new BubbleInfo(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BubbleInfo[] newArray(int size) {
|
||||||
|
return new BubbleInfo[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.ColorFilter
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.graphics.Outline
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import kotlin.math.atan
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
/** A drawable for the [BubblePopupView] that draws a popup background with a directional arrow */
|
||||||
|
class BubblePopupDrawable(val config: Config) : Drawable() {
|
||||||
|
/** The direction of the arrow in the popup drawable */
|
||||||
|
enum class ArrowDirection {
|
||||||
|
UP,
|
||||||
|
DOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The arrow position on the side of the popup bubble */
|
||||||
|
sealed class ArrowPosition {
|
||||||
|
object Start : ArrowPosition()
|
||||||
|
object Center : ArrowPosition()
|
||||||
|
object End : ArrowPosition()
|
||||||
|
class Custom(val value: Float) : ArrowPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The configuration for drawable features */
|
||||||
|
data class Config(
|
||||||
|
@ColorInt val color: Int,
|
||||||
|
val cornerRadius: Float,
|
||||||
|
val contentPadding: Int,
|
||||||
|
val arrowWidth: Float,
|
||||||
|
val arrowHeight: Float,
|
||||||
|
val arrowRadius: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The direction of the arrow in the popup drawable. It affects the content padding and requires
|
||||||
|
* it to be updated in the view.
|
||||||
|
*/
|
||||||
|
var arrowDirection: ArrowDirection by
|
||||||
|
Delegates.observable(ArrowDirection.UP) { _, _, _ -> requestPathUpdate() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arrow position along the X axis and its direction. The position is adjusted to the content
|
||||||
|
* corner radius when applied so it doesn't go into rounded corner area
|
||||||
|
*/
|
||||||
|
var arrowPosition: ArrowPosition by
|
||||||
|
Delegates.observable(ArrowPosition.Center) { _, _, _ -> requestPathUpdate() }
|
||||||
|
|
||||||
|
private val path = Path()
|
||||||
|
private val paint = Paint()
|
||||||
|
private var shouldUpdatePath = true
|
||||||
|
|
||||||
|
init {
|
||||||
|
paint.color = config.color
|
||||||
|
paint.style = Paint.Style.FILL
|
||||||
|
paint.isAntiAlias = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
updatePathIfNeeded()
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBoundsChange(bounds: Rect) {
|
||||||
|
requestPathUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Should be applied to the view padding if arrow direction changes */
|
||||||
|
override fun getPadding(padding: Rect): Boolean {
|
||||||
|
padding.set(
|
||||||
|
config.contentPadding,
|
||||||
|
config.contentPadding,
|
||||||
|
config.contentPadding,
|
||||||
|
config.contentPadding
|
||||||
|
)
|
||||||
|
when (arrowDirection) {
|
||||||
|
ArrowDirection.UP -> padding.top += config.arrowHeight.toInt()
|
||||||
|
ArrowDirection.DOWN -> padding.bottom += config.arrowHeight.toInt()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOutline(outline: Outline) {
|
||||||
|
updatePathIfNeeded()
|
||||||
|
outline.setPath(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOpacity(): Int {
|
||||||
|
return paint.alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAlpha(alpha: Int) {
|
||||||
|
paint.alpha = alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||||
|
paint.colorFilter = colorFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Schedules path update for the next redraw */
|
||||||
|
private fun requestPathUpdate() {
|
||||||
|
shouldUpdatePath = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the path if required, when bounds or arrow direction/position changes */
|
||||||
|
private fun updatePathIfNeeded() {
|
||||||
|
if (shouldUpdatePath) {
|
||||||
|
updatePath()
|
||||||
|
shouldUpdatePath = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the path value using the current bounds, config, arrow direction and position */
|
||||||
|
private fun updatePath() {
|
||||||
|
if (bounds.isEmpty) return
|
||||||
|
// Reset the path state
|
||||||
|
path.reset()
|
||||||
|
// The content rect where the filled rounded rect will be drawn
|
||||||
|
val contentRect = RectF(bounds)
|
||||||
|
when (arrowDirection) {
|
||||||
|
ArrowDirection.UP -> {
|
||||||
|
// Add rounded arrow pointing up to the path
|
||||||
|
addRoundedArrowPositioned(path, arrowPosition)
|
||||||
|
// Inset content rect by the arrow size from the top
|
||||||
|
contentRect.top += config.arrowHeight
|
||||||
|
}
|
||||||
|
ArrowDirection.DOWN -> {
|
||||||
|
val matrix = Matrix()
|
||||||
|
// Flip the path with the matrix to draw arrow pointing down
|
||||||
|
matrix.setScale(1f, -1f, bounds.width() / 2f, bounds.height() / 2f)
|
||||||
|
path.transform(matrix)
|
||||||
|
// Add rounded arrow with the flipped matrix applied, will point down
|
||||||
|
addRoundedArrowPositioned(path, arrowPosition)
|
||||||
|
// Restore the path matrix to the original state with inverted matrix
|
||||||
|
matrix.invert(matrix)
|
||||||
|
path.transform(matrix)
|
||||||
|
// Inset content rect by the arrow size from the bottom
|
||||||
|
contentRect.bottom -= config.arrowHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the content area rounded rect
|
||||||
|
path.addRoundRect(contentRect, config.cornerRadius, config.cornerRadius, Path.Direction.CW)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a rounded arrow pointing up in the horizontal position on the canvas */
|
||||||
|
private fun addRoundedArrowPositioned(path: Path, position: ArrowPosition) {
|
||||||
|
val matrix = Matrix()
|
||||||
|
var translationX = positionValue(position) - config.arrowWidth / 2
|
||||||
|
// Offset to position between rounded corners of the content view
|
||||||
|
translationX = translationX.coerceIn(config.cornerRadius,
|
||||||
|
bounds.width() - config.cornerRadius - config.arrowWidth)
|
||||||
|
// Translate to add the arrow in the center horizontally
|
||||||
|
matrix.setTranslate(-translationX, 0f)
|
||||||
|
path.transform(matrix)
|
||||||
|
// Add rounded arrow
|
||||||
|
addRoundedArrow(path)
|
||||||
|
// Restore the path matrix to the original state with inverted matrix
|
||||||
|
matrix.invert(matrix)
|
||||||
|
path.transform(matrix)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds a rounded arrow pointing up to the path, can be flipped if needed */
|
||||||
|
private fun addRoundedArrow(path: Path) {
|
||||||
|
// Theta is half of the angle inside the triangle tip
|
||||||
|
val thetaTan = config.arrowWidth / (config.arrowHeight * 2f)
|
||||||
|
val theta = atan(thetaTan)
|
||||||
|
val thetaDeg = Math.toDegrees(theta.toDouble()).toFloat()
|
||||||
|
// The center Y value of the circle for the triangle tip
|
||||||
|
val tipCircleCenterY = config.arrowRadius / sin(theta)
|
||||||
|
// The length from triangle tip to intersection point with the circle
|
||||||
|
val tipIntersectionSideLength = config.arrowRadius / thetaTan
|
||||||
|
// The offset from the top to the point of intersection
|
||||||
|
val intersectionTopOffset = tipIntersectionSideLength * cos(theta)
|
||||||
|
// The offset from the center to the point of intersection
|
||||||
|
val intersectionCenterOffset = tipIntersectionSideLength * sin(theta)
|
||||||
|
// The center X of the triangle
|
||||||
|
val arrowCenterX = config.arrowWidth / 2f
|
||||||
|
|
||||||
|
// Set initial position in bottom left of the arrow
|
||||||
|
path.moveTo(0f, config.arrowHeight)
|
||||||
|
// Add the left side of the triangle
|
||||||
|
path.lineTo(arrowCenterX - intersectionCenterOffset, intersectionTopOffset)
|
||||||
|
// Add the arc from the left to the right side of the triangle
|
||||||
|
path.arcTo(
|
||||||
|
/* left = */ arrowCenterX - config.arrowRadius,
|
||||||
|
/* top = */ tipCircleCenterY - config.arrowRadius,
|
||||||
|
/* right = */ arrowCenterX + config.arrowRadius,
|
||||||
|
/* bottom = */ tipCircleCenterY + config.arrowRadius,
|
||||||
|
/* startAngle = */ 180 + thetaDeg,
|
||||||
|
/* sweepAngle = */ 180 - (2 * thetaDeg),
|
||||||
|
/* forceMoveTo = */ false
|
||||||
|
)
|
||||||
|
// Add the right side of the triangle
|
||||||
|
path.lineTo(config.arrowWidth, config.arrowHeight)
|
||||||
|
// Close the path
|
||||||
|
path.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The value of the arrow position provided the position and current bounds */
|
||||||
|
private fun positionValue(position: ArrowPosition): Float {
|
||||||
|
return when (position) {
|
||||||
|
is ArrowPosition.Start -> 0f
|
||||||
|
is ArrowPosition.Center -> bounds.width().toFloat() / 2f
|
||||||
|
is ArrowPosition.End -> bounds.width().toFloat()
|
||||||
|
is ArrowPosition.Custom -> position.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
|
||||||
|
/** A popup container view that uses [BubblePopupDrawable] as a background */
|
||||||
|
open class BubblePopupView
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0,
|
||||||
|
defStyleRes: Int = 0
|
||||||
|
) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
|
var popupDrawable: BubblePopupDrawable? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the popup drawable with the config provided. Required to remove dependency on local
|
||||||
|
* resources
|
||||||
|
*/
|
||||||
|
fun setupBackground(config: BubblePopupDrawable.Config) {
|
||||||
|
popupDrawable = BubblePopupDrawable(config)
|
||||||
|
background = popupDrawable
|
||||||
|
forceLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the arrow direction for the background drawable and updates the padding to fit the
|
||||||
|
* content inside of the popup drawable
|
||||||
|
*/
|
||||||
|
fun setArrowDirection(direction: BubblePopupDrawable.ArrowDirection) {
|
||||||
|
popupDrawable?.let {
|
||||||
|
it.arrowDirection = direction
|
||||||
|
val padding = Rect()
|
||||||
|
if (it.getPadding(padding)) {
|
||||||
|
setPadding(padding.left, padding.top, padding.right, padding.bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the arrow position for the background drawable and triggers redraw */
|
||||||
|
fun setArrowPosition(position: BubblePopupDrawable.ArrowPosition) {
|
||||||
|
popupDrawable?.let {
|
||||||
|
it.arrowPosition = position
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManagerPolicyConstants
|
||||||
|
import com.android.internal.R
|
||||||
|
|
||||||
|
/** Simplifies accessing context fields. */
|
||||||
|
object ContextUtils {
|
||||||
|
|
||||||
|
/** Gets navigation mode. */
|
||||||
|
@JvmStatic
|
||||||
|
val Context.navigationMode: Int
|
||||||
|
get() = resources.getInteger(R.integer.config_navBarInteractionMode)
|
||||||
|
|
||||||
|
/** Returns whether the navigation mode is gestures. */
|
||||||
|
@JvmStatic
|
||||||
|
val Context.isGestureNavigationMode: Boolean
|
||||||
|
get() = navigationMode == WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL
|
||||||
|
|
||||||
|
/** Returns whether layout direction is rtl. */
|
||||||
|
@JvmStatic
|
||||||
|
val Context.isRtl: Boolean
|
||||||
|
get() = resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
import android.graphics.Insets
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View.LAYOUT_DIRECTION_RTL
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import android.view.WindowManager
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
import com.android.wm.shell.shared.ShellSharedConstants.SMALL_TABLET_MAX_EDGE_DP
|
||||||
|
|
||||||
|
/** Contains device configuration used for positioning bubbles on the screen. */
|
||||||
|
data class DeviceConfig(
|
||||||
|
val isLargeScreen: Boolean,
|
||||||
|
val isSmallTablet: Boolean,
|
||||||
|
val isLandscape: Boolean,
|
||||||
|
val isRtl: Boolean,
|
||||||
|
val windowBounds: Rect,
|
||||||
|
val insets: Insets
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val LARGE_SCREEN_MIN_EDGE_DP = 600
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun create(context: Context, windowManager: WindowManager): DeviceConfig {
|
||||||
|
val windowMetrics = windowManager.currentWindowMetrics
|
||||||
|
val metricInsets = windowMetrics.windowInsets
|
||||||
|
val insets = metricInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
|
||||||
|
or WindowInsets.Type.statusBars()
|
||||||
|
or WindowInsets.Type.displayCutout())
|
||||||
|
val windowBounds = windowMetrics.bounds
|
||||||
|
val config: Configuration = context.resources.configuration
|
||||||
|
val isLandscape = context.resources.configuration.orientation == ORIENTATION_LANDSCAPE
|
||||||
|
val isRtl = context.resources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL
|
||||||
|
return DeviceConfig(
|
||||||
|
isLargeScreen = isLargeScreen(config),
|
||||||
|
isSmallTablet = isSmallTablet(context),
|
||||||
|
isLandscape = isLandscape,
|
||||||
|
isRtl = isRtl,
|
||||||
|
windowBounds = windowBounds,
|
||||||
|
insets = insets
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isSmallTablet(context: Context): Boolean {
|
||||||
|
val config: Configuration = context.resources.configuration
|
||||||
|
if (!isLargeScreen(config)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val largestEdgeDp = max(config.screenWidthDp, config.screenHeightDp)
|
||||||
|
return largestEdgeDp < SMALL_TABLET_MAX_EDGE_DP
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isLargeScreen(config: Configuration) =
|
||||||
|
config.smallestScreenWidthDp >= LARGE_SCREEN_MIN_EDGE_DP
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.bubbles;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import androidx.annotation.DimenRes;
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Circular view with a semitransparent, circular background with an 'X' inside it.
|
||||||
|
*
|
||||||
|
* This is used by both Bubbles and PIP as the dismiss target.
|
||||||
|
*/
|
||||||
|
public class DismissCircleView extends FrameLayout {
|
||||||
|
@DrawableRes int mBackgroundResId;
|
||||||
|
@DimenRes int mIconSizeResId;
|
||||||
|
|
||||||
|
private final ImageView mIconView = new ImageView(getContext());
|
||||||
|
|
||||||
|
public DismissCircleView(Context context) {
|
||||||
|
super(context);
|
||||||
|
addView(mIconView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onConfigurationChanged(Configuration newConfig) {
|
||||||
|
super.onConfigurationChanged(newConfig);
|
||||||
|
setBackground(ContextCompat.getDrawable(getContext(), mBackgroundResId));
|
||||||
|
setViewSizes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up view with the provided resource ids.
|
||||||
|
* Decouples resource dependency in order to be used externally (e.g. Launcher)
|
||||||
|
*
|
||||||
|
* @param backgroundResId drawable resource id of the circle background
|
||||||
|
* @param iconResId drawable resource id of the icon for the dismiss view
|
||||||
|
* @param iconSizeResId dimen resource id of the icon size
|
||||||
|
*/
|
||||||
|
public void setup(@DrawableRes int backgroundResId, @DrawableRes int iconResId,
|
||||||
|
@DimenRes int iconSizeResId) {
|
||||||
|
mBackgroundResId = backgroundResId;
|
||||||
|
mIconSizeResId = iconSizeResId;
|
||||||
|
|
||||||
|
setBackground(ContextCompat.getDrawable(getContext(), backgroundResId));
|
||||||
|
mIconView.setImageDrawable(ContextCompat.getDrawable(getContext(), iconResId));
|
||||||
|
setViewSizes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retrieves the current dimensions for the icon and circle and applies them. */
|
||||||
|
private void setViewSizes() {
|
||||||
|
final int iconSize = getResources().getDimensionPixelSize(mIconSizeResId);
|
||||||
|
mIconView.setLayoutParams(
|
||||||
|
new FrameLayout.LayoutParams(iconSize, iconSize, Gravity.CENTER));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.util.IntProperty
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.annotation.DimenRes
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.dynamicanimation.animation.DynamicAnimation
|
||||||
|
import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY
|
||||||
|
import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW
|
||||||
|
import com.android.wm.shell.shared.animation.PhysicsAnimator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View that handles interactions between DismissCircleView and BubbleStackView.
|
||||||
|
*
|
||||||
|
* @note [setup] method should be called after initialisation
|
||||||
|
*/
|
||||||
|
class DismissView(context: Context) : FrameLayout(context) {
|
||||||
|
/**
|
||||||
|
* The configuration is used to provide module specific resource ids
|
||||||
|
*
|
||||||
|
* @see [setup] method
|
||||||
|
*/
|
||||||
|
data class Config(
|
||||||
|
/** The resource id to set on the dismiss target circle view */
|
||||||
|
val dismissViewResId: Int,
|
||||||
|
/** dimen resource id of the dismiss target circle view size */
|
||||||
|
@DimenRes val targetSizeResId: Int,
|
||||||
|
/** dimen resource id of the icon size in the dismiss target */
|
||||||
|
@DimenRes val iconSizeResId: Int,
|
||||||
|
/** dimen resource id of the bottom margin for the dismiss target */
|
||||||
|
@DimenRes var bottomMarginResId: Int,
|
||||||
|
/** dimen resource id of the height for dismiss area gradient */
|
||||||
|
@DimenRes val floatingGradientHeightResId: Int,
|
||||||
|
/** color resource id of the dismiss area gradient color */
|
||||||
|
@ColorRes val floatingGradientColorResId: Int,
|
||||||
|
/** drawable resource id of the dismiss target background */
|
||||||
|
@DrawableRes val backgroundResId: Int,
|
||||||
|
/** drawable resource id of the icon for the dismiss target */
|
||||||
|
@DrawableRes val iconResId: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SHOULD_SETUP =
|
||||||
|
"The view isn't ready. Should be called after `setup`"
|
||||||
|
private val TAG = DismissView::class.simpleName
|
||||||
|
}
|
||||||
|
|
||||||
|
var circle = DismissCircleView(context)
|
||||||
|
var isShowing = false
|
||||||
|
var config: Config? = null
|
||||||
|
|
||||||
|
private val animator = PhysicsAnimator.getInstance(circle)
|
||||||
|
private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY)
|
||||||
|
private val DISMISS_SCRIM_FADE_MS = 200L
|
||||||
|
private var wm: WindowManager =
|
||||||
|
context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||||
|
private var gradientDrawable: GradientDrawable? = null
|
||||||
|
|
||||||
|
private val GRADIENT_ALPHA: IntProperty<GradientDrawable> =
|
||||||
|
object : IntProperty<GradientDrawable>("alpha") {
|
||||||
|
override fun setValue(d: GradientDrawable, percent: Int) {
|
||||||
|
d.alpha = percent
|
||||||
|
}
|
||||||
|
override fun get(d: GradientDrawable): Int {
|
||||||
|
return d.alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
setClipToPadding(false)
|
||||||
|
setClipChildren(false)
|
||||||
|
setVisibility(View.INVISIBLE)
|
||||||
|
addView(circle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up view with the provided resource ids.
|
||||||
|
*
|
||||||
|
* Decouples resource dependency in order to be used externally (e.g. Launcher). Usually called
|
||||||
|
* with default params in module specific extension:
|
||||||
|
* @see [DismissView.setup] in DismissViewExt.kt
|
||||||
|
*/
|
||||||
|
fun setup(config: Config) {
|
||||||
|
this.config = config
|
||||||
|
|
||||||
|
// Setup layout
|
||||||
|
layoutParams = LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
resources.getDimensionPixelSize(config.floatingGradientHeightResId),
|
||||||
|
Gravity.BOTTOM)
|
||||||
|
updatePadding()
|
||||||
|
|
||||||
|
// Setup gradient
|
||||||
|
gradientDrawable = createGradient(color = config.floatingGradientColorResId)
|
||||||
|
setBackgroundDrawable(gradientDrawable)
|
||||||
|
|
||||||
|
// Setup DismissCircleView
|
||||||
|
circle.id = config.dismissViewResId
|
||||||
|
circle.setup(config.backgroundResId, config.iconResId, config.iconSizeResId)
|
||||||
|
val targetSize: Int = resources.getDimensionPixelSize(config.targetSizeResId)
|
||||||
|
circle.layoutParams = LayoutParams(targetSize, targetSize,
|
||||||
|
Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)
|
||||||
|
// Initial position with circle offscreen so it's animated up
|
||||||
|
circle.translationY = resources.getDimensionPixelSize(config.floatingGradientHeightResId)
|
||||||
|
.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animates this view in.
|
||||||
|
*/
|
||||||
|
fun show() {
|
||||||
|
if (isShowing) return
|
||||||
|
val gradientDrawable = checkExists(gradientDrawable) ?: return
|
||||||
|
isShowing = true
|
||||||
|
setVisibility(View.VISIBLE)
|
||||||
|
val alphaAnim = ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA,
|
||||||
|
gradientDrawable.alpha, 255)
|
||||||
|
alphaAnim.setDuration(DISMISS_SCRIM_FADE_MS)
|
||||||
|
alphaAnim.start()
|
||||||
|
|
||||||
|
animator.cancel()
|
||||||
|
animator
|
||||||
|
.spring(DynamicAnimation.TRANSLATION_Y, 0f, spring)
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animates this view out, as well as the circle that encircles the bubbles, if they
|
||||||
|
* were dragged into the target and encircled.
|
||||||
|
*/
|
||||||
|
fun hide() {
|
||||||
|
if (!isShowing) return
|
||||||
|
val gradientDrawable = checkExists(gradientDrawable) ?: return
|
||||||
|
isShowing = false
|
||||||
|
val alphaAnim = ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA,
|
||||||
|
gradientDrawable.alpha, 0)
|
||||||
|
alphaAnim.setDuration(DISMISS_SCRIM_FADE_MS)
|
||||||
|
alphaAnim.start()
|
||||||
|
animator
|
||||||
|
.spring(DynamicAnimation.TRANSLATION_Y, height.toFloat(),
|
||||||
|
spring)
|
||||||
|
.withEndActions({
|
||||||
|
visibility = View.INVISIBLE
|
||||||
|
circle.scaleX = 1f
|
||||||
|
circle.scaleY = 1f
|
||||||
|
})
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the animator for the dismiss target.
|
||||||
|
*/
|
||||||
|
fun cancelAnimators() {
|
||||||
|
animator.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateResources() {
|
||||||
|
val config = checkExists(config) ?: return
|
||||||
|
updatePadding()
|
||||||
|
layoutParams.height = resources.getDimensionPixelSize(config.floatingGradientHeightResId)
|
||||||
|
val targetSize = resources.getDimensionPixelSize(config.targetSizeResId)
|
||||||
|
circle.layoutParams.width = targetSize
|
||||||
|
circle.layoutParams.height = targetSize
|
||||||
|
circle.requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createGradient(@ColorRes color: Int): GradientDrawable {
|
||||||
|
val gradientColor = ContextCompat.getColor(context, color)
|
||||||
|
val alpha = 0.7f * 255
|
||||||
|
val gradientColorWithAlpha = Color.argb(alpha.toInt(),
|
||||||
|
Color.red(gradientColor),
|
||||||
|
Color.green(gradientColor),
|
||||||
|
Color.blue(gradientColor))
|
||||||
|
val gd = GradientDrawable(
|
||||||
|
GradientDrawable.Orientation.BOTTOM_TOP,
|
||||||
|
intArrayOf(gradientColorWithAlpha, Color.TRANSPARENT))
|
||||||
|
gd.setDither(true)
|
||||||
|
gd.setAlpha(0)
|
||||||
|
return gd
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePadding() {
|
||||||
|
val config = checkExists(config) ?: return
|
||||||
|
val insets: WindowInsets = wm.getCurrentWindowMetrics().getWindowInsets()
|
||||||
|
val navInset = insets.getInsetsIgnoringVisibility(
|
||||||
|
WindowInsets.Type.navigationBars())
|
||||||
|
setPadding(0, 0, 0, navInset.bottom +
|
||||||
|
resources.getDimensionPixelSize(config.bottomMarginResId))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the value is set up and exists, if not logs an exception.
|
||||||
|
* Used for convenient logging in case `setup` wasn't called before
|
||||||
|
*
|
||||||
|
* @return value provided as argument
|
||||||
|
*/
|
||||||
|
private fun <T>checkExists(value: T?): T? {
|
||||||
|
if (value == null) Log.e(TAG, SHOULD_SETUP)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an invisible area on the screen that determines what happens to a dragged object if it
|
||||||
|
* is released in that area.
|
||||||
|
*
|
||||||
|
* [bounds] are the bounds of the drag zone. Drag zones have an associated drop target that serves
|
||||||
|
* as visual feedback hinting what would happen if the object is released. When a dragged object is
|
||||||
|
* dragged into a drag zone, the associated drop target will be displayed. Not all drag zones have
|
||||||
|
* drop targets; only those that are made visible by Bubbles do.
|
||||||
|
*/
|
||||||
|
sealed interface DragZone {
|
||||||
|
|
||||||
|
/** The bounds of this drag zone. */
|
||||||
|
val bounds: Rect
|
||||||
|
/** The bounds of the drop target associated with this drag zone. */
|
||||||
|
val dropTarget: Rect?
|
||||||
|
|
||||||
|
fun contains(x: Int, y: Int) = bounds.contains(x, y)
|
||||||
|
|
||||||
|
/** Represents the bubble drag area on the screen. */
|
||||||
|
sealed class Bubble(override val bounds: Rect, override val dropTarget: Rect) : DragZone {
|
||||||
|
data class Left(override val bounds: Rect, override val dropTarget: Rect) :
|
||||||
|
Bubble(bounds, dropTarget)
|
||||||
|
|
||||||
|
data class Right(override val bounds: Rect, override val dropTarget: Rect) :
|
||||||
|
Bubble(bounds, dropTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Represents dragging to Desktop Window. */
|
||||||
|
data class DesktopWindow(override val bounds: Rect, override val dropTarget: Rect) : DragZone
|
||||||
|
|
||||||
|
/** Represents dragging to Full Screen. */
|
||||||
|
data class FullScreen(override val bounds: Rect, override val dropTarget: Rect) : DragZone
|
||||||
|
|
||||||
|
/** Represents dragging to dismiss. */
|
||||||
|
data class Dismiss(override val bounds: Rect) : DragZone {
|
||||||
|
override val dropTarget: Rect? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Represents dragging to enter Split or replace a Split app. */
|
||||||
|
sealed class Split(override val bounds: Rect) : DragZone {
|
||||||
|
override val dropTarget: Rect? = null
|
||||||
|
|
||||||
|
data class Left(override val bounds: Rect) : Split(bounds)
|
||||||
|
|
||||||
|
data class Right(override val bounds: Rect) : Split(bounds)
|
||||||
|
|
||||||
|
data class Top(override val bounds: Rect) : Split(bounds)
|
||||||
|
|
||||||
|
data class Bottom(override val bounds: Rect) : Split(bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,577 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.util.TypedValue
|
||||||
|
import androidx.annotation.DimenRes
|
||||||
|
import com.android.wm.shell.shared.R
|
||||||
|
import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode
|
||||||
|
|
||||||
|
/** A class for creating drag zones for dragging bubble objects or dragging into bubbles. */
|
||||||
|
class DragZoneFactory(
|
||||||
|
private val context: Context,
|
||||||
|
private val deviceConfig: DeviceConfig,
|
||||||
|
private val splitScreenModeChecker: SplitScreenModeChecker,
|
||||||
|
private val desktopWindowModeChecker: DesktopWindowModeChecker,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val windowBounds: Rect
|
||||||
|
get() = deviceConfig.windowBounds
|
||||||
|
|
||||||
|
private var dismissDragZoneSize = 0
|
||||||
|
private var bubbleDragZoneTabletSize = 0
|
||||||
|
private var bubbleDragZoneFoldableSize = 0
|
||||||
|
private var fullScreenDragZoneWidth = 0
|
||||||
|
private var fullScreenDragZoneHeight = 0
|
||||||
|
private var desktopWindowDragZoneWidth = 0
|
||||||
|
private var desktopWindowDragZoneHeight = 0
|
||||||
|
private var desktopWindowFromExpandedViewDragZoneWidth = 0
|
||||||
|
private var desktopWindowFromExpandedViewDragZoneHeight = 0
|
||||||
|
private var splitFromBubbleDragZoneHeight = 0
|
||||||
|
private var splitFromBubbleDragZoneWidth = 0
|
||||||
|
private var hSplitFromExpandedViewDragZoneWidth = 0
|
||||||
|
private var vSplitFromExpandedViewDragZoneWidth = 0
|
||||||
|
private var vSplitFromExpandedViewDragZoneHeightTablet = 0
|
||||||
|
private var vSplitFromExpandedViewDragZoneHeightFoldTall = 0
|
||||||
|
private var vSplitFromExpandedViewDragZoneHeightFoldShort = 0
|
||||||
|
|
||||||
|
private var fullScreenDropTargetPadding = 0
|
||||||
|
private var desktopWindowDropTargetPaddingSmall = 0
|
||||||
|
private var desktopWindowDropTargetPaddingLarge = 0
|
||||||
|
private var expandedViewDropTargetWidth = 0
|
||||||
|
private var expandedViewDropTargetHeight = 0
|
||||||
|
private var expandedViewDropTargetPaddingBottom = 0
|
||||||
|
private var expandedViewDropTargetPaddingHorizontal = 0
|
||||||
|
|
||||||
|
private val fullScreenDropTarget: Rect
|
||||||
|
get() =
|
||||||
|
Rect(windowBounds).apply {
|
||||||
|
inset(fullScreenDropTargetPadding, fullScreenDropTargetPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val desktopWindowDropTarget: Rect
|
||||||
|
get() =
|
||||||
|
Rect(windowBounds).apply {
|
||||||
|
if (deviceConfig.isLandscape) {
|
||||||
|
inset(
|
||||||
|
/* dx= */ desktopWindowDropTargetPaddingLarge,
|
||||||
|
/* dy= */ desktopWindowDropTargetPaddingSmall
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
inset(
|
||||||
|
/* dx= */ desktopWindowDropTargetPaddingSmall,
|
||||||
|
/* dy= */ desktopWindowDropTargetPaddingLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val expandedViewDropTargetLeft: Rect
|
||||||
|
get() =
|
||||||
|
Rect(
|
||||||
|
expandedViewDropTargetPaddingHorizontal,
|
||||||
|
windowBounds.bottom -
|
||||||
|
expandedViewDropTargetPaddingBottom -
|
||||||
|
expandedViewDropTargetHeight,
|
||||||
|
expandedViewDropTargetWidth + expandedViewDropTargetPaddingHorizontal,
|
||||||
|
windowBounds.bottom - expandedViewDropTargetPaddingBottom
|
||||||
|
)
|
||||||
|
|
||||||
|
private val expandedViewDropTargetRight: Rect
|
||||||
|
get() =
|
||||||
|
Rect(
|
||||||
|
windowBounds.right -
|
||||||
|
expandedViewDropTargetPaddingHorizontal -
|
||||||
|
expandedViewDropTargetWidth,
|
||||||
|
windowBounds.bottom -
|
||||||
|
expandedViewDropTargetPaddingBottom -
|
||||||
|
expandedViewDropTargetHeight,
|
||||||
|
windowBounds.right - expandedViewDropTargetPaddingHorizontal,
|
||||||
|
windowBounds.bottom - expandedViewDropTargetPaddingBottom
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
onConfigurationUpdated()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates all dimensions after a configuration change. */
|
||||||
|
fun onConfigurationUpdated() {
|
||||||
|
// TODO b/396539130: Use the shared xml resources once we can easily access them from
|
||||||
|
// launcher
|
||||||
|
dismissDragZoneSize =
|
||||||
|
if (deviceConfig.isSmallTablet) 140.dpToPx() else 200.dpToPx()
|
||||||
|
bubbleDragZoneTabletSize = 200.dpToPx()
|
||||||
|
bubbleDragZoneFoldableSize = 140.dpToPx()
|
||||||
|
fullScreenDragZoneWidth = 512.dpToPx()
|
||||||
|
fullScreenDragZoneHeight = 44.dpToPx()
|
||||||
|
desktopWindowDragZoneWidth = 880.dpToPx()
|
||||||
|
desktopWindowDragZoneHeight = 300.dpToPx()
|
||||||
|
desktopWindowFromExpandedViewDragZoneWidth = 200.dpToPx()
|
||||||
|
desktopWindowFromExpandedViewDragZoneHeight = 350.dpToPx()
|
||||||
|
splitFromBubbleDragZoneHeight = 100.dpToPx()
|
||||||
|
splitFromBubbleDragZoneWidth = 60.dpToPx()
|
||||||
|
hSplitFromExpandedViewDragZoneWidth = 60.dpToPx()
|
||||||
|
vSplitFromExpandedViewDragZoneWidth = 200.dpToPx()
|
||||||
|
vSplitFromExpandedViewDragZoneHeightTablet = 285.dpToPx()
|
||||||
|
vSplitFromExpandedViewDragZoneHeightFoldTall = 150.dpToPx()
|
||||||
|
vSplitFromExpandedViewDragZoneHeightFoldShort = 100.dpToPx()
|
||||||
|
fullScreenDropTargetPadding = 20.dpToPx()
|
||||||
|
desktopWindowDropTargetPaddingSmall = 100.dpToPx()
|
||||||
|
desktopWindowDropTargetPaddingLarge = 130.dpToPx()
|
||||||
|
expandedViewDropTargetWidth = 330.dpToPx()
|
||||||
|
expandedViewDropTargetHeight = 578.dpToPx()
|
||||||
|
expandedViewDropTargetPaddingBottom = 108.dpToPx()
|
||||||
|
expandedViewDropTargetPaddingHorizontal = 24.dpToPx()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.resolveDimension(@DimenRes dimension: Int) =
|
||||||
|
resources.getDimensionPixelSize(dimension)
|
||||||
|
|
||||||
|
private fun Int.dpToPx() =
|
||||||
|
TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
this.toFloat(),
|
||||||
|
context.resources.displayMetrics
|
||||||
|
)
|
||||||
|
.toInt()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the list of drag zones for the dragged object.
|
||||||
|
*
|
||||||
|
* Drag zones may have overlap, but the list is sorted by priority where the first drag zone has
|
||||||
|
* the highest priority so it should be checked first.
|
||||||
|
*/
|
||||||
|
fun createSortedDragZones(draggedObject: DraggedObject): List<DragZone> {
|
||||||
|
val dragZones = mutableListOf<DragZone>()
|
||||||
|
when (draggedObject) {
|
||||||
|
is DraggedObject.BubbleBar -> {
|
||||||
|
dragZones.add(createDismissDragZone())
|
||||||
|
dragZones.addAll(createBubbleHalfScreenDragZones())
|
||||||
|
}
|
||||||
|
is DraggedObject.Bubble -> {
|
||||||
|
dragZones.add(createDismissDragZone())
|
||||||
|
dragZones.addAll(createBubbleCornerDragZones())
|
||||||
|
dragZones.add(createFullScreenDragZone())
|
||||||
|
if (shouldShowDesktopWindowDragZones()) {
|
||||||
|
dragZones.add(createDesktopWindowDragZoneForBubble())
|
||||||
|
}
|
||||||
|
dragZones.addAll(createSplitScreenDragZonesForBubble())
|
||||||
|
}
|
||||||
|
is DraggedObject.ExpandedView -> {
|
||||||
|
dragZones.add(createDismissDragZone())
|
||||||
|
dragZones.add(createFullScreenDragZone())
|
||||||
|
if (shouldShowDesktopWindowDragZones()) {
|
||||||
|
dragZones.add(createDesktopWindowDragZoneForExpandedView())
|
||||||
|
}
|
||||||
|
if (deviceConfig.isSmallTablet) {
|
||||||
|
dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnFoldable())
|
||||||
|
} else {
|
||||||
|
dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnTablet())
|
||||||
|
}
|
||||||
|
dragZones.addAll(createBubbleHalfScreenDragZones())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dragZones
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDismissDragZone(): DragZone {
|
||||||
|
return DragZone.Dismiss(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
windowBounds.right / 2 - dismissDragZoneSize / 2,
|
||||||
|
windowBounds.bottom - dismissDragZoneSize,
|
||||||
|
windowBounds.right / 2 + dismissDragZoneSize / 2,
|
||||||
|
windowBounds.bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createBubbleCornerDragZones(): List<DragZone> {
|
||||||
|
val dragZoneSize =
|
||||||
|
if (deviceConfig.isSmallTablet) {
|
||||||
|
bubbleDragZoneFoldableSize
|
||||||
|
} else {
|
||||||
|
bubbleDragZoneTabletSize
|
||||||
|
}
|
||||||
|
return listOf(
|
||||||
|
DragZone.Bubble.Left(
|
||||||
|
bounds =
|
||||||
|
Rect(0, windowBounds.bottom - dragZoneSize, dragZoneSize, windowBounds.bottom),
|
||||||
|
dropTarget = expandedViewDropTargetLeft,
|
||||||
|
),
|
||||||
|
DragZone.Bubble.Right(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
windowBounds.right - dragZoneSize,
|
||||||
|
windowBounds.bottom - dragZoneSize,
|
||||||
|
windowBounds.right,
|
||||||
|
windowBounds.bottom,
|
||||||
|
),
|
||||||
|
dropTarget = expandedViewDropTargetRight,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createBubbleHalfScreenDragZones(): List<DragZone> {
|
||||||
|
return listOf(
|
||||||
|
DragZone.Bubble.Left(
|
||||||
|
bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom),
|
||||||
|
dropTarget = expandedViewDropTargetLeft,
|
||||||
|
),
|
||||||
|
DragZone.Bubble.Right(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
windowBounds.right / 2,
|
||||||
|
0,
|
||||||
|
windowBounds.right,
|
||||||
|
windowBounds.bottom,
|
||||||
|
),
|
||||||
|
dropTarget = expandedViewDropTargetRight,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFullScreenDragZone(): DragZone {
|
||||||
|
return DragZone.FullScreen(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
windowBounds.right / 2 - fullScreenDragZoneWidth / 2,
|
||||||
|
0,
|
||||||
|
windowBounds.right / 2 + fullScreenDragZoneWidth / 2,
|
||||||
|
fullScreenDragZoneHeight
|
||||||
|
),
|
||||||
|
dropTarget = fullScreenDropTarget
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldShowDesktopWindowDragZones() =
|
||||||
|
!deviceConfig.isSmallTablet && desktopWindowModeChecker.isSupported()
|
||||||
|
|
||||||
|
private fun createDesktopWindowDragZoneForBubble(): DragZone {
|
||||||
|
return DragZone.DesktopWindow(
|
||||||
|
bounds =
|
||||||
|
if (deviceConfig.isLandscape) {
|
||||||
|
Rect(
|
||||||
|
windowBounds.right / 2 - desktopWindowDragZoneWidth / 2,
|
||||||
|
windowBounds.bottom / 2 - desktopWindowDragZoneHeight / 2,
|
||||||
|
windowBounds.right / 2 + desktopWindowDragZoneWidth / 2,
|
||||||
|
windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Rect(
|
||||||
|
0,
|
||||||
|
windowBounds.bottom / 2 - desktopWindowDragZoneHeight / 2,
|
||||||
|
windowBounds.right,
|
||||||
|
windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2
|
||||||
|
)
|
||||||
|
},
|
||||||
|
dropTarget = desktopWindowDropTarget
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDesktopWindowDragZoneForExpandedView(): DragZone {
|
||||||
|
return DragZone.DesktopWindow(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
windowBounds.right / 2 - desktopWindowFromExpandedViewDragZoneWidth / 2,
|
||||||
|
windowBounds.bottom / 2 - desktopWindowFromExpandedViewDragZoneHeight / 2,
|
||||||
|
windowBounds.right / 2 + desktopWindowFromExpandedViewDragZoneWidth / 2,
|
||||||
|
windowBounds.bottom / 2 + desktopWindowFromExpandedViewDragZoneHeight / 2
|
||||||
|
),
|
||||||
|
dropTarget = desktopWindowDropTarget
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSplitScreenDragZonesForBubble(): List<DragZone> {
|
||||||
|
// for foldables in landscape mode or tables in portrait modes we have vertical split drag
|
||||||
|
// zones. otherwise we have horizontal split drag zones.
|
||||||
|
val isVerticalSplit = deviceConfig.isSmallTablet == deviceConfig.isLandscape
|
||||||
|
return if (isVerticalSplit) {
|
||||||
|
when (splitScreenModeChecker.getSplitScreenMode()) {
|
||||||
|
SplitScreenMode.UNSUPPORTED -> emptyList()
|
||||||
|
SplitScreenMode.SPLIT_50_50,
|
||||||
|
SplitScreenMode.NONE ->
|
||||||
|
listOf(
|
||||||
|
DragZone.Split.Top(
|
||||||
|
bounds = Rect(0, 0, windowBounds.right, windowBounds.bottom / 2),
|
||||||
|
),
|
||||||
|
DragZone.Split.Bottom(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
0,
|
||||||
|
windowBounds.bottom / 2,
|
||||||
|
windowBounds.right,
|
||||||
|
windowBounds.bottom
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
SplitScreenMode.SPLIT_90_10 -> {
|
||||||
|
listOf(
|
||||||
|
DragZone.Split.Top(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
windowBounds.right,
|
||||||
|
windowBounds.bottom - splitFromBubbleDragZoneHeight
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DragZone.Split.Bottom(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
0,
|
||||||
|
windowBounds.bottom - splitFromBubbleDragZoneHeight,
|
||||||
|
windowBounds.right,
|
||||||
|
windowBounds.bottom
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SplitScreenMode.SPLIT_10_90 -> {
|
||||||
|
listOf(
|
||||||
|
DragZone.Split.Top(
|
||||||
|
bounds = Rect(0, 0, windowBounds.right, splitFromBubbleDragZoneHeight),
|
||||||
|
),
|
||||||
|
DragZone.Split.Bottom(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
0,
|
||||||
|
splitFromBubbleDragZoneHeight,
|
||||||
|
windowBounds.right,
|
||||||
|
windowBounds.bottom
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (splitScreenModeChecker.getSplitScreenMode()) {
|
||||||
|
SplitScreenMode.UNSUPPORTED -> emptyList()
|
||||||
|
SplitScreenMode.SPLIT_50_50,
|
||||||
|
SplitScreenMode.NONE ->
|
||||||
|
listOf(
|
||||||
|
DragZone.Split.Left(
|
||||||
|
bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom),
|
||||||
|
),
|
||||||
|
DragZone.Split.Right(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
windowBounds.right / 2,
|
||||||
|
0,
|
||||||
|
windowBounds.right,
|
||||||
|
windowBounds.bottom
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
SplitScreenMode.SPLIT_90_10 ->
|
||||||
|
listOf(
|
||||||
|
DragZone.Split.Left(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
windowBounds.right - splitFromBubbleDragZoneWidth,
|
||||||
|
windowBounds.bottom
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DragZone.Split.Right(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
windowBounds.right - splitFromBubbleDragZoneWidth,
|
||||||
|
0,
|
||||||
|
windowBounds.right,
|
||||||
|
windowBounds.bottom
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
SplitScreenMode.SPLIT_10_90 ->
|
||||||
|
listOf(
|
||||||
|
DragZone.Split.Left(
|
||||||
|
bounds = Rect(0, 0, splitFromBubbleDragZoneWidth, windowBounds.bottom),
|
||||||
|
),
|
||||||
|
DragZone.Split.Right(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
splitFromBubbleDragZoneWidth,
|
||||||
|
0,
|
||||||
|
windowBounds.right,
|
||||||
|
windowBounds.bottom
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSplitScreenDragZonesForExpandedViewOnTablet(): List<DragZone> {
|
||||||
|
return if (deviceConfig.isLandscape) {
|
||||||
|
createHorizontalSplitDragZonesForExpandedView()
|
||||||
|
} else {
|
||||||
|
// for tablets in portrait mode, split drag zones appear below the full screen drag zone
|
||||||
|
// for the top split zone, and above the dismiss zone. Both are horizontally centered.
|
||||||
|
val splitZoneLeft = windowBounds.right / 2 - vSplitFromExpandedViewDragZoneWidth / 2
|
||||||
|
val splitZoneRight = splitZoneLeft + vSplitFromExpandedViewDragZoneWidth
|
||||||
|
val bottomSplitZoneBottom = windowBounds.bottom - dismissDragZoneSize
|
||||||
|
listOf(
|
||||||
|
DragZone.Split.Top(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
splitZoneLeft,
|
||||||
|
fullScreenDragZoneHeight,
|
||||||
|
splitZoneRight,
|
||||||
|
fullScreenDragZoneHeight + vSplitFromExpandedViewDragZoneHeightTablet
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DragZone.Split.Bottom(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
splitZoneLeft,
|
||||||
|
bottomSplitZoneBottom - vSplitFromExpandedViewDragZoneHeightTablet,
|
||||||
|
splitZoneRight,
|
||||||
|
bottomSplitZoneBottom
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSplitScreenDragZonesForExpandedViewOnFoldable(): List<DragZone> {
|
||||||
|
return if (deviceConfig.isLandscape) {
|
||||||
|
// vertical split drag zones are aligned with the full screen drag zone width
|
||||||
|
val splitZoneLeft = windowBounds.right / 2 - fullScreenDragZoneWidth / 2
|
||||||
|
when (splitScreenModeChecker.getSplitScreenMode()) {
|
||||||
|
SplitScreenMode.UNSUPPORTED -> emptyList()
|
||||||
|
SplitScreenMode.SPLIT_50_50,
|
||||||
|
SplitScreenMode.NONE ->
|
||||||
|
listOf(
|
||||||
|
DragZone.Split.Top(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
splitZoneLeft,
|
||||||
|
fullScreenDragZoneHeight,
|
||||||
|
splitZoneLeft + fullScreenDragZoneWidth,
|
||||||
|
fullScreenDragZoneHeight +
|
||||||
|
vSplitFromExpandedViewDragZoneHeightFoldTall
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DragZone.Split.Bottom(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
splitZoneLeft,
|
||||||
|
windowBounds.bottom / 2,
|
||||||
|
splitZoneLeft + fullScreenDragZoneWidth,
|
||||||
|
windowBounds.bottom / 2 +
|
||||||
|
vSplitFromExpandedViewDragZoneHeightFoldTall
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
SplitScreenMode.SPLIT_10_90 ->
|
||||||
|
listOf(
|
||||||
|
DragZone.Split.Top(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
windowBounds.right,
|
||||||
|
vSplitFromExpandedViewDragZoneHeightFoldShort
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DragZone.Split.Bottom(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
splitZoneLeft,
|
||||||
|
vSplitFromExpandedViewDragZoneHeightFoldShort,
|
||||||
|
splitZoneLeft + fullScreenDragZoneWidth,
|
||||||
|
vSplitFromExpandedViewDragZoneHeightFoldShort +
|
||||||
|
vSplitFromExpandedViewDragZoneHeightFoldTall
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
SplitScreenMode.SPLIT_90_10 ->
|
||||||
|
listOf(
|
||||||
|
DragZone.Split.Top(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
splitZoneLeft,
|
||||||
|
fullScreenDragZoneHeight,
|
||||||
|
splitZoneLeft + fullScreenDragZoneWidth,
|
||||||
|
fullScreenDragZoneHeight +
|
||||||
|
vSplitFromExpandedViewDragZoneHeightFoldTall
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DragZone.Split.Bottom(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
0,
|
||||||
|
windowBounds.bottom -
|
||||||
|
vSplitFromExpandedViewDragZoneHeightFoldShort,
|
||||||
|
windowBounds.right,
|
||||||
|
windowBounds.bottom
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// horizontal split drag zones
|
||||||
|
createHorizontalSplitDragZonesForExpandedView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createHorizontalSplitDragZonesForExpandedView(): List<DragZone> {
|
||||||
|
// horizontal split drag zones for expanded view appear on the edges of the screen from the
|
||||||
|
// top down until the dismiss drag zone height
|
||||||
|
return listOf(
|
||||||
|
DragZone.Split.Left(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
hSplitFromExpandedViewDragZoneWidth,
|
||||||
|
windowBounds.bottom - dismissDragZoneSize
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DragZone.Split.Right(
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
windowBounds.right - hSplitFromExpandedViewDragZoneWidth,
|
||||||
|
0,
|
||||||
|
windowBounds.right,
|
||||||
|
windowBounds.bottom - dismissDragZoneSize
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks the current split screen mode. */
|
||||||
|
fun interface SplitScreenModeChecker {
|
||||||
|
enum class SplitScreenMode {
|
||||||
|
NONE,
|
||||||
|
SPLIT_50_50,
|
||||||
|
SPLIT_10_90,
|
||||||
|
SPLIT_90_10,
|
||||||
|
UNSUPPORTED
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSplitScreenMode(): SplitScreenMode
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks if desktop window mode is supported. */
|
||||||
|
fun interface DesktopWindowModeChecker {
|
||||||
|
fun isSupported(): Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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.shared.bubbles
|
||||||
|
|
||||||
|
/** A Bubble object being dragged. */
|
||||||
|
sealed interface DraggedObject {
|
||||||
|
/** The initial location of the object at the start of the drag gesture. */
|
||||||
|
val initialLocation: BubbleBarLocation
|
||||||
|
|
||||||
|
data class Bubble(override val initialLocation: BubbleBarLocation) : DraggedObject
|
||||||
|
data class BubbleBar(override val initialLocation: BubbleBarLocation) : DraggedObject
|
||||||
|
data class ExpandedView(override val initialLocation: BubbleBarLocation) : DraggedObject
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.core.animation.Animator
|
||||||
|
import androidx.core.animation.AnimatorListenerAdapter
|
||||||
|
import androidx.core.animation.ValueAnimator
|
||||||
|
import com.android.wm.shell.shared.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages animating drop targets in response to dragging bubble icons or bubble expanded views
|
||||||
|
* across different drag zones.
|
||||||
|
*/
|
||||||
|
class DropTargetManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val container: FrameLayout,
|
||||||
|
private val dragZoneChangedListener: DragZoneChangedListener,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var state: DragState? = null
|
||||||
|
private val dropTargetView = DropTargetView(context)
|
||||||
|
private var animator: ValueAnimator? = null
|
||||||
|
private var morphRect: RectF = RectF(0f, 0f, 0f, 0f)
|
||||||
|
private val isLayoutRtl = container.isLayoutRtl
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val MORPH_ANIM_DURATION = 250L
|
||||||
|
const val DROP_TARGET_ALPHA_IN_DURATION = 150L
|
||||||
|
const val DROP_TARGET_ALPHA_OUT_DURATION = 100L
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Must be called when a drag gesture is starting. */
|
||||||
|
fun onDragStarted(draggedObject: DraggedObject, dragZones: List<DragZone>) {
|
||||||
|
val state = DragState(dragZones, draggedObject)
|
||||||
|
dragZoneChangedListener.onInitialDragZoneSet(state.initialDragZone)
|
||||||
|
this.state = state
|
||||||
|
animator?.cancel()
|
||||||
|
setupDropTarget()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupDropTarget() {
|
||||||
|
if (dropTargetView.parent != null) container.removeView(dropTargetView)
|
||||||
|
container.addView(dropTargetView, 0)
|
||||||
|
dropTargetView.alpha = 0f
|
||||||
|
dropTargetView.elevation = context.resources.getDimension(R.dimen.drop_target_elevation)
|
||||||
|
// Match parent and the target is drawn within the view
|
||||||
|
dropTargetView.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when the user drags to a new location. */
|
||||||
|
fun onDragUpdated(x: Int, y: Int) {
|
||||||
|
val state = state ?: return
|
||||||
|
val oldDragZone = state.currentDragZone
|
||||||
|
val newDragZone = state.getMatchingDragZone(x = x, y = y)
|
||||||
|
state.currentDragZone = newDragZone
|
||||||
|
if (oldDragZone != newDragZone) {
|
||||||
|
dragZoneChangedListener.onDragZoneChanged(
|
||||||
|
draggedObject = state.draggedObject,
|
||||||
|
from = oldDragZone,
|
||||||
|
to = newDragZone
|
||||||
|
)
|
||||||
|
updateDropTarget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when the drag ended. */
|
||||||
|
fun onDragEnded() {
|
||||||
|
val dropState = state ?: return
|
||||||
|
startFadeAnimation(from = dropTargetView.alpha, to = 0f) {
|
||||||
|
container.removeView(dropTargetView)
|
||||||
|
}
|
||||||
|
dragZoneChangedListener.onDragEnded(dropState.currentDragZone)
|
||||||
|
state = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDropTarget() {
|
||||||
|
val currentDragZone = state?.currentDragZone ?: return
|
||||||
|
val dropTargetBounds = currentDragZone.dropTarget
|
||||||
|
when {
|
||||||
|
dropTargetBounds == null -> startFadeAnimation(from = dropTargetView.alpha, to = 0f)
|
||||||
|
dropTargetView.alpha == 0f -> {
|
||||||
|
dropTargetView.update(RectF(dropTargetBounds))
|
||||||
|
startFadeAnimation(from = 0f, to = 1f)
|
||||||
|
}
|
||||||
|
else -> startMorphAnimation(dropTargetBounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startFadeAnimation(from: Float, to: Float, onEnd: (() -> Unit)? = null) {
|
||||||
|
animator?.cancel()
|
||||||
|
val duration =
|
||||||
|
if (from < to) DROP_TARGET_ALPHA_IN_DURATION else DROP_TARGET_ALPHA_OUT_DURATION
|
||||||
|
val animator = ValueAnimator.ofFloat(from, to).setDuration(duration)
|
||||||
|
animator.addUpdateListener { _ -> dropTargetView.alpha = animator.animatedValue as Float }
|
||||||
|
if (onEnd != null) {
|
||||||
|
animator.doOnEnd(onEnd)
|
||||||
|
}
|
||||||
|
this.animator = animator
|
||||||
|
animator.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startMorphAnimation(endBounds: Rect) {
|
||||||
|
animator?.cancel()
|
||||||
|
val startAlpha = dropTargetView.alpha
|
||||||
|
val startRect = dropTargetView.getRect()
|
||||||
|
val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(MORPH_ANIM_DURATION)
|
||||||
|
animator.addUpdateListener { _ ->
|
||||||
|
val fraction = animator.animatedValue as Float
|
||||||
|
dropTargetView.alpha = startAlpha + (1 - startAlpha) * fraction
|
||||||
|
|
||||||
|
morphRect.left = (startRect.left + (endBounds.left - startRect.left) * fraction)
|
||||||
|
morphRect.top = (startRect.top + (endBounds.top - startRect.top) * fraction)
|
||||||
|
morphRect.right = (startRect.right + (endBounds.right - startRect.right) * fraction)
|
||||||
|
morphRect.bottom = (startRect.bottom + (endBounds.bottom - startRect.bottom) * fraction)
|
||||||
|
dropTargetView.update(morphRect)
|
||||||
|
}
|
||||||
|
this.animator = animator
|
||||||
|
animator.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stores the current drag state. */
|
||||||
|
private inner class DragState(
|
||||||
|
private val dragZones: List<DragZone>,
|
||||||
|
val draggedObject: DraggedObject
|
||||||
|
) {
|
||||||
|
val initialDragZone =
|
||||||
|
if (draggedObject.initialLocation.isOnLeft(isLayoutRtl)) {
|
||||||
|
dragZones.filterIsInstance<DragZone.Bubble.Left>().first()
|
||||||
|
} else {
|
||||||
|
dragZones.filterIsInstance<DragZone.Bubble.Right>().first()
|
||||||
|
}
|
||||||
|
var currentDragZone: DragZone = initialDragZone
|
||||||
|
|
||||||
|
fun getMatchingDragZone(x: Int, y: Int): DragZone {
|
||||||
|
return dragZones.firstOrNull { it.contains(x, y) } ?: currentDragZone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An interface to be notified when drag zones change. */
|
||||||
|
interface DragZoneChangedListener {
|
||||||
|
/** An initial drag zone was set. Called when a drag starts. */
|
||||||
|
fun onInitialDragZoneSet(dragZone: DragZone)
|
||||||
|
|
||||||
|
/** Called when the object was dragged to a different drag zone. */
|
||||||
|
fun onDragZoneChanged(draggedObject: DraggedObject, from: DragZone, to: DragZone)
|
||||||
|
|
||||||
|
/** Called when the drag has ended with the zone it ended in. */
|
||||||
|
fun onDragEnded(zone: DragZone)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Animator.doOnEnd(onEnd: () -> Unit) {
|
||||||
|
addListener(
|
||||||
|
object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
onEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.View
|
||||||
|
import com.android.wm.shell.shared.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a drop target within this view.
|
||||||
|
*/
|
||||||
|
class DropTargetView(context: Context) : View(context) {
|
||||||
|
|
||||||
|
private val rectPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = context.getColor(com.android.internal.R.color.materialColorPrimaryFixed)
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
alpha = (0.35f * 255).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = context.getColor(com.android.internal.R.color.materialColorPrimaryFixed)
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = 2.dpToPx()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cornerRadius = 28.dpToPx()
|
||||||
|
|
||||||
|
private val rect = RectF(0f, 0f, 0f, 0f)
|
||||||
|
|
||||||
|
// TODO b/396539130: Use shared xml resources once we can access them in launcher
|
||||||
|
private fun Int.dpToPx() =
|
||||||
|
TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
this.toFloat(),
|
||||||
|
context.resources.displayMetrics
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
canvas.save()
|
||||||
|
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, rectPaint)
|
||||||
|
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, strokePaint)
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(positionRect: RectF) {
|
||||||
|
rect.set(positionRect)
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRect(): RectF {
|
||||||
|
return RectF(rect)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
object FlyoutDrawableLoader {
|
||||||
|
|
||||||
|
private const val TAG = "FlyoutDrawableLoader"
|
||||||
|
|
||||||
|
/** Loads the flyout icon as a [Drawable]. */
|
||||||
|
@JvmStatic
|
||||||
|
fun Icon?.loadFlyoutDrawable(context: Context): Drawable? {
|
||||||
|
if (this == null) return null
|
||||||
|
try {
|
||||||
|
if (this.type == Icon.TYPE_URI || this.type == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
|
||||||
|
context.grantUriPermission(
|
||||||
|
context.packageName,
|
||||||
|
this.uri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return loadDrawable(context)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "loadFlyoutDrawable failed: ${e.message}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# WM shell sub-module bubble owner
|
||||||
|
madym@google.com
|
||||||
|
atsjenk@google.com
|
||||||
|
liranb@google.com
|
||||||
|
sukeshram@google.com
|
||||||
|
mpodolian@google.com
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
/** The contents of the flyout message to be passed to launcher for rendering in the bubble bar. */
|
||||||
|
class ParcelableFlyoutMessage(
|
||||||
|
val icon: Icon?,
|
||||||
|
val title: String?,
|
||||||
|
val message: String?,
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
parcel: Parcel
|
||||||
|
) : this(
|
||||||
|
icon = parcel.readParcelable(Icon::class.java.classLoader),
|
||||||
|
title = parcel.readString(),
|
||||||
|
message = parcel.readString(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeParcelable(icon, flags)
|
||||||
|
parcel.writeString(title)
|
||||||
|
parcel.writeString(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents() = 0
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmField
|
||||||
|
val CREATOR =
|
||||||
|
object : Parcelable.Creator<ParcelableFlyoutMessage> {
|
||||||
|
override fun createFromParcel(parcel: Parcel) = ParcelableFlyoutMessage(parcel)
|
||||||
|
|
||||||
|
override fun newArray(size: Int) = arrayOfNulls<ParcelableFlyoutMessage>(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+180
@@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.shared.bubbles
|
||||||
|
|
||||||
|
import android.graphics.PointF
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.VelocityTracker
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewConfiguration
|
||||||
|
import kotlin.math.hypot
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener which receives [onDown], [onMove], and [onUp] events, with relevant information about
|
||||||
|
* the coordinates of the touch and the view relative to the initial ACTION_DOWN event and the
|
||||||
|
* view's initial position.
|
||||||
|
*/
|
||||||
|
abstract class RelativeTouchListener : View.OnTouchListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an ACTION_DOWN event is received for the given view.
|
||||||
|
*
|
||||||
|
* @return False if the object is not interested in MotionEvents at this time, or true if we
|
||||||
|
* should consume this event and subsequent events, and begin calling [onMove].
|
||||||
|
*/
|
||||||
|
abstract fun onDown(v: View, ev: MotionEvent): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an ACTION_MOVE event is received for the given view. This signals that the view
|
||||||
|
* is being dragged.
|
||||||
|
*
|
||||||
|
* @param viewInitialX The view's translationX value when this touch gesture started.
|
||||||
|
* @param viewInitialY The view's translationY value when this touch gesture started.
|
||||||
|
* @param dx Horizontal distance covered since the initial ACTION_DOWN event, in pixels.
|
||||||
|
* @param dy Vertical distance covered since the initial ACTION_DOWN event, in pixels.
|
||||||
|
*/
|
||||||
|
abstract fun onMove(
|
||||||
|
v: View,
|
||||||
|
ev: MotionEvent,
|
||||||
|
viewInitialX: Float,
|
||||||
|
viewInitialY: Float,
|
||||||
|
dx: Float,
|
||||||
|
dy: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an ACTION_UP event is received for the given view. This signals that a drag or
|
||||||
|
* fling gesture has completed.
|
||||||
|
*
|
||||||
|
* @param viewInitialX The view's translationX value when this touch gesture started.
|
||||||
|
* @param viewInitialY The view's translationY value when this touch gesture started.
|
||||||
|
* @param dx Horizontal distance covered, in pixels.
|
||||||
|
* @param dy Vertical distance covered, in pixels.
|
||||||
|
* @param velX The final horizontal velocity of the gesture, in pixels/second.
|
||||||
|
* @param velY The final vertical velocity of the gesture, in pixels/second.
|
||||||
|
*/
|
||||||
|
abstract fun onUp(
|
||||||
|
v: View,
|
||||||
|
ev: MotionEvent,
|
||||||
|
viewInitialX: Float,
|
||||||
|
viewInitialY: Float,
|
||||||
|
dx: Float,
|
||||||
|
dy: Float,
|
||||||
|
velX: Float,
|
||||||
|
velY: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
open fun onCancel(
|
||||||
|
v: View,
|
||||||
|
ev: MotionEvent,
|
||||||
|
viewInitialX: Float,
|
||||||
|
viewInitialY: Float
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** The raw coordinates of the last ACTION_DOWN event. */
|
||||||
|
private var touchDown: PointF? = null
|
||||||
|
|
||||||
|
/** The coordinates of the view, at the time of the last ACTION_DOWN event. */
|
||||||
|
private val viewPositionOnTouchDown = PointF()
|
||||||
|
|
||||||
|
private val velocityTracker = VelocityTracker.obtain()
|
||||||
|
|
||||||
|
private var touchSlop: Int = -1
|
||||||
|
private var movedEnough = false
|
||||||
|
|
||||||
|
private var performedLongClick = false
|
||||||
|
|
||||||
|
override fun onTouch(v: View, ev: MotionEvent): Boolean {
|
||||||
|
addMovement(ev)
|
||||||
|
|
||||||
|
val dx = touchDown?.let { ev.rawX - it.x } ?: 0f
|
||||||
|
val dy = touchDown?.let { ev.rawY - it.y } ?: 0f
|
||||||
|
|
||||||
|
when (ev.action) {
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
if (!onDown(v, ev)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the touch slop, it might have changed if the config changed since the
|
||||||
|
// last gesture.
|
||||||
|
touchSlop = ViewConfiguration.get(v.context).scaledTouchSlop
|
||||||
|
|
||||||
|
touchDown = PointF(ev.rawX, ev.rawY)
|
||||||
|
viewPositionOnTouchDown.set(v.translationX, v.translationY)
|
||||||
|
|
||||||
|
performedLongClick = false
|
||||||
|
v.handler?.postDelayed({
|
||||||
|
if (v.isLongClickable) {
|
||||||
|
performedLongClick = v.performLongClick()
|
||||||
|
}
|
||||||
|
}, ViewConfiguration.getLongPressTimeout().toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
MotionEvent.ACTION_MOVE -> {
|
||||||
|
if (touchDown == null) return false
|
||||||
|
if (!movedEnough && hypot(dx, dy) > touchSlop && !performedLongClick) {
|
||||||
|
movedEnough = true
|
||||||
|
v.handler?.removeCallbacksAndMessages(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movedEnough) {
|
||||||
|
onMove(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MotionEvent.ACTION_UP -> {
|
||||||
|
if (touchDown == null) return false
|
||||||
|
if (movedEnough) {
|
||||||
|
velocityTracker.computeCurrentVelocity(1000 /* units */)
|
||||||
|
onUp(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy,
|
||||||
|
velocityTracker.xVelocity, velocityTracker.yVelocity)
|
||||||
|
} else if (!performedLongClick) {
|
||||||
|
v.performClick()
|
||||||
|
} else {
|
||||||
|
v.handler?.removeCallbacksAndMessages(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
velocityTracker.clear()
|
||||||
|
movedEnough = false
|
||||||
|
touchDown = null
|
||||||
|
}
|
||||||
|
|
||||||
|
MotionEvent.ACTION_CANCEL -> {
|
||||||
|
if (touchDown == null) return false
|
||||||
|
v.handler?.removeCallbacksAndMessages(null)
|
||||||
|
velocityTracker.clear()
|
||||||
|
movedEnough = false
|
||||||
|
touchDown = null
|
||||||
|
onCancel(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a movement to the velocity tracker using raw screen coordinates.
|
||||||
|
*/
|
||||||
|
private fun addMovement(event: MotionEvent) {
|
||||||
|
val deltaX = event.rawX - event.x
|
||||||
|
val deltaY = event.rawY - event.y
|
||||||
|
event.offsetLocation(deltaX, deltaY)
|
||||||
|
velocityTracker.addMovement(event)
|
||||||
|
event.offsetLocation(-deltaX, -deltaY)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.bubbles;
|
||||||
|
|
||||||
|
import android.annotation.NonNull;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a removed bubble, defining the key and reason the bubble was removed.
|
||||||
|
*/
|
||||||
|
public class RemovedBubble implements Parcelable {
|
||||||
|
|
||||||
|
private final String mKey;
|
||||||
|
private final int mRemovalReason;
|
||||||
|
|
||||||
|
public RemovedBubble(String key, int removalReason) {
|
||||||
|
mKey = key;
|
||||||
|
mRemovalReason = removalReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemovedBubble(Parcel parcel) {
|
||||||
|
mKey = parcel.readString();
|
||||||
|
mRemovalReason = parcel.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return mKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRemovalReason() {
|
||||||
|
return mRemovalReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
dest.writeString(mKey);
|
||||||
|
dest.writeInt(mRemovalReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final Creator<RemovedBubble> CREATOR =
|
||||||
|
new Creator<RemovedBubble>() {
|
||||||
|
public RemovedBubble createFromParcel(Parcel source) {
|
||||||
|
return new RemovedBubble(source);
|
||||||
|
}
|
||||||
|
public RemovedBubble[] newArray(int size) {
|
||||||
|
return new RemovedBubble[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.desktopmode
|
||||||
|
|
||||||
|
import android.Manifest.permission.SYSTEM_ALERT_WINDOW
|
||||||
|
import android.app.TaskInfo
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.window.DesktopModeFlags
|
||||||
|
import com.android.internal.R
|
||||||
|
import com.android.internal.policy.DesktopModeCompatUtils
|
||||||
|
import java.util.function.Supplier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to decide whether to apply app compat policies in desktop mode.
|
||||||
|
*/
|
||||||
|
// TODO(b/347289970): Consider replacing with API
|
||||||
|
class DesktopModeCompatPolicy(private val context: Context) {
|
||||||
|
|
||||||
|
private val systemUiPackage: String = context.resources.getString(R.string.config_systemUi)
|
||||||
|
private val pkgManager: PackageManager
|
||||||
|
get() = context.getPackageManager()
|
||||||
|
private val defaultHomePackage: String?
|
||||||
|
get() = defaultHomePackageSupplier?.get()
|
||||||
|
?: pkgManager.getHomeActivities(ArrayList())?.packageName
|
||||||
|
private val packageInfoCache = mutableMapOf<String, Boolean>()
|
||||||
|
|
||||||
|
var defaultHomePackageSupplier: Supplier<String?>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the top activity should be exempt from desktop windowing and forced back to fullscreen.
|
||||||
|
* Currently includes all system ui, default home and transparent stack activities. However if
|
||||||
|
* the top activity is not being displayed, regardless of its configuration, we will not exempt
|
||||||
|
* it as to remain in the desktop windowing environment.
|
||||||
|
*/
|
||||||
|
fun isTopActivityExemptFromDesktopWindowing(task: TaskInfo) =
|
||||||
|
isTopActivityExemptFromDesktopWindowing(task.baseActivity?.packageName,
|
||||||
|
task.numActivities, task.isTopActivityNoDisplay, task.isActivityStackTransparent,
|
||||||
|
task.userId)
|
||||||
|
|
||||||
|
fun isTopActivityExemptFromDesktopWindowing(
|
||||||
|
packageName: String?,
|
||||||
|
numActivities: Int,
|
||||||
|
isTopActivityNoDisplay: Boolean,
|
||||||
|
isActivityStackTransparent: Boolean,
|
||||||
|
userId: Int
|
||||||
|
) =
|
||||||
|
DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue &&
|
||||||
|
((isSystemUiTask(packageName) ||
|
||||||
|
isPartOfDefaultHomePackageOrNoHomeAvailable(packageName) ||
|
||||||
|
(isTransparentTask(isActivityStackTransparent, numActivities) &&
|
||||||
|
hasFullscreenTransparentPermission(packageName, userId))) &&
|
||||||
|
!isTopActivityNoDisplay)
|
||||||
|
|
||||||
|
/** @see DesktopModeCompatUtils.shouldExcludeCaptionFromAppBounds */
|
||||||
|
fun shouldExcludeCaptionFromAppBounds(taskInfo: TaskInfo): Boolean =
|
||||||
|
taskInfo.topActivityInfo?.let {
|
||||||
|
DesktopModeCompatUtils.shouldExcludeCaptionFromAppBounds(it, taskInfo.isResizeable,
|
||||||
|
taskInfo.appCompatTaskInfo.hasOptOutEdgeToEdge())
|
||||||
|
} ?: false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if all activities in a tasks stack are transparent. If there are no activities
|
||||||
|
* will return false.
|
||||||
|
*/
|
||||||
|
fun isTransparentTask(task: TaskInfo): Boolean =
|
||||||
|
isTransparentTask(task.isActivityStackTransparent, task.numActivities)
|
||||||
|
|
||||||
|
private fun isTransparentTask(isActivityStackTransparent: Boolean, numActivities: Int) =
|
||||||
|
isActivityStackTransparent && numActivities > 0
|
||||||
|
|
||||||
|
private fun isSystemUiTask(packageName: String?) = packageName == systemUiPackage
|
||||||
|
|
||||||
|
// Checks if the app for the given package has the SYSTEM_ALERT_WINDOW permission.
|
||||||
|
private fun hasFullscreenTransparentPermission(packageName: String?, userId: Int): Boolean {
|
||||||
|
if (DesktopModeFlags.ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS.isTrue) {
|
||||||
|
if (packageName == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return packageInfoCache.getOrPut("$userId@$packageName") {
|
||||||
|
try {
|
||||||
|
val packageInfo = pkgManager.getPackageInfoAsUser(
|
||||||
|
packageName,
|
||||||
|
PackageManager.GET_PERMISSIONS,
|
||||||
|
userId
|
||||||
|
)
|
||||||
|
packageInfo?.requestedPermissions?.contains(SYSTEM_ALERT_WINDOW) == true
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
false // Package not found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the flag is disabled we make this condition neutral.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the tasks base activity is part of the default home package, or there is
|
||||||
|
* currently no default home package available.
|
||||||
|
*/
|
||||||
|
private fun isPartOfDefaultHomePackageOrNoHomeAvailable(packageName: String?) =
|
||||||
|
defaultHomePackage == null || (packageName != null && packageName == defaultHomePackage)
|
||||||
|
}
|
||||||
+456
@@ -0,0 +1,456 @@
|
|||||||
|
/*
|
||||||
|
* 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.shared.desktopmode;
|
||||||
|
|
||||||
|
import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED;
|
||||||
|
import static android.window.DesktopExperienceFlags.ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE;
|
||||||
|
|
||||||
|
import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement;
|
||||||
|
import static com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper.enableBubbleToFullscreen;
|
||||||
|
|
||||||
|
import android.annotation.NonNull;
|
||||||
|
import android.annotation.Nullable;
|
||||||
|
import android.app.TaskInfo;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.hardware.display.DisplayManager;
|
||||||
|
import android.os.SystemProperties;
|
||||||
|
import android.view.Display;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.window.DesktopExperienceFlags;
|
||||||
|
import android.window.DesktopModeFlags;
|
||||||
|
|
||||||
|
import com.android.internal.R;
|
||||||
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
|
import com.android.window.flags.Flags;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants for desktop mode feature
|
||||||
|
*/
|
||||||
|
// TODO(b/237575897): Move this file to the `com.android.wm.shell.shared.desktopmode` package
|
||||||
|
public class DesktopModeStatus {
|
||||||
|
|
||||||
|
private static final String TAG = "DesktopModeStatus";
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static Boolean sIsLargeScreenDevice = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to indicate whether task resizing is veiled.
|
||||||
|
*/
|
||||||
|
private static final boolean IS_VEILED_RESIZE_ENABLED = SystemProperties.getBoolean(
|
||||||
|
"persist.wm.debug.desktop_veiled_resizing", true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to indicate is moving task to another display is enabled.
|
||||||
|
*/
|
||||||
|
public static final boolean IS_DISPLAY_CHANGE_ENABLED = SystemProperties.getBoolean(
|
||||||
|
"persist.wm.debug.desktop_change_display", false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to indicate whether to apply shadows to windows in desktop mode.
|
||||||
|
*/
|
||||||
|
private static final boolean USE_WINDOW_SHADOWS = SystemProperties.getBoolean(
|
||||||
|
"persist.wm.debug.desktop_use_window_shadows", true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to indicate whether to apply shadows to the focused window in desktop mode.
|
||||||
|
*
|
||||||
|
* Note: this flag is only relevant if USE_WINDOW_SHADOWS is false.
|
||||||
|
*/
|
||||||
|
private static final boolean USE_WINDOW_SHADOWS_FOCUSED_WINDOW = SystemProperties.getBoolean(
|
||||||
|
"persist.wm.debug.desktop_use_window_shadows_focused_window", false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to indicate whether to use rounded corners for windows in desktop mode.
|
||||||
|
*/
|
||||||
|
private static final boolean USE_ROUNDED_CORNERS = SystemProperties.getBoolean(
|
||||||
|
"persist.wm.debug.desktop_use_rounded_corners", true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to indicate whether to restrict desktop mode to supported devices.
|
||||||
|
*/
|
||||||
|
private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean(
|
||||||
|
"persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
|
||||||
|
|
||||||
|
private static final boolean USE_APP_TO_WEB_BUILD_TIME_GENERIC_LINKS =
|
||||||
|
SystemProperties.getBoolean(
|
||||||
|
"persist.wm.debug.use_app_to_web_build_time_generic_links", true);
|
||||||
|
|
||||||
|
/** Whether the desktop density override is enabled. */
|
||||||
|
public static final boolean DESKTOP_DENSITY_OVERRIDE_ENABLED =
|
||||||
|
SystemProperties.getBoolean("persist.wm.debug.desktop_mode_density_enabled", false);
|
||||||
|
|
||||||
|
/** Override density for tasks when they're inside the desktop. */
|
||||||
|
public static final int DESKTOP_DENSITY_OVERRIDE =
|
||||||
|
SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 284);
|
||||||
|
|
||||||
|
/** The minimum override density allowed for tasks inside the desktop. */
|
||||||
|
private static final int DESKTOP_DENSITY_MIN = 100;
|
||||||
|
|
||||||
|
/** The maximum override density allowed for tasks inside the desktop. */
|
||||||
|
private static final int DESKTOP_DENSITY_MAX = 1000;
|
||||||
|
|
||||||
|
/** The number of [WindowDecorViewHost] instances to warm up on system start. */
|
||||||
|
private static final int WINDOW_DECOR_PRE_WARM_SIZE = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sysprop declaring whether to enters desktop mode by default when the windowing mode of the
|
||||||
|
* display's root TaskDisplayArea is set to WINDOWING_MODE_FREEFORM.
|
||||||
|
*
|
||||||
|
* <p>If it is not defined, then {@code R.integer.config_enterDesktopByDefaultOnFreeformDisplay}
|
||||||
|
* is used.
|
||||||
|
*/
|
||||||
|
public static final String ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP =
|
||||||
|
"persist.wm.debug.enter_desktop_by_default_on_freeform_display";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sysprop declaring whether to enable drag-to-maximize for desktop windows.
|
||||||
|
*
|
||||||
|
* <p>If it is not defined, then {@code R.integer.config_dragToMaximizeInDesktopMode}
|
||||||
|
* is used.
|
||||||
|
*/
|
||||||
|
public static final String ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP =
|
||||||
|
"persist.wm.debug.enable_drag_to_maximize";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sysprop declaring the maximum number of Tasks to show in Desktop Mode at any one time.
|
||||||
|
*
|
||||||
|
* <p>If it is not defined, then {@code R.integer.config_maxDesktopWindowingActiveTasks} is
|
||||||
|
* used.
|
||||||
|
*
|
||||||
|
* <p>The limit does NOT affect Picture-in-Picture, Bubbles, or System Modals (like a screen
|
||||||
|
* recording window, or Bluetooth pairing window).
|
||||||
|
*/
|
||||||
|
private static final String MAX_TASK_LIMIT_SYS_PROP = "persist.wm.debug.desktop_max_task_limit";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sysprop declaring the number of [WindowDecorViewHost] instances to warm up on system start.
|
||||||
|
*
|
||||||
|
* <p>If it is not defined, then [WINDOW_DECOR_PRE_WARM_SIZE] is used.
|
||||||
|
*/
|
||||||
|
private static final String WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP =
|
||||||
|
"persist.wm.debug.desktop_window_decor_pre_warm_size";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if veiled resizing is active. If false, fluid resizing is used.
|
||||||
|
*/
|
||||||
|
public static boolean isVeiledResizeEnabled() {
|
||||||
|
return IS_VEILED_RESIZE_ENABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether to use window shadows.
|
||||||
|
*
|
||||||
|
* @param isFocusedWindow whether the window to apply shadows to is focused
|
||||||
|
*/
|
||||||
|
public static boolean useWindowShadow(boolean isFocusedWindow) {
|
||||||
|
return USE_WINDOW_SHADOWS
|
||||||
|
|| (USE_WINDOW_SHADOWS_FOCUSED_WINDOW && isFocusedWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether to use rounded corners for windows.
|
||||||
|
*/
|
||||||
|
public static boolean useRoundedCorners() {
|
||||||
|
return USE_ROUNDED_CORNERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if desktop mode should be restricted to supported devices.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public static boolean enforceDeviceRestrictions() {
|
||||||
|
return ENFORCE_DEVICE_RESTRICTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the maximum limit on the number of Tasks to show in Desktop Mode at any one time.
|
||||||
|
*/
|
||||||
|
public static int getMaxTaskLimit(@NonNull Context context) {
|
||||||
|
return SystemProperties.getInt(MAX_TASK_LIMIT_SYS_PROP,
|
||||||
|
context.getResources().getInteger(R.integer.config_maxDesktopWindowingActiveTasks));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the maximum size of the window decoration surface control view host pool, or zero if
|
||||||
|
* there should be no pooling.
|
||||||
|
*/
|
||||||
|
public static int getWindowDecorScvhPoolSize(@NonNull Context context) {
|
||||||
|
if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_SCVH_CACHE.isTrue()) return 0;
|
||||||
|
final int maxTaskLimit = getMaxTaskLimit(context);
|
||||||
|
if (maxTaskLimit > 0) {
|
||||||
|
return maxTaskLimit;
|
||||||
|
}
|
||||||
|
// TODO: b/368032552 - task limit equal to 0 means unlimited. Figure out what the pool
|
||||||
|
// size should be in that case.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The number of [WindowDecorViewHost] instances to warm up on system start. */
|
||||||
|
public static int getWindowDecorPreWarmSize() {
|
||||||
|
return SystemProperties.getInt(WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP,
|
||||||
|
WINDOW_DECOR_PRE_WARM_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if the current device supports desktop mode.
|
||||||
|
*/
|
||||||
|
private static boolean isDesktopModeSupported(@NonNull Context context) {
|
||||||
|
return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if the current device supports the developer option for desktop mode.
|
||||||
|
*/
|
||||||
|
private static boolean isDesktopModeDevOptionSupported(@NonNull Context context) {
|
||||||
|
return context.getResources().getBoolean(R.bool.config_isDesktopModeDevOptionSupported);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if the current device can host desktop sessions on its internal display.
|
||||||
|
*/
|
||||||
|
private static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
|
||||||
|
return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if desktop mode dev option should be shown on current device
|
||||||
|
*/
|
||||||
|
public static boolean canShowDesktopModeDevOption(@NonNull Context context) {
|
||||||
|
return isDeviceEligibleForDesktopModeDevOption(context)
|
||||||
|
&& Flags.showDesktopWindowingDevOption();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if desktop mode dev option should be shown on current device
|
||||||
|
*/
|
||||||
|
public static boolean canShowDesktopExperienceDevOption(@NonNull Context context) {
|
||||||
|
return Flags.showDesktopExperienceDevOption()
|
||||||
|
&& isDeviceEligibleForDesktopMode(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns if desktop mode dev option should be enabled if there is no user override. */
|
||||||
|
public static boolean shouldDevOptionBeEnabledByDefault(Context context) {
|
||||||
|
return isDeviceEligibleForDesktopMode(context)
|
||||||
|
&& Flags.enableDesktopWindowingMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if desktop mode is enabled and can be entered on the current device.
|
||||||
|
*/
|
||||||
|
public static boolean canEnterDesktopMode(@NonNull Context context) {
|
||||||
|
return (isDeviceEligibleForDesktopMode(context)
|
||||||
|
&& DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue())
|
||||||
|
|| isDesktopModeEnabledByDevOption(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Desktop mode should be enabled because the dev option is shown and enabled.
|
||||||
|
*/
|
||||||
|
private static boolean isDesktopModeEnabledByDevOption(@NonNull Context context) {
|
||||||
|
return DesktopModeFlags.isDesktopModeForcedEnabled()
|
||||||
|
&& canShowDesktopModeDevOption(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if a display should have desktop mode enabled or not. Internal
|
||||||
|
* and external displays have separate logic.
|
||||||
|
*/
|
||||||
|
public static boolean isDesktopModeSupportedOnDisplay(Context context, Display display) {
|
||||||
|
if (!canEnterDesktopMode(context)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!enforceDeviceRestrictions()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (display.getType() == Display.TYPE_INTERNAL) {
|
||||||
|
return canInternalDisplayHostDesktops(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO (b/395014779): Change this to use WM API
|
||||||
|
if ((display.getType() == Display.TYPE_EXTERNAL
|
||||||
|
|| display.getType() == Display.TYPE_OVERLAY)
|
||||||
|
&& enableDisplayContentModeManagement()) {
|
||||||
|
final WindowManager wm = context.getSystemService(WindowManager.class);
|
||||||
|
return wm != null && wm.shouldShowSystemDecors(display.getDisplayId());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the multiple desktops feature is enabled for this device (both backend and
|
||||||
|
* frontend implementations).
|
||||||
|
*/
|
||||||
|
public static boolean enableMultipleDesktops(@NonNull Context context) {
|
||||||
|
return DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()
|
||||||
|
&& Flags.enableMultipleDesktopsFrontend()
|
||||||
|
&& canEnterDesktopMode(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@code true} if this device is requesting to show the app handle despite non
|
||||||
|
* necessarily enabling desktop mode
|
||||||
|
*/
|
||||||
|
public static boolean overridesShowAppHandle(@NonNull Context context) {
|
||||||
|
return (Flags.showAppHandleLargeScreens() || enableBubbleToFullscreen())
|
||||||
|
&& deviceHasLargeScreen(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return If {@code true} we set opaque background for all freeform tasks to prevent freeform
|
||||||
|
* tasks below from being visible if freeform task window above is translucent.
|
||||||
|
* Otherwise if fluid resize is enabled, add a background to freeform tasks.
|
||||||
|
*/
|
||||||
|
public static boolean shouldSetBackground(@NonNull TaskInfo taskInfo) {
|
||||||
|
return taskInfo.isFreeform() && (!DesktopModeStatus.isVeiledResizeEnabled()
|
||||||
|
|| DesktopModeFlags.ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS.isTrue());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@code true} if the app handle should be shown because desktop mode is enabled or
|
||||||
|
* the device has a large screen
|
||||||
|
*/
|
||||||
|
public static boolean canEnterDesktopModeOrShowAppHandle(@NonNull Context context) {
|
||||||
|
return canEnterDesktopMode(context) || overridesShowAppHandle(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if the override desktop density is enabled and valid.
|
||||||
|
*/
|
||||||
|
public static boolean useDesktopOverrideDensity() {
|
||||||
|
return isDesktopDensityOverrideEnabled() && isValidDesktopDensityOverrideSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the app-to-web feature is using the build-time generic links list.
|
||||||
|
*/
|
||||||
|
public static boolean useAppToWebBuildTimeGenericLinks() {
|
||||||
|
return USE_APP_TO_WEB_BUILD_TIME_GENERIC_LINKS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if the override desktop density is enabled.
|
||||||
|
*/
|
||||||
|
private static boolean isDesktopDensityOverrideEnabled() {
|
||||||
|
return DESKTOP_DENSITY_OVERRIDE_ENABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if the override desktop density is set and within a valid range.
|
||||||
|
*/
|
||||||
|
private static boolean isValidDesktopDensityOverrideSet() {
|
||||||
|
return DESKTOP_DENSITY_OVERRIDE >= DESKTOP_DENSITY_MIN
|
||||||
|
&& DESKTOP_DENSITY_OVERRIDE <= DESKTOP_DENSITY_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if desktop mode is unrestricted and is supported on the device.
|
||||||
|
*/
|
||||||
|
public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
|
||||||
|
if (!enforceDeviceRestrictions()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If projected display is enabled, #canInternalDisplayHostDesktops is no longer a
|
||||||
|
// requirement.
|
||||||
|
final boolean desktopModeSupported = ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE.isTrue()
|
||||||
|
? isDesktopModeSupported(context) : (isDesktopModeSupported(context)
|
||||||
|
&& canInternalDisplayHostDesktops(context));
|
||||||
|
final boolean desktopModeSupportedByDevOptions =
|
||||||
|
Flags.enableDesktopModeThroughDevOption()
|
||||||
|
&& isDesktopModeDevOptionSupported(context);
|
||||||
|
return desktopModeSupported || desktopModeSupportedByDevOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if the developer option for desktop mode is unrestricted and is supported
|
||||||
|
* in the device.
|
||||||
|
*
|
||||||
|
* Note that, if {@link #isDeviceEligibleForDesktopMode(Context)} is true, then
|
||||||
|
* {@link #isDeviceEligibleForDesktopModeDevOption(Context)} is also true.
|
||||||
|
*/
|
||||||
|
private static boolean isDeviceEligibleForDesktopModeDevOption(@NonNull Context context) {
|
||||||
|
if (!enforceDeviceRestrictions()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final boolean desktopModeSupported = isDesktopModeSupported(context)
|
||||||
|
&& canInternalDisplayHostDesktops(context);
|
||||||
|
return desktopModeSupported || isDesktopModeDevOptionSupported(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@code true} if this device has an internal large screen
|
||||||
|
*/
|
||||||
|
private static boolean deviceHasLargeScreen(@NonNull Context context) {
|
||||||
|
if (sIsLargeScreenDevice == null) {
|
||||||
|
sIsLargeScreenDevice = Arrays.stream(
|
||||||
|
context.getSystemService(DisplayManager.class)
|
||||||
|
.getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED))
|
||||||
|
.filter(display -> display.getType() == Display.TYPE_INTERNAL)
|
||||||
|
.anyMatch(display -> display.getMinSizeDimensionDp()
|
||||||
|
>= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP);
|
||||||
|
}
|
||||||
|
return sIsLargeScreenDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if a display should enter desktop mode by default when the windowing mode
|
||||||
|
* of the display's root [TaskDisplayArea] is set to WINDOWING_MODE_FREEFORM.
|
||||||
|
*/
|
||||||
|
public static boolean enterDesktopByDefaultOnFreeformDisplay(@NonNull Context context) {
|
||||||
|
if (!DesktopExperienceFlags.ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS.isTrue()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return SystemProperties.getBoolean(ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP,
|
||||||
|
context.getResources().getBoolean(
|
||||||
|
R.bool.config_enterDesktopByDefaultOnFreeformDisplay));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if a window should be maximized when it's dragged to the top edge of the
|
||||||
|
* screen.
|
||||||
|
*/
|
||||||
|
public static boolean shouldMaximizeWhenDragToTopEdge(@NonNull Context context) {
|
||||||
|
if (!DesktopExperienceFlags.ENABLE_DRAG_TO_MAXIMIZE.isTrue()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return SystemProperties.getBoolean(ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP,
|
||||||
|
context.getResources().getBoolean(R.bool.config_dragToMaximizeInDesktopMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dumps DesktopModeStatus flags and configs. */
|
||||||
|
public static void dump(PrintWriter pw, String prefix, Context context) {
|
||||||
|
String innerPrefix = prefix + " ";
|
||||||
|
pw.print(prefix); pw.println(TAG);
|
||||||
|
pw.print(innerPrefix); pw.print("maxTaskLimit="); pw.println(getMaxTaskLimit(context));
|
||||||
|
|
||||||
|
pw.print(innerPrefix); pw.print("maxTaskLimit config override=");
|
||||||
|
pw.println(context.getResources().getInteger(
|
||||||
|
R.integer.config_maxDesktopWindowingActiveTasks));
|
||||||
|
|
||||||
|
SystemProperties.Handle maxTaskLimitHandle = SystemProperties.find(MAX_TASK_LIMIT_SYS_PROP);
|
||||||
|
pw.print(innerPrefix); pw.print("maxTaskLimit sysprop=");
|
||||||
|
pw.println(maxTaskLimitHandle == null ? "null" : maxTaskLimitHandle.getInt(/* def= */ -1));
|
||||||
|
|
||||||
|
pw.print(innerPrefix); pw.print("showAppHandle config override=");
|
||||||
|
pw.println(overridesShowAppHandle(context));
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.desktopmode;
|
||||||
|
|
||||||
|
parcelable DesktopModeTransitionSource;
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.desktopmode
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
/** Transition source types for Desktop Mode. */
|
||||||
|
enum class DesktopModeTransitionSource : Parcelable {
|
||||||
|
/** Transitions that originated as a consequence of task dragging. */
|
||||||
|
TASK_DRAG,
|
||||||
|
/** Transitions that originated from an app from Overview. */
|
||||||
|
APP_FROM_OVERVIEW,
|
||||||
|
/** Transitions that originated from app handle menu button */
|
||||||
|
APP_HANDLE_MENU_BUTTON,
|
||||||
|
/** Transitions that originated as a result of keyboard shortcuts. */
|
||||||
|
KEYBOARD_SHORTCUT,
|
||||||
|
/** Transitions with source unknown. */
|
||||||
|
UNKNOWN;
|
||||||
|
|
||||||
|
override fun describeContents(): Int = 0
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
dest.writeString(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmField
|
||||||
|
val CREATOR =
|
||||||
|
object : Parcelable.Creator<DesktopModeTransitionSource> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource =
|
||||||
|
parcel.readString()?.let { valueOf(it) } ?: UNKNOWN
|
||||||
|
|
||||||
|
override fun newArray(size: Int) = arrayOfNulls<DesktopModeTransitionSource>(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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.shared.desktopmode;
|
||||||
|
|
||||||
|
parcelable DesktopTaskToFrontReason;
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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.shared.desktopmode
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
/** Reason for moving a task to front in Desktop Mode. */
|
||||||
|
enum class DesktopTaskToFrontReason : Parcelable {
|
||||||
|
UNKNOWN,
|
||||||
|
TASKBAR_TAP,
|
||||||
|
ALT_TAB,
|
||||||
|
TASKBAR_MANAGE_WINDOW;
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
dest.writeString(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmField
|
||||||
|
val CREATOR = object : Parcelable.Creator<DesktopTaskToFrontReason> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): DesktopTaskToFrontReason {
|
||||||
|
return parcel.readString()?.let { valueOf(it) } ?: UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int) = arrayOfNulls<DesktopTaskToFrontReason>(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
file:/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.draganddrop;
|
||||||
|
|
||||||
|
/** Constants that can be used by both Shell and other users of the library, e.g. Launcher */
|
||||||
|
public class DragAndDropConstants {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An Intent extra that Launcher can use to specify a region of the screen where Shell should
|
||||||
|
* ignore drag events.
|
||||||
|
*/
|
||||||
|
public static final String EXTRA_DISALLOW_HIT_REGION = "DISALLOW_HIT_REGION";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An Intent extra that Launcher can use to specify the {@link android.content.pm.ShortcutInfo}
|
||||||
|
*/
|
||||||
|
public static final String EXTRA_SHORTCUT_INFO = "EXTRA_SHORTCUT_INFO";
|
||||||
|
}
|
||||||
+359
@@ -0,0 +1,359 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.shared.handles;
|
||||||
|
|
||||||
|
import static android.view.Display.DEFAULT_DISPLAY;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.view.CompositionSamplingListener;
|
||||||
|
import android.view.SurfaceControl;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewRootImpl;
|
||||||
|
import android.view.ViewTreeObserver;
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper class to sample regions on the screen and inspect its luminosity.
|
||||||
|
*/
|
||||||
|
@TargetApi(Build.VERSION_CODES.Q)
|
||||||
|
public class RegionSamplingHelper implements View.OnAttachStateChangeListener,
|
||||||
|
View.OnLayoutChangeListener {
|
||||||
|
|
||||||
|
// Luminance threshold to determine black/white contrast for the navigation affordances.
|
||||||
|
// Passing the threshold of this luminance value will make the button black otherwise white
|
||||||
|
private static final float NAVIGATION_LUMINANCE_THRESHOLD = 0.5f;
|
||||||
|
// Luminance change threshold that allows applying new value if difference was exceeded
|
||||||
|
private static final float NAVIGATION_LUMINANCE_CHANGE_THRESHOLD = 0.05f;
|
||||||
|
|
||||||
|
private final Handler mHandler = new Handler();
|
||||||
|
private final View mSampledView;
|
||||||
|
|
||||||
|
private final CompositionSamplingListener mSamplingListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The requested sampling bounds that we want to sample from
|
||||||
|
*/
|
||||||
|
private final Rect mSamplingRequestBounds = new Rect();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sampling bounds that are currently registered.
|
||||||
|
*/
|
||||||
|
private final Rect mRegisteredSamplingBounds = new Rect();
|
||||||
|
private final SamplingCallback mCallback;
|
||||||
|
private final Executor mBackgroundExecutor;
|
||||||
|
private final SysuiCompositionSamplingListener mCompositionSamplingListener;
|
||||||
|
private boolean mSamplingEnabled = false;
|
||||||
|
private boolean mSamplingListenerRegistered = false;
|
||||||
|
|
||||||
|
private float mLastMedianLuma;
|
||||||
|
private float mCurrentMedianLuma;
|
||||||
|
private boolean mWaitingOnDraw;
|
||||||
|
private boolean mIsDestroyed;
|
||||||
|
|
||||||
|
private boolean mFirstSamplingAfterStart;
|
||||||
|
private boolean mWindowVisible;
|
||||||
|
private boolean mWindowHasBlurs;
|
||||||
|
private SurfaceControl mRegisteredStopLayer = null;
|
||||||
|
// A copy of mRegisteredStopLayer where we own the life cycle and can access from a bg thread.
|
||||||
|
private SurfaceControl mWrappedStopLayer = null;
|
||||||
|
private ViewTreeObserver.OnDrawListener mUpdateOnDraw = new ViewTreeObserver.OnDrawListener() {
|
||||||
|
@Override
|
||||||
|
public void onDraw() {
|
||||||
|
// We need to post the remove runnable, since it's not allowed to remove in onDraw
|
||||||
|
mHandler.post(mRemoveDrawRunnable);
|
||||||
|
RegionSamplingHelper.this.onDraw();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private Runnable mRemoveDrawRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mSampledView.getViewTreeObserver().removeOnDrawListener(mUpdateOnDraw);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Pass a main executor.
|
||||||
|
*/
|
||||||
|
public RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback,
|
||||||
|
Executor backgroundExecutor) {
|
||||||
|
this(sampledView, samplingCallback, sampledView.getContext().getMainExecutor(),
|
||||||
|
backgroundExecutor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback,
|
||||||
|
Executor mainExecutor, Executor backgroundExecutor) {
|
||||||
|
this(sampledView, samplingCallback, mainExecutor,
|
||||||
|
backgroundExecutor, new SysuiCompositionSamplingListener());
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback,
|
||||||
|
Executor mainExecutor, Executor backgroundExecutor,
|
||||||
|
SysuiCompositionSamplingListener compositionSamplingListener) {
|
||||||
|
mBackgroundExecutor = backgroundExecutor;
|
||||||
|
mCompositionSamplingListener = compositionSamplingListener;
|
||||||
|
mSamplingListener = new CompositionSamplingListener(mainExecutor) {
|
||||||
|
@Override
|
||||||
|
public void onSampleCollected(float medianLuma) {
|
||||||
|
if (mSamplingEnabled) {
|
||||||
|
updateMedianLuma(medianLuma);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mSampledView = sampledView;
|
||||||
|
mSampledView.addOnAttachStateChangeListener(this);
|
||||||
|
mSampledView.addOnLayoutChangeListener(this);
|
||||||
|
|
||||||
|
mCallback = samplingCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make callback accessible
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
public SamplingCallback getCallback() {
|
||||||
|
return mCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onDraw() {
|
||||||
|
if (mWaitingOnDraw) {
|
||||||
|
mWaitingOnDraw = false;
|
||||||
|
updateSamplingListener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start(Rect initialSamplingBounds) {
|
||||||
|
if (!mCallback.isSamplingEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (initialSamplingBounds != null) {
|
||||||
|
mSamplingRequestBounds.set(initialSamplingBounds);
|
||||||
|
}
|
||||||
|
mSamplingEnabled = true;
|
||||||
|
// make sure we notify once
|
||||||
|
mLastMedianLuma = -1;
|
||||||
|
mFirstSamplingAfterStart = true;
|
||||||
|
updateSamplingListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
mSamplingEnabled = false;
|
||||||
|
updateSamplingListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopAndDestroy() {
|
||||||
|
stop();
|
||||||
|
mBackgroundExecutor.execute(mSamplingListener::destroy);
|
||||||
|
mIsDestroyed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewAttachedToWindow(View view) {
|
||||||
|
updateSamplingListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewDetachedFromWindow(View view) {
|
||||||
|
stopAndDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLayoutChange(View v, int left, int top, int right, int bottom,
|
||||||
|
int oldLeft, int oldTop, int oldRight, int oldBottom) {
|
||||||
|
updateSamplingRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSamplingListener() {
|
||||||
|
boolean isSamplingEnabled = mSamplingEnabled
|
||||||
|
&& !mSamplingRequestBounds.isEmpty()
|
||||||
|
&& mWindowVisible
|
||||||
|
&& !mWindowHasBlurs
|
||||||
|
&& (mSampledView.isAttachedToWindow() || mFirstSamplingAfterStart);
|
||||||
|
if (isSamplingEnabled) {
|
||||||
|
ViewRootImpl viewRootImpl = mSampledView.getViewRootImpl();
|
||||||
|
SurfaceControl stopLayerControl = null;
|
||||||
|
if (viewRootImpl != null) {
|
||||||
|
stopLayerControl = viewRootImpl.getSurfaceControl();
|
||||||
|
}
|
||||||
|
if (stopLayerControl == null || !stopLayerControl.isValid()) {
|
||||||
|
if (!mWaitingOnDraw) {
|
||||||
|
mWaitingOnDraw = true;
|
||||||
|
// The view might be attached but we haven't drawn yet, so wait until the
|
||||||
|
// next draw to update the listener again with the stop layer, such that our
|
||||||
|
// own drawing doesn't affect the sampling.
|
||||||
|
if (mHandler.hasCallbacks(mRemoveDrawRunnable)) {
|
||||||
|
mHandler.removeCallbacks(mRemoveDrawRunnable);
|
||||||
|
} else {
|
||||||
|
mSampledView.getViewTreeObserver().addOnDrawListener(mUpdateOnDraw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If there's no valid surface, let's just sample without a stop layer, so we
|
||||||
|
// don't have to delay
|
||||||
|
stopLayerControl = null;
|
||||||
|
}
|
||||||
|
if (!mSamplingRequestBounds.equals(mRegisteredSamplingBounds)
|
||||||
|
|| mRegisteredStopLayer != stopLayerControl) {
|
||||||
|
// We only want to re-register if something actually changed
|
||||||
|
unregisterSamplingListener();
|
||||||
|
mSamplingListenerRegistered = true;
|
||||||
|
SurfaceControl wrappedStopLayer = wrap(stopLayerControl);
|
||||||
|
|
||||||
|
// pass this to background thread to avoid empty Rect race condition
|
||||||
|
final Rect boundsCopy = new Rect(mSamplingRequestBounds);
|
||||||
|
|
||||||
|
mBackgroundExecutor.execute(() -> {
|
||||||
|
if (wrappedStopLayer != null && !wrappedStopLayer.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mCompositionSamplingListener.register(mSamplingListener, DEFAULT_DISPLAY,
|
||||||
|
wrappedStopLayer, boundsCopy);
|
||||||
|
});
|
||||||
|
mRegisteredSamplingBounds.set(mSamplingRequestBounds);
|
||||||
|
mRegisteredStopLayer = stopLayerControl;
|
||||||
|
mWrappedStopLayer = wrappedStopLayer;
|
||||||
|
}
|
||||||
|
mFirstSamplingAfterStart = false;
|
||||||
|
} else {
|
||||||
|
unregisterSamplingListener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
protected SurfaceControl wrap(SurfaceControl stopLayerControl) {
|
||||||
|
return stopLayerControl == null ? null : new SurfaceControl(stopLayerControl,
|
||||||
|
"regionSampling");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unregisterSamplingListener() {
|
||||||
|
if (mSamplingListenerRegistered) {
|
||||||
|
mSamplingListenerRegistered = false;
|
||||||
|
SurfaceControl wrappedStopLayer = mWrappedStopLayer;
|
||||||
|
mRegisteredStopLayer = null;
|
||||||
|
mWrappedStopLayer = null;
|
||||||
|
mRegisteredSamplingBounds.setEmpty();
|
||||||
|
mBackgroundExecutor.execute(() -> {
|
||||||
|
mCompositionSamplingListener.unregister(mSamplingListener);
|
||||||
|
if (wrappedStopLayer != null && wrappedStopLayer.isValid()) {
|
||||||
|
wrappedStopLayer.release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMedianLuma(float medianLuma) {
|
||||||
|
mCurrentMedianLuma = medianLuma;
|
||||||
|
|
||||||
|
// If the difference between the new luma and the current luma is larger than threshold
|
||||||
|
// then apply the current luma, this is to prevent small changes causing colors to flicker
|
||||||
|
if (Math.abs(mCurrentMedianLuma - mLastMedianLuma)
|
||||||
|
> NAVIGATION_LUMINANCE_CHANGE_THRESHOLD) {
|
||||||
|
mCallback.onRegionDarknessChanged(
|
||||||
|
medianLuma < NAVIGATION_LUMINANCE_THRESHOLD /* isRegionDark */);
|
||||||
|
mLastMedianLuma = medianLuma;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSamplingRect() {
|
||||||
|
Rect sampledRegion = mCallback.getSampledRegion(mSampledView);
|
||||||
|
if (!mSamplingRequestBounds.equals(sampledRegion)) {
|
||||||
|
mSamplingRequestBounds.set(sampledRegion);
|
||||||
|
updateSamplingListener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWindowVisible(boolean visible) {
|
||||||
|
mWindowVisible = visible;
|
||||||
|
updateSamplingListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we're blurring the shade window.
|
||||||
|
*/
|
||||||
|
public void setWindowHasBlurs(boolean hasBlurs) {
|
||||||
|
mWindowHasBlurs = hasBlurs;
|
||||||
|
updateSamplingListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dump(PrintWriter pw) {
|
||||||
|
dump("", pw);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dump(String prefix, PrintWriter pw) {
|
||||||
|
pw.println(prefix + "RegionSamplingHelper:");
|
||||||
|
pw.println(prefix + "\tsampleView isAttached: " + mSampledView.isAttachedToWindow());
|
||||||
|
pw.println(prefix + "\tsampleView isScValid: " + (mSampledView.isAttachedToWindow()
|
||||||
|
? mSampledView.getViewRootImpl().getSurfaceControl().isValid()
|
||||||
|
: "notAttached"));
|
||||||
|
pw.println(prefix + "\tmSamplingEnabled: " + mSamplingEnabled);
|
||||||
|
pw.println(prefix + "\tmSamplingListenerRegistered: " + mSamplingListenerRegistered);
|
||||||
|
pw.println(prefix + "\tmSamplingRequestBounds: " + mSamplingRequestBounds);
|
||||||
|
pw.println(prefix + "\tmRegisteredSamplingBounds: " + mRegisteredSamplingBounds);
|
||||||
|
pw.println(prefix + "\tmLastMedianLuma: " + mLastMedianLuma);
|
||||||
|
pw.println(prefix + "\tmCurrentMedianLuma: " + mCurrentMedianLuma);
|
||||||
|
pw.println(prefix + "\tmWindowVisible: " + mWindowVisible);
|
||||||
|
pw.println(prefix + "\tmWindowHasBlurs: " + mWindowHasBlurs);
|
||||||
|
pw.println(prefix + "\tmWaitingOnDraw: " + mWaitingOnDraw);
|
||||||
|
pw.println(prefix + "\tmRegisteredStopLayer: " + mRegisteredStopLayer);
|
||||||
|
pw.println(prefix + "\tmWrappedStopLayer: " + mWrappedStopLayer);
|
||||||
|
pw.println(prefix + "\tmIsDestroyed: " + mIsDestroyed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface SamplingCallback {
|
||||||
|
/**
|
||||||
|
* Called when the darkness of the sampled region changes
|
||||||
|
* @param isRegionDark true if the sampled luminance is below the luminance threshold
|
||||||
|
*/
|
||||||
|
void onRegionDarknessChanged(boolean isRegionDark);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the sampled region of interest from the sampled view
|
||||||
|
* @param sampledView The view that this helper is attached to for convenience
|
||||||
|
* @return the region to be sampled in screen coordinates. Return {@code null} to avoid
|
||||||
|
* sampling in this frame
|
||||||
|
*/
|
||||||
|
Rect getSampledRegion(View sampledView);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return if sampling should be enabled in the current configuration
|
||||||
|
*/
|
||||||
|
default boolean isSamplingEnabled() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public static class SysuiCompositionSamplingListener {
|
||||||
|
public void register(CompositionSamplingListener listener,
|
||||||
|
int displayId, SurfaceControl stopLayer, Rect samplingArea) {
|
||||||
|
CompositionSamplingListener.register(listener, displayId, stopLayer, samplingArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters a sampling listener.
|
||||||
|
*/
|
||||||
|
public void unregister(CompositionSamplingListener listener) {
|
||||||
|
CompositionSamplingListener.unregister(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+702
@@ -0,0 +1,702 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.magnetictarget
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.PointF
|
||||||
|
import android.os.VibrationAttributes
|
||||||
|
import android.os.VibrationEffect
|
||||||
|
import android.os.Vibrator
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.VelocityTracker
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewConfiguration
|
||||||
|
import androidx.dynamicanimation.animation.DynamicAnimation
|
||||||
|
import androidx.dynamicanimation.animation.FloatPropertyCompat
|
||||||
|
import androidx.dynamicanimation.animation.SpringForce
|
||||||
|
import com.android.wm.shell.shared.animation.PhysicsAnimator
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.hypot
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for creating 'magnetized' objects that are attracted to one or more magnetic
|
||||||
|
* targets. Magnetic targets attract objects that are dragged near them, and hold them there unless
|
||||||
|
* they're moved away or released. Releasing objects inside a magnetic target typically performs an
|
||||||
|
* action on the object.
|
||||||
|
*
|
||||||
|
* MagnetizedObject also supports flinging to targets, which will result in the object being pulled
|
||||||
|
* into the target and released as if it was dragged into it.
|
||||||
|
*
|
||||||
|
* To use this class, either construct an instance with an object of arbitrary type, or use the
|
||||||
|
* [MagnetizedObject.magnetizeView] shortcut method if you're magnetizing a view. Then, set
|
||||||
|
* [magnetListener] to receive event callbacks. In your touch handler, pass all MotionEvents
|
||||||
|
* that move this object to [maybeConsumeMotionEvent]. If that method returns true, consider the
|
||||||
|
* event consumed by the MagnetizedObject and don't move the object unless it begins returning false
|
||||||
|
* again.
|
||||||
|
*
|
||||||
|
* @param context Context, used to retrieve a Vibrator instance for vibration effects.
|
||||||
|
* @param underlyingObject The actual object that we're magnetizing.
|
||||||
|
* @param xProperty Property that sets the x value of the object's position.
|
||||||
|
* @param yProperty Property that sets the y value of the object's position.
|
||||||
|
*/
|
||||||
|
abstract class MagnetizedObject<T : Any>(
|
||||||
|
val context: Context,
|
||||||
|
|
||||||
|
/** The actual object that is animated. */
|
||||||
|
val underlyingObject: T,
|
||||||
|
|
||||||
|
/** Property that gets/sets the object's X value. */
|
||||||
|
val xProperty: FloatPropertyCompat<in T>,
|
||||||
|
|
||||||
|
/** Property that gets/sets the object's Y value. */
|
||||||
|
val yProperty: FloatPropertyCompat<in T>
|
||||||
|
) {
|
||||||
|
|
||||||
|
/** Return the width of the object. */
|
||||||
|
abstract fun getWidth(underlyingObject: T): Float
|
||||||
|
|
||||||
|
/** Return the height of the object. */
|
||||||
|
abstract fun getHeight(underlyingObject: T): Float
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill the provided array with the location of the top-left of the object, relative to the
|
||||||
|
* entire screen. Compare to [View.getLocationOnScreen].
|
||||||
|
*/
|
||||||
|
abstract fun getLocationOnScreen(underlyingObject: T, loc: IntArray)
|
||||||
|
|
||||||
|
/** Methods for listening to events involving a magnetized object. */
|
||||||
|
interface MagnetListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when touch events move within the magnetic field of a target, causing the
|
||||||
|
* object to animate to the target and become 'stuck' there. The animation happens
|
||||||
|
* automatically here - you should not move the object. You can, however, change its state
|
||||||
|
* to indicate to the user that it's inside the target and releasing it will have an effect.
|
||||||
|
*
|
||||||
|
* [maybeConsumeMotionEvent] is now returning true and will continue to do so until a call
|
||||||
|
* to [onUnstuckFromTarget] or [onReleasedInTarget].
|
||||||
|
*
|
||||||
|
* @param target The target that the object is now stuck to.
|
||||||
|
* @param draggedObject The object that is stuck to the target.
|
||||||
|
*/
|
||||||
|
fun onStuckToTarget(target: MagneticTarget, draggedObject: MagnetizedObject<*>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the object is no longer stuck to a target. This means that either touch
|
||||||
|
* events moved outside of the magnetic field radius, or that a forceful fling out of the
|
||||||
|
* target was detected.
|
||||||
|
*
|
||||||
|
* The object won't be automatically animated out of the target, since you're responsible
|
||||||
|
* for moving the object again. You should move it (or animate it) using your own
|
||||||
|
* movement/animation logic.
|
||||||
|
*
|
||||||
|
* Reverse any effects applied in [onStuckToTarget] here.
|
||||||
|
*
|
||||||
|
* If [wasFlungOut] is true, [maybeConsumeMotionEvent] returned true for the ACTION_UP event
|
||||||
|
* that concluded the fling. If [wasFlungOut] is false, that means a drag gesture is ongoing
|
||||||
|
* and [maybeConsumeMotionEvent] is now returning false.
|
||||||
|
*
|
||||||
|
* @param target The target that this object was just unstuck from.
|
||||||
|
* @param draggedObject The object being unstuck from the target.
|
||||||
|
* @param velX The X velocity of the touch gesture when it exited the magnetic field.
|
||||||
|
* @param velY The Y velocity of the touch gesture when it exited the magnetic field.
|
||||||
|
* @param wasFlungOut Whether the object was unstuck via a fling gesture. This means that
|
||||||
|
* an ACTION_UP event was received, and that the gesture velocity was sufficient to conclude
|
||||||
|
* that the user wants to un-stick the object despite no touch events occurring outside of
|
||||||
|
* the magnetic field radius.
|
||||||
|
*/
|
||||||
|
fun onUnstuckFromTarget(
|
||||||
|
target: MagneticTarget,
|
||||||
|
draggedObject: MagnetizedObject<*>,
|
||||||
|
velX: Float,
|
||||||
|
velY: Float,
|
||||||
|
wasFlungOut: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the object is released inside a target, or flung towards it with enough
|
||||||
|
* velocity to reach it.
|
||||||
|
*
|
||||||
|
* @param target The target that the object was released in.
|
||||||
|
* @param draggedObject The object released in the target.
|
||||||
|
*/
|
||||||
|
fun onReleasedInTarget(target: MagneticTarget, draggedObject: MagnetizedObject<*>)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val animator: PhysicsAnimator<T> = PhysicsAnimator.getInstance(underlyingObject)
|
||||||
|
private val objectLocationOnScreen = IntArray(2)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Targets that have been added to this object. These will all be considered when determining
|
||||||
|
* magnetic fields and fling trajectories.
|
||||||
|
*/
|
||||||
|
private val associatedTargets = ArrayList<MagneticTarget>()
|
||||||
|
|
||||||
|
private val velocityTracker: VelocityTracker = VelocityTracker.obtain()
|
||||||
|
private val vibrator: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||||
|
private val vibrationAttributes: VibrationAttributes = VibrationAttributes.createForUsage(
|
||||||
|
VibrationAttributes.USAGE_TOUCH)
|
||||||
|
|
||||||
|
private var touchDown = PointF()
|
||||||
|
private var touchSlop = 0
|
||||||
|
private var movedBeyondSlop = false
|
||||||
|
|
||||||
|
/** Whether touch events are presently occurring within the magnetic field area of a target. */
|
||||||
|
val objectStuckToTarget: Boolean
|
||||||
|
get() = targetObjectIsStuckTo != null
|
||||||
|
|
||||||
|
/** The target the object is stuck to, or null if the object is not stuck to any target. */
|
||||||
|
private var targetObjectIsStuckTo: MagneticTarget? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the listener to receive events. This must be set, or [maybeConsumeMotionEvent]
|
||||||
|
* will always return false and no magnetic effects will occur.
|
||||||
|
*/
|
||||||
|
lateinit var magnetListener: MagnetizedObject.MagnetListener
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional update listener to provide to the PhysicsAnimator that is used to spring the object
|
||||||
|
* into the target.
|
||||||
|
*/
|
||||||
|
var physicsAnimatorUpdateListener: PhysicsAnimator.UpdateListener<T>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional end listener to provide to the PhysicsAnimator that is used to spring the object
|
||||||
|
* into the target.
|
||||||
|
*/
|
||||||
|
var physicsAnimatorEndListener: PhysicsAnimator.EndListener<T>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that is called when the object should be animated stuck to the target. The default
|
||||||
|
* implementation uses the object's x and y properties to animate the object centered inside the
|
||||||
|
* target. You can override this if you need custom animation.
|
||||||
|
*
|
||||||
|
* The method is invoked with the MagneticTarget that the object is sticking to, the X and Y
|
||||||
|
* velocities of the gesture that brought the object into the magnetic radius, whether or not it
|
||||||
|
* was flung, and a callback you must call after your animation completes.
|
||||||
|
*/
|
||||||
|
var animateStuckToTarget: (MagneticTarget, Float, Float, Boolean, (() -> Unit)?) -> Unit =
|
||||||
|
::animateStuckToTargetInternal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether forcefully flinging the object vertically towards a target causes it to be
|
||||||
|
* attracted to the target and then released immediately, despite never being dragged within the
|
||||||
|
* magnetic field.
|
||||||
|
*/
|
||||||
|
var flingToTargetEnabled = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If fling to target is enabled, forcefully flinging the object towards a target will cause
|
||||||
|
* it to be attracted to the target and then released immediately, despite never being dragged
|
||||||
|
* within the magnetic field.
|
||||||
|
*
|
||||||
|
* This sets the width of the area considered 'near' enough a target to be considered a fling,
|
||||||
|
* in terms of percent of the target view's width. For example, setting this to 3f means that
|
||||||
|
* flings towards a 100px-wide target will be considered 'near' enough if they're towards the
|
||||||
|
* 300px-wide area around the target.
|
||||||
|
*
|
||||||
|
* Flings whose trajectory intersects the area will be attracted and released - even if the
|
||||||
|
* target view itself isn't intersected:
|
||||||
|
*
|
||||||
|
* | |
|
||||||
|
* | 0 |
|
||||||
|
* | / |
|
||||||
|
* | / |
|
||||||
|
* | X / |
|
||||||
|
* |.....###.....|
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Flings towards the target whose trajectories do not intersect the area will be treated as
|
||||||
|
* normal flings and the magnet will leave the object alone:
|
||||||
|
*
|
||||||
|
* | |
|
||||||
|
* | |
|
||||||
|
* | 0 |
|
||||||
|
* | / |
|
||||||
|
* | / X |
|
||||||
|
* |.....###.....|
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
var flingToTargetWidthPercent = 3f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the minimum velocity (in pixels per second) required to fling an object to the target
|
||||||
|
* without dragging it into the magnetic field.
|
||||||
|
*/
|
||||||
|
var flingToTargetMinVelocity = 4000f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the minimum velocity (in pixels per second) required to fling un-stuck an object stuck
|
||||||
|
* to the target. If this velocity is reached, the object will be freed even if it wasn't moved
|
||||||
|
* outside the magnetic field radius.
|
||||||
|
*/
|
||||||
|
var flingUnstuckFromTargetMinVelocity = 4000f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum X velocity above which the object will not stick to the target. Even if the
|
||||||
|
* object is dragged through the magnetic field, it will not stick to the target until the
|
||||||
|
* horizontal velocity is below this value.
|
||||||
|
*/
|
||||||
|
var stickToTargetMaxXVelocity = 2000f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable haptic vibration effects when the object interacts with the magnetic field.
|
||||||
|
*
|
||||||
|
* If you're experiencing crashes when the object enters targets, ensure that you have the
|
||||||
|
* android.permission.VIBRATE permission!
|
||||||
|
*/
|
||||||
|
var hapticsEnabled = true
|
||||||
|
|
||||||
|
/** Default spring configuration to use for animating the object into a target. */
|
||||||
|
var springConfig = PhysicsAnimator.SpringConfig(
|
||||||
|
SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_NO_BOUNCY)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring configuration to use to spring the object into a target specifically when it's flung
|
||||||
|
* towards (rather than dragged near) it.
|
||||||
|
*/
|
||||||
|
var flungIntoTargetSpringConfig = springConfig
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the provided MagneticTarget to this object. The object will now be attracted to the
|
||||||
|
* target if it strays within its magnetic field or is flung towards it.
|
||||||
|
*
|
||||||
|
* If this target (or its magnetic field) overlaps another target added to this object, the
|
||||||
|
* prior target will take priority.
|
||||||
|
*/
|
||||||
|
fun addTarget(target: MagneticTarget) {
|
||||||
|
associatedTargets.add(target)
|
||||||
|
target.updateLocationOnScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut that accepts a View and a magnetic field radius and adds it as a magnetic target.
|
||||||
|
*
|
||||||
|
* @return The MagneticTarget instance for the given View. This can be used to change the
|
||||||
|
* target's magnetic field radius after it's been added. It can also be added to other
|
||||||
|
* magnetized objects.
|
||||||
|
*/
|
||||||
|
fun addTarget(target: View, magneticFieldRadiusPx: Int): MagneticTarget {
|
||||||
|
return MagneticTarget(target, magneticFieldRadiusPx).also { addTarget(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the given target from this object. The target will no longer attract the object.
|
||||||
|
*/
|
||||||
|
fun removeTarget(target: MagneticTarget) {
|
||||||
|
associatedTargets.remove(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all associated targets from this object.
|
||||||
|
*/
|
||||||
|
fun clearAllTargets() {
|
||||||
|
associatedTargets.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide this method with all motion events that move the magnetized object. If the
|
||||||
|
* location of the motion events moves within the magnetic field of a target, or indicate a
|
||||||
|
* fling-to-target gesture, this method will return true and you should not move the object
|
||||||
|
* yourself until it returns false again.
|
||||||
|
*
|
||||||
|
* Note that even when this method returns true, you should continue to pass along new motion
|
||||||
|
* events so that we know when the events move back outside the magnetic field area.
|
||||||
|
*
|
||||||
|
* This method will always return false if you haven't set a [magnetListener].
|
||||||
|
*/
|
||||||
|
fun maybeConsumeMotionEvent(ev: MotionEvent): Boolean {
|
||||||
|
// Short-circuit if we don't have a listener or any targets, since those are required.
|
||||||
|
if (associatedTargets.size == 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a gesture begins, recalculate target views' positions on the screen in case they
|
||||||
|
// have changed. Also, clear state.
|
||||||
|
if (ev.action == MotionEvent.ACTION_DOWN) {
|
||||||
|
updateTargetViews()
|
||||||
|
|
||||||
|
// Clear the velocity tracker and stuck target.
|
||||||
|
velocityTracker.clear()
|
||||||
|
targetObjectIsStuckTo = null
|
||||||
|
|
||||||
|
// Set the touch down coordinates and reset movedBeyondSlop.
|
||||||
|
touchDown.set(ev.rawX, ev.rawY)
|
||||||
|
movedBeyondSlop = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always pass events to the VelocityTracker.
|
||||||
|
addMovement(ev)
|
||||||
|
|
||||||
|
// If we haven't yet moved beyond the slop distance, check if we have.
|
||||||
|
if (!movedBeyondSlop) {
|
||||||
|
val dragDistance = hypot(ev.rawX - touchDown.x, ev.rawY - touchDown.y)
|
||||||
|
if (dragDistance > touchSlop) {
|
||||||
|
// If we're beyond the slop distance, save that and continue.
|
||||||
|
movedBeyondSlop = true
|
||||||
|
} else {
|
||||||
|
// Otherwise, don't do anything yet.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetObjectIsInMagneticFieldOf = associatedTargets.firstOrNull { target ->
|
||||||
|
val distanceFromTargetCenter = hypot(
|
||||||
|
ev.rawX - target.centerOnDisplayX(),
|
||||||
|
ev.rawY - target.centerOnDisplayY())
|
||||||
|
distanceFromTargetCenter < target.magneticFieldRadiusPx
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we aren't currently stuck to a target, and we're in the magnetic field of a target,
|
||||||
|
// we're newly stuck.
|
||||||
|
val objectNewlyStuckToTarget =
|
||||||
|
!objectStuckToTarget && targetObjectIsInMagneticFieldOf != null
|
||||||
|
|
||||||
|
// If we are currently stuck to a target, we're in the magnetic field of a target, and that
|
||||||
|
// target isn't the one we're currently stuck to, then touch events have moved into a
|
||||||
|
// adjacent target's magnetic field.
|
||||||
|
val objectMovedIntoDifferentTarget =
|
||||||
|
objectStuckToTarget &&
|
||||||
|
targetObjectIsInMagneticFieldOf != null &&
|
||||||
|
targetObjectIsStuckTo != targetObjectIsInMagneticFieldOf
|
||||||
|
|
||||||
|
if (objectNewlyStuckToTarget || objectMovedIntoDifferentTarget) {
|
||||||
|
velocityTracker.computeCurrentVelocity(1000)
|
||||||
|
val velX = velocityTracker.xVelocity
|
||||||
|
val velY = velocityTracker.yVelocity
|
||||||
|
|
||||||
|
// If the object is moving too quickly within the magnetic field, do not stick it. This
|
||||||
|
// only applies to objects newly stuck to a target. If the object is moved into a new
|
||||||
|
// target, it wasn't moving at all (since it was stuck to the previous one).
|
||||||
|
if (objectNewlyStuckToTarget && abs(velX) > stickToTargetMaxXVelocity) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// This touch event is newly within the magnetic field - let the listener know, and
|
||||||
|
// animate sticking to the magnet.
|
||||||
|
targetObjectIsStuckTo = targetObjectIsInMagneticFieldOf
|
||||||
|
cancelAnimations()
|
||||||
|
magnetListener.onStuckToTarget(targetObjectIsInMagneticFieldOf!!, this)
|
||||||
|
animateStuckToTarget(targetObjectIsInMagneticFieldOf, velX, velY, false, null)
|
||||||
|
|
||||||
|
vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK)
|
||||||
|
} else if (targetObjectIsInMagneticFieldOf == null && objectStuckToTarget) {
|
||||||
|
velocityTracker.computeCurrentVelocity(1000)
|
||||||
|
|
||||||
|
// This touch event is newly outside the magnetic field - let the listener know. It will
|
||||||
|
// move the object out of the target using its own movement logic.
|
||||||
|
cancelAnimations()
|
||||||
|
magnetListener.onUnstuckFromTarget(
|
||||||
|
targetObjectIsStuckTo!!, this,
|
||||||
|
velocityTracker.xVelocity, velocityTracker.yVelocity,
|
||||||
|
wasFlungOut = false)
|
||||||
|
targetObjectIsStuckTo = null
|
||||||
|
|
||||||
|
vibrateIfEnabled(VibrationEffect.EFFECT_TICK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, check for relevant gestures concluding with an ACTION_UP.
|
||||||
|
if (ev.action == MotionEvent.ACTION_UP) {
|
||||||
|
velocityTracker.computeCurrentVelocity(1000 /* units */)
|
||||||
|
val velX = velocityTracker.xVelocity
|
||||||
|
val velY = velocityTracker.yVelocity
|
||||||
|
|
||||||
|
// Cancel the magnetic animation since we might still be springing into the magnetic
|
||||||
|
// target, but we're about to fling away or release.
|
||||||
|
cancelAnimations()
|
||||||
|
|
||||||
|
if (objectStuckToTarget) {
|
||||||
|
if (-velY > flingUnstuckFromTargetMinVelocity) {
|
||||||
|
// If the object is stuck, but it was forcefully flung away from the target in
|
||||||
|
// the upward direction, tell the listener so the object can be animated out of
|
||||||
|
// the target.
|
||||||
|
magnetListener.onUnstuckFromTarget(
|
||||||
|
targetObjectIsStuckTo!!, this,
|
||||||
|
velX, velY, wasFlungOut = true)
|
||||||
|
} else {
|
||||||
|
// If the object is stuck and not flung away, it was released inside the target.
|
||||||
|
magnetListener.onReleasedInTarget(targetObjectIsStuckTo!!, this)
|
||||||
|
vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either way, we're no longer stuck.
|
||||||
|
targetObjectIsStuckTo = null
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// The target we're flinging towards, or null if we're not flinging towards any target.
|
||||||
|
val flungToTarget = associatedTargets.firstOrNull { target ->
|
||||||
|
isForcefulFlingTowardsTarget(target, ev.rawX, ev.rawY, velX, velY)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flungToTarget != null) {
|
||||||
|
// If this is a fling-to-target, animate the object to the magnet and then release
|
||||||
|
// it.
|
||||||
|
magnetListener.onStuckToTarget(flungToTarget, this)
|
||||||
|
targetObjectIsStuckTo = flungToTarget
|
||||||
|
|
||||||
|
animateStuckToTarget(flungToTarget, velX, velY, true) {
|
||||||
|
magnetListener.onReleasedInTarget(flungToTarget, this)
|
||||||
|
targetObjectIsStuckTo = null
|
||||||
|
vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not either of those things, we are not interested.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return objectStuckToTarget // Always consume touch events if the object is stuck.
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Plays the given vibration effect if haptics are enabled. */
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private fun vibrateIfEnabled(effectId: Int) {
|
||||||
|
if (hapticsEnabled) {
|
||||||
|
vibrator.vibrate(VibrationEffect.createPredefined(effectId), vibrationAttributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds the movement to the velocity tracker using raw coordinates. */
|
||||||
|
private fun addMovement(event: MotionEvent) {
|
||||||
|
// Add movement to velocity tracker using raw screen X and Y coordinates instead
|
||||||
|
// of window coordinates because the window frame may be moving at the same time.
|
||||||
|
val deltaX = event.rawX - event.x
|
||||||
|
val deltaY = event.rawY - event.y
|
||||||
|
event.offsetLocation(deltaX, deltaY)
|
||||||
|
velocityTracker.addMovement(event)
|
||||||
|
event.offsetLocation(-deltaX, -deltaY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Animates sticking the object to the provided target with the given start velocities. */
|
||||||
|
private fun animateStuckToTargetInternal(
|
||||||
|
target: MagneticTarget,
|
||||||
|
velX: Float,
|
||||||
|
velY: Float,
|
||||||
|
flung: Boolean,
|
||||||
|
after: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
target.updateLocationOnScreen()
|
||||||
|
getLocationOnScreen(underlyingObject, objectLocationOnScreen)
|
||||||
|
|
||||||
|
// Calculate the difference between the target's center coordinates and the object's.
|
||||||
|
// Animating the object's x/y properties by these values will center the object on top
|
||||||
|
// of the magnetic target.
|
||||||
|
val xDiff = target.centerOnScreen.x -
|
||||||
|
getWidth(underlyingObject) / 2f - objectLocationOnScreen[0]
|
||||||
|
val yDiff = target.centerOnScreen.y -
|
||||||
|
getHeight(underlyingObject) / 2f - objectLocationOnScreen[1]
|
||||||
|
|
||||||
|
val springConfig = if (flung) flungIntoTargetSpringConfig else springConfig
|
||||||
|
|
||||||
|
cancelAnimations()
|
||||||
|
|
||||||
|
// Animate to the center of the target.
|
||||||
|
animator
|
||||||
|
.spring(xProperty, xProperty.getValue(underlyingObject) + xDiff, velX,
|
||||||
|
springConfig)
|
||||||
|
.spring(yProperty, yProperty.getValue(underlyingObject) + yDiff, velY,
|
||||||
|
springConfig)
|
||||||
|
|
||||||
|
if (physicsAnimatorUpdateListener != null) {
|
||||||
|
animator.addUpdateListener(physicsAnimatorUpdateListener!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (physicsAnimatorEndListener != null) {
|
||||||
|
animator.addEndListener(physicsAnimatorEndListener!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (after != null) {
|
||||||
|
animator.withEndActions(after)
|
||||||
|
}
|
||||||
|
|
||||||
|
animator.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the provided values match a 'fast fling' towards the provided target. If it
|
||||||
|
* does, we consider it a fling-to-target gesture.
|
||||||
|
*/
|
||||||
|
private fun isForcefulFlingTowardsTarget(
|
||||||
|
target: MagneticTarget,
|
||||||
|
rawX: Float,
|
||||||
|
rawY: Float,
|
||||||
|
velX: Float,
|
||||||
|
velY: Float
|
||||||
|
): Boolean {
|
||||||
|
if (!flingToTargetEnabled) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether velocity is sufficient, depending on whether we're flinging into a target at the
|
||||||
|
// top or the bottom of the screen.
|
||||||
|
val velocitySufficient =
|
||||||
|
if (rawY < target.centerOnDisplayY()) velY > flingToTargetMinVelocity
|
||||||
|
else velY < flingToTargetMinVelocity
|
||||||
|
|
||||||
|
if (!velocitySufficient) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether the trajectory of the fling intersects the target area.
|
||||||
|
var targetCenterXIntercept = rawX
|
||||||
|
|
||||||
|
// Only do math if the X velocity is non-zero, otherwise X won't change.
|
||||||
|
if (velX != 0f) {
|
||||||
|
// Rise over run...
|
||||||
|
val slope = velY / velX
|
||||||
|
// ...y = mx + b, b = y / mx...
|
||||||
|
val yIntercept = rawY - slope * rawX
|
||||||
|
|
||||||
|
// ...calculate the x value when y = the target's y-coordinate.
|
||||||
|
targetCenterXIntercept = (target.centerOnDisplayY() - yIntercept) / slope
|
||||||
|
}
|
||||||
|
|
||||||
|
// The width of the area we're looking for a fling towards.
|
||||||
|
val targetAreaWidth = target.targetView.width * flingToTargetWidthPercent
|
||||||
|
|
||||||
|
// Velocity was sufficient, so return true if the intercept is within the target area.
|
||||||
|
return targetCenterXIntercept > target.centerOnDisplayX() - targetAreaWidth / 2 &&
|
||||||
|
targetCenterXIntercept < target.centerOnDisplayX() + targetAreaWidth / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cancel animations on this object's x/y properties. */
|
||||||
|
internal fun cancelAnimations() {
|
||||||
|
animator.cancel(xProperty, yProperty)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the locations on screen of all of the [associatedTargets]. */
|
||||||
|
internal fun updateTargetViews() {
|
||||||
|
associatedTargets.forEach { it.updateLocationOnScreen() }
|
||||||
|
|
||||||
|
// Update the touch slop, since the configuration may have changed.
|
||||||
|
if (associatedTargets.size > 0) {
|
||||||
|
touchSlop =
|
||||||
|
ViewConfiguration.get(associatedTargets[0].targetView.context).scaledTouchSlop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a target view with a magnetic field radius and cached center-on-screen
|
||||||
|
* coordinates.
|
||||||
|
*
|
||||||
|
* Instances of MagneticTarget are passed to a MagnetizedObject's [addTarget], and can then
|
||||||
|
* attract the object if it's dragged near or flung towards it. MagneticTargets can be added to
|
||||||
|
* multiple objects.
|
||||||
|
*/
|
||||||
|
class MagneticTarget(
|
||||||
|
val targetView: View,
|
||||||
|
var magneticFieldRadiusPx: Int
|
||||||
|
) {
|
||||||
|
val centerOnScreen = PointF()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set screen vertical offset amount.
|
||||||
|
*
|
||||||
|
* Screen surface may be vertically shifted in some cases, for example when one-handed mode
|
||||||
|
* is enabled. [MagneticTarget] and [MagnetizedObject] set their location in screen
|
||||||
|
* coordinates (see [MagneticTarget.centerOnScreen] and
|
||||||
|
* [MagnetizedObject.getLocationOnScreen] respectively).
|
||||||
|
*
|
||||||
|
* When a [MagnetizedObject] is dragged, the touch location is determined by
|
||||||
|
* [MotionEvent.getRawX] and [MotionEvent.getRawY]. These work in display coordinates. When
|
||||||
|
* screen is shifted due to one-handed mode, display coordinates and screen coordinates do
|
||||||
|
* not match. To determine if a [MagnetizedObject] is dragged into a [MagneticTarget], view
|
||||||
|
* location on screen is translated to display coordinates using this offset value.
|
||||||
|
*/
|
||||||
|
var screenVerticalOffset: Int = 0
|
||||||
|
|
||||||
|
private val tempLoc = IntArray(2)
|
||||||
|
|
||||||
|
fun updateLocationOnScreen() {
|
||||||
|
targetView.post {
|
||||||
|
targetView.getLocationOnScreen(tempLoc)
|
||||||
|
|
||||||
|
// Add half of the target size to get the center, and subtract translation since the
|
||||||
|
// target could be animating in while we're doing this calculation.
|
||||||
|
centerOnScreen.set(
|
||||||
|
tempLoc[0] + targetView.width / 2f - targetView.translationX,
|
||||||
|
tempLoc[1] + targetView.height / 2f - targetView.translationY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get target center coordinate on x-axis on display. [centerOnScreen] has to be up to date
|
||||||
|
* by calling [updateLocationOnScreen] first.
|
||||||
|
*/
|
||||||
|
fun centerOnDisplayX(): Float {
|
||||||
|
return centerOnScreen.x
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get target center coordinate on y-axis on display. [centerOnScreen] has to be up to date
|
||||||
|
* by calling [updateLocationOnScreen] first. Use [screenVerticalOffset] to update the
|
||||||
|
* screen offset compared to the display.
|
||||||
|
*/
|
||||||
|
fun centerOnDisplayY(): Float {
|
||||||
|
return centerOnScreen.y + screenVerticalOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Magnetizes the given view. Magnetized views are attracted to one or more magnetic
|
||||||
|
* targets. Magnetic targets attract objects that are dragged near them, and hold them there
|
||||||
|
* unless they're moved away or released. Releasing objects inside a magnetic target
|
||||||
|
* typically performs an action on the object.
|
||||||
|
*
|
||||||
|
* Magnetized views can also be flung to targets, which will result in the view being pulled
|
||||||
|
* into the target and released as if it was dragged into it.
|
||||||
|
*
|
||||||
|
* To use the returned MagnetizedObject<View> instance, first set [magnetListener] to
|
||||||
|
* receive event callbacks. In your touch handler, pass all MotionEvents that move this view
|
||||||
|
* to [maybeConsumeMotionEvent]. If that method returns true, consider the event consumed by
|
||||||
|
* MagnetizedObject and don't move the view unless it begins returning false again.
|
||||||
|
*
|
||||||
|
* The view will be moved via translationX/Y properties, and its
|
||||||
|
* width/height will be determined via getWidth()/getHeight(). If you are animating
|
||||||
|
* something other than a view, or want to position your view using properties other than
|
||||||
|
* translationX/Y, implement an instance of [MagnetizedObject].
|
||||||
|
*
|
||||||
|
* Note that the magnetic library can't re-order your view automatically. If the view
|
||||||
|
* renders on top of the target views, it will obscure the target when it sticks to it.
|
||||||
|
* You'll want to bring the view to the front in [MagnetListener.onStuckToTarget].
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun <T : View> magnetizeView(view: T): MagnetizedObject<T> {
|
||||||
|
return object : MagnetizedObject<T>(
|
||||||
|
view.context,
|
||||||
|
view,
|
||||||
|
DynamicAnimation.TRANSLATION_X,
|
||||||
|
DynamicAnimation.TRANSLATION_Y) {
|
||||||
|
override fun getWidth(underlyingObject: T): Float {
|
||||||
|
return underlyingObject.width.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHeight(underlyingObject: T): Float {
|
||||||
|
return underlyingObject.height.toFloat() }
|
||||||
|
|
||||||
|
override fun getLocationOnScreen(underlyingObject: T, loc: IntArray) {
|
||||||
|
underlyingObject.getLocationOnScreen(loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+328
@@ -0,0 +1,328 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.multiinstance
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.animation.AnimatorSet
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.drawable.ShapeDrawable
|
||||||
|
import android.graphics.drawable.shapes.RoundRectShape
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.MotionEvent.ACTION_OUTSIDE
|
||||||
|
import android.view.SurfaceView
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.ALPHA
|
||||||
|
import android.view.View.SCALE_X
|
||||||
|
import android.view.View.SCALE_Y
|
||||||
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.window.TaskSnapshot
|
||||||
|
import com.android.wm.shell.shared.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View for the All Windows menu option, used by both Desktop Windowing and Taskbar.
|
||||||
|
* The menu displays icons of all open instances of an app. Clicking the icon should launch
|
||||||
|
* the instance, which will be performed by the child class.
|
||||||
|
*/
|
||||||
|
abstract class ManageWindowsViewContainer(
|
||||||
|
val context: Context,
|
||||||
|
@ColorInt private val menuBackgroundColor: Int
|
||||||
|
) {
|
||||||
|
lateinit var menuView: ManageWindowsView
|
||||||
|
|
||||||
|
/** Creates the base menu view and fills it with icon views. */
|
||||||
|
fun createMenu(snapshotList: List<Pair<Int, TaskSnapshot?>>,
|
||||||
|
onIconClickListener: ((Int) -> Unit),
|
||||||
|
onOutsideClickListener: (() -> Unit)): ManageWindowsView {
|
||||||
|
val bitmapList = snapshotList
|
||||||
|
.filter { it.second != null }
|
||||||
|
.map { (index, snapshot) ->
|
||||||
|
index to Bitmap.wrapHardwareBuffer(snapshot!!.hardwareBuffer, snapshot.colorSpace)
|
||||||
|
}
|
||||||
|
return createAndShowMenuView(
|
||||||
|
bitmapList,
|
||||||
|
onIconClickListener,
|
||||||
|
onOutsideClickListener
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates the menu view with the given bitmaps, and displays it. */
|
||||||
|
fun createAndShowMenuView(
|
||||||
|
snapshotList: List<Pair<Int, Bitmap?>>,
|
||||||
|
onIconClickListener: ((Int) -> Unit),
|
||||||
|
onOutsideClickListener: (() -> Unit)
|
||||||
|
): ManageWindowsView {
|
||||||
|
menuView = ManageWindowsView(context, menuBackgroundColor).apply {
|
||||||
|
this.onOutsideClickListener = onOutsideClickListener
|
||||||
|
this.onIconClickListener = onIconClickListener
|
||||||
|
this.generateIconViews(snapshotList)
|
||||||
|
}
|
||||||
|
addToContainer(menuView)
|
||||||
|
return menuView
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Play the animation for opening the menu. */
|
||||||
|
fun animateOpen() {
|
||||||
|
menuView.animateOpen()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play the animation for closing the menu. On finish, will run the provided callback,
|
||||||
|
* which will be responsible for removing the view from the container used in [addToContainer].
|
||||||
|
*/
|
||||||
|
fun animateClose() {
|
||||||
|
menuView.animateClose { removeFromContainer() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds the menu view to the container responsible for displaying it. */
|
||||||
|
abstract fun addToContainer(menuView: ManageWindowsView)
|
||||||
|
|
||||||
|
/** Removes the menu view from the container used in the method above */
|
||||||
|
abstract fun removeFromContainer()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MANAGE_WINDOWS_MINIMUM_INSTANCES = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
class ManageWindowsView(
|
||||||
|
private val context: Context,
|
||||||
|
menuBackgroundColor: Int
|
||||||
|
) {
|
||||||
|
private val animators = mutableListOf<Animator>()
|
||||||
|
private val iconViews = mutableListOf<SurfaceView>()
|
||||||
|
val rootView: LinearLayout = LinearLayout(context)
|
||||||
|
var menuHeight = 0
|
||||||
|
var menuWidth = 0
|
||||||
|
var onIconClickListener: ((Int) -> Unit)? = null
|
||||||
|
var onOutsideClickListener: (() -> Unit)? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
rootView.orientation = LinearLayout.VERTICAL
|
||||||
|
val menuBackground = ShapeDrawable()
|
||||||
|
val menuRadius = getDimensionPixelSize(MENU_RADIUS_DP)
|
||||||
|
menuBackground.shape = RoundRectShape(
|
||||||
|
FloatArray(8) { menuRadius },
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
menuBackground.paint.color = menuBackgroundColor
|
||||||
|
rootView.background = menuBackground
|
||||||
|
rootView.elevation = getDimensionPixelSize(MENU_ELEVATION_DP)
|
||||||
|
rootView.setOnTouchListener { _, event ->
|
||||||
|
if (event.actionMasked == ACTION_OUTSIDE) {
|
||||||
|
onOutsideClickListener?.invoke()
|
||||||
|
}
|
||||||
|
return@setOnTouchListener true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDimensionPixelSize(sizeDp: Float): Float {
|
||||||
|
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
sizeDp, context.resources.displayMetrics)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateIconViews(
|
||||||
|
snapshotList: List<Pair<Int, Bitmap?>>
|
||||||
|
) {
|
||||||
|
menuWidth = 0
|
||||||
|
menuHeight = 0
|
||||||
|
rootView.removeAllViews()
|
||||||
|
val instanceIconHeight = getDimensionPixelSize(ICON_HEIGHT_DP)
|
||||||
|
val instanceIconWidth = getDimensionPixelSize(ICON_WIDTH_DP)
|
||||||
|
val iconRadius = getDimensionPixelSize(ICON_RADIUS_DP)
|
||||||
|
val iconMargin = getDimensionPixelSize(ICON_MARGIN_DP)
|
||||||
|
var rowLayout: LinearLayout? = null
|
||||||
|
// Add each icon to the menu, adding a new row when needed.
|
||||||
|
for ((iconCount, taskInfoSnapshotPair) in snapshotList.withIndex()) {
|
||||||
|
val taskId = taskInfoSnapshotPair.first
|
||||||
|
val snapshotBitmap = taskInfoSnapshotPair.second
|
||||||
|
// Once a row is filled, make a new row and increase the menu height.
|
||||||
|
if (iconCount % MENU_MAX_ICONS_PER_ROW == 0) {
|
||||||
|
rowLayout = LinearLayout(context)
|
||||||
|
rowLayout.orientation = LinearLayout.HORIZONTAL
|
||||||
|
rootView.addView(rowLayout)
|
||||||
|
menuHeight += (instanceIconHeight + iconMargin).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
val croppedBitmap = snapshotBitmap?.let { cropBitmap(it) }
|
||||||
|
val scaledSnapshotBitmap = croppedBitmap?.let {
|
||||||
|
Bitmap.createScaledBitmap(
|
||||||
|
it, instanceIconWidth.toInt(), instanceIconHeight.toInt(), true /* filter */
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val appSnapshotButton = SurfaceView(context)
|
||||||
|
appSnapshotButton.cornerRadius = iconRadius
|
||||||
|
appSnapshotButton.setZOrderOnTop(true)
|
||||||
|
appSnapshotButton.contentDescription = context.resources.getString(
|
||||||
|
R.string.manage_windows_icon_text, iconCount + 1
|
||||||
|
)
|
||||||
|
appSnapshotButton.setOnClickListener {
|
||||||
|
onIconClickListener?.invoke(taskId)
|
||||||
|
}
|
||||||
|
val lp = MarginLayoutParams(
|
||||||
|
instanceIconWidth.toInt(), instanceIconHeight.toInt()
|
||||||
|
)
|
||||||
|
lp.apply {
|
||||||
|
marginStart = iconMargin.toInt()
|
||||||
|
topMargin = iconMargin.toInt()
|
||||||
|
}
|
||||||
|
appSnapshotButton.layoutParams = lp
|
||||||
|
// If we haven't already reached one full row, increment width.
|
||||||
|
if (iconCount < MENU_MAX_ICONS_PER_ROW) {
|
||||||
|
menuWidth += (instanceIconWidth + iconMargin).toInt()
|
||||||
|
}
|
||||||
|
rowLayout?.addView(appSnapshotButton)
|
||||||
|
iconViews += appSnapshotButton
|
||||||
|
appSnapshotButton.requestLayout()
|
||||||
|
rowLayout?.post {
|
||||||
|
appSnapshotButton.holder.surface
|
||||||
|
.attachAndQueueBufferWithColorSpace(
|
||||||
|
scaledSnapshotBitmap?.hardwareBuffer,
|
||||||
|
scaledSnapshotBitmap?.colorSpace
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add margin again for the right/bottom of the menu.
|
||||||
|
menuWidth += iconMargin.toInt()
|
||||||
|
menuHeight += iconMargin.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cropBitmap(
|
||||||
|
bitmapToCrop: Bitmap
|
||||||
|
): Bitmap {
|
||||||
|
val ratioToMatch = ICON_WIDTH_DP / ICON_HEIGHT_DP
|
||||||
|
val bitmapWidth = bitmapToCrop.width
|
||||||
|
val bitmapHeight = bitmapToCrop.height
|
||||||
|
if (bitmapWidth > bitmapHeight * ratioToMatch) {
|
||||||
|
// Crop based on height
|
||||||
|
val newWidth = bitmapHeight * ratioToMatch
|
||||||
|
return Bitmap.createBitmap(
|
||||||
|
bitmapToCrop,
|
||||||
|
((bitmapWidth - newWidth) / 2).toInt(),
|
||||||
|
0,
|
||||||
|
newWidth.toInt(),
|
||||||
|
bitmapHeight
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Crop based on width
|
||||||
|
val newHeight = bitmapWidth / ratioToMatch
|
||||||
|
return Bitmap.createBitmap(
|
||||||
|
bitmapToCrop,
|
||||||
|
0,
|
||||||
|
((bitmapHeight - newHeight) / 2).toInt(),
|
||||||
|
bitmapWidth,
|
||||||
|
newHeight.toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Play the animation for opening the menu. */
|
||||||
|
fun animateOpen() {
|
||||||
|
animateView(rootView, MENU_BOUNDS_SHRUNK_SCALE, MENU_BOUNDS_FULL_SCALE,
|
||||||
|
MENU_START_ALPHA, MENU_FULL_ALPHA
|
||||||
|
)
|
||||||
|
for (view in iconViews) {
|
||||||
|
animateView(view, MENU_BOUNDS_SHRUNK_SCALE, MENU_BOUNDS_FULL_SCALE,
|
||||||
|
MENU_START_ALPHA, MENU_FULL_ALPHA
|
||||||
|
)
|
||||||
|
}
|
||||||
|
createAnimatorSet().start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Play the animation for closing the menu. */
|
||||||
|
fun animateClose(callback: () -> Unit) {
|
||||||
|
animateView(rootView, MENU_BOUNDS_FULL_SCALE, MENU_BOUNDS_SHRUNK_SCALE,
|
||||||
|
MENU_FULL_ALPHA, MENU_START_ALPHA
|
||||||
|
)
|
||||||
|
for (view in iconViews) {
|
||||||
|
animateView(view, MENU_BOUNDS_FULL_SCALE, MENU_BOUNDS_SHRUNK_SCALE,
|
||||||
|
MENU_FULL_ALPHA, MENU_START_ALPHA
|
||||||
|
)
|
||||||
|
}
|
||||||
|
createAnimatorSet().apply {
|
||||||
|
addListener(
|
||||||
|
object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
callback.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateView(
|
||||||
|
view: View,
|
||||||
|
startBoundsScale: Float,
|
||||||
|
endBoundsScale: Float,
|
||||||
|
startAlpha: Float,
|
||||||
|
endAlpha: Float) {
|
||||||
|
animators += ObjectAnimator.ofFloat(
|
||||||
|
view,
|
||||||
|
SCALE_X,
|
||||||
|
startBoundsScale,
|
||||||
|
endBoundsScale
|
||||||
|
).apply {
|
||||||
|
duration = MENU_BOUNDS_ANIM_DURATION
|
||||||
|
}
|
||||||
|
animators += ObjectAnimator.ofFloat(
|
||||||
|
view,
|
||||||
|
SCALE_Y,
|
||||||
|
startBoundsScale,
|
||||||
|
endBoundsScale
|
||||||
|
).apply {
|
||||||
|
duration = MENU_BOUNDS_ANIM_DURATION
|
||||||
|
}
|
||||||
|
animators += ObjectAnimator.ofFloat(
|
||||||
|
view,
|
||||||
|
ALPHA,
|
||||||
|
startAlpha,
|
||||||
|
endAlpha
|
||||||
|
).apply {
|
||||||
|
duration = MENU_ALPHA_ANIM_DURATION
|
||||||
|
startDelay = MENU_ALPHA_ANIM_DELAY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAnimatorSet(): AnimatorSet {
|
||||||
|
val animatorSet = AnimatorSet().apply {
|
||||||
|
playTogether(animators)
|
||||||
|
}
|
||||||
|
animators.clear()
|
||||||
|
return animatorSet
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MENU_RADIUS_DP = 26f
|
||||||
|
private const val ICON_WIDTH_DP = 204f
|
||||||
|
private const val ICON_HEIGHT_DP = 127.5f
|
||||||
|
private const val ICON_RADIUS_DP = 16f
|
||||||
|
private const val ICON_MARGIN_DP = 16f
|
||||||
|
private const val MENU_ELEVATION_DP = 1f
|
||||||
|
private const val MENU_MAX_ICONS_PER_ROW = 3
|
||||||
|
private const val MENU_BOUNDS_ANIM_DURATION = 200L
|
||||||
|
private const val MENU_BOUNDS_SHRUNK_SCALE = 0.8f
|
||||||
|
private const val MENU_BOUNDS_FULL_SCALE = 1f
|
||||||
|
private const val MENU_ALPHA_ANIM_DURATION = 100L
|
||||||
|
private const val MENU_ALPHA_ANIM_DELAY = 50L
|
||||||
|
private const val MENU_START_ALPHA = 0f
|
||||||
|
private const val MENU_FULL_ALPHA = 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# WM shell sub-module PiP owner
|
||||||
|
hwwang@google.com
|
||||||
|
gabiyev@google.com
|
||||||
|
wuperry@google.com
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.pip;
|
||||||
|
|
||||||
|
import static android.util.TypedValue.COMPLEX_UNIT_DIP;
|
||||||
|
|
||||||
|
import android.annotation.NonNull;
|
||||||
|
import android.annotation.Nullable;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.hardware.HardwareBuffer;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.SurfaceControl;
|
||||||
|
import android.window.TaskSnapshot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the content overlay used during the entering PiP animation.
|
||||||
|
*/
|
||||||
|
public abstract class PipContentOverlay {
|
||||||
|
// Fixed string used in WMShellFlickerTests
|
||||||
|
protected static final String LAYER_NAME = "PipContentOverlay";
|
||||||
|
|
||||||
|
protected SurfaceControl mLeash;
|
||||||
|
|
||||||
|
/** Attaches the internal {@link #mLeash} to the given parent leash. */
|
||||||
|
public abstract void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash);
|
||||||
|
|
||||||
|
/** Detaches the internal {@link #mLeash} from its parent by removing itself. */
|
||||||
|
public void detach(SurfaceControl.Transaction tx) {
|
||||||
|
if (mLeash != null && mLeash.isValid()) {
|
||||||
|
tx.remove(mLeash);
|
||||||
|
tx.apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public SurfaceControl getLeash() {
|
||||||
|
return mLeash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animates the internal {@link #mLeash} by a given fraction.
|
||||||
|
* @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly
|
||||||
|
* call apply on this transaction, it should be applied on the caller side.
|
||||||
|
* @param currentBounds {@link Rect} of the current animation bounds.
|
||||||
|
* @param fraction progress of the animation ranged from 0f to 1f.
|
||||||
|
*/
|
||||||
|
public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
|
||||||
|
Rect currentBounds, float fraction) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animates the internal {@link #mLeash} by a given fraction for a config-at-end transition.
|
||||||
|
* @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly
|
||||||
|
* call apply on this transaction, it should be applied on the caller side.
|
||||||
|
* @param scale scaling to apply onto the overlay.
|
||||||
|
* @param fraction progress of the animation ranged from 0f to 1f.
|
||||||
|
* @param endBounds the final bounds PiP is animating into.
|
||||||
|
*/
|
||||||
|
public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
|
||||||
|
float scale, float fraction, Rect endBounds) {}
|
||||||
|
|
||||||
|
/** A {@link PipContentOverlay} uses solid color. */
|
||||||
|
public static final class PipColorOverlay extends PipContentOverlay {
|
||||||
|
private static final String TAG = PipColorOverlay.class.getSimpleName();
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
|
public PipColorOverlay(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mLeash = new SurfaceControl.Builder()
|
||||||
|
.setCallsite(TAG)
|
||||||
|
.setName(LAYER_NAME)
|
||||||
|
.setColorLayer()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
|
||||||
|
tx.show(mLeash);
|
||||||
|
tx.setLayer(mLeash, Integer.MAX_VALUE);
|
||||||
|
tx.setColor(mLeash, getContentOverlayColor(mContext));
|
||||||
|
tx.setAlpha(mLeash, 0f);
|
||||||
|
tx.reparent(mLeash, parentLeash);
|
||||||
|
tx.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
|
||||||
|
Rect currentBounds, float fraction) {
|
||||||
|
atomicTx.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] getContentOverlayColor(Context context) {
|
||||||
|
final TypedArray ta = context.obtainStyledAttributes(new int[] {
|
||||||
|
android.R.attr.colorBackground });
|
||||||
|
try {
|
||||||
|
int colorAccent = ta.getColor(0, 0);
|
||||||
|
return new float[] {
|
||||||
|
Color.red(colorAccent) / 255f,
|
||||||
|
Color.green(colorAccent) / 255f,
|
||||||
|
Color.blue(colorAccent) / 255f };
|
||||||
|
} finally {
|
||||||
|
ta.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A {@link PipContentOverlay} uses {@link TaskSnapshot}. */
|
||||||
|
public static final class PipSnapshotOverlay extends PipContentOverlay {
|
||||||
|
private static final String TAG = PipSnapshotOverlay.class.getSimpleName();
|
||||||
|
|
||||||
|
private final TaskSnapshot mSnapshot;
|
||||||
|
private final Rect mSourceRectHint;
|
||||||
|
|
||||||
|
public PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
|
||||||
|
mSnapshot = snapshot;
|
||||||
|
mSourceRectHint = new Rect(sourceRectHint);
|
||||||
|
mLeash = new SurfaceControl.Builder()
|
||||||
|
.setCallsite(TAG)
|
||||||
|
.setName(LAYER_NAME)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
|
||||||
|
final float taskSnapshotScaleX = (float) mSnapshot.getTaskSize().x
|
||||||
|
/ mSnapshot.getHardwareBuffer().getWidth();
|
||||||
|
final float taskSnapshotScaleY = (float) mSnapshot.getTaskSize().y
|
||||||
|
/ mSnapshot.getHardwareBuffer().getHeight();
|
||||||
|
tx.show(mLeash);
|
||||||
|
tx.setLayer(mLeash, Integer.MAX_VALUE);
|
||||||
|
tx.setBuffer(mLeash, mSnapshot.getHardwareBuffer());
|
||||||
|
// Relocate the content to parentLeash's coordinates.
|
||||||
|
tx.setPosition(mLeash, -mSourceRectHint.left, -mSourceRectHint.top);
|
||||||
|
tx.setScale(mLeash, taskSnapshotScaleX, taskSnapshotScaleY);
|
||||||
|
tx.reparent(mLeash, parentLeash);
|
||||||
|
tx.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
|
||||||
|
Rect currentBounds, float fraction) {
|
||||||
|
// Do nothing. Keep the snapshot till animation ends.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A {@link PipContentOverlay} shows app icon on solid color background. */
|
||||||
|
public static final class PipAppIconOverlay extends PipContentOverlay {
|
||||||
|
private static final String TAG = PipAppIconOverlay.class.getSimpleName();
|
||||||
|
// The maximum size for app icon in pixel.
|
||||||
|
private static final int MAX_APP_ICON_SIZE_DP = 72;
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final int mAppIconSizePx;
|
||||||
|
/**
|
||||||
|
* The bounds of the application window relative to the task leash.
|
||||||
|
*/
|
||||||
|
private final Rect mRelativeAppBounds;
|
||||||
|
private final int mOverlayHalfSize;
|
||||||
|
private final Matrix mTmpTransform = new Matrix();
|
||||||
|
private final float[] mTmpFloat9 = new float[9];
|
||||||
|
|
||||||
|
private Bitmap mBitmap;
|
||||||
|
|
||||||
|
// TODO(b/356277166): add non-match_parent support on PIP2.
|
||||||
|
/**
|
||||||
|
* @param context the {@link Context} that contains the icon information
|
||||||
|
* @param relativeAppBounds the bounds of the app window frame relative to the task leash
|
||||||
|
* @param destinationBounds the bounds for rhe PIP task
|
||||||
|
* @param appIcon the app icon {@link Drawable}
|
||||||
|
* @param appIconSizePx the icon dimension in pixel
|
||||||
|
*/
|
||||||
|
public PipAppIconOverlay(@NonNull Context context, @NonNull Rect relativeAppBounds,
|
||||||
|
@NonNull Rect destinationBounds, @NonNull Drawable appIcon, int appIconSizePx) {
|
||||||
|
mContext = context;
|
||||||
|
final int maxAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP,
|
||||||
|
MAX_APP_ICON_SIZE_DP, context.getResources().getDisplayMetrics());
|
||||||
|
mAppIconSizePx = Math.min(maxAppIconSizePx, appIconSizePx);
|
||||||
|
|
||||||
|
final int overlaySize = getOverlaySize(relativeAppBounds, destinationBounds);
|
||||||
|
mOverlayHalfSize = overlaySize >> 1;
|
||||||
|
mRelativeAppBounds = relativeAppBounds;
|
||||||
|
|
||||||
|
mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888);
|
||||||
|
prepareAppIconOverlay(appIcon);
|
||||||
|
mLeash = new SurfaceControl.Builder()
|
||||||
|
.setCallsite(TAG)
|
||||||
|
.setName(LAYER_NAME)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the app icon overlay.
|
||||||
|
*
|
||||||
|
* In order to have the overlay always cover the pip window during the transition,
|
||||||
|
* the overlay will be drawn with the max size of the start and end bounds in different
|
||||||
|
* rotation.
|
||||||
|
*/
|
||||||
|
public static int getOverlaySize(Rect overlayBounds, Rect destinationBounds) {
|
||||||
|
final int appWidth = overlayBounds.width();
|
||||||
|
final int appHeight = overlayBounds.height();
|
||||||
|
|
||||||
|
return Math.max(Math.max(appWidth, appHeight),
|
||||||
|
Math.max(destinationBounds.width(), destinationBounds.height())) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
|
||||||
|
final HardwareBuffer buffer = mBitmap.getHardwareBuffer();
|
||||||
|
tx.show(mLeash);
|
||||||
|
tx.setLayer(mLeash, Integer.MAX_VALUE);
|
||||||
|
tx.setBuffer(mLeash, buffer);
|
||||||
|
tx.setAlpha(mLeash, 0f);
|
||||||
|
tx.reparent(mLeash, parentLeash);
|
||||||
|
tx.apply();
|
||||||
|
// Cleanup the bitmap and buffer after setting up the leash
|
||||||
|
mBitmap.recycle();
|
||||||
|
mBitmap = null;
|
||||||
|
buffer.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
|
||||||
|
Rect currentBounds, float fraction) {
|
||||||
|
mTmpTransform.reset();
|
||||||
|
// In order for the overlay to always cover the pip window, the overlay may have a
|
||||||
|
// size larger than the pip window. Make sure that app icon is at the center.
|
||||||
|
final int appBoundsCenterX = mRelativeAppBounds.centerX();
|
||||||
|
final int appBoundsCenterY = mRelativeAppBounds.centerY();
|
||||||
|
mTmpTransform.setTranslate(
|
||||||
|
appBoundsCenterX - mOverlayHalfSize,
|
||||||
|
appBoundsCenterY - mOverlayHalfSize);
|
||||||
|
// Scale back the bitmap with the pivot point at center.
|
||||||
|
final float scale = Math.min(
|
||||||
|
(float) mRelativeAppBounds.width() / currentBounds.width(),
|
||||||
|
(float) mRelativeAppBounds.height() / currentBounds.height());
|
||||||
|
mTmpTransform.postScale(scale, scale, appBoundsCenterX, appBoundsCenterY);
|
||||||
|
atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9)
|
||||||
|
.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareAppIconOverlay(Drawable appIcon) {
|
||||||
|
final Canvas canvas = new Canvas();
|
||||||
|
canvas.setBitmap(mBitmap);
|
||||||
|
final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
|
||||||
|
android.R.attr.colorBackground });
|
||||||
|
try {
|
||||||
|
int colorAccent = ta.getColor(0, 0);
|
||||||
|
canvas.drawRGB(
|
||||||
|
Color.red(colorAccent),
|
||||||
|
Color.green(colorAccent),
|
||||||
|
Color.blue(colorAccent));
|
||||||
|
} finally {
|
||||||
|
ta.recycle();
|
||||||
|
}
|
||||||
|
final Rect appIconBounds = new Rect(
|
||||||
|
mOverlayHalfSize - mAppIconSizePx / 2,
|
||||||
|
mOverlayHalfSize - mAppIconSizePx / 2,
|
||||||
|
mOverlayHalfSize + mAppIconSizePx / 2,
|
||||||
|
mOverlayHalfSize + mAppIconSizePx / 2);
|
||||||
|
appIcon.setBounds(appIconBounds);
|
||||||
|
appIcon.draw(canvas);
|
||||||
|
Bitmap oldBitmap = mBitmap;
|
||||||
|
mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */);
|
||||||
|
oldBitmap.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
jeremysim@google.com
|
||||||
|
winsonc@google.com
|
||||||
|
peanutbutter@google.com
|
||||||
|
shuminghao@google.com
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.split;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container of various information needed to display split screen
|
||||||
|
* tasks/leashes/etc in Launcher
|
||||||
|
*/
|
||||||
|
public class SplitBounds implements Parcelable {
|
||||||
|
public static final String KEY_EXTRA_SPLIT_BOUNDS = "key_SplitBounds";
|
||||||
|
|
||||||
|
public final Rect leftTopBounds;
|
||||||
|
public final Rect rightBottomBounds;
|
||||||
|
/** This rect represents the actual gap between the two apps */
|
||||||
|
public final Rect visualDividerBounds;
|
||||||
|
// This class is orientation-agnostic, so we compute both for later use
|
||||||
|
public final float topTaskPercent;
|
||||||
|
public final float leftTaskPercent;
|
||||||
|
public final float dividerWidthPercent;
|
||||||
|
public final float dividerHeightPercent;
|
||||||
|
public final @PersistentSnapPosition int snapPosition;
|
||||||
|
/**
|
||||||
|
* If {@code true}, that means at the time of creation of this object, the
|
||||||
|
* split-screened apps were vertically stacked. This is useful in scenarios like
|
||||||
|
* rotation where the bounds won't change, but this variable can indicate what orientation
|
||||||
|
* the bounds were originally in
|
||||||
|
*/
|
||||||
|
public final boolean appsStackedVertically;
|
||||||
|
public final int leftTopTaskId;
|
||||||
|
public final int rightBottomTaskId;
|
||||||
|
|
||||||
|
public SplitBounds(Rect leftTopBounds, Rect rightBottomBounds, int leftTopTaskId,
|
||||||
|
int rightBottomTaskId, @PersistentSnapPosition int snapPosition) {
|
||||||
|
this.leftTopBounds = leftTopBounds;
|
||||||
|
this.rightBottomBounds = rightBottomBounds;
|
||||||
|
this.leftTopTaskId = leftTopTaskId;
|
||||||
|
this.rightBottomTaskId = rightBottomTaskId;
|
||||||
|
this.snapPosition = snapPosition;
|
||||||
|
|
||||||
|
if (rightBottomBounds.top > leftTopBounds.top) {
|
||||||
|
// vertical apps, horizontal divider
|
||||||
|
this.visualDividerBounds = new Rect(leftTopBounds.left, leftTopBounds.bottom,
|
||||||
|
leftTopBounds.right, rightBottomBounds.top);
|
||||||
|
appsStackedVertically = true;
|
||||||
|
} else {
|
||||||
|
// horizontal apps, vertical divider
|
||||||
|
this.visualDividerBounds = new Rect(leftTopBounds.right, leftTopBounds.top,
|
||||||
|
rightBottomBounds.left, leftTopBounds.bottom);
|
||||||
|
appsStackedVertically = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float totalWidth = rightBottomBounds.right - leftTopBounds.left;
|
||||||
|
float totalHeight = rightBottomBounds.bottom - leftTopBounds.top;
|
||||||
|
leftTaskPercent = leftTopBounds.width() / totalWidth;
|
||||||
|
topTaskPercent = leftTopBounds.height() / totalHeight;
|
||||||
|
dividerWidthPercent = visualDividerBounds.width() / totalWidth;
|
||||||
|
dividerHeightPercent = visualDividerBounds.height() / totalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SplitBounds(Parcel parcel) {
|
||||||
|
leftTopBounds = parcel.readTypedObject(Rect.CREATOR);
|
||||||
|
rightBottomBounds = parcel.readTypedObject(Rect.CREATOR);
|
||||||
|
visualDividerBounds = parcel.readTypedObject(Rect.CREATOR);
|
||||||
|
topTaskPercent = parcel.readFloat();
|
||||||
|
leftTaskPercent = parcel.readFloat();
|
||||||
|
appsStackedVertically = parcel.readBoolean();
|
||||||
|
leftTopTaskId = parcel.readInt();
|
||||||
|
rightBottomTaskId = parcel.readInt();
|
||||||
|
dividerWidthPercent = parcel.readFloat();
|
||||||
|
dividerHeightPercent = parcel.readFloat();
|
||||||
|
snapPosition = parcel.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel parcel, int flags) {
|
||||||
|
parcel.writeTypedObject(leftTopBounds, flags);
|
||||||
|
parcel.writeTypedObject(rightBottomBounds, flags);
|
||||||
|
parcel.writeTypedObject(visualDividerBounds, flags);
|
||||||
|
parcel.writeFloat(topTaskPercent);
|
||||||
|
parcel.writeFloat(leftTaskPercent);
|
||||||
|
parcel.writeBoolean(appsStackedVertically);
|
||||||
|
parcel.writeInt(leftTopTaskId);
|
||||||
|
parcel.writeInt(rightBottomTaskId);
|
||||||
|
parcel.writeFloat(dividerWidthPercent);
|
||||||
|
parcel.writeFloat(dividerHeightPercent);
|
||||||
|
parcel.writeInt(snapPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (!(obj instanceof SplitBounds)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Only need to check the base fields (the other fields are derived from these)
|
||||||
|
final SplitBounds other = (SplitBounds) obj;
|
||||||
|
return Objects.equals(leftTopBounds, other.leftTopBounds)
|
||||||
|
&& Objects.equals(rightBottomBounds, other.rightBottomBounds)
|
||||||
|
&& leftTopTaskId == other.leftTopTaskId
|
||||||
|
&& rightBottomTaskId == other.rightBottomTaskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(leftTopBounds, rightBottomBounds, leftTopTaskId, rightBottomTaskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "LeftTop: " + leftTopBounds + ", taskId: " + leftTopTaskId + "\n"
|
||||||
|
+ "RightBottom: " + rightBottomBounds + ", taskId: " + rightBottomTaskId + "\n"
|
||||||
|
+ "Divider: " + visualDividerBounds + "\n"
|
||||||
|
+ "AppsVertical? " + appsStackedVertically + "\n"
|
||||||
|
+ "snapPosition: " + snapPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<SplitBounds> CREATOR = new Creator<SplitBounds>() {
|
||||||
|
@Override
|
||||||
|
public SplitBounds createFromParcel(Parcel in) {
|
||||||
|
return new SplitBounds(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SplitBounds[] newArray(int size) {
|
||||||
|
return new SplitBounds[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.split;
|
||||||
|
|
||||||
|
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
|
||||||
|
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
|
||||||
|
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
|
||||||
|
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
|
||||||
|
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
|
||||||
|
|
||||||
|
import android.annotation.IntDef;
|
||||||
|
|
||||||
|
import com.android.wm.shell.shared.TransitionUtil;
|
||||||
|
|
||||||
|
/** Helper utility class of methods and constants that are available to be imported in Launcher. */
|
||||||
|
public class SplitScreenConstants {
|
||||||
|
/** Duration used for every split fade-in or fade-out. */
|
||||||
|
public static final int FADE_DURATION = 133;
|
||||||
|
/** Duration where we keep an app veiled to allow it to redraw itself behind the scenes. */
|
||||||
|
public static final int VEIL_DELAY_DURATION = 300;
|
||||||
|
|
||||||
|
/** Key for passing in widget intents when invoking split from launcher workspace. */
|
||||||
|
public static final String KEY_EXTRA_WIDGET_INTENT = "key_extra_widget_intent";
|
||||||
|
|
||||||
|
///////////////
|
||||||
|
// IMPORTANT for the following SPLIT_POSITION and SNAP_TO constants:
|
||||||
|
// These int values must not be changed -- they are persisted to user-defined app pairs, and
|
||||||
|
// will break things if changed.
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split position isn't specified normally meaning to use what ever it is currently set to.
|
||||||
|
*/
|
||||||
|
public static final int SPLIT_POSITION_UNDEFINED = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies that a split is positioned at the top half of the screen if
|
||||||
|
* in portrait mode or at the left half of the screen if in landscape mode.
|
||||||
|
*/
|
||||||
|
public static final int SPLIT_POSITION_TOP_OR_LEFT = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies that a split is positioned at the bottom half of the screen if
|
||||||
|
* in portrait mode or at the right half of the screen if in landscape mode.
|
||||||
|
*/
|
||||||
|
public static final int SPLIT_POSITION_BOTTOM_OR_RIGHT = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deprecated and will be replaced fully by @SplitIndex. With support for 3+ apps in split,
|
||||||
|
* existing references to top/left and bottom/right will be replaced by INDEX_0 and INDEX_1
|
||||||
|
* respectively. For now they can be used interchangeably, the underlying ints are the same.
|
||||||
|
*/
|
||||||
|
@IntDef(prefix = {"SPLIT_POSITION_"}, value = {
|
||||||
|
SPLIT_POSITION_UNDEFINED,
|
||||||
|
SPLIT_POSITION_TOP_OR_LEFT,
|
||||||
|
SPLIT_POSITION_BOTTOM_OR_RIGHT,
|
||||||
|
})
|
||||||
|
public @interface SplitPosition {
|
||||||
|
}
|
||||||
|
|
||||||
|
// These SPLIT_INDEX constants will be used in the same way as the above SPLIT_POSITION ints,
|
||||||
|
// but scalable to n apps. Eventually, SPLIT_POSITION can be deprecated and only the below
|
||||||
|
// will be used.
|
||||||
|
public static final int SPLIT_INDEX_UNDEFINED = -1;
|
||||||
|
public static final int SPLIT_INDEX_0 = 0;
|
||||||
|
public static final int SPLIT_INDEX_1 = 1;
|
||||||
|
public static final int SPLIT_INDEX_2 = 2;
|
||||||
|
public static final int SPLIT_INDEX_3 = 3;
|
||||||
|
|
||||||
|
@IntDef(prefix = {"SPLIT_INDEX_"}, value = {
|
||||||
|
SPLIT_INDEX_UNDEFINED,
|
||||||
|
SPLIT_INDEX_0,
|
||||||
|
SPLIT_INDEX_1,
|
||||||
|
SPLIT_INDEX_2,
|
||||||
|
SPLIT_INDEX_3
|
||||||
|
})
|
||||||
|
public @interface SplitIndex {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the @SplitIndex constant for a given integer index. @SplitIndex is the replacement
|
||||||
|
* for @SplitPosition, and will be used interchangeably with @SplitPosition to support 3+ apps
|
||||||
|
* in split.
|
||||||
|
*/
|
||||||
|
public static int getIndex(int i) {
|
||||||
|
return switch (i) {
|
||||||
|
case 0 -> SPLIT_INDEX_0;
|
||||||
|
case 1 -> SPLIT_INDEX_1;
|
||||||
|
case 2 -> SPLIT_INDEX_2;
|
||||||
|
case 3 -> SPLIT_INDEX_3;
|
||||||
|
default -> SPLIT_INDEX_UNDEFINED;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Signifies that user is currently not in split screen. */
|
||||||
|
public static final int NOT_IN_SPLIT = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A snap target for two apps, where the split is 33-66. With FLAG_ENABLE_FLEXIBLE_SPLIT,
|
||||||
|
* only used on tablets.
|
||||||
|
*/
|
||||||
|
public static final int SNAP_TO_2_33_66 = 0;
|
||||||
|
|
||||||
|
/** A snap target for two apps, where the split is 50-50. */
|
||||||
|
public static final int SNAP_TO_2_50_50 = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A snap target for two apps, where the split is 66-33. With FLAG_ENABLE_FLEXIBLE_SPLIT,
|
||||||
|
* only used on tablets.
|
||||||
|
*/
|
||||||
|
public static final int SNAP_TO_2_66_33 = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A snap target for two apps, where the split is 90-10. The "10" app extends off the screen,
|
||||||
|
* and is actually the same size as the onscreen app, but the visible portion takes up 10% of
|
||||||
|
* the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, used on phones and foldables.
|
||||||
|
*/
|
||||||
|
public static final int SNAP_TO_2_90_10 = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A snap target for two apps, where the split is 10-90. The "10" app extends off the screen,
|
||||||
|
* and is actually the same size as the onscreen app, but the visible portion takes up 10% of
|
||||||
|
* the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, used on phones and foldables.
|
||||||
|
*/
|
||||||
|
public static final int SNAP_TO_2_10_90 = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A snap target for three apps, where the split is 33-33-33. With FLAG_ENABLE_FLEXIBLE_SPLIT,
|
||||||
|
* only used on tablets.
|
||||||
|
*/
|
||||||
|
public static final int SNAP_TO_3_33_33_33 = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A snap target for three apps, where the split is 45-45-10. The "10" app extends off the
|
||||||
|
* screen, and is actually the same size as the onscreen apps, but the visible portion takes
|
||||||
|
* up 10% of the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, only used on unfolded foldables.
|
||||||
|
*/
|
||||||
|
public static final int SNAP_TO_3_45_45_10 = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A snap target for three apps, where the split is 10-45-45. The "10" app extends off the
|
||||||
|
* screen, and is actually the same size as the onscreen apps, but the visible portion takes
|
||||||
|
* up 10% of the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, only used on unfolded foldables.
|
||||||
|
*/
|
||||||
|
public static final int SNAP_TO_3_10_45_45 = 7;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These snap targets are used for split pairs in a stable, non-transient state. They may be
|
||||||
|
* persisted in Launcher when the user saves an app pair. They are a subset of
|
||||||
|
* {@link SnapPosition}.
|
||||||
|
*/
|
||||||
|
@IntDef(prefix = { "SNAP_TO_" }, value = {
|
||||||
|
SNAP_TO_2_33_66,
|
||||||
|
SNAP_TO_2_50_50,
|
||||||
|
SNAP_TO_2_66_33,
|
||||||
|
SNAP_TO_2_90_10,
|
||||||
|
SNAP_TO_2_10_90,
|
||||||
|
SNAP_TO_3_33_33_33,
|
||||||
|
SNAP_TO_3_45_45_10,
|
||||||
|
SNAP_TO_3_10_45_45,
|
||||||
|
})
|
||||||
|
public @interface PersistentSnapPosition {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are all the valid "states" that split screen can be in. It's the set of
|
||||||
|
* {@link PersistentSnapPosition} + {@link #NOT_IN_SPLIT}.
|
||||||
|
*/
|
||||||
|
@IntDef(value = {
|
||||||
|
NOT_IN_SPLIT, // user is not in split screen
|
||||||
|
SNAP_TO_NONE, // in "free snap mode," where apps are fully resizable
|
||||||
|
SNAP_TO_2_33_66,
|
||||||
|
SNAP_TO_2_50_50,
|
||||||
|
SNAP_TO_2_66_33,
|
||||||
|
SNAP_TO_2_90_10,
|
||||||
|
SNAP_TO_2_10_90,
|
||||||
|
SNAP_TO_3_33_33_33,
|
||||||
|
SNAP_TO_3_45_45_10,
|
||||||
|
SNAP_TO_3_10_45_45,
|
||||||
|
})
|
||||||
|
public @interface SplitScreenState {}
|
||||||
|
|
||||||
|
/** Converts a {@link SplitScreenState} to a human-readable string. */
|
||||||
|
public static String stateToString(@SplitScreenState int state) {
|
||||||
|
return switch (state) {
|
||||||
|
case NOT_IN_SPLIT -> "NOT_IN_SPLIT";
|
||||||
|
case SNAP_TO_NONE -> "SNAP_TO_NONE";
|
||||||
|
case SNAP_TO_2_33_66 -> "SNAP_TO_2_33_66";
|
||||||
|
case SNAP_TO_2_50_50 -> "SNAP_TO_2_50_50";
|
||||||
|
case SNAP_TO_2_66_33 -> "SNAP_TO_2_66_33";
|
||||||
|
case SNAP_TO_2_90_10 -> "SNAP_TO_2_90_10";
|
||||||
|
case SNAP_TO_2_10_90 -> "SNAP_TO_2_10_90";
|
||||||
|
case SNAP_TO_3_33_33_33 -> "SNAP_TO_3_33_33_33";
|
||||||
|
case SNAP_TO_3_45_45_10 -> "SNAP_TO_3_45_45_10";
|
||||||
|
case SNAP_TO_3_10_45_45 -> "SNAP_TO_3_10_45_45";
|
||||||
|
default -> "UNKNOWN";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the snapPosition in question is a {@link PersistentSnapPosition}.
|
||||||
|
*/
|
||||||
|
public static boolean isPersistentSnapPosition(@SnapPosition int snapPosition) {
|
||||||
|
return snapPosition == SNAP_TO_2_33_66
|
||||||
|
|| snapPosition == SNAP_TO_2_50_50
|
||||||
|
|| snapPosition == SNAP_TO_2_66_33
|
||||||
|
|| snapPosition == SNAP_TO_2_90_10
|
||||||
|
|| snapPosition == SNAP_TO_2_10_90
|
||||||
|
|| snapPosition == SNAP_TO_3_33_33_33
|
||||||
|
|| snapPosition == SNAP_TO_3_45_45_10
|
||||||
|
|| snapPosition == SNAP_TO_3_10_45_45;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The divider doesn't snap to any target and is freely placeable. */
|
||||||
|
public static final int SNAP_TO_NONE = 10;
|
||||||
|
|
||||||
|
/** If the divider reaches this value, the left/top task should be dismissed. */
|
||||||
|
public static final int SNAP_TO_START_AND_DISMISS = 11;
|
||||||
|
|
||||||
|
/** If the divider reaches this value, the right/bottom task should be dismissed. */
|
||||||
|
public static final int SNAP_TO_END_AND_DISMISS = 12;
|
||||||
|
|
||||||
|
/** A snap target positioned near the screen edge for a minimized task */
|
||||||
|
public static final int SNAP_TO_MINIMIZE = 13;
|
||||||
|
|
||||||
|
@IntDef(prefix = { "SNAP_TO_" }, value = {
|
||||||
|
SNAP_TO_2_33_66,
|
||||||
|
SNAP_TO_2_50_50,
|
||||||
|
SNAP_TO_2_66_33,
|
||||||
|
SNAP_TO_2_90_10,
|
||||||
|
SNAP_TO_2_10_90,
|
||||||
|
SNAP_TO_3_33_33_33,
|
||||||
|
SNAP_TO_3_45_45_10,
|
||||||
|
SNAP_TO_3_10_45_45,
|
||||||
|
SNAP_TO_NONE,
|
||||||
|
SNAP_TO_START_AND_DISMISS,
|
||||||
|
SNAP_TO_END_AND_DISMISS,
|
||||||
|
SNAP_TO_MINIMIZE,
|
||||||
|
})
|
||||||
|
public @interface SnapPosition {}
|
||||||
|
|
||||||
|
///////////////
|
||||||
|
|
||||||
|
public static final int[] CONTROLLED_ACTIVITY_TYPES = {ACTIVITY_TYPE_STANDARD};
|
||||||
|
public static final int[] CONTROLLED_WINDOWING_MODES =
|
||||||
|
{WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED};
|
||||||
|
public static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE =
|
||||||
|
{WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW,
|
||||||
|
WINDOWING_MODE_FREEFORM};
|
||||||
|
|
||||||
|
/** Flag applied to a transition change to identify it as a divider bar for animation. */
|
||||||
|
public static final int FLAG_IS_DIVIDER_BAR = TransitionUtil.FLAG_IS_DIVIDER_BAR;
|
||||||
|
public static final int FLAG_IS_DIM_LAYER = TransitionUtil.FLAG_IS_DIM_LAYER;
|
||||||
|
|
||||||
|
public static final String splitPositionToString(@SplitPosition int pos) {
|
||||||
|
switch (pos) {
|
||||||
|
case SPLIT_POSITION_UNDEFINED:
|
||||||
|
return "SPLIT_POSITION_UNDEFINED";
|
||||||
|
case SPLIT_POSITION_TOP_OR_LEFT:
|
||||||
|
return "SPLIT_POSITION_TOP_OR_LEFT";
|
||||||
|
case SPLIT_POSITION_BOTTOM_OR_RIGHT:
|
||||||
|
return "SPLIT_POSITION_BOTTOM_OR_RIGHT";
|
||||||
|
default:
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+459
@@ -0,0 +1,459 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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.shared.startingsurface;
|
||||||
|
|
||||||
|
import static android.view.Choreographer.CALLBACK_COMMIT;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.annotation.IntDef;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.graphics.BlendMode;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Point;
|
||||||
|
import android.graphics.RadialGradient;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.Shader;
|
||||||
|
import android.util.MathUtils;
|
||||||
|
import android.util.Slog;
|
||||||
|
import android.view.Choreographer;
|
||||||
|
import android.view.SurfaceControl;
|
||||||
|
import android.view.SyncRtSurfaceTransactionApplier;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.view.animation.Interpolator;
|
||||||
|
import android.view.animation.PathInterpolator;
|
||||||
|
import android.window.SplashScreenView;
|
||||||
|
|
||||||
|
import com.android.wm.shell.shared.TransactionPool;
|
||||||
|
import com.android.wm.shell.shared.animation.Interpolators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for creating the splash screen window animations.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public class SplashScreenExitAnimationUtils {
|
||||||
|
private static final boolean DEBUG_EXIT_ANIMATION = false;
|
||||||
|
private static final boolean DEBUG_EXIT_ANIMATION_BLEND = false;
|
||||||
|
private static final boolean DEBUG_EXIT_FADE_ANIMATION = false;
|
||||||
|
private static final String TAG = "SplashScreenExitAnimationUtils";
|
||||||
|
|
||||||
|
private static final Interpolator ICON_INTERPOLATOR = new PathInterpolator(0.15f, 0f, 1f, 1f);
|
||||||
|
private static final Interpolator MASK_RADIUS_INTERPOLATOR =
|
||||||
|
new PathInterpolator(0f, 0f, 0.4f, 1f);
|
||||||
|
private static final Interpolator SHIFT_UP_INTERPOLATOR = new PathInterpolator(0f, 0f, 0f, 1f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This splash screen exit animation type uses a radial vanish to hide
|
||||||
|
* the starting window and slides up the main window content.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public static final int TYPE_RADIAL_VANISH_SLIDE_UP = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This splash screen exit animation type fades out the starting window
|
||||||
|
* to reveal the main window content.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public static final int TYPE_FADE_OUT = 1;
|
||||||
|
|
||||||
|
/** @hide */
|
||||||
|
@IntDef(prefix = { "TYPE_" }, value = {
|
||||||
|
TYPE_RADIAL_VANISH_SLIDE_UP,
|
||||||
|
TYPE_FADE_OUT,
|
||||||
|
})
|
||||||
|
public @interface ExitAnimationType {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and starts the animator to fade out the icon, reveal the app, and shift up main
|
||||||
|
* window with rounded corner radius.
|
||||||
|
*/
|
||||||
|
public static void startAnimations(@ExitAnimationType int animationType,
|
||||||
|
ViewGroup splashScreenView, SurfaceControl firstWindowSurface,
|
||||||
|
int mainWindowShiftLength, TransactionPool transactionPool, Rect firstWindowFrame,
|
||||||
|
int animationDuration, int iconFadeOutDuration, float iconStartAlpha,
|
||||||
|
float brandingStartAlpha, int appRevealDelay, int appRevealDuration,
|
||||||
|
Animator.AnimatorListener animatorListener, float roundedCornerRadius) {
|
||||||
|
ValueAnimator animator;
|
||||||
|
if (animationType == TYPE_FADE_OUT) {
|
||||||
|
animator = createFadeOutAnimation(splashScreenView, animationDuration,
|
||||||
|
iconFadeOutDuration, iconStartAlpha, brandingStartAlpha, appRevealDelay,
|
||||||
|
appRevealDuration, animatorListener);
|
||||||
|
} else {
|
||||||
|
animator = createRadialVanishSlideUpAnimator(splashScreenView,
|
||||||
|
firstWindowSurface, mainWindowShiftLength, transactionPool, firstWindowFrame,
|
||||||
|
animationDuration, iconFadeOutDuration, iconStartAlpha, brandingStartAlpha,
|
||||||
|
appRevealDelay, appRevealDuration, animatorListener, roundedCornerRadius);
|
||||||
|
}
|
||||||
|
animator.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and starts the animator to fade out the icon, reveal the app, and shift up main
|
||||||
|
* window.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public static void startAnimations(ViewGroup splashScreenView,
|
||||||
|
SurfaceControl firstWindowSurface, int mainWindowShiftLength,
|
||||||
|
TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration,
|
||||||
|
int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha,
|
||||||
|
int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener) {
|
||||||
|
// Start the default 'reveal' animation.
|
||||||
|
startAnimations(TYPE_RADIAL_VANISH_SLIDE_UP, splashScreenView,
|
||||||
|
firstWindowSurface, mainWindowShiftLength, transactionPool, firstWindowFrame,
|
||||||
|
animationDuration, iconFadeOutDuration, iconStartAlpha, brandingStartAlpha,
|
||||||
|
appRevealDelay, appRevealDuration, animatorListener, 0f /* roundedCornerRadius */);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the animator to fade out the icon, reveal the app, and shift up main window.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
private static ValueAnimator createRadialVanishSlideUpAnimator(ViewGroup splashScreenView,
|
||||||
|
SurfaceControl firstWindowSurface, int mMainWindowShiftLength,
|
||||||
|
TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration,
|
||||||
|
int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha,
|
||||||
|
int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener,
|
||||||
|
float roundedCornerRadius) {
|
||||||
|
// reveal app
|
||||||
|
final float transparentRatio = 0.8f;
|
||||||
|
final int globalHeight = splashScreenView.getHeight();
|
||||||
|
final int verticalCircleCenter = 0;
|
||||||
|
final int finalVerticalLength = globalHeight - verticalCircleCenter;
|
||||||
|
final int halfWidth = splashScreenView.getWidth() / 2;
|
||||||
|
final int endRadius = (int) (0.5 + (1f / transparentRatio * (int)
|
||||||
|
Math.sqrt(finalVerticalLength * finalVerticalLength + halfWidth * halfWidth)));
|
||||||
|
final int[] colors = {Color.WHITE, Color.WHITE, Color.TRANSPARENT};
|
||||||
|
final float[] stops = {0f, transparentRatio, 1f};
|
||||||
|
|
||||||
|
RadialVanishAnimation radialVanishAnimation = new RadialVanishAnimation(splashScreenView);
|
||||||
|
radialVanishAnimation.setCircleCenter(halfWidth, verticalCircleCenter);
|
||||||
|
radialVanishAnimation.setRadius(0 /* initRadius */, endRadius);
|
||||||
|
radialVanishAnimation.setRadialPaintParam(colors, stops);
|
||||||
|
|
||||||
|
View occludeHoleView = null;
|
||||||
|
ShiftUpAnimation shiftUpAnimation = null;
|
||||||
|
if (firstWindowSurface != null && firstWindowSurface.isValid()) {
|
||||||
|
// shift up main window
|
||||||
|
occludeHoleView = new View(splashScreenView.getContext());
|
||||||
|
if (DEBUG_EXIT_ANIMATION_BLEND) {
|
||||||
|
occludeHoleView.setBackgroundColor(Color.BLUE);
|
||||||
|
} else if (splashScreenView instanceof SplashScreenView) {
|
||||||
|
occludeHoleView.setBackgroundColor(
|
||||||
|
((SplashScreenView) splashScreenView).getInitBackgroundColor());
|
||||||
|
} else {
|
||||||
|
occludeHoleView.setBackgroundColor(
|
||||||
|
isDarkTheme(splashScreenView.getContext()) ? Color.BLACK : Color.WHITE);
|
||||||
|
}
|
||||||
|
final ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT, mMainWindowShiftLength);
|
||||||
|
splashScreenView.addView(occludeHoleView, params);
|
||||||
|
|
||||||
|
shiftUpAnimation = new ShiftUpAnimation(0, -mMainWindowShiftLength, occludeHoleView,
|
||||||
|
firstWindowSurface, splashScreenView, transactionPool, firstWindowFrame,
|
||||||
|
mMainWindowShiftLength, roundedCornerRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
|
||||||
|
animator.setDuration(animationDuration);
|
||||||
|
animator.setInterpolator(Interpolators.LINEAR);
|
||||||
|
if (animatorListener != null) {
|
||||||
|
animator.addListener(animatorListener);
|
||||||
|
}
|
||||||
|
View finalOccludeHoleView = occludeHoleView;
|
||||||
|
ShiftUpAnimation finalShiftUpAnimation = shiftUpAnimation;
|
||||||
|
animator.addListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
super.onAnimationEnd(animation);
|
||||||
|
if (finalShiftUpAnimation != null) {
|
||||||
|
finalShiftUpAnimation.finish();
|
||||||
|
}
|
||||||
|
splashScreenView.removeView(radialVanishAnimation);
|
||||||
|
splashScreenView.removeView(finalOccludeHoleView);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
animator.addUpdateListener(animation -> {
|
||||||
|
float linearProgress = (float) animation.getAnimatedValue();
|
||||||
|
|
||||||
|
// Fade out progress
|
||||||
|
final float iconProgress =
|
||||||
|
ICON_INTERPOLATOR.getInterpolation(getProgress(
|
||||||
|
linearProgress, 0 /* delay */, iconFadeOutDuration, animationDuration));
|
||||||
|
View iconView = null;
|
||||||
|
View brandingView = null;
|
||||||
|
if (splashScreenView instanceof SplashScreenView) {
|
||||||
|
iconView = ((SplashScreenView) splashScreenView).getIconView();
|
||||||
|
brandingView = ((SplashScreenView) splashScreenView).getBrandingView();
|
||||||
|
}
|
||||||
|
if (iconView != null) {
|
||||||
|
iconView.setAlpha(iconStartAlpha * (1 - iconProgress));
|
||||||
|
}
|
||||||
|
if (brandingView != null) {
|
||||||
|
brandingView.setAlpha(brandingStartAlpha * (1 - iconProgress));
|
||||||
|
}
|
||||||
|
|
||||||
|
final float revealLinearProgress = getProgress(linearProgress, appRevealDelay,
|
||||||
|
appRevealDuration, animationDuration);
|
||||||
|
|
||||||
|
radialVanishAnimation.onAnimationProgress(revealLinearProgress);
|
||||||
|
|
||||||
|
if (finalShiftUpAnimation != null) {
|
||||||
|
finalShiftUpAnimation.onAnimationProgress(revealLinearProgress);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return animator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float getProgress(float linearProgress, long delay, long duration,
|
||||||
|
int animationDuration) {
|
||||||
|
return MathUtils.constrain(
|
||||||
|
(linearProgress * (animationDuration) - delay) / duration,
|
||||||
|
0.0f,
|
||||||
|
1.0f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isDarkTheme(Context context) {
|
||||||
|
Configuration configuration = context.getResources().getConfiguration();
|
||||||
|
int nightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||||
|
return nightMode == Configuration.UI_MODE_NIGHT_YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ValueAnimator createFadeOutAnimation(ViewGroup splashScreenView,
|
||||||
|
int animationDuration, int iconFadeOutDuration, float iconStartAlpha,
|
||||||
|
float brandingStartAlpha, int appRevealDelay, int appRevealDuration,
|
||||||
|
Animator.AnimatorListener animatorListener) {
|
||||||
|
|
||||||
|
if (DEBUG_EXIT_FADE_ANIMATION) {
|
||||||
|
splashScreenView.setBackgroundColor(Color.BLUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
|
||||||
|
animator.setDuration(animationDuration);
|
||||||
|
animator.setInterpolator(Interpolators.LINEAR);
|
||||||
|
animator.addUpdateListener(animation -> {
|
||||||
|
|
||||||
|
float linearProgress = (float) animation.getAnimatedValue();
|
||||||
|
|
||||||
|
// Icon fade out progress (always starts immediately)
|
||||||
|
final float iconFadeProgress = ICON_INTERPOLATOR.getInterpolation(getProgress(
|
||||||
|
linearProgress, 0 /* delay */, iconFadeOutDuration, animationDuration));
|
||||||
|
View iconView = null;
|
||||||
|
View brandingView = null;
|
||||||
|
|
||||||
|
if (splashScreenView instanceof SplashScreenView) {
|
||||||
|
iconView = ((SplashScreenView) splashScreenView).getIconView();
|
||||||
|
brandingView = ((SplashScreenView) splashScreenView).getBrandingView();
|
||||||
|
}
|
||||||
|
if (iconView != null) {
|
||||||
|
iconView.setAlpha(iconStartAlpha * (1f - iconFadeProgress));
|
||||||
|
}
|
||||||
|
if (brandingView != null) {
|
||||||
|
brandingView.setAlpha(brandingStartAlpha * (1f - iconFadeProgress));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splash screen fade out progress (possibly delayed)
|
||||||
|
final float splashFadeProgress = Interpolators.ALPHA_OUT.getInterpolation(
|
||||||
|
getProgress(linearProgress, appRevealDelay,
|
||||||
|
appRevealDuration, animationDuration));
|
||||||
|
|
||||||
|
splashScreenView.setAlpha(1f - splashFadeProgress);
|
||||||
|
|
||||||
|
if (DEBUG_EXIT_FADE_ANIMATION) {
|
||||||
|
Slog.d(TAG, "progress -> animation: " + linearProgress
|
||||||
|
+ "\t icon alpha: " + ((iconView != null) ? iconView.getAlpha() : "n/a")
|
||||||
|
+ "\t splash alpha: " + splashScreenView.getAlpha()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (animatorListener != null) {
|
||||||
|
animator.addListener(animatorListener);
|
||||||
|
}
|
||||||
|
return animator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View which creates a circular reveal of the underlying view.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
@SuppressLint("ViewConstructor")
|
||||||
|
public static class RadialVanishAnimation extends View {
|
||||||
|
private final ViewGroup mView;
|
||||||
|
private int mInitRadius;
|
||||||
|
private int mFinishRadius;
|
||||||
|
|
||||||
|
private final Point mCircleCenter = new Point();
|
||||||
|
private final Matrix mVanishMatrix = new Matrix();
|
||||||
|
private final Paint mVanishPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
|
||||||
|
public RadialVanishAnimation(ViewGroup target) {
|
||||||
|
super(target.getContext());
|
||||||
|
mView = target;
|
||||||
|
mView.addView(this);
|
||||||
|
if (getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
|
||||||
|
((ViewGroup.MarginLayoutParams) getLayoutParams()).setMargins(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
mVanishPaint.setAlpha(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAnimationProgress(float linearProgress) {
|
||||||
|
if (mVanishPaint.getShader() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final float radiusProgress = MASK_RADIUS_INTERPOLATOR.getInterpolation(linearProgress);
|
||||||
|
final float alphaProgress = Interpolators.ALPHA_OUT.getInterpolation(linearProgress);
|
||||||
|
final float scale = mInitRadius + (mFinishRadius - mInitRadius) * radiusProgress;
|
||||||
|
|
||||||
|
mVanishMatrix.setScale(scale, scale);
|
||||||
|
mVanishMatrix.postTranslate(mCircleCenter.x, mCircleCenter.y);
|
||||||
|
mVanishPaint.getShader().setLocalMatrix(mVanishMatrix);
|
||||||
|
mVanishPaint.setAlpha(Math.round(0xFF * alphaProgress));
|
||||||
|
|
||||||
|
postInvalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRadius(int initRadius, int finishRadius) {
|
||||||
|
if (DEBUG_EXIT_ANIMATION) {
|
||||||
|
Slog.v(TAG, "RadialVanishAnimation setRadius init: " + initRadius
|
||||||
|
+ " final " + finishRadius);
|
||||||
|
}
|
||||||
|
mInitRadius = initRadius;
|
||||||
|
mFinishRadius = finishRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCircleCenter(int x, int y) {
|
||||||
|
if (DEBUG_EXIT_ANIMATION) {
|
||||||
|
Slog.v(TAG, "RadialVanishAnimation setCircleCenter x: " + x + " y " + y);
|
||||||
|
}
|
||||||
|
mCircleCenter.set(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRadialPaintParam(int[] colors, float[] stops) {
|
||||||
|
// setup gradient shader
|
||||||
|
final RadialGradient rShader =
|
||||||
|
new RadialGradient(0, 0, 1, colors, stops, Shader.TileMode.CLAMP);
|
||||||
|
mVanishPaint.setShader(rShader);
|
||||||
|
if (!DEBUG_EXIT_ANIMATION_BLEND) {
|
||||||
|
// We blend the reveal gradient with the splash screen using DST_OUT so that the
|
||||||
|
// splash screen is fully visible when radius = 0 (or gradient opacity is 0) and
|
||||||
|
// fully invisible when radius = finishRadius AND gradient opacity is 1.
|
||||||
|
mVanishPaint.setBlendMode(BlendMode.DST_OUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
canvas.drawRect(0, 0, mView.getWidth(), mView.getHeight(), mVanishPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shifts up the main window.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public static final class ShiftUpAnimation {
|
||||||
|
private final float mFromYDelta;
|
||||||
|
private final float mToYDelta;
|
||||||
|
private final View mOccludeHoleView;
|
||||||
|
private final SyncRtSurfaceTransactionApplier mApplier;
|
||||||
|
private final Matrix mTmpTransform = new Matrix();
|
||||||
|
private final SurfaceControl mFirstWindowSurface;
|
||||||
|
private final ViewGroup mSplashScreenView;
|
||||||
|
private final TransactionPool mTransactionPool;
|
||||||
|
private final Rect mFirstWindowFrame;
|
||||||
|
private final int mMainWindowShiftLength;
|
||||||
|
|
||||||
|
public ShiftUpAnimation(float fromYDelta, float toYDelta, View occludeHoleView,
|
||||||
|
SurfaceControl firstWindowSurface, ViewGroup splashScreenView,
|
||||||
|
TransactionPool transactionPool, Rect firstWindowFrame,
|
||||||
|
int mainWindowShiftLength, float roundedCornerRadius) {
|
||||||
|
mFromYDelta = firstWindowFrame.top
|
||||||
|
- Math.max(firstWindowFrame.top, roundedCornerRadius);
|
||||||
|
mToYDelta = toYDelta;
|
||||||
|
mOccludeHoleView = occludeHoleView;
|
||||||
|
mApplier = new SyncRtSurfaceTransactionApplier(occludeHoleView);
|
||||||
|
mFirstWindowSurface = firstWindowSurface;
|
||||||
|
mSplashScreenView = splashScreenView;
|
||||||
|
mTransactionPool = transactionPool;
|
||||||
|
mFirstWindowFrame = firstWindowFrame;
|
||||||
|
mMainWindowShiftLength = mainWindowShiftLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAnimationProgress(float linearProgress) {
|
||||||
|
if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid()
|
||||||
|
|| !mSplashScreenView.isAttachedToWindow()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final float progress = SHIFT_UP_INTERPOLATOR.getInterpolation(linearProgress);
|
||||||
|
final float dy = mFromYDelta + (mToYDelta - mFromYDelta) * progress;
|
||||||
|
|
||||||
|
mOccludeHoleView.setTranslationY(dy);
|
||||||
|
mTmpTransform.setTranslate(0 /* dx */, dy);
|
||||||
|
|
||||||
|
// set the vsyncId to ensure the transaction doesn't get applied too early.
|
||||||
|
final SurfaceControl.Transaction tx = mTransactionPool.acquire();
|
||||||
|
tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId());
|
||||||
|
mTmpTransform.postTranslate(mFirstWindowFrame.left,
|
||||||
|
mFirstWindowFrame.top + mMainWindowShiftLength);
|
||||||
|
|
||||||
|
SyncRtSurfaceTransactionApplier.SurfaceParams
|
||||||
|
params = new SyncRtSurfaceTransactionApplier.SurfaceParams
|
||||||
|
.Builder(mFirstWindowSurface)
|
||||||
|
.withMatrix(mTmpTransform)
|
||||||
|
.withMergeTransaction(tx)
|
||||||
|
.build();
|
||||||
|
mApplier.scheduleApply(params);
|
||||||
|
|
||||||
|
mTransactionPool.release(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void finish() {
|
||||||
|
if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final SurfaceControl.Transaction tx = mTransactionPool.acquire();
|
||||||
|
if (mSplashScreenView.isAttachedToWindow()) {
|
||||||
|
tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId());
|
||||||
|
|
||||||
|
SyncRtSurfaceTransactionApplier.SurfaceParams
|
||||||
|
params = new SyncRtSurfaceTransactionApplier.SurfaceParams
|
||||||
|
.Builder(mFirstWindowSurface)
|
||||||
|
.withWindowCrop(null)
|
||||||
|
.withMergeTransaction(tx)
|
||||||
|
.build();
|
||||||
|
mApplier.scheduleApply(params);
|
||||||
|
} else {
|
||||||
|
tx.setWindowCrop(mFirstWindowSurface, null);
|
||||||
|
tx.apply();
|
||||||
|
}
|
||||||
|
mTransactionPool.release(tx);
|
||||||
|
|
||||||
|
Choreographer.getSfInstance().postCallback(CALLBACK_COMMIT,
|
||||||
|
mFirstWindowSurface::release, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
BIN
Binary file not shown.
@@ -188,6 +188,7 @@ dependencies {
|
|||||||
implementation project(':animationlib')
|
implementation project(':animationlib')
|
||||||
implementation project(':Shared')
|
implementation project(':Shared')
|
||||||
implementation project(':msdllib')
|
implementation project(':msdllib')
|
||||||
|
implementation project(':WindowManager-Shared')
|
||||||
// implementation project(':UiTestsLibLauncher')
|
// implementation project(':UiTestsLibLauncher')
|
||||||
// withQuickstepImplementation project(':SharedLibWrapper')
|
// withQuickstepImplementation project(':SharedLibWrapper')
|
||||||
implementation 'com.google.protobuf:protobuf-javalite:4.26.1'
|
implementation 'com.google.protobuf:protobuf-javalite:4.26.1'
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -4,6 +4,8 @@ include ':Shared'
|
|||||||
project(':Shared').projectDir = new File(rootDir, 'shared')
|
project(':Shared').projectDir = new File(rootDir, 'shared')
|
||||||
include ':msdllib'
|
include ':msdllib'
|
||||||
project(':msdllib').projectDir = new File(rootDir, 'msdllib')
|
project(':msdllib').projectDir = new File(rootDir, 'msdllib')
|
||||||
|
include ':WindowManager-Shared'
|
||||||
|
project(':WindowManager-Shared').projectDir = new File(rootDir, 'WindowManager/shared')
|
||||||
|
|
||||||
//include ':SharedLibWrapper'
|
//include ':SharedLibWrapper'
|
||||||
//project(':SharedLibWrapper').projectDir = new File(rootDir, 'SharedLibWrapper')
|
//project(':SharedLibWrapper').projectDir = new File(rootDir, 'SharedLibWrapper')
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Reference in new issue
Block a user