fix(post-clean): Cleanup mixed branch status
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
This commit is contained in:
1 parent
8fa8e5a4ba
commit
270030e5b1
328 files changed
-50906
No files matched your search
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-55
@@ -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");
|
|
||||||
}
|
|
||||||
-54
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-395
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-99
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-164
@@ -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
|
|
||||||
}
|
|
||||||
-454
@@ -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
|
|
||||||
-118
@@ -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() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-178
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-166
@@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-90
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-510
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-80
@@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-219
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-219
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-99
@@ -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
Reference in new issue
Block a user