mirror of
https://github.com/alesimula/Murine-launcher.git
synced 2026-10-28 09:43:42 +00:00
Merge branch 'main' into Launcher3-16-QPR2-test-CHERRY
This commit is contained in:
711 files changed
+88331
-66
No files matched your search
@@ -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,6 +1,5 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
alias(libs.plugins.android.library)
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
+277
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
@@ -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()
|
||||
}
|
||||
}
|
||||
+40
@@ -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()
|
||||
}
|
||||
+168
@@ -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() {}
|
||||
}
|
||||
+39
@@ -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()
|
||||
}
|
||||
+76
@@ -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
|
||||
}
|
||||
}
|
||||
+132
@@ -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)
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.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)
|
||||
}
|
||||
}
|
||||
+84
@@ -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.
|
||||
}
|
||||
+120
@@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+228
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+485
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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`
|
||||
@@ -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,6 +1,5 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
alias(libs.plugins.android.library)
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
+15
-9
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
+27
@@ -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
|
||||
+65
@@ -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
|
||||
)
|
||||
+29
@@ -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
|
||||
+27
@@ -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
|
||||
@@ -0,0 +1,13 @@
|
||||
*.iml
|
||||
.project
|
||||
.classpath
|
||||
.project.properties
|
||||
gen/
|
||||
bin/
|
||||
.idea/
|
||||
.gradle/
|
||||
local.properties
|
||||
gradle/
|
||||
build/
|
||||
gradlew*
|
||||
.DS_Store
|
||||
@@ -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",
|
||||
],
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 711 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user