Merge branch 'main' into Launcher3-16-QPR2-test-CHERRY

This commit is contained in:
alesimula authored and GitHub committed 2026-02-04 00:07:16 +01:00
commit d862d5391b
712 files changed
+88332 -67

No files matched your search

+8 -7
View File
@@ -1,17 +1,17 @@
# Launcher3
## 更新:20251014
* android16-s2-release
* https://android.googlesource.com/platform/packages/apps/Launcher3/+/refs/heads/android16-s2-release
## 更新:20251214
* android16-qpr2-release
* https://android.googlesource.com/platform/packages/apps/Launcher3/+/refs/heads/android16-qpr2-release
## 说明
目前支持编译:
AospWithQuickstep
L3goWithQuickstep
暂无法编译
## 主要分支
* main (android16-s2-release)--Android-16
* main (android16-qpr2-release)--Android-16
* Launcher3-16-s2-release (android16-s2-release)
* Launcher3-15-qc(高通版本,可以编译Quickstep版本)
* Launcher3-15-s1-release (android15-s1-release)
* Launcher3-14-s2-release (android14-s2-release)
@@ -34,10 +34,11 @@ https://cs.android.com/android/platform/superproject/+/master:packages/apps/Laun
## 依赖库
### wmshell
// https://android.googlesource.com/platform/frameworks/base/
> // frameworks/base/libs/WindowManager/Shell
### systemui
// https://android.googlesource.com/platform/frameworks/libs/systemui/+/refs/heads/android16-s2-release
// https://android.googlesource.com/platform/frameworks/libs/systemui/
// frameworks/libs/systemui
* aconfig/
* ambientlib/
+1 -2
View File
@@ -1,6 +1,5 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
alias(libs.plugins.android.library)
}
android {
+3 -6
View File
@@ -1,5 +1,6 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
plugins {
alias(libs.plugins.android.library)
}
android {
namespace = "com.android.app.animation"
@@ -36,10 +37,6 @@ android {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
kotlinOptions {
jvmTarget = '21'
freeCompilerArgs = ["-Xjvm-default=all"]
}
}
dependencies {
@@ -0,0 +1,30 @@
/*
* 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.compose.core.widgetpicker
import dagger.Binds
import dagger.Module
/**
* A module that provides a no-op [WidgetPickerComposeWrapper] for dagger graph that doesn't
* involve widget picker e.g. launcher preview OR when compose is disabled via build flag.
*/
@Module
interface NoOpWidgetPickerModule {
@Binds
fun bindWidgetPickerWrapper(noOp: NoOpWidgetPickerComposeWrapper): WidgetPickerComposeWrapper
}
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.compose.core.widgetpicker
import com.android.launcher3.widgetpicker.WidgetPickerActivity
import com.android.launcher3.widgetpicker.WidgetPickerConfig
import javax.annotation.Nonnull
import javax.inject.Inject
/**
* A wrapper for widget picker activity that is responsible for displaying the compose based
* widget picker in [WidgetPickerActivity] when compose is enabled via build flag.
*/
interface WidgetPickerComposeWrapper {
fun showAllWidgets(
activity: WidgetPickerActivity,
@Nonnull
widgetPickerConfig: WidgetPickerConfig
)
}
/**
* A No-op [WidgetPickerComposeWrapper] that doesn't include widget picker in dagger graph that
* don't involve widget picker e.g. launcher preview OR when compose is disabled via build flag.
*/
class NoOpWidgetPickerComposeWrapper @Inject constructor() : WidgetPickerComposeWrapper {
override fun showAllWidgets(
activity: WidgetPickerActivity,
@Nonnull
widgetPickerConfig: WidgetPickerConfig
) {
error("Widget picker with compose is not supported")
}
}
@@ -0,0 +1,26 @@
/*
* 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.compose.widgetpicker
import com.android.launcher3.compose.core.widgetpicker.NoOpWidgetPickerModule
import dagger.Module
/**
* A no-op module that is used when compose is disabled in launcher.
*/
@Module(includes = [NoOpWidgetPickerModule::class])
class LauncherWidgetPickerModule
@@ -0,0 +1,63 @@
/*
* 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.compose.widgetpicker
import com.android.launcher3.compose.core.widgetpicker.WidgetPickerComposeWrapper
import com.android.launcher3.widgetpicker.WidgetPickerComponent
import com.android.launcher3.widgetpicker.WidgetPickerComposeWrapperImpl
import com.android.launcher3.widgetpicker.data.repository.WidgetAppIconsRepository
import com.android.launcher3.widgetpicker.data.repository.WidgetUsersRepository
import com.android.launcher3.widgetpicker.data.repository.WidgetsRepository
import com.android.launcher3.widgetpicker.datasource.ConfigResourceFeaturedWidgetsDataSource
import com.android.launcher3.widgetpicker.datasource.FeaturedWidgetsDataSource
import com.android.launcher3.widgetpicker.datasource.InMemoryWidgetSearchAlgorithm
import com.android.launcher3.widgetpicker.datasource.WidgetsSearchAlgorithm
import com.android.launcher3.widgetpicker.repository.WidgetsRepositoryImpl
import com.android.launcher3.widgetpicker.repository.WidgetAppIconsRepositoryImpl
import com.android.launcher3.widgetpicker.repository.WidgetUsersRepositoryImpl
import dagger.Binds
import dagger.Module
/**
* A module that installs widget picker for launcher.
*/
@Module(subcomponents = [WidgetPickerComponent::class])
interface LauncherWidgetPickerModule {
@Binds
fun bindWidgetPickerComposeWrapper(
impl: WidgetPickerComposeWrapperImpl
): WidgetPickerComposeWrapper
@Binds
fun bindWidgetUsersRepository(impl: WidgetUsersRepositoryImpl): WidgetUsersRepository
@Binds
fun bindWidgetsRepository(impl: WidgetsRepositoryImpl): WidgetsRepository
@Binds
fun bindWidgetAppIconsRepository(impl: WidgetAppIconsRepositoryImpl): WidgetAppIconsRepository
@Binds
fun bindFeaturedWidgetsDataSource(
impl: ConfigResourceFeaturedWidgetsDataSource
): FeaturedWidgetsDataSource
@Binds
fun bindWidgetsSearchAlgorithm(
impl: InMemoryWidgetSearchAlgorithm
): WidgetsSearchAlgorithm
}
@@ -0,0 +1,58 @@
/*
* 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.util
import android.animation.TimeInterpolator
import android.graphics.drawable.Drawable
import androidx.compose.animation.core.Easing
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.graphics.drawable.toBitmap
/**
* Converts a [TimeInterpolator] to a compose [Easing].
*
* This function allows you to use Android's [TimeInterpolator] instances, such as those defined in
* `android.R.interpolator`, directly with Compose animations that expect an [Easing].
*/
fun TimeInterpolator.toComposeEasing(): Easing = Easing { fraction -> getInterpolation(fraction) }
/**
* A composable function that takes a [Drawable] and converts it into a [Painter] which can be used
* to draw the drawable on a Compose canvas.
*
* This function uses [remember] to cache the conversion of the [Drawable] to an [ImageBitmap]. This
* means that if the same [Drawable] instance is provided across recompositions, the conversion will
* only happen once, improving performance.
*/
@Composable
fun painterResource(drawable: Drawable): Painter {
val imageBitmap = remember(drawable) { drawable.toBitmap().asImageBitmap() }
return BitmapPainter(imageBitmap)
}
/**
* A multi-preview annotation that displays the same Composable with different font scales.
*/
@Preview(name = "normal font", group = "font scales", fontScale = 1f)
@Preview(name = "large font", group = "font scales", fontScale = 2f)
annotation class FontScalePreviews
@@ -0,0 +1,102 @@
/*
* 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.util
import androidx.annotation.StyleRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.DeviceFontFamilyName
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.TextUnit
import com.android.launcher3.R
/**
* Creates a Jetpack Compose [TextStyle] from an Android [R.style] resource ID.
*
* Extracts basic text-related attributes from the provided style resource and maps them to the
* corresponding [TextStyle] properties.
*
* @param styleResId The resource ID of the Android style (e.g., `R.style.MyTextStyle`).
* @return A [TextStyle] object populated with attributes from the R.style.
*/
@Composable
fun textStyleFromResource(@StyleRes styleResId: Int): TextStyle {
val context = LocalContext.current
val density = LocalDensity.current
val theme = context.theme
var textSize: TextUnit = TextUnit.Unspecified
var fontFamily: FontFamily? = null
var fontWeight: FontWeight? = null
var lineHeight: TextUnit = TextUnit.Unspecified
// -- Separate groups of similar typed attributes. --
theme.obtainStyledAttributes(styleResId, intArrayOf(android.R.attr.fontFamily))
.use { typedArray ->
if (typedArray.hasValue(0)) {
typedArray.getString(0)?.let { fontName ->
fontFamily = FontFamily(
Font(DeviceFontFamilyName(fontName), weight = FontWeight.Medium),
Font(DeviceFontFamilyName(fontName), weight = FontWeight.Normal),
)
}
}
}
theme.obtainStyledAttributes(
styleResId,
intArrayOf(android.R.attr.textSize, android.R.attr.lineHeight)
)
.use { typedArray ->
if (typedArray.hasValue(0)) {
val textSizePx = typedArray.getDimensionPixelSize(0, 0)
if (textSizePx != 0) {
textSize = with(density) { textSizePx.toSp() }
}
}
if (typedArray.hasValue(1)) {
val lineHeightPx = typedArray.getDimensionPixelSize(1, 0)
if (lineHeightPx != 0) {
lineHeight = with(density) { lineHeightPx.toSp() }
}
}
}
theme.obtainStyledAttributes(
styleResId,
intArrayOf(android.R.attr.fontWeight)
).use { typedArray ->
if (typedArray.hasValue(0)) {
val fontWeightInt = typedArray.getInt(0, -1)
if (fontWeightInt != -1) {
fontWeight = FontWeight(fontWeightInt)
}
}
}
return TextStyle(
fontSize = textSize,
lineHeight = lineHeight,
fontFamily = fontFamily,
fontWeight = fontWeight,
)
}
@@ -0,0 +1,277 @@
/*
* 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.widgetpicker
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalView
import com.android.launcher3.Launcher
import com.android.launcher3.LauncherSettings.Favorites
import com.android.launcher3.R
import com.android.launcher3.compose.ComposeFacade
import com.android.launcher3.compose.core.widgetpicker.WidgetPickerComposeWrapper
import com.android.launcher3.concurrent.annotations.BackgroundContext
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.util.ApiWrapper
import com.android.launcher3.widgetpicker.WidgetPickerConfig.Companion.EXTRA_IS_PENDING_WIDGET_DRAG
import com.android.launcher3.widgetpicker.data.repository.WidgetAppIconsRepository
import com.android.launcher3.widgetpicker.data.repository.WidgetUsersRepository
import com.android.launcher3.widgetpicker.data.repository.WidgetsRepository
import com.android.launcher3.widgetpicker.listeners.WidgetPickerAddItemListener
import com.android.launcher3.widgetpicker.listeners.WidgetPickerDragItemListener
import com.android.launcher3.widgetpicker.logging.LauncherWidgetPickerCuiReporter
import com.android.launcher3.widgetpicker.shared.model.CloseBehavior
import com.android.launcher3.widgetpicker.shared.model.HostConstraint
import com.android.launcher3.widgetpicker.shared.model.WidgetHostInfo
import com.android.launcher3.widgetpicker.shared.model.isAppWidget
import com.android.launcher3.widgetpicker.theme.LauncherWidgetPickerTheme
import com.android.launcher3.widgetpicker.ui.WidgetInteractionInfo
import com.android.launcher3.widgetpicker.ui.WidgetInteractionSource
import com.android.launcher3.widgetpicker.ui.WidgetPickerEventListeners
import javax.inject.Inject
import javax.inject.Provider
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.launch
/**
* An helper that bootstraps widget picker UI (from [WidgetPickerComponent]) in to
* [WidgetPickerActivity] when compose is available and widget picker refactor flags are on.
*
* Sets up the bindings necessary for widget picker component.
*/
class WidgetPickerComposeWrapperImpl
@Inject
constructor(
private val widgetPickerComponentProvider: Provider<WidgetPickerComponent.Factory>,
private val widgetsRepository: WidgetsRepository,
private val widgetUsersRepository: WidgetUsersRepository,
private val widgetAppIconsRepository: WidgetAppIconsRepository,
@BackgroundContext private val backgroundContext: CoroutineContext,
@ApplicationContext private val appContext: Context,
private val apiWrapper: ApiWrapper,
) : WidgetPickerComposeWrapper {
override fun showAllWidgets(
activity: WidgetPickerActivity,
widgetPickerConfig: WidgetPickerConfig,
) {
val widgetPickerComponent = newWidgetPickerComponent(widgetPickerConfig)
val callbacks = activity.buildEventListeners(widgetPickerConfig, apiWrapper)
val uiEventsReporter = LauncherWidgetPickerCuiReporter(activity.statsLogManager)
val fullWidgetsCatalog = widgetPickerComponent.getFullWidgetsCatalog()
val composeView = ComposeFacade.initComposeView(activity.asContext()) as ComposeView
composeView.apply {
setContent {
val scope = rememberCoroutineScope()
val view = LocalView.current
LauncherWidgetPickerTheme {
val eventListeners = remember { callbacks }
fullWidgetsCatalog.Content(eventListeners, uiEventsReporter)
}
DisposableEffect(view) {
scope.launch { initializeRepositories() }
onDispose { cleanUpRepositories() }
}
}
}
checkNotNull(activity.dragLayer).addView(composeView)
}
private fun newWidgetPickerComponent(
widgetPickerConfig: WidgetPickerConfig
): WidgetPickerComponent {
return widgetPickerComponentProvider
.get()
.build(
widgetsRepository = widgetsRepository,
widgetUsersRepository = widgetUsersRepository,
widgetAppIconsRepository = widgetAppIconsRepository,
widgetHostInfo =
WidgetHostInfo(
title =
widgetPickerConfig.title
?: appContext.resources.getString(R.string.widget_button_text),
description = widgetPickerConfig.description,
constraints = widgetPickerConfig.asHostConstraints(),
showDragShadow = !widgetPickerConfig.isForHomeScreen,
enableSwipeUpToDismiss = widgetPickerConfig.enableSwipeUpToDismiss,
closeBehavior =
if (widgetPickerConfig.isDesktopFormFactor) CloseBehavior.CLOSE_BUTTON
else CloseBehavior.DRAG_HANDLE,
),
backgroundContext = backgroundContext,
)
}
private fun initializeRepositories() {
widgetsRepository.initialize()
widgetUsersRepository.initialize()
widgetAppIconsRepository.initialize()
}
private fun cleanUpRepositories() {
widgetsRepository.cleanUp()
widgetUsersRepository.cleanUp()
widgetAppIconsRepository.cleanUp()
}
companion object {
private const val TAG = "WidgetPickerComposeWrapperImpl"
private const val HOME_SCREEN_WIDGET_INTERACTION_REASON_STRING =
"WidgetPickerActivity.OnWidgetInteraction"
private fun WidgetPickerActivity.buildEventListeners(
widgetPickerConfig: WidgetPickerConfig,
apiWrapper: ApiWrapper,
) =
object : WidgetPickerEventListeners {
override fun onClose() {
Log.d(TAG, "Closing widget picker")
finish()
}
override fun onWidgetInteraction(widgetInteractionInfo: WidgetInteractionInfo) {
if (widgetPickerConfig.isForHomeScreen) {
handleWidgetInteractionForHomeScreen(widgetInteractionInfo, apiWrapper)
} else {
handleWidgetInteractionForExternalHost(widgetInteractionInfo)
}
}
}
/**
* Handles communication with the home screen about the "add" and "drag" interactions on
* widgets within widget picker.
*
* For home screen, we register a listener that is called back when home screen is shown;
* - WidgetPickerDragItemListener: bootstraps the drag helper that displays the shadow and
* handles the drag until completion.
* - WidgetPickerAddItemListener: once launcher is shown, triggers the flow to add the
* widget to workspace.
*/
private fun WidgetPickerActivity.handleWidgetInteractionForHomeScreen(
interactionInfo: WidgetInteractionInfo,
apiWrapper: ApiWrapper,
) {
val interactionListener =
when (interactionInfo) {
is WidgetInteractionInfo.WidgetDragInfo ->
WidgetPickerDragItemListener(
container = interactionInfo.source.toContainer(),
mimeType = interactionInfo.mimeType,
widgetInfo = interactionInfo.widgetInfo,
widgetPreview = interactionInfo.previewInfo,
previewRect = interactionInfo.bounds,
previewWidth = interactionInfo.widthPx,
)
is WidgetInteractionInfo.WidgetAddInfo ->
WidgetPickerAddItemListener(
container = interactionInfo.source.toContainer(),
widgetInfo = interactionInfo.widgetInfo,
)
}
Launcher.ACTIVITY_TRACKER.registerCallback(
interactionListener,
HOME_SCREEN_WIDGET_INTERACTION_REASON_STRING,
)
startActivity(
/*intent=*/ Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_HOME)
.setPackage(packageName)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
/*options=*/ apiWrapper.createFadeOutAnimOptions().toBundle(),
)
finish()
}
/**
* Handles communication with the external host about the "add" and "drag" interactions on
* widgets within widget picker.
* - In case of drag and drop, finishes the activity with result indicating that there is a
* pending drag [EXTRA_IS_PENDING_WIDGET_DRAG] (that would contain the widget info as part
* of clip data) that the host should be handling.
* - In case of add, finishes the activity with result containing extra information about
* the widget being added (namely [Intent.EXTRA_COMPONENT_NAME] and [Intent.EXTRA_USER].
*/
private fun WidgetPickerActivity.handleWidgetInteractionForExternalHost(
widgetInteractionInfo: WidgetInteractionInfo
) {
when (widgetInteractionInfo) {
is WidgetInteractionInfo.WidgetDragInfo ->
setResult(RESULT_OK, Intent().putExtra(EXTRA_IS_PENDING_WIDGET_DRAG, true))
is WidgetInteractionInfo.WidgetAddInfo -> {
val widgetInfo = widgetInteractionInfo.widgetInfo
if (widgetInfo.isAppWidget()) {
val providerInfo = widgetInfo.appWidgetProviderInfo
setResult(
RESULT_OK,
Intent().apply {
putExtra(Intent.EXTRA_COMPONENT_NAME, providerInfo.provider)
putExtra(Intent.EXTRA_USER, providerInfo.profile)
},
)
} else {
throw IllegalStateException(
"AppWidgetInfo not provided for external host drag"
)
}
}
}
finish()
}
/** Builds the host constraints to provide to the widget picker module. */
fun WidgetPickerConfig.asHostConstraints() = buildList {
if (filteredUsers.isNotEmpty()) {
add(HostConstraint.HostUserConstraint(filteredUsers))
}
if (!isForHomeScreen) {
add(HostConstraint.NoShortcutsConstraint)
}
if (categoryInclusionFilter != 0 || categoryExclusionFilter != 0) {
add(
HostConstraint.HostCategoryConstraint(
categoryInclusionMask = categoryInclusionFilter,
categoryExclusionMask = categoryExclusionFilter,
)
)
}
}
private fun WidgetInteractionSource.toContainer(): Int =
when (this) {
WidgetInteractionSource.FEATURED -> Favorites.CONTAINER_WIDGETS_PREDICTION
WidgetInteractionSource.SEARCH,
WidgetInteractionSource.BROWSE -> Favorites.CONTAINER_WIDGETS_TRAY
}
}
}
@@ -0,0 +1,104 @@
/*
* 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.widgetpicker.datasource
import android.content.Context
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.R
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize
import com.android.launcher3.widgetpicker.shared.model.PickableWidget
import com.android.launcher3.widgetpicker.shared.model.WidgetApp
import com.android.launcher3.widgetpicker.shared.model.isAppWidget
import java.util.Arrays
import java.util.stream.Collectors
import javax.inject.Inject
/**
* An implementation of [FeaturedWidgetsDataSource] that provides featured widgets based on a static
* configuration from resources and pre-defined size templates.
*
* Only appwidgets; no shortcuts
*/
@LauncherAppSingleton
class ConfigResourceFeaturedWidgetsDataSource
@Inject
constructor(
@ApplicationContext private val appContext: Context,
private val idp: InvariantDeviceProfile,
) : FeaturedWidgetsDataSource {
// the package part in component name e.g. "com.example" in {com.example/widget.Provider}
private var eligiblePackages: Set<String> = emptySet()
override suspend fun initialize() {
if (eligiblePackages.isEmpty()) {
eligiblePackages =
Arrays.stream(
appContext.resources.getStringArray(R.array.default_featured_widget_apps)
)
.collect(Collectors.toSet())
}
}
override suspend fun getFeaturedWidgets(widgetApps: List<WidgetApp>): List<PickableWidget> {
val widgetsByContainerSize =
widgetApps
.shuffled()
// pick only one of user profiles
.distinctBy { Pair(it.id.packageName, it.id.category) }
.flatMap { it.widgets }
.filter {
eligiblePackages.isEmpty() ||
eligiblePackages.contains(it.id.componentName.packageName)
}
.groupBy {
WidgetPreviewContainerSize(
it.sizeInfo.containerSpanX,
it.sizeInfo.containerSpanY,
)
}
val selected: MutableList<PickableWidget> = mutableListOf()
val usedAppIds: MutableSet<String> = mutableSetOf()
val sizesToPick =
WidgetPreviewContainerSize.pickTemplateForFeaturedWidgets(
idp.getDeviceProfile(appContext)
)
for (sizeToPick in sizesToPick) {
widgetsByContainerSize[sizeToPick]?.shuffled()?.let { items ->
for (item in items) {
if (
item.widgetInfo.isAppWidget() &&
!usedAppIds.contains(item.appId.packageName)
) {
selected.add(item)
usedAppIds.add(item.appId.packageName)
break
}
}
}
}
return selected
}
override fun cleanup() {
eligiblePackages = emptySet()
}
}
@@ -0,0 +1,40 @@
/*
* 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.widgetpicker.datasource
import com.android.launcher3.widgetpicker.shared.model.PickableWidget
import com.android.launcher3.widgetpicker.shared.model.WidgetApp
/**
* An interface representing a datasource that provides featured widgets.
*/
interface FeaturedWidgetsDataSource {
/**
* Perform any initializations here.
*/
suspend fun initialize()
/**
* Provides widgets from the given list that can be featured in widget picker.
*/
suspend fun getFeaturedWidgets(widgetApps: List<WidgetApp>): List<PickableWidget>
/**
* Clear any service bindings or state here.
*/
fun cleanup()
}
@@ -0,0 +1,168 @@
/*
* 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.widgetpicker.datasource
import com.android.launcher3.concurrent.annotations.BackgroundContext
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.widgetpicker.shared.model.WidgetApp
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
/**
* A simple in-memory implementation of [WidgetsSearchAlgorithm] that matches input query against
* the app labels, widget title and description.
*
* More weight is given to app names than widget title; and description has lowest weight in scoring
* the results.
*/
@LauncherAppSingleton
class InMemoryWidgetSearchAlgorithm @Inject constructor(
@BackgroundContext
private val backgroundContext: CoroutineContext,
) : WidgetsSearchAlgorithm {
override suspend fun initialize() {}
override suspend fun searchWidgets(
query: String,
corpus: List<WidgetApp>
): List<WidgetApp> = coroutineScope {
// Ideally, uses search only one word, but supporting multiple words.
val queryWords = query.trim().lowercase().split("\\s+".toRegex())
.filter { it.isNotBlank() }
if (queryWords.isEmpty()) {
return@coroutineScope emptyList()
}
val results = corpus.map { widgetApp ->
async(backgroundContext) {
matchAndScoreWidgetApp(widgetApp, queryWords)
}
}
results.awaitAll()
.filter { it.first.widgets.isNotEmpty() && it.second > 0 }
.sortedByDescending { it.second }
.map { it.first } // Return just the WidgetApp
}
private fun matchAndScoreWidgetApp(
widgetApp: WidgetApp,
queryWords: List<String>
): Pair<WidgetApp, Int> {
val appTitleScore = widgetApp.title?.let {
matchAndCalculateScore(
queryWords = queryWords,
targetText = widgetApp.title.toString(),
matchType = MatchType.APP_TITLE
)
} ?: 0
val scoredWidgets = widgetApp.widgets.map { widget ->
val labelScore =
matchAndCalculateScore(
queryWords = queryWords,
targetText = widget.label,
matchType = MatchType.WIDGET_LABEL
)
val descriptionScore = widget.description?.let {
matchAndCalculateScore(
queryWords = queryWords,
targetText = it.toString(),
matchType = MatchType.WIDGET_DESCRIPTION
)
} ?: 0
val totalWidgetScore = labelScore + descriptionScore
widget to totalWidgetScore
}
.filter { it.second > 0 }
.sortedByDescending { it.second }
val totalAppScore = appTitleScore + scoredWidgets.sumOf { it.second }
return if (appTitleScore > 0) {
widgetApp to appTitleScore + totalAppScore
} else {
widgetApp.copy(
widgets = scoredWidgets.map { it.first }
) to totalAppScore
}
}
private fun matchAndCalculateScore(
queryWords: List<String>,
targetText: String,
matchType: MatchType
): Int {
var totalScore = 0
val wordsInTarget = targetText
.lowercase()
.split("\\s+".toRegex()).filter { it.isNotBlank() }
for (queryWord in queryWords) {
var wordScore = 0
for (targetWord in wordsInTarget) {
wordScore = wordScore.coerceAtLeast(
calculateSingleWordScore(
queryWord,
targetWord,
matchType
)
)
}
totalScore += wordScore
}
return totalScore
}
private fun calculateSingleWordScore(
queryWord: String,
targetWord: String,
matchType: MatchType
): Int {
val baseScore = when {
// exact matches are score higher.
targetWord == queryWord -> 2
// Then the matches that begin with given input as prefix
targetWord.startsWith(queryWord) -> 1
else -> 0
}
return baseScore * matchType.weightFactor
}
private enum class MatchType(val weightFactor: Int) {
// Highest weight to app title matches; users are likely to search for apps
APP_TITLE(12),
// Medium weight to widget labels (lower than app title as some widgets might have app title
// in name and based on number of items, might add unnecessary weight).
WIDGET_LABEL(3),
// Lowest weight to description; description might might not be as impactful as the labels
// or app title; but, it might still help in cases where alternate words for the widgets
// functionality are used (e.g. stocks vs watchlist).
WIDGET_DESCRIPTION(1)
}
override fun cleanup() {}
}
@@ -0,0 +1,39 @@
/*
* 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.widgetpicker.datasource
import com.android.launcher3.widgetpicker.shared.model.WidgetApp
/**
* An interface representing algorithm used to search widgets for widget picker.
*/
interface WidgetsSearchAlgorithm {
/**
* Perform any initializations here
*/
suspend fun initialize()
/**
* Returns apps and its widgets that match the given input string.
*/
suspend fun searchWidgets(query: String, corpus: List<WidgetApp>): List<WidgetApp>
/**
* Clear any service bindings or state here.
*/
fun cleanup()
}
@@ -0,0 +1,76 @@
/*
* 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.widgetpicker.listeners
import android.view.View
import com.android.launcher3.Launcher
import com.android.launcher3.PendingAddItemInfo
import com.android.launcher3.logging.StatsLogManager.LauncherEvent
import com.android.launcher3.pm.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVO
import com.android.launcher3.util.ContextTracker.SchedulerCallback
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
import com.android.launcher3.widget.PendingAddShortcutInfo
import com.android.launcher3.widget.PendingAddWidgetInfo
import com.android.launcher3.widgetpicker.shared.model.WidgetInfo
/**
* A callback listener (for tap-to-add flow) that handles adding a widget from a separate widget
* picker activity. Invoked once widget picker is closed and home screen is showing / ready.
*
* Also logs to stats logger once widget is added.
*
* @param container the Favorites container that the widget was interacted from e.g.
* CONTAINER_WIDGETS_PREDICTION; enables the stats logs to capture the source container.
* @param widgetInfo metadata of the widget being added
*/
class WidgetPickerAddItemListener(private val container: Int, private val widgetInfo: WidgetInfo) :
SchedulerCallback<Launcher> {
override fun init(launcher: Launcher?, isHomeStarted: Boolean): Boolean {
checkNotNull(launcher)
val pendingAddItemInfo: PendingAddItemInfo =
when (widgetInfo) {
is WidgetInfo.AppWidgetInfo -> {
val launcherProviderInfo =
LauncherAppWidgetProviderInfo.fromProviderInfo(
launcher,
widgetInfo.appWidgetProviderInfo,
)
PendingAddWidgetInfo(launcherProviderInfo, container)
}
is WidgetInfo.ShortcutInfo ->
PendingAddShortcutInfo(
ShortcutConfigActivityInfoVO(widgetInfo.launcherActivityInfo)
)
}
val view = View(launcher)
view.tag = pendingAddItemInfo
launcher.accessibilityDelegate?.addToWorkspace(
/*item=*/ pendingAddItemInfo,
/*accessibility=*/ false,
) {
launcher.statsLogManager
.logger()
.withItemInfo(pendingAddItemInfo)
.log(LauncherEvent.LAUNCHER_WIDGET_ADD_BUTTON_TAP)
}
return false // don't receive any more callbacks as we got launcher and handled it
}
}
@@ -0,0 +1,132 @@
/*
* 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.widgetpicker.listeners
import android.graphics.Point
import android.graphics.Rect
import android.view.View
import com.android.launcher3.PendingAddItemInfo
import com.android.launcher3.dragndrop.BaseItemDragListener
import com.android.launcher3.dragndrop.DragOptions
import com.android.launcher3.pm.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVO
import com.android.launcher3.widget.DatabaseWidgetPreviewLoader.WidgetPreviewInfo
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
import com.android.launcher3.widget.PendingAddShortcutInfo
import com.android.launcher3.widget.PendingAddWidgetInfo
import com.android.launcher3.widget.PendingItemDragHelper
import com.android.launcher3.widgetpicker.shared.model.WidgetInfo
import com.android.launcher3.widgetpicker.shared.model.WidgetPreview
import com.android.launcher3.widgetpicker.shared.model.isAppWidget
/**
* A callback listener of type [BaseItemDragListener] that handles widget drag and drop from widget
* picker hosted in a separate activity than home screen.
*
* Responsible for initializing the [PendingItemDragHelper] that then handles the rest of the drag
* and drop (including showing a drag shadow for the widget).
*
* @param container the source container in which widget was interacted with; enables the stats logs
* to capture the source container.
* @param mimeType a mime type used by widget picker when attaching this listener for a specific
* widget's drag and drop session.
* @param widgetInfo metadata of the widget being dragged
* @param widgetPreview provides the preview information for widgets
* @param previewRect the bounds of widget's preview offset by the point of long press
* @param previewWidth width of the preview as it appears in the widget picker.
*/
class WidgetPickerDragItemListener(
private val container: Int,
private val mimeType: String,
private val widgetInfo: WidgetInfo,
private val widgetPreview: WidgetPreview,
previewRect: Rect,
previewWidth: Int,
) : BaseItemDragListener(previewRect, previewWidth, previewWidth) {
override fun getMimeType(): String = mimeType
override fun startDrag(
previewRect: Rect,
previewBitmapWidth: Int,
previewViewWidth: Int,
screenPos: Point,
options: DragOptions,
) {
val pendingAddItemInfo: PendingAddItemInfo =
when (widgetInfo) {
is WidgetInfo.AppWidgetInfo -> {
val launcherProviderInfo =
LauncherAppWidgetProviderInfo.fromProviderInfo(
mLauncher,
widgetInfo.appWidgetProviderInfo,
)
PendingAddWidgetInfo(launcherProviderInfo, container)
}
is WidgetInfo.ShortcutInfo ->
PendingAddShortcutInfo(
ShortcutConfigActivityInfoVO(widgetInfo.launcherActivityInfo)
)
}
val view = View(mLauncher)
view.tag = pendingAddItemInfo
val dragHelper = PendingItemDragHelper(view)
if (widgetInfo.isAppWidget()) {
setAppWidgetPreviewInfo(widgetPreview, widgetInfo, dragHelper)
} // shortcut preview is fetched by home screen.
dragHelper.startDrag(
previewRect,
previewBitmapWidth,
previewViewWidth,
screenPos,
/*source=*/ this,
options,
)
}
private fun setAppWidgetPreviewInfo(
appWidgetPreview: WidgetPreview,
widgetInfo: WidgetInfo.AppWidgetInfo,
dragHelper: PendingItemDragHelper,
) {
val info = WidgetPreviewInfo()
when (appWidgetPreview) {
is WidgetPreview.BitmapWidgetPreview -> {
info.previewBitmap = appWidgetPreview.bitmap
info.providerInfo = widgetInfo.appWidgetProviderInfo
}
is WidgetPreview.ProviderInfoWidgetPreview -> {
info.providerInfo = appWidgetPreview.providerInfo
}
is WidgetPreview.RemoteViewsWidgetPreview -> {
info.remoteViews = appWidgetPreview.remoteViews
info.providerInfo = widgetInfo.appWidgetProviderInfo
}
else ->
throw IllegalStateException(
"Unsupported preview type when dropping widget to launcher"
)
}
dragHelper.setWidgetPreviewInfo(info)
}
}
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.widgetpicker.logging
import android.view.View
import com.android.launcher3.logging.StatsLogManager
import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_PICKER_APP_EXPAND_ANIMATION_BEGIN
import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_PICKER_APP_EXPAND_ANIMATION_END
import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_PICKER_OPEN_ANIMATION_BEGIN
import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_PICKER_OPEN_ANIMATION_END
import com.android.launcher3.widgetpicker.ui.WidgetPickerCui
import com.android.launcher3.widgetpicker.ui.WidgetPickerCuiReporter
/** Reports widget picker's UI events to stats logger for instrumentation purposes. */
class LauncherWidgetPickerCuiReporter(private val statsLogManager: StatsLogManager) :
WidgetPickerCuiReporter {
override fun report(event: WidgetPickerCui, view: View) {
val launcherUiEvent =
when (event) {
WidgetPickerCui.OPEN_ANIMATION_BEGIN -> LAUNCHER_WIDGET_PICKER_OPEN_ANIMATION_BEGIN
WidgetPickerCui.OPEN_ANIMATION_END -> LAUNCHER_WIDGET_PICKER_OPEN_ANIMATION_END
WidgetPickerCui.WIDGET_APP_EXPAND_BEGIN ->
LAUNCHER_WIDGET_PICKER_APP_EXPAND_ANIMATION_BEGIN
WidgetPickerCui.WIDGET_APP_EXPAND_END ->
LAUNCHER_WIDGET_PICKER_APP_EXPAND_ANIMATION_END
}
statsLogManager.logger().sendToInteractionJankMonitor(launcherUiEvent, view)
}
}
@@ -0,0 +1,84 @@
/*
* 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.widgetpicker.repository
import com.android.launcher3.icons.BitmapInfo
import com.android.launcher3.icons.IconCache
import com.android.launcher3.icons.cache.CacheLookupFlag
import com.android.launcher3.model.data.PackageItemInfo
import com.android.launcher3.widgetpicker.data.repository.WidgetAppIconsRepository
import com.android.launcher3.widgetpicker.shared.model.AppIcon
import com.android.launcher3.widgetpicker.shared.model.AppIconBadge
import com.android.launcher3.widgetpicker.shared.model.WidgetAppIcon
import com.android.launcher3.widgetpicker.shared.model.WidgetAppId
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import javax.inject.Inject
/**
* An implementation of [WidgetAppIconsRepository] that provides the app icons for the widget picker
* using the [IconCache].
*/
class WidgetAppIconsRepositoryImpl @Inject constructor(
private val iconCache: IconCache
) : WidgetAppIconsRepository {
override fun initialize() {} // nothing to do here.
override fun getAppIcon(widgetAppId: WidgetAppId) = callbackFlow {
trySend(WidgetAppIcon(AppIcon.PlaceHolderAppIcon, AppIconBadge.NoBadge))
val category = widgetAppId.category
val packageItemInfo = if (category != null) {
PackageItemInfo(widgetAppId.packageName, category, widgetAppId.userHandle)
} else {
PackageItemInfo(widgetAppId.packageName, widgetAppId.userHandle)
}
iconCache.updateIconInBackground({ itemInfoWithIcon ->
itemInfoWithIcon?.let {
if (itemInfoWithIcon.bitmap.isLowRes) {
trySend(
WidgetAppIcon(
icon = AppIcon.LowResColorIcon(itemInfoWithIcon.bitmap.color),
badge = AppIconBadge.NoBadge
)
)
} else {
trySend(
WidgetAppIcon(
icon = AppIcon.HighResBitmapIcon(
bitmap = itemInfoWithIcon.bitmap.icon,
isFullBleed = itemInfoWithIcon.bitmap.flags and
BitmapInfo.FLAG_FULL_BLEED == BitmapInfo.FLAG_FULL_BLEED
),
badge = itemInfoWithIcon.bitmap.getBadgeDrawableInfo()?.let {
AppIconBadge.DrawableBadge(
it.drawableRes,
it.colorRes
)
} ?: AppIconBadge.NoBadge
)
)
}
}
}, packageItemInfo, CacheLookupFlag.DEFAULT_LOOKUP_FLAG)
awaitClose()
}
override fun cleanUp() {} // nothing to do here.
}
@@ -0,0 +1,120 @@
/*
* 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.widgetpicker.repository
import android.content.Context
import android.os.UserHandle
import com.android.launcher3.concurrent.annotations.BackgroundContext
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.model.StringCache
import com.android.launcher3.pm.UserCache
import com.android.launcher3.util.SafeCloseable
import com.android.launcher3.widgetpicker.data.repository.WidgetUsersRepository
import com.android.launcher3.widgetpicker.shared.model.WidgetUserProfile
import com.android.launcher3.widgetpicker.shared.model.WidgetUserProfileType
import com.android.launcher3.widgetpicker.shared.model.WidgetUserProfiles
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
/**
* An implementation of [WidgetUsersRepository] that provides user profile info to widget picker by
* looking up the [UserCache].
*/
class WidgetUsersRepositoryImpl
@Inject
constructor(
@ApplicationContext private val appContext: Context,
private val userCache: UserCache,
@BackgroundContext private val backgroundContext: CoroutineContext,
) : WidgetUsersRepository {
private var stringCache: StringCache = StringCache.EMPTY
private var closableUseChangeListener: SafeCloseable? = null
private val _userProfiles = MutableStateFlow<WidgetUserProfiles?>(null)
private var workProfileUser: UserHandle? = null
private var backgroundScope =
CoroutineScope(
SupervisorJob() +
backgroundContext +
CoroutineName("widgetUsersRepositoryBackgroundWork")
)
override fun initialize() {
backgroundScope.launch {
stringCache = StringCache.fromContext(appContext)
maybeUpdate(changedUser = null)
closableUseChangeListener?.close()
closableUseChangeListener =
userCache.addUserEventListener { userHandle, _ -> maybeUpdate(userHandle) }
}
}
override fun observeUserProfiles(): Flow<WidgetUserProfiles?> = _userProfiles.asStateFlow()
override fun getWorkProfileUser(): UserHandle? = workProfileUser
override fun cleanUp() {
closableUseChangeListener?.close()
backgroundScope.apply {
if (isActive) {
cancel()
}
}
}
private fun maybeUpdate(changedUser: UserHandle?) {
workProfileUser = userCache.userProfiles.firstOrNull { userCache.getUserInfo(it).isWork }
val needsUpdate = changedUser == null || changedUser == workProfileUser
if (needsUpdate) {
val isUserQuiet =
workProfileUser?.let { userCache.userManagerState.isUserQuiet(it) } ?: false
_userProfiles.update {
WidgetUserProfiles(
personal =
WidgetUserProfile(
type = WidgetUserProfileType.PERSONAL,
label = stringCache.widgetsPersonalTab ?: "",
paused = false,
pausedProfileMessage = null,
),
work =
workProfileUser?.let {
WidgetUserProfile(
type = WidgetUserProfileType.WORK,
label = stringCache.widgetsWorkTab ?: "",
paused = isUserQuiet,
pausedProfileMessage = stringCache.workProfilePausedTitle,
)
},
)
}
}
}
}
@@ -0,0 +1,228 @@
/*
* 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.widgetpicker.repository
import android.content.Context
import com.android.launcher3.DeviceProfile
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.concurrent.annotations.BackgroundContext
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.model.WidgetItem
import com.android.launcher3.model.WidgetsModel
import com.android.launcher3.model.data.PackageItemInfo
import com.android.launcher3.pm.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVO
import com.android.launcher3.util.ComponentKey
import com.android.launcher3.util.Executors.MODEL_EXECUTOR
import com.android.launcher3.util.PackageUserKey
import com.android.launcher3.widget.DatabaseWidgetPreviewLoader
import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize
import com.android.launcher3.widget.util.WidgetSizes
import com.android.launcher3.widgetpicker.data.repository.WidgetsRepository
import com.android.launcher3.widgetpicker.datasource.FeaturedWidgetsDataSource
import com.android.launcher3.widgetpicker.datasource.WidgetsSearchAlgorithm
import com.android.launcher3.widgetpicker.shared.model.PickableWidget
import com.android.launcher3.widgetpicker.shared.model.WidgetApp
import com.android.launcher3.widgetpicker.shared.model.WidgetAppId
import com.android.launcher3.widgetpicker.shared.model.WidgetId
import com.android.launcher3.widgetpicker.shared.model.WidgetInfo
import com.android.launcher3.widgetpicker.shared.model.WidgetPreview
import com.android.launcher3.widgetpicker.shared.model.WidgetSizeInfo
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* An implementation of the [WidgetsRepository] that provides widgets for widget picker using the
* [WidgetsModel], [FeaturedWidgetsDataSource] & enables search using the provided
* [WidgetsSearchAlgorithm].
*/
class WidgetsRepositoryImpl
@Inject
constructor(
@ApplicationContext private val appContext: Context,
idp: InvariantDeviceProfile,
private val widgetsModel: WidgetsModel,
private val featuredWidgetsDataSource: FeaturedWidgetsDataSource,
private val searchAlgorithm: WidgetsSearchAlgorithm,
@BackgroundContext private val backgroundContext: CoroutineContext,
) : WidgetsRepository {
private val deviceProfile = idp.getDeviceProfile(appContext)
private val backgroundScope =
CoroutineScope(SupervisorJob() + backgroundContext + CoroutineName("WidgetsRepository"))
private val _widgetItemsByPackage = MutableStateFlow<List<WidgetApp>>(emptyList())
private val databaseWidgetPreviewLoader = DatabaseWidgetPreviewLoader(appContext, deviceProfile)
override fun initialize(options: WidgetsRepository.InitializationOptions) {
val packageUserKeyOrAll =
options.widgetAppId?.let {
checkNotNull(it.packageName) { "invalid package name passed" }
PackageUserKey(it.packageName, it.userHandle)
}
// TODO(b/419495339): Remove the model executor requirement from widgets model and replace
// with scope.launch
MODEL_EXECUTOR.execute {
widgetsModel.update(/* packageUser= */ packageUserKeyOrAll)
_widgetItemsByPackage.update {
widgetsModel.widgetsByPackageItemForPicker.toPickableWidgets(deviceProfile)
}
}
if (options.loadFeaturedWidgets) {
backgroundScope.launch { featuredWidgetsDataSource.initialize() }
}
}
override fun observeWidgets(): Flow<List<WidgetApp>> = _widgetItemsByPackage.asStateFlow()
override fun observeWidgetApp(widgetAppId: WidgetAppId): Flow<WidgetApp?> =
_widgetItemsByPackage
.map { apps -> apps.firstOrNull { it.id == widgetAppId } }
.distinctUntilChanged()
override suspend fun getWidgetPreview(id: WidgetId): WidgetPreview {
val componentKey = ComponentKey(id.componentName, id.userHandle)
val widgetItem =
widgetsModel.widgetsByComponentKey[componentKey]
?: return WidgetPreview.PlaceholderWidgetPreview
val previewSizePx = WidgetSizes.getWidgetItemSizePx(appContext, deviceProfile, widgetItem)
val preview =
withContext(backgroundContext) {
val result =
databaseWidgetPreviewLoader.generatePreviewInfoBg(
widgetItem,
previewSizePx.width,
previewSizePx.height,
)
when {
result.remoteViews != null ->
WidgetPreview.RemoteViewsWidgetPreview(result.remoteViews)
result.providerInfo != null ->
WidgetPreview.ProviderInfoWidgetPreview(result.providerInfo)
result.previewBitmap != null ->
WidgetPreview.BitmapWidgetPreview(result.previewBitmap)
else -> WidgetPreview.PlaceholderWidgetPreview
}
}
return preview
}
override suspend fun searchWidgets(query: String): List<WidgetApp> =
searchAlgorithm.searchWidgets(query, _widgetItemsByPackage.value)
override fun cleanUp() {
_widgetItemsByPackage.update { emptyList() }
backgroundScope.apply {
if (isActive) {
cancel()
}
}
}
override fun getFeaturedWidgets(): Flow<List<PickableWidget>> {
return _widgetItemsByPackage
.map { widgets -> featuredWidgetsDataSource.getFeaturedWidgets(widgets) }
.flowOn(backgroundContext)
}
companion object {
private fun Map<PackageItemInfo, List<WidgetItem>>.toPickableWidgets(
deviceProfile: DeviceProfile
) = map { (packageItemInfo, widgetItems) ->
val widgetAppId =
WidgetAppId(
packageName = packageItemInfo.packageName,
userHandle = packageItemInfo.user,
category = packageItemInfo.widgetCategory,
)
WidgetApp(
id = widgetAppId,
title = packageItemInfo.title,
widgets =
widgetItems.map { widgetItem ->
val previewSize =
WidgetSizes.getWidgetSizePx(
deviceProfile,
widgetItem.spanX,
widgetItem.spanY,
)
val containerSpan =
WidgetPreviewContainerSize.forItem(widgetItem, deviceProfile)
val containerSize =
WidgetSizes.getWidgetSizePx(
deviceProfile,
containerSpan.spanX,
containerSpan.spanY,
)
PickableWidget(
id =
WidgetId(
componentName = widgetItem.componentName,
userHandle = widgetItem.user,
),
appId = widgetAppId,
label = widgetItem.label,
description = widgetItem.description,
widgetInfo =
if (widgetItem.widgetInfo != null) {
WidgetInfo.AppWidgetInfo(
appWidgetProviderInfo = widgetItem.widgetInfo.clone()
)
} else {
check(widgetItem.activityInfo is ShortcutConfigActivityInfoVO)
WidgetInfo.ShortcutInfo(
launcherActivityInfo = widgetItem.activityInfo.mInfo
)
},
sizeInfo =
WidgetSizeInfo(
spanX = widgetItem.spanX,
spanY = widgetItem.spanY,
widthPx = previewSize.width,
heightPx = previewSize.height,
containerSpanX = containerSpan.spanX,
containerSpanY = containerSpan.spanY,
containerWidthPx = containerSize.width,
containerHeightPx = containerSize.height,
),
)
},
)
}
}
}
@@ -0,0 +1,199 @@
/*
* 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.widgetpicker.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import com.android.launcher3.R
import com.android.launcher3.widgetpicker.ui.theme.WidgetPickerColors
/**
* An adapter that maps the resource tokens for widget picker dark colors to the
* [WidgetPickerColors] accepted by the widget picker module.
*/
@Composable
fun darkWidgetPickerColors() =
WidgetPickerColors(
// Bottom Sheet
sheetBackground = colorResource(R.color.widget_picker_primary_surface_color_dark),
dragHandle = colorResource(R.color.widget_picker_collapse_handle_color_dark),
sheetTitle = colorResource(R.color.widget_picker_title_color_dark),
sheetDescription = colorResource(R.color.widget_picker_description_color_dark),
// Expand collapse list
expandCollapseIndicatorIcon =
colorResource(R.color.widget_picker_expand_icon_button_color_dark),
expandCollapseIndicatorBackground =
colorResource(R.color.widget_picker_expand_icon_button_background_dark),
expandableListItemsBackground =
colorResource(R.color.widget_picker_expandable_list_items_background_dark),
expandableListHeaderTitle =
colorResource(R.color.widget_picker_header_app_title_color_dark),
expandableListHeaderSubTitle =
colorResource(R.color.widget_picker_header_app_subtitle_color_dark),
// List header
selectedListHeaderBackground =
colorResource(R.color.widget_picker_clickable_list_header_background_dark),
unselectedListHeaderBackground = Color.Transparent,
featuredHeaderLeadingIconBackground =
colorResource(R.color.widget_picker_featured_header_icon_background_dark),
featuredHeaderLeadingIcon =
colorResource(R.color.widget_picker_featured_header_icon_color_dark),
unSelectedListHeaderTitle =
colorResource(R.color.widget_picker_header_app_title_color_dark),
unSelectedListHeaderSubTitle =
colorResource(R.color.widget_picker_header_app_subtitle_color_dark),
selectedListHeaderTitle =
colorResource(R.color.widget_picker_selected_header_app_title_color_dark),
selectedListHeaderSubTitle =
colorResource(R.color.widget_picker_selected_header_app_subtitle_color_dark),
// Error message
noWidgetsErrorText = colorResource(R.color.widget_picker_no_widget_error_color_dark),
// Widgets container
widgetsContainerBackground =
colorResource(R.color.widget_picker_widgets_container_background_dark),
// App icon
placeholderAppIcon = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f),
// Widget details
widgetLabel = colorResource(R.color.widget_cell_title_color_dark),
widgetSpanText = colorResource(R.color.widget_cell_subtitle_color_dark),
widgetDescription = colorResource(R.color.widget_cell_subtitle_color_dark),
addButtonContent = colorResource(R.color.widget_picker_add_button_text_color_dark),
addButtonBackground = colorResource(R.color.widget_picker_add_button_background_color_dark),
// Widget preview
widgetPlaceholderBackground =
colorResource(R.color.widget_picker_preview_placeholder_background_dark),
widgetPlaceholderContent =
colorResource(R.color.widget_picker_preview_placeholder_content_dark),
// Floating Toolbar
toolbarBackground = colorResource(R.color.widget_picker_toolbar_background_dark),
toolbarTabSelectedBackground =
colorResource(R.color.widget_picker_toolbar_selected_tab_background_dark),
toolbarTabUnSelectedBackground =
colorResource(R.color.widget_picker_toolbar_unselected_tab_background_dark),
toolbarSelectedTabContent =
colorResource(R.color.widget_picker_toolbar_selected_tab_content_color_dark),
toolbarUnSelectedTabContent =
colorResource(R.color.widget_picker_toolbar_unselected_tab_content_color_dark),
// Search bar
searchBarBackground = colorResource(R.color.widget_picker_search_bar_background_color_dark),
searchBarPlaceholderText = colorResource(R.color.widget_picker_search_text_color_dark),
searchBarText = colorResource(R.color.widget_picker_search_text_color_dark),
searchBarSearchIcon = colorResource(R.color.widget_picker_search_text_color_dark),
searchBarClearButtonIcon = colorResource(R.color.widget_picker_search_text_color_dark),
searchBarBackButtonIcon = colorResource(R.color.widget_picker_search_text_color_dark),
searchBarCursor = colorResource(R.color.widget_picker_search_cursor_color_dark),
focusOutline = colorResource(R.color.widget_picker_focus_outline_color_dark),
)
/**
* An adapter that maps the resource tokens for widget picker light colors to the
* [WidgetPickerColors] accepted by the widget picker module
*/
@Composable
fun lightWidgetPickerColors() =
WidgetPickerColors(
// Bottom Sheet
sheetBackground = colorResource(R.color.widget_picker_primary_surface_color_light),
dragHandle = colorResource(R.color.widget_picker_collapse_handle_color_light),
sheetTitle = colorResource(R.color.widget_picker_title_color_light),
sheetDescription = colorResource(R.color.widget_picker_description_color_light),
// Expand collapse list
expandCollapseIndicatorIcon =
colorResource(R.color.widget_picker_expand_icon_button_color_light),
expandCollapseIndicatorBackground =
colorResource(R.color.widget_picker_expand_icon_button_background_light),
expandableListItemsBackground =
colorResource(R.color.widget_picker_expandable_list_items_background_light),
expandableListHeaderTitle =
colorResource(R.color.widget_picker_header_app_title_color_light),
expandableListHeaderSubTitle =
colorResource(R.color.widget_picker_header_app_subtitle_color_light),
// List header
selectedListHeaderBackground =
colorResource(R.color.widget_picker_clickable_list_header_background_light),
unselectedListHeaderBackground = Color.Transparent,
featuredHeaderLeadingIconBackground =
colorResource(R.color.widget_picker_featured_header_icon_background_light),
featuredHeaderLeadingIcon =
colorResource(R.color.widget_picker_featured_header_icon_color_light),
selectedListHeaderTitle =
colorResource(R.color.widget_picker_selected_header_app_title_color_light),
selectedListHeaderSubTitle =
colorResource(R.color.widget_picker_selected_header_app_subtitle_color_light),
unSelectedListHeaderTitle =
colorResource(R.color.widget_picker_header_app_title_color_light),
unSelectedListHeaderSubTitle =
colorResource(R.color.widget_picker_header_app_subtitle_color_light),
// Error message
noWidgetsErrorText = colorResource(R.color.widget_picker_no_widget_error_color_light),
// Widgets container
widgetsContainerBackground =
colorResource(R.color.widget_picker_widgets_container_background_light),
// App icon
placeholderAppIcon = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f),
// Widget details
widgetLabel = colorResource(R.color.widget_cell_title_color_light),
widgetSpanText = colorResource(R.color.widget_cell_subtitle_color_light),
widgetDescription = colorResource(R.color.widget_cell_subtitle_color_light),
addButtonContent = colorResource(R.color.widget_picker_add_button_text_color_light),
addButtonBackground =
colorResource(R.color.widget_picker_add_button_background_color_light),
// Widget preview
widgetPlaceholderBackground =
colorResource(R.color.widget_picker_preview_placeholder_background_light),
widgetPlaceholderContent =
colorResource(R.color.widget_picker_preview_placeholder_content_light),
// Floating Toolbar
toolbarBackground = colorResource(R.color.widget_picker_toolbar_background_light),
toolbarTabSelectedBackground =
colorResource(R.color.widget_picker_toolbar_selected_tab_background_light),
toolbarTabUnSelectedBackground =
colorResource(R.color.widget_picker_toolbar_unselected_tab_background_light),
toolbarSelectedTabContent =
colorResource(R.color.widget_picker_toolbar_selected_tab_content_color_light),
toolbarUnSelectedTabContent =
colorResource(R.color.widget_picker_toolbar_unselected_tab_content_color_light),
// Search bar
searchBarBackground =
colorResource(R.color.widget_picker_search_bar_background_color_light),
searchBarPlaceholderText = colorResource(R.color.widget_picker_search_text_color_light),
searchBarText = colorResource(R.color.widget_picker_search_text_color_light),
searchBarSearchIcon = colorResource(R.color.widget_picker_search_text_color_light),
searchBarClearButtonIcon = colorResource(R.color.widget_picker_search_text_color_light),
searchBarBackButtonIcon = colorResource(R.color.widget_picker_search_text_color_light),
searchBarCursor = colorResource(R.color.widget_picker_search_cursor_color_light),
focusOutline = colorResource(R.color.widget_picker_focus_outline_color_light),
)
@@ -0,0 +1,42 @@
/*
* 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.widgetpicker.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import com.android.launcher3.widgetpicker.ui.theme.WidgetPickerTheme
/** Contains theme that launcher applies to the widget picker. */
@Composable
fun LauncherWidgetPickerTheme(content: @Composable () -> Unit) {
val widgetPickerColors =
if (isSystemInDarkTheme()) {
darkWidgetPickerColors()
} else {
lightWidgetPickerColors()
}
MaterialTheme {
WidgetPickerTheme(
colors = widgetPickerColors,
textStyles = launcherWidgetPickerTextStyles()
) {
content()
}
}
}
@@ -0,0 +1,54 @@
/*
* 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.widgetpicker.theme
import androidx.compose.runtime.Composable
import com.android.launcher3.R
import com.android.launcher3.util.textStyleFromResource
import com.android.launcher3.widgetpicker.ui.theme.WidgetPickerTextStyles
/**
* Parses styles from resources (R.style) to prepare [WidgetPickerTextStyles] for use in compose
* theme for widget picker.
*/
@Composable
fun launcherWidgetPickerTextStyles(): WidgetPickerTextStyles {
val searchBarTextStyle = textStyleFromResource(R.style.WidgetSearchBarText)
val widgetHeaderTitleStyle = textStyleFromResource(R.style.WidgetListHeader_Title)
val widgetHeaderSubTitleStyle = textStyleFromResource(R.style.WidgetListHeader_SubTitle)
return WidgetPickerTextStyles(
sheetTitle = textStyleFromResource(R.style.WidgetsTitle),
sheetDescription = textStyleFromResource(R.style.WidgetPickerDescription),
expandableListHeaderTitle = widgetHeaderTitleStyle,
expandableListHeaderSubTitle = widgetHeaderSubTitleStyle,
unSelectedListHeaderTitle = widgetHeaderTitleStyle,
selectedListHeaderTitle = textStyleFromResource(R.style.WidgetListHeader_Title_Selected),
unSelectedListHeaderSubTitle = widgetHeaderSubTitleStyle,
selectedListHeaderSubTitle =
textStyleFromResource(R.style.WidgetListHeader_SubTitle_Selected),
widgetLabel = textStyleFromResource(R.style.WidgetLabel),
widgetDescription = textStyleFromResource(R.style.WidgetDescription),
widgetSpanText = textStyleFromResource(R.style.WidgetDims),
addWidgetButtonLabel = textStyleFromResource(R.style.WidgetAddButton),
toolbarUnSelectedTabLabel = textStyleFromResource(R.style.WidgetToolbarTabLabel),
toolbarSelectedTabLabel = textStyleFromResource(R.style.WidgetToolbarTabLabel_Selected),
searchBarText = searchBarTextStyle,
searchBarPlaceholderText = searchBarTextStyle,
noWidgetsErrorText = textStyleFromResource(R.style.WidgetErrorText),
)
}
@@ -0,0 +1,40 @@
/*
* 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.helper
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.NativeKeyEvent
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
val NATIVE_DPAD_DOWN = NativeKeyEvent(NativeKeyEvent.ACTION_DOWN, NativeKeyEvent.KEYCODE_DPAD_DOWN)
val NATIVE_TAB = NativeKeyEvent(NativeKeyEvent.ACTION_DOWN, NativeKeyEvent.KEYCODE_TAB)
val NavigateDownKeyEvent = KeyEvent(NATIVE_DPAD_DOWN)
val NavigateDownTabEvent = KeyEvent(NATIVE_TAB)
fun hasMinTouchArea(minWidth: Dp = 48.dp, minHeight: Dp = 48.dp): SemanticsMatcher =
SemanticsMatcher("touch area is smaller than $minWidth x $minHeight") { node ->
val density: Density = node.root?.density ?: error("root node not available!")
with(density) {
val rect = node.touchBoundsInRoot
val size = node.size
(rect.width.toDp() >= minWidth && rect.height.toDp() >= minHeight) ||
(size.width.toDp() >= minWidth && size.height.toDp() >= minHeight)
}
}
@@ -0,0 +1,160 @@
/*
* 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.widget
import android.appwidget.AppWidgetManager
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractFloatingView
import com.android.launcher3.Launcher
import com.android.launcher3.LauncherSettings.Favorites
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.model.data.LauncherAppWidgetInfo
import com.android.launcher3.testcomponent.WidgetConfigActivity
import com.android.launcher3.util.BaseLauncherActivityTest
import com.android.launcher3.util.BlockingBroadcastReceiver
import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator
import com.android.launcher3.util.Wait
import com.android.launcher3.util.rule.ShellCommandRule
import com.android.launcher3.util.ui.PortraitLandscapeRunner.PortraitLandscape
import com.android.launcher3.util.ui.TestViewHelpers
import com.android.launcher3.util.workspace.FavoriteItemsTransaction
import com.android.launcher3.widgetpicker.listeners.WidgetPickerAddItemListener
import com.android.launcher3.widgetpicker.shared.model.WidgetInfo
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Test verifies that when adding a widget to homescreen using [WidgetPickerAddItemListener], if the
* widget has a configuration activity, it is shown correctly.
*/
@LargeTest
@RunWith(AndroidJUnit4::class)
class AddWidgetConfigTest : BaseLauncherActivityTest<Launcher>() {
@get:Rule val grantWidgetRule: ShellCommandRule = ShellCommandRule.grantWidgetBind()
private lateinit var widgetInfo: LauncherAppWidgetProviderInfo
private lateinit var appWidgetManager: AppWidgetManager
private var widgetId = 0
@Before
@Throws(Exception::class)
fun setUp() {
widgetInfo = TestViewHelpers.findWidgetProvider(/* hasConfigureScreen= */ true)
appWidgetManager = AppWidgetManager.getInstance(targetContext())
}
@Test
@PortraitLandscape
@Throws(Throwable::class)
fun testWidgetConfig() {
runTest(acceptConfig = true)
}
@Test
@PortraitLandscape
@Throws(Throwable::class)
fun testConfigCancelled() {
runTest(acceptConfig = false)
}
/** @param acceptConfig accept the config activity */
@Throws(Throwable::class)
private fun runTest(acceptConfig: Boolean) {
FavoriteItemsTransaction(targetContext()).commit()
loadLauncherSync()
// Add widget to home screen
val monitor = WidgetConfigStartupMonitor()
launcherActivity.executeOnLauncher { l: Launcher ->
val addItemListener =
WidgetPickerAddItemListener(
container = Favorites.CONTAINER_WIDGETS_TRAY,
widgetInfo = WidgetInfo.AppWidgetInfo(widgetInfo),
)
addItemListener.init(l, /* isHomeStarted= */ true)
}
uiDevice.waitForIdle()
// Widget id for which the config activity was opened
widgetId = monitor.widgetId
// Verify that the widget id is valid and bound
assertThat(appWidgetManager.getAppWidgetInfo(widgetId)).isNotNull()
setResult(acceptConfig)
if (acceptConfig) {
launcherActivity.getOnceNotNull("Widget was not added") { l: Launcher ->
// Close the resize frame before searching for widget
AbstractFloatingView.closeAllOpenViews(l)
l.workspace.mapOverItems(WidgetSearchCondition())
}
assertThat(appWidgetManager.getAppWidgetInfo(widgetId)).isNotNull()
} else {
// Verify that the widget id is deleted.
Wait.atMost(
"no widget with id",
{ appWidgetManager.getAppWidgetInfo(widgetId) == null },
)
}
}
private fun setResult(success: Boolean) {
InstrumentationRegistry.getInstrumentation()
.targetContext
.sendBroadcast(
WidgetConfigActivity.getCommandIntent(
WidgetConfigActivity::class.java,
if (success) "clickOK" else "clickCancel",
)
)
uiDevice.waitForIdle()
}
/** Condition for searching widget id */
private inner class WidgetSearchCondition : ItemOperator {
override fun evaluate(info: ItemInfo?, view: View): Boolean {
return info is LauncherAppWidgetInfo &&
info.providerName == widgetInfo.provider &&
info.appWidgetId == widgetId
}
}
/** Broadcast receiver for receiving widget config activity status. */
private class WidgetConfigStartupMonitor :
BlockingBroadcastReceiver(WidgetConfigActivity::class.java.name) {
@get:Throws(InterruptedException::class)
val widgetId: Int
get() {
val intent = checkNotNull(blockingGetExtraIntent())
assertThat(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE).isEqualTo(intent.action)
val widgetId =
intent.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
LauncherAppWidgetInfo.NO_ID,
)
assertThat(widgetId).isNotEqualTo(LauncherAppWidgetInfo.NO_ID)
return widgetId
}
}
}
@@ -0,0 +1,485 @@
/*
* 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.widgetpicker.repository
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
import android.content.ComponentName
import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.os.UserHandle
import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn
import com.android.launcher3.AppFilter
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherAppState
import com.android.launcher3.R
import com.android.launcher3.icons.IconCache
import com.android.launcher3.icons.cache.CachedObject
import com.android.launcher3.model.WidgetsModel
import com.android.launcher3.model.data.PackageItemInfo
import com.android.launcher3.pm.ShortcutConfigActivityInfo
import com.android.launcher3.util.Executors
import com.android.launcher3.util.SandboxApplication
import com.android.launcher3.util.TestActivityContext
import com.android.launcher3.util.TestUtil
import com.android.launcher3.util.UserIconInfo
import com.android.launcher3.util.WidgetUtils
import com.android.launcher3.util.rule.MockUsersRule
import com.android.launcher3.util.rule.MockUsersRule.MockUser
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
import com.android.launcher3.widgetpicker.data.repository.WidgetsRepository
import com.android.launcher3.widgetpicker.datasource.FeaturedWidgetsDataSource
import com.android.launcher3.widgetpicker.datasource.InMemoryWidgetSearchAlgorithm
import com.android.launcher3.widgetpicker.shared.model.PickableWidget
import com.android.launcher3.widgetpicker.shared.model.WidgetApp
import com.android.launcher3.widgetpicker.shared.model.WidgetAppId
import com.android.launcher3.widgetpicker.shared.model.WidgetId
import com.android.launcher3.widgetpicker.shared.model.WidgetPreview
import com.android.launcher3.widgetpicker.shared.model.isAppWidget
import com.android.launcher3.widgetpicker.shared.model.isShortcut
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertNotNull
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.After
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.junit.MockitoRule
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
@MockUser(userType = UserIconInfo.TYPE_MAIN)
@OptIn(ExperimentalCoroutinesApi::class)
class WidgetsRepositoryImplTest {
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
@get:Rule val context = SandboxApplication()
@get:Rule val uiContext = TestActivityContext(context, R.style.WidgetContainerTheme)
@get:Rule var mMockUsersRule: MockUsersRule = MockUsersRule(context)
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
@Mock private lateinit var iconCacheMock: IconCache
@Mock private lateinit var appFilterMock: AppFilter
@Mock lateinit var mockShortcutLauncherActivityInfo: LauncherActivityInfo
@Mock lateinit var mockActivityInfo: ActivityInfo
@Mock lateinit var mockApplicationInfo: ApplicationInfo
private lateinit var user: UserHandle
private lateinit var idp: InvariantDeviceProfile
private lateinit var appWidgetManager: AppWidgetManager
private lateinit var launcherApps: LauncherApps
private lateinit var generatedPreview: RemoteViews
private lateinit var underTest: WidgetsRepository
@Before
fun setup() {
idp = InvariantDeviceProfile.INSTANCE[uiContext]
underTest =
WidgetsRepositoryImpl(
appContext = context,
idp = idp,
widgetsModel = WidgetsModel(context, idp, iconCacheMock, appFilterMock),
featuredWidgetsDataSource = FakeFeaturedDataSource,
searchAlgorithm = InMemoryWidgetSearchAlgorithm(testDispatcher),
backgroundContext = testDispatcher,
)
generatedPreview = RemoteViews(context.packageName, GENERATED_PREVIEW_LAYOUT_ID)
appWidgetManager = context.spyService(AppWidgetManager::class.java)
launcherApps = context.spyService(LauncherApps::class.java)
whenever(appFilterMock.shouldShowApp(any())).thenReturn(true)
seedAppTitles()
seedWidgetAndShortcutTitles()
user = mMockUsersRule.findUser { it.isMain }
seedShortcutInLauncherApps(user)
seedWidgetsInAppWidgetManager(user)
}
@After
fun tearDown() {
underTest.cleanUp()
}
private fun seedAppTitles() {
whenever(iconCacheMock.getTitleAndIconForApp(any(), any())).thenAnswer {
(it.arguments[0] as PackageItemInfo).apply { title = packageName.simpleName() }
}
}
private fun seedWidgetAndShortcutTitles() {
whenever(iconCacheMock.getTitleNoCache(any())).thenAnswer {
(it.arguments[0] as CachedObject).let { info ->
when (info) {
is LauncherAppWidgetProviderInfo -> {
info.component.className
}
is ShortcutConfigActivityInfo -> {
info.component.className
}
else -> "title"
}
}
}
}
private fun seedWidgetsInAppWidgetManager(user: UserHandle) {
whenever(appWidgetManager.getInstalledProvidersForProfile(user))
.thenReturn(listOf(App1Widget1ProviderInfo, App2Widget1ProviderInfo))
whenever(appWidgetManager.getInstalledProvidersForPackage(APP_1_PACKAGE_NAME, user))
.thenReturn(listOf(App1Widget1ProviderInfo))
whenever(appWidgetManager.getInstalledProvidersForPackage(APP_2_PACKAGE_NAME, user))
.thenReturn(listOf(App2Widget1ProviderInfo))
doAnswer { i ->
generatedPreview.takeIf {
i.arguments[0] == App2Widget1ProviderName &&
i.arguments[1] == user &&
i.arguments[2] == WIDGET_CATEGORY_HOME_SCREEN
}
}
.whenever(appWidgetManager)
.getWidgetPreview(any(), any(), any())
}
private fun seedShortcutInLauncherApps(user: UserHandle) {
mockApplicationInfo.targetSdkVersion = Build.VERSION_CODES.BAKLAVA
mockActivityInfo.apply { whenever(componentName).thenReturn(App1Shortcut1Name) }
mockShortcutLauncherActivityInfo.apply {
whenever(componentName).thenReturn(App1Shortcut1Name)
whenever(applicationInfo).thenReturn(mockApplicationInfo)
whenever(activityInfo).thenReturn(mockActivityInfo)
whenever(getUser()).thenReturn(user)
}
whenever(launcherApps.getShortcutConfigActivityList(eq(null), eq(user)))
.thenReturn(listOf(mockShortcutLauncherActivityInfo))
whenever(launcherApps.getShortcutConfigActivityList(eq(APP_1_PACKAGE_NAME), eq(user)))
.thenReturn(listOf(mockShortcutLauncherActivityInfo))
whenever(launcherApps.getShortcutConfigActivityList(eq(APP_2_PACKAGE_NAME), eq(user)))
.thenReturn(listOf())
val iconCache = LauncherAppState.INSTANCE[context].iconCache
spyOn(iconCache)
doReturn(ShortcutPreviewDrawable).whenever(iconCache).getFullResIcon(any())
}
@Test
@MockUser(userType = UserIconInfo.TYPE_MAIN)
fun initializeAndObserveWidgets_widgetsAndShortcutsMappedCorrectly() =
testScope.runTest {
underTest.initialize()
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {}
val widgetApps = underTest.observeWidgets().first()
// 2 apps; first with a widget an shortcut and second with just a widget
assertThat(widgetApps).hasSize(2)
val expectedWidgetApp1Id =
WidgetAppId(packageName = APP_1_PACKAGE_NAME, userHandle = user, category = -1)
val expectedWidgetApp2Id =
WidgetAppId(packageName = APP_2_PACKAGE_NAME, userHandle = user, category = -1)
val expectedWidget1AId = WidgetId(App1Widget1ProviderName, user)
val expectedShortcut1BId = WidgetId(App1Shortcut1Name, user)
val expectedWidget2AId = WidgetId(App2Widget1ProviderName, user)
val app1 = assertNotNull(widgetApps.find { it.id == expectedWidgetApp1Id })
app1.apply {
assertThat(title).isEqualTo(APP_1)
assertThat(widgets).hasSize(2)
val widget1A = assertNotNull(app1.widgets.find { it.id == expectedWidget1AId })
assertThat(widget1A.widgetInfo.isAppWidget()).isTrue()
assertThat(widget1A.label).isEqualTo(WIDGET_1A_NAME)
val shortcut1B = assertNotNull(app1.widgets.find { it.id == expectedShortcut1BId })
assertThat(shortcut1B.widgetInfo.isShortcut()).isTrue()
assertThat(shortcut1B.label).isEqualTo(SHORTCUT_1B_NAME)
}
val app2 = assertNotNull(widgetApps.find { it.id == expectedWidgetApp2Id })
app2.apply {
assertThat(app2.title).isEqualTo(APP_2)
assertThat(app2.widgets).hasSize(1)
val widget2A = assertNotNull(app2.widgets.find { it.id == expectedWidget2AId })
assertThat(widget2A.widgetInfo.isAppWidget()).isTrue()
assertThat(widget2A.label).isEqualTo(WIDGET_2A_NAME)
}
}
@Test
fun initializeWidgetAppAndObserve_appOne_hasAWidgetAndAShortcut() =
testScope.runTest {
val widgetAppId =
WidgetAppId(packageName = APP_1_PACKAGE_NAME, userHandle = user, category = -1)
underTest.initialize(
WidgetsRepository.InitializationOptions(
widgetAppId = widgetAppId,
loadFeaturedWidgets = false,
)
)
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {}
val widgetApp = underTest.observeWidgetApp(widgetAppId = widgetAppId).first()
assertThat(widgetApp).isNotNull()
widgetApp?.apply {
assertThat(title).isEqualTo(APP_1)
assertThat(widgets).hasSize(2)
val expectedWidget1AId = WidgetId(App1Widget1ProviderName, user)
val widget1A = assertNotNull(widgets.find { it.id == expectedWidget1AId })
assertThat(widget1A.widgetInfo.isAppWidget()).isTrue()
assertThat(widget1A.label).isEqualTo(WIDGET_1A_NAME)
val expectedShortcut1BId = WidgetId(App1Shortcut1Name, user)
val shortcut1B = assertNotNull(widgets.find { it.id == expectedShortcut1BId })
assertThat(shortcut1B.widgetInfo.isShortcut()).isTrue()
assertThat(shortcut1B.label).isEqualTo(SHORTCUT_1B_NAME)
}
}
@Test
fun initializeWidgetAppAndObserve_appTwo_hasOnlyAWidget() =
testScope.runTest {
val widgetAppId =
WidgetAppId(packageName = APP_2_PACKAGE_NAME, userHandle = user, category = -1)
underTest.initialize(
WidgetsRepository.InitializationOptions(
widgetAppId = widgetAppId,
loadFeaturedWidgets = false,
)
)
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {}
val widgetApp = underTest.observeWidgetApp(widgetAppId = widgetAppId).first()
assertThat(widgetApp).isNotNull()
widgetApp?.apply {
assertThat(title).isEqualTo(APP_2)
assertThat(widgets).hasSize(1)
val expectedWidget2AId = WidgetId(App2Widget1ProviderName, user)
val widget2A = assertNotNull(widgets.find { it.id == expectedWidget2AId })
assertThat(widget2A.widgetInfo.isAppWidget()).isTrue()
assertThat(widget2A.label).isEqualTo(WIDGET_2A_NAME)
}
}
@Test
fun searchWidgets_returnsMatchedWidgetsAndShortcuts() =
testScope.runTest {
val query = APP_1
underTest.initialize()
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {}
val result = underTest.searchWidgets(query)
assertThat(result).hasSize(1)
assertThat(result[0].title).isEqualTo(APP_1)
// the widget and the shortcut from app1
assertThat(result[0].widgets).hasSize(2)
}
@Test
fun getWidgetPreview_widgetInfoNotAvailable_returnsPlaceholder() =
testScope.runTest {
underTest.initialize()
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {}
// If for some reason, widget's info wasn't available, maybe package updated, a
// placeholder
// will show up
val preview =
underTest.getWidgetPreview(
WidgetId(
componentName = ComponentName("unknownPackage", "unknownWidget"),
userHandle = user,
)
)
assertThat(preview).isInstanceOf(WidgetPreview.PlaceholderWidgetPreview::class.java)
}
@Test
fun getWidgetPreview_layoutXml_returnsProviderInfoPreview() =
testScope.runTest {
underTest.initialize()
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {}
// in the seeded data, widget 1 from app1 uses layout xml
val preview = underTest.getWidgetPreview(WidgetId(App1Widget1ProviderName, user))
assertThat(preview).isInstanceOf(WidgetPreview.ProviderInfoWidgetPreview::class.java)
// Initial layout set as the preview layout.
assertThat(
(preview as WidgetPreview.ProviderInfoWidgetPreview).providerInfo.initialLayout
)
.isEqualTo(PREVIEW_LAYOUT_ID)
}
@Test
fun getWidgetPreview_generatedPreview_returnsRemoteViewsPreview() =
testScope.runTest {
underTest.initialize()
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {}
// in the seeded data, widget 1 from app2 uses generated preview
val preview = underTest.getWidgetPreview(WidgetId(App2Widget1ProviderName, user))
assertThat(preview).isInstanceOf(WidgetPreview.RemoteViewsWidgetPreview::class.java)
assertThat((preview as WidgetPreview.RemoteViewsWidgetPreview).remoteViews.layoutId)
.isEqualTo(GENERATED_PREVIEW_LAYOUT_ID)
}
@Test
fun getWidgetPreview_shortcut_returnsBitmapPreview() =
testScope.runTest {
underTest.initialize()
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {}
val preview = underTest.getWidgetPreview(WidgetId(App1Shortcut1Name, user))
assertThat(preview).isInstanceOf(WidgetPreview.BitmapWidgetPreview::class.java)
}
@Test
fun cleanUp_clearsState() =
testScope.runTest {
underTest.initialize()
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {}
assertThat(underTest.observeWidgets().first()).isNotEmpty()
assertThat(underTest.getFeaturedWidgets().first()).isNotEmpty()
underTest.cleanUp()
assertThat(underTest.observeWidgets().first()).isEmpty()
assertThat(underTest.getFeaturedWidgets().first()).isEmpty()
}
@Test
fun getFeaturedWidgets_returnsFromDataSource() =
testScope.runTest {
underTest.initialize()
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {}
val result = underTest.getFeaturedWidgets().first()
assertThat(result).hasSize(1)
}
companion object {
const val APP_1 = "app1"
const val APP_1_PACKAGE_NAME = "com.test.app1"
const val WIDGET_1A_NAME = "TestWidgetProvider1A"
const val SHORTCUT_1B_NAME = "TestShortcut1B"
const val APP_2 = "app2"
const val APP_2_PACKAGE_NAME = "com.test.app2"
const val WIDGET_2A_NAME = "TestWidgetProvider2A"
private val PREVIEW_LAYOUT_ID =
getInstrumentation().context.run {
resources.getIdentifier("test_layout_appwidget_view", "layout", packageName)
}
private val GENERATED_PREVIEW_LAYOUT_ID =
getInstrumentation().context.run {
resources.getIdentifier("test_layout_appwidget_blue", "layout", packageName)
}
private val ShortcutPreviewDrawable =
BitmapDrawable(
getInstrumentation().context.resources,
Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888),
)
val App1Widget1ProviderName = ComponentName(APP_1_PACKAGE_NAME, WIDGET_1A_NAME)
val App1Widget1ProviderInfo: AppWidgetProviderInfo =
WidgetUtils.createAppWidgetProviderInfo(App1Widget1ProviderName).apply {
previewLayout = PREVIEW_LAYOUT_ID
}
val App1Shortcut1Name = ComponentName(APP_1_PACKAGE_NAME, SHORTCUT_1B_NAME)
val App2Widget1ProviderName = ComponentName(APP_2_PACKAGE_NAME, WIDGET_2A_NAME)
val App2Widget1ProviderInfo: AppWidgetProviderInfo =
WidgetUtils.createAppWidgetProviderInfo(App2Widget1ProviderName).apply {
generatedPreviewCategories = WIDGET_CATEGORY_HOME_SCREEN
}
private fun String.simpleName() = split(".").last()
val FakeFeaturedDataSource =
object : FeaturedWidgetsDataSource {
var eligibleApps = emptyList<String>()
override suspend fun initialize() {
eligibleApps = listOf(APP_2_PACKAGE_NAME)
}
override suspend fun getFeaturedWidgets(
widgetApps: List<WidgetApp>
): List<PickableWidget> =
widgetApps
.filter { eligibleApps.contains(it.id.packageName) }
.map { it.widgets[0] }
override fun cleanup() {
eligibleApps = emptyList()
}
}
}
}
+31
View File
@@ -0,0 +1,31 @@
// 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 {
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
java_library {
name: "launcher-dagger-qualifiers",
srcs: [
"src/com/android/launcher3/dagger/ActivityContextSingleton.java",
"src/com/android/launcher3/dagger/ApplicationContext.java",
"src/com/android/launcher3/dagger/LauncherAppSingleton.java",
],
static_libs: [
"androidx.annotation_annotation",
"jsr330",
],
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.dagger;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.inject.Scope;
/**
* Scope annotation for singletons associated with Launcher activity context.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Scope
public @interface ActivityContextSingleton {
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.dagger;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.inject.Qualifier;
/**
* Qualifier for Launcher application context.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface ApplicationContext {
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.dagger;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.inject.Scope;
/**
* Scope annotation for singleton items within the LauncherAppComponent.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Scope
public @interface LauncherAppSingleton {
}
+536
View File
@@ -0,0 +1,536 @@
# DepthController 深度控制流程分析报告
## 1. 概述
DepthController 是 Android Launcher3 中的一个关键组件,负责控制 Launcher 表面的模糊效果和壁纸缩放。通过分析 `DepthController.java` 和 `BaseDepthController.java` 的源码,本文档详细阐述了 DEPTH、stateDepth 和 widgetDepth 的定义、调用流程、上游关系以及技术实现。
## 2. 核心组件定义与含义
### 2.1 DEPTH
- **定义**:`FloatProperty<BaseDepthController>` 类型的静态属性
- **路径**:`BaseDepthController.java:59-70`
- **含义**:核心深度控制属性,范围 0-1
- 0:完全缩小(壁纸完全缩放,最大模糊效果)
- 1:完全放大(壁纸原始大小,最小模糊效果)
- **作用**:直接控制壁纸缩放级别和模糊效果强度
### 2.2 stateDepth
- **定义**:`MultiProperty` 类型的实例属性
- **路径**:`BaseDepthController.java:81`
- **含义**:用于状态转换时的深度控制
- **作用**:在 Launcher 状态变化时(如从主屏幕到概览模式),控制深度变化的动画效果
### 2.3 widgetDepth
- **定义**:`MultiProperty` 类型的实例属性
- **路径**:`BaseDepthController.java:83`
- **含义**:用于小部件选择器的深度控制
- **作用**:在打开小部件选择器时,控制深度变化效果
## 3. 调用流程分析
### 3.1 初始化流程
1. **BaseDepthController 构造函数**
- 创建 `MultiPropertyFactory`,关联 DEPTH 属性
- 初始化 stateDepth 和 widgetDepth:
```java
MultiPropertyFactory<BaseDepthController> depthProperty =
new MultiPropertyFactory<>(this, DEPTH, DEPTH_INDEX_COUNT, Float::max);
stateDepth = depthProperty.get(DEPTH_INDEX_STATE_TRANSITION);
widgetDepth = depthProperty.get(DEPTH_INDEX_WIDGET);
```
- 路径:`BaseDepthController.java:142-146`
### 3.2 顶层调用流程
#### 3.2.1 用户交互触发点
1. **OptionsPopupView.enterAllApps**
- **触发方式**:用户点击打开所有应用按钮
- **调用路径**:`OptionsPopupView.java:595`
- **操作**:调用 `StateManager.goToState` 进入 ALL_APPS 状态
2. **OptionsPopupView.enterHomeGardening**
- **触发方式**:用户点击进入主屏幕编辑模式
- **调用路径**:`OptionsPopupView.java:601`
- **操作**:调用 `StateManager.goToState` 进入 HOME_GARDENING 状态
3. **AccessibilityActionsView**
- **触发方式**:辅助功能操作
- **调用路径**:`AccessibilityActionsView.java:501`
- **操作**:调用 `StateManager.goToState` 进入 ALL_APPS 状态
4. **KeyboardShortcutsDelegate**
- **触发方式**:键盘快捷键操作
- **调用路径**:`KeyboardShortcutsDelegate.java:131`
- **操作**:调用 `StateManager.goToState` 进入 ALL_APPS 状态
5. **BaseWidgetSheet**
- **触发方式**:完成小部件添加
- **调用路径**:`BaseWidgetSheet.java:455`
- **操作**:调用 `StateManager.goToState` 返回 NORMAL 状态
6. **FlingAnimation**
- **触发方式**:用户拖拽操作
- **调用路径**:`FlingAnimation.java:293`
- **操作**:调用 `StateManager.goToState` 进入目标状态
7. **WorkspaceTouchListener**
- **触发方式**:触摸操作返回主屏幕
- **调用路径**:`WorkspaceTouchListener.java:111`
- **操作**:调用 `StateManager.goToState` 返回 NORMAL 状态
#### 3.2.2 StateManager 调用流程
1. **StateManager.goToState**
- **调用方式**:根据是否需要动画,调用不同方法
- **接口**:`StateManager.goToState(STATE_TYPE, boolean, Runnable)`
- **参数**:
- 目标状态(如 NORMAL、ALL_APPS、OVERVIEW 等)
- 是否带动画
- 完成回调(可选)
- **路径**:`StateManager.java:423-456`
2. **无动画状态切换**
- **调用路径**:`StateManager.java:288-295`
- **操作**:
- 取消所有原子动画
- 调用 `onStateTransitionStart`
- **遍历所有 StateHandlers**,调用 `setState` 方法
- 调用 `onStateTransitionEnd`
3. **带动画状态切换**
- **调用路径**:`StateManager.java:390-397`
- **操作**:
- 创建 `PendingAnimation` 对象
- **遍历所有 StateHandlers**,调用 `setStateWithAnimation` 方法
- 添加动画监听器
- 启动动画
#### 3.2.3 DepthController 注册流程
1. **QuickstepLauncher.collectStateHandlers**
- **调用方式**:在 Launcher 初始化时被 StateManager 调用
- **接口**:`collectStateHandlers(List<StateHandler<LauncherState>>)`
- **操作**:
- 调用父类 `collectStateHandlers`
- 添加 `DepthController` 到 StateHandlers 列表:
```java
@Override
public void collectStateHandlers(List<StateHandler<LauncherState>> out) {
super.collectStateHandlers(out);
out.add(getDepthController());
}
```
- **路径**:`QuickstepLauncher.java:1325-1329`
### 3.3 stateDepth 调用流程
#### 3.3.1 直接设置方式
1. **DepthController.setState(LauncherState)**:
- **调用方式**:直接调用 `setValue` 方法
- **接口**:`MultiProperty.setValue(float)`
- **参数**:`toState.getDepth(mLauncher)` - 目标状态的深度值
- **路径**:`DepthController.java:147`
- **代码**:
```java
stateDepth.setValue(toState.getDepth(mLauncher));
```
2. **LauncherBackAnimationController**:
- **调用方式**:直接调用 `setValue` 方法
- **接口**:`MultiProperty.setValue(float)`
- **参数**:`LauncherState.BACKGROUND_APP.getDepth(mLauncher)` - 后台应用状态的深度值
- **路径**:`LauncherBackAnimationController.java:352-353`
- **代码**:
```java
mLauncher.getDepthController().stateDepth.setValue(
LauncherState.BACKGROUND_APP.getDepth(mLauncher));
```
#### 3.3.2 动画设置方式
1. **DepthController.setStateWithAnimation()**:
- **调用方式**:通过 `PendingAnimation.setFloat` 设置动画
- **接口**:`PendingAnimation.setFloat(Object, String, float, Interpolator)`
- **参数**:
- `stateDepth` - 目标属性
- `MULTI_PROPERTY_VALUE` - 属性名称
- `toDepth` - 目标深度值
- `config.getInterpolator(ANIM_DEPTH, LINEAR)` - 动画插值器
- **路径**:`DepthController.java:161-162`
- **代码**:
```java
animation.setFloat(stateDepth, MULTI_PROPERTY_VALUE, toDepth,
config.getInterpolator(ANIM_DEPTH, LINEAR));
```
2. **RecentsView**:
- **调用方式**:使用 `ObjectAnimator.ofFloat` 创建动画
- **接口**:`ObjectAnimator.ofFloat(Object, String, float...)`
- **参数**:
- `depthController.stateDepth` - 目标属性
- `MULTI_PROPERTY_VALUE` - 属性名称
- `targetDepth` - 目标深度值(根据任务视图类型计算)
- **路径**:`RecentsView.java:5862-5863`
- **代码**:
```java
anim.play(ObjectAnimator.ofFloat(depthController.stateDepth, MULTI_PROPERTY_VALUE,
targetDepth));
```
3. **LauncherActivityInterface**:
- **调用方式**:通过 `PendingAnimation.addFloat` 添加动画
- **接口**:`PendingAnimation.addFloat(Object, Property, float, float, Interpolator)`
- **参数**:
- `getDepthController().stateDepth` - 目标属性
- `new LauncherAnimUtils.ClampedProperty<>(MULTI_PROPERTY_VALUE, fromDepthRatio, toDepthRatio)` - clamped属性
- `fromDepthRatio` - 起始深度值(后台应用状态)
- `toDepthRatio` - 结束深度值(概览状态)
- `LINEAR` - 动画插值器
- **路径**:`LauncherActivityInterface.java:123-126`
- **代码**:
```java
pa.addFloat(getDepthController().stateDepth,
new LauncherAnimUtils.ClampedProperty<>(
MULTI_PROPERTY_VALUE, fromDepthRatio, toDepthRatio),
fromDepthRatio, toDepthRatio, LINEAR);
```
4. **TaskViewUtils**:
- **调用方式**:通过 `PendingAnimation.setFloat` 设置动画
- **接口**:`PendingAnimation.setFloat(Object, String, float, Interpolator)`
- **参数**:
- `depthController.stateDepth` - 目标属性
- `MULTI_PROPERTY_VALUE` - 属性名称
- `BACKGROUND_APP.getDepth(container)` - 目标深度值(后台应用状态)
- `TOUCH_RESPONSE` - 动画插值器
- **路径**:`TaskViewUtils.java:472-474`
- **代码**:
```java
out.setFloat(depthController.stateDepth, MULTI_PROPERTY_VALUE,
BACKGROUND_APP.getDepth(container),
TOUCH_RESPONSE);
```
5. **QuickstepTransitionManager**:
- **调用方式**:使用 `ObjectAnimator.ofFloat` 创建动画
- **接口**:`ObjectAnimator.ofFloat(Object, String, float...)`
- **参数**:
- `mLauncher.getDepthController().stateDepth` - 目标属性
- `MULTI_PROPERTY_VALUE` - 属性名称
- `BACKGROUND_APP.getDepth(mLauncher)` - 目标深度值(后台应用状态)
- **路径**:`QuickstepTransitionManager.java:1244-1246`
- **代码**:
```java
ObjectAnimator backgroundRadiusAnim = ObjectAnimator.ofFloat(
mLauncher.getDepthController().stateDepth, MULTI_PROPERTY_VALUE,
BACKGROUND_APP.getDepth(mLauncher))
```
#### 3.2.3 上游调用方
- **StateManager**:状态管理系统在状态变化时调用 `DepthController.setState()` 和 `setStateWithAnimation()`
- **RecentsView**:在最近任务视图切换时创建深度动画
- **LauncherBackAnimationController**:在返回动画开始时直接设置深度
- **LauncherActivityInterface**:在活动转换时添加深度动画
- **TaskViewUtils**:在任务视图操作时设置深度动画
- **QuickstepTransitionManager**:在快速步骤过渡时创建深度动画
### 3.4 widgetDepth 调用流程
#### 3.4.1 小部件选择器调用
1. **QuickstepLauncher.onWidgetsTransition(float)**:
- **调用方式**:直接调用 `setValue` 方法
- **接口**:`MultiProperty.setValue(float)`
- **参数**:
- `Utilities.mapToRange(progress, 0f, 1f, 0f, getDeviceProfile().getBottomSheetProfile().getBottomSheetDepth(), EMPHASIZED)`
- 计算逻辑:将小部件选择器的进度值映射到对应的深度值范围
- **路径**:`QuickstepLauncher.java:1050-1052`
- **代码**:
```java
getDepthController().widgetDepth.setValue(Utilities.mapToRange(
progress, 0f, 1f, 0f,
getDeviceProfile().getBottomSheetProfile().getBottomSheetDepth(), EMPHASIZED));
```
#### 3.3.2 调用时机
- **触发条件**:当小部件选择器打开或关闭时
- **调用逻辑**:根据小部件选择器的显示进度(0到1),动态计算并设置对应的深度值
- **特点**:使用 `Utilities.mapToRange` 方法实现平滑的深度过渡效果
### 3.4 核心处理流程
1. **MultiProperty 机制**:
- 当 stateDepth 或 widgetDepth 的值变化时,MultiPropertyFactory 会根据 `Float::max` 策略计算最终深度值
- 调用 DEPTH.set() 方法,传入计算后的深度值
2. **深度值处理**:
- DEPTH.set() 调用 `BaseDepthController.setDepth(float)`:
```java
private void setDepth(float depth) {
depth = Utilities.boundToRange(depth, 0, 1);
// 深度值处理逻辑...
if (Float.compare(mDepth, depthF) == 0) {
return;
}
mDepth = depthF;
applyDepthAndBlur();
}
```
- 路径:`BaseDepthController.java:334-353`
3. **深度与模糊效果应用**:
- `applyDepthAndBlur()` 方法应用深度和模糊效果:
- 设置壁纸缩放:`mWallpaperManager.setWallpaperZoomOut(windowToken, depth)`
- 计算并应用模糊半径:基于深度值计算模糊强度
- 处理跨窗口模糊:根据 `mCrossWindowBlursEnabled` 状态
- 路径:`BaseDepthController.java:190-270`
## 4. 上游调用关系
### 4.1 状态管理系统
- **调用方**:`StateManager`
- **调用时机**:当 Launcher 状态变化时(如主屏幕 ↔ 概览模式)
- **调用方式**:
- 直接状态切换:调用 `DepthController.setState()`
- 带动画的状态切换:调用 `DepthController.setStateWithAnimation()`
### 4.2 动画控制器
- **调用方**:各种动画控制器
- `AllAppsSwipeController`
- `LauncherBackAnimationController`
- `RecentsAtomicAnimationFactory`
- `ScalingWorkspaceRevealAnim`
- **调用时机**:执行各种过渡动画时
- **调用方式**:通过 `PendingAnimation` 设置 stateDepth 的动画值
### 4.3 小部件选择器
- **调用方**:`QuickstepLauncher`
- **调用时机**:当打开或关闭小部件选择器时
- **调用方式**:调用 `widgetDepth.setValue()` 设置深度值
### 4.4 其他触发点
- **Surface 变化**:当基础 Surface 变化时,会重新应用深度和模糊效果
- **模糊状态变化**:当跨窗口模糊状态变化时,会重新应用深度和模糊效果
- **绘制事件**:Launcher 绘制时会确保 Surface 有效,间接影响深度效果
## 5. 技术要点分析
### 5.1 深度值映射
- **壁纸缩放**:深度值直接映射到壁纸缩放级别
- **模糊强度**:通过 `mapDepthToBlur()` 方法将深度值映射到模糊半径:
```java
private static float mapDepthToBlur(float depth) {
return Interpolators.clampToProgress(depth, 0, 0.3f);
}
```
- 路径:`BaseDepthController.java:422-424`
### 5.2 性能优化
- **深度值去重**:对深度值进行舍入处理,避免频繁、不可感知的更新
```java
int depthI = (int) (depth * 256);
depthF = depthI / 256f;
```
- 路径:`BaseDepthController.java:345-346`
- **模糊效果优化**:跳过微小的模糊变化,减少不必要的渲染操作
```java
if (skipSimilarBlur && delta < Utilities.dpToPx(1) && newBlur != 0 && previousBlur != 0
&& blurAmount != 1f) {
return;
}
```
- 路径:`BaseDepthController.java:235-239`
### 5.3 视觉效果增强
- **早期唤醒**:当执行昂贵的模糊操作时,通知 SurfaceFlinger 调整内部偏移,避免卡顿
```java
if (wantsEarlyWakeUp && !mInEarlyWakeUp) {
setEarlyWakeup(surfaceTransaction.getTransaction(), true);
}
```
- 路径:`BaseDepthController.java:255-256`
- **工作区模糊**:根据状态和模糊设置,为工作区应用模糊效果
```java
mLauncher.getDepthBlurTargets().forEach(target -> target.setRenderEffect(blurEffect));
```
- 路径:`BaseDepthController.java:330`
## 6. 调用流程图
```mermaid
flowchart TD
subgraph 顶层调用触发点
A1[用户点击] -->|打开所有应用| B1[OptionsPopupView.enterAllApps]
A2[用户点击] -->|进入编辑模式| B2[OptionsPopupView.enterHomeGardening]
A3[辅助功能] -->|打开所有应用| B3[AccessibilityActionsView]
A4[键盘快捷键] -->|打开所有应用| B4[KeyboardShortcutsDelegate]
A5[小部件操作] -->|完成添加| B5[BaseWidgetSheet]
A6[拖拽操作] -->|完成拖拽| B6[FlingAnimation]
A7[触摸操作] -->|返回主屏幕| B7[WorkspaceTouchListener]
end
subgraph 状态管理系统
B1 --> C[StateManager.goToState]
B2 --> C
B3 --> C
B4 --> C
B5 --> C
B6 --> C
B7 --> C
C -->|无动画| D[StateManager.setState]
C -->|有动画| E[StateManager.setStateWithAnimation]
D --> F[遍历StateHandlers]
E --> G[遍历StateHandlers]
end
subgraph 上游调用方
F --> H[DepthController.setState]
G --> I[DepthController.setStateWithAnimation]
J[RecentsView] -->|最近任务切换| K[ObjectAnimator.ofFloat]
L[LauncherBackAnimationController] -->|返回动画开始| M[直接设置深度]
N[LauncherActivityInterface] -->|活动转换| O[PendingAnimation.addFloat]
P[TaskViewUtils] -->|任务视图操作| Q[PendingAnimation.setFloat]
R[QuickstepTransitionManager] -->|快速步骤过渡| S[ObjectAnimator.ofFloat]
T[QuickstepLauncher] -->|小部件选择器| U[直接设置深度]
end
subgraph 深度控制器
H --> V[stateDepth.setValue]
I --> W[animation.setFloat]
K --> V
M --> V
O --> V
Q --> V
S --> V
U --> X[widgetDepth.setValue]
V --> Y[MultiPropertyFactory]
X --> Y
Y -->|Float::max 策略| Z[计算最终深度]
Z --> AA[DEPTH.set]
AA --> BB[BaseDepthController.setDepth]
BB --> CC[应用深度和模糊]
end
subgraph 效果应用
CC --> DD[设置壁纸缩放]
CC --> EE[计算模糊半径]
CC --> FF[应用Surface模糊]
CC --> GG[处理早期唤醒]
CC --> HH[应用工作区模糊]
end
style C fill:#000,stroke:#333,stroke-width:2px
style H fill:#f9f,stroke:#333,stroke-width:2px
style I fill:#f9f,stroke:#333,stroke-width:2px
style V fill:#f9f,stroke:#333,stroke-width:2px
style X fill:#f9f,stroke:#333,stroke-width:2px
style Y fill:#bbf,stroke:#333,stroke-width:2px
style Z fill:#bbf,stroke:#333,stroke-width:2px
style AA fill:#bbf,stroke:#333,stroke-width:2px
style BB fill:#bbf,stroke:#333,stroke-width:2px
style CC fill:#bfb,stroke:#333,stroke-width:2px
```
## 7. 时序图
```mermaid
sequenceDiagram
participant User as 用户
participant Trigger as 触发组件
participant StateManager as 状态管理器
participant DepthController as 深度控制器
participant MultiProperty as 多属性工厂
participant BaseDepth as 基础深度控制
participant Visual as 视觉效果系统
%% 无动画状态切换流程
User->>Trigger: 触发操作(如点击)
Trigger->>StateManager: goToState(目标状态, false)
StateManager->>StateManager: 检查当前状态
StateManager->>DepthController: setState(目标状态)
DepthController->>DepthController: 获取目标状态深度值
DepthController->>MultiProperty: stateDepth.setValue(深度值)
MultiProperty->>MultiProperty: 计算最终深度(Float::max)
MultiProperty->>BaseDepth: DEPTH.set(最终深度)
BaseDepth->>BaseDepth: setDepth(最终深度)
BaseDepth->>Visual: 设置壁纸缩放
BaseDepth->>Visual: 计算模糊半径
BaseDepth->>Visual: 应用Surface模糊
BaseDepth->>Visual: 应用工作区模糊
Visual-->>User: 显示更新后的视觉效果
%% 带动画状态切换流程
User->>Trigger: 触发操作(如滑动)
Trigger->>StateManager: goToState(目标状态, true)
StateManager->>StateManager: 创建PendingAnimation
StateManager->>DepthController: setStateWithAnimation(目标状态, 配置, 动画)
DepthController->>DepthController: 获取目标状态深度值
DepthController->>DepthController: 配置动画插值器
DepthController->>DepthController: animation.setFloat(stateDepth, 深度值, 插值器)
DepthController-->>StateManager: 返回配置好的动画
StateManager->>StateManager: 启动动画
StateManager->>MultiProperty: 动画过程中更新stateDepth
MultiProperty->>MultiProperty: 实时计算最终深度
MultiProperty->>BaseDepth: DEPTH.set(实时深度)
BaseDepth->>BaseDepth: setDepth(实时深度)
BaseDepth->>Visual: 实时更新视觉效果
Visual-->>User: 显示动画效果
StateManager->>StateManager: 动画完成回调
StateManager-->>User: 显示最终状态
%% 小部件选择器流程
User->>Trigger: 打开小部件选择器
Trigger->>DepthController: 直接设置widgetDepth
DepthController->>MultiProperty: widgetDepth.setValue(深度值)
MultiProperty->>MultiProperty: 计算最终深度(Float::max)
MultiProperty->>BaseDepth: DEPTH.set(最终深度)
BaseDepth->>BaseDepth: setDepth(最终深度)
BaseDepth->>Visual: 更新视觉效果
Visual-->>User: 显示小部件选择器
User->>Trigger: 关闭小部件选择器
Trigger->>DepthController: 重置widgetDepth
DepthController->>MultiProperty: widgetDepth.setValue(0)
MultiProperty->>MultiProperty: 计算最终深度(Float::max)
MultiProperty->>BaseDepth: DEPTH.set(最终深度)
BaseDepth->>BaseDepth: setDepth(最终深度)
BaseDepth->>Visual: 更新视觉效果
Visual-->>User: 返回正常状态
```
## 8. 代码优化建议
### 8.1 代码结构优化
- **建议**:将 `applyDepthAndBlur()` 方法拆分为更小的职责单一方法
- **理由**:当前方法超过 100 行,包含多个职责(壁纸缩放、模糊计算、Surface 处理等),不利于维护
### 8.2 性能优化
- **建议**:增加深度值变化的阈值判断,进一步减少不必要的更新
- **理由**:当前的舍入处理已经减少了更新,但可以根据实际视觉效果,设置一个更大的阈值
### 8.3 可测试性优化
- **建议**:为深度计算和模糊映射逻辑添加单元测试
- **理由**:这些核心逻辑直接影响用户体验,确保其正确性非常重要
### 8.4 可读性优化
- **建议**:增加更多的注释,特别是对于深度值与模糊效果之间的映射关系
- **理由**:深度控制逻辑较为复杂,详细的注释有助于后续维护
## 9. 结论
DepthController 系统通过 DEPTH、stateDepth 和 widgetDepth 三个核心组件,实现了 Launcher 在不同状态下的深度和模糊效果控制。其设计采用了属性动画和 Surface 控制的结合,确保了视觉效果的平滑过渡和性能的优化。
- **DEPTH**:核心深度控制属性,直接影响壁纸缩放和模糊效果
- **stateDepth**:处理状态转换时的深度变化,确保状态切换的视觉连贯性
- **widgetDepth**:处理小部件选择器的深度变化,提供专门的视觉体验
这种设计不仅实现了复杂的视觉效果,还通过一系列优化措施确保了性能的稳定,是 Android Launcher 视觉体验的重要组成部分。通过本文档的分析,我们对 DepthController 的工作原理有了更深入的理解,为后续的开发和优化提供了参考。
## 9. 参考文件
- `quickstep/src/com/android/launcher3/statehandlers/DepthController.java`
- `quickstep/src/com/android/quickstep/util/BaseDepthController.java`
- `quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java`
- `quickstep/src/com/android/quickstep/views/RecentsView.java`
- `quickstep/src/com/android/quickstep/LauncherBackAnimationController.java`
- `quickstep/src/com/android/quickstep/LauncherActivityInterface.java`
- `quickstep/src/com/android/quickstep/TaskViewUtils.java`
- `quickstep/src/com/android/launcher3/QuickstepTransitionManager.java`
- `src/com/android/launcher3/touch/AllAppsSwipeController.java`
+651
View File
@@ -0,0 +1,651 @@
# InputConsumer架构分析报告
## 概述
本文档基于对AOSP Launcher3源码的深入分析,详细解析了`InputConsumer`接口及其实现类的架构设计、触发条件和作用机制。分析范围涵盖`TouchInteractionService`中的输入事件处理流程,以及所有InputConsumer实现类的功能特性。
## 1. InputConsumer接口定义
### 1.1 接口位置
- **文件路径**: `quickstep/src/com/android/quickstep/InputConsumer.java`
- **接口类型**: 输入事件消费者接口
### 1.2 核心类型定义
```java
public interface InputConsumer {
int TYPE_NO_OP = 1 << 0; // 无操作类型
int TYPE_LAUNCHER = 1 << 1; // Launcher输入处理
int TYPE_OTHER_ACTIVITY = 1 << 2; // 其他Activity输入处理
int TYPE_ASSISTANT = 1 << 3; // 助手输入处理
int TYPE_DEVICE_LOCKED = 1 << 4; // 设备锁定状态输入处理
int TYPE_ACCESSIBILITY = 1 << 5; // 无障碍功能输入处理
int TYPE_SCREEN_PINNED = 1 << 6; // 屏幕固定状态输入处理
int TYPE_LAUNCHER_WITHOUT_FOCUS = 1 << 7; // 无焦点Launcher输入处理
int TYPE_RESET_GESTURE = 1 << 8; // 手势重置处理
int TYPE_PROGRESS_DELEGATE = 1 << 9; // 进度委托处理
int TYPE_SYSUI_OVERLAY = 1 << 10; // 系统UI覆盖层输入处理
int TYPE_ONE_HANDED = 1 << 11; // 单手模式输入处理
int TYPE_TASKBAR_STASH = 1 << 12; // 任务栏隐藏/显示处理
int TYPE_STATUS_BAR = 1 << 13; // 状态栏输入处理
int TYPE_CURSOR_HOVER = 1 << 14; // 光标悬停处理
int TYPE_NAV_HANDLE_LONG_PRESS = 1 << 15; // 导航手柄长按处理
int TYPE_BUBBLE_BAR = 1 << 16; // 气泡栏输入处理
}
```
## 2. InputConsumer实现类分析
### 2.1 实现类列表
| 实现类 | 类型 | 主要功能 |
|--------|------|----------|
| LauncherInputConsumer | TYPE_LAUNCHER | 处理Launcher界面的触摸输入 |
| OtherActivityInputConsumer | TYPE_OTHER_ACTIVITY | 处理其他Activity的输入事件 |
| AssistantInputConsumer | TYPE_ASSISTANT | 处理助手手势输入 |
| DeviceLockedInputConsumer | TYPE_DEVICE_LOCKED | 设备锁定状态下的输入处理 |
| AccessibilityInputConsumer | TYPE_ACCESSIBILITY | 无障碍功能输入处理 |
| ScreenPinnedInputConsumer | TYPE_SCREEN_PINNED | 屏幕固定模式输入处理 |
| LauncherWithoutFocusInputConsumer | TYPE_LAUNCHER_WITHOUT_FOCUS | 无焦点Launcher输入处理 |
| ResetGestureInputConsumer | TYPE_RESET_GESTURE | 手势重置处理 |
| ProgressDelegateInputConsumer | TYPE_PROGRESS_DELEGATE | 进度委托输入处理 |
| SysUiOverlayInputConsumer | TYPE_SYSUI_OVERLAY | 系统UI覆盖层输入处理 |
| OneHandedModeInputConsumer | TYPE_ONE_HANDED | 单手模式输入处理 |
| TaskbarUnstashInputConsumer | TYPE_TASKBAR_STASH | 任务栏显示/隐藏处理 |
| TrackpadStatusBarInputConsumer | TYPE_STATUS_BAR | 触控板状态栏输入处理 |
| BubbleBarInputConsumer | TYPE_BUBBLE_BAR | 气泡栏输入处理 |
| NavHandleLongPressInputConsumer | TYPE_NAV_HANDLE_LONG_PRESS | 导航手柄长按处理 |
### 2.2 关键实现类详细分析
#### 2.2.1 LauncherInputConsumer
- **触发条件**: 当Launcher处于活动状态且用户触摸在Launcher界面时
- **主要功能**:
- 处理Launcher界面的触摸事件
- 支持拖拽操作和手势识别
- 处理音量键等系统按键
#### 2.2.2 OtherActivityInputConsumer
- **触发条件**: 当用户在其他应用界面进行手势操作时
- **主要功能**:
- 处理从其他应用启动的手势
- 支持快速切换应用
- 处理手势暂停检测
#### 2.2.3 AssistantInputConsumer
- **触发条件**: 设备支持助手手势且满足触发条件时
- **主要功能**:
- 处理助手手势输入
- 支持拖拽距离和角度检测
- 提供触觉反馈
#### 2.2.4 DeviceLockedInputConsumer
- **触发条件**: 设备处于锁定状态时
- **主要功能**:
- 处理锁定状态下的输入事件
- 支持直接启动应用(如相机)
- 提供占位符输入处理
## 3. TouchInteractionService中的InputConsumer创建逻辑
### 3.1 核心入口方法
- **方法**: `onInputEvent(InputEvent ev)`
- **位置**: `TouchInteractionService.java` 第1057-1285行
### 3.2 输入事件处理流程
```java
private void onInputEvent(InputEvent ev) {
// 1. 输入事件类型检查
if (!(ev instanceof MotionEvent)) return;
// 2. 用户解锁状态检查
if (!LockedUserState.get(this).isUserUnlocked()) return;
// 3. 设备状态获取
RecentsAnimationDeviceState deviceState = mDeviceStateRepository.get(displayId);
// 4. 导航模式检查
NavigationMode currentNavMode = deviceState.getMode();
// 5. InputConsumer选择逻辑
if (deviceState.isButtonNavMode() && deviceState.supportsAssistantGestureInButtonNav()) {
// 三键导航模式且支持助手手势
mUncheckedConsumer = tryCreateAssistantInputConsumer(...);
} else if ((!isOneHandedModeActive && isInSwipeUpTouchRegion) || isHoverActionWithoutConsumer) {
// 常规手势区域或悬停操作
mConsumer = newConsumer(...);
} else if ((deviceState.isFullyGesturalNavMode() || isTrackpadMultiFingerSwipe(event))
&& deviceState.canTriggerAssistantAction(event)) {
// 全手势导航或触控板多指滑动且可触发助手
mUncheckedConsumer = tryCreateAssistantInputConsumer(...);
} else if (deviceState.canTriggerOneHandedAction(event)) {
// 可触发单手模式
mUncheckedConsumer = new OneHandedModeInputConsumer(...);
} else {
// 默认无操作
mUncheckedConsumer = InputConsumer.createNoOpInputConsumer(displayId);
}
}
```
### 3.3 InputConsumer选择优先级
1. **BubbleBar输入处理** - 事件在气泡上时优先处理
2. **进度委托处理** - 当存在进度代理时
3. **设备锁定状态** - 设备未解锁时
4. **助手手势** - 满足助手触发条件时
5. **任务栏显示/隐藏** - 任务栏相关操作
6. **导航手柄长按** - 导航手柄长按操作
7. **系统UI对话框** - 系统对话框显示时
8. **触控板状态栏** - 触控板三指手势
9. **屏幕固定** - 屏幕固定模式
10. **单手模式** - 单手模式手势
11. **无障碍功能** - 无障碍菜单可用时
## 4. InputConsumerUtils.kt中的创建逻辑
### 4.1 newConsumer方法核心逻辑
```kotlin
fun newConsumer(...): InputConsumer {
// 1. 气泡栏检查
if (BubbleBarInputConsumer.isEventOnBubbles(tac, event)) {
return BubbleBarInputConsumer(...)
}
// 2. 进度委托检查
if (progressProxy != null) {
return ProgressDelegateInputConsumer(...)
}
// 3. 设备锁定状态检查
if (!get(context).isUserUnlocked) {
return createDeviceLockedInputConsumer(...)
}
// 4. 基础消费者创建
val base = if (canStartSystemGesture || previousGestureState.isRecentsAnimationRunning) {
newBaseConsumer(...)
} else {
getDefaultInputConsumer(...)
}
// 5. 分层包装逻辑
if (deviceState.isGesturalNavMode || gestureState.isTrackpadGesture) {
// 助手输入处理
if (deviceState.canTriggerAssistantAction(event)) {
base = tryCreateAssistantInputConsumer(...)
}
// 任务栏处理
if (tac != null && base !is AssistantInputConsumer) {
base = TaskbarUnstashInputConsumer(...)
}
// 导航手柄长按处理
if (canStartSystemGesture && navHandle.canNavHandleBeLongPressed()) {
base = NavHandleLongPressInputConsumer(...)
}
// 系统UI对话框处理
if (deviceState.isSystemUiDialogShowing) {
base = SysUiOverlayInputConsumer(...)
}
// 触控板状态栏处理
if (gestureState.isTrackpadGesture) {
base = TrackpadStatusBarInputConsumer(...)
}
// 屏幕固定处理
if (deviceState.isScreenPinningActive) {
base = ScreenPinnedInputConsumer(...)
}
// 单手模式处理
if (deviceState.canTriggerOneHandedAction(event)) {
base = OneHandedModeInputConsumer(...)
}
// 无障碍功能处理
if (deviceState.isAccessibilityMenuAvailable) {
base = AccessibilityInputConsumer(...)
}
}
return base
}
```
## 5. 架构图与流程图
### 5.1 InputConsumer类图
```mermaid
classDiagram
class InputConsumer {
<<interface>>
+getType() int
+getDisplayId() int
+onMotionEvent(MotionEvent)
+onHoverEvent(MotionEvent)
+onKeyEvent(KeyEvent)
}
class DelegateInputConsumer {
<<abstract>>
-mDelegate: InputConsumer
+onMotionEvent(MotionEvent)
}
class LauncherInputConsumer {
-mContainer: T
-mContainerInterface: BaseContainerInterface
+onMotionEvent(MotionEvent)
+onKeyEvent(KeyEvent)
}
class OtherActivityInputConsumer {
-mDeviceState: RecentsAnimationDeviceState
-mTaskAnimationManager: TaskAnimationManager
+onMotionEvent(MotionEvent)
}
class AssistantInputConsumer {
-mDownPos: PointF
-mLastPos: PointF
+onMotionEvent(MotionEvent)
}
InputConsumer <|-- DelegateInputConsumer
InputConsumer <|-- LauncherInputConsumer
InputConsumer <|-- OtherActivityInputConsumer
DelegateInputConsumer <|-- AssistantInputConsumer
InputConsumer <|-- DeviceLockedInputConsumer
InputConsumer <|-- AccessibilityInputConsumer
InputConsumer <|-- ScreenPinnedInputConsumer
InputConsumer <|-- OneHandedModeInputConsumer
InputConsumer <|-- TaskbarUnstashInputConsumer
InputConsumer <|-- TrackpadStatusBarInputConsumer
InputConsumer <|-- BubbleBarInputConsumer
InputConsumer <|-- NavHandleLongPressInputConsumer
```
### 5.2 InputConsumer创建流程图
```mermaid
flowchart TD
A["输入事件到达<br/>TouchInteractionService.onInputEvent"] --> B{"事件类型检查<br/>if (!(ev instanceof MotionEvent))"}
B -->|MotionEvent| C{"用户解锁状态<br/>LockedUserState.get(this).isUserUnlocked()"}
B -->|非MotionEvent| Z["记录并返回<br/>ActiveGestureProtoLogProxy.logUnknownInputEvent"]
C -->|已解锁| D{"设备状态检查<br/>mDeviceStateRepository.get(displayId)"}
C -->|未解锁| Y["设备锁定处理<br/>DeviceLockedInputConsumer"]
D --> E{"气泡栏检查<br/>BubbleBarInputConsumer.isEventOnBubbles(tac, event)"}
E -->|在气泡上| F["BubbleBarInputConsumer<br/>InputConsumerUtils.newConsumer() 第1优先级"]
E -->|不在气泡上| G{"进度委托检查<br/>progressProxy != null"}
G -->|存在进度代理| H["ProgressDelegateInputConsumer<br/>InputConsumerUtils.newConsumer() 第2优先级"]
G -->|无进度代理| I{"系统手势检查<br/>canStartSystemGesture || previousGestureState.isRecentsAnimationRunning"}
I -->|可启动系统手势| J["newBaseConsumer<br/>LauncherInputConsumer/OtherActivityInputConsumer"]
I -->|不可启动系统手势| K["getDefaultInputConsumer<br/>默认无操作消费者"]
J --> L{"基础消费者类型<br/>newBaseConsumer() 内部逻辑"}
L -->|"Launcher活动<br/>containerInterface.isInLiveTileMode()等"| M["LauncherInputConsumer"]
L -->|其他Activity<br/>默认情况| N["OtherActivityInputConsumer"]
M --> O{"分层包装检查<br/>deviceState.isGesturalNavMode || gestureState.isTrackpadGesture"}
N --> O
K --> O
O --> P{"助手手势检查<br/>deviceState.canTriggerAssistantAction(event)"}
P -->|可触发助手| Q["AssistantInputConsumer<br/>tryCreateAssistantInputConsumer()"]
P -->|不可触发助手| R{"任务栏检查<br/>tac != null && base !is AssistantInputConsumer"}
R -->|任务栏相关<br/>tac.deviceProfile.isTaskbarPresent等| S["TaskbarUnstashInputConsumer"]
R -->|非任务栏相关| T{"导航手柄检查<br/>navHandle.canNavHandleBeLongPressed()"}
T -->|可长按| U["NavHandleLongPressInputConsumer"]
T -->|不可长按| V{"系统UI检查<br/>deviceState.isSystemUiDialogShowing"}
V -->|系统对话框| W["SysUiOverlayInputConsumer"]
V -->|无系统对话框| X{"触控板检查<br/>gestureState.isTrackpadGesture"}
X -->|触控板手势| Y1["TrackpadStatusBarInputConsumer"]
X -->|非触控板| Z1{"屏幕固定检查<br/>deviceState.isScreenPinningActive"}
Z1 -->|屏幕固定| A1["ScreenPinnedInputConsumer"]
Z1 -->|非屏幕固定| B1{"单手模式检查<br/>deviceState.canTriggerOneHandedAction(event)"}
B1 -->|可触发单手| C1["OneHandedModeInputConsumer"]
B1 -->|不可触发单手| D1{"无障碍检查<br/>deviceState.isAccessibilityMenuAvailable"}
D1 -->|无障碍可用| E1["AccessibilityInputConsumer"]
D1 -->|无障碍不可用| F1["返回最终消费者<br/>InputConsumerUtils.newConsumer() 返回"]
F --> F1
H --> F1
Q --> F1
S --> F1
U --> F1
W --> F1
Y1 --> F1
A1 --> F1
C1 --> F1
E1 --> F1
Y --> F1
```
### 5.3 时序图
```mermaid
sequenceDiagram
participant User as 用户
participant TIS as TouchInteractionService
participant ICUtils as InputConsumerUtils
participant DeviceState as DeviceState
participant TaskbarMgr as TaskbarManager
participant IC as InputConsumer
User->>TIS: 输入事件(MotionEvent)
TIS->>TIS: 1. 事件类型检查<br/>if (!(ev instanceof MotionEvent))
TIS->>TIS: 2. 用户解锁状态检查<br/>LockedUserState.get(this).isUserUnlocked()
TIS->>DeviceState: 3. 获取设备状态<br/>mDeviceStateRepository.get(displayId)
TIS->>TaskbarMgr: 4. 获取Taskbar状态<br/>mTaskbarManager.getTaskbarForDisplay(displayId)
TIS->>ICUtils: 5. 调用newConsumer()
ICUtils->>ICUtils: 6. 气泡栏检查<br/>BubbleBarInputConsumer.isEventOnBubbles(tac, event)
alt 事件在气泡上
ICUtils->>IC: 创建BubbleBarInputConsumer<br/>return BubbleBarInputConsumer(...)
else 不在气泡上
ICUtils->>ICUtils: 7. 进度委托检查<br/>progressProxy != null
alt 存在进度代理
ICUtils->>IC: 创建ProgressDelegateInputConsumer<br/>return ProgressDelegateInputConsumer(...)
else 无进度代理
ICUtils->>ICUtils: 8. 设备锁定检查<br/>!get(context).isUserUnlocked
alt 设备未解锁
ICUtils->>IC: 创建DeviceLockedInputConsumer<br/>return createDeviceLockedInputConsumer(...)
else 设备已解锁
ICUtils->>ICUtils: 9. 基础消费者选择<br/>canStartSystemGesture判断
ICUtils->>ICUtils: 10. 分层包装逻辑<br/>deviceState.isGesturalNavMode检查
ICUtils->>IC: 创建最终InputConsumer
end
end
end
ICUtils-->>TIS: 11. 返回InputConsumer实例
TIS->>IC: 12. 调用onMotionEvent()<br/>mUncheckedConsumer.onMotionEvent(event)
IC->>IC: 13. 具体事件处理逻辑<br/>根据InputConsumer类型处理
IC-->>TIS: 14. 处理完成
TIS-->>User: 15. 系统响应
```
## 6. 关键设计模式分析
### 6.1 责任链模式 (Chain of Responsibility)
**实现原理**: InputConsumer的创建过程采用了责任链模式,每个条件检查都是一个处理节点,按照优先级依次处理。
**代码实现**:
```java
// TouchInteractionService.java 第1156-1185行
if (deviceState.isButtonNavMode() && deviceState.supportsAssistantGestureInButtonNav()) {
// 第一个处理节点:三键导航模式助手手势检查
mUncheckedConsumer = tryCreateAssistantInputConsumer(...);
} else if ((!isOneHandedModeActive && isInSwipeUpTouchRegion) || isHoverActionWithoutConsumer) {
// 第二个处理节点:常规手势区域检查
mConsumer = newConsumer(...);
} else if ((deviceState.isFullyGesturalNavMode() || isTrackpadMultiFingerSwipe(event))
&& deviceState.canTriggerAssistantAction(event)) {
// 第三个处理节点:全手势导航助手检查
mUncheckedConsumer = tryCreateAssistantInputConsumer(...);
} else if (deviceState.canTriggerOneHandedAction(event)) {
// 第四个处理节点:单手模式检查
mUncheckedConsumer = new OneHandedModeInputConsumer(...);
} else {
// 默认处理节点
mUncheckedConsumer = InputConsumer.createNoOpInputConsumer(displayId);
}
```
**设计优势**:
- 每个条件检查独立,便于维护和扩展
- 按照优先级处理,提高执行效率
- 支持动态添加新的处理节点
### 6.2 装饰器模式 (Decorator)
**实现原理**: 通过DelegateInputConsumer实现了装饰器模式,可以在基础InputConsumer上添加额外的功能。
**代码实现**:
```java
// DelegateInputConsumer.java 基类
public abstract class DelegateInputConsumer implements InputConsumer {
protected final InputConsumer mDelegate;
public DelegateInputConsumer(int displayId, InputConsumer delegate,
InputMonitorCompat inputMonitorCompat) {
mDelegate = delegate;
// ...
}
@Override
public void onMotionEvent(MotionEvent ev) {
// 装饰器逻辑
if (mState != STATE_DELEGATE_ACTIVE) {
// 添加额外功能
handleCustomLogic(ev);
}
// 委托给基础消费者
mDelegate.onMotionEvent(ev);
}
}
// AssistantInputConsumer.java 具体装饰器
public class AssistantInputConsumer extends DelegateInputConsumer {
public AssistantInputConsumer(Context context, GestureState gestureState,
InputConsumer delegate, InputMonitorCompat inputMonitorCompat,
RecentsAnimationDeviceState deviceState, MotionEvent startEvent) {
super(gestureState.getDisplayId(), delegate, inputMonitorCompat);
// 助手特定的初始化逻辑
}
@Override
public void onMotionEvent(MotionEvent ev) {
// 助手手势识别逻辑
if (isAssistantGesture(ev)) {
handleAssistantGesture(ev);
} else {
super.onMotionEvent(ev);
}
}
}
```
**设计优势**:
- 功能分层,支持功能组合
- 不修改基础消费者,符合开闭原则
- 支持动态添加和移除功能
### 6.3 策略模式 (Strategy)
**实现原理**: 不同的InputConsumer实现类对应不同的输入处理策略,根据运行时条件动态选择。
**代码实现**:
```java
// InputConsumerUtils.kt 第63-150行
fun newConsumer(...): InputConsumer {
// 策略选择逻辑
val base = if (canStartSystemGesture || previousGestureState.isRecentsAnimationRunning) {
newBaseConsumer(...) // 选择基础策略
} else {
getDefaultInputConsumer(...) // 选择默认策略
}
// 策略包装逻辑
if (deviceState.isGesturalNavMode || gestureState.isTrackpadGesture) {
if (deviceState.canTriggerAssistantAction(event)) {
base = tryCreateAssistantInputConsumer(...) // 助手策略
}
if (tac != null && base !is AssistantInputConsumer) {
base = TaskbarUnstashInputConsumer(...) // 任务栏策略
}
// ... 更多策略选择
}
return base
}
// 具体策略实现示例
class LauncherInputConsumer : InputConsumer { /* Launcher界面策略 */ }
class OtherActivityInputConsumer : InputConsumer { /* 其他应用策略 */ }
class AssistantInputConsumer : DelegateInputConsumer { /* 助手手势策略 */ }
```
**设计优势**:
- 策略独立,便于测试和维护
- 运行时动态选择策略
- 支持策略组合和扩展
## 7. 性能优化考虑
### 7.1 延迟初始化
**实现代码**:
```java
// InputConsumer只在需要时创建,避免不必要的对象实例化
// TouchInteractionService.java 第1156行
if (action == ACTION_DOWN || isHoverActionWithoutConsumer) {
// 只有在ACTION_DOWN或悬停操作时才创建InputConsumer
mUncheckedConsumer = createAppropriateInputConsumer(...);
}
```
**优化效果**: 减少内存占用,提高响应速度
### 7.2 条件检查优化
**实现代码**:
```kotlin
// InputConsumerUtils.kt 第85-150行
// 按照触发概率从高到低排序条件检查
if (bubbleControllers != null && BubbleBarInputConsumer.isEventOnBubbles(tac, event)) {
return BubbleBarInputConsumer(...) // 高概率条件在前
}
if (progressProxy != null) {
return ProgressDelegateInputConsumer(...) // 次高概率条件
}
if (!get(context).isUserUnlocked) {
return createDeviceLockedInputConsumer(...) // 较低概率条件
}
// ... 后续条件检查
```
**优化效果**: 短路评估,减少不必要的条件检查
### 7.3 对象复用
**实现代码**:
```java
// GestureState.java 中复用机制
public class GestureState {
private InputConsumer mCurrentConsumer;
public void updateConsumer(InputConsumer newConsumer) {
if (mCurrentConsumer != null && canReuseConsumer(mCurrentConsumer, newConsumer)) {
// 复用现有消费者
mCurrentConsumer.onConsumerAboutToBeSwitched();
} else {
mCurrentConsumer = newConsumer;
}
}
}
```
**优化效果**: 减少GC压力,提高性能
## 8. 扩展性设计
### 8.1 接口设计
**代码实现**:
```java
// InputConsumer.java 统一接口定义
public interface InputConsumer {
int getType();
int getDisplayId();
void onMotionEvent(MotionEvent ev);
void onHoverEvent(MotionEvent ev);
void onKeyEvent(KeyEvent ev);
// ... 其他统一方法
}
// 新InputConsumer实现示例
public class CustomInputConsumer implements InputConsumer {
@Override
public int getType() {
return TYPE_CUSTOM;
}
@Override
public void onMotionEvent(MotionEvent ev) {
// 自定义处理逻辑
}
}
```
**扩展优势**: 新实现只需遵循接口规范
### 8.2 类型标识
**代码实现**:
```java
// InputConsumer.java 类型常量定义
public interface InputConsumer {
int TYPE_NO_OP = 1 << 0;
int TYPE_LAUNCHER = 1 << 1;
int TYPE_OTHER_ACTIVITY = 1 << 2;
// ... 其他类型常量
String[] NAMES = new String[] {
"TYPE_NO_OP", "TYPE_LAUNCHER", "TYPE_OTHER_ACTIVITY", // ...
};
}
// 类型检查和组合
public static boolean isLauncherConsumer(InputConsumer consumer) {
return (consumer.getType() & TYPE_LAUNCHER) != 0;
}
```
**扩展优势**: 支持类型检查和位运算组合
### 8.3 配置化
**代码实现**:
```kotlin
// InputConsumerUtils.kt 配置化支持
fun newConsumer(
context: Context,
deviceState: RecentsAnimationDeviceState, // 设备状态配置
gestureState: GestureState, // 手势状态配置
// ... 其他配置参数
): InputConsumer {
// 根据配置动态选择InputConsumer
return when {
deviceState.isGesturalNavMode -> createGestureNavConsumer(...)
deviceState.isButtonNavMode -> createButtonNavConsumer(...)
else -> createDefaultConsumer(...)
}
}
```
**扩展优势**: 支持运行时配置,适应不同设备和场景
## 9. 总结
InputConsumer架构是AOSP Launcher3中处理输入事件的核心机制,它通过分层设计和条件判断实现了灵活的事件分发和处理。这种设计不仅提高了代码的可维护性,还为系统的扩展性提供了良好的基础。
关键优势:
- **模块化设计**: 每个InputConsumer专注于特定功能
- **条件驱动**: 根据运行时状态动态选择处理策略
- **扩展性强**: 易于添加新的InputConsumer实现
- **性能优化**: 延迟初始化和条件检查优化
这种架构设计为Android系统的多任务手势操作提供了强大的支持,是理解Launcher3输入处理机制的重要基础。
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,632 @@
# InputConsumer触发条件详细分析
## 概述
本文档详细分析了AOSP Launcher3中所有InputConsumer实现类的具体触发条件,基于对`TouchInteractionService.java`和`InputConsumerUtils.kt`源码的深入分析。
## 1. TouchInteractionService中的触发条件分析
### 1.1 核心触发逻辑位置
- **文件**: `quickstep/src/com/android/quickstep/TouchInteractionService.java`
- **方法**: `onInputEvent(InputEvent ev)` (第1057-1285行)
### 1.2 主要触发条件分支
#### 分支1: 三键导航模式 + 助手手势支持
```java
if (deviceState.isButtonNavMode()
&& deviceState.supportsAssistantGestureInButtonNav()) {
// 条件: 三键导航模式且支持助手手势
if (deviceState.canTriggerAssistantAction(event)) {
// 子条件: 事件可触发助手动作
mUncheckedConsumer = tryCreateAssistantInputConsumer(...);
} else {
// 子条件: 事件不可触发助手动作
mUncheckedConsumer = createNoOpInputConsumer(displayId);
}
}
```
#### 分支2: 常规手势区域或悬停操作
```java
else if ((!isOneHandedModeActive && isInSwipeUpTouchRegion)
|| isHoverActionWithoutConsumer || isOnBubbles) {
// 条件1: 单手模式未激活且在滑动手势区域
// 条件2: 悬停操作且无当前消费者
// 条件3: 事件在气泡上
mConsumer = newConsumer(...); // 调用InputConsumerUtils.newConsumer
}
```
#### 分支3: 全手势导航或触控板多指滑动 + 助手触发
```java
else if ((deviceState.isFullyGesturalNavMode() || isTrackpadMultiFingerSwipe(event))
&& deviceState.canTriggerAssistantAction(event)) {
// 条件1: 全手势导航模式
// 条件2: 触控板多指滑动
// 条件3: 事件可触发助手动作
mUncheckedConsumer = tryCreateAssistantInputConsumer(...);
}
```
#### 分支4: 单手模式触发
```java
else if (deviceState.canTriggerOneHandedAction(event)) {
// 条件: 事件可触发单手模式
mUncheckedConsumer = new OneHandedModeInputConsumer(...);
}
```
#### 分支5: 默认情况
```java
else {
// 默认: 无操作消费者
mUncheckedConsumer = InputConsumer.createNoOpInputConsumer(displayId);
}
```
## 2. InputConsumerUtils.kt中的详细触发条件
### 2.1 优先级1: 气泡栏检查
```kotlin
if (bubbleControllers != null && BubbleBarInputConsumer.isEventOnBubbles(tac, event)) {
// 触发条件: 存在气泡控制器且事件在气泡上
return BubbleBarInputConsumer(...)
}
```
### 2.2 优先级2: 进度委托检查
```kotlin
if (progressProxy != null) {
// 触发条件: 存在进度代理
return ProgressDelegateInputConsumer(...)
}
```
### 2.3 优先级3: 设备锁定状态检查
```kotlin
if (!get(context).isUserUnlocked) {
// 触发条件: 设备未解锁
return if (canStartSystemGesture) {
createDeviceLockedInputConsumer(...) // 可启动系统手势
} else {
getDefaultInputConsumer(...) // 不可启动系统手势
}
}
```
### 2.4 优先级4: 基础消费者选择
```kotlin
val base = if (canStartSystemGesture || previousGestureState.isRecentsAnimationRunning) {
// 条件1: 可启动系统手势
// 条件2: 最近动画正在运行
newBaseConsumer(...)
} else {
// 默认情况
getDefaultInputConsumer(...)
}
```
### 2.5 优先级5: 分层包装逻辑
#### 2.5.1 助手手势检查
```kotlin
if (deviceState.canTriggerAssistantAction(event)) {
// 触发条件: 事件可触发助手动作
base = tryCreateAssistantInputConsumer(...)
}
```
#### 2.5.2 任务栏检查
```kotlin
if (tac != null && base !is AssistantInputConsumer) {
val useTaskbarConsumer = (tac.deviceProfile.isTaskbarPresent
&& !tac.isPhoneMode
&& !tac.isInStashedLauncherState)
if (canStartSystemGesture && useTaskbarConsumer) {
// 触发条件:
// 1. 存在TaskbarActivityContext
// 2. 不是AssistantInputConsumer
// 3. 可启动系统手势
// 4. 任务栏存在且不在手机模式且不在隐藏状态
base = TaskbarUnstashInputConsumer(...)
}
}
```
#### 2.5.3 导航手柄长按检查
```kotlin
if (canStartSystemGesture
&& !previousGestureState.isRecentsAnimationRunning
&& navHandle.canNavHandleBeLongPressed()
&& !ignoreThreeFingerTrackpadForNavHandleLongPress(gestureState)) {
// 触发条件:
// 1. 可启动系统手势
// 2. 最近动画未运行
// 3. 导航手柄可长按
// 4. 不是触控板三指手势
base = NavHandleLongPressInputConsumer(...)
}
```
#### 2.5.4 系统UI对话框检查
```kotlin
if (deviceState.isSystemUiDialogShowing) {
// 触发条件: 系统UI对话框正在显示
base = SysUiOverlayInputConsumer(...)
}
```
#### 2.5.5 触控板状态栏检查
```kotlin
if (gestureState.isTrackpadGesture
&& canStartSystemGesture
&& !previousGestureState.isRecentsAnimationRunning) {
// 触发条件:
// 1. 触控板手势
// 2. 可启动系统手势
// 3. 最近动画未运行
base = TrackpadStatusBarInputConsumer(...)
}
```
#### 2.5.6 屏幕固定检查
```kotlin
if (deviceState.isScreenPinningActive) {
// 触发条件: 屏幕固定模式激活
base = ScreenPinnedInputConsumer(...)
}
```
#### 2.5.7 单手模式检查
```kotlin
if (deviceState.canTriggerOneHandedAction(event)) {
// 触发条件: 事件可触发单手模式
base = OneHandedModeInputConsumer(...)
}
```
#### 2.5.8 无障碍功能检查
```kotlin
if (deviceState.isAccessibilityMenuAvailable) {
// 触发条件: 无障碍菜单可用
base = AccessibilityInputConsumer(...)
}
```
## 3. newBaseConsumer方法中的基础消费者选择
### 3.1 设备锁定状态检查
```kotlin
if (deviceState.isKeyguardShowingOccluded) {
// 触发条件: 锁屏被遮挡显示
return createDeviceLockedInputConsumer(...)
}
```
### 3.2 Launcher输入消费者选择条件
#### 条件1: Live Tile模式
```kotlin
if (containerInterface.isInLiveTileMode()) {
// 触发条件: 处于Live Tile模式
return createLauncherInputConsumer(...)
}
```
#### 条件2: 运行任务为空
```kotlin
if (runningTask == null) {
// 触发条件: 运行任务为空
return getDefaultInputConsumer(...)
}
```
#### 条件3: Launcher相关状态
```kotlin
if (previousGestureAnimatedToLauncher
|| launcherResumedThroughShellTransition
|| forceLauncherInputConsumer) {
// 触发条件1: 前一个手势动画到Launcher
// 触发条件2: Launcher通过Shell Transition恢复
// 触发条件3: 强制使用Launcher输入消费者
return createLauncherInputConsumer(...)
}
```
#### 条件4: 手势阻塞任务或Launcher子活动
```kotlin
if (deviceState.isGestureBlockedTask(runningTask)
|| launcherChildActivityResumed
|| ignoreNonTrackpadMouseEvent(context, gestureState, event)) {
// 触发条件1: 手势被任务阻塞
// 触发条件2: Launcher子活动恢复
// 触发条件3: 忽略非触控板鼠标事件
return getDefaultInputConsumer(...)
}
```
#### 条件5: 默认情况
```kotlin
else {
// 默认: 使用OtherActivityInputConsumer
return createOtherActivityInputConsumer(...)
}
```
## 4. 各InputConsumer实现类的具体触发条件
### 4.1 LauncherInputConsumer
- **触发条件**:
- 处于Live Tile模式
- 前一个手势动画到Launcher
- Launcher通过Shell Transition恢复
- 强制使用Launcher输入消费者
- **适用场景**: Launcher界面活动时的输入处理
### 4.2 OtherActivityInputConsumer
- **触发条件**: 默认情况,当不满足其他特定条件时
- **适用场景**: 其他应用界面手势操作
### 4.3 AssistantInputConsumer
- **触发条件**:
- 设备支持助手手势
- 事件满足助手触发条件(距离、角度等)
- 不是手势阻塞任务
- **适用场景**: 助手手势识别和处理
### 4.4 DeviceLockedInputConsumer
- **触发条件**:
- 设备未解锁
- 锁屏被遮挡显示
- **适用场景**: 设备锁定状态下的输入处理
### 4.5 AccessibilityInputConsumer
- **触发条件**: 无障碍菜单可用
- **适用场景**: 无障碍功能输入处理
### 4.6 ScreenPinnedInputConsumer
- **触发条件**: 屏幕固定模式激活
- **适用场景**: 屏幕固定状态下的输入限制
### 4.7 ProgressDelegateInputConsumer
- **触发条件**: 存在进度代理
- **适用场景**: 进度委托输入处理
### 4.8 SysUiOverlayInputConsumer
- **触发条件**: 系统UI对话框正在显示
- **适用场景**: 系统UI覆盖层输入处理
### 4.9 OneHandedModeInputConsumer
- **触发条件**: 事件可触发单手模式
- **适用场景**: 单手模式手势处理
### 4.10 TaskbarUnstashInputConsumer
- **触发条件**:
- 任务栏存在
- 不在手机模式
- 任务栏不在隐藏状态
- 可启动系统手势
- **适用场景**: 任务栏显示/隐藏操作
### 4.11 TrackpadStatusBarInputConsumer
- **触发条件**:
- 触控板手势
- 可启动系统手势
- 最近动画未运行
- **适用场景**: 触控板状态栏操作
### 4.12 BubbleBarInputConsumer
- **触发条件**: 事件在气泡上
- **适用场景**: 气泡栏输入处理
### 4.13 NavHandleLongPressInputConsumer
- **触发条件**:
- 可启动系统手势
- 最近动画未运行
- 导航手柄可长按
- 不是触控板三指手势
- **适用场景**: 导航手柄长按操作
## 5. 触发条件优先级总结
### 5.1 最高优先级(立即返回)
1. **气泡栏检查** - 事件在气泡上 → **BubbleBarInputConsumer**
2. **进度委托检查** - 存在进度代理 → **ProgressDelegateInputConsumer**
3. **设备锁定检查** - 设备未解锁 → **DeviceLockedInputConsumer**
### 5.2 基础消费者选择
4. **系统手势能力检查** - 决定使用基础消费者还是默认消费者
- 可启动系统手势或最近动画运行 → **newBaseConsumer()** (LauncherInputConsumer/OtherActivityInputConsumer)
- 不可启动系统手势 → **getDefaultInputConsumer()** (默认无操作消费者)
### 5.3 分层包装(按顺序)
5. **助手手势检查** - 可触发助手动作 → **AssistantInputConsumer**
6. **任务栏检查** - 任务栏相关操作 → **TaskbarUnstashInputConsumer**
7. **导航手柄检查** - 长按操作 → **NavHandleLongPressInputConsumer**
8. **系统UI检查** - 对话框显示 → **SysUiOverlayInputConsumer**
9. **触控板检查** - 状态栏操作 → **TrackpadStatusBarInputConsumer**
10. **屏幕固定检查** - 固定模式 → **ScreenPinnedInputConsumer**
11. **单手模式检查** - 单手操作 → **OneHandedModeInputConsumer**
12. **无障碍检查** - 无障碍功能 → **AccessibilityInputConsumer**
### 5.4 默认情况
13. **OtherActivityInputConsumer** - 其他应用手势处理 → **OtherActivityInputConsumer**
## 6. 条件检查的性能优化
### 6.1 短路评估
条件检查采用短路评估,一旦满足条件立即返回,避免不必要的检查。
### 6.2 成本排序
条件按照检查成本从低到高排序,低成本检查在前。
### 6.3 缓存利用
充分利用设备状态缓存,避免重复的状态检查。
## 7. 扩展性考虑
### 7.1 新InputConsumer添加
添加新的InputConsumer时,需要:
1. 在合适的位置插入条件检查
2. 确定正确的优先级
3. 提供清晰的触发条件文档
### 7.2 条件配置化
支持通过配置动态调整条件检查顺序和阈值。
## 8. InputConsumer在手机上的触发区域分析
### 8.1 基础手势区域定义
#### 8.1.1 滑动手势区域 (Swipe Up Touch Region)
**区域定义**: 屏幕底部特定高度的区域,用于检测上滑手势
**实现代码**:
```java
// RotationTouchHelper.java - 滑动手势区域检查
public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex) {
if (isTrackpadScroll(event)) {
return false; // 触控板滚动不处理
}
if (isTrackpadMultiFingerSwipe(event)) {
return true; // 触控板多指滑动
}
// 检查触摸点是否在有效滑动区域内
return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(pointerIndex),
event.getY(pointerIndex));
}
// OrientationTouchTransformer.java - 滑动区域定义
boolean touchInValidSwipeRegions(float x, float y) {
if (mLastRectTouched != null) {
return mLastRectTouched.contains(x, y);
}
return false;
}
```
**区域位置**:
- **竖屏模式 (ROTATION_0)**: 屏幕底部 `mNavBarGesturalHeight` 像素高度区域
- **横屏模式 (ROTATION_90/270)**: 屏幕底部 `mNavBarGesturalHeight` 像素高度区域
- **倒屏模式 (ROTATION_180)**: 屏幕顶部 `mNavBarGesturalHeight` 像素高度区域
**默认高度**: 约 100-150dp(根据设备密度调整)
#### 8.1.2 助手手势区域 (Assistant Gesture Region)
**区域定义**: 屏幕左右两侧特定宽度的区域,用于检测助手手势
**实现代码**:
```java
// OrientationTouchTransformer.java - 助手区域定义
boolean touchInAssistantRegion(MotionEvent ev) {
return mAssistantLeftRegion.contains(ev.getX(), ev.getY())
|| mAssistantRightRegion.contains(ev.getX(), ev.getY());
}
private void updateAssistantRegions(OrientationRectF orientationRectF) {
// 助手区域宽度约为屏幕宽度的1/6
float assistantWidth = orientationRectF.width() / 6f;
// 左侧助手区域
mAssistantLeftRegion.left = 0;
mAssistantLeftRegion.right = assistantWidth;
// 右侧助手区域
mAssistantRightRegion.right = orientationRectF.right;
mAssistantRightRegion.left = orientationRectF.right - assistantWidth;
}
```
**区域位置**:
- **左侧助手区域**: 屏幕左侧 1/6 宽度区域
- **右侧助手区域**: 屏幕右侧 1/6 宽度区域
- **高度**: 与滑动手势区域相同
#### 8.1.3 单手模式区域 (One-Handed Mode Region)
**区域定义**: 仅在竖屏模式下有效的底部扩展区域
**实现代码**:
```java
// OrientationTouchTransformer.java - 单手模式区域定义
boolean touchInOneHandedModeRegion(MotionEvent ev) {
return mOneHandedModeRegion.contains(ev.getX(), ev.getY());
}
private void updateOneHandedRegions(OrientationRectF orientationRectF) {
// 单手模式仅在竖屏模式下有效
mOneHandedModeRegion.set(0, orientationRectF.bottom - mNavBarLargerGesturalHeight,
orientationRectF.right, orientationRectF.bottom);
}
```
**区域位置**:
- **仅竖屏模式有效**
- **高度**: `mNavBarLargerGesturalHeight`(比普通手势区域更高)
- **默认高度**: 约 200-250dp
### 8.2 各InputConsumer的具体触发区域
#### 8.2.1 LauncherInputConsumer
**触发区域**: 整个屏幕区域(当应用在Launcher界面时)
**区域类型**: 全局区域
**特殊条件**: 仅在Launcher活动时有效
#### 8.2.2 OtherActivityInputConsumer
**触发区域**: 基础滑动手势区域
**区域位置**: 屏幕底部 `mNavBarGesturalHeight` 高度区域
**触发条件**: 在基础手势区域内的滑动操作
#### 8.2.3 AssistantInputConsumer
**触发区域**: 助手手势区域 + 全屏助手手势
**区域位置**:
- **三键导航模式**: 屏幕左右两侧助手区域
- **全手势导航模式**: 全屏区域(支持从任意位置触发)
- **触控板多指滑动**: 全屏区域
**触发条件**:
- 在助手区域内的滑动
- 全手势导航模式下的对角线滑动
- 触控板三指滑动
#### 8.2.4 DeviceLockedInputConsumer
**触发区域**: 基础滑动手势区域
**区域位置**: 屏幕底部 `mNavBarGesturalHeight` 高度区域
**特殊条件**: 设备锁定状态下,仅在基础手势区域有效
#### 8.2.5 AccessibilityInputConsumer
**触发区域**: 基础滑动手势区域
**区域位置**: 屏幕底部 `mNavBarGesturalHeight` 高度区域
**特殊条件**: 无障碍功能启用时,支持在基础区域内的特殊手势
#### 8.2.6 ScreenPinnedInputConsumer
**触发区域**: 无特定区域限制
**区域类型**: 全局区域
**特殊条件**: 屏幕固定模式下,限制所有输入操作
#### 8.2.7 ProgressDelegateInputConsumer
**触发区域**: 基础滑动手势区域
**区域位置**: 屏幕底部 `mNavBarGesturalHeight` 高度区域
**特殊条件**: 存在进度代理时,优先处理进度相关手势
#### 8.2.8 SysUiOverlayInputConsumer
**触发区域**: 系统UI覆盖层区域
**区域位置**: 系统对话框和覆盖层显示区域
**特殊条件**: 系统UI显示时,覆盖应用界面输入
#### 8.2.9 OneHandedModeInputConsumer
**触发区域**: 单手模式区域
**区域位置**: 屏幕底部 `mNavBarLargerGesturalHeight` 高度区域
**特殊条件**: 仅在竖屏模式下有效,支持向下滑动触发
#### 8.2.10 TaskbarUnstashInputConsumer
**触发区域**: 任务栏显示区域
**区域位置**: 屏幕底部任务栏位置
**特殊条件**: 任务栏存在且不在隐藏状态时有效
#### 8.2.11 TrackpadStatusBarInputConsumer
**触发区域**: 状态栏区域 + 触控板手势
**区域位置**: 屏幕顶部状态栏区域
**特殊条件**: 触控板手势操作状态栏
#### 8.2.12 BubbleBarInputConsumer
**触发区域**: 气泡栏显示区域
**区域位置**: 气泡栏具体位置(浮动窗口)
**特殊条件**: 事件在气泡上时立即触发
#### 8.2.13 NavHandleLongPressInputConsumer
**触发区域**: 导航手柄区域
**区域位置**: 屏幕底部导航条中心区域
**特殊条件**: 长按导航手柄触发
### 8.3 区域优先级和重叠处理
#### 8.3.1 区域重叠优先级
1. **气泡栏区域** (最高优先级) - 立即返回
2. **助手区域** - 高优先级处理
3. **单手模式区域** - 中等优先级
4. **基础手势区域** - 默认优先级
5. **全局区域** - 最低优先级
#### 8.3.2 区域冲突解决
当多个区域重叠时,系统按照以下规则处理:
- **气泡栏优先**: 事件在气泡上时,忽略其他区域
- **助手区域优先**: 在助手区域内的事件优先于基础手势区域
- **单手模式限制**: 仅在竖屏模式下有效
- **导航模式影响**: 不同导航模式下的区域定义不同
### 8.4 设备适配和动态调整
#### 8.4.1 设备尺寸适配
```java
// 根据设备尺寸动态调整区域大小
private int calculateDefaultGesturalHeight() {
return getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
}
private int getNavbarSize(String resName) {
return ResourceUtils.getNavbarSize(resName, mResources);
}
```
#### 8.4.2 屏幕方向适配
系统根据当前屏幕方向动态调整触摸区域:
- **竖屏**: 底部手势区域 + 左右助手区域
- **横屏**: 底部手势区域(适配横屏布局)
- **倒屏**: 顶部手势区域
#### 8.4.3 导航模式适配
- **三键导航**: 仅支持助手区域手势
- **手势导航**: 支持全屏手势区域
- **二键导航**: 特定区域限制
### 8.5 可视化区域分布
```
┌─────────────────────────────────────────┐
│ 状态栏区域 │ ← TrackpadStatusBarInputConsumer
├─────────────────────────────────────────┤
│ │
│ 应用内容区域 │
│ │
│ │
├───助手区域───┼─────────────┼───助手区域───┤
│ │ 基础手势 │ │ ← AssistantInputConsumer
│ 左侧助手 │ 区域 │ 右侧助手 │
│ │ │ │
├──────────────┴─────────────┴──────────────┤
│ 单手模式扩展区域 │ ← OneHandedModeInputConsumer (竖屏)
│ │
│ 基础手势区域 │ ← OtherActivityInputConsumer等
│ (mNavBarGesturalHeight) │
└─────────────────────────────────────────┘
浮动区域:
┌───┐
│ ● │ ← BubbleBarInputConsumer (气泡栏)
└───┘
```
### 8.6 性能优化考虑
#### 8.6.1 区域检查优化
- **短路评估**: 高概率区域在前检查
- **区域缓存**: 避免重复计算区域边界
- **分层检查**: 按优先级顺序检查区域
#### 8.6.2 内存优化
- **区域复用**: 相同方向的区域复用
- **动态创建**: 只在需要时创建区域对象
- **及时清理**: 手势结束后清理区域缓存
## 9. 总结
InputConsumer的触发条件设计体现了Android系统对复杂输入场景的精细处理能力。通过分层条件检查和优先级排序,系统能够高效地选择最适合的输入处理策略,为用户提供流畅的交互体验。
**区域设计的关键优势**:
1. **精确的区域划分**: 不同功能对应不同的触发区域
2. **动态的区域适配**: 根据设备状态和屏幕方向动态调整
3. **高效的冲突解决**: 明确的优先级规则处理区域重叠
4. **良好的扩展性**: 支持新增区域类型和功能
这种区域化的输入处理机制确保了Android系统在各种复杂场景下都能提供一致且流畅的用户体验。
@@ -0,0 +1,964 @@
# Launcher3拖拽系统架构分析报告
## 概述
本报告基于对AOSP Launcher3源码的深入分析,全面解析了Launcher3中的拖拽系统架构,包括拖拽控制器、拖拽驱动、拖拽目标等核心组件的设计原理和实现机制。
**源码位置**:
- DragController: `src/com/android/launcher3/dragndrop/DragController.java`
- DragDriver: `src/com/android/launcher3/dragndrop/DragDriver.java`
- DragView: `src/com/android/launcher3/dragndrop/DragView.java`
- DropTarget: `src/com/android/launcher3/DropTarget.java`
- DragSource: `src/com/android/launcher3/DragSource.java`
- Workspace: `src/com/android/launcher3/Workspace.java`
## 拖拽系统架构分析
### 核心组件架构
Launcher3的拖拽系统采用分层设计,包含控制器层、驱动层、视图层和接口层,实现了高度模块化和可扩展的拖拽功能。
#### 1. DragController - 拖拽控制器
DragController是拖拽系统的核心控制器,负责管理整个拖拽生命周期:
- 管理拖拽开始、移动、结束的完整流程
- 维护DropTarget列表和DragListener列表
- 处理拖拽状态转换和事件分发
```java
public abstract class DragController<T extends ActivityContext>
implements DragDriver.EventListener, TouchController {
protected DragDriver mDragDriver = null; // 拖拽驱动器
public DropTarget.DragObject mDragObject; // 拖拽对象
private final ArrayList<DropTarget> mDropTargets = new ArrayList<>();
private final ArrayList<DragListener> mListeners = new ArrayList<>();
public DragView startDrag(Drawable drawable, DraggableView originalView, ...) {
// 启动拖拽的核心方法
}
}
```
#### 2. DragDriver - 拖拽驱动器
DragDriver负责处理具体的触摸和拖拽事件,支持两种实现:
- **InternalDragDriver**: 处理Launcher内部的拖拽事件
- **SystemDragDriver**: 处理系统级的跨应用拖拽事件
```java
public abstract class DragDriver {
protected final EventListener mEventListener;
public interface EventListener {
void onDriverDragMove(float x, float y);
void onDriverDragEnd(float x, float y);
void onDriverDragCancel();
}
}
```
#### 3. DragView - 拖拽视图
DragView是拖拽过程中显示的视图组件,负责:
- 显示拖拽项的视觉反馈
- 实现拖拽动画效果
- 管理拖拽过程中的缩放和位置变化
```java
public abstract class DragView<T extends Context & ActivityContext> extends FrameLayout {
private final View mContent; // 拖拽内容视图
private final int mRegistrationX; // 注册点X坐标
private final int mRegistrationY; // 注册点Y坐标
private final float mInitialScale; // 初始缩放比例
public void onDragStart() {
// 拖拽开始时的动画
}
}
```
### 核心接口设计
#### 1. DropTarget接口 - 拖拽目标
DropTarget定义了拖拽目标对象的行为规范:
```java
public interface DropTarget {
boolean isDropEnabled(); // 是否允许拖拽释放
void onDrop(DragObject dragObject, DragOptions options); // 拖拽结束放开图标
void onDragEnter(DragObject dragObject); // 拖拽进入目标视图
void onDragOver(DragObject dragObject); // 拖拽在目标视图中经过
void onDragExit(DragObject dragObject); // 拖拽退出目标视图
boolean acceptDrop(DragObject dragObject); // 目标视图是否可以接受放置
}
```
#### 2. DragSource接口 - 拖拽源
DragSource定义了拖拽源对象的行为规范:
```java
public interface DragSource {
void onDropCompleted(View target, DragObject d, boolean success);
}
```
#### 3. DragListener接口 - 拖拽监听器
DragListener定义了拖拽开始和结束的监听接口:
```java
public interface DragListener {
void onDragStart(DropTarget.DragObject dragObject, DragOptions options);
void onDragEnd();
}
```
### DragObject数据结构
DragObject包含了拖拽过程中的所有数据:
```java
class DragObject {
public int x = -1; // 当前触摸位置X
public int y = -1; // 当前触摸位置Y
public int xOffset = -1; // X偏移量
public int yOffset = -1; // Y偏移量
public boolean dragComplete = false; // 拖拽是否完成
public DragView dragView = null; // 拖拽视图
public ItemInfo dragInfo = null; // 拖拽项信息
public DragSource dragSource = null; // 拖拽来源
public boolean cancelled = false; // 是否取消
public DraggableView originalView = null; // 原始视图
}
```
## 类图分析
### 1. 核心接口类图
#### DragSource接口类图
```mermaid
classDiagram
direction BT
class DragSource {
<<interface>>
+onDropCompleted(View, DragObject, boolean) void
}
class Folder {
+onDropCompleted(View, DragObject, boolean) void
+removeItem(ItemInfo) void
+notifyDataSetChanged() void
}
class Workspace {
+onDropCompleted(View, DragObject, boolean) void
+removeItem(ItemInfo) void
+addItemToCellLayout(ItemInfo, int, int) void
}
class AllAppsContainerView {
+onDropCompleted(View, DragObject, boolean) void
+removeAppInfo(ItemInfo) void
}
Folder ..|> DragSource
Workspace ..|> DragSource
AllAppsContainerView ..|> DragSource
```
#### DragObject数据结构类图
```mermaid
classDiagram
direction TB
class DropTarget {
<<interface>>
+isDropEnabled() boolean
+onDrop(DragObject, DragOptions) void
+onDragEnter(DragObject) void
+onDragOver(DragObject) void
+onDragExit(DragObject) void
+acceptDrop(DragObject) boolean
}
class DropTarget.DragObject {
+x : int
+y : int
+xOffset : int
+yOffset : int
+dragComplete : boolean
+dragView : DragView
+dragInfo : ItemInfo
+originalDragInfo : ItemInfo
+dragSource : DragSource
+cancelled : boolean
+originalView : DraggableView
+logInstanceId : InstanceId
+getVisualCenter(float[]) float[]
}
class DragView {
+onDragStart() void
+animateTo(int, int, Runnable) void
+getDragRegion() Rect
}
class ItemInfo {
+itemType : int
+container : int
+screenId : int
+cellX : int
+cellY : int
+spanX : int
+spanY : int
}
class DragSource {
<<interface>>
+onDropCompleted(View, DragObject, boolean) void
}
class DraggableView {
<<interface>>
+getView() View
+getVisualDragBounds() Rect
}
DropTarget.DragObject --> DragView
DropTarget.DragObject --> ItemInfo
DropTarget.DragObject --> DragSource
DropTarget.DragObject --> DraggableView
DropTarget --> DropTarget.DragObject : 包含
```
#### DropTarget接口类图
```mermaid
classDiagram
direction BT
class DropTarget {
<<interface>>
+isDropEnabled() boolean
+onDrop(DragObject, DragOptions) void
+onDragEnter(DragObject) void
+onDragOver(DragObject) void
+onDragExit(DragObject) void
+acceptDrop(DragObject) boolean
+getHitRectRelativeToDragLayer(Rect) void
+getDropView() View
}
class Workspace {
+isDropEnabled() boolean
+onDrop(DragObject, DragOptions) void
+onDragEnter(DragObject) void
+onDragOver(DragObject) void
+onDragExit(DragObject) void
+acceptDrop(DragObject) boolean
+findDropCell(int, int, int[]) boolean
+onDropExternal(int[], CellLayout, DragObject) void
}
class FolderIcon {
+isDropEnabled() boolean
+onDrop(DragObject, DragOptions) void
+onDragEnter(DragObject) void
+onDragOver(DragObject) void
+onDragExit(DragObject) void
+acceptDrop(DragObject) boolean
+onDrop(DragObject, boolean) void
+isFull() boolean
}
class ButtonDropTarget {
+isDropEnabled() boolean
+onDrop(DragObject, DragOptions) void
+onDragEnter(DragObject) void
+onDragOver(DragObject) void
+onDragExit(DragObject) void
+acceptDrop(DragObject) boolean
+completeDrop(DragObject) void
}
Workspace ..|> DropTarget
FolderIcon ..|> DropTarget
ButtonDropTarget ..|> DropTarget
```
### 2. 完整系统类图
```mermaid
classDiagram
class DragController {
+startDrag() DragView
+cancelDrag()
+isDragging() boolean
+addDropTarget(DropTarget)
+addDragListener(DragListener)
}
class DragDriver {
+onTouchEvent(MotionEvent) boolean
+onDragEvent(DragEvent) boolean
+EventListener mEventListener
}
class DragView {
+onDragStart()
+animateTo(int, int, Runnable)
}
class DropTarget {
<<interface>>
+isDropEnabled() boolean
+onDrop(DragObject, DragOptions)
+onDragEnter(DragObject)
+onDragOver(DragObject)
+onDragExit(DragObject)
+acceptDrop(DragObject) boolean
}
class DragSource {
<<interface>>
+onDropCompleted(View, DragObject, boolean)
}
class DragListener {
<<interface>>
+onDragStart(DragObject, DragOptions)
+onDragEnd()
}
class Workspace {
+onDrop(DragObject, DragOptions)
+acceptDrop(DragObject) boolean
+onDropCompleted(View, DragObject, boolean)
}
class FolderIcon {
+onDrop(DragObject, DragOptions)
+acceptDrop(DragObject) boolean
}
class Folder {
+onDropCompleted(View, DragObject, boolean)
+removeItem(ItemInfo) void
}
DragController --> DragDriver
DragController --> DragView
DragController --> DropTarget
DragController --> DragListener
DragDriver --> DragController : EventListener
Workspace ..|> DropTarget
Workspace ..|> DragSource
FolderIcon ..|> DropTarget
Folder ..|> DragSource
```
## 架构图
### 1. 核心架构层次图
```mermaid
graph TB
subgraph "事件输入层"
A[用户触摸事件]
B[系统拖拽事件]
end
subgraph "控制器层"
C[DragController<br/>拖拽控制器]
D[LauncherDragController<br/>主控制器实现]
E[SecondaryDragController<br/>副屏控制器]
end
subgraph "驱动层"
F[DragDriver<br/>拖拽驱动器]
G[InternalDragDriver<br/>内部驱动]
H[SystemDragDriver<br/>系统驱动]
end
subgraph "视图层"
I[DragLayer<br/>拖拽层容器]
J[DragView<br/>拖拽视图]
K[BaseDragLayer<br/>基础拖拽层]
end
subgraph "数据层"
L[DropTarget.DragObject<br/>拖拽数据对象]
M[ItemInfo<br/>项目信息]
end
subgraph "目标管理层"
N[DropTarget列表管理]
O[DragListener列表管理]
end
subgraph "放置目标层"
P[Workspace<br/>工作区]
Q[FolderIcon<br/>文件夹图标]
R[ButtonDropTarget<br/>按钮目标]
S[Folder<br/>文件夹]
end
A --> F
B --> F
C --> D
C --> E
D --> F
F --> G
F --> H
G --> I
H --> I
I --> J
J --> L
L --> M
C --> N
C --> O
N --> P
N --> Q
N --> R
P --> S
style A fill:#99f5fe
style B fill:#99f5fe
style C fill:#3f51b5,color:#ffffff
style D fill:#3f51b5,color:#ffffff
style E fill:#3f51b5,color:#ffffff
style F fill:#4caf50,color:#ffffff
style G fill:#4caf50,color:#ffffff
style H fill:#4caf50,color:#ffffff
style I fill:#ff9800,color:#ffffff
style J fill:#ff9800,color:#ffffff
style K fill:#ff9800,color:#ffffff
style L fill:#9c27b0,color:#ffffff
style M fill:#9c27b0,color:#ffffff
style N fill:#009688,color:#ffffff
style O fill:#009688,color:#ffffff
style P fill:#795548,color:#ffffff
style Q fill:#795548,color:#ffffff
style R fill:#795548,color:#ffffff
style S fill:#795548,color:#ffffff
```
### 2. 详细组件关系图
```mermaid
graph LR
subgraph "ActivityContext"
A[Launcher]
B[SecondaryDisplayLauncher]
end
subgraph "DragController实现"
C[LauncherDragController]
D[SecondaryDragController]
end
subgraph "DragDriver实现"
E[InternalDragDriver<br/>处理触摸事件]
F[SystemDragDriver<br/>处理系统拖拽]
end
subgraph "DragLayer实现"
G[DragLayer<br/>主拖拽层]
H[SecondaryDragLayer<br/>副屏拖拽层]
I[TaskbarDragLayer<br/>任务栏拖拽层]
end
subgraph "DropTarget实现"
J[Workspace]
K[FolderIcon]
L[ButtonDropTarget]
M[Folder]
N[DeleteDropTarget]
O[UninstallDropTarget]
end
subgraph "DragSource实现"
P[Workspace]
Q[Folder]
R[AllAppsContainerView]
end
A --> C
B --> D
C --> E
C --> F
C --> G
D --> H
E --> G
F --> G
G --> J
G --> K
G --> L
J --> M
P --> C
Q --> C
R --> C
style A fill:#2196f3,color:#ffffff
style B fill:#2196f3,color:#ffffff
style C fill:#3f51b5,color:#ffffff
style D fill:#3f51b5,color:#ffffff
style E fill:#4caf50,color:#ffffff
style F fill:#4caf50,color:#ffffff
style G fill:#ff9800,color:#ffffff
style H fill:#ff9800,color:#ffffff
style I fill:#ff9800,color:#ffffff
style J fill:#795548,color:#ffffff
style K fill:#795548,color:#ffffff
style L fill:#795548,color:#ffffff
style M fill:#795548,color:#ffffff
style N fill:#795548,color:#ffffff
style O fill:#795548,color:#ffffff
style P fill:#9c27b0,color:#ffffff
style Q fill:#9c27b0,color:#ffffff
style R fill:#9c27b0,color:#ffffff
```
## 拖拽流程图
### 1. 通用拖拽流程图(含代码细节)
```mermaid
flowchart TD
A[ACTION_DOWN 长按开始<br/>MotionEvent.ACTION_DOWN] --> B["创建DragView<br/>DragController.startDrag()"]
B --> C["调用DragListener.onDragStart<br/>for (DragListener listener : mListeners)"]
C --> D[进入预拖拽状态<br/>mIsInPreDrag = true]
D --> E{移动距离 > 阈值?<br/>distance > DEEP_PRESS_DISTANCE_FACTOR}
E -->|否| F[继续预拖拽<br/>mIsInPreDrag = true]
E -->|是| G["开始正式拖拽<br/>callOnDragStart()"]
G --> H["ACTION_MOVE 拖拽移动<br/>DragDriver.onTouchEvent()"]
H --> I[更新DragView位置<br/>mDragObject.x = x, mDragObject.y = y]
I --> J["遍历DropTarget列表<br/>for (DropTarget target : mDropTargets)"]
J --> K{"当前DropTarget.acceptDrop?<br/>target.acceptDrop(mDragObject)"}
K -->|是| L["调用onDragOver<br/>target.onDragOver(mDragObject)"]
K -->|否| M[检查下一个DropTarget<br/>continue to next target]
L --> N{进入新DropTarget?<br/>target != mLastDropTarget}
N -->|是| O["调用onDragEnter<br/>target.onDragEnter(mDragObject)"]
N -->|否| P{离开原DropTarget?<br/>mLastDropTarget != null && target != mLastDropTarget}
P -->|是| Q["调用onDragExit<br/>mLastDropTarget.onDragExit(mDragObject)"]
P -->|否| H
H --> R{ACTION_UP 拖拽结束?<br/>MotionEvent.ACTION_UP}
R -->|是| S["调用当前DropTarget.onDrop<br/>target.onDrop(mDragObject, mOptions)"]
S --> T["调用DragSource.onDropCompleted<br/>mDragObject.dragSource.onDropCompleted()"]
T --> U["调用DragListener.onDragEnd<br/>for (DragListener listener : mListeners)"]
U --> V["清理DragView<br/>mDragObject.dragView.cleanup()"]
V --> W["拖拽完成<br/>endDrag()"]
R -->|否| H
%% 异常处理分支
H --> X{ACTION_CANCEL?<br/>MotionEvent.ACTION_CANCEL}
X -->|是| Y["取消拖拽<br/>cancelDrag()"]
Y --> Z["调用onDragExit<br/>mLastDropTarget.onDragExit(mDragObject)"]
Z --> AA[设置取消标志<br/>mDragObject.cancelled = true]
AA --> AB["调用DragListener.onDragEnd<br/>for (DragListener listener : mListeners)"]
AB --> AC["清理DragView<br/>mDragObject.dragView.cleanup()"]
AC --> AD[拖拽取消完成]
```
### 2. 从Folder拖拽图标到Workspace的完整流程图(含事件细节)
```mermaid
flowchart TD
subgraph "事件触发阶段"
A["用户长按Folder中的图标<br/>onLongClick事件"] --> B["Folder.onLongClick触发<br/>View.OnLongClickListener"]
B --> C["DragController.startDrag被调用<br/>ItemLongClickListener.onLongClick()"]
end
subgraph "拖拽初始化阶段"
C --> D["创建DragView并设置dragSource为Folder<br/>LauncherDragController.startDrag()"]
D --> E["调用Folder.onDragStart<br/>DragListener.onDragStart()"]
E --> F["进入拖拽状态<br/>mIsInPreDrag = false"]
end
subgraph "拖拽移动阶段"
F --> G["用户拖动图标<br/>ACTION_MOVE事件循环"]
G --> H["DragDriver处理ACTION_MOVE事件<br/>InternalDragDriver.onTouchEvent()"]
H --> I["更新DragView位置<br/>mDragObject.x/y更新"]
I --> J["遍历DropTarget列表<br/>for (DropTarget target : mDropTargets)"]
J --> K{"当前在FolderIcon上?<br/>target instanceof FolderIcon"}
K -->|是| L["FolderIcon.onDragOver<br/>FolderIcon.onDragOver()<br/>作用: 检查是否可以接受拖拽项<br/>实现: acceptDrop()检查文件夹是否已满"]
L --> M{"离开FolderIcon?<br/>!isOverFolderIcon()"}
M -->|是| N["FolderIcon.onDragExit<br/>FolderIcon.onDragExit()<br/>作用: 清理拖拽进入状态<br/>实现: 重置文件夹高亮状态"]
K -->|否| O{"当前在Workspace上?<br/>target instanceof Workspace"}
O -->|是| P["Workspace.onDragEnter<br/>Workspace.onDragEnter()<br/>作用: 进入工作区拖拽区域<br/>实现: 初始化放置目标状态"]
P --> Q["Workspace.onDragOver<br/>Workspace.onDragOver()<br/>作用: 在工作区上移动<br/>实现: 计算目标单元格并高亮显示"]
Q --> R{"找到有效放置位置?<br/>Workspace.findDropCell()<br/>作用: 检查是否可以放置<br/>实现: 验证单元格是否可用"}
R -->|是| S["显示放置预览<br/>显示目标单元格高亮"]
R -->|否| T["显示无效放置提示<br/>显示禁止放置图标"]
S --> U{"用户释放图标?<br/>等待ACTION_UP事件"}
T --> U
end
subgraph "拖拽释放阶段"
U -->|是| V["Workspace.onDrop被调用<br/>ACTION_UP事件触发<br/>作用: DropTarget接口的核心方法<br/>实现: 处理图标放置到工作区的逻辑"]
V --> W["Workspace.findDropCell计算目标位置<br/>计算目标单元格坐标<br/>作用: 确定放置的目标单元格<br/>实现: 根据坐标计算cellX/cellY"]
W --> X["Workspace.onDropExternal处理外部放置<br/>处理从Folder到Workspace的放置<br/>作用: 处理从外部拖拽源的放置<br/>实现: 调用外部放置处理逻辑"]
X --> Y["从Folder中移除图标<br/>Folder.removeItem()<br/>作用: 从文件夹中删除图标<br/>实现: 更新FolderInfo数据模型"]
Y --> Z["在Workspace目标位置添加图标<br/>Workspace.addInScreen()<br/>作用: 在工作区添加新图标<br/>实现: 创建新的ItemInfo并添加到CellLayout"]
Z --> AA["Folder.onDropCompleted被调用<br/>DragSource.onDropCompleted()<br/>作用: 通知Folder拖拽完成<br/>实现: 更新文件夹数据并刷新UI"]
AA --> AB["Folder.removeItem移除数据<br/>从Folder数据模型中删除<br/>作用: 清理文件夹数据<br/>实现: 从contents列表中删除ItemInfo"]
AB --> AC["Folder.notifyDataSetChanged更新UI<br/>刷新Folder显示<br/>作用: 更新文件夹界面<br/>实现: 调用Adapter.notifyDataSetChanged()"]
AC --> AD["Workspace.onDropCompleted被调用<br/>DragSource.onDropCompleted()<br/>作用: 通知Workspace拖拽完成<br/>实现: 更新工作区数据模型"]
AD --> AE["Workspace.addItemToCellLayout添加数据<br/>更新Workspace数据模型<br/>作用: 将图标添加到工作区<br/>实现: 绑定ItemInfo到CellLayout"]
AE --> AF["调用DragListener.onDragEnd<br/>for (DragListener listener : mListeners)"]
AF --> AG["清理DragView动画<br/>DragView.cleanup()"]
AG --> AH["拖拽完成<br/>endDrag()"]
end
O -->|否| AI{"在其他DropTarget上?<br/>其他DropTarget处理"}
AI -->|是| AJ["相应DropTarget的处理<br/>如DeleteDropTarget等"]
%% 异常处理分支
G --> AK{"拖拽取消?<br/>ACTION_CANCEL事件"}
AK -->|是| AL["取消拖拽流程<br/>cancelDrag()"]
AL --> AM["恢复图标到Folder<br/>不进行数据变更"]
AM --> AN["清理DragView<br/>DragView.cleanup()"]
AN --> AO["拖拽取消完成"]
```
## 时序图
```mermaid
sequenceDiagram
participant U as 用户
participant DC as DragController
participant DD as DragDriver
participant DV as DragView
participant DT as DropTarget
participant DS as DragSource
participant DL as DragListener
U->>DC: ACTION_DOWN (长按)
DC->>DV: 创建DragView
DC->>DL: onDragStart()
loop 拖拽移动
U->>DD: ACTION_MOVE
DD->>DC: onDriverDragMove(x,y)
DC->>DV: 更新位置
DC->>DT: 遍历DropTarget列表
DT->>DT: acceptDrop()检查
DT->>DT: onDragOver()更新状态
end
U->>DD: ACTION_UP (释放)
DD->>DC: onDriverDragEnd(x,y)
DC->>DT: onDrop()
DT->>DS: 处理放置逻辑
DS->>DS: onDropCompleted()
DC->>DL: onDragEnd()
DC->>DV: 清理动画
```
## DropTarget实现类分析
### 1. Workspace - 桌面工作区
Workspace是主要的DropTarget实现,负责:
- 图标重排和位置交换
- 文件夹创建和管理
- 支持外部拖拽(从All Apps拖入)
关键方法:
```java
@Override
public boolean acceptDrop(DragObject d) {
// 检查是否在有效的放置目标上
CellLayout dropTargetLayout = mDropToLayout;
if (d.dragSource != this) {
// 外部拖拽的特殊处理
if (dropTargetLayout == null) return false;
}
return true;
}
@Override
public void onDrop(final DragObject d, DragOptions options) {
// 处理放置逻辑
mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
// 具体的放置实现...
}
```
### 2. FolderIcon - 文件夹图标
FolderIcon实现DropTarget接口,支持:
- 图标拖入文件夹
- 图标从文件夹拖出
- 文件夹内图标重排
### 3. ButtonDropTarget - 按钮式放置目标
ButtonDropTarget是抽象基类,具体实现包括:
- **DeleteDropTarget**: 删除操作
- **UninstallDropTarget**: 卸载操作
- **InfoDropTarget**: 应用信息
## 设计模式应用
### 1. 观察者模式
DragController管理多个DragListener,实现拖拽事件的广播通知。
### 2. 策略模式
不同的DragDriver实现(InternalDragDriver、SystemDragDriver)提供不同的拖拽策略。
### 3. 适配器模式
DropTarget接口适配不同的视图组件(Workspace、FolderIcon等)。
### 4. 工厂模式
DragDriver.create()方法根据条件创建不同的驱动实例。
## DropTarget和DragSource接口方法代码分析
### 1. Workspace中DropTarget接口方法实现分析
#### acceptDrop方法 - 检查是否接受拖拽项
```java
@Override
public boolean acceptDrop(DragObject d) {
// 如果是外部拖拽(如从All Apps),检查是否应该接受
CellLayout dropTargetLayout = mDropToLayout;
if (d.dragSource != this) {
// 如果在放置时没有在有效的放置目标上,不接受放置
if (dropTargetLayout == null) {
return false;
}
if (!transitionStateShouldAllowDrop()) return false;
// 如果无法处理payload,拒绝系统级放置
// ... 具体检查逻辑
}
return true;
}
```
**代码分析**:
- 区分内部拖拽和外部拖拽,外部拖拽需要额外验证
- 检查当前是否有有效的放置目标布局(mDropToLayout)
- 验证过渡状态是否允许放置(transitionStateShouldAllowDrop)
- 对系统级拖拽进行payload兼容性检查
#### onDrop方法 - 处理放置操作
```java
@Override
public void onDrop(final DragObject d, DragOptions options) {
mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
CellLayout dropTargetLayout = mDropToLayout;
// 将点映射到拖拽目标
if (dropTargetLayout != null) {
mapPointFromDropLayout(dropTargetLayout, mDragViewVisualCenter);
}
if (d.dragSource != this || mDragInfo == null) {
// 外部放置处理
final int[] touchXY = new int[]{(int) mDragViewVisualCenter[0],
(int) mDragViewVisualCenter[1]};
onDropExternal(touchXY, dropTargetLayout, d);
} else {
// 内部放置处理
// 计算目标单元格、处理文件夹创建、添加等逻辑
// ... 详细实现
}
}
```
**代码分析**:
- 获取拖拽视图的视觉中心坐标
- 将坐标映射到目标布局的坐标系
- 区分内部拖拽和外部拖拽,分别处理
- 内部拖拽处理包括:单元格计算、文件夹创建、图标重排等
#### onDragEnter方法 - 进入拖拽区域
```java
@Override
public void onDragEnter(DragObject d) {
mCreateUserFolderOnDrop = false;
mAddToExistingFolderOnDrop = false;
mDropToLayout = null;
mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
setDropLayoutForDragObject(d, mDragViewVisualCenter[0], mDragViewVisualCenter[1]);
}
```
**代码分析**:
- 重置文件夹创建和添加标志
- 初始化放置目标布局
- 设置拖拽对象的放置布局
### 2. FolderIcon中DropTarget接口方法实现分析
#### acceptDrop方法 - 检查文件夹是否接受拖拽项
```java
public boolean acceptDrop(ItemInfo dragInfo) {
return !mFolder.isDestroyed() && willAcceptItem(dragInfo);
}
```
**代码分析**:
- 检查文件夹是否已被销毁
- 调用willAcceptItem方法检查具体项目类型是否可接受
- 支持的应用类型包括:快捷方式、小部件、应用对等
#### onDragEnter方法 - 进入文件夹图标拖拽区域
```java
public void onDragEnter(ItemInfo dragInfo) {
if (mFolder.isDestroyed() || !willAcceptItem(dragInfo)) return;
CellLayoutLayoutParams lp = (CellLayoutLayoutParams) getLayoutParams();
CellLayout cl = (CellLayout) getParent().getParent();
mBackground.animateToAccept(cl, lp.getCellX(), lp.getCellY());
mOpenAlarm.setOnAlarmListener(mOnOpenListener);
// 设置弹簧加载定时器
if (SPRING_LOADING_ENABLED && willAcceptItem(dragInfo)) {
mOpenAlarm.setAlarm(OPEN_DELAY);
}
}
```
**代码分析**:
- 验证文件夹状态和项目可接受性
- 获取布局参数和父级CellLayout
- 背景动画显示接受状态
- 设置打开定时器,实现弹簧加载效果
#### onDrop方法 - 处理放置到文件夹
```java
public void onDrop(DragObject d, boolean itemReturnedOnFailedDrop) {
ItemInfo item;
if (d.dragInfo instanceof WorkspaceItemFactory) {
// 来自All Apps - 创建副本
item = ((WorkspaceItemFactory) d.dragInfo).makeWorkspaceItem(getContext());
} else if (d.dragSource instanceof BaseItemDragListener){
// 来自不同窗口 - 创建副本
item = new AppInfo((AppInfo) d.dragInfo);
} else {
// 来自同一窗口 - 使用原始项目
item = d.dragInfo;
}
// 添加到文件夹的具体逻辑
mFolder.addItem(item);
}
```
**代码分析**:
- 根据拖拽源类型创建适当的项目实例
- 处理来自不同源的拖拽项目
- 调用文件夹的addItem方法添加项目
### 3. DragSource接口方法实现分析
#### Folder中onDropCompleted方法
```java
@Override
public void onDropCompleted(final View target, final DragObject d,
final boolean success) {
if (success) {
if (getItemCount() <= 1) {
mDeleteFolderOnDropCompleted = true;
}
if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon
&& target != this) {
replaceFolderWithFinalItem();
}
} else {
// 拖拽失败,需要将项目返回到文件夹
ItemInfo info = d.dragInfo;
View icon = (mCurrentDragView != null && mCurrentDragView.getTag() == info)
? mCurrentDragView : mContent.createNewView(info);
// 恢复项目到文件夹
mContent.addView(icon);
}
}
```
**代码分析**:
- 成功放置时检查是否需要删除文件夹(当项目数<=1时)
- 如果拖拽失败,恢复项目到文件夹中
- 处理文件夹的自动删除和项目恢复逻辑
#### Workspace中onDropCompleted方法
```java
public void onDropCompleted(final View target, final DragObject d,
final boolean success) {
if (success) {
if (target != this && mDragInfo != null) {
removeWorkspaceItem(mDragInfo.cell);
}
} else if (mDragInfo != null) {
// 拖拽取消时,将内容视图重新附加到原始父级
if (mDragInfo.cell instanceof LauncherAppWidgetHostView && d.dragView != null) {
d.dragView.detachContentView(/* reattachToPreviousParent= */ true);
}
final CellLayout cellLayout = mLauncher.getCellLayout(
mDragInfo.container, mDragInfo.screenId);
if (cellLayout != null) {
cellLayout.onDropChild(mDragInfo.cell);
}
}
mDragInfo = null;
}
```
**代码分析**:
- 成功放置时从工作区移除原始项目
- 拖拽取消时恢复小部件内容视图
- 清理拖拽信息,重置状态
### 4. 关键设计模式应用分析
#### 策略模式在DropTarget中的应用
每个DropTarget实现类根据自身特性实现不同的放置策略:
- **Workspace**: 处理单元格布局和图标重排
- **FolderIcon**: 处理文件夹添加和弹簧加载
- **DeleteDropTarget**: 处理删除操作
#### 观察者模式在DragSource中的应用
DragSource接口允许不同的拖拽源(Folder、Workspace等)在拖拽完成后执行特定的清理和更新操作。
#### 模板方法模式在拖拽流程中的应用
DragController定义了拖拽的标准流程,而具体的DropTarget和DragSource实现提供特定于组件的实现。
## 性能优化特性
### 1. 延迟清理机制
通过`deferDragViewCleanupPostAnimation`标志,支持动画完成后才清理DragView,避免动画中断。
### 2. 视觉中心计算
`getVisualCenter()`方法提供精确的放置位置计算,提升用户体验。
### 3. 动画优化
使用插值器和动画监听实现流畅的拖拽体验,支持弹簧动画效果。
### 4. 内存管理
使用临时变量和对象池避免GC压力,提高性能。
## 总结
Launcher3的拖拽系统设计精良,具有以下特点:
1. **分层架构清晰**: 控制器层、驱动层、视图层职责分明
2. **接口设计灵活**: 通过接口实现高度可扩展性
3. **状态管理完善**: 预拖拽、正式拖拽、放置状态有序转换
4. **性能优化到位**: 延迟清理、视觉中心计算等优化措施
5. **用户体验流畅**: 支持流畅的拖拽动画和视觉效果
这套拖拽系统为Android Launcher提供了强大而灵活的拖拽功能,是Android系统交互体验的重要组成部分。
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,429 @@
# PagedOrientationHandler接口分析报告
## 概述
PagedOrientationHandler是Android Launcher3中一个高度抽象的方向处理接口,主要用于分离横向和纵向布局的具体实现,为分页视图(PagedView)和最近任务视图(RecentsView)提供统一的方向处理逻辑。
**源码位置**:
- 接口定义: `src/com/android/launcher3/touch/PagedOrientationHandler.java`
- 默认实现: `src/com/android/launcher3/touch/DefaultPagedViewHandler.java`
- 纵向实现: `quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.kt`
- 横向实现: `quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt`
- 扩展接口: `quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt`
## 接口设计理念
### 核心目标
1. **统一处理方向逻辑**:将X/Y轴操作抽象为"主方向"和"次方向"
2. **简化方向切换**:通过接口实现轻松切换横向/纵向布局
3. **减少代码重复**:避免在多个地方重复编写方向判断逻辑
### 设计优势
- **解耦设计**:方向逻辑与视图逻辑分离
- **扩展性强**:新增方向模式只需实现接口
- **性能优化**:避免运行时频繁方向判断
- **一致性保证**:统一接口确保行为一致性
## 接口层次结构
### 基础接口:PagedOrientationHandler
```java
public interface PagedOrientationHandler {
PagedOrientationHandler DEFAULT = new DefaultPagedViewHandler();
// 功能型接口
interface Int2DAction<T> { void call(T target, int x, int y); }
interface Float2DAction<T> { void call(T target, float x, float y); }
// 核心方法
<T> void setPrimary(T target, Int2DAction<T> action, int param);
float getPrimaryDirection(MotionEvent event, int pointerIndex);
float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId);
int getMeasuredSize(View view);
ChildBounds getChildBounds(View child, int childStart, int pageCenter, boolean layoutChild);
}
```
### 扩展接口:RecentsPagedOrientationHandler
```kotlin
interface RecentsPagedOrientationHandler : PagedOrientationHandler {
// 扩展方法
fun <T> setSecondary(target: T, action: Float2DAction<T>, param: Float)
operator fun <T> set(target: T, action: Int2DAction<T>, primaryParam: Int, secondaryParam: Int)
// 旋转相关属性
val degreesRotated: Float
val rotation: Int
val isLayoutNaturalToLauncher: Boolean
}
```
## 实现类分析
### 1. DefaultPagedViewHandler(默认实现)
**特点**:
- 提供基础的横向布局实现
- 将X轴作为主方向
- 作为其他实现的基类
**关键方法实现**:
```java
@Override
public int getPrimaryValue(int x, int y) {
return x; // 横向为主方向
}
@Override
public float getPrimaryDirection(MotionEvent event, int pointerIndex) {
return event.getX(pointerIndex);
}
```
### 2. PortraitPagedViewHandler(纵向布局)
**特点**:
- 继承DefaultPagedViewHandler
- 实现RecentsPagedOrientationHandler
- `degreesRotated = 0f`(无旋转)
- `rotation = Surface.ROTATION_0`
- `isLayoutNaturalToLauncher = true`
**适用场景**:纵向设备、自然布局
### 3. LandscapePagedViewHandler(横向布局)
**特点**:
- 直接实现RecentsPagedOrientationHandler
- 将Y轴作为主方向,X轴作为次方向
- `degreesRotated = 90f`(旋转90度)
- `rotation = Surface.ROTATION_90`
- `isLayoutNaturalToLauncher = false`
**关键方法实现**:
```kotlin
override fun <T> getPrimaryValue(x: T, y: T): T = y // 纵向为主方向
override fun <T> getSecondaryValue(x: T, y: T): T = x
```
## 实现类对比表
| 实现类 | 主方向 | 旋转角度 | 布局自然性 | 适用场景 |
|--------|--------|----------|------------|----------|
| **DefaultPagedViewHandler** | X轴 | 0° | 自然 | 基础横向布局 |
| **PortraitPagedViewHandler** | X轴 | 0° | 自然 | 纵向设备 |
| **LandscapePagedViewHandler** | Y轴 | 90° | 非自然 | 横向设备 |
## 架构设计
### 类图
```mermaid
classDiagram
class PagedOrientationHandler {
<<interface>>
+DEFAULT: DefaultPagedViewHandler
+setPrimary(T, Int2DAction~T~, int)
+getPrimaryDirection(MotionEvent, int): float
+getPrimaryVelocity(VelocityTracker, int): float
+getMeasuredSize(View): int
+getChildBounds(View, int, int, boolean): ChildBounds
}
class RecentsPagedOrientationHandler {
<<interface>>
+setSecondary(T, Float2DAction~T~, float)
+degreesRotated: Float
+rotation: Int
+isLayoutNaturalToLauncher: Boolean
}
class DefaultPagedViewHandler {
+getPrimaryValue(x, y): int
+getPrimaryDirection(event, pointerIndex): float
+getMeasuredSize(view): int
}
class PortraitPagedViewHandler {
+degreesRotated: 0f
+rotation: ROTATION_0
+isLayoutNaturalToLauncher: true
+getTaskMenuX(...): Float
+getTaskMenuY(...): Float
}
class LandscapePagedViewHandler {
+degreesRotated: 90f
+rotation: ROTATION_90
+isLayoutNaturalToLauncher: false
+getPrimaryValue(x, y): y
+getSecondaryValue(x, y): x
}
PagedOrientationHandler <|-- RecentsPagedOrientationHandler
PagedOrientationHandler <|.. DefaultPagedViewHandler
DefaultPagedViewHandler <|-- PortraitPagedViewHandler
RecentsPagedOrientationHandler <|.. PortraitPagedViewHandler
RecentsPagedOrientationHandler <|.. LandscapePagedViewHandler
```
### 系统架构图
```mermaid
graph TB
subgraph "方向处理层"
PO[PagedOrientationHandler<br/>接口]
RPO[RecentsPagedOrientationHandler<br/>扩展接口]
end
subgraph "实现层"
DPH[DefaultPagedViewHandler<br/>默认实现]
PPH[PortraitPagedViewHandler<br/>纵向实现]
LPH[LandscapePagedViewHandler<br/>横向实现]
end
subgraph "状态管理层"
ROS[RecentsOrientedState<br/>方向状态管理]
DP[DeviceProfile<br/>设备配置]
end
subgraph "视图层"
PV[PagedView<br/>分页视图基类]
RV[RecentsView<br/>最近任务视图]
TV[TaskView<br/>任务视图]
end
subgraph "用户交互层"
TC[TouchController<br/>触摸控制器]
AD[AnimationDriver<br/>动画驱动]
end
PO --> RPO
PO --> DPH
RPO --> PPH
RPO --> LPH
ROS --> PO
ROS --> RPO
DP --> ROS
PV --> PO
RV --> RPO
TV --> RPO
TC --> PV
AD --> RV
ROS -.-> TC
ROS -.-> AD
style PO fill:#1e3a8a
style RPO fill:#1e40af
style DPH fill:#3730a3
style PPH fill:#4c1d95
style LPH fill:#581c87
style ROS fill:#0f766e
style PV fill:#065f46
style RV fill:#047857
```
## 使用方法与调用关系
### 1. 初始化与配置
在`RecentsOrientedState.java`中动态选择实现:
```java
private RecentsPagedOrientationHandler mOrientationHandler =
RecentsPagedOrientationHandler.PORTRAIT; // 默认纵向
public void updateOrientationHandler() {
if (isPortraitMode()) {
mOrientationHandler = RecentsPagedOrientationHandler.PORTRAIT;
} else {
mOrientationHandler = RecentsPagedOrientationHandler.LANDSCAPE;
}
// 通知相关视图更新
mContainerInterface.setOrientationHandler(mOrientationHandler);
}
```
### 2. 在PagedView中的使用
**关键调用示例**:
```java
// 滚动操作
mOrientationHandler.setPrimary(this, VIEW_SCROLL_TO, newPosition);
// 获取滚动位置
int oldPos = mOrientationHandler.getPrimaryScroll(this);
// 子视图布局
ChildBounds bounds = mOrientationHandler.getChildBounds(child, childStart, pageCenter, layoutChildren);
// 触摸事件处理
mDownMotionPrimary = mOrientationHandler.getPrimaryDirection(ev, 0);
```
### 3. 在RecentsView中的使用
**扩展使用场景**:
```java
// 画布变换
getPagedOrientationHandler().setPrimary(canvas, CANVAS_TRANSLATE, scroll);
// 任务视图可见性判断
int screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
int screenEnd = screenStart + getPagedOrientationHandler().getMeasuredSize(this);
// 任务菜单位置计算
getPagedOrientationHandler().getTaskMenuX(x, thumbnailView, deviceProfile, taskInsetMargin, taskViewIcon);
```
## 时序图分析
### 方向变化处理流程
```mermaid
sequenceDiagram
participant System as 系统
participant ROS as RecentsOrientedState
participant POH as PagedOrientationHandler
participant RV as RecentsView
System->>ROS: 设备旋转事件
ROS->>ROS: updateOrientationHandler()
ROS->>POH: 选择新的方向处理器
POH-->>ROS: 返回Portrait/Landscape处理器
ROS->>RV: setOrientationHandler()
RV->>RV: 重新布局和绘制
```
### 用户滑动操作流程
```mermaid
sequenceDiagram
participant User as 用户
participant TC as TouchController
participant PV as PagedView
participant POH as PagedOrientationHandler
participant RV as RecentsView
User->>TC: onTouchEvent(MotionEvent)
TC->>PV: 转发触摸事件
PV->>POH: getPrimaryDirection(event, 0)
POH-->>PV: 返回主方向坐标
PV->>PV: 计算滚动偏移
PV->>POH: setPrimary(this, VIEW_SCROLL_TO, position)
POH-->>PV: 执行滚动操作
PV->>RV: 触发重绘
RV->>POH: getPrimaryScroll(this)
POH-->>RV: 返回当前滚动位置
RV->>RV: 绘制可见任务
```
## 关键技术特性
### 1. 方向抽象机制
```java
// 将X/Y坐标抽象为主/次方向
int primary = handler.getPrimaryValue(x, y);
int secondary = handler.getSecondaryValue(x, y);
// 纵向:primary=x, secondary=y
// 横向:primary=y, secondary=x
```
### 2. 统一操作接口
```java
// 滚动操作统一处理
handler.setPrimary(view, VIEW_SCROLL_TO, position);
// 尺寸获取统一处理
int size = handler.getMeasuredSize(view);
// 边界计算统一处理
ChildBounds bounds = handler.getChildBounds(child, start, center, layout);
```
### 3. 动态适配机制
通过`RecentsOrientedState`实现:
- 实时监测设备方向变化
- 动态切换方向处理器
- 保持UI状态一致性
## 实际应用场景
### 1. 分页滚动
在`PagedView.java`中:
- 触摸事件的方向处理
- 滚动位置的计算和设置
- 子视图的布局计算
### 2. 最近任务管理
在`RecentsView.java`中:
- 任务视图的可见性判断
- 任务菜单的位置计算
- 动画变换的方向处理
### 3. 多任务分屏
在分屏场景中:
- 不同方向的分屏布局计算
- 任务边界的位置调整
- 动画效果的统一处理
## 核心方法详解
### 方向抽象方法
| 方法名 | 参数 | 返回值 | 说明 |
|--------|------|--------|------|
| `getPrimaryValue(x, y)` | int/int 或 float/float | 主方向值 | 获取主方向坐标值 |
| `getSecondaryValue(x, y)` | int/int 或 float/float | 次方向值 | 获取次方向坐标值 |
| `setPrimary(target, action, param)` | 目标对象, 动作接口, 参数 | void | 设置主方向参数 |
### 滚动相关方法
| 方法名 | 参数 | 返回值 | 说明 |
|--------|------|--------|------|
| `getPrimaryScroll(view)` | View | int | 获取主方向滚动位置 |
| `getMeasuredSize(view)` | View | int | 获取主方向尺寸 |
| `setMaxScroll(event, maxScroll)` | AccessibilityEvent, int | void | 设置最大滚动范围 |
### 布局相关方法
| 方法名 | 参数 | 返回值 | 说明 |
|--------|------|--------|------|
| `getChildBounds(child, start, center, layout)` | View, int, int, boolean | ChildBounds | 计算子视图边界 |
| `getCenterForPage(view, insets)` | View, Rect | int | 获取页面中心位置 |
| `getScrollOffsetStart/End(view, insets)` | View, Rect | int | 获取滚动偏移 |
## 总结
PagedOrientationHandler是Android Launcher中一个**高度抽象且设计精良的方向处理框架**。通过将方向逻辑抽象为统一的接口,它成功解决了多方向适配的复杂性,为Launcher的流畅用户体验提供了坚实的技术基础。
### 主要贡献
1. **架构清晰**:明确的层次结构和职责分离
2. **扩展性强**:易于添加新的方向模式
3. **性能优秀**:避免了重复的方向判断逻辑
4. **维护性好**:统一的接口降低了代码复杂度
### 适用场景
这种设计模式在需要处理多种方向场景的UI框架中具有很高的参考价值,特别是:
- 多方向适配的应用程序
- 复杂的布局管理系统
- 需要动态切换UI方向的场景
---
**文档版本**: 1.0
**分析时间**: 2026-01-29
**源码版本**: Android16 QPR2 Release
File renamed without changes.
@@ -0,0 +1,595 @@
# Recents布局及上滑流程分析报告
## 概述
本报告基于对AOSP Launcher3源码的深入分析,全面解析了Recents(最近任务)界面的布局架构以及从桌面和三方应用上滑进入Recents的完整流程。
**源码位置**:
- RecentsView: `quickstep/src/com/android/quickstep/views/RecentsView.java`
- TaskView: `quickstep/src/com/android/quickstep/views/TaskView.java`
- LauncherSwipeHandlerV2: `quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java`
- FallbackSwipeHandler: `quickstep/src/com/android/quickstep/FallbackSwipeHandler.java`
- SwipeUpAnimationLogic: `quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java`
## Recents布局架构分析
### 核心组件架构
Recents(最近任务)界面是Android多任务系统的核心,其架构设计高度模块化,主要包含以下核心组件:
#### 1. RecentsView - 最近任务视图容器
RecentsView是继承自PagedView的抽象类,负责:
- 管理所有TaskView的布局和显示
- 处理分页滚动逻辑
- 实现任务切换动画
- 支持横向/纵向布局适配
```java
public abstract class RecentsView<
CONTAINER_TYPE extends Context & RecentsViewContainer & StatefulContainer<STATE_TYPE>,
STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
HighResLoadingState.HighResLoadingStateChangedCallback,
TaskVisualsChangeListener {
```
#### 2. TaskView - 单个任务视图
每个TaskView代表一个运行中的应用程序任务,负责:
- 显示任务缩略图
- 处理任务操作(启动、关闭、分屏等)
- 实现任务动画效果
- 管理任务菜单
#### 3. RecentsModel - 数据模型层
负责与系统服务交互,管理任务数据:
- 从ActivityManager获取运行中的任务列表
- 缓存任务缩略图数据
- 处理任务状态变化通知
#### 4. RecentsOrientedState - 方向状态管理
管理Recents界面的方向状态:
- 动态选择Portrait/Landscape方向处理器
- 处理设备旋转事件
- 维护方向相关的配置信息
### 布局层次结构
```
RecentsView (PagedView)
├── TaskView 1
│ ├── TaskThumbnailView (缩略图)
│ ├── IconView (应用图标)
│ └── TaskMenuView (任务菜单)
├── TaskView 2
├── ClearAllButton (清除所有按钮)
└── OverviewActionsView (概览操作栏)
```
### 关键布局特性
#### 1. 分页布局机制
RecentsView继承自PagedView,支持:
- 水平/垂直分页滚动
- 惯性滚动效果
- 边缘回弹效果
- 无障碍访问支持
#### 2. 方向自适应
通过PagedOrientationHandler实现:
- 纵向模式:水平分页,X轴为主方向
- 横向模式:垂直分页,Y轴为主方向
- 动态方向切换支持
#### 3. 任务布局策略
- **网格布局**:多任务并排显示
- **全屏布局**:单个任务占据全屏
- **分屏布局**:支持Split Screen模式
## 桌面上滑流程分析
桌面上滑流程涉及从Launcher界面切换到Recents界面的完整动画过程。
### 核心处理类
#### 1. LauncherSwipeHandlerV2
桌面上滑的主要处理类,继承自AbsSwipeUpHandler:
```java
public class LauncherSwipeHandlerV2 extends AbsSwipeUpHandler<
QuickstepLauncher, RecentsView<QuickstepLauncher, LauncherState>, LauncherState> {
```
#### 2. SwipeUpAnimationLogic
上滑动画逻辑的抽象基类,处理:
- 位移计算和动画进度管理
- 远程动画目标处理
- 阻力效果实现
### 上滑流程详细分析
#### 阶段1:手势检测和初始化
**触发条件**:用户在桌面从底部向上滑动
**关键代码流程**:
```java
// LauncherSwipeHandlerV2构造函数
public LauncherSwipeHandlerV2(Context context, TaskAnimationManager taskAnimationManager,
RecentsAnimationDeviceState deviceState, RotationTouchHelper rotationTouchHelper,
GestureState gestureState, long touchTimeMs, boolean continuingLastGesture,
InputConsumerController inputConsumer, MSDLPlayerWrapper msdlPlayerWrapper) {
super(context, taskAnimationManager, deviceState, rotationTouchHelper, gestureState,
touchTimeMs, continuingLastGesture, inputConsumer, msdlPlayerWrapper);
}
```
**初始化步骤**:
1. 创建GestureState对象,记录手势状态
2. 初始化RemoteTargetGluer,准备远程动画目标
3. 设置输入消费者,拦截后续触摸事件
#### 阶段2:动画准备和远程动画启动
**关键方法**: `onDragStart()` 和 `onDrag()`
```java
// SwipeUpAnimationLogic中的位移更新
@UiThread
public void updateDisplacement(float displacement) {
// 计算位移和动画进度
displacement = overrideDisplacementForTransientTaskbar(-displacement);
mCurrentDisplacement = displacement;
float shift;
if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
shift = mDragLengthFactor;
} else {
float translation = Math.max(displacement, 0);
shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
}
mCurrentShift.updateValue(shift); // 更新动画进度
}
```
**动画准备过程**:
1. **启动Recents动画**:通过SystemUiProxy启动远程动画
2. **获取运行任务**:从ActivityManager获取当前运行的任务信息
3. **创建任务模拟器**:TaskViewSimulator用于动画计算
4. **设置动画端点**:计算过渡距离和阻力参数
#### 阶段3:动画执行和状态同步
#### 阶段4:Home动画工厂创建
桌面上滑特有的Home动画效果:
```java
protected HomeAnimationFactory createHomeAnimationFactory(
List<IBinder> launchCookies,
long duration,
boolean isTargetTranslucent,
boolean appCanEnterPip,
RemoteAnimationTarget runningTaskTarget,
@Nullable TaskView targetTaskView) {
// 查找工作区视图(图标或小部件)
final View workspaceView = findWorkspaceView(launchCookies, sourceTaskView);
if (workspaceView instanceof LauncherAppWidgetHostView) {
return createWidgetHomeAnimationFactory((LauncherAppWidgetHostView) workspaceView,
isTargetTranslucent, runningTaskTarget);
}
return createIconHomeAnimationFactory(workspaceView, targetTaskView);
}
```
**Home动画类型**:
1. **图标动画**:应用图标飞入热座或工作区
2. **小部件动画**:小部件缩放和位置调整
3. **浮动图标动画**:FloatingIconView实现的 morphing 效果
## 三方应用界面上滑流程分析
三方应用界面的上滑流程与桌面类似,但处理方式有所不同。
### 核心处理类:FallbackSwipeHandler
当在非Launcher应用中上滑时使用FallbackSwipeHandler:
```java
public class FallbackSwipeHandler extends AbsSwipeUpHandler<
FallbackActivityInterface.FallbackActivity,
RecentsView<FallbackActivityInterface.FallbackActivity, OverviewState>,
OverviewState> {
```
### 主要差异点
#### 1. 活动接口不同
- **桌面**:使用QuickstepLauncher和LauncherState
- **三方应用**:使用FallbackActivity和OverviewState
#### 2. 动画起始状态
- **桌面**:从工作区图标开始动画
- **三方应用**:从当前应用窗口开始动画
#### 3. Home动画处理
三方应用上滑时:
- 没有工作区图标动画
- 直接切换到Recents界面
- 动画效果更简单直接
### 上滑流程对比
| 特性 | 桌面上滑 | 三方应用上滑 |
|------|----------|-------------|
| **处理类** | LauncherSwipeHandlerV2 | FallbackSwipeHandler |
| **起始状态** | Launcher工作区 | 当前应用窗口 |
| **Home动画** | 图标飞入动画 | 简单切换动画 |
| **状态管理** | LauncherState | OverviewState |
| **容器接口** | QuickstepLauncher | FallbackActivity |
## Recents布局架构图
```mermaid
graph TB
subgraph "数据层"
RM[RecentsModel<br/>任务数据管理]
RTD[RecentTasksRepository<br/>任务仓库]
RDR[RecentsDeviceProfileRepository<br/>设备配置]
RRS[RecentsRotationStateRepository<br/>旋转状态]
end
subgraph "视图层"
RV[RecentsView<br/>最近任务容器]
TV1[TaskView 1<br/>任务视图]
TV2[TaskView 2<br/>任务视图]
TV3[TaskView 3<br/>任务视图]
CA[ClearAllButton<br/>清除按钮]
OV[OverviewActionsView<br/>操作栏]
end
subgraph "任务组件层"
TTV[TaskThumbnailView<br/>缩略图]
TIV[TaskIconView<br/>应用图标]
TMV[TaskMenuView<br/>任务菜单]
end
subgraph "状态管理层"
ROS[RecentsOrientedState<br/>方向状态]
POH[PagedOrientationHandler<br/>方向处理器]
DP[DeviceProfile<br/>设备配置]
end
subgraph "动画层"
SUAL[SwipeUpAnimationLogic<br/>上滑动画逻辑]
TVS[TaskViewSimulator<br/>任务模拟器]
RTH[RemoteTargetHandle<br/>远程目标]
end
subgraph "手势处理层"
LSH[LauncherSwipeHandlerV2<br/>桌面上滑处理器]
FSH[FallbackSwipeHandler<br/>应用上滑处理器]
GS[GestureState<br/>手势状态]
end
RM --> RV
RTD --> RM
RDR --> ROS
RRS --> ROS
RV --> TV1
RV --> TV2
RV --> TV3
RV --> CA
RV --> OV
TV1 --> TTV
TV1 --> TIV
TV1 --> TMV
ROS --> POH
DP --> ROS
POH --> RV
SUAL --> TVS
TVS --> RTH
RTH --> RV
LSH --> SUAL
FSH --> SUAL
GS --> LSH
GS --> FSH
style RM fill:#1e3a8a
style RV fill:#1e40af
style TV1 fill:#3730a3
style TTV fill:#4c1d95
style ROS fill:#0f766e
style SUAL fill:#065f46
style LSH fill:#047857
```
## 上滑流程时序图
```mermaid
sequenceDiagram
participant User as 用户手势
participant IC as InputConsumer
participant LSH as LauncherSwipeHandlerV2
participant SUAL as SwipeUpAnimationLogic
participant System as 系统服务
participant RV as RecentsView
participant RM as RecentsModel
Note over User, RM: 阶段1: 手势检测和初始化
User->>IC: TouchEvent(ACTION_DOWN)
IC->>LSH: onDragStart()
LSH->>System: startRecentsAnimation()
System-->>LSH: RemoteAnimationTargets[]
LSH->>SUAL: initTransitionEndpoints()
SUAL->>RM: 获取任务数据
RM-->>SUAL: 任务列表
Note over User, RM: 阶段2: 动画执行和位移更新
loop 手势持续
User->>IC: TouchEvent(ACTION_MOVE)
IC->>LSH: onDrag(位移, 速度)
LSH->>SUAL: updateDisplacement(位移)
SUAL->>SUAL: onCurrentShiftUpdated()
SUAL->>RV: 更新任务位置/缩放
RV->>RV: 重绘界面
end
Note over User, RM: 阶段3: 手势结束和状态切换
User->>IC: TouchEvent(ACTION_UP)
IC->>LSH: onDragEnd(最终速度)
LSH->>LSH: 判断是否进入Recents
alt 进入Recents (速度/位移达标)
LSH->>System: finishRecentsAnimation()
LSH->>RV: 完成进入动画
LSH->>RV: 切换到OVERVIEW状态
RV->>RV: 显示完整Recents界面
else 返回桌面 (未达标)
LSH->>LSH: 执行回弹动画
LSH->>System: cancelRecentsAnimation()
LSH->>RV: 恢复到NORMAL状态
end
Note over User, RM: 阶段4: 清理和状态同步
LSH->>IC: 释放输入消费者
LSH->>System: 清理动画资源
System-->>LSH: 动画完成确认
```
## 桌面上滑 vs 三方应用上滑详细对比时序图
### 桌面上滑详细流程(LauncherSwipeHandlerV2)
```mermaid
sequenceDiagram
participant User as 用户手势
participant IC as InputConsumer
participant LSH as LauncherSwipeHandlerV2
participant SUAL as SwipeUpAnimationLogic
participant System as 系统服务
participant HA as HomeAnimationFactory
participant RV as RecentsView
participant DP as DeviceProfile
Note over User, DP: 阶段1: 手势检测和初始化
User->>IC: TouchEvent(ACTION_DOWN)
IC->>LSH: onDragStart()
LSH->>System: startRecentsAnimation()
System-->>LSH: RemoteAnimationTargets[]
LSH->>DP: 获取设备配置
DP-->>LSH: DeviceProfile对象
LSH->>SUAL: initTransitionEndpoints(DP)
SUAL->>SUAL: 计算过渡距离和阻力参数
Note over User, DP: 阶段2: Home动画工厂创建
LSH->>LSH: findWorkspaceView()
alt 找到工作区图标
LSH->>HA: createIconHomeAnimationFactory()
HA->>HA: 创建FloatingIconView
HA->>HA: 设置图标位置和动画参数
else 找到小部件
LSH->>HA: createWidgetHomeAnimationFactory()
HA->>HA: 创建FloatingWidgetView
HA->>HA: 设置小部件动画参数
else 无合适视图
LSH->>HA: new LauncherHomeAnimationFactory()
HA->>HA: 创建简单动画工厂
end
Note over User, DP: 阶段3: 动画执行和位移更新
loop 手势持续 (ACTION_MOVE)
User->>IC: TouchEvent(位移, 速度)
IC->>LSH: onDrag()
LSH->>SUAL: updateDisplacement(位移)
SUAL->>SUAL: onCurrentShiftUpdated()
SUAL->>HA: update()
HA->>HA: 更新图标/小部件位置和透明度
SUAL->>RV: 更新任务位置和缩放
RV->>RV: 重绘界面
end
Note over User, DP: 阶段4: 手势结束和状态切换
User->>IC: TouchEvent(ACTION_UP)
IC->>LSH: onDragEnd(最终速度)
LSH->>LSH: 判断是否进入Recents
alt 进入Recents (速度/位移达标)
LSH->>System: finishRecentsAnimation()
LSH->>HA: 完成Home动画
HA->>HA: 执行图标飞入动画
LSH->>RV: 切换到OVERVIEW状态
RV->>RV: 显示完整Recents界面
else 返回桌面 (未达标)
LSH->>LSH: 执行回弹动画
LSH->>System: cancelRecentsAnimation()
LSH->>RV: 恢复到NORMAL状态
end
Note over User, DP: 阶段5: 清理资源
LSH->>IC: 释放输入消费者
LSH->>System: 清理动画资源
System-->>LSH: 动画完成确认
```
### 三方应用上滑详细流程(FallbackSwipeHandler)
```mermaid
sequenceDiagram
participant User as 用户手势
participant IC as InputConsumer
participant FSH as FallbackSwipeHandler
participant SUAL as SwipeUpAnimationLogic
participant System as 系统服务
participant HA as FallbackHomeAnimationFactory
participant RV as FallbackRecentsView
participant DP as DeviceProfile
participant AM as ActivityManager
Note over User, AM: 阶段1: 手势检测和初始化
User->>IC: TouchEvent(ACTION_DOWN)
IC->>FSH: onDragStart()
FSH->>System: startRecentsAnimation()
System-->>FSH: RemoteAnimationTargets[]
FSH->>DP: 获取设备配置
DP-->>FSH: DeviceProfile对象
FSH->>SUAL: initTransitionEndpoints(DP)
SUAL->>SUAL: 计算过渡距离和阻力参数
Note over User, AM: 阶段2: 检查是否运行在Home上
FSH->>FSH: mRunningOverHome检查
alt 运行在Home应用上
FSH->>FSH: 设置Home Builder Proxy
FSH->>FSH: updateHomeActivityTransformDuringSwipeUp()
else 运行在三方应用上
FSH->>FSH: 标准动画处理
end
Note over User, AM: 阶段3: Home动画工厂创建
FSH->>FSH: 检查是否支持PIP
alt 应用支持PIP
FSH->>HA: new FallbackPipToHomeAnimationFactory()
HA->>HA: 设置PIP动画参数
else 标准动画
FSH->>HA: new FallbackHomeAnimationFactory()
HA->>HA: 设置标准动画参数
FSH->>AM: startHomeIntent()
AM-->>FSH: 启动Home Activity
end
Note over User, AM: 阶段4: 动画执行和位移更新
loop 手势持续 (ACTION_MOVE)
User->>IC: TouchEvent(位移, 速度)
IC->>FSH: onDrag()
FSH->>SUAL: updateDisplacement(位移)
SUAL->>SUAL: onCurrentShiftUpdated()
alt 运行在Home上
FSH->>FSH: setHomeScaleAndAlpha()
FSH->>FSH: 更新Home窗口缩放和透明度
end
SUAL->>RV: 更新任务位置和缩放
RV->>RV: 重绘界面
end
Note over User, AM: 阶段5: 手势结束和状态切换
User->>IC: TouchEvent(ACTION_UP)
IC->>FSH: onDragEnd(最终速度)
FSH->>FSH: 判断是否进入Recents
alt 进入Recents (速度/位移达标)
FSH->>System: finishRecentsAnimation()
FSH->>HA: 完成动画
FSH->>RV: 切换到OVERVIEW状态
RV->>RV: 显示完整Recents界面
else 返回应用 (未达标)
FSH->>FSH: 执行回弹动画
FSH->>System: cancelRecentsAnimation()
FSH->>RV: 恢复到应用状态
end
Note over User, AM: 阶段6: 清理资源
FSH->>IC: 释放输入消费者
FSH->>System: 清理动画资源
System-->>FSH: 动画完成确认
```
### 关键差异对比分析
| 处理阶段 | LauncherSwipeHandlerV2 (桌面) | FallbackSwipeHandler (三方应用) |
|---------|-----------------------------|-------------------------------|
| **动画工厂创建** | createIconHomeAnimationFactory()<br/>createWidgetHomeAnimationFactory() | new FallbackHomeAnimationFactory()<br/>new FallbackPipToHomeAnimationFactory() |
| **Home动画类型** | FloatingIconView图标飞入<br/>FloatingWidgetView小部件动画 | 简单窗口缩放动画<br/>PIP进入动画 |
| **工作区视图查找** | findWorkspaceView()查找图标/小部件 | 无工作区视图查找逻辑 |
| **Home状态处理** | 无特殊Home状态处理 | mRunningOverHome检查<br/>updateHomeActivityTransformDuringSwipeUp() |
| **Home启动方式** | 无需额外启动Home | startHomeIntent()启动Home Activity |
| **动画复杂度** | 复杂的图标morphing效果 | 简单的窗口缩放效果 |
| **状态切换** | LauncherState.NORMAL ↔ OVERVIEW | RecentsState.APP ↔ OVERVIEW |
| **容器类型** | QuickstepLauncher | FallbackActivity |
| **视图类型** | RecentsView<QuickstepLauncher> | FallbackRecentsView<RecentsActivity> |
## 核心发现总结
### 1. Recents布局架构特点
**模块化分层设计**:
- **数据层**:RecentsModel负责任务数据管理
- **视图层**:RecentsView作为容器,TaskView作为任务单元
- **状态层**:RecentsOrientedState管理方向状态
- **动画层**:SwipeUpAnimationLogic处理动画逻辑
- **手势层**:不同的SwipeHandler处理不同场景
**关键技术特性**:
- **方向自适应**:通过PagedOrientationHandler实现横竖屏适配
- **分页布局**:继承PagedView支持平滑滚动
- **远程动画**:与系统服务协作实现无缝过渡
- **状态管理**:完善的状态机管理界面切换
### 2. 桌面上滑流程核心机制
**四阶段处理流程**:
1. **手势检测**:InputConsumer拦截触摸事件
2. **动画准备**:启动远程动画,获取任务数据
3. **动画执行**:实时更新位移,同步界面状态
4. **状态切换**:根据手势结果决定最终状态
**特色功能**:
- **Home动画**:图标飞入效果,提升用户体验
- **阻力效果**:物理模拟的滚动阻力
- **无缝过渡**:与系统动画服务深度集成
### 3. 三方应用上滑差异
**主要区别**:
- **处理类不同**:FallbackSwipeHandler vs LauncherSwipeHandlerV2
- **动画简化**:没有复杂的Home动画效果
- **状态管理**:使用OverviewState而非LauncherState
## 架构设计优势
1. **高度解耦**:各层职责清晰,便于维护和扩展
2. **性能优化**:异步数据加载,避免主线程阻塞
3. **可扩展性**:支持新的布局模式和动画效果
4. **一致性**:统一的接口设计保证行为一致
## 实际应用价值
这种架构设计为Android多任务系统提供了:
- **流畅的用户体验**:无缝的任务切换动画
- **灵活的方向适配**:自动适应设备方向变化
- **强大的扩展能力**:支持分屏、画中画等高级功能
- **稳定的性能表现**:优化的内存管理和动画性能
该分析为理解Android多任务系统的内部机制提供了深入的技术视角,对于系统定制、性能优化和功能扩展具有重要参考价值。
---
**文档版本**: 1.0
**分析时间**: 2026-01-29
**源码版本**: Android16 QPR2 Release
+1 -2
View File
@@ -1,6 +1,5 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
alias(libs.plugins.android.library)
}
android {
+15 -9
View File
@@ -11,16 +11,22 @@
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
#Thu Oct 16 12:33:31 CST 2025
ANDROID_X_VERSION=1+
BUILD_TOOLS_VERSION=28.0.3
COMPILE_SDK=android-36
GRADLE_CLASS_PATH=com.android.tools.build\:gradle\:8.13.1
KOTLIN=org.jetbrains.kotlin\:kotlin-gradle-plugin\:2.2.21
PROTOBUF_CLASS_PATH=com.google.protobuf\:protobuf-gradle-plugin\:0.9.5
PROTOBUF_DEPENDENCY=com.google.protobuf.nano\:protobuf-javanano\:3.1.0
android.enableJetifier=true
android.r8.maxWorkers=4
android.useAndroidX=true
android.enableHiddenApiCheck=false
android.hideApiWarning=false
# Improve build performance by enabling R8 parallelism.
android.r8.maxWorkers=4
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx4096m
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
org.gradle.caching=true
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8 -XX:+UseParallelGC
org.gradle.parallel=true
org.gradle.configuration-cache=true
org.gradle.configuration-cache.parallel=true
android.experimental.enableParallelR8=true
# Lawnchair-TODO(Cleanup): Remove these flags once Protobuf Gradle Plugin support DSL
android.newDsl=false
+18 -14
View File
@@ -1,13 +1,12 @@
[versions]
agp = "8.13.1"
protobuf = "0.9.5"
kotlin = "2.2.21"
agp = "9.0.0"
kotlin = "2.3.0"
appcompat = "1.7.1"
baselineprofile = "1.4.1"
core = "1.17.0"
annotation = "1.9.1"
coreAnimation = "1.0.0"
dagger = "2.57.2"
dagger = "2.59"
dexmaker = "1.2"
dexmakerMockito = "1.2"
dynamicanimation = "1.1.0"
@@ -20,21 +19,23 @@ kotlinx-coroutines = "1.10.2"
refine = "4.4.0"
# https://github.com/JetBrains/compose-multiplatform
compose = "1.9.3"
compose = "1.10.0"
lifecycle = "2.9.1"
lottie = "6.7.1"
material = "1.13.0"
material3Version = "1.4.0"
mockitoCore = "5.20.0"
mockitoCore = "5.21.0"
navigation = "2.9.1"
protobuf = "4.33.4"
coil = "3.3.0"
kotlinx-serialization = "1.9.0"
kotlinx-serialization = "1.10.0"
kotlinx-datetime = "0.7.1"
preference = "1.2.1"
protobufJavalite = "4.33.2"
protobufJavalite = "4.33.4"
recyclerview = "1.4.0"
robolectric = "4.16"
robolectric = "4.16.1"
rulesVersion = "1.7.0"
runner = "1.0.2"
rules = "1.0.2"
@@ -57,13 +58,13 @@ gson = "2.13.2"
# https://mvnrepository.com/artifact/org.dom4j/dom4j
dom4j = "2.1.4"
# https://mvnrepository.com/artifact/org.json/json
json = "20250517"
json = "20251224"
# https://mvnrepository.com/artifact/org.apache.maven.plugin-tools/maven-plugin-annotations
maven-plugin-annotations = "3.15.2"
# https://mvnrepository.com/artifact/org.codehaus.plexus/plexus-utils
plexus-utils = "4.0.0"
# https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
jacson-databind = "2.16.1"
jacson-databind = "2.21.0"
# https://mvnrepository.com/artifact/org.apache.maven/maven-plugin-api
maven-plugin-api = "3.9.11"
richeditor-compose = "1.0.0-beta02"
@@ -109,7 +110,10 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.
lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" }
material = { module = "com.google.android.material:material", version.ref = "material" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" }
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobufJavalite" }
protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
rules = { module = "com.android.support.test:rules", version.ref = "rules" }
runner = { module = "com.android.support.test:runner", version.ref = "runner" }
@@ -138,8 +142,8 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
google-ksp = "com.google.devtools.ksp:2.3.3"
google-protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }
google-ksp = "com.google.devtools.ksp:2.3.4"
google-protobuf = "com.google.protobuf:0.9.6"
#cash-licensee = { id = "app.cash.licensee", version.ref = "licensee" }
#rikka-refine = { id = "dev.rikka.tools.refine", version.ref = "refine" }
#diffplug-spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
+1 -4
View File
@@ -1,8 +1,6 @@
plugins {
id("com.android.library")
id("kotlin-android")
alias(libs.plugins.android.library)
}
android {
namespace = "com.android.launcher3.icons"
@@ -10,7 +8,6 @@ android {
defaultConfig {
minSdk = 26
targetSdk = 36
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
+58
View File
@@ -0,0 +1,58 @@
// 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 {
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
java_library {
name: "launcher-executor-qualifiers",
srcs: [
"src/com/android/launcher3/concurrent/annotations/Background.kt",
"src/com/android/launcher3/concurrent/annotations/LightweightBackground.kt",
"src/com/android/launcher3/concurrent/annotations/ThreadPool.kt",
"src/com/android/launcher3/concurrent/annotations/Ui.kt",
"src/com/android/launcher3/concurrent/annotations/UiContext.kt",
"src/com/android/launcher3/concurrent/annotations/BackgroundContext.kt",
"src/com/android/launcher3/concurrent/annotations/LightweightBackgroundContext.kt",
"src/com/android/launcher3/concurrent/annotations/ThreadPoolContext.kt",
],
sdk_version: "current",
min_sdk_version: min_launcher3_sdk_version,
target_sdk_version: "current",
static_libs: [
"androidx.annotation_annotation",
"jsr330",
],
}
java_library {
name: "launcher-executors-module",
srcs: [
"src/com/android/launcher3/concurrent/ExecutorsModule.kt",
],
sdk_version: "current",
min_sdk_version: min_launcher3_sdk_version,
target_sdk_version: "current",
plugins: ["dagger2-compiler"],
static_libs: [
"androidx.annotation_annotation",
"dagger2",
"guava",
"jsr330",
"launcher-executor-qualifiers",
"kotlinx-coroutines-android",
],
}
@@ -0,0 +1,184 @@
/*
* 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.concurrent
import com.android.launcher3.concurrent.annotations.Background
import com.android.launcher3.concurrent.annotations.LightweightBackground
import com.android.launcher3.concurrent.annotations.LightweightBackgroundPriority
import com.android.launcher3.concurrent.annotations.ThreadPool
import com.android.launcher3.concurrent.annotations.Ui
import com.android.launcher3.concurrent.annotations.UiContext
import com.android.launcher3.concurrent.annotations.LightweightBackgroundContext
import com.android.launcher3.concurrent.annotations.BackgroundContext
import com.android.launcher3.concurrent.annotations.ThreadPoolContext
import com.google.common.util.concurrent.ListeningExecutorService
import dagger.Binds
import dagger.Module
import dagger.Provides
import kotlin.coroutines.CoroutineContext
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import javax.inject.Singleton
import kotlinx.coroutines.asCoroutineDispatcher
/**
* Module that stipulates the executors that are usable by common launcher
* modules.
*
* This module does not provide the executors directly, but rather provides
* the qualifiers that are used to inject the executors and their derivatives
* when modules themselves need to utilize them.
*
* The executors are provided as follows:
* - @ThreadPool: Provides an unordered background executor or derivative
* backed by a thread pool.
* - @Background: Provides an ordered background executor or derivative.
* - @LightweightBackground: Provides a lightweight background executor or
* derivative that is either time sensitive or not depending on the
* [LightweightBackgroundPriority] flag.
* - @Ui: Provides a UI executor or derivative.
*
* The executors are provided as [Executor], [ExecutorService] or
* [ListeningExecutorService].
*
* Implementations for the executors should be provided as
* [ListeningExecutorService] by the host.
*/
@Module
interface ExecutorsModule {
// Unordered background executors
@Binds
@ThreadPool
fun provideThreadPoolExecutor(
@ThreadPool listeningExecutorService: ListeningExecutorService
): Executor
@Binds
@ThreadPool
fun provideThreadPoolExecutorService(
@ThreadPool listeningExecutorService: ListeningExecutorService
): ExecutorService
// end unordered background executors
// Ordered background executors
@Binds
@Background
fun provideBackgroundExecutor(
@Background listeningExecutorService: ListeningExecutorService
): Executor
@Binds
@Background
fun provideBackgroundExecutorService(
@Background listeningExecutorService: ListeningExecutorService
): ExecutorService
@Binds
@LightweightBackground(LightweightBackgroundPriority.UI)
fun provideUiLightweightBackgroundExecutor(
@LightweightBackground(LightweightBackgroundPriority.UI)
listeningExecutorService: ListeningExecutorService
): Executor
@Binds
@LightweightBackground(LightweightBackgroundPriority.UI)
fun provideUiLightweightBackgroundExecutorService(
@LightweightBackground(LightweightBackgroundPriority.UI)
listeningExecutorService: ListeningExecutorService
): ExecutorService
@Binds
@LightweightBackground(LightweightBackgroundPriority.DATA)
fun provideDataLightweightBackgroundExecutor(
@LightweightBackground(LightweightBackgroundPriority.DATA)
listeningExecutorService: ListeningExecutorService
): Executor
@Binds
@LightweightBackground(LightweightBackgroundPriority.DATA)
fun provideDataLightweightBackgroundExecutorService(
@LightweightBackground(LightweightBackgroundPriority.DATA)
listeningExecutorService: ListeningExecutorService
): ExecutorService
// end ordered background executors
// UI executors
@Binds
@Ui
fun provideUiExecutor(
@Ui listeningExecutorService: ListeningExecutorService
): Executor
@Binds
@Ui
fun provideUiExecutorService(
@Ui listeningExecutorService: ListeningExecutorService
): ExecutorService
// end UI executors
// The following methods provide the CoroutineContext for the executors.
companion object {
@Provides
@UiContext
fun provideUiContext(
@Ui executor: Executor
): CoroutineContext {
return executor.asCoroutineDispatcher()
}
@Provides
@LightweightBackgroundContext(LightweightBackgroundPriority.DATA)
fun provideDataLightweightContext(
@LightweightBackground(LightweightBackgroundPriority.DATA)
executor: Executor
): CoroutineContext {
return executor.asCoroutineDispatcher()
}
@Provides
@LightweightBackgroundContext(LightweightBackgroundPriority.UI)
fun provideUiLightweightContext(
@LightweightBackground(LightweightBackgroundPriority.UI)
executor: Executor
): CoroutineContext {
return executor.asCoroutineDispatcher()
}
@Provides
@BackgroundContext
fun provideBackgroundContext(
@Background executor: Executor
): CoroutineContext {
return executor.asCoroutineDispatcher()
}
@Provides
@ThreadPoolContext
fun provideThreadPoolContext(
@ThreadPool executor: Executor
): CoroutineContext {
return executor.asCoroutineDispatcher()
}
}
}
@@ -0,0 +1,37 @@
/*
* 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.concurrent.annotations
import javax.inject.Qualifier
/**
* Qualifier for an [Executor] or derivative that runs on a background thread.
*
* This is a background executor that is suitable for common system service
* interactions that could be time consuming.
*
* Usecases include:
* - Sequential system service interactions
* - Sequential I/O interactions
* - etc.
*
* Tasks submitted to this executor are guaranteed to be ordered, but are not
* guaranteed to be executed immediately.
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Background
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.concurrent.annotations
import javax.inject.Qualifier
/**
* Qualifier for the [kotlinx.coroutines.CoroutineContext] of the [Background]
* qualifier.
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class BackgroundContext
@@ -0,0 +1,65 @@
/*
* 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.concurrent.annotations
import javax.inject.Qualifier
/**
* Priority of the lightweight background executor.
*/
enum class LightweightBackgroundPriority {
/**
* Priority definition for lightweight background work that is not time
* sensitive, typically for data transformations.
*/
DATA,
/**
* Priority definition for lightweight background work that is time
* sensitive, typically for user interaction feedback.
*/
UI
}
/**
* Qualifier for an [Executor] or derivative that runs on a lightweight
* background thread.
*
* This is a lightweight executor that is suitable for running cheap background
* tasks that can either be time sensitive or not depending on the
* [LightweightBackgroundPriority] flag.
*
* LighweightBackground is distinct from Background in that it will not pollute
* the other as to ensure responsiveness of either.
*
* Non priority usecases include:
* - Data transformations within repository layers
*
* Priority usecases include:
* - Responses to user interactions
*
* Tasks submitted to this executor are guaranteed to be ordered.
*
* @param priority The priority of the lightweight background executor. By
* default, this is [LightweightBackgroundPriority.DATA].
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class LightweightBackground(
val priority: LightweightBackgroundPriority = LightweightBackgroundPriority.DATA
)
@@ -0,0 +1,29 @@
/*
* 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.concurrent.annotations
import javax.inject.Qualifier
/**
* Qualifier for the [kotlinx.coroutines.CoroutineContext] of the
* [LightweightBackground] qualifier.
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class LightweightBackgroundContext(
val priority: LightweightBackgroundPriority = LightweightBackgroundPriority.DATA
)
@@ -0,0 +1,38 @@
/*
* 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.concurrent.annotations
import javax.inject.Qualifier
/**
* Qualifier for an [Executor] or derivative that runs on a background thread
* pool.
*
* This is a background executor that provides execution of tasks that
* are not time sensitive and are time consuming.
*
* Usecases include:
* - Multiple disk I/O interactions
* - Network I/O interactions
* - etc.
*
* Tasks submitted to this executor are not guaranteed to be ordered and may be
* executed immediately or at a later time.
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class ThreadPool
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.concurrent.annotations
import javax.inject.Qualifier
/**
* Qualifier for the [kotlinx.coroutines.CoroutineContext] of the [ThreadPool]
* qualifier.
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class ThreadPoolContext
@@ -0,0 +1,35 @@
/*
* 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.concurrent.annotations
import javax.inject.Qualifier
/**
* Qualifier for an [Executor] or derivative that runs on the UI thread.
*
* This is the main thread executor that is suitable for running tasks that are
* time sensitive and are expected to run on the UI thread immediately.
*
* Usecases include:
* - Responsive UI updates
*
* Tasks submitted to this executor are guaranteed to be ordered and executed
* immediately.
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Ui
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.concurrent.annotations
import javax.inject.Qualifier
/**
* Qualifier for the [kotlinx.coroutines.CoroutineContext] of the
* [Ui] qualifier.
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class UiContext
+13
View File
@@ -0,0 +1,13 @@
*.iml
.project
.classpath
.project.properties
gen/
bin/
.idea/
.gradle/
local.properties
gradle/
build/
gradlew*
.DS_Store
+256
View File
@@ -0,0 +1,256 @@
// 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 {
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
default_visibility: [
"//packages/apps/Launcher3:__subpackages__",
"//vendor:__subpackages__",
],
}
android_library {
name: "widget_picker_dagger_annotations",
sdk_version: "current",
min_sdk_version: min_launcher3_sdk_version,
srcs: [
"src/com/android/launcher3/widgetpicker/Annotations.kt",
],
static_libs: [
"androidx.annotation_annotation",
"jsr330",
],
}
android_library {
name: "widget_picker_component",
sdk_version: "current",
min_sdk_version: min_launcher3_sdk_version,
srcs: [
"src/com/android/launcher3/widgetpicker/WidgetPickerModule.kt",
"src/com/android/launcher3/widgetpicker/WidgetPickerComponent.kt",
],
static_libs: [
"dagger2",
"widget_picker_dagger_annotations",
"widget_picker_data_repositories",
"widget_picker_shared_data_types",
"widget_picker_domain_layer",
"widget_picker_ui",
],
}
// Data types that are accessible & potentially provided by hosts using the widget picker library
android_library {
name: "widget_picker_shared_data_types",
sdk_version: "current",
min_sdk_version: min_launcher3_sdk_version,
srcs: [
"src/com/android/launcher3/widgetpicker/shared/model/PickableWidget.kt",
"src/com/android/launcher3/widgetpicker/shared/model/WidgetApp.kt",
"src/com/android/launcher3/widgetpicker/shared/model/WidgetAppIcon.kt",
"src/com/android/launcher3/widgetpicker/shared/model/WidgetAppId.kt",
"src/com/android/launcher3/widgetpicker/shared/model/WidgetId.kt",
"src/com/android/launcher3/widgetpicker/shared/model/WidgetPreview.kt",
"src/com/android/launcher3/widgetpicker/shared/model/WidgetUserProfiles.kt",
"src/com/android/launcher3/widgetpicker/shared/model/WidgetHostInfo.kt",
],
static_libs: [
"androidx.core_core-ktx",
"androidx.annotation_annotation",
],
}
android_library {
name: "widget_picker_data_repositories",
sdk_version: "current",
min_sdk_version: min_launcher3_sdk_version,
srcs: [
"src/com/android/launcher3/widgetpicker/data/repository/WidgetAppIconsRepository.kt",
"src/com/android/launcher3/widgetpicker/data/repository/WidgetsRepository.kt",
"src/com/android/launcher3/widgetpicker/data/repository/WidgetUsersRepository.kt",
],
static_libs: [
"widget_picker_shared_data_types",
"kotlinx_coroutines_android",
"kotlinx_coroutines",
],
}
android_library {
name: "widget_picker_domain_layer",
sdk_version: "current",
min_sdk_version: min_launcher3_sdk_version,
srcs: [
"src/com/android/launcher3/widgetpicker/domain/usecase/GroupWidgetAppsByProfileUseCase.kt",
"src/com/android/launcher3/widgetpicker/domain/usecase/FilterWidgetsForHostUseCase.kt",
"src/com/android/launcher3/widgetpicker/domain/interactor/WidgetsInteractor.kt",
"src/com/android/launcher3/widgetpicker/domain/interactor/WidgetAppIconsInteractor.kt",
"src/com/android/launcher3/widgetpicker/domain/model/SearchResult.kt",
],
static_libs: [
"widget_picker_shared_data_types",
"widget_picker_data_repositories",
"widget_picker_dagger_annotations",
"dagger2",
"kotlinx-coroutines-android",
"kotlinx-coroutines-core",
],
}
android_library {
name: "widget_picker_ui_data_types",
sdk_version: "current",
min_sdk_version: min_launcher3_sdk_version,
srcs: [
"src/com/android/launcher3/widgetpicker/ui/model/DisplayableWidgetApp.kt",
"src/com/android/launcher3/widgetpicker/ui/model/WidgetSizeGroup.kt",
],
static_libs: [
"androidx.compose.runtime_runtime",
"widget_picker_shared_data_types",
],
}
android_library {
name: "widget_picker_ui_theme",
sdk_version: "current",
min_sdk_version: min_launcher3_sdk_version,
srcs: [
"src/com/android/launcher3/widgetpicker/ui/theme/WidgetPickerTheme.kt",
"src/com/android/launcher3/widgetpicker/ui/theme/WidgetPickerColors.kt",
"src/com/android/launcher3/widgetpicker/ui/theme/WidgetPickerTextStyles.kt",
],
static_libs: [
"androidx.compose.foundation_foundation",
"androidx.compose.runtime_runtime",
"androidx.compose.ui_ui",
"androidx.compose.material3_material3",
],
}
android_library {
name: "widget_picker_ui_components",
sdk_version: "current",
min_sdk_version: min_launcher3_sdk_version,
plugins: ["dagger2-compiler"],
srcs: [
"src/com/android/launcher3/widgetpicker/ui/components/ScrollableFloatingToolbar.kt",
"src/com/android/launcher3/widgetpicker/ui/components/LeadingIconToolbarTab.kt",
"src/com/android/launcher3/widgetpicker/ui/components/TwoPaneLayout.kt",
"src/com/android/launcher3/widgetpicker/ui/components/SinglePaneLayout.kt",
"src/com/android/launcher3/widgetpicker/ui/components/WidgetsSearchBar.kt",
"src/com/android/launcher3/widgetpicker/ui/components/WidgetAppIcon.kt",
"src/com/android/launcher3/widgetpicker/ui/components/Strings.kt",
"src/com/android/launcher3/widgetpicker/ui/components/WidgetDetails.kt",
"src/com/android/launcher3/widgetpicker/ui/components/WidgetPreview.kt",
"src/com/android/launcher3/widgetpicker/ui/components/DragAndDrop.kt",
"src/com/android/launcher3/widgetpicker/ui/components/WidgetPreviewHostView.kt",
"src/com/android/launcher3/widgetpicker/ui/components/WidgetsGrid.kt",
"src/com/android/launcher3/widgetpicker/ui/components/ExpandCollapseIndicator.kt",
"src/com/android/launcher3/widgetpicker/ui/components/WidgetAppsListHeader.kt",
"src/com/android/launcher3/widgetpicker/ui/components/WidgetAppsList.kt",
"src/com/android/launcher3/widgetpicker/ui/components/TestTag.kt",
"src/com/android/launcher3/widgetpicker/ui/components/bottomsheet/BottomSheetDismissModifiers.kt",
"src/com/android/launcher3/widgetpicker/ui/components/bottomsheet/BottomSheetDismissState.kt",
"src/com/android/launcher3/widgetpicker/ui/components/bottomsheet/BottomSheetNestedScrollConnection.kt",
"src/com/android/launcher3/widgetpicker/ui/components/bottomsheet/TitledBottomSheet.kt",
"src/com/android/launcher3/widgetpicker/ui/components/Animations.kt",
"src/com/android/launcher3/widgetpicker/ui/components/BorderOnFocus.kt",
"src/com/android/launcher3/widgetpicker/ui/components/accessibility/AccessibilityState.kt",
],
static_libs: [
"widget_picker_event_listeners",
"widget_picker_cui_reporter",
"widget_picker_ui_theme",
"widget_picker_ui_data_types",
"androidx.compose.foundation_foundation",
"androidx.compose.foundation_foundation-layout",
"androidx.compose.runtime_runtime",
"androidx.compose.ui_ui",
"androidx.compose.ui_ui-tooling",
"androidx.compose.material3_material3",
"androidx.compose.material_material-icons-extended",
"androidx.activity_activity-compose",
"kotlinx-coroutines-android",
"kotlinx-coroutines-core",
],
resource_dirs: [
"res",
],
}
android_library {
name: "widget_picker_event_listeners",
sdk_version: "current",
min_sdk_version: min_launcher3_sdk_version,
srcs: [
"src/com/android/launcher3/widgetpicker/ui/WidgetPickerEventListeners.kt",
],
static_libs: [
"widget_picker_shared_data_types",
],
}
android_library {
name: "widget_picker_cui_reporter",
sdk_version: "current",
min_sdk_version: min_launcher3_sdk_version,
srcs: [
"src/com/android/launcher3/widgetpicker/ui/WidgetPickerCuiReporter.kt",
],
static_libs: [
"androidx.compose.runtime_runtime",
],
}
android_library {
name: "widget_picker_ui",
sdk_version: "current",
plugins: ["dagger2-compiler"],
min_sdk_version: min_launcher3_sdk_version,
srcs: [
"src/com/android/launcher3/widgetpicker/ui/ViewModel.kt",
"src/com/android/launcher3/widgetpicker/ui/fullcatalog/FullWidgetsCatalog.kt",
"src/com/android/launcher3/widgetpicker/ui/fullcatalog/FullWidgetsCatalogViewModel.kt",
"src/com/android/launcher3/widgetpicker/ui/fullcatalog/screens/landing/LandingScreen.kt",
"src/com/android/launcher3/widgetpicker/ui/fullcatalog/screens/landing/LandingScreenSinglePane.kt",
"src/com/android/launcher3/widgetpicker/ui/fullcatalog/screens/landing/LandingScreenTwoPane.kt",
"src/com/android/launcher3/widgetpicker/ui/fullcatalog/screens/landing/LandingScreenViewModel.kt",
"src/com/android/launcher3/widgetpicker/ui/fullcatalog/screens/search/SearchScreen.kt",
"src/com/android/launcher3/widgetpicker/ui/fullcatalog/screens/search/SearchScreenViewModel.kt",
],
static_libs: [
"widget_picker_ui_theme",
"widget_picker_ui_components",
"widget_picker_event_listeners",
"widget_picker_cui_reporter",
"widget_picker_ui_data_types",
"widget_picker_domain_layer",
"dagger2",
"widget_picker_dagger_annotations",
"androidx.compose.foundation_foundation",
"androidx.compose.foundation_foundation-layout",
"androidx.compose.runtime_runtime",
"androidx.compose.ui_ui",
"androidx.compose.ui_ui-tooling",
"androidx.compose.material3_material3",
"androidx.compose.material_material-icons-extended",
"androidx.activity_activity-compose",
"kotlinx-coroutines-android",
"kotlinx-coroutines-core",
],
}
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2025 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.launcher3.widgetpicker">
</manifest>
+142
View File
@@ -0,0 +1,142 @@
/*
* 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.
*/
plugins {
id(libs.plugins.android.library.get().pluginId)
id(libs.plugins.kotlin.android.get().pluginId)
id(libs.plugins.kotlin.kapt.get().pluginId)
id(libs.plugins.compose.compiler.get().pluginId)
}
// For the screenshot testing lib dependencies
apply<ResourceFixerPlugin>()
val androidTop = extra["ANDROID_TOP"].toString()
val robolibBuildDir = project(":RobolectricLib").layout.buildDirectory.toString()
val widgetPickerDir = "$androidTop/packages/apps/Launcher3/modules/widgetpicker"
android.buildFeatures.compose = true
android {
namespace = "com.android.launcher3.widgetpicker"
testNamespace = "com.android.launcher3.widgetpicker.tests"
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testApplicationId = "com.android.launcher3.widgetpicker.tests"
}
sourceSets {
named("main") {
java.setSrcDirs(listOf("$widgetPickerDir/src"))
manifest.srcFile("$widgetPickerDir/AndroidManifest.xml")
res.setSrcDirs(listOf("$widgetPickerDir/res"))
}
named("androidTest") {
java.setSrcDirs(
listOf(
"$widgetPickerDir/tests/multivalentScreenshotTests/src",
"$widgetPickerDir/tests/multivalentTestsForDevice/src",
)
)
res.setSrcDirs(listOf(
"$widgetPickerDir/tests/multivalentScreenshotTests/res",
"$widgetPickerDir/res"
))
manifest.srcFile("$widgetPickerDir/tests/AndroidManifest.xml")
}
named("test") {
java.setSrcDirs(listOf("$widgetPickerDir/tests/multivalentTests/src"))
resources.setSrcDirs(listOf("$widgetPickerDir/tests/config"))
manifest.srcFile("$widgetPickerDir/tests/AndroidManifest.xml")
res.setSrcDirs(listOf("$widgetPickerDir/tests/multivalentScreenshotTests/res"))
}
}
signingConfigs {
getByName("debug") {
// This is necessary or the private APIs from the studiow-generate SDK won't work.
// Without the platform keystore, it will crash with:
// "java.lang.NoSuchMethodError: No static method asyncTraceForTrackBegin"
storeFile = file("$androidTop/vendor/google/certs/devkeys/platform.keystore")
}
}
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
// Exclude META-INF for running test with android studio
packagingOptions.resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF")
}
dependencies {
implementation(libs.androidx.core)
implementation(libs.dagger)
kapt(libs.dagger.compiler)
kapt(libs.dagger.android.processor)
kaptAndroidTest(libs.dagger.compiler)
kaptAndroidTest(libs.dagger.android.processor)
// Compose UI dependencies
implementation(libs.compose.ui)
implementation(libs.compose.runtime)
implementation(libs.compose.foundation.layout)
implementation(libs.compose.material3)
implementation(libs.androidx.activity.compose)
// Other UI dependencies
implementation(libs.androidx.material3.window.size.cls)
implementation(libs.androidx.window)
// Compose android studio preview support
implementation(libs.compose.material.icons.extended)
implementation(libs.compose.ui.tooling.preview)
debugImplementation(libs.compose.ui.tooling)
// Testing
// this needs to be modern to support JDK-17 + asm byte code.
testImplementation(libs.mockito.robolectric.bytebuddy.agent)
testImplementation(libs.mockito.robolectric.bytebuddy)
testImplementation(libs.mockito.robolectric)
testImplementation(libs.mockito.kotlin)
testImplementation(libs.junit)
testImplementation(libs.google.truth)
testImplementation(libs.androidx.test.runner)
testImplementation(libs.androidx.junit)
testImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.google.truth)
androidTestImplementation(libs.mockito.kotlin)
androidTestImplementation(libs.androidx.test.rules)
androidTestImplementation(libs.kotlinx.coroutines.test)
// Compose UI Tests
testApi(libs.compose.ui.test.junit4)
androidTestApi(libs.compose.ui.test.junit4)
debugApi(libs.compose.ui.test.manifest)
// Shared testing libs
testImplementation(project(":RobolectricLib"))
testImplementation(project(":SharedTestLib"))
androidTestImplementation(project(":SharedTestLib"))
androidTestImplementation(project(":PlatformParameterizedLib"))
androidTestImplementation(project(":ScreenshotLib"))
androidTestImplementation(project(":ScreenshotComposeLib"))
}
// Work around for kotlin bug with symlinked source: http://b/316363701
tasks.matching { it.name.matches(Regex("widgetpicker.*compile.*TestKotlin")) }.configureEach {
inputs.dir("$widgetPickerDir/tests/multivalentTests/src")
}
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Maak blad toe"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Appnaam"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# legstuk}other{# legstukke}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# kortpad}other{# kortpaaie}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d breed en %2$d hoog"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>-legstuk, %2$d breed en %3$d hoog"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Wys Voeg By-knoppie"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Versteek Voeg By-knoppie"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Voeg by"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Voeg <xliff:g id="WIDGET_NAME">%1$s</xliff:g>-legstuk by"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Uitgestal"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Blaai"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> legstukke"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g>-legstukke aan die regterkant, soektog en opsies aan die linkerkant"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Soek- en legstukopsies aan die linkerkant, legstukke aan die regterkant"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Soek"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Vee uit"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Terug"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Legstukke en kortpaaie is nie beskikbaar nie"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Geen legstukke of kortpaaie gevind nie"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"ሉህን ዝጋ"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"የመተግበሪያ ስም"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# ምግብር}one{# ምግብር}other{# ምግብሮች}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# አቋራጭ}one{# አቋራጭ}other{# አቋራጮች}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>፣ <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d ስፋት በ%2$d ከፍታ"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"የ<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ምግብር፣ %2$d ስፋት በ%3$d ቁመት"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"የማከል አዝራርን አሳይ"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"የማከል አዝራርን ደብቅ"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"አክል"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ምግብርን አክል"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"ተለይተው የቀረቡ"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"ያስሱ"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> ምግብሮች"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g> ምግብሮች በቀኝ በኩል፣ ፍለጋ እና አማራጮች በግራ በኩል"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"የፍለጋ እና ምግብር አማራጮች በግራ፣ ምግብሮች ወደ ቀኝ"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"ፍለጋ"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"አጽዳ"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"ተመለስ"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"ምግብሮች እና አቋራጮች አይገኙም"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"ምንም ምግብሮች ወይም አቋራጮች አልተገኙም"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"إغلاق ورقة البيانات"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"اسم التطبيق"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{تطبيق مصغَّر واحد}zero{# تطبيق مصغَّر}two{تطبيقان مصغَّران}few{# تطبيقات مصغَّرة}many{# تطبيقًا مصغَّرًا}other{# تطبيق مصغَّر}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{اختصار واحد}zero{‫# اختصار}two{اختصاران}few{‫# اختصارات}many{‫# اختصارًا}other{‫# اختصار}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"‫<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>، <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"‏عرض %1$d وارتفاع %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"‏التطبيق المصغَّر \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\"، بعرض %2$d وارتفاع %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"إظهار زر الإضافة"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"إخفاء زر الإضافة"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"إضافة"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"إضافة تطبيق \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\" المصغّر"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"تطبيقات مصغَّرة مميّزة"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"تصفّح"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"تطبيقات \"<xliff:g id="TAB_NAME">%1$s</xliff:g>\" المصغّرة"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"تطبيقات \"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g>\" المصغَّرة على اليسار، والبحث والخيارات على اليمين"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"شريط البحث وخيارات التطبيقات المصغّرة على اليسار، والتطبيقات المصغّرة على اليمين"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"بحث"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"محو"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"رجوع"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"التطبيقات المصغَّرة والاختصارات غير متاحة"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"لم يتم العثور على تطبيقات مصغَّرة أو اختصارات"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"শ্বীট বন্ধ কৰক"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"এপৰ নাম"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# টা ৱিজেট}one{# টা ৱিজেট}other{# টা ৱিজেট}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# টা শ্বৰ্টকাট}one{# টা শ্বৰ্টকাট}other{# টা শ্বৰ্টকাট}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g> টা, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g> টা"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d বহল x %2$d ওখ"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ৱিজেট, %2$d বহল %3$d ওখ"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"যোগ দিয়ক বুটামটো দেখুৱাওক"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"যোগ দিয়ক বুটামটো লুকুৱাওক"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"যোগ দিয়ক"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ৱিজেট যোগ দিয়ক"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"বিশেষভাৱে প্ৰদৰ্শিত"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"ব্ৰাউজ কৰক"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> ৱিজেট"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g> ৱিজেট সোঁফালে, সন্ধান আৰু বিকল্পসমূহ বাওঁফালে"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"বাওঁফলে সন্ধান কৰা আৰু ৱিজেটৰ বিকল্প, সোঁফালে ৱিজেট"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"সন্ধান কৰক"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"মচক"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"উভতি যাওক"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"ৱিজেট আৰু শ্বৰ্টকাট উপলব্ধ নহয়"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"কোনো ৱিজেট আৰু শ্বৰ্টকাট পোৱা নগ’ল"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Vərəqi bağlayın"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Tətbiq adı"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# vidcet}other{# vidcet}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# qısayol}other{# qısayol}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d en, %2$d hündürlük"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> vidceti, eni %2$d, hündürlüyü %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Əlavə edin düyməsini göstərin"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Əlavə edin düyməsini gizlədin"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Əlavə edin"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> vidcetini əlavə edin"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Xüsusi"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Axtarın"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> vidcetləri"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g> vidcetləri sağda, axtarış və seçimlər solda"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Solda axtarış və vidcet seçimləri, sağda vidcetlər"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Axtarış"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Silin"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Geriyə"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Vidcet və qısayollar əlçatan deyil"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Vidcet və ya qısayol tapılmayıb"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Zatvori tabelu"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Naziv aplikacije"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# vidžet}one{# vidžet}few{# vidžeta}other{# vidžeta}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# prečica}one{# prečica}few{# prečice}other{# prečica}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d×%2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"širina %1$d i visina %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Vidžet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, širina %2$d i visina %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Prikažite dugme za dodavanje"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Sakrijte dugme za dodavanje"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Dodaj"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Dodajte vidžet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Istaknuto"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Pregledajte"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"Vidžeti: <xliff:g id="TAB_NAME">%1$s</xliff:g>"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Vidžeti <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> sa desne strane, pretraga i opcije sa leve strane"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Opcije pretrage i vidžeta sa leve strane, vidžeti sa desne strane"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Pretražite"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Obrišite"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Nazad"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Vidžeti i prečice nisu dostupni"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Nije pronađen nijedan vidžet ni prečica"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Закрыць аркуш"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Назва праграмы"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# віджэт}one{# віджэт}few{# віджэты}many{# віджэтаў}other{# віджэта}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# ярлык}one{# ярлык}few{# ярлыкі}many{# ярлыкоў}other{# ярлыка}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"Шырыня: %1$d, вышыня: %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Віджэт \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\": шырыня – %2$d, вышыня – %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Паказаць кнопку \"Дадаць\""</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Схаваць кнопку \"Дадаць\""</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Дадаць"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Дадаць віджэт \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\""</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Вартае ўвагі"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Агляд"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"Віджэты \"<xliff:g id="TAB_NAME">%1$s</xliff:g>\""</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Віджэты праграмы \"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g>\" справа, пошук і параметры злева"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Параметры пошуку і віджэтаў злева, віджэты справа"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Пошук"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Ачысціць"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Назад"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Віджэты і ярлыкі недаступныя"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Віджэтаў і ярлыкоў не знойдзена"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Затваряне на таблицата"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Име на приложението"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# приспособление}other{# приспособления}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# пряк път}other{# преки пътя}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"Ширина %1$d и височина %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Приспособлението „<xliff:g id="WIDGET_NAME">%1$s</xliff:g>“ е с ширина %2$d и височина %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Показване на бутона за добавяне"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Скриване на бутона за добавяне"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Добавяне"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Добавяне на приспособлението „<xliff:g id="WIDGET_NAME">%1$s</xliff:g>“"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Представени"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Преглед"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> приспособления"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Приспособленията за <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> са отдясно, търсенето и опциите – отляво"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Опциите за търсенето и приспособленията са отляво, а самите приспособления – отдясно"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Търсене"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Изчистване"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Назад"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Няма налични преки пътища и приспособления"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Няма открити преки пътища или приспособления"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"শিট বন্ধ করুন"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"অ্যাপের নাম"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{#টি উইজেট}one{#টি উইজেট}other{#টি উইজেট}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{#টি শর্টকাট}one{#টি শর্টকাট}other{#টি শর্টকাট}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%2$d প্রস্থ x %1$d উচ্চতা"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> উইজেট, %2$d প্রস্থ ও %3$d উচ্চতা"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"যোগ করার বোতাম দেখুন"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"যোগ করার বোতাম লুকান"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"যোগ করুন"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> উইজেট যোগ করুন"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"ফিচার করা"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"ব্রাউজ করুন"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g>টি উইজেট"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g> উইজেট ডানদিকে, সার্চ ও বিকল্প বাঁদিকে রয়েছে"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"বাঁদিকে সার্চ ও উইজেট সংক্রান্ত বিকল্প, ডানদিকে উইজেট"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"সার্চ করুন"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"মুছে দিন"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"ফিরুন"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"উইজেট এবং শর্টকার্ট উপলভ্য নেই"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"কোনও উইজেট বা শর্টকার্ট খুঁজে পাওয়া যায়নি"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Zatvaranje tabele"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Naziv aplikacije"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# vidžet}one{# vidžet}few{# vidžeta}other{# vidžeta}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# prečica}one{# prečica}few{# prečice}other{# prečica}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"Širina %1$d, visina %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Vidžet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, širina %2$d, visina %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Prikazivanje dugmeta za dodavanje"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Sakrivanje dugmeta za dodavanje"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Dodajte"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Dodavanje vidžeta <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Istaknuto"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Pregledajte"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"Vidžeti kartice <xliff:g id="TAB_NAME">%1$s</xliff:g>"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Vidžeti aplikacije <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> su na desnoj, a pretraživanje i opcije na lijevoj strani"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Opcije pretraživanja i vidžeta su na lijevoj strani, a vidžeti su na desnoj"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Pretražite"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Brisanje"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Nazad"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Vidžeti i prečice nisu dostupni"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Nije pronađen nijedan vidžet ni prečica"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Tanca el full"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Nom de l\'aplicació"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}other{# widgets}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# drecera}other{# dreceres}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d d\'amplada per %2$d d\'alçària"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, %2$d d\'amplada per %3$d d\'alçària"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Mostra el botó Afegeix"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Amaga el botó Afegeix"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Afegeix"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Afegeix el widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Destacats"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Navega"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> widgets"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Widgets de <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> a la dreta, cerca i opcions a l\'esquerra"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Opcions de cerca i widgets a l\'esquerra, widgets a la dreta"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Cerca"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Esborra"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Enrere"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"No hi ha cap widget ni drecera disponibles"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"No s\'ha trobat cap widget ni drecera"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Zavřít list"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Název aplikace"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{ # widget}few{# widgety}many{# widgetu}other{# widgetů}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# zkratka}few{# zkratky}many{# zkratky}other{# zkratek}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"šířka %1$d, výška %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, šířka %2$d, výška %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Zobrazit tlačítko přidání"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Skrýt tlačítko přidání"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Přidat"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Přidat widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Vybrané"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Procházet"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> widgetů"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Widgety <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> vpravo, vyhledávání a možnosti vlevo"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Možnosti vyhledávání a widgetu vlevo, widgety vpravo"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Hledat"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Vymazat"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Zpět"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Widgety ani zkratky nejsou k dispozici"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Nebyly nalezeny žádné widgety ani zkratky"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Luk arket"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Appens navn"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}one{# widget}other{# widgets}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# genvej}one{# genvej}other{# genveje}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d i bredden og %2$d i højden"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>-widget, %2$d i bredden og %3$d i højden"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Vis knappen Tilføj"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Skjul knappen Tilføj"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Tilføj"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Tilføj <xliff:g id="WIDGET_NAME">%1$s</xliff:g>-widget"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Fremhævede"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Gennemse"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> widgets"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g>-widgets til højre, søgning og valgmuligheder til venstre"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Valgmuligheder for søgning og widgets til venstre, widgets til højre"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Søg"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Ryd"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Tilbage"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Der er ingen tilgængelige widgets eller genveje"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Der blev ikke fundet nogen widgets eller genveje"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Tabelle schließen"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"App-Name"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# Widget}other{# Widgets}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# Verknüpfung}other{# Verknüpfungen}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d breit und %2$d hoch"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Widget „<xliff:g id="WIDGET_NAME">%1$s</xliff:g>“, %2$d breit und %3$d hoch"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Button „Hinzufügen“ anzeigen"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Button „Hinzufügen“ ausblenden"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Hinzufügen"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Widget „<xliff:g id="WIDGET_NAME">%1$s</xliff:g>“ hinzufügen"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Empfohlen"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Suchen"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> Widgets"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Widgets vom Typ „<xliff:g id="SELECTED_HEADER">%1$s</xliff:g>“ rechts, Suche und Optionen links"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Such- und Widgetoptionen links, Widgets rechts"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Suchen"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Löschen"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Zurück"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Widgets und Verknüpfungen nicht verfügbar"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Keine Widgets oder Verknüpfungen gefunden"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Κλείσιμο φύλλου"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Όνομα εφαρμογής"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# γραφικό στοιχείο}other{# γραφικά στοιχεία}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# συντόμευση}other{# συντομεύσεις}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d πλάτος επί %2$d ύψος"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Γραφικό στοιχείο <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, %2$d πλάτος επί %3$d ύψος"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Εμφάνιση κουμπιού προσθήκης"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Απόκρυψη κουμπιού προσθήκης"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Προσθήκη"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Προσθήκη του γραφικού στοιχείου <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Επιλεγμένα"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Περιήγηση"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> γραφικά στοιχεία"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Γραφικά στοιχεία <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> στα δεξιά, αναζήτηση και επιλογές στα αριστερά"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Επιλογές αναζήτησης και γραφικών στοιχείων στα αριστερά, γραφικά στοιχεία στα δεξιά"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Αναζήτηση"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Διαγραφή"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Πίσω"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Δεν υπάρχουν διαθέσιμα γραφικά στοιχεία και συντομεύσεις"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Δεν βρέθηκαν γραφικά στοιχεία ή συντομεύσεις"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Close sheet"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"App name"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}other{# widgets}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# shortcut}other{# shortcuts}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d wide by %2$d high"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget, %2$d wide by %3$d high"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Show add button"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Hide add button"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Add"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Add <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Featured"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Browse"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> widgets"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g> widgets on right, search and options on left"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Search and widget options on left, widgets on right"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Search"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Clear"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Back"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Widgets and shortcuts aren\'t available"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"No widgets or shortcuts found"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Close sheet"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"App name"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}other{# widgets}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# shortcut}other{# shortcuts}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d wide by %2$d high"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget, %2$d wide by %3$d high"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Show add button"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Hide add button"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Add"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Add <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Featured"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Browse"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> widgets"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g> widgets on right, search and options on left"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Search and widget options on left, widgets on right"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Search"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Clear"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Back"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Widgets and shortcuts aren\'t available"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"No widgets or shortcuts found"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Close sheet"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"App name"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}other{# widgets}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# shortcut}other{# shortcuts}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d wide by %2$d high"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget, %2$d wide by %3$d high"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Show add button"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Hide add button"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Add"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Add <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Featured"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Browse"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> widgets"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g> widgets on right, search and options on left"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Search and widget options on left, widgets on right"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Search"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Clear"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Back"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Widgets and shortcuts aren\'t available"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"No widgets or shortcuts found"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Close sheet"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"App name"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}other{# widgets}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# shortcut}other{# shortcuts}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d wide by %2$d high"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget, %2$d wide by %3$d high"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Show add button"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Hide add button"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Add"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Add <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Featured"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Browse"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> widgets"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g> widgets on right, search and options on left"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Search and widget options on left, widgets on right"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Search"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Clear"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Back"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Widgets and shortcuts aren\'t available"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"No widgets or shortcuts found"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Cerrar hoja"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Nombre de la app"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}other{# widgets}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# acceso directo}other{# accesos directos}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d de ancho por %2$d de alto"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, %2$d de ancho por %3$d de alto"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Mostrar botón Agregar"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Ocultar botón Agregar"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Agregar"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Agregar widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Destacados"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Explorar"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> widgets"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Widgets de <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> a la derecha, búsqueda y opciones a la izquierda"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Opciones de búsqueda y widgets a la izquierda, widgets a la derecha"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Buscar"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Borrar"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Atrás"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Los widgets y accesos directos no están disponibles"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"No se encontraron widgets ni accesos directos"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Cerrar hoja"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Nombre de la aplicación"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}other{# widgets}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# acceso directo}other{# accesos directos}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d de ancho por %2$d de alto"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, %2$d de ancho por %3$d de alto"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Mostrar botón Añadir"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Ocultar botón Añadir"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Añadir"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Añadir widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Destacados"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Explorar"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"Widgets <xliff:g id="TAB_NAME">%1$s</xliff:g>"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Widgets de <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> a la derecha, búsqueda y opciones a la izquierda"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Opciones de búsqueda y de widgets a la izquierda, widgets a la derecha"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Buscar"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Borrar"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Atrás"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Los widgets y los accesos directos no están disponibles"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"No se han encontrado widgets ni accesos directos"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Lehe sulgemine"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Rakenduse nimi"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# vidin}other{# vidinat}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# otsetee}other{# otseteed}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d lai ja %2$d kõrge"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Vidin <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, %2$d lai ja %3$d kõrge"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Kuva lisamisnupp"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Peida lisamisnupp"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Lisa"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Lisa vidin <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Esiletõstetud"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Sirvimine"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"Vahekaardi <xliff:g id="TAB_NAME">%1$s</xliff:g> vidinad"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Teenuse <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> vidinad paremal, otsing ja valikud vasakul"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Otsing ja vidina valikud vasakul, vidinad paremal"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Otsing"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Kustuta"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Tagasi"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Vidinad ja otseteed pole saadaval"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Ei leitud ühtegi vidinat ega otseteed."</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Itxi orria"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Aplikazioaren izena"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}other{# widget}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# lasterbide}other{# lasterbide}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d zabal eta %2$d altu"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widgeta, %2$d zabal eta %3$d altu"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Erakutsi gehitzeko botoia"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Ezkutatu gehitzeko botoia"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Gehitu"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Gehitu <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widgeta"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Aipagarriak"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Arakatu"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"Widget <xliff:g id="TAB_NAME">%1$s</xliff:g>"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g> zerbitzuaren widgetak eskuinean, bilaketa eta aukerak ezkerrean"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Bilaketaren eta widgeten aukerak ezkerrean, widgetak eskuinean"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Bilatu"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Garbitu"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Atzera"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Widgetak eta lasterbideak ez daude erabilgarri"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Ez da aurkitu widgetik edo lasterbiderik"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"بستن برگ"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"نام برنامه"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{‫# ابزاره}one{‫# ابزاره}other{‫# ابزاره}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{‫# میان‌بر}one{‫# میان‌بر}other{‫# میان‌بر}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"‫<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>،‏ <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"‏‫%1$d در %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"‏‫%1$d عرض در %2$d طول"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"‏ابزاره <xliff:g id="WIDGET_NAME">%1$s</xliff:g>، %2$d عرض در %3$d ارتفاع"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"نشان دادن دکمه افزودن"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"پنهان کردن دکمه افزودن"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"افزودن"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"افزودن ابزاره <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"ویژه"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"مرور"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"ابزاره‌های <xliff:g id="TAB_NAME">%1$s</xliff:g>"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"ابزاره‌های <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> در سمت چپ، جستجو و گزینه‌ها در سمت راست"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"جستجو و گزینه‌های ابزاره در سمت راست، ابزاره در سمت چپ"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"جستجو"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"پاک کردن"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"برگشتن"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"ابزارک و میان‌بر دردسترس نیست"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"هیچ ابزاره یا میان‌بری پیدا نشد"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Sulje taulukko"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Sovelluksen nimi"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}other{# widgetiä}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# pikakuvake}other{# pikakuvaketta}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"Leveys %1$d, korkeus %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Widget: <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, leveys %2$d ja korkeus %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Näytä lisää-painike"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Piilota Lisää-painike"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Lisää"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Lisää widget: <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Suositellut"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Selaa"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> widgetiä"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Widgetit (<xliff:g id="SELECTED_HEADER">%1$s</xliff:g>) oikealla, haku ja vaihtoehdot vasemmalla"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Haku ja widget-vaihtoehdot vasemmalla, widgetit oikealla"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Haku"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Tyhjennä"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Takaisin"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Widgetit ja pikanäppäimet eivät ole saatavilla"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Widgetejä tai pikakuvakkeita ei löytynyt"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Fermer la feuille"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Nom de l\'appli"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}one{# widget}other{# widgets}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# raccourci}one{# raccourci}other{# raccourcis}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d x %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d de largeur sur %2$d de hauteur"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, %2$d de largeur sur %3$d de hauteur"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Afficher le bouton Ajouter"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Masquer le bouton Ajouter"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Ajouter"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Ajouter le widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"En vedette"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Parcourir"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> widgets"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Widgets <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> à droite, recherche et options à gauche"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Options de recherche et de widget à gauche, widgets à droite"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Recherchez"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Effacer"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Retour"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Aucun widget ni raccourci accessible"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Aucun widget ni raccourci trouvé"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Fermer la feuille"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Nom de l\'application"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}one{# widget}other{# widgets}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# raccourci}one{# raccourci}other{# raccourcis}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d x %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d de largeur et %2$d de hauteur"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, %2$d de large par %3$d de haut"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Afficher le bouton \"Ajouter\""</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Masquer le bouton \"Ajouter\""</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Ajouter"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Ajoutez un widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Sélection"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Parcourir"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"Widgets <xliff:g id="TAB_NAME">%1$s</xliff:g>"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Widgets <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> à droite, recherche et options à gauche"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Options de recherche et de widget à gauche, widgets à droite"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Rechercher"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Effacer"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Retour"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Les widgets et les raccourcis ne sont pas disponibles"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Aucun widget ni raccourci trouvé"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Pechar a folla"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Nome da aplicación"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}other{# widgets}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# atallo}other{# atallos}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d de largo por %2$d de alto"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>: %2$d de largo por %3$d de alto"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Mostrar o botón de engadir"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Ocultar o botón de engadir"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Engadir"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Engadir o widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Destacados"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Explorar"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g>: Widgets"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Widgets de <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> á dereita, busca e opcións á esquerda"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Opcións de busca e widgets á esquerda; widgets á dereita"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Fai unha busca"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Borrar"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Volver"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Non hai widgets nin atallos dispoñibles"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Non se atopou ningún widget nin atallo"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"શીટ બંધ કરો"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"ઍપનું નામ"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# વિજેટ}one{# વિજેટ}other{# વિજેટ}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# શૉર્ટકટ}one{# શૉર્ટકટ}other{# શૉર્ટકટ}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d પહોળાઈ X %2$d ઊંચાઈ"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> વિજેટ, %2$d પહોળું x %3$d ઊંચું"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"\'ઉમેરો\' બટન બતાવો"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"\'ઉમેરો\' બટન છુપાવો"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"ઉમેરો"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> વિજેટ ઉમેરો"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"વૈશિષ્ટિકૃત"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"બ્રાઉઝ કરો"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> વિજેટ"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g>ની જમણે વિજેટ, શોધ અને વિકલ્પો ડાબે"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"શોધ અને વિજેટના વિકલ્પો ડાબી બાજુ અને વિજેટ જમણી બાજુ છે"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"શોધો"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"સાફ કરો"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"પાછા ફરો"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"વિજેટ અને શૉર્ટકટ ઉપલબ્ધ નથી"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"કોઈ વિજેટ અથવા શૉર્ટકટ મળ્યા નથી"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"शीट बंद करें"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"ऐप्लिकेशन का नाम"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# विजेट}one{# विजेट}other{# विजेट}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# शॉर्टकट}one{# शॉर्टकट}other{# शॉर्टकट}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"यह %1$d चौड़ा और %2$d लंबा है"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट. यह %2$d चौड़ा और %3$d लंबा है"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"\'जोड़ें\' बटन दिखाएं"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"\'जोड़ें\' बटन छिपाएं"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"जोड़ें"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट जोड़ें"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"चुनिंदा"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"ब्राउज़ करें"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> विजेट"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g> के विजेट दाईं ओर, खोज का विजेट और अन्य विकल्प बाईं ओर मौजूद हैं"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Search और विजेट के विकल्प बाईं ओर और विजेट दाईं ओर मौजूद हैं"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"खोजें"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"मिटाएं"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"वापस जाएं"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"विजेट और शॉर्टकट उपलब्ध नहीं हैं"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"कोई विजेट या शॉर्टकट नहीं मिला"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Zatvori list"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Naziv aplikacije"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}one{# widget}few{# widgeta}other{# widgeta}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# prečac}one{# prečac}few{# prečaca}other{# prečaca}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"širina %1$d, visina %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, širine %2$d, visine %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Prikaži gumb za dodavanje"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Sakrij gumb za dodavanje"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Dodaj"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Dodaj widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Istaknuto"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Pregledajte"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> widgeti"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g> –widgeti zdesna, pretraživanje i opcije slijeva"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Opcije pretraživanja i widgeta slijeva, widgeti zdesna"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Pretražite"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Izbriši"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Natrag"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Widgeti i prečaci nisu dostupni"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Nema widgeta ili prečaca"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Munkalap bezárása"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Alkalmazásnév"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# modul}other{# modul}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# gyorsparancs}other{# gyorsparancs}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d széles és %2$d magas"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> modul – %2$d széles, %3$d magas"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Hozzáadás gomb megjelenítése"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Hozzáadás gomb elrejtése"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Hozzáadás"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> modul hozzáadása"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Kiemelt"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Böngészés"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> modul"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"A <xliff:g id="SELECTED_HEADER">%1$s</xliff:g>-modulok a jobb, a kereső és a beállítások pedig a bal oldalon találhatók"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"A Kereső és a modulbeállítások a bal, a modulok pedig a jobb oldalon találhatók"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Keresés"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Törlés"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Vissza"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"A modulok és parancsikonok nem állnak rendelkezésre"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Nem található modul vagy parancsikon"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Փակել թերթը"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Հավելվածի անվանումը"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# վիջեթ}one{# վիջեթ}other{# վիջեթ}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# դյուրանցում}one{# դյուրանցում}other{# դյուրանցում}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"Լայնությունը՝ %1$d, բարձրությունը՝ %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> վիջեթ, լայնությունը՝ %2$d, բարձրությունը՝ %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Ցույց տալ «Ավելացնել» կոճակը"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Թաքցնել «Ավելացնել» կոճակը"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Ավելացնել"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Ավելացնել «<xliff:g id="WIDGET_NAME">%1$s</xliff:g>» վիջեթը"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Առաջարկվող"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Դիտարկում"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> վիջեթներ"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"«<xliff:g id="SELECTED_HEADER">%1$s</xliff:g>» հավելվածի վիջեթներն աջ կողմում են, իսկ որոնման դաշտը և կարգավորումները՝ ձախ կողմում"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Որոնման և վիջեթների կարգավորումները ձախ կողմում են, իսկ վիջեթները՝ աջ"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Որոնեք"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Մաքրել"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Հետ"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Հասանելի վիջեթներ և դյուրանցումներ չկան"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Ոչ մի վիջեթ կամ դյուրանցում չի գտնվել"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Tutup sheet"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Nama aplikasi"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}other{# widget}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# pintasan}other{# pintasan}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"lebar %1$d dan tinggi %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, lebar %2$d dan tinggi %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Tampilkan tombol tambahkan"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Sembunyikan tombol tambahkan"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Tambahkan"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Tambahkan widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Unggulan"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Jelajahi"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"Widget <xliff:g id="TAB_NAME">%1$s</xliff:g>"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Widget <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> di bagian kanan, penelusuran dan opsi di bagian kiri"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Opsi penelusuran dan widget di sebelah kiri, widget di sebelah kanan"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Telusuri"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Hapus"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Kembali"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Widget dan pintasan tidak tersedia"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Tidak ada widget atau pintasan yang ditemukan"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Loka blaði"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Heiti forrits"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# græja}one{# græja}other{# græjur}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# flýtileið}one{# flýtileið}other{# flýtileiðir}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d á breidd og %2$d á hæð"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Græjan „<xliff:g id="WIDGET_NAME">%1$s</xliff:g>“, %2$d á breidd og %3$d á hæð"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Sýna hnapp til að bæta við"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Fela hnapp til að bæta við"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Bæta við"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Bæta græjunni „<xliff:g id="WIDGET_NAME">%1$s</xliff:g>“ við"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Sérvalið"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Skoða"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> græjur"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g>-græjur til hægri, leit og valkostir til vinstri"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Leitar- og græjuvalkostir vinstra megin, græjur hægra megin"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Leita"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Hreinsa"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Til baka"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Græjur og flýtilyklar eru ekki í boði"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Engar græjur eða flýtilyklar fundust"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Chiudi il foglio"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Nome dell\'app"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# widget}other{# widget}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# scorciatoia}other{# scorciatoie}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d di larghezza per %2$d di altezza"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, %2$d di larghezza per %3$d di altezza"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Mostra pulsante Aggiungi"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Nascondi pulsante Aggiungi"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Aggiungi"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Aggiungi widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"In evidenza"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Sfoglia"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"Widget <xliff:g id="TAB_NAME">%1$s</xliff:g>"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"Widget di <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> a destra, ricerca e opzioni a sinistra"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Opzioni di ricerca e widget a sinistra, widget a destra"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Cerca"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Cancella"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Indietro"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Widget e scorciatoie non disponibili"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Impossibile trovare widget o scorciatoie"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"סגירת החלונית"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"שם האפליקציה"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{ווידג\'ט אחד}one{‫# ווידג\'טים}two{‫# ווידג\'טים}other{‫# ווידג\'טים}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{קיצור דרך אחד}one{‫# קיצורי דרך}two{‫# קיצורי דרך}other{‫# קיצורי דרך}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"‫<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>‏, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"‏רוחב %1$d ואורך %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"‏ווידג\'ט <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, רוחב %2$d ואורך %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"הצגת כפתור ההוספה"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"הסתרת כפתור ההוספה"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"הוספה"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"הוספת הווידג\'ט <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"מומלצים"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"עיון"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"‫<xliff:g id="TAB_NAME">%1$s</xliff:g> ווידג\'טים"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"ווידג\'טים של <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> מימין, חיפוש ואפשרויות משמאל"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"אפשרויות החיפוש והווידג\'טים בצד שמאל, הווידג\'טים בצד ימין"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"חיפוש"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"מחיקה"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"חזרה"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"אין ווידג\'טים או קיצורי דרך זמינים"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"לא נמצאו ווידג\'טים או קיצורי דרך"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"シートを閉じる"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"アプリ名"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# 件のウィジェット}other{# 件のウィジェット}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# 件のショートカット}other{# 件のショートカット}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>、<xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"幅 %1$d、高さ %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>ウィジェット、幅 %2$d、高さ %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"追加ボタンを表示する"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"追加ボタンを非表示にする"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"追加"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>ウィジェットを追加"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"おすすめ"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"参照"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> 件のウィジェット"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g> のウィジェットは右側に、検索とオプションは左側にあります"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"検索とウィジェットのオプションは左側に、ウィジェットは右側にあります"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"検索"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"クリア"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"戻る"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"ウィジェットもショートカットも利用できません"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"ウィジェットやショートカットは見つかりませんでした"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"ფურცლის დახურვა"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"აპის სახელი"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# ვიჯეტი}other{# ვიჯეტი}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# მალსახმობი}other{# მალსახმობი}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"სიგანე: %1$d, სიმაღლე: %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ვიჯეტი, სიგანე: %2$d, სიმაღლე: %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"დამატების ღილაკის ჩვენება"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"დამატების ღილაკის დამალვა"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"დამატება"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ვიჯეტის დამატება"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"გამორჩეული"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"დათვალიერება"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> ვიჯეტი"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g> ვიჯეტები მდებარეობს მარჯვნივ, ძებნა და პარამეტრები — მარცხნივ"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"ძიებისა და ვიჯეტის ვარიანტები მარცხნივ, ვიჯეტები მარჯვნივ"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"ძიება"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"გასუფთავება"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"უკან"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"ვიჯეტები და მალსახმობები ხელმისაწვდომი არ არის"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"ვიჯეტები ან მალსახმობები ვერ მოიძებნა"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"Парақты жабу"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"Қолданба атауы"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# виджет}other{# виджет}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# таңбаша}other{# таңбаша}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"Ені: %1$d, биіктігі: %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"Виджет: <xliff:g id="WIDGET_NAME">%1$s</xliff:g>. Ені: %2$d, биіктігі: %3$d."</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"Қосу түймесін көрсету"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"Қосу түймесін жасыру"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"Қосу"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"Виджет (<xliff:g id="WIDGET_NAME">%1$s</xliff:g>) қосу"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"Таңдаулы"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"Шолу"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> виджеттері"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"<xliff:g id="SELECTED_HEADER">%1$s</xliff:g> виджеттері оң жақта, іздеу мен опциялар сол жақта орналасқан"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"Сол жақтағы іздеу мен виджет опциялары, оң жақтағы виджеттер"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"Іздеу"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"Тазарту"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"Артқа"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"Виджеттер мен таңбашалар қолжетімді емес"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"Ешқандай виджет немесе таңбаша табылмады"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"បិទសន្លឹក"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"ឈ្មោះ​កម្មវិធី"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{ធាតុ​ក្រាហ្វិក #}other{ធាតុ​ក្រាហ្វិក #}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{ផ្លូវកាត់ #}other{ផ្លូវកាត់ #}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"ទទឺង %1$d គុណនឹងកម្ពស់ %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"ធាតុ​ក្រាហ្វិក <xliff:g id="WIDGET_NAME">%1$s</xliff:g> ទទឹង %2$d គុណនឹងកម្ពស់ %3$d"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"បង្ហាញប៊ូតុង \"បញ្ចូល\""</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"លាក់ប៊ូតុង \"បញ្ចូល\""</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"បញ្ចូល"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"បញ្ចូលធាតុ​ក្រាហ្វិក <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"រំលេច​ពិសេស"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"រុករក"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"ធាតុ​ក្រាហ្វិក <xliff:g id="TAB_NAME">%1$s</xliff:g>"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"ធាតុក្រាហ្វិក <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> នៅខាងស្ដាំ ការស្វែងរក និងជម្រើសនៅខាងឆ្វេង"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"ជម្រើសធាតុក្រាហ្វិក និងម៉ាស៊ីនស្វែងរកនៅខាងឆ្វេង ធាតុ​ក្រាហ្វិកនៅខាងស្ដាំ"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"ស្វែងរក"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"សម្អាត"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"ថយក្រោយ"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"មិនមាន​ធាតុក្រាហ្វិក និងផ្លូវកាត់​បានទេ"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"រកមិនឃើញ​ធាតុក្រាហ្វិក ឬផ្លូវកាត់​ទេ"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"ಶೀಟ್ ಮುಚ್ಚಿರಿ"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"ಆ್ಯಪ್ ಹೆಸರು"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{# ವಿಜೆಟ್}one{# ವಿಜೆಟ್‌ಗಳು}other{# ವಿಜೆಟ್‌ಗಳು}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{# ಶಾರ್ಟ್‌ಕಟ್}one{# ಶಾರ್ಟ್‌ಕಟ್‌ಗಳು}other{# ಶಾರ್ಟ್‌ಕಟ್‌ಗಳು}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d × %2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"%1$d ಅಗಲ ಮತ್ತು %2$d ಎತ್ತರ"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ವಿಜೆಟ್, %2$d ಅಗಲ ಮತ್ತು %3$d ಎತ್ತರ"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"ಸೇರಿಸಿ ಬಟನ್ ಅನ್ನು ತೋರಿಸಿ"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"ಸೇರಿಸಿ ಬಟನ್ ಅನ್ನು ಮರೆಮಾಡಿ"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"ಸೇರಿಸಿ"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ವಿಜೆಟ್ ಸೇರಿಸಿ"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"ಫೀಚರ್ ಆಗಿರುವ"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"ಬ್ರೌಸ್ ಮಾಡಿ"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> ವಿಜೆಟ್‌ಗಳು"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"ಬಲಭಾಗದಲ್ಲಿ <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> ವಿಜೆಟ್‌ಗಳು, ಎಡಭಾಗದಲ್ಲಿ ಹುಡುಕಾಟ ಮತ್ತು ಆಯ್ಕೆಗಳು"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"ಎಡಭಾಗದಲ್ಲಿ ಹುಡುಕಾಟ ಮತ್ತು ವಿಜೆಟ್ ಆಯ್ಕೆಗಳು, ಬಲಭಾಗದಲ್ಲಿ ವಿಜೆಟ್‌ಗಳು"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"ಹುಡುಕಿ"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"ತೆರವುಗೊಳಿಸಿ"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"ಹಿಂದಕ್ಕೆ"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"ವಿಜೆಟ್‌ಗಳು ಮತ್ತು ಶಾರ್ಟ್‌ಕಟ್‌ಗಳು ಲಭ್ಯವಿಲ್ಲ"</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"ಯಾವುದೇ ವಿಜೆಟ್‌ಗಳು ಅಥವಾ ಶಾರ್ಟ್‌ಕಟ್‌ಗಳು ಪತ್ತೆಯಾಗಿಲ್ಲ"</string>
</resources>
@@ -0,0 +1,42 @@
<?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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="widget_picker_collapse_sheet_label" msgid="5179800676332357316">"시트 닫기"</string>
<string name="widgets_list_header_app_name_fallback_label" msgid="5151621916941957989">"앱 이름"</string>
<string name="widgets_list_header_widgets_count_label" msgid="578128450351893001">"{count,plural, =1{위젯 #개}other{위젯 #개}}"</string>
<string name="widgets_list_header_shortcuts_count_label" msgid="9168853338469361424">"{count,plural, =1{바로가기 #개}other{바로가기 #개}}"</string>
<string name="widgets_list_header_widgets_and_shortcuts_count_label" msgid="3942871723001789249">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
<string name="widget_span_dimensions_format" msgid="4325492869474815131">"%1$d×%2$d"</string>
<string name="widget_span_dimensions_accessible_format" msgid="6976586776463259811">"너비 %1$d, 높이 %2$d"</string>
<string name="widget_details_accessibility_label" msgid="4525863892199168041">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> 위젯, 너비는 %2$d, 높이는 %3$d입니다"</string>
<string name="widget_tap_to_show_add_button_label" msgid="3358071294981269937">"추가 버튼 표시"</string>
<string name="widget_tap_to_hide_add_button_label" msgid="7484536838079871360">"추가 버튼 숨기기"</string>
<string name="widget_tap_to_add_button_label" msgid="3110313936849291769">"추가"</string>
<string name="widget_tap_to_add_button_content_description" msgid="3591512565497536699">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> 위젯 추가"</string>
<string name="featured_widgets_tab_label" msgid="5911854248145111652">"추천"</string>
<string name="browse_widgets_tab_label" msgid="7179933202350730737">"둘러보기"</string>
<string name="widgets_tab_accessibility_label" msgid="559010210491169400">"<xliff:g id="TAB_NAME">%1$s</xliff:g> 위젯"</string>
<string name="widget_picker_right_pane_accessibility_label" msgid="5095301787174820892">"오른쪽에 <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> 위젯, 왼쪽에 검색 및 옵션"</string>
<string name="widget_picker_left_pane_accessibility_label" msgid="4719054594582596067">"검색 및 위젯 옵션은 왼쪽, 위젯은 오른쪽입니다"</string>
<string name="widgets_search_bar_hint" msgid="5705758627532180867">"검색"</string>
<string name="widget_search_bar_clear_button_label" msgid="6120140737164336517">"삭제"</string>
<string name="widget_search_bar_back_button_label" msgid="7028448831228587743">"뒤로"</string>
<string name="widgets_list_no_widgets_available" msgid="6451723564588621540">"위젯이나 바로가기를 사용할 수 없습니다."</string>
<string name="widgets_no_search_results" msgid="4336254967062545985">"위젯이나 바로가기가 없습니다."</string>
</resources>
Loaded 100 of 712 files, more files were not shown because too many files have changed in this diff. Show more