fix(post-clean): Cleanup mixed branch status

Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
This commit is contained in:
Pun Butrach committed 2026-01-10 22:36:39 +07:00
1 parent 8fa8e5a4ba
commit 270030e5b1
328 files changed
-50906

No files matched your search

-12
View File
@@ -1,12 +0,0 @@
package: "com.android.launcher3"
container: "system_ext"
flag {
name: "remove_exclude_from_screen_magnification_flag_usage"
namespace: "accessibility"
description: "Remove the WindowManager PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION flag usage in Launcher"
bug: "369019568"
metadata {
purpose: PURPOSE_BUGFIX
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 KiB

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="@dimen/profile_badge_size"
android:height="@dimen/profile_badge_size"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#11000000"
android:pathData="M.5,12.25
A11.5,11.5 0 1,1 23.5,12.25
A11.5,11.5 0 1,1 .5,12.25" />
<path
android:fillColor="@android:color/white"
android:pathData="M1,12
A11,11 0 1,1 23,12
A11,11 0 1,1 1,12" />
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX=".6"
android:scaleY=".6">
<path
android:fillColor="#1A73E8"
android:pathData="M20,6h-4L16,4c0,-1.11 -0.89,-2 -2,-2h-4c-1.11,0 -2,0.89 -2,2v2L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM14,6h-4L10,4h4v2z" />
</group>
</vector>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,23 +0,0 @@
package app.lawnchair
import android.content.Context
import android.content.pm.LauncherActivityInfo
import androidx.annotation.Keep
import app.lawnchair.preferences.PreferenceManager
import com.android.launcher3.icons.LauncherActivityCachingLogic as BaseLogic
import com.android.launcher3.util.ComponentKey
@Keep
class LauncherActivityCachingLogic(context: Context) : BaseLogic() {
private val prefs = PreferenceManager.getInstance(context)
override fun getLabel(info: LauncherActivityInfo): CharSequence {
val key = ComponentKey(info.componentName, info.user)
val customLabel = prefs.customAppName[key]
if (!customLabel.isNullOrEmpty()) {
return customLabel
}
return super.getLabel(info)
}
}
@@ -1,24 +0,0 @@
package app.lawnchair.qsb.providers
import android.content.Intent
import app.lawnchair.qsb.ThemingMethod
import com.android.launcher3.R
data object Mull : QsbSearchProvider(
id = "Mull",
name = R.string.search_provider_mull,
icon = R.drawable.ic_mull,
themedIcon = R.drawable.ic_mull_tinted,
themingMethod = ThemingMethod.TINT,
packageName = "us.spotco.fennec_dos",
action = "org.mozilla.fenix.OPEN_TAB",
className = "org.mozilla.fenix.IntentReceiverActivity",
website = "https://gitlab.com/divested-mobile/mull-fenix",
type = QsbSearchProviderType.APP,
supportVoiceIntent = true,
) {
override fun handleCreateVoiceIntent(): Intent = Intent(action)
.addFlags(INTENT_FLAGS)
.setClassName(packageName, "org.chromium.chrome.browser.VoiceSearchActivity")
}
@@ -1,61 +0,0 @@
package app.lawnchair.search.algorithms.engine.provider
import android.content.Context
import app.lawnchair.preferences2.PreferenceManager2
import app.lawnchair.search.algorithms.engine.SearchResult
import app.lawnchair.search.algorithms.filterHiddenApps
import com.android.launcher3.model.AllAppsList
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.search.StringMatcherUtility
import com.patrykmichalik.opto.core.firstBlocking
import java.util.Locale
import me.xdrop.fuzzywuzzy.FuzzySearch
import me.xdrop.fuzzywuzzy.algorithms.WeightedRatio
object AppSearchProvider {
fun search(context: Context, query: String, allApps: AllAppsList): List<SearchResult.App> {
val prefs = PreferenceManager2.getInstance(context)
val hiddenApps = prefs.hiddenApps.firstBlocking()
val hiddenAppsInSearch = prefs.hiddenAppsInSearch.firstBlocking()
val maxAppResults = prefs.maxAppSearchResultCount.firstBlocking()
val enableFuzzySearch = prefs.enableFuzzySearch.firstBlocking()
val appResults = if (enableFuzzySearch) {
fuzzySearch(allApps.data, query, maxAppResults, hiddenApps, hiddenAppsInSearch)
} else {
normalSearch(allApps.data, query, maxAppResults, hiddenApps, hiddenAppsInSearch)
}
return appResults.map { SearchResult.App(data = it) }
}
private fun normalSearch(apps: List<AppInfo>, query: String, maxResultsCount: Int, hiddenApps: Set<String>, hiddenAppsInSearch: String): List<AppInfo> {
// Do an intersection of the words in the query and each title, and filter out all the
// apps that don't match all of the words in the query.
val queryTextLower = query.lowercase(Locale.getDefault())
val matcher = StringMatcherUtility.StringMatcher.getInstance()
return apps.asSequence()
.filter { StringMatcherUtility.matches(queryTextLower, it.title.toString(), matcher) }
.filterHiddenApps(queryTextLower, hiddenApps, hiddenAppsInSearch)
.take(maxResultsCount)
.toList()
}
private fun fuzzySearch(apps: List<AppInfo>, query: String, maxResultsCount: Int, hiddenApps: Set<String>, hiddenAppsInSearch: String): List<AppInfo> {
val queryTextLower = query.lowercase(Locale.getDefault())
val filteredApps = apps.asSequence()
.filterHiddenApps(queryTextLower, hiddenApps, hiddenAppsInSearch)
.toList()
val matches = FuzzySearch.extractSorted(
queryTextLower,
filteredApps,
{ it.sectionName + it.title },
WeightedRatio(),
65,
)
return matches.take(maxResultsCount)
.map { it.referent }
}
}
@@ -1,348 +0,0 @@
package app.lawnchair.ui.preferences.components
import android.view.HapticFeedbackConstants
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.rounded.DragHandle
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import app.lawnchair.ui.preferences.components.controls.ClickablePreference
import app.lawnchair.ui.preferences.components.layout.ExpandAndShrink
import app.lawnchair.ui.preferences.components.layout.PreferenceGroup
import app.lawnchair.ui.preferences.components.layout.PreferenceGroupHeading
import app.lawnchair.ui.preferences.components.layout.PreferenceTemplate
import app.lawnchair.ui.theme.preferenceGroupColor
import app.lawnchair.ui.util.addIf
import com.android.launcher3.R
import com.android.launcher3.Utilities
import sh.calvin.reorderable.ReorderableColumn
import sh.calvin.reorderable.ReorderableListItemScope
@Composable
fun <T> DraggablePreferenceGroup(
label: String?,
items: List<T>,
defaultList: List<T>,
onOrderChange: (List<T>) -> Unit,
modifier: Modifier = Modifier,
onSettle: ((List<T>) -> Unit)? = null,
itemContent: @Composable ReorderableListItemScope.(
item: T,
index: Int,
isDragging: Boolean,
onDraggingChange: (Boolean) -> Unit,
) -> Unit,
) {
var localItems by remember { mutableStateOf(items) }
LaunchedEffect(items) {
if (localItems != items) {
localItems = items
}
}
var isAnyDragging by remember { mutableStateOf(false) }
LaunchedEffect(items) {
if (localItems != items) {
localItems = items
}
}
val view = LocalView.current
val color by animateColorAsState(
targetValue = if (!isAnyDragging) preferenceGroupColor() else MaterialTheme.colorScheme.surface,
label = "card background animation",
)
Column(modifier) {
PreferenceGroupHeading(
label,
)
Surface(
modifier = Modifier.padding(horizontal = 16.dp),
shape = MaterialTheme.shapes.large,
color = color,
) {
ReorderableColumn(
list = localItems,
onSettle = { fromIndex, toIndex ->
val newItems = localItems.toMutableList().apply {
add(toIndex, removeAt(fromIndex))
}.toList()
localItems = newItems
onOrderChange(newItems)
if (onSettle != null) {
onSettle(newItems)
}
isAnyDragging = false
},
onMove = {
isAnyDragging = true
if (Utilities.ATLEAST_U) {
view.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK)
}
},
) { index, item, isDragging ->
key(item.hashCode()) {
ReorderableItem {
Column {
DraggablePreferenceContainer(
isDragging = isDragging,
modifier = Modifier
.a11yDrag(
index = index,
items = items,
onMoveUp = {
localItems = it
onOrderChange(it)
if (onSettle != null) {
onSettle(it)
}
},
onMoveDown = {
localItems = it
onOrderChange(it)
if (onSettle != null) {
onSettle(it)
}
},
),
) {
itemContent(
item,
index,
isDragging,
) { isAnyDragging = it }
}
AnimatedVisibility(!isAnyDragging && index != localItems.lastIndex) {
HorizontalDivider(
Modifier.padding(start = 50.dp, end = 16.dp),
)
}
}
}
}
}
}
ExpandAndShrink(visible = localItems != defaultList) {
PreferenceGroup {
Item {
ClickablePreference(label = stringResource(id = R.string.action_reset)) {
val resetList = defaultList
onOrderChange(resetList)
if (onSettle != null) {
onSettle(resetList)
}
}
}
}
}
}
}
@Composable
fun DraggablePreferenceContainer(
isDragging: Boolean,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp, label = "elevation")
Card(
elevation = CardDefaults.cardElevation(defaultElevation = elevation),
colors = if (isDragging) {
CardDefaults.elevatedCardColors()
} else {
CardDefaults.cardColors(containerColor = Color.Transparent)
},
modifier = modifier,
) {
content()
}
}
@Composable
fun DraggableSwitchPreference(
label: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
dragIndicator: @Composable () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
enabled: Boolean = true,
description: String? = null,
) {
PreferenceTemplate(
modifier = modifier.clickable(
enabled = enabled,
onClick = {
onCheckedChange(!checked)
},
interactionSource = interactionSource,
indication = ripple(),
),
contentModifier = Modifier
.fillMaxHeight()
.padding(vertical = 16.dp)
.padding(start = 16.dp),
title = { Text(text = label) },
description = { description?.let { Text(text = it) } },
startWidget = {
dragIndicator()
},
endWidget = {
Switch(
modifier = Modifier
.padding(all = 16.dp)
.height(24.dp),
checked = checked,
onCheckedChange = onCheckedChange,
enabled = enabled,
thumbContent = {
if (checked) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = null,
modifier = Modifier.size(SwitchDefaults.IconSize),
)
} else {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = null,
modifier = Modifier.size(SwitchDefaults.IconSize),
)
}
},
)
},
enabled = enabled,
applyPaddings = false,
)
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun DragHandle(
scope: ReorderableListItemScope,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
isDraggable: Boolean = true,
onDragStop: () -> Unit = {},
) {
val view = LocalView.current
IconButton(
modifier = with(scope) {
modifier.addIf(isDraggable) {
draggableHandle(
interactionSource = interactionSource,
onDragStarted = {
if (Utilities.ATLEAST_U) {
view.performHapticFeedback(HapticFeedbackConstants.DRAG_START)
}
},
onDragStopped = {
if (Utilities.ATLEAST_R) {
view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END)
}
onDragStop()
},
)
}
},
enabled = isDraggable,
onClick = {},
interactionSource = interactionSource,
shapes = IconButtonDefaults.shapes(),
) {
Icon(
imageVector = Icons.Rounded.DragHandle,
contentDescription = "Drag indicator",
modifier = Modifier.width(24.dp),
)
}
}
fun <T> Modifier.a11yDrag(
index: Int,
items: List<T>,
onMoveUp: (List<T>) -> Unit,
onMoveDown: (List<T>) -> Unit,
) = this.semantics {
customActions = listOf(
CustomAccessibilityAction(
label = "Move up",
action = {
if (index > 0) {
onMoveUp(
items
.toMutableList()
.apply {
add(index - 1, removeAt(index))
},
)
true
} else {
false
}
},
),
CustomAccessibilityAction(
label = "Move down",
action = {
if (index < items.size - 1) {
onMoveDown(
items.toMutableList()
.apply {
add(index + 1, removeAt(index))
},
)
true
} else {
false
}
},
),
)
}
@@ -1,20 +0,0 @@
<?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">
<solid android:color="@color/app_chip_menu_item_color_fg" />
<corners android:radius="@dimen/task_menu_item_corner_radius" />
</shape>
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke
android:width="2dp"
android:color="@android:color/system_accent1_600" />
<solid android:color="@android:color/system_accent1_600" />
</shape>
-20
View File
@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#FFFFFFFF" />
</shape>
@@ -1,27 +0,0 @@
<?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.
-->
<ripple android:color="?android:attr/colorControlHighlight"
xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:shape="rectangle"
android:tint="?colorButtonNormal">
<corners android:radius="@dimen/add_desktop_button_size" />
<solid android:color="@color/materialColorSurfaceBright"/>
</shape>
</item>
</ripple>
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<ripple android:color="?android:attr/colorControlHighlight"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<item>
<shape android:shape="rectangle"
android:tint="?colorButtonNormal">
<corners android:radius="24dp" />
<solid android:color="?attr/materialColorSurfaceBright"/>
</shape>
</item>
</ripple>
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
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_neutral1_50"/>
</vector>
@@ -1,26 +0,0 @@
<!--
~ 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="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z" />
</vector>
@@ -1,30 +0,0 @@
<!--
Copyright (C) 2018 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="92dp"
android:height="80dp"
android:viewportWidth="92.0"
android:viewportHeight="80.0"
android:tint="?android:attr/textColorPrimary">
<path
android:fillColor="#AAFFFFFF"
android:pathData="M18,6H2C0.9,6,0,6.9,0,8v64c0,1.1,0.9,2,2,2h16c1.1,0,2-0.9,2-2V8C20,6.9,19.1,6,18,6z M16,69.25
c0,0.41-0.34,0.75-0.75,0.75H4.75C4.34,70,4,69.66,4,69.25v-58.5C4,10.34,4.34,10,4.75,10h10.5c0.41,0,0.75,0.34,0.75,0.75V69.25
z M90,6H74c-1.1,0-2,0.9-2,2v64c0,1.1,0.9,2,2,2h16c1.1,0,2-0.9,2-2V8C92,6.9,91.1,6,90,6z M88,69.25c0,0.41-0.34,0.75-0.75,0.75
h-10.5C76.34,70,76,69.66,76,69.25v-58.5c0-0.41,0.34-0.75,0.75-0.75h10.5c0.41,0,0.75,0.34,0.75,0.75V69.25z M64,0H28
c-2.21,0-4,1.79-4,4v72c0,2.21,1.79,4,4,4h36c2.21,0,4-1.79,4-4V4C68,1.79,66.21,0,64,0z M64,75c0,0.55-0.45,1-1,1H29
c-0.55,0-1-0.45-1-1V5c0-0.55,0.45-1,1-1h34c0.55,0,1,0.45,1,1V75z"/>
</vector>
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/materialColorSurfaceBright" />
<corners android:radius="@dimen/keyboard_quick_switch_task_view_radius" />
</shape>
@@ -1,88 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_sandbox_feedback"
android:paddingTop="24dp"
android:paddingBottom="24dp"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<TextView
android:id="@+id/gesture_tutorial_dialog_title"
style="@style/TextAppearance.GestureTutorial.Dialog.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/skip_tutorial_dialog_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/gesture_tutorial_dialog_subtitle"
style="@style/TextAppearance.GestureTutorial.Dialog.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/skip_tutorial_dialog_subtitle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_dialog_title"/>
<!-- android:stateListAnimator="@null" removes shadow and normal on click behavior (increase
of elevation and shadow) which is replaced by ripple effect in android:foreground -->
<Button
android:id="@+id/gesture_tutorial_dialog_cancel_button"
style="@style/TextAppearance.GestureTutorial.CancelButtonLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="46dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="@drawable/gesture_tutorial_cancel_button_background"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:stateListAnimator="@null"
android:text="@string/gesture_tutorial_action_button_label_cancel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_dialog_subtitle"/>
<Button
android:id="@+id/gesture_tutorial_dialog_confirm_button"
style="@style/TextAppearance.GestureTutorial.ButtonLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="46dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="@drawable/gesture_tutorial_action_button_background"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:stateListAnimator="@null"
android:text="@string/gesture_tutorial_action_button_label_skip"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_dialog_subtitle"/>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,64 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<com.android.launcher3.taskbar.KeyboardQuickSwitchTaskView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:layout_width="@dimen/keyboard_quick_switch_taskview_width"
android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
android:clipToOutline="true"
android:importantForAccessibility="yes"
launcher:borderColor="?attr/materialColorOutline">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/keyboard_quick_switch_overview_button_background"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<ImageView
android:id="@+id/icon"
android:layout_width="@dimen/keyboard_quick_switch_recents_icon_size"
android:layout_height="@dimen/keyboard_quick_switch_recents_icon_size"
android:layout_marginBottom="8dp"
android:src="@drawable/ic_empty_recents"
android:tint="?attr/materialColorOnSurface"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
style="@style/KeyboardQuickSwitchText"
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="center"
app:layout_constraintTop_toBottomOf="@id/icon"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.android.launcher3.taskbar.KeyboardQuickSwitchTaskView>
@@ -1,72 +0,0 @@
<?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.
-->
<com.android.quickstep.views.TaskThumbnailViewHeader
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/task_thumbnail_view_header"
android:background="@drawable/task_thumbnail_header_bg">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/task_thumbnail_header_height"
android:layout_marginStart="@dimen/task_thumbnail_header_margin_edge"
android:layout_marginEnd="@dimen/task_thumbnail_header_margin_edge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/header_app_icon"
android:contentDescription="@string/header_app_icon_description"
android:layout_width="@dimen/task_thumbnail_header_icon_size"
android:layout_height="@dimen/task_thumbnail_header_icon_size"
android:layout_marginEnd="@dimen/task_thumbnail_header_margin_between_views"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/header_app_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintVertical_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread_inside" />
<TextView
android:id="@+id/header_app_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/task_thumbnail_header_margin_between_views"
android:layout_marginEnd="@dimen/task_thumbnail_header_margin_between_views"
android:text="@string/header_default_app_title"
app:layout_constraintStart_toEndOf="@id/header_app_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintVertical_bias="0.5" />
<ImageButton
android:id="@+id/header_close_button"
android:contentDescription="@string/header_close_icon_description"
android:layout_width="@dimen/task_thumbnail_header_icon_size"
android:layout_height="@dimen/task_thumbnail_header_icon_size"
android:layout_marginStart="@dimen/task_thumbnail_header_margin_between_views"
android:src="@drawable/task_header_close_button"
android:tint="@android:color/darker_gray"
android:background="@null"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintVertical_bias="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.android.quickstep.views.TaskThumbnailViewHeader>
@@ -1,24 +0,0 @@
<!--
~ 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.
-->
<com.android.launcher3.dragndrop.SimpleDragLayer
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drag_layer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:importantForAccessibility="no" />
@@ -1 +0,0 @@
{"v":"5.8.1","fr":60,"ip":0,"op":181,"w":701,"h":841,"nm":"SUW_WelcomeScreen_FoldableOpen_Portrait_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[55]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.949},"o":{"x":0.167,"y":0.167},"t":0,"s":[181.172,148.425,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.038},"t":95,"s":[181.172,-68.377,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[181.172,148.425,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[21.6,21.6,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[100.594,-128.921],[118.85,-93.491],[148.984,-67.406],[148.684,-27.55],[163.244,9.552],[144.457,44.702],[140.106,84.321],[107.136,106.715],[84.872,139.773],[45.271,144.279],[10.194,163.205],[-26.964,148.792],[-66.818,149.249],[-93.023,119.218],[-128.524,101.101],[-137.771,62.332],[-160.786,29.793],[-150.957,-8.833],[-156.215,-48.341],[-129.561,-77.974],[-115.856,-115.401],[-78.484,-129.253],[-48.956,-156.023],[-9.427,-150.921],[29.159,-160.903],[61.789,-138.015]],"o":[[-100.594,128.921],[-118.85,93.491],[-148.984,67.406],[-148.684,27.55],[-163.244,-9.552],[-144.456,-44.702],[-140.106,-84.321],[-107.136,-106.715],[-84.872,-139.773],[-45.271,-144.279],[-10.194,-163.205],[26.964,-148.792],[66.818,-149.249],[93.023,-119.218],[128.524,-101.101],[137.771,-62.332],[160.786,-29.793],[150.957,8.833],[156.215,48.341],[129.561,77.974],[115.856,115.4],[78.484,129.253],[48.956,156.023],[9.427,150.921],[-29.159,160.903],[-61.789,138.015]],"v":[[975.226,761.299],[707.165,899.424],[509.8,1127.42],[208.253,1125.148],[-72.46,1235.308],[-338.41,1093.162],[-638.164,1060.25],[-807.592,810.792],[-1057.715,642.348],[-1091.808,342.727],[-1235.002,77.338],[-1125.948,-203.807],[-1129.407,-505.342],[-902.191,-703.604],[-765.123,-972.207],[-471.796,-1042.166],[-225.603,-1216.305],[66.637,-1141.935],[365.557,-1181.715],[589.761,-980.053],[872.928,-876.361],[977.734,-593.606],[1180.277,-370.197],[1141.675,-71.124],[1217.196,220.821],[1044.029,467.699]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.713725490196,0.556862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9.3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".tertiary","cl":"tertiary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.619]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[67]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.263]},"t":95,"s":[82]},{"t":180,"s":[67]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.927]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[458.803]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.05]},"t":95,"s":[536.803]},{"t":180,"s":[458.803]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[707.143]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.069]},"t":95,"s":[639.643]},{"t":180,"s":[707.143]}],"ix":4}},"a":{"k":[{"s":[164.438,1433.781,0],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[164.438,1433.781,0],"t":180,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"s":{"a":0,"k":[-30,30,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2361.125,4541.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.764705882353,0.423529411765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[350.5,420.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0Line truncated
@@ -1 +0,0 @@
{"v":"5.8.1","fr":60,"ip":0,"op":181,"w":796,"h":412,"nm":"SUW_Welcome_Handheld_Landscape_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-55]},{"t":180,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.942},"o":{"x":0.167,"y":0.167},"t":0,"s":[700.266,85.857,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.043},"t":95,"s":[700.266,-103.727,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[700.266,85.857,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[-18.2,18.2,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[100.594,-128.921],[118.85,-93.491],[148.984,-67.406],[148.684,-27.55],[163.244,9.552],[144.457,44.702],[140.106,84.321],[107.136,106.715],[84.872,139.773],[45.271,144.279],[10.194,163.205],[-26.964,148.792],[-66.818,149.249],[-93.023,119.218],[-128.524,101.101],[-137.771,62.332],[-160.786,29.793],[-150.957,-8.833],[-156.215,-48.341],[-129.561,-77.974],[-115.856,-115.401],[-78.484,-129.253],[-48.956,-156.023],[-9.427,-150.921],[29.159,-160.903],[61.789,-138.015]],"o":[[-100.594,128.921],[-118.85,93.491],[-148.984,67.406],[-148.684,27.55],[-163.244,-9.552],[-144.456,-44.702],[-140.106,-84.321],[-107.136,-106.715],[-84.872,-139.773],[-45.271,-144.279],[-10.194,-163.205],[26.964,-148.792],[66.818,-149.249],[93.023,-119.218],[128.524,-101.101],[137.771,-62.332],[160.786,-29.793],[150.957,8.833],[156.215,48.341],[129.561,77.974],[115.856,115.4],[78.484,129.253],[48.956,156.023],[9.427,150.921],[-29.159,160.903],[-61.789,138.015]],"v":[[975.226,761.299],[707.165,899.424],[509.8,1127.42],[208.253,1125.148],[-72.46,1235.308],[-338.41,1093.162],[-638.164,1060.25],[-807.592,810.792],[-1057.715,642.348],[-1091.808,342.727],[-1235.002,77.338],[-1125.948,-203.807],[-1129.407,-505.342],[-902.191,-703.604],[-765.123,-972.207],[-471.796,-1042.166],[-225.603,-1216.305],[66.637,-1141.935],[365.557,-1181.715],[589.761,-980.053],[872.928,-876.361],[977.734,-593.606],[1180.277,-370.197],[1141.675,-71.124],[1217.196,220.821],[1044.029,467.699]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.713725490196,0.556862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"k":[{"s":[10.989],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[10.989],"t":180,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".tertiary","cl":"tertiary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.047]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-56]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.773],"y":[-0.035]},"t":95,"s":[-67]},{"t":180,"s":[-56]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.073]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[319.023]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.773],"y":[-0.054]},"t":95,"s":[388.573]},{"t":180,"s":[319.023]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.927]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[565.531]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.773],"y":[0.054]},"t":95,"s":[482.331]},{"t":180,"s":[565.531]}],"ix":4}},"a":{"a":0,"k":[164.438,1433.781,0],"ix":1,"l":2},"s":{"a":0,"k":[65,65,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[1193.125,1815.766],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.764705882353,0.423529411765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[398,206,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[2472,5352],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"Line truncated
@@ -1 +0,0 @@
{"v":"5.8.1","fr":60,"ip":0,"op":181,"w":841,"h":701,"nm":"SUW_WelcomeScreen_FoldableOpen_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[55]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.939},"o":{"x":0.167,"y":0.167},"t":0,"s":[140.975,228.318,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.045},"t":95,"s":[140.975,47.65,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[140.975,228.318,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[18,18,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[100.594,-128.921],[118.85,-93.491],[148.984,-67.406],[148.684,-27.55],[163.244,9.552],[144.457,44.702],[140.106,84.321],[107.136,106.715],[84.872,139.773],[45.271,144.279],[10.194,163.205],[-26.964,148.792],[-66.818,149.249],[-93.023,119.218],[-128.524,101.101],[-137.771,62.332],[-160.786,29.793],[-150.957,-8.833],[-156.215,-48.341],[-129.561,-77.974],[-115.856,-115.401],[-78.484,-129.253],[-48.956,-156.023],[-9.427,-150.921],[29.159,-160.903],[61.789,-138.015]],"o":[[-100.594,128.921],[-118.85,93.491],[-148.984,67.406],[-148.684,27.55],[-163.244,-9.552],[-144.456,-44.702],[-140.106,-84.321],[-107.136,-106.715],[-84.872,-139.773],[-45.271,-144.279],[-10.194,-163.205],[26.964,-148.792],[66.818,-149.249],[93.023,-119.218],[128.524,-101.101],[137.771,-62.332],[160.786,-29.793],[150.957,8.833],[156.215,48.341],[129.561,77.974],[115.856,115.4],[78.484,129.253],[48.956,156.023],[9.427,150.921],[-29.159,160.903],[-61.789,138.015]],"v":[[975.226,761.299],[707.165,899.424],[509.8,1127.42],[208.253,1125.148],[-72.46,1235.308],[-338.41,1093.162],[-638.164,1060.25],[-807.592,810.792],[-1057.715,642.348],[-1091.808,342.727],[-1235.002,77.338],[-1125.948,-203.807],[-1129.407,-505.342],[-902.191,-703.604],[-765.123,-972.207],[-471.796,-1042.166],[-225.603,-1216.305],[66.637,-1141.935],[365.557,-1181.715],[589.761,-980.053],[872.928,-876.361],[977.734,-593.606],[1180.277,-370.197],[1141.675,-71.124],[1217.196,220.821],[1044.029,467.699]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.713725490196,0.556862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"k":[{"s":[11.111],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[11.111],"t":180,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".tertiary","cl":"tertiary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.619]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[67]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.263]},"t":95,"s":[82]},{"t":180,"s":[67]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.913]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[639]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.06]},"t":95,"s":[704]},{"t":180,"s":[639]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.12]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[527.25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.083]},"t":95,"s":[471]},{"t":180,"s":[527.25]}],"ix":4}},"a":{"k":[{"s":[164.438,1433.781,0],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[164.438,1433.781,0],"t":180,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"s":{"a":0,"k":[-25,25,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2361.125,4541.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.764705882353,0.423529411765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"k":[{"s":[6],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[6],"t":180,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[420.5,350.5,0],"ix":2,"l":2},"a":{"a":0,"k":[Line truncated
@@ -1 +0,0 @@
{"v":"5.8.1","fr":60,"ip":0,"op":181,"w":701,"h":841,"nm":"SUW_WelcomeScreen_FoldableOpen_Portrait_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[55]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.949},"o":{"x":0.167,"y":0.167},"t":0,"s":[181.172,148.425,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.038},"t":95,"s":[181.172,-68.377,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[181.172,148.425,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[21.6,21.6,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[100.594,-128.921],[118.85,-93.491],[148.984,-67.406],[148.684,-27.55],[163.244,9.552],[144.457,44.702],[140.106,84.321],[107.136,106.715],[84.872,139.773],[45.271,144.279],[10.194,163.205],[-26.964,148.792],[-66.818,149.249],[-93.023,119.218],[-128.524,101.101],[-137.771,62.332],[-160.786,29.793],[-150.957,-8.833],[-156.215,-48.341],[-129.561,-77.974],[-115.856,-115.401],[-78.484,-129.253],[-48.956,-156.023],[-9.427,-150.921],[29.159,-160.903],[61.789,-138.015]],"o":[[-100.594,128.921],[-118.85,93.491],[-148.984,67.406],[-148.684,27.55],[-163.244,-9.552],[-144.456,-44.702],[-140.106,-84.321],[-107.136,-106.715],[-84.872,-139.773],[-45.271,-144.279],[-10.194,-163.205],[26.964,-148.792],[66.818,-149.249],[93.023,-119.218],[128.524,-101.101],[137.771,-62.332],[160.786,-29.793],[150.957,8.833],[156.215,48.341],[129.561,77.974],[115.856,115.4],[78.484,129.253],[48.956,156.023],[9.427,150.921],[-29.159,160.903],[-61.789,138.015]],"v":[[975.226,761.299],[707.165,899.424],[509.8,1127.42],[208.253,1125.148],[-72.46,1235.308],[-338.41,1093.162],[-638.164,1060.25],[-807.592,810.792],[-1057.715,642.348],[-1091.808,342.727],[-1235.002,77.338],[-1125.948,-203.807],[-1129.407,-505.342],[-902.191,-703.604],[-765.123,-972.207],[-471.796,-1042.166],[-225.603,-1216.305],[66.637,-1141.935],[365.557,-1181.715],[589.761,-980.053],[872.928,-876.361],[977.734,-593.606],[1180.277,-370.197],[1141.675,-71.124],[1217.196,220.821],[1044.029,467.699]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.713725490196,0.556862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9.3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".tertiary","cl":"tertiary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.619]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[67]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.263]},"t":95,"s":[82]},{"t":180,"s":[67]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.927]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[458.803]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.05]},"t":95,"s":[536.803]},{"t":180,"s":[458.803]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[707.143]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.069]},"t":95,"s":[639.643]},{"t":180,"s":[707.143]}],"ix":4}},"a":{"k":[{"s":[164.438,1433.781,0],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[164.438,1433.781,0],"t":180,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"s":{"a":0,"k":[-30,30,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2361.125,4541.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.764705882353,0.423529411765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[350.5,420.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0Line truncated
@@ -1 +0,0 @@
{"v":"5.8.1","fr":60,"ip":0,"op":180,"w":1280,"h":800,"nm":"SUW_WelcomeScreen_Tablet_Landscape_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[288,540,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[25,25,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary","cl":"primary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[56]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.986},"o":{"x":0.167,"y":0.167},"t":0,"s":[375.832,-1006.545,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.01},"t":95,"s":[375.832,-1811,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[375.832,-1006.545,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[110,110,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[75.615,-96.908],[89.338,-70.276],[111.99,-50.668],[111.764,-20.709],[122.709,7.18],[108.586,33.602],[105.316,63.383],[80.533,80.216],[63.797,105.066],[34.03,108.453],[7.663,122.679],[-20.269,111.845],[-50.226,112.189],[-69.924,89.614],[-96.61,75.997],[-103.56,46.854],[-120.861,22.395],[-113.472,-6.639],[-117.425,-36.337],[-97.389,-58.612],[-87.087,-86.745],[-58.996,-97.158],[-36.8,-117.281],[-7.086,-113.445],[21.918,-120.948],[46.446,-103.744]],"o":[[-75.615,96.909],[-89.338,70.276],[-111.99,50.668],[-111.764,20.709],[-122.709,-7.18],[-108.586,-33.602],[-105.316,-63.383],[-80.533,-80.216],[-63.797,-105.066],[-34.03,-108.453],[-7.663,-122.679],[20.269,-111.845],[50.226,-112.188],[69.924,-89.614],[96.61,-75.997],[103.56,-46.854],[120.861,-22.395],[113.472,6.64],[117.425,36.337],[97.389,58.612],[87.088,86.745],[58.995,97.158],[36.8,117.281],[7.087,113.445],[-21.918,120.948],[-46.446,103.744]],"v":[[733.209,572.105],[531.711,675.932],[383.354,847.313],[156.685,845.606],[-54.323,928.412],[-254.235,821.562],[-479.555,796.823],[-606.913,609.309],[-794.927,482.691],[-820.554,257.47],[-928.191,57.981],[-846.217,-153.353],[-848.817,-380.013],[-678.021,-529.044],[-574.99,-730.949],[-354.499,-783.537],[-169.439,-914.435],[50.234,-858.532],[274.928,-888.434],[443.46,-736.847],[656.313,-658.903],[735.094,-446.359],[887.344,-278.426],[858.327,-53.616],[915.095,165.835],[784.928,351.409]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.956862745098,0.729411764706,0.619607843137,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":720,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".tertiary","cl":"tertiary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.248]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[57]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.172]},"t":95,"s":[75]},{"t":180,"s":[57]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.032]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[2618]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.022]},"t":95,"s":[2442]},{"t":180,"s":[2618]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.034]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[891]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.024]},"t":95,"s":[694]},{"t":180,"s":[891]}],"ix":4}},"a":{"a":0,"k":[164.438,1433.781,0],"ix":1,"l":2},"s":{"a":0,"k":[120,120,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3079.125,4685.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.752941176471,0.788235294118,0.752941176471,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
@@ -1 +0,0 @@
{"v":"5.8.1","fr":60,"ip":0,"op":180,"w":800,"h":1280,"nm":"SUW_WelcomeScreen_Tablet_Portrait_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[288,528,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[25,25,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary","cl":"primary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[56]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.986},"o":{"x":0.167,"y":0.167},"t":0,"s":[999.832,-2238.545,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.01},"t":95,"s":[999.832,-3043,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[999.832,-2238.545,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[200,200,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[75.615,-96.908],[89.338,-70.276],[111.99,-50.668],[111.764,-20.709],[122.709,7.18],[108.586,33.602],[105.316,63.383],[80.533,80.216],[63.797,105.066],[34.03,108.453],[7.663,122.679],[-20.269,111.845],[-50.226,112.189],[-69.924,89.614],[-96.61,75.997],[-103.56,46.854],[-120.861,22.395],[-113.472,-6.639],[-117.425,-36.337],[-97.389,-58.612],[-87.087,-86.745],[-58.996,-97.158],[-36.8,-117.281],[-7.086,-113.445],[21.918,-120.948],[46.446,-103.744]],"o":[[-75.615,96.909],[-89.338,70.276],[-111.99,50.668],[-111.764,20.709],[-122.709,-7.18],[-108.586,-33.602],[-105.316,-63.383],[-80.533,-80.216],[-63.797,-105.066],[-34.03,-108.453],[-7.663,-122.679],[20.269,-111.845],[50.226,-112.188],[69.924,-89.614],[96.61,-75.997],[103.56,-46.854],[120.861,-22.395],[113.472,6.64],[117.425,36.337],[97.389,58.612],[87.088,86.745],[58.995,97.158],[36.8,117.281],[7.087,113.445],[-21.918,120.948],[-46.446,103.744]],"v":[[733.209,572.105],[531.711,675.932],[383.354,847.313],[156.685,845.606],[-54.323,928.412],[-254.235,821.562],[-479.555,796.823],[-606.913,609.309],[-794.927,482.691],[-820.554,257.47],[-928.191,57.981],[-846.217,-153.353],[-848.817,-380.013],[-678.021,-529.044],[-574.99,-730.949],[-354.499,-783.537],[-169.439,-914.435],[50.234,-858.532],[274.928,-888.434],[443.46,-736.847],[656.313,-658.903],[735.094,-446.359],[887.344,-278.426],[858.327,-53.616],[915.095,165.835],[784.928,351.409]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.956862745098,0.729411764706,0.619607843137,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":720,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".tertiary","cl":"tertiary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.248]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-39]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.172]},"t":95,"s":[-21]},{"t":180,"s":[-39]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.032]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[1490]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.022]},"t":95,"s":[1314]},{"t":180,"s":[1490]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.034]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[2967]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.024]},"t":95,"s":[2770]},{"t":180,"s":[2967]}],"ix":4}},"a":{"a":0,"k":[164.438,1433.781,0],"ix":1,"l":2},"s":{"a":0,"k":[168,168,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3079.125,4685.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.752941176471,0.788235294118,0.752941176471,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
@@ -1,377 +0,0 @@
/*
* 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.launcher3;
import static android.content.ClipDescription.MIMETYPE_TEXT_INTENT;
import static android.view.WindowInsets.Type.navigationBars;
import static android.view.WindowInsets.Type.statusBars;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.dragndrop.SimpleDragLayer;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.WidgetPredictionsRequester;
import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.widget.BaseWidgetSheet;
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/** An Activity that can host Launcher's widget picker. */
public class WidgetPickerActivity extends BaseActivity {
private static final String TAG = "WidgetPickerActivity";
/**
* Name of the extra that indicates that a widget being dragged.
*
* <p>When set to "true" in the result of startActivityForResult, the client that launched the
* picker knows that activity was closed due to pending drag.
*/
private static final String EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag";
// Intent extras that specify the desired widget width and height. If these are not specified in
// the intent, then widgets will not be filtered for size.
private static final String EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width";
private static final String EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height";
/**
* Widgets currently added by the user in the UI surface.
* <p>This allows widget picker to exclude existing widgets from suggestions.</p>
*/
private static final String EXTRA_ADDED_APP_WIDGETS = "added_app_widgets";
/**
* A unique identifier of the surface hosting the widgets;
* <p>"widgets" is reserved for home screen surface.</p>
* <p>"widgets_hub" is reserved for glanceable hub surface.</p>
*/
private static final String EXTRA_UI_SURFACE = "ui_surface";
private static final Pattern UI_SURFACE_PATTERN =
Pattern.compile("^(widgets|widgets_hub)$");
private SimpleDragLayer<WidgetPickerActivity> mDragLayer;
private WidgetsModel mModel;
private LauncherAppState mApp;
private WidgetPredictionsRequester mWidgetPredictionsRequester;
private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
private int mDesiredWidgetWidth;
private int mDesiredWidgetHeight;
private int mWidgetCategoryFilter;
@Nullable
private String mUiSurface;
// Widgets existing on the host surface.
@NonNull
private List<AppWidgetProviderInfo> mAddedWidgets = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER);
mApp = LauncherAppState.getInstance(this);
InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
mDeviceProfile = idp.getDeviceProfile(this);
mModel = new WidgetsModel();
setContentView(R.layout.widget_picker_activity);
mDragLayer = findViewById(R.id.drag_layer);
mDragLayer.recreateControllers();
WindowInsetsController wc = mDragLayer.getWindowInsetsController();
wc.hide(navigationBars() + statusBars());
BaseWidgetSheet widgetSheet = WidgetsFullSheet.show(this, true);
widgetSheet.disableNavBarScrim(true);
widgetSheet.addOnCloseListener(this::finish);
parseIntentExtras();
refreshAndBindWidgets();
}
private void parseIntentExtras() {
// A value of 0 for either size means that no filtering will occur in that dimension. If
// both values are 0, then no size filtering will occur.
mDesiredWidgetWidth =
getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_WIDTH, 0);
mDesiredWidgetHeight =
getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_HEIGHT, 0);
// Defaults to '0' to indicate that there isn't a category filter.
mWidgetCategoryFilter =
getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0);
String uiSurfaceParam = getIntent().getStringExtra(EXTRA_UI_SURFACE);
if (uiSurfaceParam != null && UI_SURFACE_PATTERN.matcher(uiSurfaceParam).matches()) {
mUiSurface = uiSurfaceParam;
}
if (Utilities.ATLEAST_T) {
ArrayList<AppWidgetProviderInfo> addedWidgets = getIntent().getParcelableArrayListExtra(
EXTRA_ADDED_APP_WIDGETS, AppWidgetProviderInfo.class);
if (addedWidgets != null) {
mAddedWidgets = addedWidgets;
}
}
}
@NonNull
@Override
public PopupDataProvider getPopupDataProvider() {
return mPopupDataProvider;
}
@Override
public SimpleDragLayer<WidgetPickerActivity> getDragLayer() {
return mDragLayer;
}
@Override
public View.OnClickListener getItemOnClickListener() {
return v -> {
final AppWidgetProviderInfo info =
(v instanceof WidgetCell) ? ((WidgetCell) v).getWidgetItem().widgetInfo : null;
if (info == null || info.provider == null) {
return;
}
setResult(RESULT_OK, new Intent()
.putExtra(Intent.EXTRA_COMPONENT_NAME, info.provider)
.putExtra(Intent.EXTRA_USER, info.getProfile()));
finish();
};
}
@Override
public View.OnLongClickListener getAllAppsItemLongClickListener() {
return view -> {
if (!(view instanceof WidgetCell widgetCell)) return false;
if (widgetCell.getWidgetView().getDrawable() == null
&& widgetCell.getAppWidgetHostViewPreview() == null) {
// The widget preview hasn't been loaded; so, we abort the drag.
return false;
}
final AppWidgetProviderInfo info = widgetCell.getWidgetItem().widgetInfo;
if (info == null || info.provider == null) {
return false;
}
View dragView = widgetCell.getDragAndDropView();
if (dragView == null) {
return false;
}
ClipData clipData = new ClipData(
new ClipDescription(
/* label= */ "", // not displayed anywhere; so, set to empty.
new String[]{MIMETYPE_TEXT_INTENT}
),
new ClipData.Item(new Intent()
.putExtra(Intent.EXTRA_USER, info.getProfile())
.putExtra(Intent.EXTRA_COMPONENT_NAME, info.provider))
);
// Set result indicating activity was closed due a widget being dragged.
setResult(RESULT_OK, new Intent()
.putExtra(EXTRA_IS_PENDING_WIDGET_DRAG, true));
// DRAG_FLAG_GLOBAL permits dragging data beyond app window.
return dragView.startDragAndDrop(
clipData,
new View.DragShadowBuilder(dragView),
/* myLocalState= */ null,
View.DRAG_FLAG_GLOBAL
);
};
}
/** Updates the model with widgets and provides them after applying the provided filter. */
private void refreshAndBindWidgets() {
MODEL_EXECUTOR.execute(() -> {
LauncherAppState app = LauncherAppState.getInstance(this);
mModel.update(app, null);
final List<WidgetsListBaseEntry> allWidgets =
mModel.getFilteredWidgetsListForPicker(
app.getContext(),
/*widgetItemFilter=*/ widget -> {
final WidgetAcceptabilityVerdict verdict =
isWidgetAcceptable(widget);
verdict.maybeLogVerdict();
return verdict.isAcceptable;
}
);
bindWidgets(allWidgets);
if (mUiSurface != null) {
Map<PackageUserKey, List<WidgetItem>> allWidgetsMap = allWidgets.stream()
.filter(WidgetsListHeaderEntry.class::isInstance)
.collect(Collectors.toMap(
entry -> PackageUserKey.fromPackageItemInfo(entry.mPkgItem),
entry -> entry.mWidgets)
);
mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(),
mUiSurface, allWidgetsMap);
mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets);
}
});
}
private void bindWidgets(List<WidgetsListBaseEntry> widgets) {
MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets));
}
private void bindRecommendedWidgets(List<ItemInfo> recommendedWidgets) {
MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setRecommendedWidgets(recommendedWidgets));
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mWidgetPredictionsRequester != null) {
mWidgetPredictionsRequester.clear();
}
}
private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget) {
final AppWidgetProviderInfo info = widget.widgetInfo;
if (info == null) {
return rejectWidget(widget, "shortcut");
}
if (mWidgetCategoryFilter > 0 && (info.widgetCategory & mWidgetCategoryFilter) == 0) {
return rejectWidget(
widget,
"doesn't match category filter [filter=%d, widget=%d]",
mWidgetCategoryFilter,
info.widgetCategory);
}
if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) {
// Accept the widget if the desired dimensions are unspecified.
return acceptWidget(widget);
}
final boolean isHorizontallyResizable =
(info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) {
if (info.maxResizeWidth > 0
&& info.maxResizeWidth >= info.minWidth
&& info.maxResizeWidth < mDesiredWidgetWidth) {
return rejectWidget(
widget,
"maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
info.maxResizeWidth,
mDesiredWidgetWidth);
}
final int minWidth = Math.min(info.minResizeWidth, info.minWidth);
if (minWidth > mDesiredWidgetWidth) {
return rejectWidget(
widget,
"min(minWidth[%d], minResizeWidth[%d]) > mDesiredWidgetWidth[%d]",
info.minWidth,
info.minResizeWidth,
mDesiredWidgetWidth);
}
}
final boolean isVerticallyResizable =
(info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
if (mDesiredWidgetHeight > 0 && isVerticallyResizable) {
if (info.maxResizeHeight > 0
&& info.maxResizeHeight >= info.minHeight
&& info.maxResizeHeight < mDesiredWidgetHeight) {
return rejectWidget(
widget,
"maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
info.maxResizeHeight,
mDesiredWidgetHeight);
}
final int minHeight = Math.min(info.minResizeHeight, info.minHeight);
if (minHeight > mDesiredWidgetHeight) {
return rejectWidget(
widget,
"min(minHeight[%d], minResizeHeight[%d]) > mDesiredWidgetHeight[%d]",
info.minHeight,
info.minResizeHeight,
mDesiredWidgetHeight);
}
}
if (!isHorizontallyResizable || !isVerticallyResizable) {
return rejectWidget(widget, "not resizeable");
}
return acceptWidget(widget);
}
private static WidgetAcceptabilityVerdict rejectWidget(
WidgetItem widget, String rejectionReason, Object... args) {
return new WidgetAcceptabilityVerdict(
false,
widget.widgetInfo != null
? widget.widgetInfo.provider.flattenToShortString()
: widget.label,
String.format(Locale.ENGLISH, rejectionReason, args));
}
private static WidgetAcceptabilityVerdict acceptWidget(WidgetItem widget) {
return new WidgetAcceptabilityVerdict(
true, widget.widgetInfo.provider.flattenToShortString(), "");
}
private record WidgetAcceptabilityVerdict(
boolean isAcceptable, String widgetLabel, String reason) {
void maybeLogVerdict() {
// Only log a verdict if a reason is specified.
if (Log.isLoggable(TAG, Log.DEBUG) && !reason.isEmpty()) {
Log.i(TAG, String.format(
Locale.ENGLISH,
"%s: %s because %s",
widgetLabel,
isAcceptable ? "accepted" : "rejected",
reason));
}
}
}
}
@@ -1,65 +0,0 @@
/*
* 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.launcher3.hybridhotseat;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
import static com.android.launcher3.model.PredictionHelper.getAppTargetFromItemInfo;
import static com.android.launcher3.model.PredictionHelper.isTrackedForHotseatPrediction;
import static com.android.launcher3.model.PredictionHelper.wrapAppTargetWithItemLocation;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.content.Context;
import android.os.Bundle;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.data.ItemInfo;
import java.util.ArrayList;
/**
* Model helper for app predictions in workspace
*/
public class HotseatPredictionModel {
private static final String BUNDLE_KEY_PIN_EVENTS = "pin_events";
private static final String BUNDLE_KEY_CURRENT_ITEMS = "current_items";
/**
* Creates and returns bundle using workspace items
*/
public static Bundle convertDataModelToAppTargetBundle(Context context, BgDataModel dataModel) {
Bundle bundle = new Bundle();
ArrayList<AppTargetEvent> events = new ArrayList<>();
ArrayList<ItemInfo> workspaceItems = dataModel.getAllWorkspaceItems();
for (ItemInfo item : workspaceItems) {
AppTarget target = getAppTargetFromItemInfo(context, item);
if (target != null && !isTrackedForHotseatPrediction(item)) continue;
events.add(wrapAppTargetWithItemLocation(target, AppTargetEvent.ACTION_PIN, item));
}
ArrayList<AppTarget> currentTargets = new ArrayList<>();
FixedContainerItems hotseatItems = dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION);
if (hotseatItems != null) {
for (ItemInfo itemInfo : hotseatItems.items) {
AppTarget target = getAppTargetFromItemInfo(context, itemInfo);
if (target != null) currentTargets.add(target);
}
}
bundle.putParcelableArrayList(BUNDLE_KEY_PIN_EVENTS, events);
bundle.putParcelableArrayList(BUNDLE_KEY_CURRENT_ITEMS, currentTargets);
return bundle;
}
}
@@ -1,133 +0,0 @@
/*
* 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.launcher3.model;
import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.WORKSPACE;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
import android.content.ComponentName;
import android.content.Context;
import androidx.annotation.Nullable;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.Workspace;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.shortcuts.ShortcutKey;
import java.util.Locale;
/** Helper class with methods for converting launcher items to form usable by predictors */
public final class PredictionHelper {
private static final String APP_LOCATION_HOTSEAT = "hotseat";
private static final String APP_LOCATION_WORKSPACE = "workspace";
/**
* Creates and returns an {@link AppTarget} object for an {@link ItemInfo}. Returns null
* if item type is not supported in predictions
*/
@Nullable
public static AppTarget getAppTargetFromItemInfo(Context context, ItemInfo info) {
if (info == null) return null;
if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
&& info instanceof LauncherAppWidgetInfo
&& ((LauncherAppWidgetInfo) info).providerName != null) {
ComponentName cn = ((LauncherAppWidgetInfo) info).providerName;
return new AppTarget.Builder(new AppTargetId("widget:" + cn.getPackageName()),
cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
} else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
&& info.getTargetComponent() != null) {
ComponentName cn = info.getTargetComponent();
return new AppTarget.Builder(new AppTargetId("app:" + cn.getPackageName()),
cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
} else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
&& info instanceof WorkspaceItemInfo) {
ShortcutKey shortcutKey = ShortcutKey.fromItemInfo(info);
//TODO: switch to using full shortcut info
return new AppTarget.Builder(new AppTargetId("shortcut:" + shortcutKey.getId()),
shortcutKey.componentName.getPackageName(), shortcutKey.user).build();
} else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
return new AppTarget.Builder(new AppTargetId("folder:" + info.id),
context.getPackageName(), info.user).build();
} else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR) {
return new AppTarget.Builder(new AppTargetId("app_pair:" + info.id),
context.getPackageName(), info.user).build();
}
return null;
}
/**
* Creates and returns {@link AppTargetEvent} from an {@link AppTarget}, action, and item
* location using {@link ItemInfo}
*/
public static AppTargetEvent wrapAppTargetWithItemLocation(
AppTarget target, int action, ItemInfo info) {
String location = String.format(Locale.ENGLISH, "%s/%d/[%d,%d]/[%d,%d]",
info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
? APP_LOCATION_HOTSEAT : APP_LOCATION_WORKSPACE,
info.screenId, info.cellX, info.cellY, info.spanX, info.spanY);
return new AppTargetEvent.Builder(target, action).setLaunchLocation(location).build();
}
/**
* Helper method to determine if {@link ItemInfo} should be tracked and reported to hotseat
* predictors
*/
public static boolean isTrackedForHotseatPrediction(ItemInfo info) {
return info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT || (
info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP
&& info.screenId == Workspace.FIRST_SCREEN_ID);
}
/**
* Helper method to determine if {@link LauncherAtom.ItemInfo} should be tracked and reported to
* hotseat predictors
*/
public static boolean isTrackedForHotseatPrediction(LauncherAtom.ItemInfo info) {
LauncherAtom.ContainerInfo ci = info.getContainerInfo();
switch (ci.getContainerCase()) {
case HOTSEAT:
return true;
case WORKSPACE:
return ci.getWorkspace().getPageIndex() == 0;
default:
return false;
}
}
/**
* Helper method to determine if {@link ItemInfo} should be tracked and reported to widget
* predictors
*/
public static boolean isTrackedForWidgetPrediction(ItemInfo info) {
return info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
&& info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP;
}
/**
* Helper method to determine if {@link LauncherAtom.ItemInfo} should be tracked and reported
* to widget predictors
*/
public static boolean isTrackedForWidgetPrediction(LauncherAtom.ItemInfo info) {
return info.getItemCase() == LauncherAtom.ItemInfo.ItemCase.WIDGET
&& info.getContainerInfo().getContainerCase() == WORKSPACE;
}
}
@@ -1,55 +0,0 @@
/*
* 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.launcher3.secondarydisplay;
import static com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT;
import android.content.Context;
import com.android.launcher3.appprediction.AppsDividerView;
import com.android.launcher3.appprediction.PredictionRowView;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.views.ActivityContext;
/**
* Implementation of SecondaryDisplayPredictions.
*/
@SuppressWarnings("unused")
public final class SecondaryDisplayPredictionsImpl extends SecondaryDisplayPredictions {
private final ActivityContext mActivityContext;
private final Context mContext;
public SecondaryDisplayPredictionsImpl(Context context) {
mContext = context;
mActivityContext = ActivityContext.lookupContext(context);
}
@Override
void updateAppDivider() {
mActivityContext.getAppsView().getFloatingHeaderView()
.findFixedRowByType(AppsDividerView.class)
.setShowAllAppsLabel(!ALL_APPS_VISITED_COUNT.hasReachedMax(mContext));
ALL_APPS_VISITED_COUNT.increment(mContext);
}
@Override
public void setPredictedApps(BgDataModel.FixedContainerItems item) {
mActivityContext.getAppsView().getFloatingHeaderView()
.findFixedRowByType(PredictionRowView.class)
.setPredictedApps(item.items);
}
}
@@ -1,358 +0,0 @@
/*
* 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.launcher3.statehandlers;
import static android.view.View.VISIBLE;
import static com.android.launcher3.LauncherState.BACKGROUND_APP;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.window.flags2.Flags.enableDesktopWindowingWallpaperActivity;
import android.os.Debug;
import android.util.Log;
import android.view.View;
import androidx.annotation.Nullable;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.DisplayController;
import com.android.quickstep.GestureState;
import com.android.quickstep.SystemUiProxy;
import com.android.wm.shell.desktopmode.IDesktopTaskListener;
import java.util.HashSet;
import java.util.Set;
/**
* Controls the visibility of the workspace and the resumed / paused state when desktop mode
* is enabled.
*/
public class DesktopVisibilityController {
private static final String TAG = "DesktopVisController";
private static final boolean DEBUG = false;
private final Launcher mLauncher;
private final Set<DesktopVisibilityListener> mDesktopVisibilityListeners = new HashSet<>();
private int mVisibleDesktopTasksCount;
private boolean mInOverviewState;
private boolean mBackgroundStateEnabled;
private boolean mGestureInProgress;
@Nullable
private IDesktopTaskListener mDesktopTaskListener;
public DesktopVisibilityController(Launcher launcher) {
mLauncher = launcher;
}
/**
* Register a listener with System UI to receive updates about desktop tasks state
*/
public void registerSystemUiListener() {
mDesktopTaskListener = new IDesktopTaskListener.Stub() {
@Override
public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) {
MAIN_EXECUTOR.execute(() -> {
if (displayId == mLauncher.getDisplayId()) {
if (DEBUG) {
Log.d(TAG, "desktop visible tasks count changed=" + visibleTasksCount);
}
setVisibleDesktopTasksCount(visibleTasksCount);
}
});
}
@Override
public void onStashedChanged(int displayId, boolean stashed) {
Log.w(TAG, "IDesktopTaskListener: onStashedChanged is deprecated");
}
};
SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(mDesktopTaskListener);
}
/**
* Clear listener from System UI that was set with {@link #registerSystemUiListener()}
*/
public void unregisterSystemUiListener() {
SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(null);
mDesktopTaskListener = null;
}
/**
* Whether desktop tasks are visible in desktop mode.
*/
public boolean areDesktopTasksVisible() {
boolean desktopTasksVisible = mVisibleDesktopTasksCount > 0;
if (DEBUG) {
Log.d(TAG, "areDesktopTasksVisible: desktopVisible=" + desktopTasksVisible
+ " overview=" + mInOverviewState);
}
return desktopTasksVisible && !mInOverviewState;
}
/**
* Number of visible desktop windows in desktop mode.
*/
public int getVisibleDesktopTasksCount() {
return mVisibleDesktopTasksCount;
}
/** Registers a listener for Desktop Mode visibility updates. */
public void registerDesktopVisibilityListener(DesktopVisibilityListener listener) {
mDesktopVisibilityListeners.add(listener);
}
/** Removes a previously registered Desktop Mode visibility listener. */
public void unregisterDesktopVisibilityListener(DesktopVisibilityListener listener) {
mDesktopVisibilityListeners.remove(listener);
}
/**
* Sets the number of desktop windows that are visible and updates launcher visibility based on
* it.
*/
public void setVisibleDesktopTasksCount(int visibleTasksCount) {
if (DEBUG) {
Log.d(TAG, "setVisibleDesktopTasksCount: visibleTasksCount=" + visibleTasksCount
+ " currentValue=" + mVisibleDesktopTasksCount);
}
if (visibleTasksCount != mVisibleDesktopTasksCount) {
final boolean wasVisible = mVisibleDesktopTasksCount > 0;
final boolean isVisible = visibleTasksCount > 0;
final boolean wereDesktopTasksVisibleBefore = areDesktopTasksVisible();
mVisibleDesktopTasksCount = visibleTasksCount;
final boolean areDesktopTasksVisibleNow = areDesktopTasksVisible();
if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
}
if (!enableDesktopWindowingWallpaperActivity() && wasVisible != isVisible) {
// TODO: b/333533253 - Remove after flag rollout
if (mVisibleDesktopTasksCount > 0) {
setLauncherViewsVisibility(View.INVISIBLE);
if (!mInOverviewState) {
// When desktop tasks are visible & we're not in overview, we want launcher
// to appear paused, this ensures that taskbar displays.
markLauncherPaused();
}
} else {
setLauncherViewsVisibility(View.VISIBLE);
// If desktop tasks aren't visible, ensure that launcher appears resumed to
// behave normally.
markLauncherResumed();
}
}
}
}
/**
* Process launcher state change and update launcher view visibility based on desktop state
*/
public void onLauncherStateChanged(LauncherState state) {
if (DEBUG) {
Log.d(TAG, "onLauncherStateChanged: newState=" + state);
}
setBackgroundStateEnabled(state == BACKGROUND_APP);
// Desktop visibility tracks overview and background state separately
setOverviewStateEnabled(state != BACKGROUND_APP && state.isRecentsViewVisible);
}
private void setOverviewStateEnabled(boolean overviewStateEnabled) {
if (DEBUG) {
Log.d(TAG, "setOverviewStateEnabled: enabled=" + overviewStateEnabled
+ " currentValue=" + mInOverviewState);
}
if (overviewStateEnabled != mInOverviewState) {
final boolean wereDesktopTasksVisibleBefore = areDesktopTasksVisible();
mInOverviewState = overviewStateEnabled;
final boolean areDesktopTasksVisibleNow = areDesktopTasksVisible();
if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
}
if (enableDesktopWindowingWallpaperActivity()) {
return;
}
// TODO: b/333533253 - Clean up after flag rollout
if (mInOverviewState) {
setLauncherViewsVisibility(View.VISIBLE);
markLauncherResumed();
} else if (areDesktopTasksVisibleNow && !mGestureInProgress) {
// Switching out of overview state and gesture finished.
// If desktop tasks are still visible, hide launcher again.
setLauncherViewsVisibility(View.INVISIBLE);
markLauncherPaused();
}
}
}
private void notifyDesktopVisibilityListeners(boolean areDesktopTasksVisible) {
if (DEBUG) {
Log.d(TAG, "notifyDesktopVisibilityListeners: visible=" + areDesktopTasksVisible);
}
for (DesktopVisibilityListener listener : mDesktopVisibilityListeners) {
listener.onDesktopVisibilityChanged(areDesktopTasksVisible);
}
DisplayController.handleInfoChangeForDesktopMode(mLauncher);
}
/**
* TODO: b/333533253 - Remove after flag rollout
*/
private void setBackgroundStateEnabled(boolean backgroundStateEnabled) {
if (DEBUG) {
Log.d(TAG, "setBackgroundStateEnabled: enabled=" + backgroundStateEnabled
+ " currentValue=" + mBackgroundStateEnabled);
}
if (backgroundStateEnabled != mBackgroundStateEnabled) {
mBackgroundStateEnabled = backgroundStateEnabled;
if (mBackgroundStateEnabled) {
setLauncherViewsVisibility(View.VISIBLE);
markLauncherResumed();
} else if (areDesktopTasksVisible() && !mGestureInProgress) {
// Switching out of background state. If desktop tasks are visible, pause launcher.
setLauncherViewsVisibility(View.INVISIBLE);
markLauncherPaused();
}
}
}
/**
* Whether recents gesture is currently in progress.
*
* TODO: b/333533253 - Remove after flag rollout
*/
public boolean isRecentsGestureInProgress() {
return mGestureInProgress;
}
/**
* Notify controller that recents gesture has started.
*
* TODO: b/333533253 - Remove after flag rollout
*/
public void setRecentsGestureStart() {
if (DEBUG) {
Log.d(TAG, "setRecentsGestureStart");
}
setRecentsGestureInProgress(true);
}
/**
* Notify controller that recents gesture finished with the given
* {@link com.android.quickstep.GestureState.GestureEndTarget}
*
* TODO: b/333533253 - Remove after flag rollout
*/
public void setRecentsGestureEnd(@Nullable GestureState.GestureEndTarget endTarget) {
if (DEBUG) {
Log.d(TAG, "setRecentsGestureEnd: endTarget=" + endTarget);
}
setRecentsGestureInProgress(false);
if (endTarget == null) {
// Gesture did not result in a new end target. Ensure launchers gets paused again.
markLauncherPaused();
}
}
/**
* TODO: b/333533253 - Remove after flag rollout
*/
private void setRecentsGestureInProgress(boolean gestureInProgress) {
if (gestureInProgress != mGestureInProgress) {
mGestureInProgress = gestureInProgress;
}
}
/**
* TODO: b/333533253 - Remove after flag rollout
*/
private void setLauncherViewsVisibility(int visibility) {
if (enableDesktopWindowingWallpaperActivity()) {
return;
}
if (DEBUG) {
Log.d(TAG, "setLauncherViewsVisibility: visibility=" + visibility + " "
+ Debug.getCaller());
}
View workspaceView = mLauncher.getWorkspace();
if (workspaceView != null) {
workspaceView.setVisibility(visibility);
}
View dragLayer = mLauncher.getDragLayer();
if (dragLayer != null) {
dragLayer.setVisibility(visibility);
}
if (mLauncher instanceof QuickstepLauncher ql && ql.getTaskbarUIController() != null
&& mVisibleDesktopTasksCount != 0) {
ql.getTaskbarUIController().onLauncherVisibilityChanged(visibility == VISIBLE);
}
}
/**
* TODO: b/333533253 - Remove after flag rollout
*/
private void markLauncherPaused() {
if (enableDesktopWindowingWallpaperActivity()) {
return;
}
if (DEBUG) {
Log.d(TAG, "markLauncherPaused " + Debug.getCaller());
}
StatefulActivity<LauncherState> activity =
QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
if (activity != null) {
activity.setPaused();
}
}
/**
* TODO: b/333533253 - Remove after flag rollout
*/
private void markLauncherResumed() {
if (enableDesktopWindowingWallpaperActivity()) {
return;
}
if (DEBUG) {
Log.d(TAG, "markLauncherResumed " + Debug.getCaller());
}
StatefulActivity<LauncherState> activity =
QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
// Check activity state before calling setResumed(). Launcher may have been actually
// paused (eg fullscreen task moved to front).
// In this case we should not mark the activity as resumed.
if (activity != null && activity.isResumed()) {
activity.setResumed();
}
}
/** A listener for when the user enters/exits Desktop Mode. */
public interface DesktopVisibilityListener {
/**
* Callback for when the user enters or exits Desktop Mode
*
* @param visible whether Desktop Mode is now visible
*/
void onDesktopVisibilityChanged(boolean visible);
}
}
@@ -1,686 +0,0 @@
/*
* 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.launcher3.taskbar;
import static android.content.Context.RECEIVER_NOT_EXPORTED;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
import static com.android.launcher3.BaseActivity.EVENT_DESTROYED;
import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate;
import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
import static com.android.launcher3.util.DisplayController.CHANGE_DESKTOP_MODE;
import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
import static com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING;
import static com.android.launcher3.util.DisplayController.TASKBAR_NOT_DESTROYED_TAG;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
import static com.android.quickstep.util.SystemActionConstants.ACTION_SHOW_TASKBAR;
import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.ComponentCallbacks;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.hardware.display.DisplayManager;
import android.net.Uri;
import android.os.Handler;
import android.os.Trace;
import android.provider.Settings;
import android.util.Log;
import android.view.Display;
import android.view.MotionEvent;
import android.view.WindowManager;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks;
import com.android.launcher3.taskbar.unfold.NonDestroyableScopedUnfoldTransitionProgressProvider;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.SettingsCache;
import com.android.launcher3.util.SimpleBroadcastReceiver;
import com.android.quickstep.AllAppsActionManager;
import com.android.quickstep.RecentsActivity;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.AssistUtils;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
import java.io.PrintWriter;
import java.util.StringJoiner;
import app.lawnchair.LawnchairApp;
/**
* Class to manage taskbar lifecycle
*/
public class TaskbarManager {
private static final String TAG = "TaskbarManager";
private static final boolean DEBUG = false;
/**
* All the configurations which do not initiate taskbar recreation.
* This includes all the configurations defined in Launcher's manifest entry and
* ActivityController#filterConfigChanges
*/
private static final int SKIP_RECREATE_CONFIG_CHANGES = ActivityInfo.CONFIG_WINDOW_CONFIGURATION
| ActivityInfo.CONFIG_KEYBOARD
| ActivityInfo.CONFIG_KEYBOARD_HIDDEN
| ActivityInfo.CONFIG_MCC
| ActivityInfo.CONFIG_MNC
| ActivityInfo.CONFIG_NAVIGATION
| ActivityInfo.CONFIG_ORIENTATION
| ActivityInfo.CONFIG_SCREEN_SIZE
| ActivityInfo.CONFIG_SCREEN_LAYOUT
| ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
private final Context mContext;
private final @Nullable Context mNavigationBarPanelContext;
private WindowManager mWindowManager;
private FrameLayout mTaskbarRootLayout;
private boolean mAddedWindow;
private final TaskbarNavButtonController mNavButtonController;
private final ComponentCallbacks mComponentCallbacks;
private final SimpleBroadcastReceiver mShutdownReceiver =
new SimpleBroadcastReceiver(i -> destroyExistingTaskbar());
// The source for this provider is set when Launcher is available
// We use 'non-destroyable' version here so the original provider won't be destroyed
// as it is tied to the activity lifecycle, not the taskbar lifecycle.
// It's destruction/creation will be managed by the activity.
private final ScopedUnfoldTransitionProgressProvider mUnfoldProgressProvider =
new NonDestroyableScopedUnfoldTransitionProgressProvider();
private TaskbarActivityContext mTaskbarActivityContext;
private StatefulActivity mActivity;
/**
* Cache a copy here so we can initialize state whenever taskbar is recreated, since
* this class does not get re-initialized w/ new taskbars.
*/
private final TaskbarSharedState mSharedState = new TaskbarSharedState();
/**
* We use WindowManager's ComponentCallbacks() for internal UI changes (similar to an Activity)
* which comes via a different channel
*/
private final RecreationListener mRecreationListener = new RecreationListener();
private class RecreationListener implements DisplayController.DisplayInfoChangeListener {
@Override
public void onDisplayInfoChanged(Context context, DisplayController.Info info, int flags) {
if ((flags & (CHANGE_DENSITY | CHANGE_NAVIGATION_MODE | CHANGE_DESKTOP_MODE
| CHANGE_TASKBAR_PINNING)) != 0) {
recreateTaskbar();
}
}
}
private final SettingsCache.OnChangeListener mOnSettingsChangeListener = c -> recreateTaskbar();
private boolean mUserUnlocked = false;
private final SimpleBroadcastReceiver mTaskbarBroadcastReceiver =
new SimpleBroadcastReceiver(this::showTaskbarFromBroadcast);
private final AllAppsActionManager mAllAppsActionManager;
private final Runnable mActivityOnDestroyCallback = new Runnable() {
@Override
public void run() {
if (mActivity != null) {
mActivity.removeOnDeviceProfileChangeListener(
mDebugActivityDeviceProfileChanged);
Log.d(TASKBAR_NOT_DESTROYED_TAG,
"unregistering activity lifecycle callbacks from "
+ "onActivityDestroyed.");
mActivity.removeEventCallback(EVENT_DESTROYED, this);
}
mActivity = null;
debugWhyTaskbarNotDestroyed("clearActivity");
if (mTaskbarActivityContext != null) {
mTaskbarActivityContext.setUIController(TaskbarUIController.DEFAULT);
}
mUnfoldProgressProvider.setSourceProvider(null);
}
};
UnfoldTransitionProgressProvider.TransitionProgressListener mUnfoldTransitionProgressListener =
new UnfoldTransitionProgressProvider.TransitionProgressListener() {
@Override
public void onTransitionStarted() {
Log.d(TASKBAR_NOT_DESTROYED_TAG,
"fold/unfold transition started getting called.");
}
@Override
public void onTransitionProgress(float progress) {
Log.d(TASKBAR_NOT_DESTROYED_TAG,
"fold/unfold transition progress : " + progress);
}
@Override
public void onTransitionFinishing() {
Log.d(TASKBAR_NOT_DESTROYED_TAG,
"fold/unfold transition finishing getting called.");
}
@Override
public void onTransitionFinished() {
Log.d(TASKBAR_NOT_DESTROYED_TAG,
"fold/unfold transition finished getting called.");
}
};
@SuppressLint("WrongConstant")
public TaskbarManager(
Context context,
AllAppsActionManager allAppsActionManager,
TaskbarNavButtonCallbacks navCallbacks) {
Display display =
context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY);
mContext = context.createWindowContext(display,
ENABLE_TASKBAR_NAVBAR_UNIFICATION ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL,
null);
mAllAppsActionManager = allAppsActionManager;
mNavigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
? context.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
: null;
if (enableTaskbarNoRecreate()) {
mWindowManager = mContext.getSystemService(WindowManager.class);
mTaskbarRootLayout = new FrameLayout(mContext) {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// The motion events can be outside the view bounds of task bar, and hence
// manually dispatching them to the drag layer here.
if (mTaskbarActivityContext != null
&& mTaskbarActivityContext.getDragLayer().isAttachedToWindow()) {
return mTaskbarActivityContext.getDragLayer().dispatchTouchEvent(ev);
}
return super.dispatchTouchEvent(ev);
}
};
}
mNavButtonController = new TaskbarNavButtonController(
context,
navCallbacks,
SystemUiProxy.INSTANCE.get(mContext),
new Handler(),
AssistUtils.newInstance(mContext));
mComponentCallbacks = new ComponentCallbacks() {
private Configuration mOldConfig = mContext.getResources().getConfiguration();
@Override
public void onConfigurationChanged(Configuration newConfig) {
Trace.instantForTrack(Trace.TRACE_TAG_APP, "TaskbarManager",
"onConfigurationChanged: " + newConfig);
debugWhyTaskbarNotDestroyed(
"TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
DeviceProfile dp = mUserUnlocked
? LauncherAppState.getIDP(mContext).getDeviceProfile(mContext)
: null;
int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES;
if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0) {
// Only recreate for theme changes, not other UI mode changes such as docking.
int oldUiNightMode = (mOldConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
int newUiNightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
if (oldUiNightMode == newUiNightMode) {
configDiff &= ~ActivityInfo.CONFIG_UI_MODE;
}
}
debugWhyTaskbarNotDestroyed("ComponentCallbacks#onConfigurationChanged() "
+ "configDiff=" + Configuration.configurationDiffToString(configDiff));
if (configDiff != 0 || mTaskbarActivityContext == null) {
recreateTaskbar();
} else {
// Config change might be handled without re-creating the taskbar
if (dp != null && !isTaskbarEnabled(dp)) {
destroyExistingTaskbar();
} else {
if (dp != null && isTaskbarEnabled(dp)) {
if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
// Re-initialize for screen size change? Should this be done
// by looking at screen-size change flag in configDiff in the
// block above?
recreateTaskbar();
} else {
mTaskbarActivityContext.updateDeviceProfile(dp);
}
}
mTaskbarActivityContext.onConfigurationChanged(configDiff);
}
}
mOldConfig = new Configuration(newConfig);
// reset taskbar was pinned value, so we don't automatically unstash taskbar upon
// user unfolding the device.
mSharedState.setTaskbarWasPinned(false);
}
@Override
public void onLowMemory() { }
};
if (LawnchairApp.isRecentsEnabled ()) {
final Uri USER_SETUP_COMPLETE_URI = Settings.Secure.getUriFor(
Settings.Secure.USER_SETUP_COMPLETE);
final Uri NAV_BAR_KIDS_MODE = Settings.Secure.getUriFor(
Settings.Secure.NAV_BAR_KIDS_MODE);
SettingsCache.INSTANCE.get(mContext)
.register(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
SettingsCache.INSTANCE.get(mContext)
.register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
}
Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
mContext.registerComponentCallbacks(mComponentCallbacks);
mShutdownReceiver.register(mContext, Intent.ACTION_SHUTDOWN);
UI_HELPER_EXECUTOR.execute(() -> {
mSharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast(
mContext,
SYSTEM_ACTION_ID_TASKBAR,
new Intent(ACTION_SHOW_TASKBAR).setPackage(mContext.getPackageName()),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
mContext.registerReceiver(
mTaskbarBroadcastReceiver,
new IntentFilter(ACTION_SHOW_TASKBAR),
RECEIVER_NOT_EXPORTED);
});
debugWhyTaskbarNotDestroyed("TaskbarManager created");
recreateTaskbar();
}
private void destroyExistingTaskbar() {
debugWhyTaskbarNotDestroyed("destroyExistingTaskbar: " + mTaskbarActivityContext);
if (mTaskbarActivityContext != null) {
mTaskbarActivityContext.onDestroy();
if (!ENABLE_TASKBAR_NAVBAR_UNIFICATION || enableTaskbarNoRecreate()) {
mTaskbarActivityContext = null;
}
}
DeviceProfile dp = mUserUnlocked ?
LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null;
if (dp == null || !isTaskbarEnabled(dp)) {
removeTaskbarRootViewFromWindow();
}
}
/**
* Show Taskbar upon receiving broadcast
*/
private void showTaskbarFromBroadcast(Intent intent) {
if (ACTION_SHOW_TASKBAR.equals(intent.getAction()) && mTaskbarActivityContext != null) {
mTaskbarActivityContext.showTaskbarFromBroadcast();
}
}
/**
* Toggles All Apps for Taskbar or Launcher depending on the current state.
*/
public void toggleAllApps() {
if (mTaskbarActivityContext == null || mTaskbarActivityContext.canToggleHomeAllApps()) {
// Home All Apps should be toggled from this class, because the controllers are not
// initialized when Taskbar is disabled (i.e. TaskbarActivityContext is null).
if (mActivity instanceof Launcher l) l.toggleAllAppsSearch();
} else {
mTaskbarActivityContext.toggleAllAppsSearch();
}
}
/**
* Displays a frame of the first Launcher reveal animation.
*
* This should be used to run a first Launcher reveal animation whose progress matches a swipe
* progress.
*/
public AnimatorPlaybackController createLauncherStartFromSuwAnim(int duration) {
return mTaskbarActivityContext == null
? null : mTaskbarActivityContext.createLauncherStartFromSuwAnim(duration);
}
/**
* Called when the user is unlocked
*/
public void onUserUnlocked() {
mUserUnlocked = true;
DisplayController.INSTANCE.get(mContext).addChangeListener(mRecreationListener);
recreateTaskbar();
addTaskbarRootViewToWindow();
}
/**
* Sets a {@link StatefulActivity} to act as taskbar callback
*/
public void setActivity(@NonNull StatefulActivity activity) {
if (mActivity == activity) {
return;
}
removeActivityCallbacksAndListeners();
mActivity = activity;
debugWhyTaskbarNotDestroyed("Set mActivity=" + mActivity);
mActivity.addOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
Log.d(TASKBAR_NOT_DESTROYED_TAG,
"registering activity lifecycle callbacks from setActivity().");
mActivity.addEventCallback(EVENT_DESTROYED, mActivityOnDestroyCallback);
UnfoldTransitionProgressProvider unfoldTransitionProgressProvider =
getUnfoldTransitionProgressProviderForActivity(activity);
if (unfoldTransitionProgressProvider != null) {
unfoldTransitionProgressProvider.addCallback(mUnfoldTransitionProgressListener);
}
mUnfoldProgressProvider.setSourceProvider(unfoldTransitionProgressProvider);
if (mTaskbarActivityContext != null) {
mTaskbarActivityContext.setUIController(
createTaskbarUIControllerForActivity(mActivity));
}
}
/**
* Returns an {@link UnfoldTransitionProgressProvider} to use while the given StatefulActivity
* is active.
*/
private UnfoldTransitionProgressProvider getUnfoldTransitionProgressProviderForActivity(
StatefulActivity activity) {
if (!enableUnfoldStateAnimation()) {
if (activity instanceof QuickstepLauncher ql) {
return ql.getUnfoldTransitionProgressProvider();
}
} else {
return SystemUiProxy.INSTANCE.get(mContext).getUnfoldTransitionProvider();
}
return null;
}
/**
* Creates a {@link TaskbarUIController} to use while the given StatefulActivity is active.
*/
private TaskbarUIController createTaskbarUIControllerForActivity(StatefulActivity activity) {
if (activity instanceof QuickstepLauncher) {
return new LauncherTaskbarUIController((QuickstepLauncher) activity);
}
if (activity instanceof RecentsActivity) {
return new FallbackTaskbarUIController((RecentsActivity) activity);
}
return TaskbarUIController.DEFAULT;
}
/**
* This method is called multiple times (ex. initial init, then when user unlocks) in which case
* we fully want to destroy an existing taskbar and create a new one.
* In other case (folding/unfolding) we don't need to remove and add window.
*/
@VisibleForTesting
public synchronized void recreateTaskbar() {
Trace.beginSection("recreateTaskbar");
try {
DeviceProfile dp = mUserUnlocked ?
LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null;
// All Apps action is unrelated to navbar unification, so we only need to check DP.
final boolean isLargeScreenTaskbar = dp != null && dp.isTaskbarPresent;
mAllAppsActionManager.setTaskbarPresent(isLargeScreenTaskbar);
destroyExistingTaskbar();
boolean isTaskbarEnabled = dp != null && isTaskbarEnabled(dp);
debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled
+ " [dp != null (i.e. mUserUnlocked)]=" + (dp != null)
+ " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION
+ " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent));
if (!isTaskbarEnabled) {
SystemUiProxy.INSTANCE.get(mContext)
.notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
return;
}
if (enableTaskbarNoRecreate() || mTaskbarActivityContext == null) {
mTaskbarActivityContext = new TaskbarActivityContext(mContext,
mNavigationBarPanelContext, dp, mNavButtonController,
mUnfoldProgressProvider);
} else {
mTaskbarActivityContext.updateDeviceProfile(dp);
}
mSharedState.startTaskbarVariantIsTransient =
DisplayController.isTransientTaskbar(mTaskbarActivityContext);
mSharedState.allAppsVisible = mSharedState.allAppsVisible && isLargeScreenTaskbar;
mTaskbarActivityContext.init(mSharedState);
if (mActivity != null) {
mTaskbarActivityContext.setUIController(
createTaskbarUIControllerForActivity(mActivity));
}
if (enableTaskbarNoRecreate()) {
addTaskbarRootViewToWindow();
mTaskbarRootLayout.removeAllViews();
mTaskbarRootLayout.addView(mTaskbarActivityContext.getDragLayer());
mTaskbarActivityContext.notifyUpdateLayoutParams();
}
} finally {
Trace.endSection();
}
}
public void onSystemUiFlagsChanged(@SystemUiStateFlags long systemUiStateFlags) {
if (DEBUG) {
Log.d(TAG, "SysUI flags changed: " + formatFlagChange(systemUiStateFlags,
mSharedState.sysuiStateFlags, QuickStepContract::getSystemUiStateString));
}
mSharedState.sysuiStateFlags = systemUiStateFlags;
if (mTaskbarActivityContext != null) {
mTaskbarActivityContext.updateSysuiStateFlags(systemUiStateFlags, false /* fromInit */);
}
}
public void onLongPressHomeEnabled(boolean assistantLongPressEnabled) {
if (mNavButtonController != null) {
mNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
}
}
/**
* Sets the flag indicating setup UI is visible
*/
public void setSetupUIVisible(boolean isVisible) {
mSharedState.setupUIVisible = isVisible;
if (mTaskbarActivityContext != null) {
mTaskbarActivityContext.setSetupUIVisible(isVisible);
}
}
private boolean isTaskbarEnabled(DeviceProfile deviceProfile) {
return ENABLE_TASKBAR_NAVBAR_UNIFICATION || deviceProfile.isTaskbarPresent;
}
public void onRotationProposal(int rotation, boolean isValid) {
if (mTaskbarActivityContext != null) {
mTaskbarActivityContext.onRotationProposal(rotation, isValid);
}
}
public void disableNavBarElements(int displayId, int state1, int state2, boolean animate) {
mSharedState.disableNavBarDisplayId = displayId;
mSharedState.disableNavBarState1 = state1;
mSharedState.disableNavBarState2 = state2;
if (mTaskbarActivityContext != null) {
mTaskbarActivityContext.disableNavBarElements(displayId, state1, state2, animate);
}
}
public void onSystemBarAttributesChanged(int displayId, int behavior) {
mSharedState.systemBarAttrsDisplayId = displayId;
mSharedState.systemBarAttrsBehavior = behavior;
if (mTaskbarActivityContext != null) {
mTaskbarActivityContext.onSystemBarAttributesChanged(displayId, behavior);
}
}
public void onNavButtonsDarkIntensityChanged(float darkIntensity) {
mSharedState.navButtonsDarkIntensity = darkIntensity;
if (mTaskbarActivityContext != null) {
mTaskbarActivityContext.onNavButtonsDarkIntensityChanged(darkIntensity);
}
}
public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
mSharedState.mLumaSamplingDisplayId = displayId;
mSharedState.mIsLumaSamplingEnabled = enable;
if (mTaskbarActivityContext != null) {
mTaskbarActivityContext.onNavigationBarLumaSamplingEnabled(displayId, enable);
}
}
private void removeActivityCallbacksAndListeners() {
if (mActivity != null) {
mActivity.removeOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
Log.d(TASKBAR_NOT_DESTROYED_TAG,
"unregistering activity lifecycle callbacks from "
+ "removeActivityCallbackAndListeners().");
mActivity.removeEventCallback(EVENT_DESTROYED, mActivityOnDestroyCallback);
UnfoldTransitionProgressProvider unfoldTransitionProgressProvider =
getUnfoldTransitionProgressProviderForActivity(mActivity);
if (unfoldTransitionProgressProvider != null) {
unfoldTransitionProgressProvider.removeCallback(mUnfoldTransitionProgressListener);
}
}
}
/**
* Called when the manager is no longer needed
*/
public void destroy() {
debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
removeActivityCallbacksAndListeners();
UI_HELPER_EXECUTOR.execute(
() -> mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext));
destroyExistingTaskbar();
removeTaskbarRootViewFromWindow();
if (mUserUnlocked) {
DisplayController.INSTANCE.get(mContext).removeChangeListener(mRecreationListener);
}
if (LawnchairApp.isRecentsEnabled()) {
final Uri USER_SETUP_COMPLETE_URI = Settings.Secure.getUriFor(
Settings.Secure.USER_SETUP_COMPLETE);
final Uri NAV_BAR_KIDS_MODE = Settings.Secure.getUriFor(
Settings.Secure.NAV_BAR_KIDS_MODE);
SettingsCache.INSTANCE.get(mContext)
.unregister(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
SettingsCache.INSTANCE.get(mContext)
.unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
}
Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
mContext.unregisterComponentCallbacks(mComponentCallbacks);
mContext.unregisterReceiver(mShutdownReceiver);
}
public @Nullable TaskbarActivityContext getCurrentActivityContext() {
return mTaskbarActivityContext;
}
public void dumpLogs(String prefix, PrintWriter pw) {
pw.println(prefix + "TaskbarManager:");
if (mTaskbarActivityContext == null) {
pw.println(prefix + "\tTaskbarActivityContext: null");
} else {
mTaskbarActivityContext.dumpLogs(prefix + "\t", pw);
}
}
@VisibleForTesting
void addTaskbarRootViewToWindow() {
if (enableTaskbarNoRecreate() && !mAddedWindow && mTaskbarActivityContext != null) {
mWindowManager.addView(mTaskbarRootLayout,
mTaskbarActivityContext.getWindowLayoutParams());
mAddedWindow = true;
}
}
@VisibleForTesting
void removeTaskbarRootViewFromWindow() {
if (enableTaskbarNoRecreate() && mAddedWindow) {
mWindowManager.removeViewImmediate(mTaskbarRootLayout);
mAddedWindow = false;
}
}
/** Temp logs for b/254119092. */
public void debugWhyTaskbarNotDestroyed(String debugReason) {
StringJoiner log = new StringJoiner("\n");
log.add(debugReason);
boolean activityTaskbarPresent = mActivity != null
&& mActivity.getDeviceProfile().isTaskbarPresent;
boolean contextTaskbarPresent = mUserUnlocked
&& LauncherAppState.getIDP(mContext).getDeviceProfile(mContext).isTaskbarPresent;
if (activityTaskbarPresent == contextTaskbarPresent) {
log.add("mActivity and mContext agree taskbarIsPresent=" + contextTaskbarPresent);
Log.d(TASKBAR_NOT_DESTROYED_TAG, log.toString());
return;
}
log.add("mActivity and mContext device profiles have different values, add more logs.");
log.add("\tmActivity logs:");
log.add("\t\tmActivity=" + mActivity);
if (mActivity != null) {
log.add("\t\tmActivity.getResources().getConfiguration()="
+ mActivity.getResources().getConfiguration());
log.add("\t\tmActivity.getDeviceProfile().isTaskbarPresent="
+ activityTaskbarPresent);
}
log.add("\tmContext logs:");
log.add("\t\tmContext=" + mContext);
log.add("\t\tmContext.getResources().getConfiguration()="
+ mContext.getResources().getConfiguration());
if (mUserUnlocked) {
log.add("\t\tLauncherAppState.getIDP().getDeviceProfile(mContext).isTaskbarPresent="
+ contextTaskbarPresent);
} else {
log.add("\t\tCouldn't get DeviceProfile because !mUserUnlocked");
}
Log.d(TASKBAR_NOT_DESTROYED_TAG, log.toString());
}
private final DeviceProfile.OnDeviceProfileChangeListener mDebugActivityDeviceProfileChanged =
dp -> debugWhyTaskbarNotDestroyed("mActivity onDeviceProfileChanged");
}
@@ -1,54 +0,0 @@
/*
* 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.launcher3.taskbar.allapps;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import com.android.launcher3.ExtendedEditText;
import com.android.launcher3.allapps.ActivityAllAppsContainerView;
import com.android.launcher3.allapps.SearchUiManager;
/** Empty search container for Taskbar All Apps used as a fallback if search is not supported. */
public class TaskbarAllAppsFallbackSearchContainer extends View implements SearchUiManager {
public TaskbarAllAppsFallbackSearchContainer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TaskbarAllAppsFallbackSearchContainer(
Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void initializeSearch(ActivityAllAppsContainerView<?> containerView) {
// Do nothing.
}
@Override
public void resetSearch() {
// Do nothing.
}
@Nullable
@Override
public ExtendedEditText getEditText() {
return null;
}
}
@@ -1,481 +0,0 @@
/*
* 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.launcher3.taskbar.bubbles;
import static java.lang.Math.abs;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.annotation.Nullable;
import android.view.InsetsController;
import android.view.MotionEvent;
import android.view.View;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.taskbar.StashedHandleViewController;
import com.android.launcher3.taskbar.TaskbarActivityContext;
import com.android.launcher3.taskbar.TaskbarControllers;
import com.android.launcher3.taskbar.TaskbarInsetsController;
import com.android.launcher3.taskbar.TaskbarStashController;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.animation.PhysicsAnimator;
/**
* Coordinates between controllers such as BubbleBarView and
* BubbleHandleViewController to
* create a cohesive animation between stashed/unstashed states.
*/
public class BubbleStashController {
private static final String TAG = "BubbleStashController";
/**
* How long to stash/unstash.
*/
public static final long BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE;
/**
* The scale bubble bar animates to when being stashed.
*/
private static final float STASHED_BAR_SCALE = 0.5f;
protected final TaskbarActivityContext mActivity;
// Initialized in init.
private TaskbarControllers mControllers;
private TaskbarInsetsController mTaskbarInsetsController;
private BubbleBarViewController mBarViewController;
private BubbleStashedHandleViewController mHandleViewController;
private TaskbarStashController mTaskbarStashController;
private MultiPropertyFactory.MultiProperty mIconAlphaForStash;
private AnimatedFloat mIconScaleForStash;
private AnimatedFloat mIconTranslationYForStash;
private MultiPropertyFactory.MultiProperty mBubbleStashedHandleAlpha;
private boolean mRequestedStashState;
private boolean mRequestedExpandedState;
private boolean mIsStashed;
private int mStashedHeight;
private int mUnstashedHeight;
private boolean mBubblesShowingOnHome;
private boolean mBubblesShowingOnOverview;
private boolean mIsSysuiLocked;
private final float mHandleCenterFromScreenBottom;
@Nullable
private AnimatorSet mAnimator;
public BubbleStashController(TaskbarActivityContext activity) {
mActivity = activity;
// the handle is centered within the stashed taskbar area
mHandleCenterFromScreenBottom = mActivity.getResources().getDimensionPixelSize(R.dimen.bubblebar_stashed_size)
/ 2f;
}
public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
mControllers = controllers;
mTaskbarInsetsController = controllers.taskbarInsetsController;
mBarViewController = bubbleControllers.bubbleBarViewController;
mHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
mTaskbarStashController = controllers.taskbarStashController;
mIconAlphaForStash = mBarViewController.getBubbleBarAlpha().get(0);
mIconScaleForStash = mBarViewController.getBubbleBarScale();
mIconTranslationYForStash = mBarViewController.getBubbleBarTranslationY();
mBubbleStashedHandleAlpha = mHandleViewController.getStashedHandleAlpha().get(
StashedHandleViewController.ALPHA_INDEX_STASHED);
mStashedHeight = mHandleViewController.getStashedHeight();
mUnstashedHeight = mHandleViewController.getUnstashedHeight();
}
/**
* Returns the touchable height of the bubble bar based on it's stashed state.
*/
public int getTouchableHeight() {
return mIsStashed ? mStashedHeight : mUnstashedHeight;
}
/**
* Returns whether the bubble bar is currently stashed.
*/
public boolean isStashed() {
return mIsStashed;
}
/**
* Animates the handle (or bubble bar depending on state) to be visible after
* the device is
* unlocked.
*
* <p>
* Normally either the bubble bar or the handle is visible,
* and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to
* transition
* between these two states. But the transition from the state where both the
* bar and handle
* are invisible is slightly different.
*/
private void animateAfterUnlock() {
AnimatorSet animatorSet = new AnimatorSet();
if (mBubblesShowingOnHome || mBubblesShowingOnOverview) {
mIsStashed = false;
animatorSet.playTogether(mIconScaleForStash.animateToValue(1),
mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()),
mIconAlphaForStash.animateToValue(1));
} else {
mIsStashed = true;
animatorSet.playTogether(mBubbleStashedHandleAlpha.animateToValue(1));
}
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
onIsStashedChanged();
}
});
animatorSet.setDuration(BAR_STASH_DURATION).start();
}
/**
* Called when launcher enters or exits the home page. Bubbles are unstashed on
* home.
*/
public void setBubblesShowingOnHome(boolean onHome) {
if (mBubblesShowingOnHome != onHome) {
mBubblesShowingOnHome = onHome;
if (!mBarViewController.hasBubbles()) {
// if there are no bubbles, there's nothing to show, so just return.
return;
}
if (mBubblesShowingOnHome) {
showBubbleBar(/* expanded= */ false);
// When transitioning from app to home the stash animator may already have been
// created, so we need to animate the bubble bar here to align with hotseat.
if (!mIsStashed) {
mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForHotseat())
.start();
}
// If the bubble bar is already unstashed, the taskbar touchable region won't be
// updated correctly, so force an update here.
mControllers
.runAfterInit(() -> mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged());
} else if (!mBarViewController.isExpanded()) {
stashBubbleBar();
}
}
}
/** Whether bubbles are showing on the launcher home page. */
public boolean isBubblesShowingOnHome() {
return mBubblesShowingOnHome;
}
// TODO: when tapping on an app in overview, this is a bit delayed compared to
// taskbar stashing
/**
* Called when launcher enters or exits overview. Bubbles are unstashed on
* overview.
*/
public void setBubblesShowingOnOverview(boolean onOverview) {
if (mBubblesShowingOnOverview != onOverview) {
mBubblesShowingOnOverview = onOverview;
if (!mBubblesShowingOnOverview && !mBarViewController.isExpanded()) {
stashBubbleBar();
} else {
// When transitioning to overview the stash animator may already have been
// created, so we need to animate the bubble bar here to align with taskbar.
mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForTaskbar())
.start();
}
}
}
/** Whether bubbles are showing on Overview. */
public boolean isBubblesShowingOnOverview() {
return mBubblesShowingOnOverview;
}
/**
* Called when sysui locked state changes, when locked, bubble bar is stashed.
*/
public void onSysuiLockedStateChange(boolean isSysuiLocked) {
if (isSysuiLocked != mIsSysuiLocked) {
mIsSysuiLocked = isSysuiLocked;
if (!mIsSysuiLocked && mBarViewController.hasBubbles()) {
animateAfterUnlock();
}
}
}
/**
* Stashes the bubble bar if allowed based on other state (e.g. on home and
* overview the
* bar does not stash).
*/
public void stashBubbleBar() {
mRequestedStashState = true;
mRequestedExpandedState = false;
updateStashedAndExpandedState();
}
/**
* Shows the bubble bar, and expands bubbles depending on
* {@param expandBubbles}.
*/
public void showBubbleBar(boolean expandBubbles) {
mRequestedStashState = false;
mRequestedExpandedState = expandBubbles;
updateStashedAndExpandedState();
}
private void updateStashedAndExpandedState() {
if (mBarViewController.isHiddenForNoBubbles()) {
// If there are no bubbles the bar and handle are invisible, nothing to do here.
return;
}
boolean isStashed = mRequestedStashState
&& !mBubblesShowingOnHome
&& !mBubblesShowingOnOverview;
if (mIsStashed != isStashed) {
// notify the view controller that the stash state is about to change so that it
// can
// cancel an ongoing animation if there is one.
// note that this has to be called before updating mIsStashed with the new
// value,
// otherwise interrupting an ongoing animation may update it again with the
// wrong state
mBarViewController.onStashStateChanging();
mIsStashed = isStashed;
if (mAnimator != null) {
mAnimator.cancel();
}
mAnimator = createStashAnimator(mIsStashed, BAR_STASH_DURATION);
mAnimator.start();
onIsStashedChanged();
}
if (mBarViewController.isExpanded() != mRequestedExpandedState) {
mBarViewController.setExpanded(mRequestedExpandedState);
}
}
/**
* Create a stash animation.
*
* @param isStashed whether it's a stash animation or an unstash animation
* @param duration duration of the animation
* @return the animation
*/
private AnimatorSet createStashAnimator(boolean isStashed, long duration) {
AnimatorSet animatorSet = new AnimatorSet();
AnimatorSet fullLengthAnimatorSet = new AnimatorSet();
// Not exactly half and may overlap. See [first|second]HalfDurationScale below.
AnimatorSet firstHalfAnimatorSet = new AnimatorSet();
AnimatorSet secondHalfAnimatorSet = new AnimatorSet();
final float firstHalfDurationScale;
final float secondHalfDurationScale;
if (isStashed) {
firstHalfDurationScale = 0.75f;
secondHalfDurationScale = 0.5f;
fullLengthAnimatorSet.play(
mIconTranslationYForStash.animateToValue(getStashTranslation()));
firstHalfAnimatorSet.playTogether(
mIconAlphaForStash.animateToValue(0),
mIconScaleForStash.animateToValue(STASHED_BAR_SCALE));
secondHalfAnimatorSet.playTogether(
mBubbleStashedHandleAlpha.animateToValue(1));
} else {
firstHalfDurationScale = 0.5f;
secondHalfDurationScale = 0.75f;
final float translationY = getBubbleBarTranslationY();
fullLengthAnimatorSet.playTogether(
mIconScaleForStash.animateToValue(1),
mIconTranslationYForStash.animateToValue(translationY));
firstHalfAnimatorSet.playTogether(
mBubbleStashedHandleAlpha.animateToValue(0));
secondHalfAnimatorSet.playTogether(
mIconAlphaForStash.animateToValue(1));
}
fullLengthAnimatorSet.play(mHandleViewController.createRevealAnimToIsStashed(isStashed));
fullLengthAnimatorSet.setDuration(duration);
firstHalfAnimatorSet.setDuration((long) (duration * firstHalfDurationScale));
secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale));
secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale)));
animatorSet.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet,
secondHalfAnimatorSet);
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mAnimator = null;
mControllers.runAfterInit(() -> {
if (isStashed) {
mBarViewController.setExpanded(false);
}
mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
});
}
});
return animatorSet;
}
private float getStashTranslation() {
return (mUnstashedHeight - mStashedHeight) / 2f;
}
private void onIsStashedChanged() {
mControllers.runAfterInit(() -> {
mHandleViewController.onIsStashedChanged();
mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
});
}
public float getBubbleBarTranslationYForTaskbar() {
return -mActivity.getDeviceProfile().taskbarBottomMargin;
}
private float getBubbleBarTranslationYForHotseat() {
final float hotseatBottomSpace = mActivity.getDeviceProfile().hotseatBarBottomSpacePx;
final float hotseatCellHeight = mActivity.getDeviceProfile().hotseatCellHeightPx;
return -hotseatBottomSpace - hotseatCellHeight + mUnstashedHeight - abs(
hotseatCellHeight - mUnstashedHeight) / 2;
}
public float getBubbleBarTranslationY() {
// If we're on home, adjust the translation so the bubble bar aligns with
// hotseat.
// Otherwise we're either showing in an app or in overview. In either case
// adjust it so
// the bubble bar aligns with the taskbar.
return mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat()
: getBubbleBarTranslationYForTaskbar();
}
/**
* The difference on the Y axis between the center of the handle and the center
* of the bubble
* bar.
*/
public float getDiffBetweenHandleAndBarCenters() {
// the difference between the centers of the handle and the bubble bar is the
// difference
// between their distance from the bottom of the screen.
float barCenter = mBarViewController.getBubbleBarCollapsedHeight() / 2f;
return mHandleCenterFromScreenBottom - barCenter;
}
/** The distance the handle moves as part of the new bubble animation. */
public float getStashedHandleTranslationForNewBubbleAnimation() {
// the should move up to the top of the stashed taskbar area. it is centered
// within it so
// it should move the same distance as it is away from the bottom.
return -mHandleCenterFromScreenBottom;
}
/** Checks whether the motion event is over the stash handle. */
public boolean isEventOverStashHandle(MotionEvent ev) {
return mHandleViewController.isEventOverHandle(ev);
}
/** Set a bubble bar location */
public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
mHandleViewController.setBubbleBarLocation(bubbleBarLocation);
}
/** Returns the [PhysicsAnimator] for the stashed handle view. */
public PhysicsAnimator<View> getStashedHandlePhysicsAnimator() {
return mHandleViewController.getPhysicsAnimator();
}
/** Notifies taskbar that it should update its touchable region. */
public void updateTaskbarTouchRegion() {
mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
}
/** Shows the bubble bar immediately without animation. */
public void showBubbleBarImmediate() {
mHandleViewController.setTranslationYForSwipe(0);
mIconTranslationYForStash.updateValue(getBubbleBarTranslationY());
mIconAlphaForStash.setValue(1);
mIconScaleForStash.updateValue(1);
mIsStashed = false;
onIsStashedChanged();
}
/** Stashes the bubble bar immediately without animation. */
public void stashBubbleBarImmediate() {
mHandleViewController.setTranslationYForSwipe(0);
mBubbleStashedHandleAlpha.setValue(1);
mIconAlphaForStash.setValue(0);
mIconTranslationYForStash.updateValue(getStashTranslation());
mIconScaleForStash.updateValue(STASHED_BAR_SCALE);
mIsStashed = true;
onIsStashedChanged();
}
/**
* Updates the values of the internal animators after the new bubble animation
* was interrupted
*
* @param isStashed whether the current state should be stashed
* @param bubbleBarTranslationY the current bubble bar translation. this is only
* used if the
* bubble bar is showing to ensure that the stash
* animator runs
* smoothly.
*/
public void onNewBubbleAnimationInterrupted(boolean isStashed, float bubbleBarTranslationY) {
if (isStashed) {
mBubbleStashedHandleAlpha.setValue(1);
mIconAlphaForStash.setValue(0);
mIconScaleForStash.updateValue(STASHED_BAR_SCALE);
mIconTranslationYForStash.updateValue(getStashTranslation());
} else {
mBubbleStashedHandleAlpha.setValue(0);
mHandleViewController.setTranslationYForSwipe(0);
mIconAlphaForStash.setValue(1);
mIconScaleForStash.updateValue(1);
mIconTranslationYForStash.updateValue(bubbleBarTranslationY);
}
mIsStashed = isStashed;
onIsStashedChanged();
}
/** Set the translation Y for the stashed handle. */
public void setHandleTranslationY(float ty) {
mHandleViewController.setTranslationYForSwipe(ty);
}
}
@@ -1,161 +0,0 @@
/*
* Copyright (C) 2019 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.launcher3.uioverrides;
import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
import static com.android.app.animation.Interpolators.AGGRESSIVE_EASE_IN_OUT;
import static com.android.app.animation.Interpolators.FINAL_FRAME;
import static com.android.app.animation.Interpolators.INSTANT;
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA;
import android.util.FloatProperty;
import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
import com.android.launcher3.LauncherState;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.quickstep.views.RecentsView;
/**
* State handler for recents view. Manages UI changes and animations for recents view based off the
* current {@link LauncherState}.
*
* @param <T> the recents view
*/
public abstract class BaseRecentsViewStateController<T extends RecentsView>
implements StateHandler<LauncherState> {
protected final T mRecentsView;
protected final QuickstepLauncher mLauncher;
public BaseRecentsViewStateController(@NonNull QuickstepLauncher launcher) {
mLauncher = launcher;
mRecentsView = launcher.getOverviewPanel();
}
@Override
public void setState(@NonNull LauncherState state) {
float[] scaleAndOffset = state.getOverviewScaleAndOffset(mLauncher);
RECENTS_SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mRecentsView, scaleAndOffset[1]);
TASK_SECONDARY_TRANSLATION.set(mRecentsView, 0f);
getContentAlphaProperty().set(mRecentsView, state.isRecentsViewVisible ? 1f : 0);
getTaskModalnessProperty().set(mRecentsView, state.getOverviewModalness());
RECENTS_GRID_PROGRESS.set(mRecentsView,
state.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f);
TASK_THUMBNAIL_SPLASH_ALPHA.set(mRecentsView, state.showTaskThumbnailSplash() ? 1f : 0f);
}
@Override
public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
PendingAnimation builder) {
if (config.hasAnimationFlag(SKIP_OVERVIEW)) {
return;
}
setStateWithAnimationInternal(toState, config, builder);
builder.addEndListener(success -> {
if (!success) {
mRecentsView.reset();
}
});
}
/**
* Core logic for animating the recents view UI.
*
* @param toState state to animate to
* @param config current animation config
* @param setter animator set builder
*/
void setStateWithAnimationInternal(@NonNull final LauncherState toState,
@NonNull StateAnimationConfig config, @NonNull PendingAnimation setter) {
float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
setter.setFloat(mRecentsView, ADJACENT_PAGE_HORIZONTAL_OFFSET, scaleAndOffset[1],
config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
setter.setFloat(mRecentsView, TASK_SECONDARY_TRANSLATION, 0f,
config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
boolean exitingOverview =
!FeatureFlags.enableSplitContextually() && !toState.isRecentsViewVisible;
if (mRecentsView.isSplitSelectionActive() && exitingOverview) {
setter.add(mRecentsView.getSplitSelectController().getSplitAnimationController()
.createPlaceholderDismissAnim(mLauncher, LAUNCHER_SPLIT_SELECTION_EXIT_HOME,
setter.getDuration()));
setter.setViewAlpha(
mRecentsView.getSplitInstructionsView(),
0,
config.getInterpolator(
ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE,
LINEAR
)
);
}
setter.setFloat(mRecentsView, getContentAlphaProperty(),
toState.isRecentsViewVisible ? 1 : 0,
config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
setter.setFloat(
mRecentsView, getTaskModalnessProperty(),
toState.getOverviewModalness(),
config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
LauncherState fromState = mLauncher.getStateManager().getState();
setter.setFloat(mRecentsView, TASK_THUMBNAIL_SPLASH_ALPHA,
toState.showTaskThumbnailSplash() ? 1f : 0f,
getOverviewInterpolator(fromState, toState));
setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS,
toState.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f,
getOverviewInterpolator(fromState, toState));
}
private Interpolator getOverviewInterpolator(LauncherState fromState, LauncherState toState) {
return fromState == QUICK_SWITCH_FROM_HOME
? ACCELERATE_DECELERATE
: toState.isRecentsViewVisible ? INSTANT : FINAL_FRAME;
}
abstract FloatProperty getTaskModalnessProperty();
/**
* Get property for content alpha for the recents view.
*
* @return the float property for the view's content alpha
*/
abstract FloatProperty getContentAlphaProperty();
}
@@ -1,74 +0,0 @@
/**
* 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.launcher3.uioverrides;
import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID;
import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.os.Looper;
import androidx.annotation.NonNull;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.util.Executors;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.LauncherWidgetHolder;
import java.util.function.IntConsumer;
/**
* {@link AppWidgetHost} that is used to receive the changes to the widgets without
* storing any {@code Activity} info like that of the launcher.
*/
final class QuickstepAppWidgetHost extends AppWidgetHost {
private final @NonNull Context mContext;
private final @NonNull IntConsumer mAppWidgetRemovedCallback;
private final @NonNull LauncherWidgetHolder.ProviderChangedListener mProvidersChangedListener;
QuickstepAppWidgetHost(@NonNull Context context, @NonNull IntConsumer appWidgetRemovedCallback,
@NonNull LauncherWidgetHolder.ProviderChangedListener listener,
@NonNull Looper looper) {
super(context, APPWIDGET_HOST_ID, null, looper);
mContext = context;
mAppWidgetRemovedCallback = appWidgetRemovedCallback;
mProvidersChangedListener = listener;
}
@Override
protected void onProvidersChanged() {
mProvidersChangedListener.notifyWidgetProvidersChanged();
}
@Override
public void onAppWidgetRemoved(int appWidgetId) {
// Route the call via model thread, in case it comes while a loader-bind is in progress
Executors.MODEL_EXECUTOR.execute(
() -> Executors.MAIN_EXECUTOR.execute(
() -> mAppWidgetRemovedCallback.accept(appWidgetId)));
}
@Override
protected void onProviderChanged(int appWidgetId, @NonNull AppWidgetProviderInfo appWidget) {
LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo(
mContext, appWidget);
super.onProviderChanged(appWidgetId, info);
// The super method updates the dimensions of the providerInfo. Update the
// launcher spans accordingly.
info.initSpans(mContext, LauncherAppState.getIDP(mContext));
}
}
@@ -1,185 +0,0 @@
/*
* Copyright (C) 2017 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.launcher3.uioverrides;
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
import static com.android.quickstep.views.RecentsView.TASK_PRIMARY_SPLIT_TRANSLATION;
import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION;
import static com.android.quickstep.views.TaskView.FLAG_UPDATE_ALL;
import static com.android.wm.shell.Flags.enableSplitContextual;
import android.animation.AnimatorSet;
import android.annotation.TargetApi;
import android.os.Build;
import android.util.FloatProperty;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.anim.PropertySetter;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.quickstep.util.AnimUtils;
import com.android.quickstep.util.SplitAnimationTimings;
import com.android.quickstep.views.ClearAllButton;
import com.android.quickstep.views.LauncherRecentsView;
import com.android.quickstep.views.RecentsView;
/**
* State handler for handling UI changes for {@link LauncherRecentsView}. In addition to managing
* the basic view properties, this class also manages changes in the task visuals.
*/
@TargetApi(Build.VERSION_CODES.O)
public final class RecentsViewStateController extends
BaseRecentsViewStateController<LauncherRecentsView> {
public RecentsViewStateController(QuickstepLauncher launcher) {
super(launcher);
}
@Override
public void setState(@NonNull LauncherState state) {
super.setState(state);
if (state.isRecentsViewVisible) {
mRecentsView.updateEmptyMessage();
} else {
mRecentsView.resetTaskVisuals();
}
setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, new StateAnimationConfig(), state);
mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress());
// In Overview, we may be layering app surfaces behind Launcher, so we need to notify
// DepthController to prevent optimizations which might occlude the layers behind
mLauncher.getDepthController().setHasContentBehindLauncher(state.isRecentsViewVisible);
PendingAnimation builder =
new PendingAnimation(state.getTransitionDuration(mLauncher, true));
handleSplitSelectionState(state, builder, /* animate */false);
}
@Override
void setStateWithAnimationInternal(@NonNull LauncherState toState,
@NonNull StateAnimationConfig config, @NonNull PendingAnimation builder) {
super.setStateWithAnimationInternal(toState, config, builder);
if (toState.isRecentsViewVisible) {
// While animating into recents, update the visible task data as needed
builder.addOnFrameCallback(() -> mRecentsView.loadVisibleTaskData(FLAG_UPDATE_ALL));
mRecentsView.updateEmptyMessage();
// TODO(b/246283207): Remove logging once root cause of flake detected.
if (Utilities.isRunningInTestHarness()) {
Log.d("b/246283207", "RecentsView#setStateWithAnimationInternal getCurrentPage(): "
+ mRecentsView.getCurrentPage()
+ ", getScrollForPage(getCurrentPage())): "
+ mRecentsView.getScrollForPage(mRecentsView.getCurrentPage()));
}
} else {
builder.addListener(
AnimatorListeners.forSuccessCallback(mRecentsView::resetTaskVisuals));
}
// In Overview, we may be layering app surfaces behind Launcher, so we need to notify
// DepthController to prevent optimizations which might occlude the layers behind
builder.addListener(AnimatorListeners.forSuccessCallback(() ->
mLauncher.getDepthController().setHasContentBehindLauncher(
toState.isRecentsViewVisible)));
handleSplitSelectionState(toState, builder, /* animate */true);
setAlphas(builder, config, toState);
builder.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
toState.getOverviewFullscreenProgress(), LINEAR);
}
/**
* Create or dismiss split screen select animations.
* @param builder if null then this will run the split select animations right away, otherwise
* will add animations to builder.
*/
private void handleSplitSelectionState(@NonNull LauncherState toState,
@NonNull PendingAnimation builder, boolean animate) {
boolean goingToOverviewFromWorkspaceContextual = enableSplitContextual() &&
toState == OVERVIEW && mLauncher.isSplitSelectionActive();
if (toState != OVERVIEW_SPLIT_SELECT && !goingToOverviewFromWorkspaceContextual) {
// Not going to split
return;
}
// Create transition animations to split select
RecentsPagedOrientationHandler orientationHandler =
((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler();
Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
orientationHandler.getSplitSelectTaskOffset(
TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
mLauncher.getDeviceProfile());
SplitAnimationTimings timings =
AnimUtils.getDeviceOverviewToSplitTimings(mLauncher.getDeviceProfile().isTablet);
if (!goingToOverviewFromWorkspaceContextual) {
// This animation is already done for the contextual case, don't redo it
mRecentsView.createSplitSelectInitAnimation(builder,
toState.getTransitionDuration(mLauncher, true /* isToState */));
}
// Shift tasks vertically downward to get out of placeholder view
builder.setFloat(mRecentsView, taskViewsFloat.first,
toState.getSplitSelectTranslation(mLauncher),
timings.getGridSlidePrimaryInterpolator());
// Zero out horizontal translation
builder.setFloat(mRecentsView, taskViewsFloat.second,
0,
timings.getGridSlideSecondaryInterpolator());
if (!animate) {
AnimatorSet as = builder.buildAnim();
as.start();
as.end();
}
}
private void setAlphas(PropertySetter propertySetter, StateAnimationConfig config,
LauncherState state) {
float clearAllButtonAlpha = state.areElementsVisible(mLauncher, CLEAR_ALL_BUTTON) ? 1 : 0;
propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
clearAllButtonAlpha, LINEAR);
float overviewButtonAlpha = state.areElementsVisible(mLauncher, OVERVIEW_ACTIONS) ? 1 : 0;
propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
AnimatedFloat.VALUE, overviewButtonAlpha, config.getInterpolator(
ANIM_OVERVIEW_ACTIONS_FADE, LINEAR));
}
@Override
FloatProperty<RecentsView> getTaskModalnessProperty() {
return TASK_MODALNESS;
}
@Override
FloatProperty<RecentsView> getContentAlphaProperty() {
return CONTENT_ALPHA;
}
}
@@ -1,395 +0,0 @@
/*
* Copyright (C) 2018 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.launcher3.uioverrides.touchcontrollers;
import static com.android.launcher3.AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT;
import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.os.VibrationEffect;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Interpolator;
import com.android.app.animation.Interpolators;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.touch.BaseSwipeDetector;
import com.android.launcher3.touch.SingleAxisSwipeDetector;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.FlingBlockCheck;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.util.VibratorWrapper;
import com.android.launcher3.views.BaseDragLayer;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.quickstep.util.VibrationConstants;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.RecentsViewContainer;
import com.android.quickstep.views.TaskView;
/**
* Touch controller for handling task view card swipes
*/
public abstract class TaskViewTouchController<CONTAINER extends Context & RecentsViewContainer>
extends AnimatorListenerAdapter implements TouchController,
SingleAxisSwipeDetector.Listener {
private static final float ANIMATION_PROGRESS_FRACTION_MIDPOINT = 0.5f;
private static final long MIN_TASK_DISMISS_ANIMATION_DURATION = 300;
private static final long MAX_TASK_DISMISS_ANIMATION_DURATION = 600;
public static final int TASK_DISMISS_VIBRATION_PRIMITIVE =
VibrationEffect.Composition.PRIMITIVE_TICK;
public static final float TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE = 1f;
public static final VibrationEffect TASK_DISMISS_VIBRATION_FALLBACK =
VibrationConstants.EFFECT_TEXTURE_TICK;
protected final CONTAINER mContainer;
private final SingleAxisSwipeDetector mDetector;
private final RecentsView mRecentsView;
private final int[] mTempCords = new int[2];
private final boolean mIsRtl;
private AnimatorPlaybackController mCurrentAnimation;
private boolean mCurrentAnimationIsGoingUp;
private boolean mAllowGoingUp;
private boolean mAllowGoingDown;
private boolean mNoIntercept;
private float mDisplacementShift;
private float mProgressMultiplier;
private float mEndDisplacement;
private boolean mDraggingEnabled = true;
private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck();
private Float mOverrideVelocity = null;
private TaskView mTaskBeingDragged;
private boolean mIsDismissHapticRunning = false;
public TaskViewTouchController(CONTAINER container) {
mContainer = container;
mRecentsView = container.getOverviewPanel();
mIsRtl = Utilities.isRtl(container.getResources());
SingleAxisSwipeDetector.Direction dir =
mRecentsView.getPagedOrientationHandler().getUpDownSwipeDirection();
mDetector = new SingleAxisSwipeDetector(container, this, dir);
}
private boolean canInterceptTouch(MotionEvent ev) {
if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0) {
// Don't intercept swipes on the nav bar, as user might be trying to go home
// during a task dismiss animation.
if (mCurrentAnimation != null) {
mCurrentAnimation.getAnimationPlayer().end();
}
return false;
}
if (mCurrentAnimation != null) {
mCurrentAnimation.forceFinishIfCloseToEnd();
}
if (mCurrentAnimation != null) {
// If we are already animating from a previous state, we can intercept.
return true;
}
if (AbstractFloatingView.getTopOpenViewWithType(
mContainer, TYPE_TOUCH_CONTROLLER_NO_INTERCEPT) != null) {
return false;
}
return isRecentsInteractive();
}
protected abstract boolean isRecentsInteractive();
/** Is recents view showing a single task in a modal way. */
protected abstract boolean isRecentsModal();
protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
}
@Override
public void onAnimationCancel(Animator animation) {
if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) {
clearState();
}
}
@Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
if ((ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL)
&& mCurrentAnimation == null) {
clearState();
}
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mNoIntercept = !canInterceptTouch(ev);
if (mNoIntercept) {
return false;
}
// Now figure out which direction scroll events the controller will start
// calling the callbacks.
int directionsToDetectScroll = 0;
boolean ignoreSlopWhenSettling = false;
if (mCurrentAnimation != null) {
directionsToDetectScroll = DIRECTION_BOTH;
ignoreSlopWhenSettling = true;
} else {
mTaskBeingDragged = null;
for (int i = 0; i < mRecentsView.getTaskViewCount(); i++) {
TaskView view = mRecentsView.getTaskViewAt(i);
if (mRecentsView.isTaskViewVisible(view) && mContainer.getDragLayer()
.isEventOverView(view, ev)) {
// Disable swiping up and down if the task overlay is modal.
if (isRecentsModal()) {
mTaskBeingDragged = null;
break;
}
mTaskBeingDragged = view;
int upDirection = mRecentsView.getPagedOrientationHandler()
.getUpDirection(mIsRtl);
// The task can be dragged up to dismiss it
mAllowGoingUp = true;
// The task can be dragged down to open it if:
// - It's the current page
// - We support gestures to enter overview
// - It's the focused task if in grid view
// - The task is snapped
mAllowGoingDown = i == mRecentsView.getCurrentPage()
&& DisplayController.getNavigationMode(mContainer).hasGestures
&& (!mRecentsView.showAsGrid() || mTaskBeingDragged.isFocusedTask())
&& mRecentsView.isTaskInExpectedScrollPosition(i);
directionsToDetectScroll = mAllowGoingDown ? DIRECTION_BOTH : upDirection;
break;
}
}
if (mTaskBeingDragged == null) {
mNoIntercept = true;
return false;
}
}
mDetector.setDetectableScrollConditions(
directionsToDetectScroll, ignoreSlopWhenSettling);
}
if (mNoIntercept) {
return false;
}
onControllerTouchEvent(ev);
return mDetector.isDraggingOrSettling();
}
@Override
public boolean onControllerTouchEvent(MotionEvent ev) {
return mDetector.onTouchEvent(ev);
}
private void reInitAnimationController(boolean goingUp) {
if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) {
// No need to init
return;
}
if ((goingUp && !mAllowGoingUp) || (!goingUp && !mAllowGoingDown)) {
// Trying to re-init in an unsupported direction.
return;
}
if (mCurrentAnimation != null) {
mCurrentAnimation.setPlayFraction(0);
mCurrentAnimation.getTarget().removeListener(this);
mCurrentAnimation.dispatchOnCancel();
}
RecentsPagedOrientationHandler orientationHandler =
mRecentsView.getPagedOrientationHandler();
mCurrentAnimationIsGoingUp = goingUp;
BaseDragLayer dl = mContainer.getDragLayer();
final int secondaryLayerDimension = orientationHandler.getSecondaryDimension(dl);
long maxDuration = 2 * secondaryLayerDimension;
int verticalFactor = orientationHandler.getTaskDragDisplacementFactor(mIsRtl);
int secondaryTaskDimension = orientationHandler.getSecondaryDimension(mTaskBeingDragged);
// The interpolator controlling the most prominent visual movement. We use this to determine
// whether we passed SUCCESS_TRANSITION_PROGRESS.
final Interpolator currentInterpolator;
PendingAnimation pa;
if (goingUp) {
currentInterpolator = Interpolators.LINEAR;
pa = new PendingAnimation(maxDuration);
mRecentsView.createTaskDismissAnimation(pa, mTaskBeingDragged,
true /* animateTaskView */, true /* removeTask */, maxDuration,
false /* dismissingForSplitSelection*/);
mEndDisplacement = -secondaryTaskDimension;
} else {
currentInterpolator = Interpolators.ZOOM_IN;
pa = mRecentsView.createTaskLaunchAnimation(
mTaskBeingDragged, maxDuration, currentInterpolator);
// Since the thumbnail is what is filling the screen, based the end displacement on it.
View thumbnailView = mTaskBeingDragged.getFirstThumbnailViewDeprecated();
mTempCords[1] = orientationHandler.getSecondaryDimension(thumbnailView);
dl.getDescendantCoordRelativeToSelf(thumbnailView, mTempCords);
mEndDisplacement = secondaryLayerDimension - mTempCords[1];
}
mEndDisplacement *= verticalFactor;
mCurrentAnimation = pa.createPlaybackController();
// Setting this interpolator doesn't affect the visual motion, but is used to determine
// whether we successfully reached the target state in onDragEnd().
mCurrentAnimation.getTarget().setInterpolator(currentInterpolator);
onUserControlledAnimationCreated(mCurrentAnimation);
mCurrentAnimation.getTarget().addListener(this);
mCurrentAnimation.dispatchOnStart();
mProgressMultiplier = 1 / mEndDisplacement;
}
@Override
public void onDragStart(boolean start, float startDisplacement) {
if (!mDraggingEnabled) return;
RecentsPagedOrientationHandler orientationHandler =
mRecentsView.getPagedOrientationHandler();
if (mCurrentAnimation == null) {
reInitAnimationController(orientationHandler.isGoingUp(startDisplacement, mIsRtl));
mDisplacementShift = 0;
} else {
mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier;
mCurrentAnimation.pause();
}
mFlingBlockCheck.unblockFling();
mOverrideVelocity = null;
}
@Override
public boolean onDrag(float displacement) {
if (!mDraggingEnabled) return true;
RecentsPagedOrientationHandler orientationHandler =
mRecentsView.getPagedOrientationHandler();
float totalDisplacement = displacement + mDisplacementShift;
boolean isGoingUp = totalDisplacement == 0 ? mCurrentAnimationIsGoingUp :
orientationHandler.isGoingUp(totalDisplacement, mIsRtl);
if (isGoingUp != mCurrentAnimationIsGoingUp) {
reInitAnimationController(isGoingUp);
mFlingBlockCheck.blockFling();
} else {
mFlingBlockCheck.onEvent();
}
if (isGoingUp) {
if (mCurrentAnimation.getProgressFraction() < ANIMATION_PROGRESS_FRACTION_MIDPOINT) {
// Halve the value when dismissing, as we are animating the drag across the full
// length for only the first half of the progress
mCurrentAnimation.setPlayFraction(
Utilities.boundToRange(totalDisplacement * mProgressMultiplier / 2, 0, 1));
} else {
// Set mOverrideVelocity to control task dismiss velocity in onDragEnd
int velocityDimenId = R.dimen.default_task_dismiss_drag_velocity;
if (mRecentsView.showAsGrid()) {
if (mTaskBeingDragged.isFocusedTask()) {
velocityDimenId =
R.dimen.default_task_dismiss_drag_velocity_grid_focus_task;
} else {
velocityDimenId = R.dimen.default_task_dismiss_drag_velocity_grid;
}
}
mOverrideVelocity = -mTaskBeingDragged.getResources().getDimension(velocityDimenId);
// Once halfway through task dismissal interpolation, switch from reversible
// dragging-task animation to playing the remaining task translation animations,
// while this is in progress disable dragging.
mDraggingEnabled = false;
}
} else {
mCurrentAnimation.setPlayFraction(
Utilities.boundToRange(totalDisplacement * mProgressMultiplier, 0, 1));
}
return true;
}
@Override
public void onDragEnd(float velocity) {
if (mOverrideVelocity != null) {
velocity = mOverrideVelocity;
mOverrideVelocity = null;
}
// Limit velocity, as very large scalar values make animations play too quickly
float maxTaskDismissDragVelocity = mTaskBeingDragged.getResources().getDimension(
R.dimen.max_task_dismiss_drag_velocity);
velocity = Utilities.boundToRange(velocity, -maxTaskDismissDragVelocity,
maxTaskDismissDragVelocity);
boolean fling = mDraggingEnabled && mDetector.isFling(velocity);
final boolean goingToEnd;
boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
if (blockedFling) {
fling = false;
}
RecentsPagedOrientationHandler orientationHandler =
mRecentsView.getPagedOrientationHandler();
boolean goingUp = orientationHandler.isGoingUp(velocity, mIsRtl);
float progress = mCurrentAnimation.getProgressFraction();
float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
if (fling) {
goingToEnd = goingUp == mCurrentAnimationIsGoingUp;
} else {
goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS;
}
long animationDuration = BaseSwipeDetector.calculateDuration(
velocity, goingToEnd ? (1 - progress) : progress);
if (blockedFling && !goingToEnd) {
animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity);
}
// Due to very high or low velocity dismissals, animation durations can be inconsistently
// long or short. Bound the duration for animation of task translations for a more
// standardized feel.
animationDuration = Utilities.boundToRange(animationDuration,
MIN_TASK_DISMISS_ANIMATION_DURATION, MAX_TASK_DISMISS_ANIMATION_DURATION);
mCurrentAnimation.setEndAction(this::clearState);
mCurrentAnimation.startWithVelocity(mContainer, goingToEnd, Math.abs(velocity),
mEndDisplacement, animationDuration);
if (goingUp && goingToEnd && !mIsDismissHapticRunning) {
VibratorWrapper.INSTANCE.get(mContainer).vibrate(TASK_DISMISS_VIBRATION_PRIMITIVE,
TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE, TASK_DISMISS_VIBRATION_FALLBACK);
mIsDismissHapticRunning = true;
}
mDraggingEnabled = true;
}
private void clearState() {
mDetector.finishedScrolling();
mDetector.setDetectableScrollConditions(0, false);
mDraggingEnabled = true;
mTaskBeingDragged = null;
mCurrentAnimation = null;
mIsDismissHapticRunning = false;
}
}
@@ -1,93 +0,0 @@
/*
* Copyright (C) 2018 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.quickstep;
import android.annotation.TargetApi;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.pm.ResolveInfo;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.os.Build;
import android.provider.SearchIndexablesContract.XmlResource;
import android.provider.SearchIndexablesProvider;
import android.util.Xml;
import com.android.launcher3.R;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import static android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS;
import static android.provider.SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS;
import static android.provider.SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS;
@TargetApi(Build.VERSION_CODES.O)
public class LauncherSearchIndexablesProvider extends SearchIndexablesProvider {
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor queryXmlResources(String[] strings) {
MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
ResolveInfo settingsActivity = getContext().getPackageManager().resolveActivity(
new Intent(Intent.ACTION_APPLICATION_PREFERENCES)
.setPackage(getContext().getPackageName()), 0);
cursor.newRow()
.add(XmlResource.COLUMN_XML_RESID, R.xml.indexable_launcher_prefs)
.add(XmlResource.COLUMN_INTENT_ACTION, Intent.ACTION_APPLICATION_PREFERENCES)
.add(XmlResource.COLUMN_INTENT_TARGET_PACKAGE, getContext().getPackageName())
.add(XmlResource.COLUMN_INTENT_TARGET_CLASS, settingsActivity.activityInfo.name);
return cursor;
}
@Override
public Cursor queryRawData(String[] projection) {
return new MatrixCursor(INDEXABLES_RAW_COLUMNS);
}
@Override
public Cursor queryNonIndexableKeys(String[] projection) {
MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS);
if (!getContext().getSystemService(LauncherApps.class).hasShortcutHostPermission()) {
// We are not the current launcher. Hide all preferences
try (XmlResourceParser parser = getContext().getResources()
.getXml(R.xml.indexable_launcher_prefs)) {
final int depth = parser.getDepth();
final int[] attrs = new int[] { android.R.attr.key };
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type == XmlPullParser.START_TAG) {
TypedArray a = getContext().obtainStyledAttributes(
Xml.asAttributeSet(parser), attrs);
cursor.addRow(new String[] {a.getString(0)});
a.recycle();
}
}
} catch (IOException |XmlPullParserException e) {
throw new RuntimeException(e);
}
}
return cursor;
}
}
@@ -1,525 +0,0 @@
/*
* Copyright (C) 2018 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.quickstep;
import static com.android.launcher3.PagedView.INVALID_PAGE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Intent;
import android.graphics.PointF;
import android.os.SystemClock;
import android.os.Trace;
import android.util.Log;
import android.view.View;
import androidx.annotation.BinderThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.android.internal.jank.Cuj;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.util.RunnableList;
import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.RecentsViewContainer;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
/**
* Helper class to handle various atomic commands for switching between Overview.
*/
public class OverviewCommandHelper {
private static final String TAG = "OverviewCommandHelper";
public static final int TYPE_SHOW = 1;
public static final int TYPE_KEYBOARD_INPUT = 2;
public static final int TYPE_HIDE = 3;
public static final int TYPE_TOGGLE = 4;
public static final int TYPE_HOME = 5;
/**
* Use case for needing a queue is double tapping recents button in 3 button nav.
* Size of 2 should be enough. We'll toss in one more because we're kind hearted.
*/
private final static int MAX_QUEUE_SIZE = 3;
private static final String TRANSITION_NAME = "Transition:toOverview";
private final TouchInteractionService mService;
private final OverviewComponentObserver mOverviewComponentObserver;
private final TaskAnimationManager mTaskAnimationManager;
private final ArrayList<CommandInfo> mPendingCommands = new ArrayList<>();
/**
* Index of the TaskView that should be focused when launching Overview. Persisted so that we
* do not lose the focus across multiple calls of
* {@link OverviewCommandHelper#executeCommand(CommandInfo)} for the same command
*/
private int mKeyboardTaskFocusIndex = -1;
/**
* Whether we should incoming toggle commands while a previous toggle command is still ongoing.
* This serves as a rate-limiter to prevent overlapping animations that can clobber each other
* and prevent clean-up callbacks from running. This thus prevents a recurring set of bugs with
* janky recents animations and unresponsive home and overview buttons.
*/
private boolean mWaitForToggleCommandComplete = false;
public OverviewCommandHelper(TouchInteractionService service,
OverviewComponentObserver observer,
TaskAnimationManager taskAnimationManager) {
mService = service;
mOverviewComponentObserver = observer;
mTaskAnimationManager = taskAnimationManager;
}
/**
* Called when the command finishes execution.
*/
private void scheduleNextTask(CommandInfo command) {
if (mPendingCommands.isEmpty()) {
Log.d(TAG, "no pending commands to schedule");
return;
}
if (mPendingCommands.get(0) != command) {
Log.d(TAG, "next task not scheduled."
+ " mPendingCommands[0] type is " + mPendingCommands.get(0)
+ " - command type is: " + command);
return;
}
Log.d(TAG, "scheduleNextTask called: " + command);
mPendingCommands.remove(0);
executeNext();
}
/**
* Executes the next command from the queue. If the command finishes immediately (returns true),
* it continues to execute the next command, until the queue is empty of a command defer's its
* completion (returns false).
*/
@UiThread
private void executeNext() {
if (mPendingCommands.isEmpty()) {
Log.d(TAG, "executeNext - mPendingCommands is empty");
return;
}
CommandInfo cmd = mPendingCommands.get(0);
boolean result = executeCommand(cmd);
Log.d(TAG, "executeNext cmd type: " + cmd + ", result: " + result);
if (result) {
scheduleNextTask(cmd);
}
}
@UiThread
private void addCommand(CommandInfo cmd) {
boolean wasEmpty = mPendingCommands.isEmpty();
mPendingCommands.add(cmd);
if (wasEmpty) {
executeNext();
}
}
/**
* Adds a command to be executed next, after all pending tasks are completed.
* Max commands that can be queued is {@link #MAX_QUEUE_SIZE}.
* Requests after reaching that limit will be silently dropped.
*/
@BinderThread
public void addCommand(int type) {
if (mPendingCommands.size() >= MAX_QUEUE_SIZE) {
Log.d(TAG, "the pending command queue is full (" + mPendingCommands.size() + "). "
+ "command not added: " + type);
return;
}
Log.d(TAG, "adding command type: " + type);
CommandInfo cmd = new CommandInfo(type);
MAIN_EXECUTOR.execute(() -> addCommand(cmd));
}
@UiThread
public void clearPendingCommands() {
Log.d(TAG, "clearing pending commands - size: " + mPendingCommands.size());
mPendingCommands.clear();
}
@UiThread
public boolean canStartHomeSafely() {
return mPendingCommands.isEmpty() || mPendingCommands.get(0).type == TYPE_HOME;
}
@Nullable
private TaskView getNextTask(RecentsView view) {
final TaskView runningTaskView = view.getRunningTaskView();
if (runningTaskView == null) {
return view.getTaskViewAt(0);
} else {
final TaskView nextTask = view.getNextTaskView();
return nextTask != null ? nextTask : runningTaskView;
}
}
private boolean launchTask(RecentsView recents, @Nullable TaskView taskView, CommandInfo cmd) {
RunnableList callbackList = null;
if (taskView != null) {
mWaitForToggleCommandComplete = true;
taskView.setEndQuickSwitchCuj(true);
callbackList = taskView.launchTasks();
}
if (callbackList != null) {
callbackList.add(() -> {
Log.d(TAG, "launching task callback: " + cmd);
scheduleNextTask(cmd);
mWaitForToggleCommandComplete = false;
});
Log.d(TAG, "launching task - waiting for callback: " + cmd);
return false;
} else {
recents.startHome();
mWaitForToggleCommandComplete = false;
return true;
}
}
/**
* Executes the task and returns true if next task can be executed. If false, then the next
* task is deferred until {@link #scheduleNextTask} is called
*/
private <T extends StatefulActivity<?> & RecentsViewContainer> boolean executeCommand(
CommandInfo cmd) {
if (mWaitForToggleCommandComplete && cmd.type == TYPE_TOGGLE) {
Log.d(TAG, "executeCommand: " + cmd
+ " - waiting for toggle command complete");
return true;
}
BaseActivityInterface<?, T> activityInterface =
mOverviewComponentObserver.getActivityInterface();
RecentsView<?, ?> visibleRecentsView = activityInterface.getVisibleRecentsView();
RecentsView<?, ?> createdRecentsView;
Log.d(TAG, "executeCommand: " + cmd
+ " - visibleRecentsView: " + visibleRecentsView);
if (visibleRecentsView == null) {
T activity = activityInterface.getCreatedContainer();
createdRecentsView = activity == null ? null : activity.getOverviewPanel();
DeviceProfile dp = activity == null ? null : activity.getDeviceProfile();
TaskbarUIController uiController = activityInterface.getTaskbarController();
boolean allowQuickSwitch = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
&& uiController != null
&& dp != null
&& (dp.isTablet || dp.isTwoPanels);
switch (cmd.type) {
case TYPE_HIDE:
if (!allowQuickSwitch) {
return true;
}
mKeyboardTaskFocusIndex = uiController.launchFocusedTask();
if (mKeyboardTaskFocusIndex == -1) {
return true;
}
break;
case TYPE_KEYBOARD_INPUT:
if (allowQuickSwitch) {
uiController.openQuickSwitchView();
return true;
} else {
mKeyboardTaskFocusIndex = 0;
break;
}
case TYPE_HOME:
ActiveGestureLog.INSTANCE.addLog(
"OverviewCommandHelper.executeCommand(TYPE_HOME)");
mService.startActivity(mOverviewComponentObserver.getHomeIntent());
return true;
case TYPE_SHOW:
// When Recents is not currently visible, the command's type is TYPE_SHOW
// when overview is triggered via the keyboard overview button or Action+Tab
// keys (Not Alt+Tab which is KQS). The overview button on-screen in 3-button
// nav is TYPE_TOGGLE.
mKeyboardTaskFocusIndex = 0;
break;
default:
// continue below to handle displaying Recents.
}
} else {
createdRecentsView = visibleRecentsView;
switch (cmd.type) {
case TYPE_SHOW:
// already visible
return true;
case TYPE_KEYBOARD_INPUT: {
if (visibleRecentsView.isHandlingTouch()) {
return true;
}
}
case TYPE_HIDE: {
if (visibleRecentsView.isHandlingTouch()) {
return true;
}
mKeyboardTaskFocusIndex = INVALID_PAGE;
int currentPage = visibleRecentsView.getNextPage();
TaskView tv = (currentPage >= 0
&& currentPage < visibleRecentsView.getTaskViewCount())
? (TaskView) visibleRecentsView.getPageAt(currentPage)
: null;
return launchTask(visibleRecentsView, tv, cmd);
}
case TYPE_TOGGLE:
return launchTask(visibleRecentsView, getNextTask(visibleRecentsView), cmd);
case TYPE_HOME:
visibleRecentsView.startHome();
return true;
}
}
if (createdRecentsView != null) {
createdRecentsView.setKeyboardTaskFocusIndex(mKeyboardTaskFocusIndex);
}
// Handle recents view focus when launching from home
Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
updateRecentsViewFocus(cmd);
logShowOverviewFrom(cmd.type);
}
@Override
public void onAnimationEnd(Animator animation) {
Log.d(TAG, "switching to Overview state - onAnimationEnd: " + cmd);
super.onAnimationEnd(animation);
onRecentsViewFocusUpdated(cmd);
scheduleNextTask(cmd);
}
};
if (activityInterface.switchToRecentsIfVisible(animatorListener)) {
Log.d(TAG, "switching to Overview state - waiting: " + cmd);
// If successfully switched, wait until animation finishes
return false;
}
final T activity = activityInterface.getCreatedContainer();
if (activity != null) {
InteractionJankMonitorWrapper.begin(
activity.getRootView(),
Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
}
GestureState gestureState = mService.createGestureState(GestureState.DEFAULT_STATE,
GestureState.TrackpadGestureType.NONE);
gestureState.setHandlingAtomicEvent(true);
AbsSwipeUpHandler interactionHandler = mService.getSwipeUpHandlerFactory()
.newHandler(gestureState, cmd.createTime);
interactionHandler.setGestureEndCallback(
() -> onTransitionComplete(cmd, interactionHandler));
interactionHandler.initWhenReady("OverviewCommandHelper: cmd.type=" + cmd.type);
RecentsAnimationListener recentAnimListener = new RecentsAnimationListener() {
@Override
public void onRecentsAnimationStart(RecentsAnimationController controller,
RecentsAnimationTargets targets) {
updateRecentsViewFocus(cmd);
logShowOverviewFrom(cmd.type);
activityInterface.runOnInitBackgroundStateUI(() ->
interactionHandler.onGestureEnded(0, new PointF()));
cmd.removeListener(this);
}
@Override
public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
interactionHandler.onGestureCancelled();
cmd.removeListener(this);
T createdActivity = activityInterface.getCreatedContainer();
if (createdActivity == null) {
return;
}
if (createdRecentsView != null) {
createdRecentsView.onRecentsAnimationComplete();
}
}
};
if (visibleRecentsView != null) {
visibleRecentsView.moveRunningTaskToFront();
}
if (mTaskAnimationManager.isRecentsAnimationRunning()) {
cmd.mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(gestureState);
cmd.mActiveCallbacks.addListener(interactionHandler);
mTaskAnimationManager.notifyRecentsAnimationState(interactionHandler);
interactionHandler.onGestureStarted(true /*isLikelyToStartNewTask*/);
cmd.mActiveCallbacks.addListener(recentAnimListener);
mTaskAnimationManager.notifyRecentsAnimationState(recentAnimListener);
} else {
Intent intent = new Intent(interactionHandler.getLaunchIntent());
intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, gestureState.getGestureId());
cmd.mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(
gestureState, intent, interactionHandler);
interactionHandler.onGestureStarted(false /*isLikelyToStartNewTask*/);
cmd.mActiveCallbacks.addListener(recentAnimListener);
}
Trace.beginAsyncSection(TRANSITION_NAME, 0);
Log.d(TAG, "switching via recents animation - onGestureStarted: " + cmd);
return false;
}
private void onTransitionComplete(CommandInfo cmd, AbsSwipeUpHandler handler) {
Log.d(TAG, "switching via recents animation - onTransitionComplete: " + cmd);
cmd.removeListener(handler);
Trace.endAsyncSection(TRANSITION_NAME, 0);
onRecentsViewFocusUpdated(cmd);
scheduleNextTask(cmd);
}
private void updateRecentsViewFocus(CommandInfo cmd) {
RecentsView recentsView =
mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
if (recentsView == null || (cmd.type != TYPE_KEYBOARD_INPUT && cmd.type != TYPE_HIDE
&& cmd.type != TYPE_SHOW)) {
return;
}
// When the overview is launched via alt tab (cmd type is TYPE_KEYBOARD_INPUT),
// the touch mode somehow is not change to false by the Android framework.
// The subsequent tab to go through tasks in overview can only be dispatched to
// focuses views, while focus can only be requested in
// {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note,
// here we launch overview with live tile.
recentsView.getViewRootImpl().touchModeChanged(false);
// Ensure that recents view has focus so that it receives the followup key inputs
if (requestFocus(recentsView.getTaskViewAt(mKeyboardTaskFocusIndex))) {
return;
}
if (requestFocus(recentsView.getNextTaskView())) {
return;
}
if (requestFocus(recentsView.getTaskViewAt(0))) {
return;
}
requestFocus(recentsView);
}
private void onRecentsViewFocusUpdated(CommandInfo cmd) {
RecentsView recentsView =
mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
if (recentsView == null
|| cmd.type != TYPE_HIDE
|| mKeyboardTaskFocusIndex == INVALID_PAGE) {
return;
}
recentsView.setKeyboardTaskFocusIndex(INVALID_PAGE);
recentsView.setCurrentPage(mKeyboardTaskFocusIndex);
mKeyboardTaskFocusIndex = INVALID_PAGE;
}
private boolean requestFocus(@Nullable View taskView) {
if (taskView == null) {
return false;
}
taskView.post(() -> {
taskView.requestFocus();
taskView.requestAccessibilityFocus();
});
return true;
}
private <T extends StatefulActivity<?> & RecentsViewContainer>
void logShowOverviewFrom(int cmdType) {
BaseActivityInterface<?, T> activityInterface =
mOverviewComponentObserver.getActivityInterface();
var container = activityInterface.getCreatedContainer();
if (container != null) {
StatsLogManager.LauncherEvent event;
switch (cmdType) {
case TYPE_SHOW -> event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT;
case TYPE_HIDE ->
event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH;
case TYPE_TOGGLE -> event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON;
default -> {
return;
}
}
StatsLogManager.newInstance(container.asContext())
.logger()
.withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
.setTaskSwitcherContainer(
LauncherAtom.TaskSwitcherContainer.getDefaultInstance())
.build())
.log(event);
}
}
public void dump(PrintWriter pw) {
pw.println("OverviewCommandHelper:");
pw.println(" mPendingCommands=" + mPendingCommands.size());
if (!mPendingCommands.isEmpty()) {
pw.println(" pendingCommandType=" + mPendingCommands.get(0).type);
}
pw.println(" mKeyboardTaskFocusIndex=" + mKeyboardTaskFocusIndex);
pw.println(" mWaitForToggleCommandComplete=" + mWaitForToggleCommandComplete);
}
private static class CommandInfo {
public final long createTime = SystemClock.elapsedRealtime();
public final int type;
RecentsAnimationCallbacks mActiveCallbacks;
CommandInfo(int type) {
this.type = type;
}
void removeListener(RecentsAnimationListener listener) {
if (mActiveCallbacks != null) {
mActiveCallbacks.removeListener(listener);
}
}
@NonNull
@Override
public String toString() {
return "CommandInfo("
+ "type=" + type + ", "
+ "createTime=" + createTime + ", "
+ "mActiveCallbacks=" + mActiveCallbacks
+ ")";
}
}
}
@@ -1,296 +0,0 @@
/*
* Copyright (C) 2018 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.quickstep;
import static com.android.launcher3.Flags.enableOverviewIconMenu;
import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManager.TaskDescription;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.SparseArray;
import androidx.annotation.WorkerThread;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.BaseIconFactory.IconOptions;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.IconProvider;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.util.CancellableTask;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
import com.android.launcher3.util.DisplayController.Info;
import com.android.launcher3.util.FlagOp;
import com.android.launcher3.util.Preconditions;
import com.android.quickstep.util.TaskKeyLruCache;
import com.android.quickstep.util.TaskVisualsChangeListener;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.Task.TaskKey;
import com.android.systemui.shared.system.PackageManagerWrapper;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* Manages the caching of task icons and related data.
*/
public class TaskIconCache implements DisplayInfoChangeListener {
private final Executor mBgExecutor;
private final Context mContext;
private final TaskKeyLruCache<TaskCacheEntry> mIconCache;
private final SparseArray<BitmapInfo> mDefaultIcons = new SparseArray<>();
private BitmapInfo mDefaultIconBase = null;
private final IconProvider mIconProvider;
private BaseIconFactory mIconFactory;
@Nullable
public TaskVisualsChangeListener mTaskVisualsChangeListener = null;
public TaskIconCache(Context context, Executor bgExecutor, IconProvider iconProvider) {
mContext = context;
mBgExecutor = bgExecutor;
mIconProvider = iconProvider;
Resources res = context.getResources();
int cacheSize = res.getInteger(R.integer.recentsIconCacheSize);
mIconCache = new TaskKeyLruCache<>(cacheSize);
DisplayController.INSTANCE.get(mContext).addChangeListener(this);
}
@Override
public void onDisplayInfoChanged(Context context, Info info, int flags) {
if ((flags & CHANGE_DENSITY) != 0) {
clearCache();
}
}
/**
* Asynchronously fetches the icon and other task data.
*
* @param task The task to fetch the data for
* @param callback The callback to receive the task after its data has been populated.
* @return A cancelable handle to the request
*/
public CancellableTask updateIconInBackground(Task task, Consumer<Task> callback) {
Preconditions.assertUIThread();
if (task.icon != null) {
// Nothing to load, the icon is already loaded
callback.accept(task);
return null;
}
CancellableTask<TaskCacheEntry> request = new CancellableTask<>(
() -> getCacheEntry(task),
MAIN_EXECUTOR,
result -> {
task.icon = result.icon;
task.titleDescription = result.contentDescription;
task.title = result.title;
callback.accept(task);
dispatchIconUpdate(task.key.id);
}
);
mBgExecutor.execute(request);
return request;
}
/**
* Clears the icon cache
*/
public void clearCache() {
mBgExecutor.execute(this::resetFactory);
}
void onTaskRemoved(TaskKey taskKey) {
mIconCache.remove(taskKey);
}
void invalidateCacheEntries(String pkg, UserHandle handle) {
mBgExecutor.execute(() -> mIconCache.removeAll(key ->
pkg.equals(key.getPackageName()) && handle.getIdentifier() == key.userId));
}
@WorkerThread
private TaskCacheEntry getCacheEntry(Task task) {
TaskCacheEntry entry = mIconCache.getAndInvalidateIfModified(task.key);
if (entry != null) {
return entry;
}
TaskDescription desc = task.taskDescription;
TaskKey key = task.key;
ActivityInfo activityInfo = null;
// Create new cache entry
entry = new TaskCacheEntry();
// Load icon
// TODO: Load icon resource (b/143363444)
Bitmap icon = getIcon(desc, key.userId);
if (icon != null) {
entry.icon = getBitmapInfo(
new BitmapDrawable(mContext.getResources(), icon),
key.userId,
desc.getPrimaryColor(),
false /* isInstantApp */).newIcon(mContext);
} else {
activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
key.getComponent(), key.userId);
if (activityInfo != null) {
BitmapInfo bitmapInfo = getBitmapInfo(
mIconProvider.getIcon(activityInfo),
key.userId,
desc.getPrimaryColor(),
activityInfo.applicationInfo.isInstantApp());
entry.icon = bitmapInfo.newIcon(mContext);
} else {
entry.icon = getDefaultIcon(key.userId);
}
}
// Skip loading the content description if the activity no longer exists
if (activityInfo == null) {
activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
key.getComponent(), key.userId);
}
if (activityInfo != null) {
entry.contentDescription = getBadgedContentDescription(
activityInfo, task.key.userId, task.taskDescription);
if (enableOverviewIconMenu()) {
entry.title = Utilities.trim(activityInfo.loadLabel(mContext.getPackageManager()));
}
}
mIconCache.put(task.key, entry);
return entry;
}
private Bitmap getIcon(ActivityManager.TaskDescription desc, int userId) {
if (desc.getInMemoryIcon() != null) {
return desc.getInMemoryIcon();
}
return ActivityManager.TaskDescription.loadTaskDescriptionIcon(
desc.getIconFilename(), userId);
}
private String getBadgedContentDescription(ActivityInfo info, int userId, TaskDescription td) {
PackageManager pm = mContext.getPackageManager();
String taskLabel = td == null ? null : Utilities.trim(td.getLabel());
if (TextUtils.isEmpty(taskLabel)) {
taskLabel = Utilities.trim(info.loadLabel(pm));
}
String applicationLabel = Utilities.trim(info.applicationInfo.loadLabel(pm));
String badgedApplicationLabel = userId != UserHandle.myUserId()
? pm.getUserBadgedLabel(applicationLabel, UserHandle.of(userId)).toString()
: applicationLabel;
return applicationLabel.equals(taskLabel)
? badgedApplicationLabel : badgedApplicationLabel + " " + taskLabel;
}
@WorkerThread
private Drawable getDefaultIcon(int userId) {
synchronized (mDefaultIcons) {
if (mDefaultIconBase == null) {
try (BaseIconFactory bif = getIconFactory()) {
mDefaultIconBase = bif.makeDefaultIcon();
}
}
int index;
if ((index = mDefaultIcons.indexOfKey(userId)) >= 0) {
return mDefaultIcons.valueAt(index).newIcon(mContext);
} else {
BitmapInfo info = mDefaultIconBase.withFlags(
UserCache.INSTANCE.get(mContext).getUserInfo(UserHandle.of(userId))
.applyBitmapInfoFlags(FlagOp.NO_OP));
mDefaultIcons.put(userId, info);
return info.newIcon(mContext);
}
}
}
@WorkerThread
private BitmapInfo getBitmapInfo(Drawable drawable, int userId,
int primaryColor, boolean isInstantApp) {
try (BaseIconFactory bif = getIconFactory()) {
bif.setWrapperBackgroundColor(primaryColor);
// User version code O, so that the icon is always wrapped in an adaptive icon container
return bif.createBadgedIconBitmap(drawable,
new IconOptions()
.setUser(UserCache.INSTANCE.get(mContext)
.getUserInfo(UserHandle.of(userId)))
.setInstantApp(isInstantApp)
.setExtractedColor(0));
}
}
@WorkerThread
private BaseIconFactory getIconFactory() {
if (mIconFactory == null) {
mIconFactory = new BaseIconFactory(mContext,
DisplayController.INSTANCE.get(mContext).getInfo().getDensityDpi(),
mContext.getResources().getDimensionPixelSize(
R.dimen.task_icon_cache_default_icon_size));
}
return mIconFactory;
}
@WorkerThread
private void resetFactory() {
mIconFactory = null;
mIconCache.evictAll();
}
private static class TaskCacheEntry {
public Drawable icon;
public String contentDescription = "";
public String title = "";
}
void registerTaskVisualsChangeListener(TaskVisualsChangeListener newListener) {
mTaskVisualsChangeListener = newListener;
}
void removeTaskVisualsChangeListener() {
mTaskVisualsChangeListener = null;
}
void dispatchIconUpdate(int taskId) {
if (mTaskVisualsChangeListener != null) {
mTaskVisualsChangeListener.onTaskIconChanged(taskId);
}
}
}
@@ -1,279 +0,0 @@
/*
* Copyright (C) 2018 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.quickstep;
import static com.android.launcher3.Flags.enableGridOnlyOverview;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import android.content.Context;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.R;
import com.android.launcher3.util.CancellableTask;
import com.android.launcher3.util.Preconditions;
import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource;
import com.android.quickstep.util.TaskKeyByLastActiveTimeCache;
import com.android.quickstep.util.TaskKeyCache;
import com.android.quickstep.util.TaskKeyLruCache;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.Task.TaskKey;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import java.util.ArrayList;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
public class TaskThumbnailCache implements TaskThumbnailDataSource {
private final Executor mBgExecutor;
private final TaskKeyCache<ThumbnailData> mCache;
private final HighResLoadingState mHighResLoadingState;
private final boolean mEnableTaskSnapshotPreloading;
private final Context mContext;
public static class HighResLoadingState {
private boolean mForceHighResThumbnails;
private boolean mVisible;
private boolean mFlingingFast;
private boolean mHighResLoadingEnabled;
private ArrayList<HighResLoadingStateChangedCallback> mCallbacks = new ArrayList<>();
public interface HighResLoadingStateChangedCallback {
void onHighResLoadingStateChanged(boolean enabled);
}
private HighResLoadingState(Context context) {
// If the device does not support low-res thumbnails, only attempt to load high-res
// thumbnails
mForceHighResThumbnails = !supportsLowResThumbnails();
}
public void addCallback(HighResLoadingStateChangedCallback callback) {
mCallbacks.add(callback);
}
public void removeCallback(HighResLoadingStateChangedCallback callback) {
mCallbacks.remove(callback);
}
public void setVisible(boolean visible) {
mVisible = visible;
updateState();
}
public void setFlingingFast(boolean flingingFast) {
mFlingingFast = flingingFast;
updateState();
}
public boolean isEnabled() {
return mHighResLoadingEnabled;
}
private void updateState() {
boolean prevState = mHighResLoadingEnabled;
mHighResLoadingEnabled = mForceHighResThumbnails || (mVisible && !mFlingingFast);
if (prevState != mHighResLoadingEnabled) {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
mCallbacks.get(i).onHighResLoadingStateChanged(mHighResLoadingEnabled);
}
}
}
}
public TaskThumbnailCache(Context context, Executor bgExecutor) {
this(context, bgExecutor,
context.getResources().getInteger(R.integer.recentsThumbnailCacheSize));
}
private TaskThumbnailCache(Context context, Executor bgExecutor, int cacheSize) {
this(context, bgExecutor,
enableGridOnlyOverview() ? new TaskKeyByLastActiveTimeCache<>(cacheSize)
: new TaskKeyLruCache<>(cacheSize));
}
@VisibleForTesting
TaskThumbnailCache(Context context, Executor bgExecutor, TaskKeyCache<ThumbnailData> cache) {
mBgExecutor = bgExecutor;
mHighResLoadingState = new HighResLoadingState(context);
mContext = context;
Resources res = context.getResources();
mEnableTaskSnapshotPreloading = res.getBoolean(R.bool.config_enableTaskSnapshotPreloading);
mCache = cache;
}
/**
* Synchronously fetches the thumbnail for the given task at the specified resolution level, and
* puts it in the cache.
*/
public void updateThumbnailInCache(Task task, boolean lowResolution) {
if (task == null) {
return;
}
Preconditions.assertUIThread();
// Fetch the thumbnail for this task and put it in the cache
if (task.thumbnail == null) {
updateThumbnailInBackground(task.key, lowResolution,
t -> task.thumbnail = t);
}
}
/**
* Synchronously updates the thumbnail in the cache if it is already there.
*/
public void updateTaskSnapShot(int taskId, ThumbnailData thumbnail) {
Preconditions.assertUIThread();
mCache.updateIfAlreadyInCache(taskId, thumbnail);
}
/**
* Asynchronously fetches the icon and other task data for the given {@param task}.
*
* @param callback The callback to receive the task after its data has been populated.
* @return A cancelable handle to the request
*/
@Override
public CancellableTask<ThumbnailData> updateThumbnailInBackground(
Task task, @NonNull Consumer<ThumbnailData> callback) {
Preconditions.assertUIThread();
boolean lowResolution = !mHighResLoadingState.isEnabled();
if (task.thumbnail != null && task.thumbnail.getThumbnail() != null
&& (!task.thumbnail.reducedResolution || lowResolution)) {
// Nothing to load, the thumbnail is already high-resolution or matches what the
// request, so just callback
callback.accept(task.thumbnail);
return null;
}
return updateThumbnailInBackground(task.key, !mHighResLoadingState.isEnabled(), t -> {
task.thumbnail = t;
callback.accept(t);
});
}
/**
* Updates cache size and remove excess entries if current size is more than new cache size.
*
* @return whether cache size has increased
*/
public boolean updateCacheSizeAndRemoveExcess() {
int newSize = mContext.getResources().getInteger(R.integer.recentsThumbnailCacheSize);
int oldSize = mCache.getMaxSize();
if (newSize == oldSize) {
// Return if no change in size
return false;
}
mCache.updateCacheSizeAndRemoveExcess(newSize);
return newSize > oldSize;
}
private CancellableTask<ThumbnailData> updateThumbnailInBackground(TaskKey key,
boolean lowResolution, Consumer<ThumbnailData> callback) {
Preconditions.assertUIThread();
ThumbnailData cachedThumbnail = mCache.getAndInvalidateIfModified(key);
if (cachedThumbnail != null && cachedThumbnail.getThumbnail() != null
&& (!cachedThumbnail.reducedResolution || lowResolution)) {
// Already cached, lets use that thumbnail
callback.accept(cachedThumbnail);
return null;
}
CancellableTask<ThumbnailData> request = new CancellableTask<>(
() -> {
ThumbnailData thumbnailData = ActivityManagerWrapper.getInstance()
.getTaskThumbnail(key.id, lowResolution);
return thumbnailData.getThumbnail() != null ? thumbnailData
: ActivityManagerWrapper.getInstance().takeTaskThumbnail(key.id);
},
MAIN_EXECUTOR,
result -> {
// Avoid an async timing issue that a low res entry replaces an existing high
// res entry in high res enabled state, so we check before putting it to cache
if (enableGridOnlyOverview() && result.reducedResolution
&& getHighResLoadingState().isEnabled()) {
ThumbnailData newCachedThumbnail = mCache.getAndInvalidateIfModified(key);
if (newCachedThumbnail != null && newCachedThumbnail.getThumbnail() != null
&& !newCachedThumbnail.reducedResolution) {
return;
}
}
mCache.put(key, result);
callback.accept(result);
}
);
mBgExecutor.execute(request);
return request;
}
/**
* Clears the cache.
*/
public void clear() {
mCache.evictAll();
}
/**
* Removes the cached thumbnail for the given task.
*/
public void remove(Task.TaskKey key) {
mCache.remove(key);
}
/**
* @return The cache size.
*/
public int getCacheSize() {
return mCache.getMaxSize();
}
/**
* @return The mutable high-res loading state.
*/
public HighResLoadingState getHighResLoadingState() {
return mHighResLoadingState;
}
/**
* @return Whether to enable background preloading of task thumbnails.
*/
public boolean isPreloadingEnabled() {
return mEnableTaskSnapshotPreloading && mHighResLoadingState.mVisible;
}
/**
* @return Whether device supports low-res thumbnails. Low-res files are an optimization
* for faster load times of snapshots. Devices can optionally disable low-res files so that
* they only store snapshots at high-res scale. The actual scale can be configured in
* frameworks/base config overlay.
*/
private static boolean supportsLowResThumbnails() {
Resources res = Resources.getSystem();
int resId = res.getIdentifier("config_lowResTaskSnapshotScale", "dimen", "android");
if (resId != 0) {
return 0 < res.getFloat(resId);
}
return true;
}
}
@@ -1,36 +0,0 @@
/*
* Copyright (C) 2018 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.quickstep.fallback;
import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
import com.android.quickstep.RecentsActivity;
public class RecentsTaskController extends TaskViewTouchController<RecentsActivity> {
public RecentsTaskController(RecentsActivity activity) {
super(activity);
}
@Override
protected boolean isRecentsInteractive() {
return mContainer.hasWindowFocus() || mContainer.getStateManager().getState().hasLiveTile();
}
@Override
protected boolean isRecentsModal() {
return false;
}
}
@@ -1,128 +0,0 @@
/*
* 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.quickstep.fallback.window
import android.content.Context
import android.util.Log
import android.view.Display
import android.view.Display.DEFAULT_DISPLAY
import androidx.core.util.valueIterator
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.util.DaggerSingletonObject
import com.android.launcher3.util.DaggerSingletonTracker
import com.android.launcher3.util.WallpaperColorHints
import com.android.quickstep.DisplayModel
import com.android.quickstep.FallbackWindowInterface
import com.android.quickstep.RecentsAnimationDeviceState
import com.android.quickstep.TaskAnimationManager
import com.android.quickstep.dagger.QuickstepBaseAppComponent
import com.android.quickstep.fallback.window.RecentsDisplayModel.RecentsDisplayResource
import com.android.quickstep.fallback.window.RecentsWindowFlags.Companion.enableOverviewInWindow
import java.io.PrintWriter
import javax.inject.Inject
@LauncherAppSingleton
class RecentsDisplayModel
@Inject
constructor(
@ApplicationContext context: Context,
private val wallpaperColorHints: WallpaperColorHints,
tracker: DaggerSingletonTracker,
) : DisplayModel<RecentsDisplayResource>(context) {
companion object {
private const val TAG = "RecentsDisplayModel"
private const val DEBUG = false
@JvmStatic
val INSTANCE: DaggerSingletonObject<RecentsDisplayModel> =
DaggerSingletonObject<RecentsDisplayModel>(
QuickstepBaseAppComponent::getRecentsDisplayModel
)
}
init {
if (enableOverviewInWindow) {
initializeDisplays()
} else {
// Always create resource for default display
storeDisplayResource(DEFAULT_DISPLAY)
}
tracker.addCloseable { destroy() }
}
override fun createDisplayResource(display: Display): RecentsDisplayResource {
return RecentsDisplayResource(
display.displayId,
context.createDisplayContext(display),
wallpaperColorHints.hints,
)
}
fun getRecentsWindowManager(displayId: Int): RecentsWindowManager? {
if (DEBUG) Log.d(TAG, "getRecentsWindowManager for display $displayId")
return getDisplayResource(displayId)?.recentsWindowManager
}
fun getFallbackWindowInterface(displayId: Int): FallbackWindowInterface? {
if (DEBUG) Log.d(TAG, "getFallbackWindowInterface for display $displayId")
return getDisplayResource(displayId)?.fallbackWindowInterface
}
fun getTaskAnimationManager(displayId: Int): TaskAnimationManager? {
return getDisplayResource(displayId)?.taskAnimationManager
}
val activeDisplayResources: Iterable<RecentsDisplayResource>
get() =
object : Iterable<RecentsDisplayResource> {
override fun iterator() = displayResourceArray.valueIterator()
}
data class RecentsDisplayResource(
val displayId: Int,
val displayContext: Context,
val wallpaperColorHints: Int,
) : DisplayResource() {
val recentsWindowManager =
if (enableOverviewInWindow) RecentsWindowManager(displayContext, wallpaperColorHints)
else null
val fallbackWindowInterface =
if (enableOverviewInWindow) FallbackWindowInterface(recentsWindowManager) else null
val taskAnimationManager =
TaskAnimationManager(
displayContext,
RecentsAnimationDeviceState.INSTANCE.get(displayContext),
displayId,
)
override fun cleanup() {
recentsWindowManager?.destroy()
}
override fun dump(prefix: String, writer: PrintWriter) {
writer.println("${prefix}RecentsDisplayResource:")
writer.println("${prefix}\tdisplayId=${displayId}")
writer.println("${prefix}\tdisplayContext=${displayContext}")
writer.println("${prefix}\twallpaperColorHints=${wallpaperColorHints}")
writer.println("${prefix}\trecentsWindowManager=${recentsWindowManager}")
writer.println("${prefix}\tfallbackWindowInterface=${fallbackWindowInterface}")
}
}
}
@@ -1,55 +0,0 @@
/*
* Copyright (C) 2019 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.quickstep.inputconsumers;
import android.view.MotionEvent;
import com.android.launcher3.taskbar.TaskbarActivityContext;
import com.android.quickstep.InputConsumer;
import com.android.quickstep.TaskAnimationManager;
import java.util.function.Supplier;
/**
* A NO_OP input consumer which also resets any pending gesture
*/
public class ResetGestureInputConsumer implements InputConsumer {
private final TaskAnimationManager mTaskAnimationManager;
private final Supplier<TaskbarActivityContext> mActivityContextSupplier;
public ResetGestureInputConsumer(
TaskAnimationManager taskAnimationManager,
Supplier<TaskbarActivityContext> activityContextSupplier) {
mTaskAnimationManager = taskAnimationManager;
mActivityContextSupplier = activityContextSupplier;
}
@Override
public int getType() {
return TYPE_RESET_GESTURE;
}
@Override
public void onMotionEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN
&& mTaskAnimationManager.isRecentsAnimationRunning()) {
TaskbarActivityContext tac = mActivityContextSupplier.get();
mTaskAnimationManager.finishRunningRecentsAnimation(
/* toHome= */ tac != null && !tac.isInApp());
}
}
}
@@ -1,118 +0,0 @@
/*
* 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.quickstep.interaction;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.appcompat.content.res.AppCompatResources;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.icons.GraphicsUtils;
/** Indicator displaying the current progress through the gesture navigation tutorial. */
public class TutorialStepIndicator extends LinearLayout {
private static final String LOG_TAG = "TutorialStepIndicator";
private int mCurrentStep = -1;
private int mTotalSteps = -1;
public TutorialStepIndicator(Context context) {
super(context);
}
public TutorialStepIndicator(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TutorialStepIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public TutorialStepIndicator(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
/**
* Updates this indicator to display totalSteps indicator pills, with the first currentStep
* pills highlighted.
*/
public void setTutorialProgress(int currentStep, int totalSteps) {
if (currentStep <= 0) {
Log.w(LOG_TAG, "Current step number invalid: " + currentStep + ". Assuming step 1.");
currentStep = 1;
}
if (totalSteps <= 0) {
Log.w(LOG_TAG, "Total number of steps invalid: " + totalSteps + ". Assuming 1 step.");
totalSteps = 1;
}
if (currentStep > totalSteps) {
Log.w(LOG_TAG, "Current step number greater than the total number of steps. Assuming"
+ " final step.");
currentStep = totalSteps;
}
if (totalSteps < 2) {
setVisibility(GONE);
return;
}
setVisibility(VISIBLE);
mCurrentStep = currentStep;
mTotalSteps = totalSteps;
initializeStepIndicators();
}
private void initializeStepIndicators() {
for (int i = mTotalSteps; i < getChildCount(); i++) {
removeViewAt(i);
}
int activeStepIndicatorColor = GraphicsUtils.getAttrColor(
getContext(), android.R.attr.textColorPrimary);
int inactiveStepIndicatorColor = GraphicsUtils.getAttrColor(
getContext(), android.R.attr.textColorSecondaryInverse);
for (int i = 0; i < mTotalSteps; i++) {
Drawable pageIndicatorPillDrawable = AppCompatResources.getDrawable(
getContext(), R.drawable.tutorial_step_indicator_pill);
if (i >= getChildCount()) {
ImageView pageIndicatorPill = new ImageView(getContext());
pageIndicatorPill.setImageDrawable(pageIndicatorPillDrawable);
LinearLayout.LayoutParams lp = new LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
lp.setMarginStart(Utilities.dpToPx(3));
lp.setMarginEnd(Utilities.dpToPx(3));
addView(pageIndicatorPill, lp);
}
if (pageIndicatorPillDrawable != null) {
if (i < mCurrentStep) {
pageIndicatorPillDrawable.setTint(activeStepIndicatorColor);
} else {
pageIndicatorPillDrawable.setTint(inactiveStepIndicatorColor);
}
}
}
}
}
@@ -1,820 +0,0 @@
/*
* 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.quickstep.orientation;
import static android.view.Gravity.BOTTOM;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.Gravity.END;
import static android.view.Gravity.START;
import static android.view.Gravity.TOP;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static com.android.launcher3.Flags.enableOverviewIconMenu;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.ShapeDrawable;
import android.util.FloatProperty;
import android.util.Pair;
import android.view.Gravity;
import android.view.Surface;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.touch.DefaultPagedViewHandler;
import com.android.launcher3.touch.SingleAxisSwipeDetector;
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
import com.android.quickstep.views.IconAppChipView;
import java.util.ArrayList;
import java.util.List;
public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements
RecentsPagedOrientationHandler {
private final Matrix mTmpMatrix = new Matrix();
private final RectF mTmpRectF = new RectF();
@Override
public <T> T getPrimaryValue(T x, T y) {
return x;
}
@Override
public <T> T getSecondaryValue(T x, T y) {
return y;
}
@Override
public boolean isLayoutNaturalToLauncher() {
return true;
}
@Override
public void adjustFloatingIconStartVelocity(PointF velocity) {
//no-op
}
@Override
public void fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile) {
if (outStartRect.left > deviceProfile.widthPx) {
outStartRect.offsetTo(0, outStartRect.top);
} else if (outStartRect.left < -deviceProfile.widthPx) {
outStartRect.offsetTo(0, outStartRect.top);
}
}
@Override
public <T> void setSecondary(T target, Float2DAction<T> action, float param) {
action.call(target, 0, param);
}
@Override
public <T> void set(T target, Int2DAction<T> action, int primaryParam,
int secondaryParam) {
action.call(target, primaryParam, secondaryParam);
}
@Override
public int getPrimarySize(View view) {
return view.getWidth();
}
@Override
public float getPrimarySize(RectF rect) {
return rect.width();
}
@Override
public float getStart(RectF rect) {
return rect.left;
}
@Override
public float getEnd(RectF rect) {
return rect.right;
}
@Override
public void rotateInsets(@NonNull Rect insets, @NonNull Rect outInsets) {
outInsets.set(insets);
}
@Override
public int getClearAllSidePadding(View view, boolean isRtl) {
return (isRtl ? view.getPaddingRight() : - view.getPaddingLeft()) / 2;
}
@Override
public int getSecondaryDimension(View view) {
return view.getHeight();
}
@Override
public FloatProperty<View> getPrimaryViewTranslate() {
return VIEW_TRANSLATE_X;
}
@Override
public FloatProperty<View> getSecondaryViewTranslate() {
return VIEW_TRANSLATE_Y;
}
@Override
public float getDegreesRotated() {
return 0;
}
@Override
public int getRotation() {
return Surface.ROTATION_0;
}
@Override
public void setPrimaryScale(View view, float scale) {
view.setScaleX(scale);
}
@Override
public void setSecondaryScale(View view, float scale) {
view.setScaleY(scale);
}
public int getSecondaryTranslationDirectionFactor() {
return -1;
}
@Override
public int getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile) {
if (deviceProfile.isLeftRightSplit && stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
return -1;
} else {
return 1;
}
}
@Override
public float getTaskMenuX(float x, View thumbnailView,
DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) {
if (deviceProfile.isLandscape) {
return x + taskInsetMargin
+ (thumbnailView.getMeasuredWidth() - thumbnailView.getMeasuredHeight()) / 2f;
} else {
return x + taskInsetMargin;
}
}
@Override
public float getTaskMenuY(float y, View thumbnailView, int stagePosition,
View taskMenuView, float taskInsetMargin, View taskViewIcon) {
return y + taskInsetMargin;
}
@Override
public int getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile,
@StagePosition int stagePosition) {
if (enableOverviewIconMenu()) {
return thumbnailView.getResources().getDimensionPixelSize(
R.dimen.task_thumbnail_icon_menu_expanded_width);
}
int padding = thumbnailView.getResources()
.getDimensionPixelSize(R.dimen.task_menu_edge_padding);
return (deviceProfile.isLandscape && !deviceProfile.isTablet
? thumbnailView.getMeasuredHeight()
: thumbnailView.getMeasuredWidth()) - (2 * padding);
}
@Override
public int getTaskMenuHeight(float taskInsetMargin, DeviceProfile deviceProfile,
float taskMenuX, float taskMenuY) {
return (int) (deviceProfile.heightPx - deviceProfile.getInsets().top - taskMenuY
- deviceProfile.getOverviewActionsClaimedSpaceBelow());
}
@Override
public void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile,
LinearLayout taskMenuLayout, int dividerSpacing,
ShapeDrawable dividerDrawable) {
taskMenuLayout.setOrientation(LinearLayout.VERTICAL);
dividerDrawable.setIntrinsicHeight(dividerSpacing);
taskMenuLayout.setDividerDrawable(dividerDrawable);
}
@Override
public void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp,
LinearLayout viewGroup, DeviceProfile deviceProfile) {
viewGroup.setOrientation(LinearLayout.HORIZONTAL);
lp.width = LinearLayout.LayoutParams.MATCH_PARENT;
lp.height = WRAP_CONTENT;
}
@Override
public Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth,
int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile,
View[] thumbnailViews, int desiredTaskId, View banner) {
float translationX = 0;
float translationY = 0;
FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams();
banner.setPivotX(0);
banner.setPivotY(0);
banner.setRotation(getDegreesRotated());
if (splitBounds == null) {
// Single, fullscreen case
bannerParams.width = MATCH_PARENT;
bannerParams.gravity = BOTTOM | CENTER_HORIZONTAL;
return new Pair<>(translationX, translationY);
}
bannerParams.gravity =
BOTTOM | (deviceProfile.isLeftRightSplit ? START : CENTER_HORIZONTAL);
// Set correct width
if (desiredTaskId == splitBounds.leftTopTaskId) {
bannerParams.width = thumbnailViews[0].getMeasuredWidth();
} else {
bannerParams.width = thumbnailViews[1].getMeasuredWidth();
}
// Set translations
if (deviceProfile.isLeftRightSplit) {
if (desiredTaskId == splitBounds.rightBottomTaskId) {
float leftTopTaskPercent = splitBounds.appsStackedVertically
? splitBounds.topTaskPercent
: splitBounds.leftTaskPercent;
float dividerThicknessPercent = splitBounds.appsStackedVertically
? splitBounds.dividerHeightPercent
: splitBounds.dividerWidthPercent;
translationX = ((taskViewWidth * leftTopTaskPercent)
+ (taskViewWidth * dividerThicknessPercent));
}
} else {
if (desiredTaskId == splitBounds.leftTopTaskId) {
FrameLayout.LayoutParams snapshotParams =
(FrameLayout.LayoutParams) thumbnailViews[0]
.getLayoutParams();
float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically
? (1f - splitBounds.topTaskPercent)
: (1f - splitBounds.leftTaskPercent);
translationY = -((taskViewHeight - snapshotParams.topMargin)
* bottomRightTaskPlusDividerPercent);
}
}
return new Pair<>(translationX, translationY);
}
/* ---------- The following are only used by TaskViewTouchHandler. ---------- */
@Override
public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() {
return VERTICAL;
}
@Override
public int getUpDirection(boolean isRtl) {
// Ignore rtl since it only affects X value displacement, Y displacement doesn't change
return SingleAxisSwipeDetector.DIRECTION_POSITIVE;
}
@Override
public boolean isGoingUp(float displacement, boolean isRtl) {
// Ignore rtl since it only affects X value displacement, Y displacement doesn't change
return displacement < 0;
}
@Override
public int getTaskDragDisplacementFactor(boolean isRtl) {
// Ignore rtl since it only affects X value displacement, Y displacement doesn't change
return 1;
}
/* -------------------- */
@Override
public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
return dp.heightPx - rect.bottom;
}
@Override
public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) {
if (dp.isTablet) {
return Utilities.getSplitPositionOptions(dp);
}
List<SplitPositionOption> options = new ArrayList<>();
if (dp.isSeascape()) {
options.add(new SplitPositionOption(
R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen,
STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN));
} else if (dp.isLeftRightSplit) {
options.add(new SplitPositionOption(
R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen,
STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
} else {
// Only add top option
options.add(new SplitPositionOption(
R.drawable.ic_split_vertical, R.string.recent_task_option_split_screen,
STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
}
return options;
}
@Override
public void getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset,
DeviceProfile dp, @StagePosition int stagePosition, Rect out) {
int screenWidth = dp.widthPx;
int screenHeight = dp.heightPx;
boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT;
int insetSizeAdjustment = getPlaceholderSizeAdjustment(dp, pinToRight);
out.set(0, 0, screenWidth, placeholderHeight + insetSizeAdjustment);
if (!dp.isLeftRightSplit) {
// portrait, phone or tablet - spans width of screen, nothing else to do
out.inset(placeholderInset, 0);
// Adjust the top to account for content off screen. This will help to animate the view
// in with rounded corners.
int totalHeight = (int) (1.0f * screenHeight / 2 * (screenWidth - 2 * placeholderInset)
/ screenWidth);
out.top -= (totalHeight - placeholderHeight);
return;
}
// Now we rotate the portrait rect depending on what side we want pinned
float postRotateScale = (float) screenHeight / screenWidth;
mTmpMatrix.reset();
mTmpMatrix.postRotate(pinToRight ? 90 : 270);
mTmpMatrix.postTranslate(pinToRight ? screenWidth : 0, pinToRight ? 0 : screenWidth);
// The placeholder height stays constant after rotation, so we don't change width scale
mTmpMatrix.postScale(1, postRotateScale);
mTmpRectF.set(out);
mTmpMatrix.mapRect(mTmpRectF);
mTmpRectF.inset(0, placeholderInset);
mTmpRectF.roundOut(out);
// Adjust the top to account for content off screen. This will help to animate the view in
// with rounded corners.
int totalWidth = (int) (1.0f * screenWidth / 2 * (screenHeight - 2 * placeholderInset)
/ screenHeight);
int width = out.width();
if (pinToRight) {
out.right += totalWidth - width;
} else {
out.left -= totalWidth - width;
}
}
@Override
public void updateSplitIconParams(View out, float onScreenRectCenterX,
float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY,
int drawableWidth, int drawableHeight, DeviceProfile dp,
@StagePosition int stagePosition) {
boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT;
float insetAdjustment = getPlaceholderSizeAdjustment(dp, pinToRight) / 2f;
if (!dp.isLeftRightSplit) {
out.setX(onScreenRectCenterX / fullscreenScaleX
- 1.0f * drawableWidth / 2);
out.setY((onScreenRectCenterY + insetAdjustment) / fullscreenScaleY
- 1.0f * drawableHeight / 2);
} else {
if (pinToRight) {
out.setX((onScreenRectCenterX - insetAdjustment) / fullscreenScaleX
- 1.0f * drawableWidth / 2);
} else {
out.setX((onScreenRectCenterX + insetAdjustment) / fullscreenScaleX
- 1.0f * drawableWidth / 2);
}
out.setY(onScreenRectCenterY / fullscreenScaleY
- 1.0f * drawableHeight / 2);
}
}
/**
* The split placeholder comes with a default inset to buffer the icon from the top of the
* screen. But if the device already has a large inset (from cutouts etc), use that instead.
*/
private int getPlaceholderSizeAdjustment(DeviceProfile dp, boolean pinToRight) {
int insetThickness;
if (!dp.isLandscape) {
insetThickness = dp.getInsets().top;
} else {
insetThickness = pinToRight ? dp.getInsets().right : dp.getInsets().left;
}
return Math.max(insetThickness - dp.splitPlaceholderInset, 0);
}
@Override
public void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight,
int splitInstructionsWidth) {
out.setPivotX(0);
out.setPivotY(splitInstructionsHeight);
out.setRotation(getDegreesRotated());
int distanceToEdge;
if (dp.isPhone) {
if (dp.isLandscape) {
distanceToEdge = out.getResources().getDimensionPixelSize(
R.dimen.split_instructions_bottom_margin_phone_landscape);
} else {
distanceToEdge = out.getResources().getDimensionPixelSize(
R.dimen.split_instructions_bottom_margin_phone_portrait);
}
} else {
distanceToEdge = dp.getOverviewActionsClaimedSpaceBelow();
}
// Center the view in case of unbalanced insets on left or right of screen
int insetCorrectionX = (dp.getInsets().right - dp.getInsets().left) / 2;
// Adjust for any insets on the bottom edge
int insetCorrectionY = dp.getInsets().bottom;
out.setTranslationX(insetCorrectionX);
out.setTranslationY(-distanceToEdge + insetCorrectionY);
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) out.getLayoutParams();
lp.gravity = CENTER_HORIZONTAL | BOTTOM;
out.setLayoutParams(lp);
}
@Override
public void getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp,
@StagePosition int stagePosition, Rect out1, Rect out2) {
int screenHeight = dp.heightPx;
int screenWidth = dp.widthPx;
out1.set(0, 0, screenWidth, screenHeight / 2 - splitDividerSize);
out2.set(0, screenHeight / 2 + splitDividerSize, screenWidth, screenHeight);
if (!dp.isLeftRightSplit) {
// Portrait - the window bounds are always top and bottom half
return;
}
// Now we rotate the portrait rect depending on what side we want pinned
boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT;
float postRotateScale = (float) screenHeight / screenWidth;
mTmpMatrix.reset();
mTmpMatrix.postRotate(pinToRight ? 90 : 270);
mTmpMatrix.postTranslate(pinToRight ? screenHeight : 0, pinToRight ? 0 : screenWidth);
mTmpMatrix.postScale(1 / postRotateScale, postRotateScale);
mTmpRectF.set(out1);
mTmpMatrix.mapRect(mTmpRectF);
mTmpRectF.roundOut(out1);
mTmpRectF.set(out2);
mTmpMatrix.mapRect(mTmpRectF);
mTmpRectF.roundOut(out2);
}
@Override
public void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect,
SplitBounds splitInfo, int desiredStagePosition) {
float topLeftTaskPercent = splitInfo.appsStackedVertically
? splitInfo.topTaskPercent
: splitInfo.leftTaskPercent;
float dividerBarPercent = splitInfo.appsStackedVertically
? splitInfo.dividerHeightPercent
: splitInfo.dividerWidthPercent;
int taskbarHeight = dp.isTransientTaskbar ? 0 : dp.taskbarHeight;
float scale = (float) outRect.height() / (dp.availableHeightPx - taskbarHeight);
float topTaskHeight = dp.availableHeightPx * topLeftTaskPercent;
float scaledTopTaskHeight = topTaskHeight * scale;
float dividerHeight = dp.availableHeightPx * dividerBarPercent;
float scaledDividerHeight = dividerHeight * scale;
if (desiredStagePosition == SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT) {
if (dp.isLeftRightSplit) {
outRect.right = outRect.left + Math.round(outRect.width() * topLeftTaskPercent);
} else {
outRect.bottom = Math.round(outRect.top + scaledTopTaskHeight);
}
} else {
if (dp.isLeftRightSplit) {
outRect.left += Math.round(outRect.width()
* (topLeftTaskPercent + dividerBarPercent));
} else {
outRect.top += Math.round(scaledTopTaskHeight + scaledDividerHeight);
}
}
}
@Override
public void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot,
int parentWidth, int parentHeight, SplitBounds splitBoundsConfig,
DeviceProfile dp, boolean isRtl) {
int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
float dividerScale = splitBoundsConfig.appsStackedVertically
? splitBoundsConfig.dividerHeightPercent
: splitBoundsConfig.dividerWidthPercent;
Pair<Point, Point> taskViewSizes =
getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight);
if (dp.isLeftRightSplit) {
int scaledDividerBar = Math.round(parentWidth * dividerScale);
if (isRtl) {
int translationX = taskViewSizes.second.x + scaledDividerBar;
primarySnapshot.setTranslationX(-translationX);
secondarySnapshot.setTranslationX(0);
} else {
int translationX = taskViewSizes.first.x + scaledDividerBar;
secondarySnapshot.setTranslationX(translationX);
primarySnapshot.setTranslationX(0);
}
secondarySnapshot.setTranslationY(spaceAboveSnapshot);
// Reset unused translations
primarySnapshot.setTranslationY(0);
} else {
float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale);
float translationY = taskViewSizes.first.y + spaceAboveSnapshot + finalDividerHeight;
secondarySnapshot.setTranslationY(translationY);
FrameLayout.LayoutParams primaryParams =
(FrameLayout.LayoutParams) primarySnapshot.getLayoutParams();
FrameLayout.LayoutParams secondaryParams =
(FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams();
secondaryParams.topMargin = 0;
primaryParams.topMargin = spaceAboveSnapshot;
// Reset unused translations
primarySnapshot.setTranslationY(0);
secondarySnapshot.setTranslationX(0);
primarySnapshot.setTranslationX(0);
}
primarySnapshot.measure(
View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.x, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.y, View.MeasureSpec.EXACTLY));
secondarySnapshot.measure(
View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.x, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.y,
View.MeasureSpec.EXACTLY));
primarySnapshot.setScaleX(1);
secondarySnapshot.setScaleX(1);
primarySnapshot.setScaleY(1);
secondarySnapshot.setScaleY(1);
}
@Override
public Pair<Point, Point> getGroupedTaskViewSizes(
DeviceProfile dp,
SplitBounds splitBoundsConfig,
int parentWidth,
int parentHeight) {
int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
float dividerScale = splitBoundsConfig.appsStackedVertically
? splitBoundsConfig.dividerHeightPercent
: splitBoundsConfig.dividerWidthPercent;
float taskPercent = splitBoundsConfig.appsStackedVertically
? splitBoundsConfig.topTaskPercent
: splitBoundsConfig.leftTaskPercent;
Point firstTaskViewSize = new Point();
Point secondTaskViewSize = new Point();
if (dp.isLeftRightSplit) {
int scaledDividerBar = Math.round(parentWidth * dividerScale);
firstTaskViewSize.x = Math.round(parentWidth * taskPercent);
firstTaskViewSize.y = totalThumbnailHeight;
secondTaskViewSize.x = parentWidth - firstTaskViewSize.x - scaledDividerBar;
secondTaskViewSize.y = totalThumbnailHeight;
} else {
int taskbarHeight = dp.isTransientTaskbar ? 0 : dp.taskbarHeight;
float scale = (float) totalThumbnailHeight / (dp.availableHeightPx - taskbarHeight);
float topTaskHeight = dp.availableHeightPx * taskPercent;
float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale);
float scaledTopTaskHeight = topTaskHeight * scale;
firstTaskViewSize.x = parentWidth;
firstTaskViewSize.y = Math.round(scaledTopTaskHeight);
secondTaskViewSize.x = parentWidth;
secondTaskViewSize.y = Math.round(totalThumbnailHeight - firstTaskViewSize.y
- finalDividerHeight);
}
return new Pair<>(firstTaskViewSize, secondTaskViewSize);
}
@Override
public void setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin,
int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
iconParams.gravity = TOP | CENTER_HORIZONTAL;
// Reset margins, since they may have been set on rotation
iconParams.leftMargin = iconParams.rightMargin = 0;
iconParams.topMargin = iconParams.bottomMargin = 0;
}
@Override
public void setIconAppChipChildrenParams(FrameLayout.LayoutParams iconParams,
int chipChildMarginStart) {
iconParams.setMarginStart(chipChildMarginStart);
iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
iconParams.topMargin = 0;
}
@Override
public void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin) {
iconMenuParams.gravity = TOP | START;
iconMenuParams.setMarginStart(iconMenuMargin);
iconMenuParams.topMargin = thumbnailTopMargin;
iconMenuParams.bottomMargin = 0;
iconMenuParams.setMarginEnd(0);
iconAppChipView.setPivotX(0);
iconAppChipView.setPivotY(0);
iconAppChipView.setSplitTranslationY(0);
iconAppChipView.setRotation(getDegreesRotated());
}
@Override
public void setSplitIconParams(View primaryIconView, View secondaryIconView,
int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
DeviceProfile deviceProfile, SplitBounds splitConfig) {
FrameLayout.LayoutParams primaryIconParams =
(FrameLayout.LayoutParams) primaryIconView.getLayoutParams();
FrameLayout.LayoutParams secondaryIconParams = enableOverviewIconMenu()
? (FrameLayout.LayoutParams) secondaryIconView.getLayoutParams()
: new FrameLayout.LayoutParams(primaryIconParams);
if (enableOverviewIconMenu()) {
IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView;
IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView;
primaryIconParams.gravity = TOP | START;
secondaryIconParams.gravity = TOP | START;
secondaryIconParams.topMargin = primaryIconParams.topMargin;
secondaryIconParams.setMarginStart(primaryIconParams.getMarginStart());
if (deviceProfile.isLeftRightSplit) {
if (isRtl) {
int secondarySnapshotWidth = groupedTaskViewWidth - primarySnapshotWidth;
primaryAppChipView.setSplitTranslationX(-secondarySnapshotWidth);
} else {
secondaryAppChipView.setSplitTranslationX(primarySnapshotWidth);
}
} else {
primaryAppChipView.setSplitTranslationX(0);
secondaryAppChipView.setSplitTranslationX(0);
int dividerThickness = Math.min(splitConfig.visualDividerBounds.width(),
splitConfig.visualDividerBounds.height());
secondaryAppChipView.setSplitTranslationY(
primarySnapshotHeight + (deviceProfile.isTablet ? 0 : dividerThickness));
}
} else if (deviceProfile.isLeftRightSplit) {
// We calculate the "midpoint" of the thumbnail area, and place the icons there.
// This is the place where the thumbnail area splits by default, in a near-50/50 split.
// It is usually not exactly 50/50, due to insets/screen cutouts.
int fullscreenInsetThickness = deviceProfile.isSeascape()
? deviceProfile.getInsets().right
: deviceProfile.getInsets().left;
int fullscreenMidpointFromBottom = ((deviceProfile.widthPx
- fullscreenInsetThickness) / 2);
float midpointFromEndPct = (float) fullscreenMidpointFromBottom
/ deviceProfile.widthPx;
float insetPct = (float) fullscreenInsetThickness / deviceProfile.widthPx;
int spaceAboveSnapshots = 0;
int overviewThumbnailAreaThickness = groupedTaskViewWidth - spaceAboveSnapshots;
int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness
* midpointFromEndPct);
int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct);
if (deviceProfile.isSeascape()) {
primaryIconParams.gravity = TOP | (isRtl ? END : START);
secondaryIconParams.gravity = TOP | (isRtl ? END : START);
if (splitConfig.initiatedFromSeascape) {
// if the split was initiated from seascape,
// the task on the right (secondary) is slightly larger
primaryIconView.setTranslationX(bottomToMidpointOffset - taskIconHeight);
secondaryIconView.setTranslationX(bottomToMidpointOffset);
} else {
// if not,
// the task on the left (primary) is slightly larger
primaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset
- taskIconHeight);
secondaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset);
}
} else {
primaryIconParams.gravity = TOP | (isRtl ? START : END);
secondaryIconParams.gravity = TOP | (isRtl ? START : END);
if (!splitConfig.initiatedFromSeascape) {
// if the split was initiated from landscape,
// the task on the left (primary) is slightly larger
primaryIconView.setTranslationX(-bottomToMidpointOffset);
secondaryIconView.setTranslationX(-bottomToMidpointOffset + taskIconHeight);
} else {
// if not,
// the task on the right (secondary) is slightly larger
primaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset);
secondaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset
+ taskIconHeight);
}
}
} else {
primaryIconParams.gravity = TOP | CENTER_HORIZONTAL;
// shifts icon half a width left (height is used here since icons are square)
primaryIconView.setTranslationX(-(taskIconHeight / 2f));
secondaryIconParams.gravity = TOP | CENTER_HORIZONTAL;
secondaryIconView.setTranslationX(taskIconHeight / 2f);
}
if (!enableOverviewIconMenu()) {
primaryIconView.setTranslationY(0);
secondaryIconView.setTranslationY(0);
}
primaryIconView.setLayoutParams(primaryIconParams);
secondaryIconView.setLayoutParams(secondaryIconParams);
}
@Override
public int getDefaultSplitPosition(DeviceProfile deviceProfile) {
if (!deviceProfile.isTablet) {
throw new IllegalStateException("Default position available only for large screens");
}
if (deviceProfile.isLeftRightSplit) {
return STAGE_POSITION_BOTTOM_OR_RIGHT;
} else {
return STAGE_POSITION_TOP_OR_LEFT;
}
}
@Override
public Pair<FloatProperty, FloatProperty> getSplitSelectTaskOffset(FloatProperty primary,
FloatProperty secondary, DeviceProfile deviceProfile) {
if (deviceProfile.isLeftRightSplit) { // or seascape
return new Pair<>(primary, secondary);
} else {
return new Pair<>(secondary, primary);
}
}
@Override
public float getFloatingTaskOffscreenTranslationTarget(View floatingTask, RectF onScreenRect,
@StagePosition int stagePosition, DeviceProfile dp) {
if (dp.isLeftRightSplit) {
float currentTranslationX = floatingTask.getTranslationX();
return stagePosition == STAGE_POSITION_TOP_OR_LEFT
? currentTranslationX - onScreenRect.width()
: currentTranslationX + onScreenRect.width();
} else {
float currentTranslationY = floatingTask.getTranslationY();
return currentTranslationY - onScreenRect.height();
}
}
@Override
public void setFloatingTaskPrimaryTranslation(View floatingTask, float translation,
DeviceProfile dp) {
if (dp.isLeftRightSplit) {
floatingTask.setTranslationX(translation);
} else {
floatingTask.setTranslationY(translation);
}
}
@Override
public float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp) {
return dp.isLeftRightSplit
? floatingTask.getTranslationX()
: floatingTask.getTranslationY();
}
@NonNull
@Override
public LauncherAtom.TaskSwitcherContainer.OrientationHandler getHandlerTypeForLogging() {
return LauncherAtom.TaskSwitcherContainer.OrientationHandler.PORTRAIT;
}
}
@@ -1,91 +0,0 @@
/*
* 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.quickstep.task.thumbnail
import android.annotation.ColorInt
import android.graphics.Rect
import androidx.core.graphics.ColorUtils
import com.android.quickstep.recents.data.RecentTasksRepository
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.systemui.shared.recents.model.Task
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@OptIn(ExperimentalCoroutinesApi::class)
class TaskThumbnailViewModel(
recentsViewData: RecentsViewData,
taskViewData: TaskViewData,
private val tasksRepository: RecentTasksRepository,
) {
private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
private var boundTaskIsRunning = false
val recentsFullscreenProgress = recentsViewData.fullscreenProgress
val inheritedScale =
combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
recentsScale * taskScale
}
val uiState: Flow<TaskThumbnailUiState> =
task
.flatMapLatest { taskFlow ->
taskFlow.map { taskVal ->
when {
taskVal == null -> Uninitialized
boundTaskIsRunning -> LiveTile
isBackgroundOnly(taskVal) ->
BackgroundOnly(taskVal.colorBackground.removeAlpha())
isSnapshotState(taskVal) -> {
val bitmap = taskVal.thumbnail?.thumbnail!!
Snapshot(
bitmap,
Rect(0, 0, bitmap.width, bitmap.height),
taskVal.colorBackground.removeAlpha()
)
}
else -> Uninitialized
}
}
}
.distinctUntilChanged()
fun bind(taskThumbnail: TaskThumbnail) {
boundTaskIsRunning = taskThumbnail.isRunning
task.value = tasksRepository.getTaskDataById(taskThumbnail.taskId)
}
private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null
private fun isSnapshotState(task: Task): Boolean {
val thumbnailPresent = task.thumbnail?.thumbnail != null
val taskLocked = task.isLocked
return thumbnailPresent && !taskLocked
}
@ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
}
@@ -1,24 +0,0 @@
/*
* 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.quickstep.task.viewmodel
import kotlinx.coroutines.flow.MutableStateFlow
class TaskViewData {
// This is typically a View concern but it is used to invalidate rendering in other Views
val scale = MutableStateFlow(1f)
}
@@ -1,454 +0,0 @@
/*
* 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.quickstep.util;
import android.util.ArraySet;
import androidx.annotation.NonNull;
import java.io.PrintWriter;
import java.util.Set;
/**
* Utility class for tracking gesture navigation events as they happen, then detecting and reporting
* known issues at log dump time.
*/
public class ActiveGestureErrorDetector {
/**
* Enums associated to gesture navigation events.
*/
public enum GestureEvent {
MOTION_DOWN, MOTION_UP, MOTION_MOVE, SET_END_TARGET, SET_END_TARGET_HOME,
SET_END_TARGET_NEW_TASK, SET_END_TARGET_ALL_APPS, ON_SETTLED_ON_END_TARGET,
ON_START_RECENTS_ANIMATION, ON_FINISH_RECENTS_ANIMATION, ON_CANCEL_RECENTS_ANIMATION,
START_RECENTS_ANIMATION, FINISH_RECENTS_ANIMATION, CANCEL_RECENTS_ANIMATION,
SET_ON_PAGE_TRANSITION_END_CALLBACK, CANCEL_CURRENT_ANIMATION, CLEANUP_SCREENSHOT,
SCROLLER_ANIMATION_ABORTED, TASK_APPEARED, EXPECTING_TASK_APPEARED,
FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER, LAUNCHER_DESTROYED, RECENT_TASKS_MISSING,
INVALID_VELOCITY_ON_SWIPE_UP, RECENTS_ANIMATION_START_PENDING,
/**
* These GestureEvents are specifically associated to state flags that get set in
* {@link com.android.quickstep.MultiStateCallback}. If a state flag needs to be tracked
* for error detection, an enum should be added here and that state flag-enum pair should
* be added to the state flag's container class' {@code getTrackedEventForState} method.
*/
STATE_GESTURE_STARTED, STATE_GESTURE_COMPLETED, STATE_GESTURE_CANCELLED,
STATE_END_TARGET_ANIMATION_FINISHED, STATE_RECENTS_SCROLLING_FINISHED,
STATE_CAPTURE_SCREENSHOT, STATE_SCREENSHOT_CAPTURED, STATE_HANDLER_INVALIDATED,
STATE_RECENTS_ANIMATION_CANCELED, STATE_LAUNCHER_DRAWN(true, false);
public final boolean mLogEvent;
public final boolean mTrackEvent;
GestureEvent() {
this(false, true);
}
GestureEvent(boolean logEvent, boolean trackEvent) {
mLogEvent = logEvent;
mTrackEvent = trackEvent;
}
}
private ActiveGestureErrorDetector() {}
private static final long ON_START_RECENT_ANIMATION_TIME_LIMIT = 500;
protected static void analyseAndDump(
@NonNull String prefix,
@NonNull PrintWriter writer,
@NonNull ActiveGestureLog.EventLog eventLog) {
writer.println(prefix + "Error messages for gesture ID: " + eventLog.logId);
if (!eventLog.mIsFullyGesturalNavMode) {
writer.println(prefix
+ "\tSkipping gesture error detection because gesture navigation not enabled");
return;
}
boolean errorDetected = false;
// Use a Set since the order is inherently checked in the loop.
final Set<GestureEvent> encounteredEvents = new ArraySet<>();
// Set flags and check order of operations.
long lastStartRecentAnimationEventEntryTime = 0;
for (ActiveGestureLog.EventEntry eventEntry : eventLog.eventEntries) {
GestureEvent gestureEvent = eventEntry.getGestureEvent();
if (gestureEvent == null) {
continue;
}
encounteredEvents.add(gestureEvent);
switch (gestureEvent) {
case MOTION_UP:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.MOTION_DOWN),
prefix,
/* errorMessage= */ "Motion up detected before/without"
+ " motion down.",
writer);
break;
case ON_SETTLED_ON_END_TARGET:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.SET_END_TARGET),
prefix,
/* errorMessage= */ "onSettledOnEndTarget called "
+ "before/without setEndTarget.",
writer);
break;
case FINISH_RECENTS_ANIMATION:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.START_RECENTS_ANIMATION),
prefix,
/* errorMessage= */ "finishRecentsAnimation called "
+ "before/without startRecentsAnimation.",
writer);
break;
case CANCEL_RECENTS_ANIMATION:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.START_RECENTS_ANIMATION),
prefix,
/* errorMessage= */ "cancelRecentsAnimation called "
+ "before/without startRecentsAnimation.",
writer);
break;
case CLEANUP_SCREENSHOT:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.STATE_SCREENSHOT_CAPTURED),
prefix,
/* errorMessage= */ "recents activity screenshot was "
+ "cleaned up before/without STATE_SCREENSHOT_CAPTURED "
+ "being set.",
writer);
break;
case SCROLLER_ANIMATION_ABORTED:
errorDetected |= printErrorIfTrue(
encounteredEvents.contains(GestureEvent.SET_END_TARGET_HOME)
&& !encounteredEvents.contains(
GestureEvent.ON_SETTLED_ON_END_TARGET),
prefix,
/* errorMessage= */ "recents view scroller animation "
+ "aborted after setting end target HOME, but before"
+ " settling on end target.",
writer);
break;
case TASK_APPEARED:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.EXPECTING_TASK_APPEARED),
prefix,
/* errorMessage= */ "onTasksAppeared was not expected to be called",
writer);
if (encounteredEvents.contains(GestureEvent.EXPECTING_TASK_APPEARED)) {
// Remove both events so that we can properly detect following errors.
encounteredEvents.remove(GestureEvent.EXPECTING_TASK_APPEARED);
encounteredEvents.remove(GestureEvent.TASK_APPEARED);
}
break;
case LAUNCHER_DESTROYED:
errorDetected |= printErrorIfTrue(
true,
prefix,
/* errorMessage= */ "Launcher destroyed mid-gesture",
writer);
break;
case STATE_GESTURE_COMPLETED:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.MOTION_UP),
prefix,
/* errorMessage= */ "STATE_GESTURE_COMPLETED set "
+ "before/without motion up.",
writer);
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED),
prefix,
/* errorMessage= */ "STATE_GESTURE_COMPLETED set "
+ "before/without STATE_GESTURE_STARTED.",
writer);
break;
case STATE_GESTURE_CANCELLED:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.MOTION_UP),
prefix,
/* errorMessage= */ "STATE_GESTURE_CANCELLED set "
+ "before/without motion up.",
writer);
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED),
prefix,
/* errorMessage= */ "STATE_GESTURE_CANCELLED set "
+ "before/without STATE_GESTURE_STARTED.",
writer);
break;
case STATE_SCREENSHOT_CAPTURED:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.STATE_CAPTURE_SCREENSHOT),
prefix,
/* errorMessage= */ "STATE_SCREENSHOT_CAPTURED set "
+ "before/without STATE_CAPTURE_SCREENSHOT.",
writer);
break;
case STATE_RECENTS_SCROLLING_FINISHED:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(
GestureEvent.SET_ON_PAGE_TRANSITION_END_CALLBACK),
prefix,
/* errorMessage= */ "STATE_RECENTS_SCROLLING_FINISHED "
+ "set before/without calling "
+ "setOnPageTransitionEndCallback.",
writer);
break;
case STATE_RECENTS_ANIMATION_CANCELED:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(
GestureEvent.START_RECENTS_ANIMATION),
prefix,
/* errorMessage= */ "STATE_RECENTS_ANIMATION_CANCELED "
+ "set before/without startRecentsAnimation.",
writer);
break;
case RECENT_TASKS_MISSING:
errorDetected |= printErrorIfTrue(
true,
prefix,
/* errorMessage= */ "SystemUiProxy.mRecentTasks missing,"
+ " couldn't start the recents activity",
writer);
break;
case ON_START_RECENTS_ANIMATION:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.START_RECENTS_ANIMATION),
prefix,
/* errorMessage= */ "ON_START_RECENTS_ANIMATION "
+ "onAnimationStart callback ran before startRecentsAnimation",
writer);
errorDetected |= printErrorIfTrue(
eventEntry.getTime() - lastStartRecentAnimationEventEntryTime
> ON_START_RECENT_ANIMATION_TIME_LIMIT,
prefix,
/* errorMessage= */"ON_START_RECENTS_ANIMATION "
+ "startRecentsAnimation was never called or onAnimationStart "
+ "callback was called more than 500 ms after "
+ "startRecentsAnimation.",
writer);
lastStartRecentAnimationEventEntryTime = 0;
break;
case ON_CANCEL_RECENTS_ANIMATION:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.ON_START_RECENTS_ANIMATION),
prefix,
/* errorMessage= */ "ON_CANCEL_RECENTS_ANIMATION "
+ "onAnimationCanceled callback ran before onAnimationStart "
+ "callback",
writer);
break;
case ON_FINISH_RECENTS_ANIMATION:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.ON_START_RECENTS_ANIMATION),
prefix,
/* errorMessage= */ "ON_FINISH_RECENTS_ANIMATION "
+ "onAnimationFinished callback ran before onAnimationStart "
+ "callback",
writer);
break;
case INVALID_VELOCITY_ON_SWIPE_UP:
errorDetected |= printErrorIfTrue(
true,
prefix,
/* errorMessage= */ "invalid velocity on swipe up gesture.",
writer);
break;
case START_RECENTS_ANIMATION:
lastStartRecentAnimationEventEntryTime = eventEntry.getTime();
break;
case RECENTS_ANIMATION_START_PENDING:
errorDetected |= printErrorIfTrue(
true,
prefix,
/* errorMessage= */ (eventEntry.getDuplicateCount() + 1)
+ " gesture(s) attempted while a requested recents"
+ " animation is still pending.",
writer);
break;
case EXPECTING_TASK_APPEARED:
case MOTION_DOWN:
case SET_END_TARGET:
case SET_END_TARGET_HOME:
case SET_END_TARGET_ALL_APPS:
case SET_END_TARGET_NEW_TASK:
case SET_ON_PAGE_TRANSITION_END_CALLBACK:
case CANCEL_CURRENT_ANIMATION:
case FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER:
case STATE_GESTURE_STARTED:
case STATE_END_TARGET_ANIMATION_FINISHED:
case STATE_CAPTURE_SCREENSHOT:
case STATE_HANDLER_INVALIDATED:
case STATE_LAUNCHER_DRAWN:
default:
// No-Op
}
}
// Check that all required events were found.
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.MOTION_DOWN),
prefix,
/* errorMessage= */ "Motion down never detected.",
writer);
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.MOTION_UP),
prefix,
/* errorMessage= */ "Motion up never detected.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET)
&& !encounteredEvents.contains(GestureEvent.ON_SETTLED_ON_END_TARGET),
prefix,
/* errorMessage= */ "setEndTarget was called, but "
+ "onSettledOnEndTarget wasn't.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET)
&& !encounteredEvents.contains(
GestureEvent.STATE_END_TARGET_ANIMATION_FINISHED),
prefix,
/* errorMessage= */ "setEndTarget was called, but "
+ "STATE_END_TARGET_ANIMATION_FINISHED was never set.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET)
&& !encounteredEvents.contains(
GestureEvent.STATE_RECENTS_SCROLLING_FINISHED),
prefix,
/* errorMessage= */ "setEndTarget was called, but "
+ "STATE_RECENTS_SCROLLING_FINISHED was never set.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(
GestureEvent.STATE_END_TARGET_ANIMATION_FINISHED)
&& encounteredEvents.contains(
GestureEvent.STATE_RECENTS_SCROLLING_FINISHED)
&& !encounteredEvents.contains(GestureEvent.ON_SETTLED_ON_END_TARGET),
prefix,
/* errorMessage= */ "STATE_END_TARGET_ANIMATION_FINISHED and "
+ "STATE_RECENTS_SCROLLING_FINISHED were set, but onSettledOnEndTarget "
+ "wasn't called.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(
GestureEvent.START_RECENTS_ANIMATION)
&& !encounteredEvents.contains(GestureEvent.FINISH_RECENTS_ANIMATION)
&& !encounteredEvents.contains(GestureEvent.CANCEL_RECENTS_ANIMATION),
prefix,
/* errorMessage= */ "startRecentsAnimation was called, but "
+ "finishRecentsAnimation and cancelRecentsAnimation weren't.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED)
&& !encounteredEvents.contains(GestureEvent.STATE_GESTURE_COMPLETED)
&& !encounteredEvents.contains(GestureEvent.STATE_GESTURE_CANCELLED),
prefix,
/* errorMessage= */ "STATE_GESTURE_STARTED was set, but "
+ "STATE_GESTURE_COMPLETED and STATE_GESTURE_CANCELLED weren't.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(
GestureEvent.STATE_CAPTURE_SCREENSHOT)
&& !encounteredEvents.contains(GestureEvent.STATE_SCREENSHOT_CAPTURED),
prefix,
/* errorMessage= */ "STATE_CAPTURE_SCREENSHOT was set, but "
+ "STATE_SCREENSHOT_CAPTURED wasn't.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(
GestureEvent.SET_ON_PAGE_TRANSITION_END_CALLBACK)
&& !encounteredEvents.contains(
GestureEvent.STATE_RECENTS_SCROLLING_FINISHED),
prefix,
/* errorMessage= */ "setOnPageTransitionEndCallback called, but "
+ "STATE_RECENTS_SCROLLING_FINISHED wasn't set.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(
GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER)
&& !encounteredEvents.contains(GestureEvent.CANCEL_CURRENT_ANIMATION)
&& !encounteredEvents.contains(GestureEvent.STATE_HANDLER_INVALIDATED),
prefix,
/* errorMessage= */ "AbsSwipeUpHandler.cancelCurrentAnimation "
+ "wasn't called and STATE_HANDLER_INVALIDATED wasn't set.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(
GestureEvent.STATE_RECENTS_ANIMATION_CANCELED)
&& !encounteredEvents.contains(GestureEvent.CLEANUP_SCREENSHOT),
prefix,
/* errorMessage= */ "STATE_RECENTS_ANIMATION_CANCELED was set but "
+ "the task screenshot wasn't cleaned up.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(
GestureEvent.EXPECTING_TASK_APPEARED)
&& !encounteredEvents.contains(GestureEvent.TASK_APPEARED),
prefix,
/* errorMessage= */ "onTaskAppeared was expected to be called but wasn't.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(GestureEvent.START_RECENTS_ANIMATION)
&& !encounteredEvents.contains(GestureEvent.ON_START_RECENTS_ANIMATION),
prefix,
/* errorMessage= */
"startRecentAnimation was called but onAnimationStart callback was not",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */
encounteredEvents.contains(GestureEvent.FINISH_RECENTS_ANIMATION)
&& !encounteredEvents.contains(GestureEvent.ON_FINISH_RECENTS_ANIMATION),
prefix,
/* errorMessage= */
"finishController was called but onAnimationFinished callback was not",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */
encounteredEvents.contains(GestureEvent.CANCEL_RECENTS_ANIMATION)
&& !encounteredEvents.contains(GestureEvent.ON_CANCEL_RECENTS_ANIMATION),
prefix,
/* errorMessage= */
"onRecentsAnimationCanceled was called but onAnimationCanceled was not",
writer);
if (!errorDetected) {
writer.println(prefix + "\tNo errors detected.");
}
}
private static boolean printErrorIfTrue(
boolean condition, String prefix, String errorMessage, PrintWriter writer) {
if (!condition) {
return false;
}
writer.println(prefix + "\t- " + errorMessage);
return true;
}
}
@@ -1,376 +0,0 @@
/*
* Copyright (C) 2019 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.quickstep.util;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.util.Preconditions;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
/**
* A log to keep track of the active gesture.
*/
public class ActiveGestureLog {
private static final int MAX_GESTURES_TRACKED = 15;
public static final ActiveGestureLog INSTANCE = new ActiveGestureLog();
private boolean mIsFullyGesturalNavMode;
/**
* NOTE: This value should be kept same as
* ActivityTaskManagerService#INTENT_EXTRA_LOG_TRACE_ID in platform
*/
public static final String INTENT_EXTRA_LOG_TRACE_ID = "INTENT_EXTRA_LOG_TRACE_ID";
private final EventLog[] logs;
private int nextIndex;
private int mCurrentLogId = 100;
private ActiveGestureLog() {
this.logs = new EventLog[MAX_GESTURES_TRACKED];
this.nextIndex = 0;
}
/**
* Track the given event for error detection.
*
* @param gestureEvent GestureEvent representing an event during the current gesture's
* execution.
*/
public void trackEvent(@Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) {
addLog(CompoundString.NO_OP, gestureEvent);
}
/**
* Adds a log to be printed at log-dump-time.
*/
public void addLog(@NonNull String event) {
addLog(event, null);
}
public void addLog(@NonNull String event, int extras) {
addLog(event, extras, null);
}
public void addLog(@NonNull String event, boolean extras) {
addLog(event, extras, null);
}
/**
* Adds a log to be printed at log-dump-time and track the associated event for error detection.
*
* @param gestureEvent GestureEvent representing the event being logged.
*/
public void addLog(
@NonNull String event, @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) {
addLog(new CompoundString(event), gestureEvent);
}
public void addLog(
@NonNull String event,
int extras,
@Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) {
addLog(new CompoundString(event).append(": ").append(extras), gestureEvent);
}
public void addLog(
@NonNull String event,
boolean extras,
@Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) {
addLog(new CompoundString(event).append(": ").append(extras), gestureEvent);
}
public void addLog(@NonNull CompoundString compoundString) {
addLog(compoundString, null);
}
public void addLog(
@NonNull CompoundString compoundString,
@Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) {
EventLog lastEventLog = logs[(nextIndex + logs.length - 1) % logs.length];
if (lastEventLog == null || mCurrentLogId != lastEventLog.logId) {
EventLog eventLog = new EventLog(mCurrentLogId, mIsFullyGesturalNavMode);
EventEntry eventEntry = new EventEntry();
eventEntry.update(compoundString, gestureEvent);
eventLog.eventEntries.add(eventEntry);
logs[nextIndex] = eventLog;
nextIndex = (nextIndex + 1) % logs.length;
return;
}
// Update the last EventLog
List<EventEntry> lastEventEntries = lastEventLog.eventEntries;
EventEntry lastEntry = !lastEventEntries.isEmpty()
? lastEventEntries.get(lastEventEntries.size() - 1) : null;
// Update the last EventEntry if it's a duplicate
if (isEntrySame(lastEntry, compoundString, gestureEvent)) {
lastEntry.duplicateCount++;
return;
}
EventEntry eventEntry = new EventEntry();
eventEntry.update(compoundString, gestureEvent);
lastEventEntries.add(eventEntry);
}
public void dump(String prefix, PrintWriter writer) {
writer.println(prefix + "ActiveGestureErrorDetector:");
for (int i = 0; i < logs.length; i++) {
EventLog eventLog = logs[(nextIndex + i) % logs.length];
if (eventLog == null) {
continue;
}
ActiveGestureErrorDetector.analyseAndDump(prefix + '\t', writer, eventLog);
}
writer.println(prefix + "ActiveGestureLog history:");
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSSZ ", Locale.US);
Date date = new Date();
for (int i = 0; i < logs.length; i++) {
EventLog eventLog = logs[(nextIndex + i) % logs.length];
if (eventLog == null) {
continue;
}
writer.println(prefix + "\tLogs for logId: " + eventLog.logId);
for (EventEntry eventEntry : eventLog.eventEntries) {
if (eventEntry.mCompoundString.mIsNoOp) {
continue;
}
date.setTime(eventEntry.time);
StringBuilder msg = new StringBuilder(prefix + "\t\t")
.append(sdf.format(date))
.append(eventEntry.mCompoundString);
if (eventEntry.duplicateCount > 0) {
msg.append(" & ").append(eventEntry.duplicateCount).append(" similar events");
}
writer.println(msg);
}
}
}
/**
* Increments and returns the current log ID. This should be used every time a new log trace
* is started.
*/
public int incrementLogId() {
return mCurrentLogId++;
}
public void setIsFullyGesturalNavMode(boolean isFullyGesturalNavMode) {
mIsFullyGesturalNavMode = isFullyGesturalNavMode;
}
/** Returns the current log ID. This should be used when a log trace is being reused. */
public int getLogId() {
return mCurrentLogId;
}
private boolean isEntrySame(
EventEntry entry,
CompoundString compoundString,
ActiveGestureErrorDetector.GestureEvent gestureEvent) {
return entry != null
&& entry.mCompoundString.equals(compoundString)
&& entry.gestureEvent == gestureEvent;
}
/** A single event entry. */
protected static class EventEntry {
@NonNull private CompoundString mCompoundString;
private ActiveGestureErrorDetector.GestureEvent gestureEvent;
private long time;
private int duplicateCount;
private EventEntry() {}
@Nullable
protected ActiveGestureErrorDetector.GestureEvent getGestureEvent() {
return gestureEvent;
}
public int getDuplicateCount() {
return duplicateCount;
}
private void update(
@NonNull CompoundString compoundString,
ActiveGestureErrorDetector.GestureEvent gestureEvent) {
this.mCompoundString = compoundString;
this.gestureEvent = gestureEvent;
time = System.currentTimeMillis();
duplicateCount = 0;
}
public long getTime() {
return time;
}
}
/** An entire log of entries associated with a single log ID */
protected static class EventLog {
protected final List<EventEntry> eventEntries = new ArrayList<>();
protected final int logId;
protected final boolean mIsFullyGesturalNavMode;
private EventLog(int logId, boolean isFullyGesturalNavMode) {
this.logId = logId;
mIsFullyGesturalNavMode = isFullyGesturalNavMode;
}
}
/** A buildable string stored as an array for memory efficiency. */
public static class CompoundString {
public static final CompoundString NO_OP = new CompoundString();
private final List<String> mSubstrings;
private final List<Object> mArgs;
private final boolean mIsNoOp;
private CompoundString() {
this(null);
}
public CompoundString(String substring) {
mIsNoOp = substring == null;
mSubstrings = mIsNoOp ? null : new ArrayList<>();
mArgs = mIsNoOp ? null : new ArrayList<>();
if (!mIsNoOp) {
mSubstrings.add(substring);
}
}
public CompoundString append(CompoundString substring) {
if (mIsNoOp || substring.mIsNoOp) {
return this;
}
mSubstrings.addAll(substring.mSubstrings);
mArgs.addAll(substring.mArgs);
return this;
}
public CompoundString append(String substring) {
if (mIsNoOp) {
return this;
}
mSubstrings.add(substring);
return this;
}
public CompoundString append(int num) {
if (mIsNoOp) {
return this;
}
mArgs.add(num);
return append("%d");
}
public CompoundString append(long num) {
if (mIsNoOp) {
return this;
}
mArgs.add(num);
return append("%d");
}
public CompoundString append(float num) {
if (mIsNoOp) {
return this;
}
mArgs.add(num);
return append("%.2f");
}
public CompoundString append(double num) {
if (mIsNoOp) {
return this;
}
mArgs.add(num);
return append("%.2f");
}
public CompoundString append(boolean bool) {
if (mIsNoOp) {
return this;
}
mArgs.add(bool);
return append("%b");
}
private Object[] getArgs() {
Preconditions.assertTrue(!mIsNoOp);
return mArgs.toArray();
}
@Override
public String toString() {
return String.format(toUnformattedString(), getArgs());
}
private String toUnformattedString() {
Preconditions.assertTrue(!mIsNoOp);
StringBuilder sb = new StringBuilder();
for (String substring : mSubstrings) {
sb.append(substring);
}
return sb.toString();
}
@Override
public int hashCode() {
return Objects.hash(mIsNoOp, mSubstrings, mArgs);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof CompoundString)) {
return false;
}
CompoundString other = (CompoundString) obj;
return (mIsNoOp == other.mIsNoOp)
&& Objects.equals(mSubstrings, other.mSubstrings)
&& Objects.equals(mArgs, other.mArgs);
}
}
}
@@ -1,73 +0,0 @@
/*
* Copyright (C) 2019 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.quickstep.util;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.util.ActivityTracker;
import com.android.launcher3.util.ActivityTracker.SchedulerCallback;
import java.util.function.BiPredicate;
public class ActivityInitListener<T extends BaseActivity> implements
SchedulerCallback<T> {
private BiPredicate<T, Boolean> mOnInitListener;
private final ActivityTracker<T> mActivityTracker;
private boolean mIsRegistered = false;
/**
* @param onInitListener a callback made when the activity is initialized. The callback should
* return true to continue receiving callbacks (ie. for if the activity is
* recreated).
*/
public ActivityInitListener(BiPredicate<T, Boolean> onInitListener,
ActivityTracker<T> tracker) {
mOnInitListener = onInitListener;
mActivityTracker = tracker;
}
@Override
public final boolean init(T activity, boolean alreadyOnHome) {
if (!mIsRegistered) {
// Don't receive any more updates
return false;
}
return handleInit(activity, alreadyOnHome);
}
protected boolean handleInit(T activity, boolean alreadyOnHome) {
return mOnInitListener.test(activity, alreadyOnHome);
}
/**
* Registers the activity-created listener. If the activity is already created, then the
* callback provided in the constructor will be called synchronously.
*/
public void register(String reasonString) {
mIsRegistered = true;
mActivityTracker.registerCallback(this, reasonString);
}
/**
* After calling this, we won't {@link #init} even when the activity is ready.
*/
public void unregister(String reasonString) {
mActivityTracker.unregisterCallback(this, reasonString);
mIsRegistered = false;
mOnInitListener = null;
}
}
@@ -1,93 +0,0 @@
/*
* 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.quickstep.util;
import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
import com.android.launcher3.R;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.ResourceBasedOverride;
import com.android.launcher3.util.SafeCloseable;
import java.io.PrintWriter;
import java.util.Optional;
/** Class to manage Assistant states. */
public class AssistStateManager implements ResourceBasedOverride, SafeCloseable {
public static final MainThreadInitializedObject<AssistStateManager> INSTANCE =
forOverride(AssistStateManager.class, R.string.assist_state_manager_class);
public AssistStateManager() {}
/** Return {@code true} if the Settings toggle is enabled. */
public boolean isSettingsAllEntrypointsEnabled() {
return false;
}
/** Whether search supports showing on the lockscreen. */
public boolean supportsShowWhenLocked() {
return false;
}
/** Whether ContextualSearchService invocation path is available. */
public boolean isContextualSearchServiceAvailable() {
return false;
}
/** Get the Launcher overridden long press nav handle duration to trigger Assistant. */
public Optional<Long> getLPNHDurationMillis() {
return Optional.empty();
}
/**
* Get the Launcher overridden long press nav handle touch slop multiplier to trigger Assistant.
*/
public Optional<Float> getLPNHCustomSlopMultiplier() {
return Optional.empty();
}
/** Get the Launcher overridden long press home duration to trigger Assistant. */
public Optional<Long> getLPHDurationMillis() {
return Optional.empty();
}
/** Get the Launcher overridden long press home touch slop multiplier to trigger Assistant. */
public Optional<Float> getLPHCustomSlopMultiplier() {
return Optional.empty();
}
/** Get the long press duration data source. */
public int getDurationDataSource() {
return 0;
}
/** Get the long press touch slop multiplier data source. */
public int getSlopDataSource() {
return 0;
}
/** Get the haptic bit overridden by AGSA. */
public Optional<Boolean> getShouldPlayHapticOverride() {
return Optional.empty();
}
/** Dump states. */
public void dump(String prefix, PrintWriter writer) {}
@Override
public void close() {}
}
@@ -1,45 +0,0 @@
/*
* 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.quickstep.util;
import android.content.Context;
import com.android.launcher3.R;
import com.android.launcher3.util.ResourceBasedOverride;
/** Utilities to work with Assistant functionality. */
public class AssistUtils implements ResourceBasedOverride {
public AssistUtils() {}
/** Creates AssistUtils as specified by overrides */
public static AssistUtils newInstance(Context context) {
return Overrides.getObject(AssistUtils.class, context, R.string.assist_utils_class);
}
/** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */
public int[] getSysUiAssistOverrideInvocationTypes() {
return new int[0];
}
/**
* @return {@code true} if the override was handled, i.e. an assist surface was shown or the
* request should be ignored. {@code false} means the caller should start assist another way.
*/
public boolean tryStartAssistOverride(int invocationType) {
return false;
}
}
@@ -1,71 +0,0 @@
/*
* 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.quickstep.util;
import androidx.annotation.NonNull;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
import java.util.List;
/**
* A {@link Task} container that can contain N number of tasks that are part of the desktop in
* recent tasks list.
*/
public class DesktopTask extends GroupTask {
@NonNull
public final List<Task> tasks;
public DesktopTask(@NonNull List<Task> tasks) {
super(tasks.get(0), null, null, TaskView.Type.DESKTOP);
this.tasks = tasks;
}
@Override
public boolean containsTask(int taskId) {
for (Task task : tasks) {
if (task.key.id == taskId) {
return true;
}
}
return false;
}
@Override
public boolean hasMultipleTasks() {
return true;
}
@Override
@NonNull
public List<Task> getTasks() {
return tasks;
}
@Override
public DesktopTask copy() {
return new DesktopTask(tasks);
}
@Override
public String toString() {
return "type=" + taskViewType + " tasks=" + tasks;
}
}
@@ -1,94 +0,0 @@
/*
* 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.quickstep.util;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* A {@link Task} container that can contain one or two tasks, depending on if the two tasks
* are represented as an app-pair in the recents task list.
*/
public class GroupTask {
@NonNull
public final Task task1;
@Nullable
public final Task task2;
@Nullable
public final SplitBounds mSplitBounds;
@TaskView.Type
public final int taskViewType;
public GroupTask(@NonNull Task task) {
this(task, null, null);
}
public GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds) {
this(t1, t2, splitBounds, t2 != null ? TaskView.Type.GROUPED : TaskView.Type.SINGLE);
}
protected GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds,
@TaskView.Type int taskViewType) {
task1 = t1;
task2 = t2;
mSplitBounds = splitBounds;
this.taskViewType = taskViewType;
}
public boolean containsTask(int taskId) {
return task1.key.id == taskId || (task2 != null && task2.key.id == taskId);
}
public boolean hasMultipleTasks() {
return task2 != null;
}
/**
* Returns a List of all the Tasks in this GroupTask
*/
public List<Task> getTasks() {
if (task2 == null) {
return Collections.singletonList(task1);
} else {
return Arrays.asList(task1, task2);
}
}
/**
* Create a copy of this instance
*/
public GroupTask copy() {
return new GroupTask(
new Task(task1),
task2 != null ? new Task(task2) : null,
mSplitBounds);
}
@Override
public String toString() {
return "type=" + taskViewType + " task1=" + task1 + " task2=" + task2;
}
}
@@ -1,136 +0,0 @@
/*
* 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.quickstep.util;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import androidx.annotation.IntDef;
import com.android.launcher3.util.IntArray;
import java.lang.annotation.Retention;
/**
* Helper class for navigating RecentsView grid tasks via arrow keys and tab.
*/
public class TaskGridNavHelper {
public static final int CLEAR_ALL_PLACEHOLDER_ID = -1;
public static final int INVALID_FOCUSED_TASK_ID = -1;
public static final int DIRECTION_UP = 0;
public static final int DIRECTION_DOWN = 1;
public static final int DIRECTION_LEFT = 2;
public static final int DIRECTION_RIGHT = 3;
public static final int DIRECTION_TAB = 4;
@Retention(SOURCE)
@IntDef({DIRECTION_UP, DIRECTION_DOWN, DIRECTION_LEFT, DIRECTION_RIGHT, DIRECTION_TAB})
public @interface TASK_NAV_DIRECTION {}
private final IntArray mOriginalTopRowIds;
private IntArray mTopRowIds;
private IntArray mBottomRowIds;
private final int mFocusedTaskId;
public TaskGridNavHelper(IntArray topIds, IntArray bottomIds, int focusedTaskId) {
mFocusedTaskId = focusedTaskId;
mOriginalTopRowIds = topIds.clone();
generateTaskViewIdGrid(topIds, bottomIds);
}
private void generateTaskViewIdGrid(IntArray topRowIdArray, IntArray bottomRowIdArray) {
boolean hasFocusedTask = mFocusedTaskId != INVALID_FOCUSED_TASK_ID;
int maxSize =
Math.max(topRowIdArray.size(), bottomRowIdArray.size()) + (hasFocusedTask ? 1 : 0);
int minSize =
Math.min(topRowIdArray.size(), bottomRowIdArray.size()) + (hasFocusedTask ? 1 : 0);
// Add the focused task to the beginning of both arrays if it exists.
if (hasFocusedTask) {
topRowIdArray.add(0, mFocusedTaskId);
bottomRowIdArray.add(0, mFocusedTaskId);
}
// Fill in the shorter array with the ids from the longer one.
for (int i = minSize; i < maxSize; i++) {
if (i >= topRowIdArray.size()) {
topRowIdArray.add(bottomRowIdArray.get(i));
} else {
bottomRowIdArray.add(topRowIdArray.get(i));
}
}
// Add the clear all button to the end of both arrays
topRowIdArray.add(CLEAR_ALL_PLACEHOLDER_ID);
bottomRowIdArray.add(CLEAR_ALL_PLACEHOLDER_ID);
mTopRowIds = topRowIdArray;
mBottomRowIds = bottomRowIdArray;
}
/**
* Returns the id of the next page in the grid or -1 for the clear all button.
*/
public int getNextGridPage(int currentPageTaskViewId, int delta,
@TASK_NAV_DIRECTION int direction, boolean cycle) {
boolean inTop = mTopRowIds.contains(currentPageTaskViewId);
int index = inTop ? mTopRowIds.indexOf(currentPageTaskViewId)
: mBottomRowIds.indexOf(currentPageTaskViewId);
int maxSize = Math.max(mTopRowIds.size(), mBottomRowIds.size());
int nextIndex = index + delta;
switch (direction) {
case DIRECTION_UP:
case DIRECTION_DOWN: {
return inTop ? mBottomRowIds.get(index) : mTopRowIds.get(index);
}
case DIRECTION_LEFT: {
int boundedIndex = cycle ? nextIndex % maxSize : Math.min(nextIndex, maxSize - 1);
return inTop ? mTopRowIds.get(boundedIndex)
: mBottomRowIds.get(boundedIndex);
}
case DIRECTION_RIGHT: {
int boundedIndex =
cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex) : Math.max(
nextIndex, 0);
boolean inOriginalTop = mOriginalTopRowIds.contains(currentPageTaskViewId);
return inOriginalTop ? mTopRowIds.get(boundedIndex)
: mBottomRowIds.get(boundedIndex);
}
case DIRECTION_TAB: {
int boundedIndex =
cycle ? nextIndex < 0 ? maxSize - 1 : nextIndex % maxSize : Math.min(
nextIndex, maxSize - 1);
if (delta >= 0) {
return inTop && mTopRowIds.get(index) != mBottomRowIds.get(index)
? mBottomRowIds.get(index)
: mTopRowIds.get(boundedIndex);
} else {
if (mTopRowIds.contains(currentPageTaskViewId)) {
return mBottomRowIds.get(boundedIndex);
} else {
// Go up to top if there is task above
return mTopRowIds.get(index) != mBottomRowIds.get(index)
? mTopRowIds.get(index)
: mBottomRowIds.get(boundedIndex);
}
}
}
default:
return currentPageTaskViewId;
}
}
}
@@ -1,345 +0,0 @@
/*
* Copyright (C) 2018 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.quickstep.views;
import static com.android.launcher3.Flags.enableGridOnlyOverview;
import static com.android.quickstep.util.BorderAnimator.DEFAULT_BORDER_COLOR;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
import com.android.launcher3.R;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.quickstep.util.BorderAnimator;
import kotlin.Unit;
import app.lawnchair.font.FontManager;
import app.lawnchair.theme.drawable.DrawableTokens;
public class ClearAllButton extends Button {
public static final FloatProperty<ClearAllButton> VISIBILITY_ALPHA = new FloatProperty<ClearAllButton>(
"visibilityAlpha") {
@Override
public Float get(ClearAllButton view) {
return view.mVisibilityAlpha;
}
@Override
public void setValue(ClearAllButton view, float v) {
view.setVisibilityAlpha(v);
}
};
public static final FloatProperty<ClearAllButton> DISMISS_ALPHA = new FloatProperty<ClearAllButton>(
"dismissAlpha") {
@Override
public Float get(ClearAllButton view) {
return view.mDismissAlpha;
}
@Override
public void setValue(ClearAllButton view, float v) {
view.setDismissAlpha(v);
}
};
private final RecentsViewContainer mContainer;
private float mScrollAlpha = 1;
private float mContentAlpha = 1;
private float mVisibilityAlpha = 1;
private float mDismissAlpha = 1;
private float mFullscreenProgress = 1;
private float mGridProgress = 1;
private boolean mIsRtl;
private float mNormalTranslationPrimary;
private float mFullscreenTranslationPrimary;
private float mGridTranslationPrimary;
private float mGridScrollOffset;
private float mScrollOffsetPrimary;
private int mSidePadding;
private int mOutlinePadding;
private boolean mBorderEnabled;
@Nullable
private final BorderAnimator mFocusBorderAnimator;
public ClearAllButton(Context context, AttributeSet attrs) {
super(context, attrs);
mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
mContainer = RecentsViewContainer.containerFromContext(context);
FontManager.INSTANCE.get(context).overrideFont(this, attrs);
setBackground(DrawableTokens.BgOverviewClearAllButton.resolve(context));
if (Flags.enableFocusOutline()) {
TypedArray styledAttrs = context.obtainStyledAttributes(attrs,
R.styleable.ClearAllButton);
Resources resources = getResources();
mOutlinePadding = resources.getDimensionPixelSize(
R.dimen.recents_clear_all_outline_padding);
mFocusBorderAnimator = BorderAnimator.createSimpleBorderAnimator(
/* borderRadiusPx= */ resources.getDimensionPixelSize(
R.dimen.recents_clear_all_outline_radius),
/* borderWidthPx= */ context.getResources().getDimensionPixelSize(
R.dimen.keyboard_quick_switch_border_width),
/* boundsBuilder= */ this::updateBorderBounds,
/* targetView= */ this,
/* borderColor= */ styledAttrs.getColor(
R.styleable.ClearAllButton_focusBorderColor,
DEFAULT_BORDER_COLOR));
styledAttrs.recycle();
} else {
mFocusBorderAnimator = null;
}
}
private Unit updateBorderBounds(@NonNull Rect bounds) {
bounds.set(0, 0, getWidth(), getHeight());
// Make the value negative to form a padding between button and outline
bounds.inset(-mOutlinePadding, -mOutlinePadding);
return Unit.INSTANCE;
}
@Override
public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
if (mFocusBorderAnimator != null && mBorderEnabled) {
mFocusBorderAnimator.setBorderVisibility(gainFocus, /* animated= */ true);
}
}
/**
* Enable or disable showing border on focus change
*/
public void setBorderEnabled(boolean enabled) {
if (mBorderEnabled == enabled) {
return;
}
mBorderEnabled = enabled;
if (mFocusBorderAnimator != null) {
mFocusBorderAnimator.setBorderVisibility(/* visible= */
enabled && isFocused(), /* animated= */true);
}
}
@Override
public void draw(Canvas canvas) {
if (mFocusBorderAnimator != null) {
mFocusBorderAnimator.drawBorder(canvas);
}
super.draw(canvas);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
RecentsPagedOrientationHandler orientationHandler = getRecentsView().getPagedOrientationHandler();
mSidePadding = orientationHandler.getClearAllSidePadding(getRecentsView(), mIsRtl);
}
private RecentsView getRecentsView() {
return (RecentsView) getParent();
}
@Override
public void onRtlPropertiesChanged(int layoutDirection) {
super.onRtlPropertiesChanged(layoutDirection);
mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
public float getScrollAlpha() {
return mScrollAlpha;
}
public void setContentAlpha(float alpha) {
if (mContentAlpha != alpha) {
mContentAlpha = alpha;
updateAlpha();
}
}
public void setVisibilityAlpha(float alpha) {
if (mVisibilityAlpha != alpha) {
mVisibilityAlpha = alpha;
updateAlpha();
}
}
public void setDismissAlpha(float alpha) {
if (mDismissAlpha != alpha) {
mDismissAlpha = alpha;
updateAlpha();
}
}
public void onRecentsViewScroll(int scroll, boolean gridEnabled) {
RecentsView recentsView = getRecentsView();
if (recentsView == null) {
return;
}
RecentsPagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
float orientationSize = orientationHandler.getPrimaryValue(getWidth(), getHeight());
if (orientationSize == 0) {
return;
}
int clearAllScroll = recentsView.getClearAllScroll();
int adjustedScrollFromEdge = Math.abs(scroll - clearAllScroll);
float shift = Math.min(adjustedScrollFromEdge, orientationSize);
mNormalTranslationPrimary = mIsRtl ? -shift : shift;
if (!gridEnabled) {
mNormalTranslationPrimary += mSidePadding;
}
applyPrimaryTranslation();
applySecondaryTranslation();
float clearAllSpacing = recentsView.getPageSpacing() + recentsView.getClearAllExtraPageSpacing();
clearAllSpacing = mIsRtl ? -clearAllSpacing : clearAllSpacing;
mScrollAlpha = Math.max((clearAllScroll + clearAllSpacing - scroll) / clearAllSpacing, 0);
updateAlpha();
}
private void updateAlpha() {
final float alpha = mScrollAlpha * mContentAlpha * mVisibilityAlpha * mDismissAlpha;
setAlpha(alpha);
setClickable(Math.min(alpha, 1) == 1);
}
public void setFullscreenTranslationPrimary(float fullscreenTranslationPrimary) {
mFullscreenTranslationPrimary = fullscreenTranslationPrimary;
applyPrimaryTranslation();
}
public void setGridTranslationPrimary(float gridTranslationPrimary) {
mGridTranslationPrimary = gridTranslationPrimary;
applyPrimaryTranslation();
}
public void setGridScrollOffset(float gridScrollOffset) {
mGridScrollOffset = gridScrollOffset;
}
public void setScrollOffsetPrimary(float scrollOffsetPrimary) {
mScrollOffsetPrimary = scrollOffsetPrimary;
}
public float getScrollAdjustment(boolean fullscreenEnabled, boolean gridEnabled) {
float scrollAdjustment = 0;
if (fullscreenEnabled) {
scrollAdjustment += mFullscreenTranslationPrimary;
}
if (gridEnabled) {
scrollAdjustment += mGridTranslationPrimary + mGridScrollOffset;
}
scrollAdjustment += mScrollOffsetPrimary;
return scrollAdjustment;
}
public float getOffsetAdjustment(boolean fullscreenEnabled, boolean gridEnabled) {
return getScrollAdjustment(fullscreenEnabled, gridEnabled);
}
/**
* Adjust translation when this TaskView is about to be shown fullscreen.
*
* @param progress: 0 = no translation; 1 = translate according to TaskVIew
* translations.
*/
public void setFullscreenProgress(float progress) {
mFullscreenProgress = progress;
applyPrimaryTranslation();
}
/**
* Moves ClearAllButton between carousel and 2 row grid.
*
* @param gridProgress 0 = carousel; 1 = 2 row grid.
*/
public void setGridProgress(float gridProgress) {
mGridProgress = gridProgress;
applyPrimaryTranslation();
}
private void applyPrimaryTranslation() {
RecentsView recentsView = getRecentsView();
if (recentsView == null) {
return;
}
RecentsPagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
orientationHandler.getPrimaryViewTranslate().set(this,
orientationHandler.getPrimaryValue(0f, getOriginalTranslationY())
+ mNormalTranslationPrimary + getFullscreenTrans(
mFullscreenTranslationPrimary)
+ getGridTrans(mGridTranslationPrimary));
}
private void applySecondaryTranslation() {
RecentsView recentsView = getRecentsView();
if (recentsView == null) {
return;
}
RecentsPagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
orientationHandler.getSecondaryViewTranslate().set(this,
orientationHandler.getSecondaryValue(0f, getOriginalTranslationY()));
}
private float getFullscreenTrans(float endTranslation) {
return mFullscreenProgress > 0 ? endTranslation : 0;
}
private float getGridTrans(float endTranslation) {
return mGridProgress > 0 ? endTranslation : 0;
}
/**
* Get the Y translation that is set in the original layout position, before
* scrolling.
*/
private float getOriginalTranslationY() {
DeviceProfile deviceProfile = mContainer.getDeviceProfile();
if (deviceProfile.isTablet) {
if (enableGridOnlyOverview()) {
return (getRecentsView().getLastComputedTaskSize().height()
+ deviceProfile.overviewTaskThumbnailTopMarginPx) / 2.0f
+ deviceProfile.overviewRowSpacing;
} else {
return deviceProfile.overviewRowSpacing;
}
}
return deviceProfile.overviewTaskThumbnailTopMarginPx / 2.0f;
}
}
@@ -1,443 +0,0 @@
/*
* Copyright (C) 2018 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.quickstep.views;
import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS;
import static com.android.launcher3.Utilities.prefixTextWithIcon;
import static com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR;
import android.app.ActivityOptions;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.pm.LauncherApps.AppUsageLimit;
import android.graphics.Outline;
import android.graphics.Paint;
import android.icu.text.MeasureFormat;
import android.icu.text.MeasureFormat.FormatWidth;
import android.icu.util.Measure;
import android.icu.util.MeasureUnit;
import android.os.UserHandle;
import android.util.Log;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
import com.android.quickstep.TaskUtils;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.systemui.shared.recents.model.Task;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Duration;
import java.util.Locale;
public final class DigitalWellBeingToast {
private static final float THRESHOLD_LEFT_ICON_ONLY = 0.4f;
private static final float THRESHOLD_RIGHT_ICON_ONLY = 0.6f;
/** Will span entire width of taskView with full text */
private static final int SPLIT_BANNER_FULLSCREEN = 0;
/** Used for grid task view, only showing icon and time */
private static final int SPLIT_GRID_BANNER_LARGE = 1;
/** Used for grid task view, only showing icon */
private static final int SPLIT_GRID_BANNER_SMALL = 2;
@IntDef(value = {
SPLIT_BANNER_FULLSCREEN,
SPLIT_GRID_BANNER_LARGE,
SPLIT_GRID_BANNER_SMALL,
})
@Retention(RetentionPolicy.SOURCE)
@interface SplitBannerConfig {
}
static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
static final int MINUTE_MS = 60000;
private static final String TAG = "DigitalWellBeingToast";
private final RecentsViewContainer mContainer;
private final TaskView mTaskView;
private final LauncherApps mLauncherApps;
private final int mBannerHeight;
private Task mTask;
private boolean mHasLimit;
private long mAppRemainingTimeMs;
@Nullable
private View mBanner;
private ViewOutlineProvider mOldBannerOutlineProvider;
private float mBannerOffsetPercentage;
@Nullable
private SplitBounds mSplitBounds;
private float mSplitOffsetTranslationY;
private float mSplitOffsetTranslationX;
private boolean mIsDestroyed = false;
public DigitalWellBeingToast(RecentsViewContainer container, TaskView taskView) {
mContainer = container;
mTaskView = taskView;
mLauncherApps = container.asContext().getSystemService(LauncherApps.class);
mBannerHeight = container.asContext().getResources().getDimensionPixelSize(
R.dimen.digital_wellbeing_toast_height);
}
private void setNoLimit() {
mHasLimit = false;
mTaskView.setContentDescription(mTask.titleDescription);
replaceBanner(null);
mAppRemainingTimeMs = -1;
}
private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) {
mAppRemainingTimeMs = appRemainingTimeMs;
mHasLimit = true;
TextView toast = mContainer.getViewCache().getView(R.layout.digital_wellbeing_toast,
mContainer.asContext(), mTaskView);
toast.setText(prefixTextWithIcon(mContainer.asContext(), R.drawable.ic_hourglass_top,
getText()));
toast.setOnClickListener(this::openAppUsageSettings);
replaceBanner(toast);
mTaskView.setContentDescription(
getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs));
}
public String getText() {
return getText(mAppRemainingTimeMs, false /* forContentDesc */);
}
public boolean hasLimit() {
return mHasLimit;
}
public void initialize(Task task) {
if (mIsDestroyed) {
throw new IllegalStateException("Cannot re-initialize a destroyed toast");
}
mTask = task;
ORDERED_BG_EXECUTOR.execute(() -> {
AppUsageLimit usageLimit = null;
try {
usageLimit = mLauncherApps.getAppUsageLimit(
mTask.getTopComponent().getPackageName(),
UserHandle.of(mTask.key.userId));
} catch (Exception e) {
Log.e(TAG, "Error initializing digital well being toast", e);
}
final long appUsageLimitTimeMs = usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
final long appRemainingTimeMs = usageLimit != null ? usageLimit.getUsageRemaining() : -1;
mTaskView.post(() -> {
if (mIsDestroyed) {
return;
}
if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
setNoLimit();
} else {
setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
}
});
});
}
/**
* Mark the DWB toast as destroyed and remove banner from TaskView.
*/
public void destroy() {
mIsDestroyed = true;
mTaskView.post(() -> replaceBanner(null));
}
public void setSplitBounds(@Nullable SplitBounds splitBounds) {
mSplitBounds = splitBounds;
}
private @SplitBannerConfig int getSplitBannerConfig() {
if (mSplitBounds == null
|| !mContainer.getDeviceProfile().isTablet
|| mTaskView.isFocusedTask()) {
return SPLIT_BANNER_FULLSCREEN;
}
// For portrait grid only height of task changes, not width. So we keep the text
// the same
if (!mContainer.getDeviceProfile().isLeftRightSplit) {
return SPLIT_GRID_BANNER_LARGE;
}
// For landscape grid, for 30% width we only show icon, otherwise show icon and
// time
if (mTask.key.id == mSplitBounds.leftTopTaskId) {
return mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY
? SPLIT_GRID_BANNER_SMALL
: SPLIT_GRID_BANNER_LARGE;
} else {
return mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY
? SPLIT_GRID_BANNER_SMALL
: SPLIT_GRID_BANNER_LARGE;
}
}
private String getReadableDuration(
Duration duration,
@StringRes int durationLessThanOneMinuteStringId) {
int hours = Math.toIntExact(duration.toHours());
int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes());
// Apply FormatWidth.WIDE if both the hour part and the minute part are
// non-zero.
if (hours > 0 && minutes > 0) {
return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.NARROW)
.formatMeasures(
new Measure(hours, MeasureUnit.HOUR),
new Measure(minutes, MeasureUnit.MINUTE));
}
// Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced).
if (hours > 0) {
return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
new Measure(hours, MeasureUnit.HOUR));
}
// Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced).
if (minutes > 0) {
return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
new Measure(minutes, MeasureUnit.MINUTE));
}
// Use a specific string for usage less than one minute but non-zero.
if (duration.compareTo(Duration.ZERO) > 0) {
return mContainer.asContext().getString(durationLessThanOneMinuteStringId);
}
// Otherwise, return 0-minute string.
return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
new Measure(0, MeasureUnit.MINUTE));
}
/**
* Returns text to show for the banner depending on
* {@link #getSplitBannerConfig()}
* If {@param forContentDesc} is {@code true}, this will always return the full
* string corresponding to {@link #SPLIT_BANNER_FULLSCREEN}
*/
private String getText(long remainingTime, boolean forContentDesc) {
final Duration duration = Duration.ofMillis(
remainingTime > MINUTE_MS ? (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS : remainingTime);
String readableDuration = getReadableDuration(duration,
R.string.shorter_duration_less_than_one_minute
/* forceFormatWidth */);
@SplitBannerConfig
int splitBannerConfig = getSplitBannerConfig();
if (forContentDesc || splitBannerConfig == SPLIT_BANNER_FULLSCREEN) {
return mContainer.asContext().getString(
R.string.time_left_for_app,
readableDuration);
}
if (splitBannerConfig == SPLIT_GRID_BANNER_SMALL) {
// show no text
return "";
} else { // SPLIT_GRID_BANNER_LARGE
// only show time
return readableDuration;
}
}
public void openAppUsageSettings(View view) {
final Intent intent = new Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
.putExtra(Intent.EXTRA_PACKAGE_NAME,
mTask.getTopComponent().getPackageName())
.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
try {
final RecentsViewContainer container = RecentsViewContainer.containerFromContext(view.getContext());
final ActivityOptions options = ActivityOptions.makeScaleUpAnimation(
view, 0, 0,
view.getWidth(), view.getHeight());
container.asContext().startActivity(intent, options.toBundle());
// TODO: add WW logging on the app usage settings click.
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Failed to open app usage settings for task "
+ mTask.getTopComponent().getPackageName(), e);
}
}
private String getContentDescriptionForTask(
Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) {
return appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0 ? mContainer.asContext().getString(
R.string.task_contents_description_with_remaining_time,
task.titleDescription,
getText(appRemainingTimeMs, true /* forContentDesc */)) : task.titleDescription;
}
private void replaceBanner(@Nullable View view) {
resetOldBanner();
setBanner(view);
}
private void resetOldBanner() {
if (mBanner != null) {
mBanner.setOutlineProvider(mOldBannerOutlineProvider);
mTaskView.removeView(mBanner);
mBanner.setOnClickListener(null);
mContainer.getViewCache().recycleView(R.layout.digital_wellbeing_toast, mBanner);
}
}
private void setBanner(@Nullable View view) {
mBanner = view;
if (mBanner != null && mTaskView.getRecentsView() != null) {
setupAndAddBanner();
setBannerOutline();
}
}
private void setupAndAddBanner() {
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mBanner.getLayoutParams();
DeviceProfile deviceProfile = mContainer.getDeviceProfile();
layoutParams.bottomMargin = ((ViewGroup.MarginLayoutParams) mTaskView.getFirstThumbnailViewDeprecated()
.getLayoutParams()).bottomMargin;
RecentsPagedOrientationHandler orientationHandler = mTaskView.getPagedOrientationHandler();
Pair<Float, Float> translations = orientationHandler
.getDwbLayoutTranslations(mTaskView.getMeasuredWidth(),
mTaskView.getMeasuredHeight(), mSplitBounds, deviceProfile,
mTaskView.getThumbnailViews(), mTask.key.id, mBanner);
mSplitOffsetTranslationX = translations.first;
mSplitOffsetTranslationY = translations.second;
updateTranslationY();
updateTranslationX();
mTaskView.addView(mBanner);
}
private void setBannerOutline() {
// TODO(b\273367585) to investigate why mBanner.getOutlineProvider() can be null
mOldBannerOutlineProvider = mBanner.getOutlineProvider() != null
? mBanner.getOutlineProvider()
: ViewOutlineProvider.BACKGROUND;
mBanner.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
mOldBannerOutlineProvider.getOutline(view, outline);
float verticalTranslation = -view.getTranslationY() + mSplitOffsetTranslationY;
outline.offset(0, Math.round(verticalTranslation));
}
});
mBanner.setClipToOutline(true);
}
void updateBannerOffset(float offsetPercentage) {
if (mBannerOffsetPercentage != offsetPercentage) {
mBannerOffsetPercentage = offsetPercentage;
if (mBanner != null) {
updateTranslationY();
mBanner.invalidateOutline();
}
}
}
private void updateTranslationY() {
if (mBanner == null) {
return;
}
mBanner.setTranslationY(
(mBannerOffsetPercentage * mBannerHeight) + mSplitOffsetTranslationY);
}
private void updateTranslationX() {
if (mBanner == null) {
return;
}
mBanner.setTranslationX(mSplitOffsetTranslationX);
}
void setBannerColorTint(int color, float amount) {
if (mBanner == null) {
return;
}
if (amount == 0) {
mBanner.setLayerType(View.LAYER_TYPE_NONE, null);
}
Paint layerPaint = new Paint();
layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
mBanner.setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint);
mBanner.setLayerPaint(layerPaint);
}
void setBannerVisibility(int visibility) {
if (mBanner == null) {
return;
}
mBanner.setVisibility(visibility);
}
private int getAccessibilityActionId() {
return (mSplitBounds != null
&& mSplitBounds.rightBottomTaskId == mTask.key.id)
? R.id.action_digital_wellbeing_bottom_right
: R.id.action_digital_wellbeing_top_left;
}
@Nullable
public AccessibilityNodeInfo.AccessibilityAction getDWBAccessibilityAction() {
if (!hasLimit()) {
return null;
}
Context context = mContainer.asContext();
String label = (mTaskView.containsMultipleTasks())
? context.getString(
R.string.split_app_usage_settings,
TaskUtils.getTitle(context, mTask))
: context.getString(R.string.accessibility_app_usage_settings);
return new AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label);
}
public boolean handleAccessibilityAction(int action) {
if (getAccessibilityActionId() == action) {
openAppUsageSettings(mTaskView);
return true;
} else {
return false;
}
}
}
@@ -1,464 +0,0 @@
/*
* 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.quickstep.views;
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.RectEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewOutlineProvider;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.quickstep.util.RecentsOrientedState;
/**
* An icon app menu view which can be used in place of an IconView in overview TaskViews.
*/
public class IconAppChipView extends FrameLayout implements TaskViewIcon {
private static final int MENU_BACKGROUND_REVEAL_DURATION = 417;
private static final int MENU_BACKGROUND_HIDE_DURATION = 333;
private static final int NUM_ALPHA_CHANNELS = 3;
private static final int INDEX_CONTENT_ALPHA = 0;
private static final int INDEX_COLOR_FILTER_ALPHA = 1;
private static final int INDEX_MODAL_ALPHA = 2;
private final MultiValueAlpha mMultiValueAlpha;
private View mMenuAnchorView;
private IconView mIconView;
// Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name.
private TextView mIconTextCollapsedView;
private TextView mIconTextExpandedView;
private ImageView mIconArrowView;
private final Rect mBackgroundRelativeLtrLocation = new Rect();
final RectEvaluator mBackgroundAnimationRectEvaluator =
new RectEvaluator(mBackgroundRelativeLtrLocation);
private final int mCollapsedMenuDefaultWidth;
private final int mExpandedMenuDefaultWidth;
private final int mCollapsedMenuDefaultHeight;
private final int mExpandedMenuDefaultHeight;
private final int mIconMenuMarginTopStart;
private final int mMenuToChipGap;
private final int mBackgroundMarginTopStart;
private final int mAppNameHorizontalMargin;
private final int mIconViewMarginStart;
private final int mAppIconSize;
private final int mArrowSize;
private final int mIconViewDrawableExpandedSize;
private final int mArrowMarginEnd;
private AnimatorSet mAnimator;
private int mMaxWidth = Integer.MAX_VALUE;
private static final int INDEX_SPLIT_TRANSLATION = 0;
private static final int INDEX_MENU_TRANSLATION = 1;
private static final int INDEX_COUNT_TRANSLATION = 2;
private final MultiPropertyFactory<View> mViewTranslationX;
private final MultiPropertyFactory<View> mViewTranslationY;
/**
* Gets the view split x-axis translation
*/
public MultiPropertyFactory<View>.MultiProperty getSplitTranslationX() {
return mViewTranslationX.get(INDEX_SPLIT_TRANSLATION);
}
/**
* Sets the view split x-axis translation
* @param translationX x-axis translation
*/
public void setSplitTranslationX(float translationX) {
getSplitTranslationX().setValue(translationX);
}
/**
* Gets the view split y-axis translation
*/
public MultiPropertyFactory<View>.MultiProperty getSplitTranslationY() {
return mViewTranslationY.get(INDEX_SPLIT_TRANSLATION);
}
/**
* Sets the view split y-axis translation
* @param translationY y-axis translation
*/
public void setSplitTranslationY(float translationY) {
getSplitTranslationY().setValue(translationY);
}
/**
* Gets the menu x-axis translation for split task
*/
public MultiPropertyFactory<View>.MultiProperty getMenuTranslationX() {
return mViewTranslationX.get(INDEX_MENU_TRANSLATION);
}
/**
* Gets the menu y-axis translation for split task
*/
public MultiPropertyFactory<View>.MultiProperty getMenuTranslationY() {
return mViewTranslationY.get(INDEX_MENU_TRANSLATION);
}
public IconAppChipView(Context context) {
this(context, null);
}
public IconAppChipView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public IconAppChipView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public IconAppChipView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
Resources res = getResources();
mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS);
mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true);
// Menu dimensions
mCollapsedMenuDefaultWidth =
res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width);
mExpandedMenuDefaultWidth =
res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width);
mCollapsedMenuDefaultHeight =
res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height);
mExpandedMenuDefaultHeight =
res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height);
mIconMenuMarginTopStart = res.getDimensionPixelSize(
R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin);
mMenuToChipGap = res.getDimensionPixelSize(
R.dimen.task_thumbnail_icon_menu_expanded_gap);
// Background dimensions
mBackgroundMarginTopStart = res.getDimensionPixelSize(
R.dimen.task_thumbnail_icon_menu_background_margin_top_start);
// Contents dimensions
mAppNameHorizontalMargin = res.getDimensionPixelSize(
R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed);
mArrowMarginEnd = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin);
mIconViewMarginStart = res.getDimensionPixelSize(
R.dimen.task_thumbnail_icon_view_start_margin);
mAppIconSize = res.getDimensionPixelSize(
R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size);
mArrowSize = res.getDimensionPixelSize(
R.dimen.task_thumbnail_icon_menu_arrow_size);
mIconViewDrawableExpandedSize = res.getDimensionPixelSize(
R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size);
mViewTranslationX = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_X,
INDEX_COUNT_TRANSLATION,
Float::sum);
mViewTranslationY = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_Y,
INDEX_COUNT_TRANSLATION,
Float::sum);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mIconView = findViewById(R.id.icon_view);
mIconTextCollapsedView = findViewById(R.id.icon_text_collapsed);
mIconTextExpandedView = findViewById(R.id.icon_text_expanded);
mIconArrowView = findViewById(R.id.icon_arrow);
mMenuAnchorView = findViewById(R.id.icon_view_menu_anchor);
}
protected IconView getIconView() {
return mIconView;
}
@Override
public void setText(CharSequence text) {
if (mIconTextCollapsedView != null) {
mIconTextCollapsedView.setText(text);
}
if (mIconTextExpandedView != null) {
mIconTextExpandedView.setText(text);
}
}
@Override
public Drawable getDrawable() {
return mIconView == null ? null : mIconView.getDrawable();
}
@Override
public void setDrawable(Drawable icon) {
if (mIconView != null) {
mIconView.setDrawable(icon);
}
}
@Override
public void setDrawableSize(int iconWidth, int iconHeight) {
if (mIconView != null) {
mIconView.setDrawableSize(iconWidth, iconHeight);
}
}
/**
* Sets the maximum width of this Icon Menu. This is usually used when space is limited for
* split screen.
*/
public void setMaxWidth(int maxWidth) {
// Width showing only the app icon and arrow. Max width should not be set to less than this.
int minimumMaxWidth = mIconViewMarginStart + mAppIconSize + mArrowSize + mArrowMarginEnd;
mMaxWidth = Math.max(maxWidth, minimumMaxWidth);
}
@Override
public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
RecentsPagedOrientationHandler orientationHandler =
orientationState.getOrientationHandler();
// Layout params for anchor view
LayoutParams anchorLayoutParams = (LayoutParams) mMenuAnchorView.getLayoutParams();
anchorLayoutParams.topMargin = mExpandedMenuDefaultHeight + mMenuToChipGap;
mMenuAnchorView.setLayoutParams(anchorLayoutParams);
// Layout Params for the Menu View (this)
LayoutParams iconMenuParams = (LayoutParams) getLayoutParams();
iconMenuParams.width = mExpandedMenuDefaultWidth;
iconMenuParams.height = mExpandedMenuDefaultHeight;
orientationHandler.setIconAppChipMenuParams(this, iconMenuParams, mIconMenuMarginTopStart,
mIconMenuMarginTopStart);
setLayoutParams(iconMenuParams);
// Layout params for the background
Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds();
mBackgroundRelativeLtrLocation.set(collapsedBackgroundBounds);
setOutlineProvider(new ViewOutlineProvider() {
final Rect mRtlAppliedOutlineBounds = new Rect();
@Override
public void getOutline(View view, Outline outline) {
mRtlAppliedOutlineBounds.set(mBackgroundRelativeLtrLocation);
if (isLayoutRtl()) {
int width = getWidth();
mRtlAppliedOutlineBounds.left = width - mBackgroundRelativeLtrLocation.right;
mRtlAppliedOutlineBounds.right = width - mBackgroundRelativeLtrLocation.left;
}
outline.setRoundRect(
mRtlAppliedOutlineBounds, mRtlAppliedOutlineBounds.height() / 2f);
}
});
// Layout Params for the Icon View
LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
int iconMarginStartRelativeToParent = mIconViewMarginStart + mBackgroundMarginTopStart;
orientationHandler.setIconAppChipChildrenParams(
iconParams, iconMarginStartRelativeToParent);
mIconView.setLayoutParams(iconParams);
mIconView.setDrawableSize(mAppIconSize, mAppIconSize);
// Layout Params for the collapsed Icon Text View
int textMarginStart =
iconMarginStartRelativeToParent + mAppIconSize + mAppNameHorizontalMargin;
LayoutParams iconTextCollapsedParams =
(LayoutParams) mIconTextCollapsedView.getLayoutParams();
orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart);
int collapsedTextWidth = collapsedBackgroundBounds.width() - mIconViewMarginStart
- mAppIconSize - mArrowSize - mAppNameHorizontalMargin - mArrowMarginEnd;
iconTextCollapsedParams.width = collapsedTextWidth;
mIconTextCollapsedView.setLayoutParams(iconTextCollapsedParams);
mIconTextCollapsedView.setAlpha(1f);
// Layout Params for the expanded Icon Text View
LayoutParams iconTextExpandedParams =
(LayoutParams) mIconTextExpandedView.getLayoutParams();
orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart);
mIconTextExpandedView.setLayoutParams(iconTextExpandedParams);
mIconTextExpandedView.setAlpha(0f);
mIconTextExpandedView.setRevealClip(true, 0, mAppIconSize / 2f, collapsedTextWidth);
// Layout Params for the Icon Arrow View
LayoutParams iconArrowParams = (LayoutParams) mIconArrowView.getLayoutParams();
int arrowMarginStart = collapsedBackgroundBounds.right - mArrowMarginEnd - mArrowSize;
orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart);
mIconArrowView.setPivotY(iconArrowParams.height / 2f);
mIconArrowView.setLayoutParams(iconArrowParams);
// This method is called twice sometimes (like when rotating split tasks). It is called
// once before onMeasure and onLayout, and again after onMeasure but before onLayout with
// a new width. This happens because we update widths on rotation and on measure of
// grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if
// it has just measured, so we explicitly call it here.
measure(MeasureSpec.makeMeasureSpec(getLayoutParams().width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getLayoutParams().height, MeasureSpec.EXACTLY));
}
@Override
public void setIconColorTint(int color, float amount) {
// RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu.
float colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, LINEAR);
mMultiValueAlpha.get(INDEX_COLOR_FILTER_ALPHA).setValue(colorTintAlpha);
}
@Override
public void setContentAlpha(float alpha) {
mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha);
}
@Override
public void setModalAlpha(float alpha) {
mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha);
}
@Override
public int getDrawableWidth() {
return mIconView == null ? 0 : mIconView.getDrawableWidth();
}
@Override
public int getDrawableHeight() {
return mIconView == null ? 0 : mIconView.getDrawableHeight();
}
protected void revealAnim(boolean isRevealing) {
cancelInProgressAnimations();
final Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds();
final Rect expandedBackgroundBounds = getExpandedBackgroundLtrBounds();
final Rect initialBackground = new Rect(mBackgroundRelativeLtrLocation);
mAnimator = new AnimatorSet();
if (isRevealing) {
boolean isRtl = isLayoutRtl();
bringToFront();
// Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
Animator expandedTextRevealAnim = ViewAnimationUtils.createCircularReveal(
mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
mIconTextCollapsedView.getWidth(), mIconTextExpandedView.getWidth());
// Animate background clipping
ValueAnimator backgroundAnimator = ValueAnimator.ofObject(
mBackgroundAnimationRectEvaluator,
initialBackground,
expandedBackgroundBounds);
backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline());
float iconViewScaling = mIconViewDrawableExpandedSize / (float) mAppIconSize;
float arrowTranslationX =
expandedBackgroundBounds.right - collapsedBackgroundBounds.right;
float iconCenterToTextCollapsed = mAppIconSize / 2f + mAppNameHorizontalMargin;
float iconCenterToTextExpanded =
mIconViewDrawableExpandedSize / 2f + mAppNameHorizontalMargin;
float textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed;
float textTranslationXWithRtl = isRtl ? -textTranslationX : textTranslationX;
float arrowTranslationWithRtl = isRtl ? -arrowTranslationX : arrowTranslationX;
mAnimator.playTogether(
expandedTextRevealAnim,
backgroundAnimator,
ObjectAnimator.ofFloat(mIconView, SCALE_X, iconViewScaling),
ObjectAnimator.ofFloat(mIconView, SCALE_Y, iconViewScaling),
ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X,
textTranslationXWithRtl),
ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X,
textTranslationXWithRtl),
ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 0),
ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 1),
ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, arrowTranslationWithRtl),
ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, -1));
mAnimator.setDuration(MENU_BACKGROUND_REVEAL_DURATION);
} else {
// Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
Animator expandedTextClipAnim = ViewAnimationUtils.createCircularReveal(
mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
mIconTextExpandedView.getWidth(), mIconTextCollapsedView.getWidth());
// Animate background clipping
ValueAnimator backgroundAnimator = ValueAnimator.ofObject(
mBackgroundAnimationRectEvaluator,
initialBackground,
collapsedBackgroundBounds);
backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline());
mAnimator.playTogether(
expandedTextClipAnim,
backgroundAnimator,
ObjectAnimator.ofFloat(mIconView, SCALE_PROPERTY, 1),
ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X, 0),
ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X, 0),
ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 1),
ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 0),
ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0),
ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, 1));
mAnimator.setDuration(MENU_BACKGROUND_HIDE_DURATION);
}
mAnimator.setInterpolator(EMPHASIZED);
mAnimator.start();
}
private Rect getCollapsedBackgroundLtrBounds() {
Rect bounds = new Rect(
0,
0,
Math.min(mMaxWidth, mCollapsedMenuDefaultWidth),
mCollapsedMenuDefaultHeight);
bounds.offset(mBackgroundMarginTopStart, mBackgroundMarginTopStart);
return bounds;
}
private Rect getExpandedBackgroundLtrBounds() {
return new Rect(0, 0, mExpandedMenuDefaultWidth, mExpandedMenuDefaultHeight);
}
private void cancelInProgressAnimations() {
// We null the `AnimatorSet` because it holds references to the `Animators` which aren't
// expecting to be mutable and will cause a crash if they are re-used.
if (mAnimator != null && mAnimator.isStarted()) {
mAnimator.cancel();
mAnimator = null;
}
}
@Override
public View asView() {
return this;
}
}
@@ -1,209 +0,0 @@
/*
* Copyright (C) 2018 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.quickstep.views;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Utilities;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.views.ActivityContext;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.quickstep.util.RecentsOrientedState;
/**
* A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
* when the drawable changes.
*/
public class IconView extends View implements TaskViewIcon {
private static final int NUM_ALPHA_CHANNELS = 2;
private static final int INDEX_CONTENT_ALPHA = 0;
private static final int INDEX_MODAL_ALPHA = 1;
private final MultiValueAlpha mMultiValueAlpha;
@Nullable
private Drawable mDrawable;
private int mDrawableWidth, mDrawableHeight;
public IconView(Context context) {
this(context, null);
}
public IconView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public IconView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public IconView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS);
mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true);
}
/**
* Sets a {@link Drawable} to be displayed.
*/
@Override
public void setDrawable(@Nullable Drawable d) {
if (mDrawable != null) {
mDrawable.setCallback(null);
}
mDrawable = d;
if (mDrawable != null) {
mDrawable.setCallback(this);
setDrawableSizeInternal(getWidth(), getHeight());
}
invalidate();
}
/**
* Sets the size of the icon drawable.
*/
@Override
public void setDrawableSize(int iconWidth, int iconHeight) {
mDrawableWidth = iconWidth;
mDrawableHeight = iconHeight;
if (mDrawable != null) {
setDrawableSizeInternal(getWidth(), getHeight());
}
}
private void setDrawableSizeInternal(int selfWidth, int selfHeight) {
Rect selfRect = new Rect(0, 0, selfWidth, selfHeight);
Rect drawableRect = new Rect();
Gravity.apply(Gravity.CENTER, mDrawableWidth, mDrawableHeight, selfRect, drawableRect);
mDrawable.setBounds(drawableRect);
}
@Override
@Nullable
public Drawable getDrawable() {
return mDrawable;
}
@Override
public int getDrawableWidth() {
return mDrawableWidth;
}
@Override
public int getDrawableHeight() {
return mDrawableHeight;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mDrawable != null) {
setDrawableSizeInternal(w, h);
}
}
@Override
protected boolean verifyDrawable(Drawable who) {
return super.verifyDrawable(who) || who == mDrawable;
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
final Drawable drawable = mDrawable;
if (drawable != null && drawable.isStateful()
&& drawable.setState(getDrawableState())) {
invalidateDrawable(drawable);
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mDrawable != null) {
mDrawable.draw(canvas);
}
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
@Override
public void setContentAlpha(float alpha) {
mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha);
}
@Override
public void setModalAlpha(float alpha) {
mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha);
}
/**
* Set the tint color of the icon, useful for scrimming or dimming.
*
* @param color to blend in.
* @param amount [0,1] 0 no tint, 1 full tint
*/
@Override
public void setIconColorTint(int color, float amount) {
if (mDrawable != null) {
mDrawable.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
}
}
@Override
public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
RecentsPagedOrientationHandler orientationHandler =
orientationState.getOrientationHandler();
boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
DeviceProfile deviceProfile =
ActivityContext.lookupContext(getContext()).getDeviceProfile();
FrameLayout.LayoutParams iconParams = (FrameLayout.LayoutParams) getLayoutParams();
int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
int taskIconHeight = deviceProfile.overviewTaskIconSizePx;
int taskMargin = deviceProfile.overviewTaskMarginPx;
orientationHandler.setTaskIconParams(iconParams, taskMargin, taskIconHeight,
thumbnailTopMargin, isRtl);
iconParams.width = iconParams.height = taskIconHeight;
setLayoutParams(iconParams);
setRotation(orientationHandler.getDegreesRotated());
int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
: deviceProfile.overviewTaskIconDrawableSizePx;
setDrawableSize(iconDrawableSize, iconDrawableSize);
}
@Override
public View asView() {
return this;
}
}
@@ -1,435 +0,0 @@
/*
* Copyright (C) 2018 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.quickstep.views;
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.launcher3.Flags.enableOverviewIconMenu;
import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.quickstep.views.TaskThumbnailViewDeprecated.DIM_ALPHA;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RectShape;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.android.app.animation.Interpolators;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.views.BaseDragLayer;
import com.android.quickstep.TaskOverlayFactory;
import com.android.quickstep.TaskUtils;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.quickstep.util.TaskCornerRadius;
import com.android.quickstep.views.TaskView.TaskContainer;
import app.lawnchair.theme.drawable.DrawableTokens;
/**
* Contains options for a recent task when long-pressing its icon.
*/
public class TaskMenuView extends AbstractFloatingView {
private static final Rect sTempRect = new Rect();
private static final int REVEAL_OPEN_DURATION = enableOverviewIconMenu() ? 417 : 150;
private static final int REVEAL_CLOSE_DURATION = enableOverviewIconMenu() ? 333 : 100;
private RecentsViewContainer mContainer;
private TextView mTaskName;
@Nullable
private AnimatorSet mOpenCloseAnimator;
@Nullable
private ValueAnimator mRevealAnimator;
@Nullable
private Runnable mOnClosingStartCallback;
private TaskView mTaskView;
private TaskContainer mTaskContainer;
private LinearLayout mOptionLayout;
private float mMenuTranslationYBeforeOpen;
private float mMenuTranslationXBeforeOpen;
public TaskMenuView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContainer = RecentsViewContainer.containerFromContext(context);
setClipToOutline(true);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mTaskName = findViewById(R.id.task_name);
mTaskName.setBackground(DrawableTokens.TaskMenuItemBg.resolve(getContext()));
mOptionLayout = findViewById(R.id.menu_option_layout);
}
@Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
BaseDragLayer dl = mContainer.getDragLayer();
if (!dl.isEventOverView(this, ev)) {
// TODO: log this once we have a new container type for it?
close(true);
return true;
}
}
return false;
}
@Override
protected void handleClose(boolean animate) {
animateClose();
}
@Override
protected boolean isOfType(int type) {
return (type & TYPE_TASK_MENU) != 0;
}
@Override
public ViewOutlineProvider getOutlineProvider() {
return new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(),
TaskCornerRadius.get(view.getContext()));
}
};
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!(enableOverviewIconMenu()
&& ((RecentsView) mContainer.getOverviewPanel()).isOnGridBottomRow(mTaskView))) {
// TODO(b/326952853): Cap menu height for grid bottom row in a way that doesn't
// break
// additionalTranslationY.
int maxMenuHeight = calculateMaxHeight();
if (MeasureSpec.getSize(heightMeasureSpec) > maxMenuHeight) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST);
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public void onRotationChanged() {
if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
mOpenCloseAnimator.end();
}
if (mIsOpen) {
mOptionLayout.removeAllViews();
if (enableOverviewIconMenu() || !populateAndLayoutMenu()) {
close(false);
}
}
}
/**
* Show a task menu for the given taskContainer.
*/
public static boolean showForTask(TaskContainer taskContainer,
@Nullable Runnable onClosingStartCallback) {
RecentsViewContainer container = RecentsViewContainer.containerFromContext(
taskContainer.getTaskView().getContext());
final TaskMenuView taskMenuView = (TaskMenuView) container.getLayoutInflater().inflate(
R.layout.task_menu, container.getDragLayer(), false);
taskMenuView.setOnClosingStartCallback(onClosingStartCallback);
return taskMenuView.populateAndShowForTask(taskContainer);
}
/**
* Show a task menu for the given taskContainer.
*/
public static boolean showForTask(TaskContainer taskContainer) {
return showForTask(taskContainer, null);
}
private boolean populateAndShowForTask(TaskContainer taskContainer) {
if (isAttachedToWindow()) {
return false;
}
mContainer.getDragLayer().addView(this);
mTaskView = taskContainer.getTaskView();
mTaskContainer = taskContainer;
if (!populateAndLayoutMenu()) {
return false;
}
post(this::animateOpen);
return true;
}
/**
* @return true if successfully able to populate task view menu, false otherwise
*/
private boolean populateAndLayoutMenu() {
addMenuOptions(mTaskContainer);
orientAroundTaskView(mTaskContainer);
return true;
}
private void addMenuOptions(TaskContainer taskContainer) {
if (enableOverviewIconMenu()) {
removeView(mTaskName);
} else {
mTaskName.setText(TaskUtils.getTitle(getContext(), taskContainer.getTask()));
mTaskName.setOnClickListener(v -> close(true));
}
TaskOverlayFactory.getEnabledShortcuts(mTaskView, taskContainer)
.forEach(this::addMenuOption);
}
private void addMenuOption(SystemShortcut menuOption) {
LinearLayout menuOptionView = (LinearLayout) mContainer.getLayoutInflater().inflate(
R.layout.task_view_menu_option, this, false);
if (enableOverviewIconMenu()) {
((GradientDrawable) menuOptionView.getBackground()).setCornerRadius(0);
}
menuOption.setIconAndLabelFor(
menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams();
mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp,
menuOptionView, mContainer.getDeviceProfile());
// Set an onClick listener on each menu option. The onClick method is
// responsible for
// ending LiveTile mode on the thumbnail if needed.
menuOptionView.setOnClickListener(menuOption::onClick);
mOptionLayout.addView(menuOptionView);
}
private void orientAroundTaskView(TaskContainer taskContainer) {
RecentsView recentsView = mContainer.getOverviewPanel();
RecentsPagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
// Get Position
DeviceProfile deviceProfile = mContainer.getDeviceProfile();
mContainer.getDragLayer().getDescendantRectRelativeToSelf(
enableOverviewIconMenu()
? getIconView().findViewById(R.id.icon_view_menu_anchor)
: taskContainer.getThumbnailViewDeprecated(),
sTempRect);
Rect insets = mContainer.getDragLayer().getInsets();
BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams();
params.width = orientationHandler.getTaskMenuWidth(
taskContainer.getThumbnailViewDeprecated(), deviceProfile,
taskContainer.getStagePosition());
// Gravity set to Left instead of Start as sTempRect.left measures Left distance
// not Start
params.gravity = Gravity.LEFT;
setLayoutParams(params);
setScaleX(mTaskView.getScaleX());
setScaleY(mTaskView.getScaleY());
// Set divider spacing
ShapeDrawable divider = new ShapeDrawable(new RectShape());
divider.getPaint().setColor(getResources().getColor(android.R.color.transparent));
int dividerSpacing = (int) getResources().getDimension(R.dimen.task_menu_spacing);
mOptionLayout.setShowDividers(
enableOverviewIconMenu() ? SHOW_DIVIDER_NONE : SHOW_DIVIDER_MIDDLE);
orientationHandler.setTaskOptionsMenuLayoutOrientation(
deviceProfile, mOptionLayout, dividerSpacing, divider);
float thumbnailAlignedX = sTempRect.left - insets.left;
float thumbnailAlignedY = sTempRect.top - insets.top;
// Changing pivot to make computations easier
// NOTE: Changing the pivots means the rotated view gets rotated about the new
// pivots set,
// which would render the X and Y position set here incorrect
setPivotX(0);
setPivotY(0);
setRotation(orientationHandler.getDegreesRotated());
if (enableOverviewIconMenu()) {
setTranslationX(thumbnailAlignedX);
setTranslationY(thumbnailAlignedY);
} else {
// Margin that insets the menuView inside the taskView
float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin);
setTranslationX(orientationHandler.getTaskMenuX(thumbnailAlignedX,
mTaskContainer.getThumbnailViewDeprecated(), deviceProfile, taskInsetMargin,
getIconView()));
setTranslationY(orientationHandler.getTaskMenuY(
thumbnailAlignedY, mTaskContainer.getThumbnailViewDeprecated(),
mTaskContainer.getStagePosition(), this, taskInsetMargin,
getIconView()));
}
}
private void animateOpen() {
mMenuTranslationYBeforeOpen = getTranslationY();
mMenuTranslationXBeforeOpen = getTranslationX();
animateOpenOrClosed(false);
mIsOpen = true;
}
private View getIconView() {
return mTaskContainer.getIconView().asView();
}
private void animateClose() {
animateOpenOrClosed(true);
}
private void animateOpenOrClosed(boolean closing) {
if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
mOpenCloseAnimator.cancel();
}
mOpenCloseAnimator = new AnimatorSet();
// If we're opening, we just start from the beginning as a new `TaskMenuView` is
// created
// each time we do the open animation so there will never be a partial value
// here.
float revealAnimationStartProgress = 0f;
if (closing && mRevealAnimator != null) {
revealAnimationStartProgress = 1f - mRevealAnimator.getAnimatedFraction();
}
mRevealAnimator = createOpenCloseOutlineProvider()
.createRevealAnimator(this, closing, revealAnimationStartProgress);
mRevealAnimator.setInterpolator(enableOverviewIconMenu() ? Interpolators.EMPHASIZED
: Interpolators.DECELERATE);
if (enableOverviewIconMenu()) {
IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView();
float additionalTranslationY = 0;
if (((RecentsView) mContainer.getOverviewPanel()).isOnGridBottomRow(mTaskView)) {
// Animate menu up for enough room to display full menu when task on bottom row.
float menuBottom = getHeight() + mMenuTranslationYBeforeOpen;
float taskBottom = mTaskView.getHeight() + mTaskView.getPersistentTranslationY();
float taskbarTop = mContainer.getDeviceProfile().heightPx
- mContainer.getDeviceProfile().getOverviewActionsClaimedSpaceBelow();
float midpoint = (taskBottom + taskbarTop) / 2f;
additionalTranslationY = -Math.max(menuBottom - midpoint, 0);
}
ObjectAnimator translationYAnim = ObjectAnimator.ofFloat(this, TRANSLATION_Y,
closing ? mMenuTranslationYBeforeOpen
: mMenuTranslationYBeforeOpen + additionalTranslationY);
translationYAnim.setInterpolator(EMPHASIZED);
ObjectAnimator menuTranslationYAnim = ObjectAnimator.ofFloat(
iconAppChip.getMenuTranslationY(),
MULTI_PROPERTY_VALUE, closing ? 0 : additionalTranslationY);
menuTranslationYAnim.setInterpolator(EMPHASIZED);
float additionalTranslationX = 0;
if (mContainer.getDeviceProfile().isLandscape
&& mTaskContainer.getStagePosition() == STAGE_POSITION_BOTTOM_OR_RIGHT) {
// Animate menu and icon when split task would display off the side of the
// screen.
additionalTranslationX = Math.max(
getTranslationX() + getWidth() - (mContainer.getDeviceProfile().widthPx
- getResources().getDimensionPixelSize(
R.dimen.task_menu_edge_padding) * 2),
0);
}
ObjectAnimator translationXAnim = ObjectAnimator.ofFloat(this, TRANSLATION_X,
closing ? mMenuTranslationXBeforeOpen
: mMenuTranslationXBeforeOpen - additionalTranslationX);
translationXAnim.setInterpolator(EMPHASIZED);
ObjectAnimator menuTranslationXAnim = ObjectAnimator.ofFloat(
iconAppChip.getMenuTranslationX(),
MULTI_PROPERTY_VALUE, closing ? 0 : -additionalTranslationX);
menuTranslationXAnim.setInterpolator(EMPHASIZED);
mOpenCloseAnimator.playTogether(translationYAnim, translationXAnim,
menuTranslationXAnim, menuTranslationYAnim);
}
mOpenCloseAnimator.playTogether(mRevealAnimator,
ObjectAnimator.ofFloat(
mTaskContainer.getThumbnailViewDeprecated(), DIM_ALPHA,
closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA),
ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1));
mOpenCloseAnimator.addListener(new AnimationSuccessListener() {
@Override
public void onAnimationStart(Animator animation) {
setVisibility(VISIBLE);
if (closing && mOnClosingStartCallback != null) {
mOnClosingStartCallback.run();
}
}
@Override
public void onAnimationSuccess(Animator animator) {
if (closing) {
closeComplete();
}
}
});
mOpenCloseAnimator.setDuration(closing ? REVEAL_CLOSE_DURATION : REVEAL_OPEN_DURATION);
mOpenCloseAnimator.start();
}
private void closeComplete() {
mIsOpen = false;
mContainer.getDragLayer().removeView(this);
mRevealAnimator = null;
}
private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
float radius = TaskCornerRadius.get(mContext);
Rect fromRect = new Rect(
enableOverviewIconMenu() && isLayoutRtl() ? getWidth() : 0,
0,
enableOverviewIconMenu() && !isLayoutRtl() ? 0 : getWidth(),
0);
Rect toRect = new Rect(0, 0, getWidth(), getHeight());
return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect);
}
/**
* Calculates max height based on how much space we have available.
* If not enough space then the view will scroll. The maximum menu size will sit
* inside the task
* with a margin on the top and bottom.
*/
private int calculateMaxHeight() {
float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin);
return mTaskView.getPagedOrientationHandler().getTaskMenuHeight(taskInsetMargin,
mContainer.getDeviceProfile(), getTranslationX(), getTranslationY());
}
private void setOnClosingStartCallback(Runnable onClosingStartCallback) {
mOnClosingStartCallback = onClosingStartCallback;
}
}
@@ -1,41 +0,0 @@
/*
* 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.quickstep.views
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import com.android.launcher3.R
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
class TaskThumbnailViewHeader
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) {
private val headerTitleView: TextView by lazy { findViewById(R.id.header_app_title) }
private val headerIconView: ImageView by lazy { findViewById(R.id.header_app_icon) }
private val headerCloseButton: ImageButton by lazy { findViewById(R.id.header_close_button) }
fun setHeader(header: ThumbnailHeader) {
headerTitleView.setText(header.title)
headerIconView.setImageDrawable(header.icon)
headerCloseButton.setOnClickListener(header.clickCloseListener)
}
}
@@ -1,99 +0,0 @@
/*
* 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.launcher3.taskbar.bubbles.testing
import android.app.Notification
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.PathParser
import android.view.LayoutInflater
import android.view.ViewGroup
import com.android.launcher3.R
import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
import com.android.launcher3.taskbar.bubbles.BubbleView
import com.android.wm.shell.shared.bubbles.BubbleInfo
object FakeBubbleViewFactory {
/** Inflates a [BubbleView] and adds it to the [parent] view if it is present. */
fun createBubble(
context: Context,
key: String,
parent: ViewGroup?,
iconSize: Int = 50,
iconColor: Int,
badgeColor: Int = Color.RED,
dotColor: Int = Color.BLUE,
suppressNotification: Boolean = false,
): BubbleView {
val inflater = LayoutInflater.from(context)
// BubbleView uses launcher's badge to icon ratio and expects the badge image to already
// have the right size
val badgeToIconRatio = 0.444f
val badgeRadius = iconSize * badgeToIconRatio / 2
val icon = createCircleBitmap(radius = iconSize / 2, color = iconColor)
val badge = createCircleBitmap(radius = badgeRadius.toInt(), color = badgeColor)
val flags =
if (suppressNotification) Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION else 0
val bubbleInfo =
BubbleInfo(
key,
flags,
null,
null,
0,
context.packageName,
null,
null,
false,
true,
null,
)
val bubbleView = inflater.inflate(R.layout.bubblebar_item_view, parent, false) as BubbleView
val dotPath =
PathParser.createPathFromPathData(
context.resources.getString(com.android.internal.R.string.config_icon_mask)
)
val bubble =
BubbleBarBubble(
bubbleInfo,
bubbleView,
badge,
icon,
dotColor,
dotPath,
"test app",
null,
)
bubbleView.setBubble(bubble)
return bubbleView
}
private fun createCircleBitmap(radius: Int, color: Int): Bitmap {
val bitmap = Bitmap.createBitmap(radius * 2, radius * 2, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawARGB(0, 0, 0, 0)
val paint = Paint()
paint.color = color
canvas.drawCircle(radius.toFloat(), radius.toFloat(), radius.toFloat(), paint)
return bitmap
}
}
@@ -1,164 +0,0 @@
/*
* 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.launcher3.taskbar
import android.app.Instrumentation
import android.app.PendingIntent
import android.content.IIntentSender
import android.content.Intent
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ServiceTestRule
import com.android.launcher3.LauncherAppState
import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks
import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric
import com.android.quickstep.AllAppsActionManager
import com.android.quickstep.TouchInteractionService
import com.android.quickstep.TouchInteractionService.TISBinder
import org.junit.Assume.assumeTrue
import org.junit.rules.MethodRule
import org.junit.runners.model.FrameworkMethod
import org.junit.runners.model.Statement
/**
* Manages the Taskbar lifecycle for unit tests.
*
* See [InjectController] for grabbing controller(s) under test with minimal boilerplate.
*
* The rule interacts with [TaskbarManager] on the main thread. A good rule of thumb for tests is
* that code that is executed on the main thread in production should also happen on that thread
* when tested.
*
* `@UiThreadTest` is a simple way to run an entire test body on the main thread. But if a test
* executes code that appends message(s) to the main thread's `MessageQueue`, the annotation will
* prevent those messages from being processed until after the test body finishes.
*
* To test pending messages, instead use something like [Instrumentation.runOnMainSync] to perform
* only sections of the test body on the main thread synchronously:
* ```
* @Test
* fun example() {
* instrumentation.runOnMainSync { doWorkThatPostsMessage() }
* // Second lambda will not execute until message is processed.
* instrumentation.runOnMainSync { verifyMessageResults() }
* }
* ```
*/
class TaskbarUnitTestRule : MethodRule {
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val serviceTestRule = ServiceTestRule()
private lateinit var taskbarManager: TaskbarManager
private lateinit var target: Any
val activityContext: TaskbarActivityContext
get() {
return taskbarManager.currentActivityContext
?: throw RuntimeException("Failed to obtain TaskbarActivityContext.")
}
override fun apply(base: Statement, method: FrameworkMethod, target: Any): Statement {
return object : Statement() {
override fun evaluate() {
this@TaskbarUnitTestRule.target = target
val context = instrumentation.targetContext
instrumentation.runOnMainSync {
assumeTrue(
LauncherAppState.getIDP(context).getDeviceProfile(context).isTaskbarPresent
)
}
// Check for existing Taskbar instance from Launcher process.
val launcherTaskbarManager: TaskbarManager? =
if (!isRunningInRobolectric) {
try {
val tisBinder =
serviceTestRule.bindService(
Intent(context, TouchInteractionService::class.java)
) as? TISBinder
tisBinder?.taskbarManager
} catch (_: Exception) {
null
}
} else {
null
}
instrumentation.runOnMainSync {
taskbarManager =
TaskbarManager(
context,
AllAppsActionManager(context, UI_HELPER_EXECUTOR) {
PendingIntent(IIntentSender.Default())
},
object : TaskbarNavButtonCallbacks {},
)
}
try {
// Replace Launcher Taskbar window with test instance.
instrumentation.runOnMainSync {
launcherTaskbarManager?.removeTaskbarRootViewFromWindow()
taskbarManager.onUserUnlocked() // Required to complete initialization.
}
injectControllers()
base.evaluate()
} finally {
// Revert Taskbar window.
instrumentation.runOnMainSync {
taskbarManager.destroy()
launcherTaskbarManager?.addTaskbarRootViewToWindow()
}
}
}
}
}
/** Simulates Taskbar recreation lifecycle. */
fun recreateTaskbar() {
taskbarManager.recreateTaskbar()
injectControllers()
}
private fun injectControllers() {
val controllers = activityContext.controllers
val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type }
target.javaClass.fields
.filter { it.isAnnotationPresent(InjectController::class.java) }
.forEach {
it.set(
target,
controllerFieldsByType[it.type]?.get(controllers)
?: throw NoSuchElementException("Failed to find controller for ${it.type}"),
)
}
}
/**
* Annotates test controller fields to inject the corresponding controllers from the current
* [TaskbarControllers] instance.
*
* Controllers are injected during test setup and upon calling [recreateTaskbar].
*
* Multiple controllers can be injected if needed.
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
annotation class InjectController
}
@@ -1,454 +0,0 @@
/*
* 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.launcher3.taskbar.bubbles.animation
import android.content.Context
import android.graphics.Color
import android.graphics.Path
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.View.VISIBLE
import android.widget.FrameLayout
import androidx.core.graphics.drawable.toBitmap
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.R
import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow
import com.android.launcher3.taskbar.bubbles.BubbleBarView
import com.android.launcher3.taskbar.bubbles.BubbleStashController
import com.android.launcher3.taskbar.bubbles.BubbleView
import com.android.wm.shell.common.bubbles.BubbleInfo
import com.android.wm.shell.shared.animation.PhysicsAnimator
import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
class BubbleBarViewAnimatorTest {
private val context = ApplicationProvider.getApplicationContext<Context>()
private lateinit var animatorScheduler: TestBubbleBarViewAnimatorScheduler
private lateinit var overflowView: BubbleView
private lateinit var bubbleView: BubbleView
private lateinit var bubble: BubbleBarBubble
private lateinit var bubbleBarView: BubbleBarView
private lateinit var bubbleStashController: BubbleStashController
@Before
fun setUp() {
animatorScheduler = TestBubbleBarViewAnimatorScheduler()
PhysicsAnimatorTestUtils.prepareForTest()
}
@Test
fun animateBubbleInForStashed() {
setUpBubbleBar()
setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
val animator =
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.animateBubbleInForStashed(bubble)
}
// let the animation start and wait for it to complete
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
assertThat(handle.alpha).isEqualTo(0)
assertThat(handle.translationY)
.isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
assertThat(bubbleBarView.scaleX).isEqualTo(1)
assertThat(bubbleBarView.scaleY).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
// execute the hide bubble animation
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
// let the animation start and wait for it to complete
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
assertThat(handle.alpha).isEqualTo(1)
assertThat(handle.translationY).isEqualTo(0)
assertThat(bubbleBarView.alpha).isEqualTo(0)
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
verify(bubbleStashController).stashBubbleBarImmediate()
}
@Test
fun animateBubbleInForStashed_tapAnimatingBubble() {
setUpBubbleBar()
setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
val animator =
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.animateBubbleInForStashed(bubble)
}
// let the animation start and wait for it to complete
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
assertThat(handle.alpha).isEqualTo(0)
assertThat(handle.translationY)
.isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
assertThat(bubbleBarView.scaleX).isEqualTo(1)
assertThat(bubbleBarView.scaleY).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
verify(bubbleStashController, atLeastOnce()).updateTaskbarTouchRegion()
// verify the hide bubble animation is pending
assertThat(animatorScheduler.delayedBlock).isNotNull()
animator.onBubbleBarTouchedWhileAnimating()
assertThat(animatorScheduler.delayedBlock).isNull()
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
}
@Test
fun animateBubbleInForStashed_touchTaskbarArea_whileShowing() {
setUpBubbleBar()
setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
val animator =
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.animateBubbleInForStashed(bubble)
}
// wait for the animation to start
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
handleAnimator.assertIsRunning()
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
// verify the hide bubble animation is pending
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.onStashStateChangingWhileAnimating()
}
// verify that the hide animation was canceled
assertThat(animatorScheduler.delayedBlock).isNull()
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
// PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
// again
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
handleAnimator.assertIsNotRunning()
}
@Test
fun animateBubbleInForStashed_touchTaskbarArea_whileHiding() {
setUpBubbleBar()
setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
val animator =
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.animateBubbleInForStashed(bubble)
}
// let the animation start and wait for it to complete
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
// execute the hide bubble animation
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
// wait for the hide animation to start
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
handleAnimator.assertIsRunning()
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.onStashStateChangingWhileAnimating()
}
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
// PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
// again
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
handleAnimator.assertIsNotRunning()
}
@Test
fun animateBubbleInForStashed_showAnimationCanceled() {
setUpBubbleBar()
setUpBubbleStashController()
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
val animator =
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.animateBubbleInForStashed(bubble)
}
// wait for the animation to start
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
handleAnimator.assertIsRunning()
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
assertThat(animatorScheduler.delayedBlock).isNotNull()
handleAnimator.cancel()
handleAnimator.assertIsNotRunning()
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
assertThat(animatorScheduler.delayedBlock).isNull()
}
@Test
fun animateToInitialState_inApp() {
setUpBubbleBar()
setUpBubbleStashController()
whenever(bubbleStashController.bubbleBarTranslationY)
.thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
val animator =
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.animateToInitialState(bubble, isInApp = true, isExpanding = false)
}
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
barAnimator.assertIsNotRunning()
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
assertThat(bubbleBarView.alpha).isEqualTo(0)
assertThat(handle.translationY).isEqualTo(0)
assertThat(handle.alpha).isEqualTo(1)
verify(bubbleStashController).stashBubbleBarImmediate()
}
@Test
fun animateToInitialState_inApp_autoExpanding() {
setUpBubbleBar()
setUpBubbleStashController()
whenever(bubbleStashController.bubbleBarTranslationY)
.thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
val handle = View(context)
val handleAnimator = PhysicsAnimator.getInstance(handle)
whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
val animator =
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.animateToInitialState(bubble, isInApp = true, isExpanding = true)
}
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
barAnimator.assertIsNotRunning()
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
verify(bubbleStashController).showBubbleBarImmediate()
}
@Test
fun animateToInitialState_inHome() {
setUpBubbleBar()
setUpBubbleStashController()
whenever(bubbleStashController.bubbleBarTranslationY)
.thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
val animator =
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
}
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
barAnimator.assertIsNotRunning()
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
verify(bubbleStashController).showBubbleBarImmediate()
}
private fun setUpBubbleBar() {
bubbleBarView = BubbleBarView(context)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
val inflater = LayoutInflater.from(context)
val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
overflowView =
inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
bubbleBarView.addView(overflowView)
val bubbleInfo =
BubbleInfo("key", 0, null, null, 0, context.packageName, null, null, false)
bubbleView =
inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
bubble =
BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
bubbleView.setBubble(bubble)
bubbleBarView.addView(bubbleView)
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
}
private fun setUpBubbleStashController() {
bubbleStashController = mock<BubbleStashController>()
whenever(bubbleStashController.isStashed).thenReturn(true)
whenever(bubbleStashController.diffBetweenHandleAndBarCenters)
.thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation)
.thenReturn(HANDLE_TRANSLATION)
whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
.thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
}
private fun <T> PhysicsAnimator<T>.assertIsRunning() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
assertThat(isRunning()).isTrue()
}
}
private fun <T> PhysicsAnimator<T>.assertIsNotRunning() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
assertThat(isRunning()).isFalse()
}
}
private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
var delayedBlock: Runnable? = null
private set
override fun post(block: Runnable) {
block.run()
}
override fun postDelayed(delayMillis: Long, block: Runnable) {
check(delayedBlock == null) { "there is already a pending block waiting to run" }
delayedBlock = block
}
override fun cancel(block: Runnable) {
check(delayedBlock == block) { "the pending block does not match the canceled block" }
delayedBlock = null
}
}
}
private const val DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS = -20f
private const val HANDLE_TRANSLATION = -30f
private const val BAR_TRANSLATION_Y_FOR_TASKBAR = -50f
private const val BAR_TRANSLATION_Y_FOR_HOTSEAT = -40f
@@ -1,118 +0,0 @@
/*
* 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.quickstep.recents.window
import android.graphics.Point
import android.hardware.display.DisplayManager
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.view.Display
import android.view.DisplayAdjustments
import android.view.Surface
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.Flags.FLAG_ENABLE_FALLBACK_OVERVIEW_IN_WINDOW
import com.android.launcher3.Flags.FLAG_ENABLE_LAUNCHER_OVERVIEW_IN_WINDOW
import com.android.launcher3.util.LauncherModelHelper
import com.android.launcher3.util.window.CachedDisplayInfo
import com.android.quickstep.fallback.window.RecentsDisplayModel
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(FLAG_ENABLE_LAUNCHER_OVERVIEW_IN_WINDOW, FLAG_ENABLE_FALLBACK_OVERVIEW_IN_WINDOW)
class RecentsDisplayModelTest {
@get:Rule val setFlagsRule = SetFlagsRule()
// initiate dagger components for injection
private val launcherModelHelper = LauncherModelHelper()
private val context = spy(launcherModelHelper.sandboxContext)
private val displayManager: DisplayManager = context.spyService(DisplayManager::class.java)
private val display: Display = mock()
private lateinit var recentsDisplayModel: RecentsDisplayModel
private val width = 2208
private val height = 1840
@Before
fun setup() {
// Mock display
val displayInfo = CachedDisplayInfo(Point(width, height), Surface.ROTATION_0)
whenever(display.rotation).thenReturn(displayInfo.rotation)
whenever(display.displayAdjustments).thenReturn(DisplayAdjustments())
whenever(context.display).thenReturn(display)
// Mock displayManager
whenever(displayManager.getDisplay(anyInt())).thenReturn(display)
runOnMainSync { recentsDisplayModel = RecentsDisplayModel.INSTANCE.get(context) }
}
@Test
fun testEnsureSingleton() {
val recentsDisplayModel2 = RecentsDisplayModel.INSTANCE.get(context)
assert(recentsDisplayModel == recentsDisplayModel2)
}
@Test
fun testDefaultDisplayCreation() {
Assert.assertNotNull(recentsDisplayModel.getRecentsWindowManager(Display.DEFAULT_DISPLAY))
Assert.assertNotNull(
recentsDisplayModel.getFallbackWindowInterface(Display.DEFAULT_DISPLAY)
)
}
@Test
fun testCreateSeparateInstances() {
val displayId = Display.DEFAULT_DISPLAY + 1
runOnMainSync { recentsDisplayModel.storeDisplayResource(displayId) }
val defaultManager = recentsDisplayModel.getRecentsWindowManager(Display.DEFAULT_DISPLAY)
val secondaryManager = recentsDisplayModel.getRecentsWindowManager(displayId)
Assert.assertNotSame(defaultManager, secondaryManager)
val defaultInterface =
recentsDisplayModel.getFallbackWindowInterface(Display.DEFAULT_DISPLAY)
val secondInterface = recentsDisplayModel.getFallbackWindowInterface(displayId)
Assert.assertNotSame(defaultInterface, secondInterface)
}
@Test
fun testDestroy() {
Assert.assertNotNull(recentsDisplayModel.getRecentsWindowManager(Display.DEFAULT_DISPLAY))
Assert.assertNotNull(
recentsDisplayModel.getFallbackWindowInterface(Display.DEFAULT_DISPLAY)
)
recentsDisplayModel.destroy()
Assert.assertNull(recentsDisplayModel.getRecentsWindowManager(Display.DEFAULT_DISPLAY))
Assert.assertNull(recentsDisplayModel.getFallbackWindowInterface(Display.DEFAULT_DISPLAY))
}
private fun runOnMainSync(f: Runnable) {
InstrumentationRegistry.getInstrumentation().runOnMainSync { f.run() }
}
}
@@ -1,178 +0,0 @@
/*
* 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.quickstep.task.thumbnail
import android.content.ComponentName
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Rect
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.quickstep.recents.data.FakeTasksRepository
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class TaskThumbnailViewModelTest {
private val recentsViewData = RecentsViewData()
private val taskViewData = TaskViewData()
private val tasksRepository = FakeTasksRepository()
private val systemUnderTest =
TaskThumbnailViewModel(recentsViewData, taskViewData, tasksRepository)
private val tasks = (0..5).map(::createTaskWithId)
@Test
fun initialStateIsUninitialized() = runTest {
assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
}
@Test
fun bindRunningTask_thenStateIs_LiveTile() = runTest {
tasksRepository.seedTasks(tasks)
val taskThumbnail = TaskThumbnail(taskId = 1, isRunning = true)
systemUnderTest.bind(taskThumbnail)
assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
}
@Test
fun setRecentsFullscreenProgress_thenProgressIsPassedThrough() = runTest {
recentsViewData.fullscreenProgress.value = 0.5f
assertThat(systemUnderTest.recentsFullscreenProgress.first()).isEqualTo(0.5f)
recentsViewData.fullscreenProgress.value = 0.6f
assertThat(systemUnderTest.recentsFullscreenProgress.first()).isEqualTo(0.6f)
}
@Test
fun setAncestorScales_thenScaleIsCalculated() = runTest {
recentsViewData.scale.value = 0.5f
taskViewData.scale.value = 0.6f
assertThat(systemUnderTest.inheritedScale.first()).isEqualTo(0.3f)
}
@Test
fun bindRunningTaskThenStoppedTaskWithoutThumbnail_thenStateChangesToBackgroundOnly() =
runTest {
tasksRepository.seedTasks(tasks)
val runningTask = TaskThumbnail(taskId = 1, isRunning = true)
val stoppedTask = TaskThumbnail(taskId = 2, isRunning = false)
systemUnderTest.bind(runningTask)
assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
systemUnderTest.bind(stoppedTask)
assertThat(systemUnderTest.uiState.first())
.isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
}
@Test
fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() = runTest {
tasksRepository.seedTasks(tasks)
val stoppedTask = TaskThumbnail(taskId = 2, isRunning = false)
systemUnderTest.bind(stoppedTask)
assertThat(systemUnderTest.uiState.first())
.isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
}
@Test
fun bindLockedTaskWithThumbnail_thenStateIs_BackgroundOnly() = runTest {
tasksRepository.seedThumbnailData(mapOf(2 to createThumbnailData()))
tasks[2].isLocked = true
tasksRepository.seedTasks(tasks)
val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
systemUnderTest.bind(recentTask)
assertThat(systemUnderTest.uiState.first())
.isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
}
@Test
fun bindStoppedTaskWithThumbnail_thenStateIs_Snapshot_withAlphaRemoved() = runTest {
val expectedThumbnailData = createThumbnailData()
tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData))
tasksRepository.seedTasks(tasks)
tasksRepository.setVisibleTasks(listOf(2))
val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
systemUnderTest.bind(recentTask)
assertThat(systemUnderTest.uiState.first())
.isEqualTo(
Snapshot(
backgroundColor = Color.rgb(2, 2, 2),
bitmap = expectedThumbnailData.thumbnail!!,
drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
)
)
}
@Test
fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshot() = runTest {
val expectedThumbnailData = createThumbnailData()
tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData))
tasksRepository.seedTasks(tasks)
val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
systemUnderTest.bind(recentTask)
assertThat(systemUnderTest.uiState.first())
.isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
tasksRepository.setVisibleTasks(listOf(2))
assertThat(systemUnderTest.uiState.first())
.isEqualTo(
Snapshot(
backgroundColor = Color.rgb(2, 2, 2),
bitmap = expectedThumbnailData.thumbnail!!,
drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
)
)
}
private fun createTaskWithId(taskId: Int) =
Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
colorBackground = Color.argb(taskId, taskId, taskId, taskId)
}
private fun createThumbnailData(): ThumbnailData {
val bitmap = mock<Bitmap>()
whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH)
whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT)
return ThumbnailData(thumbnail = bitmap)
}
companion object {
const val THUMBNAIL_WIDTH = 100
const val THUMBNAIL_HEIGHT = 200
}
}
@@ -1,166 +0,0 @@
/*
* 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.quickstep.util
import android.view.Surface
import android.view.Surface.ROTATION_0
import android.view.Surface.ROTATION_180
import android.view.Surface.ROTATION_90
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.quickstep.FallbackActivityInterface
import com.android.quickstep.orientation.RecentsPagedOrientationHandler
import com.android.quickstep.orientation.RecentsPagedOrientationHandler.Companion.PORTRAIT
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.spy
import org.mockito.kotlin.whenever
/**
* Test all possible inputs to RecentsOrientedState.updateHandler. It tests all possible
* combinations of rotations and relevant methods (two methods that return boolean values) but it
* only provides the expected result when the final rotation is different from ROTATION_0 for
* simplicity. So any case not shown in resultMap you can assume results in ROTATION_0.
*/
@SmallTest
@RunWith(AndroidJUnit4::class)
class RecentOrientedStateHandlerTests {
data class TestCase(
val recentsRotation: Int,
val displayRotation: Int,
val touchRotation: Int,
val isRotationAllowed: Boolean,
val isFixedLandscape: Boolean,
) {
override fun toString(): String {
return "TestCase(recentsRotation=${Surface.rotationToString(recentsRotation)}, " +
"displayRotation=${Surface.rotationToString(displayRotation)}, " +
"touchRotation=${Surface.rotationToString(touchRotation)}, " +
"isRotationAllowed=$isRotationAllowed, " +
"isFixedLandscape=$isFixedLandscape)"
}
}
private fun runTestCase(testCase: TestCase, expectedHandler: RecentsPagedOrientationHandler) {
val recentOrientedState =
spy(
RecentsOrientedState(
ApplicationProvider.getApplicationContext(),
FallbackActivityInterface.INSTANCE,
) {}
)
whenever(recentOrientedState.isRecentsActivityRotationAllowed).thenAnswer {
testCase.isRotationAllowed
}
whenever(recentOrientedState.isLauncherFixedLandscape).thenAnswer {
testCase.isFixedLandscape
}
recentOrientedState.update(testCase.displayRotation, testCase.touchRotation)
val rotation = recentOrientedState.orientationHandler.rotation
assertWithMessage("$testCase to ${Surface.rotationToString(rotation)},")
.that(rotation)
.isEqualTo(expectedHandler.rotation)
}
@Test
fun `test fixed landscape when device is portrait`() {
runTestCase(
TestCase(
recentsRotation = ROTATION_0,
displayRotation = -1,
touchRotation = ROTATION_0,
isRotationAllowed = false,
isFixedLandscape = true,
),
PORTRAIT,
)
}
@Test
fun `test fixed landscape when device is landscape`() {
runTestCase(
TestCase(
recentsRotation = ROTATION_90,
displayRotation = -1,
touchRotation = ROTATION_0,
isRotationAllowed = false,
isFixedLandscape = true,
),
PORTRAIT,
)
}
@Test
fun `test fixed landscape when device is seascape`() {
runTestCase(
TestCase(
recentsRotation = ROTATION_180,
displayRotation = -1,
touchRotation = ROTATION_0,
isRotationAllowed = false,
isFixedLandscape = true,
),
PORTRAIT,
)
}
@Test
fun `test fixed landscape when device is portrait and display rotation is portrait`() {
runTestCase(
TestCase(
recentsRotation = ROTATION_0,
displayRotation = ROTATION_0,
touchRotation = ROTATION_0,
isRotationAllowed = false,
isFixedLandscape = true,
),
PORTRAIT,
)
}
@Test
fun `test fixed landscape when device is landscape and display rotation is landscape `() {
runTestCase(
TestCase(
recentsRotation = ROTATION_90,
displayRotation = ROTATION_90,
touchRotation = ROTATION_0,
isRotationAllowed = false,
isFixedLandscape = true,
),
PORTRAIT,
)
}
@Test
fun `test fixed landscape when device is seascape and display rotation is seascape`() {
runTestCase(
TestCase(
recentsRotation = ROTATION_180,
displayRotation = ROTATION_180,
touchRotation = ROTATION_0,
isRotationAllowed = false,
isFixedLandscape = true,
),
PORTRAIT,
)
}
}
@@ -1,90 +0,0 @@
/*
* 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.quickstep.util;
import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_180;
import static android.view.Surface.ROTATION_90;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.quickstep.FallbackActivityInterface;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Tests for {@link RecentsOrientedState}
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public class RecentsOrientedStateTest {
private RecentsOrientedState mR1, mR2;
@Before
public void setup() {
Context context = getApplicationContext();
mR1 = new RecentsOrientedState(context, FallbackActivityInterface.INSTANCE, i -> { });
mR2 = new RecentsOrientedState(context, FallbackActivityInterface.INSTANCE, i -> { });
assertEquals(mR1.getStateId(), mR2.getStateId());
}
@Test
public void stateId_changesWithFlags() {
mR1.setGestureActive(true);
mR2.setGestureActive(false);
assertNotEquals(mR1.getStateId(), mR2.getStateId());
mR2.setGestureActive(true);
assertEquals(mR1.getStateId(), mR2.getStateId());
}
@Test
public void stateId_changesWithRecentsRotation() {
mR1.setRecentsRotation(ROTATION_90);
mR2.setRecentsRotation(ROTATION_180);
assertNotEquals(mR1.getStateId(), mR2.getStateId());
mR2.setRecentsRotation(ROTATION_90);
assertEquals(mR1.getStateId(), mR2.getStateId());
}
@Test
public void stateId_changesWithDisplayRotation() {
mR1.update(ROTATION_0, ROTATION_90);
mR2.update(ROTATION_0, ROTATION_180);
assertNotEquals(mR1.getStateId(), mR2.getStateId());
mR2.update(ROTATION_90, ROTATION_90);
assertNotEquals(mR1.getStateId(), mR2.getStateId());
mR2.update(ROTATION_90, ROTATION_0);
assertNotEquals(mR1.getStateId(), mR2.getStateId());
mR2.update(ROTATION_0, ROTATION_90);
assertEquals(mR1.getStateId(), mR2.getStateId());
}
}
@@ -1,510 +0,0 @@
/*
* 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.quickstep.util;
import static com.android.quickstep.util.TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID;
import static com.android.quickstep.util.TaskGridNavHelper.INVALID_FOCUSED_TASK_ID;
import static org.junit.Assert.assertEquals;
import com.android.launcher3.util.IntArray;
import org.junit.Test;
public class TaskGridNavHelperTest {
@Test
public void equalLengthRows_noFocused_onTop_pressDown_goesToBottom() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 1;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 2, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onTop_pressUp_goesToBottom() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 1;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 2, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onBottom_pressDown_goesToTop() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 2;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 1, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onBottom_pressUp_goesToTop() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 2;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 1, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onTop_pressLeft_goesLeft() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 1;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 3, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onBottom_pressLeft_goesLeft() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 2;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 4, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onTop_secondItem_pressRight_goesRight() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 3;
int delta = -1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 1, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onBottom_secondItem_pressRight_goesRight() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 4;
int delta = -1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 2, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onTop_pressRight_cycleToClearAll() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 1;
int delta = -1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onBottom_pressRight_cycleToClearAll() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 2;
int delta = -1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onTop_lastItem_pressLeft_toClearAll() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 5;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onBottom_lastItem_pressLeft_toClearAll() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 6;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onClearAll_pressLeft_cycleToFirst() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 1, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onClearAll_pressRight_toLastInBottom() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
int delta = -1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 6, nextGridPage);
}
@Test
public void equalLengthRows_withFocused_onFocused_pressLeft_toTop() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int focusedTaskId = 99;
int currentPageTaskViewId = focusedTaskId;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 1, nextGridPage);
}
@Test
public void equalLengthRows_withFocused_onFocused_pressUp_stayOnFocused() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int focusedTaskId = 99;
int currentPageTaskViewId = focusedTaskId;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage);
}
@Test
public void equalLengthRows_withFocused_onFocused_pressDown_stayOnFocused() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int focusedTaskId = 99;
int currentPageTaskViewId = focusedTaskId;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage);
}
@Test
public void equalLengthRows_withFocused_onFocused_pressRight_cycleToClearAll() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int focusedTaskId = 99;
int currentPageTaskViewId = focusedTaskId;
int delta = -1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
}
@Test
public void equalLengthRows_withFocused_onClearAll_pressLeft_cycleToFocusedTask() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int focusedTaskId = 99;
int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage);
}
@Test
public void longerTopRow_noFocused_atEndTopBeyondBottom_pressDown_stayTop() {
IntArray topIds = IntArray.wrap(1, 3, 5, 7);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 7;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 7, nextGridPage);
}
@Test
public void longerTopRow_noFocused_atEndTopBeyondBottom_pressUp_stayTop() {
IntArray topIds = IntArray.wrap(1, 3, 5, 7);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 7;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 7, nextGridPage);
}
@Test
public void longerTopRow_noFocused_atEndBottom_pressLeft_goToTop() {
IntArray topIds = IntArray.wrap(1, 3, 5, 7);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 6;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 7, nextGridPage);
}
@Test
public void longerTopRow_noFocused_atClearAll_pressRight_goToLonger() {
IntArray topIds = IntArray.wrap(1, 3, 5, 7);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
int delta = -1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 7, nextGridPage);
}
@Test
public void longerBottomRow_noFocused_atClearAll_pressRight_goToLonger() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6, 7);
int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
int delta = -1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 7, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onTop_pressTab_goesToBottom() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 1;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 2, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onBottom_pressTab_goesToNextTop() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 2;
int delta = 1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 3, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onTop_pressTabWithShift_goesToPreviousBottom() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 3;
int delta = -1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 2, nextGridPage);
}
@Test
public void equalLengthRows_noFocused_onBottom_pressTabWithShift_goesToTop() {
IntArray topIds = IntArray.wrap(1, 3, 5);
IntArray bottomIds = IntArray.wrap(2, 4, 6);
int currentPageTaskViewId = 2;
int delta = -1;
@TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB;
boolean cycle = true;
TaskGridNavHelper taskGridNavHelper =
new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
assertEquals("Wrong next page returned.", 1, nextGridPage);
}
}
@@ -1,127 +0,0 @@
package com.android.launcher3.model
import android.app.prediction.AppPredictor
import android.app.prediction.AppTarget
import android.app.prediction.AppTargetEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.launcher3.LauncherAppState
import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION
import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION
import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WALLPAPERS
import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION
import com.android.launcher3.util.LauncherModelHelper
import org.junit.After
import org.junit.Assert.assertNotSame
import org.junit.Assert.assertSame
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
/** Unit tests for [QuickstepModelDelegate]. */
@RunWith(AndroidJUnit4::class)
class QuickstepModelDelegateTest {
private lateinit var underTest: QuickstepModelDelegate
private lateinit var modelHelper: LauncherModelHelper
@Mock private lateinit var target: AppTarget
@Mock private lateinit var mockedAppTargetEvent: AppTargetEvent
@Mock private lateinit var allAppsPredictor: AppPredictor
@Mock private lateinit var hotseatPredictor: AppPredictor
@Mock private lateinit var widgetRecommendationPredictor: AppPredictor
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
modelHelper = LauncherModelHelper()
underTest = QuickstepModelDelegate(modelHelper.sandboxContext)
underTest.mAllAppsState.predictor = allAppsPredictor
underTest.mHotseatState.predictor = hotseatPredictor
underTest.mWidgetsRecommendationState.predictor = widgetRecommendationPredictor
underTest.mApp = LauncherAppState.getInstance(modelHelper.sandboxContext)
underTest.mDataModel = BgDataModel()
}
@After
fun tearDown() {
modelHelper.destroy()
}
@Test
fun onAppTargetEvent_notifyTarget() {
underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_PREDICTION)
verify(allAppsPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
verifyZeroInteractions(hotseatPredictor)
verifyZeroInteractions(widgetRecommendationPredictor)
}
@Test
fun onWidgetPrediction_notifyWidgetRecommendationPredictor() {
underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_WIDGETS_PREDICTION)
verifyZeroInteractions(allAppsPredictor)
verify(widgetRecommendationPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
verifyZeroInteractions(hotseatPredictor)
}
@Test
fun onHotseatPrediction_notifyHotseatPredictor() {
underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_HOTSEAT_PREDICTION)
verifyZeroInteractions(allAppsPredictor)
verifyZeroInteractions(widgetRecommendationPredictor)
verify(hotseatPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
}
@Test
fun onOtherClient_notifyHotseatPredictor() {
underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_WALLPAPERS)
verifyZeroInteractions(allAppsPredictor)
verifyZeroInteractions(widgetRecommendationPredictor)
verify(hotseatPredictor).notifyAppTargetEvent(mockedAppTargetEvent)
}
@Test
fun hotseatActionPin_recreateHotSeat() {
assertSame(underTest.mHotseatState.predictor, hotseatPredictor)
val appTargetEvent = AppTargetEvent.Builder(target, AppTargetEvent.ACTION_PIN).build()
underTest.markActive()
underTest.onAppTargetEvent(appTargetEvent, CONTAINER_HOTSEAT_PREDICTION)
verify(hotseatPredictor).destroy()
assertNotSame(underTest.mHotseatState.predictor, hotseatPredictor)
}
@Test
fun hotseatActionUnpin_recreateHotSeat() {
assertSame(underTest.mHotseatState.predictor, hotseatPredictor)
underTest.markActive()
val appTargetEvent = AppTargetEvent.Builder(target, AppTargetEvent.ACTION_UNPIN).build()
underTest.onAppTargetEvent(appTargetEvent, CONTAINER_HOTSEAT_PREDICTION)
verify(hotseatPredictor).destroy()
assertNotSame(underTest.mHotseatState.predictor, hotseatPredictor)
}
@Test
fun container_actionPin_notRecreateHotSeat() {
assertSame(underTest.mHotseatState.predictor, hotseatPredictor)
val appTargetEvent = AppTargetEvent.Builder(target, AppTargetEvent.ACTION_UNPIN).build()
underTest.markActive()
underTest.onAppTargetEvent(appTargetEvent, CONTAINER_PREDICTION)
verify(allAppsPredictor, never()).destroy()
verify(hotseatPredictor, never()).destroy()
assertSame(underTest.mHotseatState.predictor, hotseatPredictor)
}
}
@@ -1,221 +0,0 @@
/*
* 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.launcher3.model
import android.app.prediction.AppTarget
import android.app.prediction.AppTargetEvent
import android.app.prediction.AppTargetId
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
import android.content.Context
import android.os.Process.myUserHandle
import android.os.UserHandle
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.launcher3.DeviceProfile
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherAppState
import com.android.launcher3.icons.IconCache
import com.android.launcher3.model.WidgetPredictionsRequester.buildBundleForPredictionSession
import com.android.launcher3.model.WidgetPredictionsRequester.filterPredictions
import com.android.launcher3.model.WidgetPredictionsRequester.notOnUiSurfaceFilter
import com.android.launcher3.util.ActivityContextWrapper
import com.android.launcher3.util.PackageUserKey
import com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
import com.google.common.truth.Truth.assertThat
import java.util.function.Predicate
import junit.framework.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@RunWith(AndroidJUnit4::class)
class WidgetsPredictionsRequesterTest {
private lateinit var mUserHandle: UserHandle
private lateinit var context: Context
private lateinit var deviceProfile: DeviceProfile
private lateinit var testInvariantProfile: InvariantDeviceProfile
private lateinit var widget1aInfo: AppWidgetProviderInfo
private lateinit var widget1bInfo: AppWidgetProviderInfo
private lateinit var widget2Info: AppWidgetProviderInfo
private lateinit var widgetItem1a: WidgetItem
private lateinit var widgetItem1b: WidgetItem
private lateinit var widgetItem2: WidgetItem
private lateinit var allWidgets: Map<PackageUserKey, List<WidgetItem>>
@Mock private lateinit var iconCache: IconCache
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
mUserHandle = myUserHandle()
context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
testInvariantProfile = LauncherAppState.getIDP(context)
deviceProfile = testInvariantProfile.getDeviceProfile(context).copy(context)
widget1aInfo =
createAppWidgetProviderInfo(
ComponentName.createRelative(APP_1_PACKAGE_NAME, APP_1_PROVIDER_A_CLASS_NAME)
)
widget1bInfo =
createAppWidgetProviderInfo(
ComponentName.createRelative(APP_1_PACKAGE_NAME, APP_1_PROVIDER_B_CLASS_NAME)
)
widgetItem1a = createWidgetItem(widget1aInfo)
widgetItem1b = createWidgetItem(widget1bInfo)
widget2Info =
createAppWidgetProviderInfo(
ComponentName.createRelative(APP_2_PACKAGE_NAME, APP_2_PROVIDER_1_CLASS_NAME)
)
widgetItem2 = createWidgetItem(widget2Info)
allWidgets =
mapOf(
PackageUserKey(APP_1_PACKAGE_NAME, mUserHandle) to
listOf(widgetItem1a, widgetItem1b),
PackageUserKey(APP_2_PACKAGE_NAME, mUserHandle) to listOf(widgetItem2),
)
}
@Test
fun buildBundleForPredictionSession_includesAddedAppWidgets() {
val existingWidgets = arrayListOf(widget1aInfo, widget1bInfo, widget2Info)
val bundle = buildBundleForPredictionSession(existingWidgets, TEST_UI_SURFACE)
val addedWidgetsBundleExtra =
bundle.getParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, AppTarget::class.java)
assertNotNull(addedWidgetsBundleExtra)
assertThat(addedWidgetsBundleExtra)
.containsExactly(
buildExpectedAppTargetEvent(
/*pkg=*/ APP_1_PACKAGE_NAME,
/*providerClassName=*/ APP_1_PROVIDER_A_CLASS_NAME,
/*user=*/ mUserHandle
),
buildExpectedAppTargetEvent(
/*pkg=*/ APP_1_PACKAGE_NAME,
/*providerClassName=*/ APP_1_PROVIDER_B_CLASS_NAME,
/*user=*/ mUserHandle
),
buildExpectedAppTargetEvent(
/*pkg=*/ APP_2_PACKAGE_NAME,
/*providerClassName=*/ APP_2_PROVIDER_1_CLASS_NAME,
/*user=*/ mUserHandle
)
)
}
@Test
fun filterPredictions_notOnUiSurfaceFilter_returnsOnlyEligiblePredictions() {
val widgetsAlreadyOnSurface = arrayListOf(widget1bInfo)
val filter: Predicate<WidgetItem> = notOnUiSurfaceFilter(widgetsAlreadyOnSurface)
val predictions =
listOf(
// already on surface
AppTarget(
AppTargetId(APP_1_PACKAGE_NAME),
APP_1_PACKAGE_NAME,
APP_1_PROVIDER_B_CLASS_NAME,
mUserHandle
),
// eligible
AppTarget(
AppTargetId(APP_2_PACKAGE_NAME),
APP_2_PACKAGE_NAME,
APP_2_PROVIDER_1_CLASS_NAME,
mUserHandle
)
)
// only 2 was eligible
assertThat(filterPredictions(predictions, allWidgets, filter)).containsExactly(widgetItem2)
}
@Test
fun filterPredictions_appPredictions_returnsWidgetFromPackage() {
val widgetsAlreadyOnSurface = arrayListOf(widget1bInfo)
val filter: Predicate<WidgetItem> = notOnUiSurfaceFilter(widgetsAlreadyOnSurface)
val predictions =
listOf(
AppTarget(
AppTargetId(APP_1_PACKAGE_NAME),
APP_1_PACKAGE_NAME,
"$APP_1_PACKAGE_NAME.SomeActivity",
mUserHandle
),
AppTarget(
AppTargetId(APP_2_PACKAGE_NAME),
APP_2_PACKAGE_NAME,
"$APP_2_PACKAGE_NAME.SomeActivity2",
mUserHandle
),
)
assertThat(filterPredictions(predictions, allWidgets, filter))
.containsExactly(widgetItem1a, widgetItem2)
}
private fun createWidgetItem(
providerInfo: AppWidgetProviderInfo,
): WidgetItem {
val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo)
return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context)
}
companion object {
const val TEST_UI_SURFACE = "widgets_test"
const val BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets"
const val APP_1_PACKAGE_NAME = "com.example.app1"
const val APP_1_PROVIDER_A_CLASS_NAME = "app1Provider1"
const val APP_1_PROVIDER_B_CLASS_NAME = "app1Provider2"
const val APP_2_PACKAGE_NAME = "com.example.app2"
const val APP_2_PROVIDER_1_CLASS_NAME = "app2Provider1"
const val TEST_PACKAGE = "pkg"
private fun buildExpectedAppTargetEvent(
pkg: String,
providerClassName: String,
userHandle: UserHandle
): AppTargetEvent {
val appTarget =
AppTarget.Builder(
/*id=*/ AppTargetId("widget:$pkg"),
/*packageName=*/ pkg,
/*user=*/ userHandle
)
.setClassName(providerClassName)
.build()
return AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN)
.setLaunchLocation(TEST_UI_SURFACE)
.build()
}
}
}
@@ -1,80 +0,0 @@
/*
* 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.launcher3.statehandlers
import android.content.Context
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
import com.android.launcher3.util.DaggerSingletonTracker
import com.android.quickstep.SystemUiProxy
import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND
import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
/**
* Tests the behavior of [DesktopVisibilityController] in regards to multiple desktops and multiple
* displays.
*/
@SmallTest
@RunWith(AndroidJUnit4::class)
class DesktopVisibilityControllerTest {
@get:Rule val setFlagsRule = SetFlagsRule()
private val mockitoSession =
mockitoSession()
.strictness(Strictness.LENIENT)
.mockStatic(DesktopModeStatus::class.java)
.startMocking()
private val context = mock<Context>()
private val systemUiProxy = mock<SystemUiProxy>()
private val lifeCycleTracker = mock<DaggerSingletonTracker>()
private lateinit var desktopVisibilityController: DesktopVisibilityController
@Before
fun setUp() {
whenever(context.resources).thenReturn(mock())
whenever(DesktopModeStatus.enableMultipleDesktops(context)).thenReturn(true)
desktopVisibilityController =
DesktopVisibilityController(context, systemUiProxy, lifeCycleTracker)
}
@After
fun tearDown() {
mockitoSession.finishMocking()
}
@Test
@EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND)
fun noCrashWhenCheckingNonExistentDisplay() {
assertFalse(desktopVisibilityController.isInDesktopMode(displayId = 500))
assertFalse(desktopVisibilityController.isInDesktopModeAndNotInOverview(displayId = 300))
}
}
@@ -1,80 +0,0 @@
/*
* 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.launcher3.taskbar
import androidx.test.runner.AndroidJUnit4
import com.android.launcher3.statemanager.StateManager
import com.android.quickstep.RecentsActivity
import com.android.quickstep.fallback.RecentsState
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class FallbackTaskbarUIControllerTest : TaskbarBaseTestCase() {
lateinit var fallbackTaskbarUIController: FallbackTaskbarUIController
lateinit var stateListener: StateManager.StateListener<RecentsState>
private val recentsActivity: RecentsActivity = mock()
private val stateManager: StateManager<RecentsState, RecentsActivity> = mock()
@Before
override fun setup() {
super.setup()
whenever(recentsActivity.stateManager).thenReturn(stateManager)
fallbackTaskbarUIController = FallbackTaskbarUIController(recentsActivity)
// Capture registered state listener to send events to in our tests
val captor = argumentCaptor<StateManager.StateListener<RecentsState>>()
fallbackTaskbarUIController.init(taskbarControllers)
verify(stateManager).addStateListener(captor.capture())
stateListener = captor.lastValue
}
@Test
fun stateTransitionComplete_stateDefault() {
stateListener.onStateTransitionComplete(RecentsState.DEFAULT)
// verify dragging disabled
verify(taskbarDragController, times(1)).setDisallowGlobalDrag(true)
verify(taskbarAllAppsController, times(1)).setDisallowGlobalDrag(true)
// verify long click enabled
verify(taskbarDragController, times(1)).setDisallowLongClick(false)
verify(taskbarAllAppsController, times(1)).setDisallowLongClick(false)
// verify split selection enabled
verify(taskbarPopupController, times(1)).setAllowInitialSplitSelection(true)
}
@Test
fun stateTransitionComplete_stateSplitSelect() {
stateListener.onStateTransitionComplete(RecentsState.OVERVIEW_SPLIT_SELECT)
// verify dragging disabled
verify(taskbarDragController, times(1)).setDisallowGlobalDrag(false)
verify(taskbarAllAppsController, times(1)).setDisallowGlobalDrag(false)
// verify long click enabled
verify(taskbarDragController, times(1)).setDisallowLongClick(true)
verify(taskbarAllAppsController, times(1)).setDisallowLongClick(true)
// verify split selection enabled
verify(taskbarPopupController, times(1)).setAllowInitialSplitSelection(false)
}
}
@@ -1,103 +0,0 @@
/*
* 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.launcher3.taskbar
import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController
import com.android.launcher3.taskbar.bubbles.BubbleControllers
import com.android.launcher3.taskbar.overlay.TaskbarOverlayController
import com.android.systemui.shared.rotation.RotationButtonController
import java.util.Optional
import org.junit.Before
import org.mockito.Mock
import org.mockito.MockitoAnnotations
/**
* Helper class to extend to get access to all controllers. Gotta be careful of your relationship
* with this class though, it can be quite... controlling.
*/
abstract class TaskbarBaseTestCase {
@Mock lateinit var taskbarActivityContext: TaskbarActivityContext
@Mock lateinit var taskbarDragController: TaskbarDragController
@Mock lateinit var navButtonController: TaskbarNavButtonController
@Mock lateinit var navbarButtonsViewController: NavbarButtonsViewController
@Mock lateinit var rotationButtonController: RotationButtonController
@Mock lateinit var taskbarDragLayerController: TaskbarDragLayerController
@Mock lateinit var taskbarScrimViewController: TaskbarScrimViewController
@Mock lateinit var taskbarViewController: TaskbarViewController
@Mock lateinit var taskbarUnfoldAnimationController: TaskbarUnfoldAnimationController
@Mock lateinit var taskbarKeyguardController: TaskbarKeyguardController
@Mock lateinit var stashedHandleViewController: StashedHandleViewController
@Mock lateinit var taskbarStashController: TaskbarStashController
@Mock lateinit var taskbarAutohideSuspendController: TaskbarAutohideSuspendController
@Mock lateinit var taskbarPopupController: TaskbarPopupController
@Mock
lateinit var taskbarForceVisibleImmersiveController: TaskbarForceVisibleImmersiveController
@Mock lateinit var taskbarAllAppsController: TaskbarAllAppsController
@Mock lateinit var taskbarInsetsController: TaskbarInsetsController
@Mock lateinit var voiceInteractionWindowController: VoiceInteractionWindowController
@Mock lateinit var taskbarRecentAppsController: TaskbarRecentAppsController
@Mock lateinit var taskbarTranslationController: TaskbarTranslationController
@Mock lateinit var taskbarSpringOnStashController: TaskbarSpringOnStashController
@Mock lateinit var taskbarOverlayController: TaskbarOverlayController
@Mock lateinit var taskbarEduTooltipController: TaskbarEduTooltipController
@Mock lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController
@Mock lateinit var taskbarPinningController: TaskbarPinningController
@Mock lateinit var optionalBubbleControllers: Optional<BubbleControllers>
lateinit var taskbarControllers: TaskbarControllers
@Before
open fun setup() {
/*
* NOTE: Mocking of controllers that are written in Kotlin won't work since their methods
* are final by default (and should not be changed only for tests), meaning unmockable.
* Womp, womp woooommmmppp.
* If you want to mock one of those methods, you need to make a parent interface that
* includes that method to allow mocking it.
*/
MockitoAnnotations.initMocks(this)
taskbarControllers =
TaskbarControllers(
taskbarActivityContext,
taskbarDragController,
navButtonController,
navbarButtonsViewController,
rotationButtonController,
taskbarDragLayerController,
taskbarViewController,
taskbarScrimViewController,
taskbarUnfoldAnimationController,
taskbarKeyguardController,
stashedHandleViewController,
taskbarStashController,
taskbarAutohideSuspendController,
taskbarPopupController,
taskbarForceVisibleImmersiveController,
taskbarOverlayController,
taskbarAllAppsController,
taskbarInsetsController,
voiceInteractionWindowController,
taskbarTranslationController,
taskbarSpringOnStashController,
taskbarRecentAppsController,
taskbarEduTooltipController,
keyboardQuickSwitchController,
taskbarPinningController,
optionalBubbleControllers,
)
}
}
@@ -1,219 +0,0 @@
/*
* 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.launcher3.taskbar;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.Display;
import android.view.MotionEvent;
import androidx.test.filters.SmallTest;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.util.ActivityContextWrapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
/**
* Tests for TaskbarHoverToolTipController.
*/
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase {
private TaskbarHoverToolTipController mTaskbarHoverToolTipController;
private TestableLooper mTestableLooper;
@Mock private TaskbarView mTaskbarView;
@Mock private MotionEvent mMotionEvent;
@Mock private BubbleTextView mHoverBubbleTextView;
@Mock private FolderIcon mHoverFolderIcon;
@Mock private Display mDisplay;
@Mock private TaskbarDragLayer mTaskbarDragLayer;
private Folder mSpyFolderView;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
Context context = getApplicationContext();
doAnswer((Answer<Object>) invocation -> context.getSystemService(
(String) invocation.getArgument(0)))
.when(taskbarActivityContext).getSystemService(anyString());
when(taskbarActivityContext.getResources()).thenReturn(context.getResources());
when(taskbarActivityContext.getApplicationInfo()).thenReturn(
context.getApplicationInfo());
when(taskbarActivityContext.getDragLayer()).thenReturn(mTaskbarDragLayer);
when(taskbarActivityContext.getMainLooper()).thenReturn(context.getMainLooper());
when(taskbarActivityContext.getDisplay()).thenReturn(mDisplay);
when(mTaskbarDragLayer.getChildCount()).thenReturn(1);
mSpyFolderView = spy(new Folder(new ActivityContextWrapper(context), null));
when(mTaskbarDragLayer.getChildAt(anyInt())).thenReturn(mSpyFolderView);
doReturn(false).when(mSpyFolderView).isOpen();
when(mHoverBubbleTextView.getText()).thenReturn("tooltip");
doAnswer((Answer<Void>) invocation -> {
Object[] args = invocation.getArguments();
((int[]) args[0])[0] = 0;
((int[]) args[0])[1] = 0;
return null;
}).when(mHoverBubbleTextView).getLocationOnScreen(any(int[].class));
when(mHoverBubbleTextView.getWidth()).thenReturn(100);
when(mHoverBubbleTextView.getHeight()).thenReturn(100);
mHoverFolderIcon.mInfo = new FolderInfo();
mHoverFolderIcon.mInfo.title = "tooltip";
doAnswer((Answer<Void>) invocation -> {
Object[] args = invocation.getArguments();
((int[]) args[0])[0] = 0;
((int[]) args[0])[1] = 0;
return null;
}).when(mHoverFolderIcon).getLocationOnScreen(any(int[].class));
when(mHoverFolderIcon.getWidth()).thenReturn(100);
when(mHoverFolderIcon.getHeight()).thenReturn(100);
when(mTaskbarView.getTop()).thenReturn(200);
mTaskbarHoverToolTipController = new TaskbarHoverToolTipController(
taskbarActivityContext, mTaskbarView, mHoverBubbleTextView);
mTestableLooper = TestableLooper.get(this);
}
@Test
public void onHover_hoverEnterIcon_revealToolTip() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
boolean hoverHandled =
mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
waitForIdleSync();
assertThat(hoverHandled).isTrue();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
true);
}
@Test
public void onHover_hoverExitIcon_closeToolTip() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
boolean hoverHandled =
mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
waitForIdleSync();
assertThat(hoverHandled).isTrue();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
false);
}
@Test
public void onHover_hoverEnterFolderIcon_revealToolTip() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
boolean hoverHandled =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
waitForIdleSync();
assertThat(hoverHandled).isTrue();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
true);
}
@Test
public void onHover_hoverExitFolderIcon_closeToolTip() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
boolean hoverHandled =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
waitForIdleSync();
assertThat(hoverHandled).isTrue();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
false);
}
@Test
public void onHover_hoverExitFolderOpen_closeToolTip() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
doReturn(true).when(mSpyFolderView).isOpen();
boolean hoverHandled =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
waitForIdleSync();
assertThat(hoverHandled).isTrue();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
false);
}
@Test
public void onHover_hoverEnterFolderOpen_noToolTip() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
doReturn(true).when(mSpyFolderView).isOpen();
boolean hoverHandled =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
assertThat(hoverHandled).isFalse();
}
@Test
public void onHover_hoverMove_noUpdate() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_MOVE);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_MOVE);
boolean hoverHandled =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
assertThat(hoverHandled).isFalse();
}
private void waitForIdleSync() {
mTestableLooper.processAllMessages();
}
}
@@ -1,111 +0,0 @@
/*
* 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.launcher3.taskbar
import android.app.KeyguardManager
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DOZING
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
class TaskbarKeyguardControllerTest : TaskbarBaseTestCase() {
private val baseDragLayer: TaskbarDragLayer = mock()
private val keyguardManager: KeyguardManager = mock()
@Before
override fun setup() {
super.setup()
whenever(taskbarActivityContext.getSystemService(KeyguardManager::class.java))
.thenReturn(keyguardManager)
whenever(baseDragLayer.childCount).thenReturn(0)
whenever(taskbarActivityContext.dragLayer).thenReturn(baseDragLayer)
taskbarKeyguardController = TaskbarKeyguardController(taskbarActivityContext)
taskbarKeyguardController.init(navbarButtonsViewController)
}
@Test
fun uninterestingFlags_noActions() {
setFlags(0)
verify(navbarButtonsViewController, never()).setKeyguardVisible(any(), any())
}
@Test
fun keyguardShowing() {
setFlags(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING)
verify(navbarButtonsViewController, times(1))
.setKeyguardVisible(true /*isKeyguardVisible*/, false /*isKeyguardOccluded*/)
}
@Test
fun dozingShowing() {
setFlags(SYSUI_STATE_DEVICE_DOZING)
verify(navbarButtonsViewController, times(1))
.setKeyguardVisible(true /*isKeyguardVisible*/, false /*isKeyguardOccluded*/)
}
@Test
fun keyguardOccluded() {
setFlags(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED)
verify(navbarButtonsViewController, times(1))
.setKeyguardVisible(false /*isKeyguardVisible*/, true /*isKeyguardOccluded*/)
}
@Test
fun keyguardOccludedAndDozing() {
setFlags(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED.or(SYSUI_STATE_DEVICE_DOZING))
verify(navbarButtonsViewController, times(1))
.setKeyguardVisible(true /*isKeyguardVisible*/, true /*isKeyguardOccluded*/)
}
@Test
fun deviceInsecure_hideBackForBouncer() {
whenever(keyguardManager.isDeviceSecure).thenReturn(false)
setFlags(SYSUI_STATE_BOUNCER_SHOWING)
verify(navbarButtonsViewController, times(1)).setBackForBouncer(false)
}
@Test
fun deviceSecure_showBackForBouncer() {
whenever(keyguardManager.isDeviceSecure).thenReturn(true)
setFlags(SYSUI_STATE_BOUNCER_SHOWING)
verify(navbarButtonsViewController, times(1)).setBackForBouncer(true)
}
@Test
fun backDisabled_hideBackForBouncer() {
whenever(keyguardManager.isDeviceSecure).thenReturn(true)
setFlags(SYSUI_STATE_BACK_DISABLED.or(SYSUI_STATE_BOUNCER_SHOWING))
verify(navbarButtonsViewController, times(1)).setBackForBouncer(false)
}
private fun setFlags(flags: Long) {
taskbarKeyguardController.updateStateForSysuiFlags(flags)
}
}
@@ -1,242 +0,0 @@
/*
* 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.launcher3.taskbar
import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.ComponentName
import android.content.Intent
import android.os.Process
import android.os.UserHandle
import android.testing.AndroidTestingRunner
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.quickstep.RecentsModel
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.kotlin.whenever
@RunWith(AndroidTestingRunner::class)
class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() {
@get:Rule val mockitoRule = MockitoJUnit.rule()
@Mock private lateinit var mockRecentsModel: RecentsModel
@Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController
private var nextTaskId: Int = 500
private lateinit var recentAppsController: TaskbarRecentAppsController
private lateinit var userHandle: UserHandle
@Before
fun setUp() {
super.setup()
userHandle = Process.myUserHandle()
recentAppsController =
TaskbarRecentAppsController(mockRecentsModel) { mockDesktopVisibilityController }
recentAppsController.init(taskbarControllers)
recentAppsController.isEnabled = true
recentAppsController.setApps(
ALL_APP_PACKAGES.map { createTestAppInfo(packageName = it) }.toTypedArray()
)
}
@Test
fun updateHotseatItemInfos_notInDesktopMode_returnsExistingHotseatItems() {
setInDesktopMode(false)
val hotseatItems =
createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()))
.isEqualTo(hotseatItems.toTypedArray())
}
@Test
fun updateHotseatItemInfos_notInDesktopMode_runningApps_returnsExistingHotseatItems() {
setInDesktopMode(false)
val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages)
val runningTasks =
createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
recentAppsController.updateRunningApps()
val newHotseatItems =
recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
assertThat(newHotseatItems.map { it?.targetPackage })
.containsExactlyElementsIn(hotseatPackages)
}
@Test
fun updateHotseatItemInfos_noRunningApps_returnsExistingHotseatItems() {
setInDesktopMode(true)
val hotseatItems =
createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()))
.isEqualTo(hotseatItems.toTypedArray())
}
@Test
fun updateHotseatItemInfos_returnsExistingHotseatItemsAndRunningApps() {
setInDesktopMode(true)
val hotseatItems =
createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
val runningTasks =
createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
recentAppsController.updateRunningApps()
val newHotseatItems =
recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
val expectedPackages =
listOf(
HOTSEAT_PACKAGE_1,
HOTSEAT_PACKAGE_2,
RUNNING_APP_PACKAGE_1,
RUNNING_APP_PACKAGE_2,
)
assertThat(newHotseatItems.map { it?.targetPackage })
.containsExactlyElementsIn(expectedPackages)
}
@Test
fun updateHotseatItemInfos_runningAppIsHotseatItem_returnsDistinctItems() {
setInDesktopMode(true)
val hotseatItems =
createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
val runningTasks =
createDesktopTasksFromPackageNames(
listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
)
whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
recentAppsController.updateRunningApps()
val newHotseatItems =
recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
val expectedPackages =
listOf(
HOTSEAT_PACKAGE_1,
HOTSEAT_PACKAGE_2,
RUNNING_APP_PACKAGE_1,
RUNNING_APP_PACKAGE_2,
)
assertThat(newHotseatItems.map { it?.targetPackage })
.containsExactlyElementsIn(expectedPackages)
}
@Test
fun getRunningApps_notInDesktopMode_returnsEmptySet() {
setInDesktopMode(false)
val runningTasks =
createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
recentAppsController.updateRunningApps()
assertThat(recentAppsController.runningApps).isEmpty()
assertThat(recentAppsController.minimizedApps).isEmpty()
}
@Test
fun getRunningApps_inDesktopMode_returnsRunningApps() {
setInDesktopMode(true)
val runningTasks =
createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
recentAppsController.updateRunningApps()
assertThat(recentAppsController.runningApps)
.containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
assertThat(recentAppsController.minimizedApps).isEmpty()
}
@Test
fun getMinimizedApps_inDesktopMode_returnsAllAppsRunningAndInvisibleAppsMinimized() {
setInDesktopMode(true)
val runningTasks =
ArrayList(
listOf(
createDesktopTaskInfo(RUNNING_APP_PACKAGE_1) { isVisible = true },
createDesktopTaskInfo(RUNNING_APP_PACKAGE_2) { isVisible = true },
createDesktopTaskInfo(RUNNING_APP_PACKAGE_3) { isVisible = false },
)
)
whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
recentAppsController.updateRunningApps()
assertThat(recentAppsController.runningApps)
.containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3)
assertThat(recentAppsController.minimizedApps).containsExactly(RUNNING_APP_PACKAGE_3)
}
private fun createHotseatItemsFromPackageNames(packageNames: List<String>): List<ItemInfo> {
return packageNames.map { createTestAppInfo(packageName = it) }
}
private fun createDesktopTasksFromPackageNames(
packageNames: List<String>
): ArrayList<RunningTaskInfo> {
return ArrayList(packageNames.map { createDesktopTaskInfo(packageName = it) })
}
private fun createDesktopTaskInfo(
packageName: String,
init: RunningTaskInfo.() -> Unit = { isVisible = true },
): RunningTaskInfo {
return RunningTaskInfo().apply {
taskId = nextTaskId++
configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
realActivity = ComponentName(packageName, "TestActivity")
init()
}
}
private fun createTestAppInfo(
packageName: String = "testPackageName",
className: String = "testClassName"
) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent())
private fun setInDesktopMode(inDesktopMode: Boolean) {
whenever(mockDesktopVisibilityController.areDesktopTasksVisible()).thenReturn(inDesktopMode)
}
private companion object {
const val HOTSEAT_PACKAGE_1 = "hotseat1"
const val HOTSEAT_PACKAGE_2 = "hotseat2"
const val RUNNING_APP_PACKAGE_1 = "running1"
const val RUNNING_APP_PACKAGE_2 = "running2"
const val RUNNING_APP_PACKAGE_3 = "running3"
val ALL_APP_PACKAGES =
listOf(
HOTSEAT_PACKAGE_1,
HOTSEAT_PACKAGE_2,
RUNNING_APP_PACKAGE_1,
RUNNING_APP_PACKAGE_2,
RUNNING_APP_PACKAGE_3,
)
}
}
@@ -1,107 +0,0 @@
/*
* Copyright (C) 2019 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.quickstep;
import static junit.framework.TestCase.assertNull;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.app.KeyguardManager;
import androidx.test.filters.SmallTest;
import com.android.launcher3.util.LooperExecutor;
import com.android.quickstep.util.GroupTask;
import com.android.wm.shell.util.GroupedRecentTaskInfo;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@SmallTest
public class RecentTasksListTest {
@Mock
private SystemUiProxy mockSystemUiProxy;
@Mock
private TopTaskTracker mTopTaskTracker;
// Class under test
private RecentTasksList mRecentTasksList;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
LooperExecutor mockMainThreadExecutor = mock(LooperExecutor.class);
KeyguardManager mockKeyguardManager = mock(KeyguardManager.class);
mRecentTasksList = new RecentTasksList(mockMainThreadExecutor, mockKeyguardManager,
mockSystemUiProxy, mTopTaskTracker);
}
@Test
public void onRecentTasksChanged_doesNotFetchTasks() {
mRecentTasksList.onRecentTasksChanged();
verify(mockSystemUiProxy, times(0))
.getRecentTasks(anyInt(), anyInt());
}
@Test
public void loadTasksInBackground_onlyKeys_noValidTaskDescription() {
GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(
new ActivityManager.RecentTaskInfo(), new ActivityManager.RecentTaskInfo(), null);
when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
.thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1,
true);
assertEquals(1, taskList.size());
assertNull(taskList.get(0).task1.taskDescription.getLabel());
assertNull(taskList.get(0).task2.taskDescription.getLabel());
}
@Test
public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() {
String taskDescription = "Wheeee!";
ActivityManager.RecentTaskInfo task1 = new ActivityManager.RecentTaskInfo();
task1.taskDescription = new ActivityManager.TaskDescription(taskDescription);
ActivityManager.RecentTaskInfo task2 = new ActivityManager.RecentTaskInfo();
task2.taskDescription = new ActivityManager.TaskDescription();
GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(task1, task2,
null);
when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
.thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1,
false);
assertEquals(1, taskList.size());
assertEquals(taskDescription, taskList.get(0).task1.taskDescription.getLabel());
assertNull(taskList.get(0).task2.taskDescription.getLabel());
}
}
@@ -1,205 +0,0 @@
package com.android.quickstep
import android.content.Context
import android.testing.AndroidTestingRunner
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SmallTest
import com.android.launcher3.util.DisplayController.CHANGE_DENSITY
import com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE
import com.android.launcher3.util.DisplayController.CHANGE_ROTATION
import com.android.launcher3.util.DisplayController.Info
import com.android.launcher3.util.NavigationMode
import com.android.quickstep.util.GestureExclusionManager
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.verifyZeroInteractions
import org.mockito.kotlin.whenever
/** Unit test for [RecentsAnimationDeviceState]. */
@SmallTest
@RunWith(AndroidTestingRunner::class)
class RecentsAnimationDeviceStateTest {
@Mock private lateinit var exclusionManager: GestureExclusionManager
@Mock private lateinit var info: Info
private val context = ApplicationProvider.getApplicationContext() as Context
private lateinit var underTest: RecentsAnimationDeviceState
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
underTest = RecentsAnimationDeviceState(context, exclusionManager)
}
@Test
fun registerExclusionListener_success() {
underTest.registerExclusionListener()
verify(exclusionManager).addListener(underTest)
}
@Test
fun registerExclusionListener_again_fail() {
underTest.registerExclusionListener()
reset(exclusionManager)
underTest.registerExclusionListener()
verifyZeroInteractions(exclusionManager)
}
@Test
fun unregisterExclusionListener_success() {
underTest.registerExclusionListener()
reset(exclusionManager)
underTest.unregisterExclusionListener()
verify(exclusionManager).removeListener(underTest)
}
@Test
fun unregisterExclusionListener_again_fail() {
underTest.registerExclusionListener()
underTest.unregisterExclusionListener()
reset(exclusionManager)
underTest.unregisterExclusionListener()
verifyZeroInteractions(exclusionManager)
}
@Test
fun onDisplayInfoChanged_noButton_registerExclusionListener() {
doReturn(NavigationMode.NO_BUTTON).whenever(info).getNavigationMode()
underTest.onDisplayInfoChanged(context, info, CHANGE_ROTATION or CHANGE_NAVIGATION_MODE)
verify(exclusionManager).addListener(underTest)
}
@Test
fun onDisplayInfoChanged_twoButton_unregisterExclusionListener() {
underTest.registerExclusionListener()
whenever(info.getNavigationMode()).thenReturn(NavigationMode.TWO_BUTTONS)
reset(exclusionManager)
underTest.onDisplayInfoChanged(context, info, CHANGE_ROTATION or CHANGE_NAVIGATION_MODE)
verify(exclusionManager).removeListener(underTest)
}
@Test
fun onDisplayInfoChanged_changeDensity_noOp() {
underTest.registerExclusionListener()
whenever(info.getNavigationMode()).thenReturn(NavigationMode.NO_BUTTON)
reset(exclusionManager)
underTest.onDisplayInfoChanged(context, info, CHANGE_DENSITY)
verifyZeroInteractions(exclusionManager)
}
@Test
fun trackpadGesturesNotAllowedForSelectedStates() {
val disablingStates = GESTURE_DISABLING_SYSUI_STATES +
SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED
allSysUiStates().forEach { state ->
val canStartGesture = !disablingStates.contains(state)
underTest.setSystemUiFlags(state)
assertThat(underTest.canStartTrackpadGesture()).isEqualTo(canStartGesture)
}
}
@Test
fun trackpadGesturesNotAllowedIfHomeAndOverviewIsDisabled() {
val stateToExpectedResult = mapOf(
SYSUI_STATE_HOME_DISABLED to true,
SYSUI_STATE_OVERVIEW_DISABLED to true,
DEFAULT_STATE
.enable(SYSUI_STATE_OVERVIEW_DISABLED)
.enable(SYSUI_STATE_HOME_DISABLED) to false
)
stateToExpectedResult.forEach { (state, allowed) ->
underTest.setSystemUiFlags(state)
assertThat(underTest.canStartTrackpadGesture()).isEqualTo(allowed)
}
}
@Test
fun systemGesturesNotAllowedForSelectedStates() {
val disablingStates = GESTURE_DISABLING_SYSUI_STATES + SYSUI_STATE_NAV_BAR_HIDDEN
allSysUiStates().forEach { state ->
val canStartGesture = !disablingStates.contains(state)
underTest.setSystemUiFlags(state)
assertThat(underTest.canStartSystemGesture()).isEqualTo(canStartGesture)
}
}
@Test
fun systemGesturesNotAllowedWhenGestureStateDisabledAndNavBarVisible() {
val stateToExpectedResult = mapOf(
DEFAULT_STATE
.enable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY)
.disable(SYSUI_STATE_NAV_BAR_HIDDEN) to true,
DEFAULT_STATE
.enable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY)
.enable(SYSUI_STATE_NAV_BAR_HIDDEN) to true,
DEFAULT_STATE
.disable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY)
.disable(SYSUI_STATE_NAV_BAR_HIDDEN) to true,
DEFAULT_STATE
.disable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY)
.enable(SYSUI_STATE_NAV_BAR_HIDDEN) to false,
)
stateToExpectedResult.forEach {(state, gestureAllowed) ->
underTest.setSystemUiFlags(state)
assertThat(underTest.canStartSystemGesture()).isEqualTo(gestureAllowed)
}
}
private fun allSysUiStates(): List<Long> {
// SYSUI_STATES_* are binary flags
return (0..SYSUI_STATES_COUNT).map { 1L shl it }
}
companion object {
private val GESTURE_DISABLING_SYSUI_STATES = listOf(
SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED,
SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING,
SYSUI_STATE_QUICK_SETTINGS_EXPANDED,
SYSUI_STATE_MAGNIFICATION_OVERLAP,
SYSUI_STATE_DEVICE_DREAMING,
SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION,
)
private const val SYSUI_STATES_COUNT = 33
private const val DEFAULT_STATE = 0L
}
private fun Long.enable(state: Long) = this or state
private fun Long.disable(state: Long) = this and state.inv()
}
@@ -1,173 +0,0 @@
/*
* 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.quickstep;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Resources;
import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.SmallTest;
import com.android.launcher3.Flags;
import com.android.launcher3.R;
import com.android.launcher3.icons.IconProvider;
import com.android.quickstep.util.GroupTask;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.function.Consumer;
@SmallTest
public class RecentsModelTest {
@Mock
private Context mContext;
@Mock
private TaskThumbnailCache mThumbnailCache;
@Mock
private RecentTasksList mTasksList;
@Mock
private TaskThumbnailCache.HighResLoadingState mHighResLoadingState;
private RecentsModel mRecentsModel;
private RecentTasksList.TaskLoadResult mTaskResult;
private Resources mResource;
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
public void setup() throws NoSuchFieldException {
MockitoAnnotations.initMocks(this);
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW);
mTaskResult = getTaskResult();
doAnswer(invocation-> {
Consumer<ArrayList<GroupTask>> callback = invocation.getArgument(1);
callback.accept(mTaskResult);
return null;
}).when(mTasksList).getTaskKeys(anyInt(), any());
when(mHighResLoadingState.isEnabled()).thenReturn(true);
when(mThumbnailCache.getHighResLoadingState()).thenReturn(mHighResLoadingState);
when(mThumbnailCache.isPreloadingEnabled()).thenReturn(true);
mRecentsModel = new RecentsModel(mContext, mTasksList, mock(TaskIconCache.class),
mThumbnailCache, mock(IconProvider.class), mock(TaskStackChangeListeners.class));
mResource = mock(Resources.class);
when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(3);
when(mContext.getResources()).thenReturn(mResource);
}
@Test
@UiThreadTest
public void preloadOnHighResolutionEnabled() {
mRecentsModel.preloadCacheIfNeeded();
ArgumentCaptor<Task> taskArgs = ArgumentCaptor.forClass(Task.class);
verify(mRecentsModel.getThumbnailCache(), times(2))
.updateThumbnailInCache(taskArgs.capture(), /* lowResolution= */ eq(false));
GroupTask expectedGroupTask = mTaskResult.get(0);
assertThat(taskArgs.getAllValues().get(0)).isEqualTo(
expectedGroupTask.task1);
assertThat(taskArgs.getAllValues().get(1)).isEqualTo(
expectedGroupTask.task2);
}
@Test
public void notPreloadOnHighResolutionDisabled() {
when(mHighResLoadingState.isEnabled()).thenReturn(false);
when(mThumbnailCache.isPreloadingEnabled()).thenReturn(true);
mRecentsModel.preloadCacheIfNeeded();
verify(mRecentsModel.getThumbnailCache(), never())
.updateThumbnailInCache(any(), anyBoolean());
}
@Test
public void notPreloadOnPreloadDisabled() {
when(mThumbnailCache.isPreloadingEnabled()).thenReturn(false);
mRecentsModel.preloadCacheIfNeeded();
verify(mRecentsModel.getThumbnailCache(), never())
.updateThumbnailInCache(any(), anyBoolean());
}
@Test
public void increaseCacheSizeAndPreload() {
// Mock to return preload is needed
when(mThumbnailCache.updateCacheSizeAndRemoveExcess()).thenReturn(true);
// Update cache size
mRecentsModel.updateCacheSizeAndPreloadIfNeeded();
// Assert update cache is called
verify(mRecentsModel.getThumbnailCache(), times(2))
.updateThumbnailInCache(any(), /* lowResolution= */ eq(false));
}
@Test
public void decreaseCacheSizeAndNotPreload() {
// Mock to return preload is not needed
when(mThumbnailCache.updateCacheSizeAndRemoveExcess()).thenReturn(false);
// Update cache size
mRecentsModel.updateCacheSizeAndPreloadIfNeeded();
// Assert update cache is never called
verify(mRecentsModel.getThumbnailCache(), never())
.updateThumbnailInCache(any(), anyBoolean());
}
private RecentTasksList.TaskLoadResult getTaskResult() {
RecentTasksList.TaskLoadResult allTasks = new RecentTasksList.TaskLoadResult(0, false, 1);
ActivityManager.RecentTaskInfo taskInfo1 = new ActivityManager.RecentTaskInfo();
Task.TaskKey taskKey1 = new Task.TaskKey(taskInfo1);
Task task1 = Task.from(taskKey1, taskInfo1, false);
ActivityManager.RecentTaskInfo taskInfo2 = new ActivityManager.RecentTaskInfo();
Task.TaskKey taskKey2 = new Task.TaskKey(taskInfo2);
Task task2 = Task.from(taskKey2, taskInfo2, false);
allTasks.add(new GroupTask(task1, task2, null));
return allTasks;
}
}
@@ -1,109 +0,0 @@
/*
* 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.quickstep;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.app.PendingIntent;
import android.app.usage.UsageStatsManager;
import android.content.Intent;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.launcher3.Launcher;
import com.android.quickstep.views.DigitalWellBeingToast;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class TaplDigitalWellBeingToastTest extends AbstractQuickStepTest {
private static final String CALCULATOR_PACKAGE =
resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
@Test
public void testToast() throws Exception {
startAppFast(CALCULATOR_PACKAGE);
final UsageStatsManager usageStatsManager =
mTargetContext.getSystemService(UsageStatsManager.class);
final int observerId = 0;
try {
final String[] packages = new String[]{CALCULATOR_PACKAGE};
// Set time limit for app.
runWithShellPermission(() ->
usageStatsManager.registerAppUsageLimitObserver(observerId, packages,
Duration.ofSeconds(600), Duration.ofSeconds(300),
PendingIntent.getActivity(mTargetContext, -1, new Intent()
.setPackage(mTargetContext.getPackageName()),
PendingIntent.FLAG_MUTABLE)));
mLauncher.goHome();
final DigitalWellBeingToast toast = getToast();
waitForLauncherCondition("Toast is not visible", launcher -> toast.hasLimit());
assertEquals("Toast text: ", "5 minutes left today", toast.getText());
// Unset time limit for app.
runWithShellPermission(
() -> usageStatsManager.unregisterAppUsageLimitObserver(observerId));
mLauncher.goHome();
assertFalse("Toast is visible", getToast().hasLimit());
} finally {
runWithShellPermission(
() -> usageStatsManager.unregisterAppUsageLimitObserver(observerId));
}
}
private DigitalWellBeingToast getToast() {
mLauncher.getWorkspace().switchToOverview();
final TaskView task = getOnceNotNull("No latest task", launcher -> getLatestTask(launcher));
return getFromLauncher(launcher -> {
TaskView.TaskContainer taskContainer = task.getTaskContainers().get(0);
assertTrue("Latest task is not Calculator", CALCULATOR_PACKAGE.equals(
taskContainer.getTask().getTopComponent().getPackageName()));
return taskContainer.getDigitalWellBeingToast();
});
}
private TaskView getLatestTask(Launcher launcher) {
return launcher.<RecentsView>getOverviewPanel().getTaskViewAt(0);
}
private void runWithShellPermission(Runnable action) {
getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
try {
action.run();
} finally {
getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
}
}
}
@@ -1,95 +0,0 @@
/*
* 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.quickstep;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.res.Resources;
import androidx.test.filters.SmallTest;
import com.android.launcher3.R;
import com.android.quickstep.util.TaskKeyCache;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.concurrent.Executor;
@SmallTest
public class TaskThumbnailCacheTest {
@Mock
private Context mContext;
@Mock
private Resources mResource;
@Mock
private TaskKeyCache mTaskKeyCache;
@Before
public void setup() throws NoSuchFieldException {
MockitoAnnotations.initMocks(this);
when(mContext.getResources()).thenReturn(mResource);
}
@Test
public void increaseCacheSize() {
// Mock a cache size increase from 3 to 8
when(mTaskKeyCache.getMaxSize()).thenReturn(3);
when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(8);
TaskThumbnailCache thumbnailCache = new TaskThumbnailCache(mContext, mock(Executor.class),
mTaskKeyCache);
// Preload is needed when increasing size
assertTrue(thumbnailCache.updateCacheSizeAndRemoveExcess());
verify(mTaskKeyCache, times(1)).updateCacheSizeAndRemoveExcess(8);
}
@Test
public void decreaseCacheSize() {
// Mock a cache size decrease from 8 to 3
when(mTaskKeyCache.getMaxSize()).thenReturn(8);
when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(3);
TaskThumbnailCache thumbnailCache = new TaskThumbnailCache(mContext, mock(Executor.class),
mTaskKeyCache);
// Preload is not needed when decreasing size
assertFalse(thumbnailCache.updateCacheSizeAndRemoveExcess());
verify(mTaskKeyCache, times(1)).updateCacheSizeAndRemoveExcess(3);
}
@Test
public void keepSameCacheSize() {
when(mTaskKeyCache.getMaxSize()).thenReturn(3);
when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(3);
TaskThumbnailCache thumbnailCache = new TaskThumbnailCache(mContext, mock(Executor.class),
mTaskKeyCache);
// Preload is not needed when it has the same cache size
assertFalse(thumbnailCache.updateCacheSizeAndRemoveExcess());
verify(mTaskKeyCache, never()).updateCacheSizeAndRemoveExcess(anyInt());
}
}
@@ -1,219 +0,0 @@
/*
* 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.quickstep.taskbar.controllers
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.LauncherPrefs
import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING_IN_DESKTOP_MODE
import com.android.launcher3.logging.StatsLogManager
import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE
import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_OPEN
import com.android.launcher3.taskbar.TaskbarActivityContext
import com.android.launcher3.taskbar.TaskbarBaseTestCase
import com.android.launcher3.taskbar.TaskbarDividerPopupView
import com.android.launcher3.taskbar.TaskbarDragLayer
import com.android.launcher3.taskbar.TaskbarPinningController
import com.android.launcher3.taskbar.TaskbarPinningController.Companion.PINNING_PERSISTENT
import com.android.launcher3.taskbar.TaskbarPinningController.Companion.PINNING_TRANSIENT
import com.android.launcher3.taskbar.TaskbarSharedState
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doNothing
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
class TaskbarPinningControllerTest : TaskbarBaseTestCase() {
private val taskbarDragLayer = mock<TaskbarDragLayer>()
private val taskbarSharedState = mock<TaskbarSharedState>()
private var isInDesktopMode = false
private val isInDesktopModeProvider = { isInDesktopMode }
private val launcherPrefs =
mock<LauncherPrefs> {
on { get(TASKBAR_PINNING) } doReturn false
on { get(TASKBAR_PINNING_IN_DESKTOP_MODE) } doReturn false
}
private val statsLogger = mock<StatsLogManager.StatsLogger>()
private val statsLogManager = mock<StatsLogManager> { on { logger() } doReturn statsLogger }
private lateinit var pinningController: TaskbarPinningController
@Before
override fun setup() {
super.setup()
whenever(taskbarActivityContext.launcherPrefs).thenReturn(launcherPrefs)
whenever(taskbarActivityContext.dragLayer).thenReturn(taskbarDragLayer)
whenever(taskbarActivityContext.statsLogManager).thenReturn(statsLogManager)
pinningController =
spy(TaskbarPinningController(taskbarActivityContext, isInDesktopModeProvider))
pinningController.init(taskbarControllers, taskbarSharedState)
}
@Test
fun testOnCloseCallback_whenClosingPopupView_shouldLogStatsForClosingPopupMenu() {
pinningController.onCloseCallback(false)
verify(statsLogger, times(1)).log(LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE)
}
@Test
fun testOnCloseCallback_whenClosingPopupView_shouldPostVisibilityChangedToDragLayer() {
val argumentCaptor = argumentCaptor<Runnable>()
pinningController.onCloseCallback(false)
verify(taskbarDragLayer, times(1)).post(argumentCaptor.capture())
val runnable = argumentCaptor.lastValue
assertThat(runnable).isNotNull()
runnable.run()
verify(taskbarActivityContext, times(1)).onPopupVisibilityChanged(false)
}
@Test
fun testOnCloseCallback_whenPreferenceUnchanged_shouldNotAnimateTaskbarPinning() {
pinningController.onCloseCallback(false)
verify(taskbarSharedState, never()).taskbarWasPinned = true
verify(pinningController, never()).animateTaskbarPinning(any())
}
@Test
fun testOnCloseCallback_whenLauncherPreferenceChanged_shouldAnimateToPinnedTaskbar() {
whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
doNothing().whenever(pinningController).animateTaskbarPinning(any())
pinningController.onCloseCallback(true)
verify(taskbarSharedState, times(1)).taskbarWasPinned = false
verify(pinningController, times(1)).animateTaskbarPinning(PINNING_PERSISTENT)
}
@Test
fun testOnCloseCallback_whenLauncherPreferenceChanged_shouldAnimateToTransientTaskbar() {
whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(true)
doNothing().whenever(pinningController).animateTaskbarPinning(any())
pinningController.onCloseCallback(true)
verify(taskbarSharedState, times(1)).taskbarWasPinned = true
verify(pinningController, times(1)).animateTaskbarPinning(PINNING_TRANSIENT)
}
@Test
fun testShowPinningView_whenShowingPinningView_shouldSetTaskbarWindowFullscreenAndPostRunnableToView() {
val popupView =
mock<TaskbarDividerPopupView<TaskbarActivityContext>> {
on { requestFocus() } doReturn true
}
val view = mock<View>()
val argumentCaptor = argumentCaptor<Runnable>()
doReturn(popupView).whenever(pinningController).getPopupView(view)
pinningController.showPinningView(view)
verify(view, times(1)).post(argumentCaptor.capture())
val runnable = argumentCaptor.lastValue
assertThat(runnable).isNotNull()
runnable.run()
verify(pinningController, times(1)).getPopupView(view)
verify(popupView, times(1)).requestFocus()
verify(popupView, times(1)).onCloseCallback = any()
verify(taskbarActivityContext, times(1)).onPopupVisibilityChanged(true)
verify(popupView, times(1)).show()
verify(statsLogger, times(1)).log(LAUNCHER_TASKBAR_DIVIDER_MENU_OPEN)
}
@Test
fun testAnimateTaskbarPinning_whenAnimationEnds_shouldInvokeCallbackDoOnEnd() {
val animatorSet = spy(AnimatorSet())
doReturn(animatorSet)
.whenever(pinningController)
.getAnimatorSetForTaskbarPinningAnimation(PINNING_PERSISTENT)
doNothing().whenever(animatorSet).start()
pinningController.animateTaskbarPinning(PINNING_PERSISTENT)
animatorSet.listeners[0].onAnimationEnd(ObjectAnimator())
verify(pinningController, times(1)).recreateTaskbarAndUpdatePinningValue()
}
@Test
fun testAnimateTaskbarPinning_whenAnimatingToPersistentTaskbar_shouldAnimateToPinnedTaskbar() {
val animatorSet = spy(AnimatorSet())
doReturn(animatorSet)
.whenever(pinningController)
.getAnimatorSetForTaskbarPinningAnimation(PINNING_PERSISTENT)
doNothing().whenever(animatorSet).start()
pinningController.animateTaskbarPinning(PINNING_PERSISTENT)
verify(taskbarOverlayController, times(1)).hideWindow()
verify(pinningController, times(1))
.getAnimatorSetForTaskbarPinningAnimation(PINNING_PERSISTENT)
verify(taskbarViewController, times(1))
.animateAwayNotificationDotsDuringTaskbarPinningAnimation()
verify(taskbarDragLayer, times(1)).setAnimatingTaskbarPinning(true)
assertThat(pinningController.isAnimatingTaskbarPinning).isTrue()
assertThat(animatorSet.listeners).isNotNull()
}
@Test
fun testAnimateTaskbarPinning_whenAnimatingToTransientTaskbar_shouldAnimateToTransientTaskbar() {
val animatorSet = spy(AnimatorSet())
doReturn(animatorSet)
.whenever(pinningController)
.getAnimatorSetForTaskbarPinningAnimation(PINNING_TRANSIENT)
doNothing().whenever(animatorSet).start()
pinningController.animateTaskbarPinning(PINNING_TRANSIENT)
verify(taskbarOverlayController, times(1)).hideWindow()
verify(pinningController, times(1))
.getAnimatorSetForTaskbarPinningAnimation(PINNING_TRANSIENT)
verify(taskbarDragLayer, times(1)).setAnimatingTaskbarPinning(true)
assertThat(pinningController.isAnimatingTaskbarPinning).isTrue()
verify(taskbarViewController, times(1))
.animateAwayNotificationDotsDuringTaskbarPinningAnimation()
assertThat(animatorSet.listeners).isNotNull()
}
@Test
fun testRecreateTaskbarAndUpdatePinningValue_whenAnimationEnds_shouldUpdateTaskbarPinningLauncherPref() {
pinningController.recreateTaskbarAndUpdatePinningValue()
verify(taskbarDragLayer, times(1)).setAnimatingTaskbarPinning(false)
assertThat(pinningController.isAnimatingTaskbarPinning).isFalse()
verify(launcherPrefs, times(1)).put(TASKBAR_PINNING, true)
}
@Test
fun testRecreateTaskbarAndUpdatePinningValue_whenAnimationEnds_shouldUpdateTaskbarPinningDesktopModePref() {
isInDesktopMode = true
pinningController.recreateTaskbarAndUpdatePinningValue()
verify(taskbarDragLayer, times(1)).setAnimatingTaskbarPinning(false)
assertThat(pinningController.isAnimatingTaskbarPinning).isFalse()
verify(launcherPrefs, times(1)).put(TASKBAR_PINNING_IN_DESKTOP_MODE, true)
}
}
@@ -1,99 +0,0 @@
/*
* 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.quickstep.taskbar.customization
import com.android.launcher3.taskbar.customization.TaskbarFeatureEvaluator
import com.android.launcher3.taskbar.customization.TaskbarIconSpecs
import com.android.launcher3.taskbar.customization.TaskbarSpecsEvaluator
import com.android.launcher3.util.LauncherMultivalentJUnit
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.whenever
@RunWith(LauncherMultivalentJUnit::class)
class TaskbarSpecsEvaluatorTest {
private val taskbarFeatureEvaluator = mock<TaskbarFeatureEvaluator>()
private val taskbarSpecsEvaluator = spy(TaskbarSpecsEvaluator(taskbarFeatureEvaluator))
@Test
fun testGetIconSizeByGrid_whenTaskbarIsTransient_withValidRowAndColumn() {
doReturn(true).whenever(taskbarFeatureEvaluator).isTransient
assertThat(taskbarSpecsEvaluator.getIconSizeByGrid(6, 5))
.isEqualTo(TaskbarIconSpecs.iconSize52dp)
}
@Test
fun testGetIconSizeByGrid_whenTaskbarIsTransient_withInvalidRowAndColumn() {
doReturn(true).whenever(taskbarFeatureEvaluator).isTransient
assertThat(taskbarSpecsEvaluator.getIconSizeByGrid(1, 2))
.isEqualTo(TaskbarIconSpecs.defaultTransientIconSize)
}
@Test
fun testGetIconSizeByGrid_whenTaskbarIsPersistent() {
doReturn(false).whenever(taskbarFeatureEvaluator).isTransient
assertThat(taskbarSpecsEvaluator.getIconSizeByGrid(6, 5))
.isEqualTo(TaskbarIconSpecs.defaultPersistentIconSize)
}
@Test
fun testGetIconSizeStepDown_whenTaskbarIsPersistent() {
doReturn(false).whenever(taskbarFeatureEvaluator).isTransient
assertThat(taskbarSpecsEvaluator.getIconSizeStepDown(TaskbarIconSpecs.iconSize44dp))
.isEqualTo(TaskbarIconSpecs.defaultPersistentIconSize)
}
@Test
fun testGetIconSizeStepDown_whenTaskbarIsTransientAndIconSizeAreInBound() {
doReturn(true).whenever(taskbarFeatureEvaluator).isTransient
assertThat(taskbarSpecsEvaluator.getIconSizeStepDown(TaskbarIconSpecs.iconSize52dp))
.isEqualTo(TaskbarIconSpecs.iconSize48dp)
}
@Test
fun testGetIconSizeStepDown_whenTaskbarIsTransientAndIconSizeAreOutOfBound() {
doReturn(true).whenever(taskbarFeatureEvaluator).isTransient
assertThat(taskbarSpecsEvaluator.getIconSizeStepDown(TaskbarIconSpecs.iconSize44dp))
.isEqualTo(TaskbarIconSpecs.iconSize44dp)
}
@Test
fun testGetIconSizeStepUp_whenTaskbarIsPersistent() {
doReturn(false).whenever(taskbarFeatureEvaluator).isTransient
assertThat(taskbarSpecsEvaluator.getIconSizeStepUp(TaskbarIconSpecs.iconSize40dp))
.isEqualTo(TaskbarIconSpecs.iconSize40dp)
}
@Test
fun testGetIconSizeStepUp_whenTaskbarIsTransientAndIconSizeAreInBound() {
doReturn(true).whenever(taskbarFeatureEvaluator).isTransient
assertThat(taskbarSpecsEvaluator.getIconSizeStepUp(TaskbarIconSpecs.iconSize44dp))
.isEqualTo(TaskbarIconSpecs.iconSize48dp)
}
@Test
fun testGetIconSizeStepUp_whenTaskbarIsTransientAndIconSizeAreOutOfBound() {
doReturn(true).whenever(taskbarFeatureEvaluator).isTransient
assertThat(taskbarSpecsEvaluator.getIconSizeStepUp(TaskbarIconSpecs.iconSize52dp))
.isEqualTo(TaskbarIconSpecs.iconSize52dp)
}
}
@@ -1,135 +0,0 @@
/*
* 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.quickstep.util
import android.graphics.Rect
import android.graphics.Region
import android.testing.AndroidTestingRunner
import android.view.Display.DEFAULT_DISPLAY
import android.view.IWindowManager
import androidx.test.filters.SmallTest
import com.android.launcher3.util.Executors
import com.android.quickstep.util.GestureExclusionManager.ExclusionListener
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.reset
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyZeroInteractions
/** Unit test for [GestureExclusionManager]. */
@SmallTest
@RunWith(AndroidTestingRunner::class)
class GestureExclusionManagerTest {
@Mock private lateinit var windowManager: IWindowManager
@Mock private lateinit var listener1: ExclusionListener
@Mock private lateinit var listener2: ExclusionListener
private val r1 = Region().apply { union(Rect(0, 0, 100, 200)) }
private val r2 = Region().apply { union(Rect(200, 200, 500, 800)) }
private lateinit var underTest: GestureExclusionManager
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
underTest = GestureExclusionManager(windowManager)
}
@Test
fun addListener_registers() {
underTest.addListener(listener1)
awaitTasksCompleted()
verify(windowManager)
.registerSystemGestureExclusionListener(underTest.exclusionListener, DEFAULT_DISPLAY)
}
@Test
fun addListener_again_skips_register() {
underTest.addListener(listener1)
awaitTasksCompleted()
reset(windowManager)
underTest.addListener(listener2)
awaitTasksCompleted()
verifyZeroInteractions(windowManager)
}
@Test
fun removeListener_unregisters() {
underTest.addListener(listener1)
awaitTasksCompleted()
reset(windowManager)
underTest.removeListener(listener1)
awaitTasksCompleted()
verify(windowManager)
.unregisterSystemGestureExclusionListener(underTest.exclusionListener, DEFAULT_DISPLAY)
}
@Test
fun removeListener_again_skips_unregister() {
underTest.addListener(listener1)
underTest.addListener(listener2)
awaitTasksCompleted()
reset(windowManager)
underTest.removeListener(listener1)
awaitTasksCompleted()
verifyZeroInteractions(windowManager)
}
@Test
fun onSystemGestureExclusionChanged_dispatches_to_listeners() {
underTest.addListener(listener1)
underTest.addListener(listener2)
awaitTasksCompleted()
underTest.exclusionListener.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, r1, r2)
awaitTasksCompleted()
verify(listener1).onGestureExclusionChanged(r1, r2)
verify(listener2).onGestureExclusionChanged(r1, r2)
}
@Test
fun addLister_dispatches_second_time() {
underTest.exclusionListener.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, r1, r2)
awaitTasksCompleted()
underTest.addListener(listener1)
awaitTasksCompleted()
verifyZeroInteractions(listener1)
underTest.addListener(listener2)
awaitTasksCompleted()
verifyZeroInteractions(listener1)
verify(listener2).onGestureExclusionChanged(r1, r2)
}
private fun awaitTasksCompleted() {
Executors.UI_HELPER_EXECUTOR.submit<Any> { null }.get()
Executors.MAIN_EXECUTOR.submit<Any> { null }.get()
}
}
@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false"
android:showBackdrop="true">
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
android:interpolator="@interpolator/standard_decelerate_interpolator"
android:startOffset="100"
android:duration="350" />
<translate
android:fromXDelta="-25%"
android:toXDelta="0"
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
android:interpolator="@interpolator/emphasized_interpolator"
android:startOffset="0"
android:duration="450" />
</set>
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
android:interpolator="@interpolator/standard_accelerate_interpolator"
android:startOffset="0"
android:duration="100" />
<translate
android:fromXDelta="0"
android:toXDelta="25%"
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
android:interpolator="@interpolator/emphasized_interpolator"
android:startOffset="0"
android:duration="450" />
</set>
@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false"
android:showBackdrop="true">
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
android:interpolator="@interpolator/standard_decelerate_interpolator"
android:startOffset="100"
android:duration="350" />
<translate
android:fromXDelta="25%"
android:toXDelta="0"
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
android:interpolator="@interpolator/emphasized_interpolator"
android:startOffset="0"
android:duration="450" />
</set>
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
android:interpolator="@interpolator/standard_accelerate_interpolator"
android:startOffset="0"
android:duration="100" />
<translate
android:fromXDelta="0"
android:toXDelta="-25%"
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
android:interpolator="@interpolator/emphasized_interpolator"
android:startOffset="0"
android:duration="450" />
</set>
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/system_neutral1_500" android:lStar="6" />
</selector>
Loaded 100 of 328 files, more files were not shown because too many files have changed in this diff. Show more