diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 9913ed490..434c42dc4 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -181,6 +181,7 @@ dependencies {
implementation(libs.androidx.hilt.navigation)
implementation(libs.androidx.hilt.work)
+ implementation(libs.androidx.core.remoteviews)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.datastore)
implementation(libs.androidx.fragment.compose)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 826f26174..420fcfb5a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -340,8 +340,9 @@
+ android:name="androidx.core.widget.RemoteViewsCompatService"
+ android:permission="android.permission.BIND_REMOTEVIEWS"
+ android:exported="false"/>
@Inject lateinit var workManager: Lazy
@Inject lateinit var geofenceApi: Lazy
- @Inject lateinit var appWidgetManager: Lazy
@Inject lateinit var workerFactory: HiltWorkerFactory
@Inject lateinit var contentObserver: Lazy
@Inject lateinit var syncAdapters: Lazy
@@ -139,7 +136,6 @@ class TasksApplication : Application(), Configuration.Provider {
}
OpenTaskContentObserver.registerObserver(context, contentObserver.get())
geofenceApi.get().registerAll()
- appWidgetManager.get().reconfigureWidgets()
CaldavSynchronizer.registerFactories()
}
diff --git a/app/src/main/java/org/tasks/extensions/RemoteViewsExtensions.kt b/app/src/main/java/org/tasks/extensions/RemoteViewsExtensions.kt
index 46dd4997a..a6b042ec0 100644
--- a/app/src/main/java/org/tasks/extensions/RemoteViewsExtensions.kt
+++ b/app/src/main/java/org/tasks/extensions/RemoteViewsExtensions.kt
@@ -2,12 +2,23 @@ package org.tasks.extensions
import android.graphics.Paint.ANTI_ALIAS_FLAG
import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
+import android.os.Parcel
import android.widget.RemoteViews
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.graphics.ColorUtils
import org.tasks.R
+fun RemoteViews.estimateParcelSize(): Int {
+ val parcel = Parcel.obtain()
+ return try {
+ writeToParcel(parcel, 0)
+ parcel.dataSize()
+ } finally {
+ parcel.recycle()
+ }
+}
+
fun RemoteViews.setColorFilter(viewId: Int, @ColorInt color: Int) =
setInt(viewId, "setColorFilter", color)
diff --git a/app/src/main/java/org/tasks/widget/AppWidgetManager.kt b/app/src/main/java/org/tasks/widget/AppWidgetManager.kt
index 2a7bb69e4..5c894b442 100644
--- a/app/src/main/java/org/tasks/widget/AppWidgetManager.kt
+++ b/app/src/main/java/org/tasks/widget/AppWidgetManager.kt
@@ -10,11 +10,9 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-import org.tasks.R
import org.tasks.compose.throttleLatest
import org.tasks.injection.ApplicationScope
import timber.log.Timber
-import java.util.concurrent.atomic.AtomicLong
import javax.inject.Inject
import javax.inject.Singleton
@@ -26,24 +24,13 @@ class AppWidgetManager @Inject constructor(
private val appWidgetManager: AppWidgetManager? by lazy {
AppWidgetManager.getInstance(context)
}
- private val _generation = AtomicLong(0)
- val generation: Long get() = _generation.get()
-
- private val updateChannel = Channel(Channel.CONFLATED)
+ private val updateChannel = Channel(Channel.CONFLATED)
init {
updateChannel
.consumeAsFlow()
.throttleLatest(1000)
- .onEach { gen ->
- if (gen == _generation.get()) {
- val appWidgetIds = widgetIds
- Timber.d("updateWidgets: ${appWidgetIds.joinToString { it.toString() }}")
- appWidgetManager?.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.list_view)
- } else {
- Timber.d("Skipping stale widget update")
- }
- }
+ .onEach { rebuildWidgets() }
.launchIn(scope)
}
@@ -52,10 +39,9 @@ class AppWidgetManager @Inject constructor(
?.getAppWidgetIds(ComponentName(context, TasksWidget::class.java))
?: intArrayOf()
- fun reconfigureWidgets(vararg appWidgetIds: Int) {
- val newGeneration = _generation.incrementAndGet()
+ fun rebuildWidgets(vararg appWidgetIds: Int) {
val ids = appWidgetIds.takeIf { it.isNotEmpty() } ?: widgetIds
- Timber.d("reconfigureWidgets(${ids.joinToString()}) generation=$newGeneration")
+ Timber.d("rebuildWidgets(${ids.joinToString()})")
val intent = Intent(context, TasksWidget::class.java)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
.apply { action = AppWidgetManager.ACTION_APPWIDGET_UPDATE }
@@ -63,7 +49,7 @@ class AppWidgetManager @Inject constructor(
}
fun updateWidgets() {
- updateChannel.trySend(_generation.get())
+ updateChannel.trySend(Unit)
}
fun exists(id: Int) = appWidgetManager?.getAppWidgetInfo(id) != null
diff --git a/app/src/main/java/org/tasks/widget/RequestPinWidgetReceiver.kt b/app/src/main/java/org/tasks/widget/RequestPinWidgetReceiver.kt
index c5d13013e..290e1c8dd 100644
--- a/app/src/main/java/org/tasks/widget/RequestPinWidgetReceiver.kt
+++ b/app/src/main/java/org/tasks/widget/RequestPinWidgetReceiver.kt
@@ -24,7 +24,7 @@ class RequestPinWidgetReceiver : BroadcastReceiver() {
val widgetPreferences = WidgetPreferences(context, preferences, widgetId)
widgetPreferences.setFilter(filter)
widgetPreferences.setColor(color)
- appWidgetManager.reconfigureWidgets(widgetId)
+ appWidgetManager.rebuildWidgets(widgetId)
}
companion object {
diff --git a/app/src/main/java/org/tasks/widget/TasksWidget.kt b/app/src/main/java/org/tasks/widget/TasksWidget.kt
index 54bd21ef1..82764e3e9 100644
--- a/app/src/main/java/org/tasks/widget/TasksWidget.kt
+++ b/app/src/main/java/org/tasks/widget/TasksWidget.kt
@@ -6,23 +6,29 @@ import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.os.Bundle
+import android.text.format.Formatter
import android.view.View
import android.widget.RemoteViews
-import androidx.core.net.toUri
+import androidx.core.widget.RemoteViewsCompat
import com.todoroo.andlib.utility.AndroidUtilities.atLeastS
import com.todoroo.astrid.activity.MainActivity.Companion.FINISH_AFFINITY
+import com.todoroo.astrid.subtasks.SubtasksHelper
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.runBlocking
import org.tasks.R
import org.tasks.compose.FilterSelectionActivity
+import org.tasks.data.dao.TaskDao
+import org.tasks.extensions.estimateParcelSize
import org.tasks.extensions.setBackgroundColor
import org.tasks.extensions.setColorFilter
import org.tasks.extensions.setRipple
import org.tasks.filters.Filter
import org.tasks.intents.TaskIntents
+import org.tasks.markdown.MarkdownProvider
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
+import org.tasks.tasklist.HeaderFormatter
import org.tasks.themes.ThemeColor
import timber.log.Timber
import javax.inject.Inject
@@ -32,15 +38,20 @@ class TasksWidget : AppWidgetProvider() {
@Inject lateinit var preferences: Preferences
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider
@Inject @ApplicationContext lateinit var context: Context
- @Inject lateinit var widgetManager: org.tasks.widget.AppWidgetManager
+ @Inject lateinit var subtasksHelper: SubtasksHelper
+ @Inject lateinit var taskDao: TaskDao
+ @Inject lateinit var chipProvider: WidgetChipProvider
+ @Inject lateinit var markdownProvider: MarkdownProvider
+ @Inject lateinit var headerFormatter: HeaderFormatter
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
Timber.d("onUpdate appWidgetIds=${appWidgetIds.joinToString { it.toString() }}")
- val generation = widgetManager.generation
appWidgetIds.forEach { id ->
try {
val options = appWidgetManager.getAppWidgetOptions(id)
- appWidgetManager.updateAppWidget(id, createWidget(context, id, options, generation))
+ val remoteViews = createWidget(context, id, options)
+ Timber.d("Widget $id main layout: ${Formatter.formatShortFileSize(context, remoteViews.estimateParcelSize().toLong())}")
+ appWidgetManager.updateAppWidget(id, remoteViews)
} catch (e: Exception) {
Timber.e(e)
}
@@ -54,24 +65,24 @@ class TasksWidget : AppWidgetProvider() {
newOptions: Bundle
) {
Timber.d("onAppWidgetOptionsChanged appWidgetId=$appWidgetId")
- appWidgetManager.updateAppWidget(
- appWidgetId,
- createWidget(context, appWidgetId, newOptions, widgetManager.generation)
- )
+ val remoteViews = createWidget(context, appWidgetId, newOptions)
+ Timber.d("Widget $appWidgetId main layout: ${Formatter.formatShortFileSize(context, remoteViews.estimateParcelSize().toLong())}")
+ appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
}
- private fun createWidget(context: Context, id: Int, options: Bundle, generation: Long): RemoteViews {
+ private fun createWidget(context: Context, id: Int, options: Bundle): RemoteViews {
val widgetPreferences = WidgetPreferences(context, preferences, id)
val settings = widgetPreferences.getWidgetHeaderSettings()
widgetPreferences.setCompact(
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) < COMPACT_MAX
)
+ val filterId = widgetPreferences.filterId
val filter = runBlocking {
- defaultFilterProvider.getFilterFromPreference(widgetPreferences.filterId)
+ defaultFilterProvider.getFilterFromPreference(filterId)
}
- Timber.d("createWidget id=$id generation=$generation filter=$filter")
+ Timber.d("createWidget id=$id filter=$filter")
- return RemoteViews(context.packageName, R.layout.scrollable_widget).apply {
+ val remoteViews = RemoteViews(context.packageName, R.layout.scrollable_widget).apply {
if (settings.showHeader) {
setViewVisibility(R.id.widget_header, View.VISIBLE)
setupHeader(settings, filter, id)
@@ -89,15 +100,30 @@ class TasksWidget : AppWidgetProvider() {
opacity = widgetPreferences.footerOpacity,
)
setOnClickPendingIntent(R.id.empty_view, getOpenListIntent(context, filter, id))
- val cacheBuster = "tasks://widget/$id/$generation".toUri()
- setRemoteAdapter(
- R.id.list_view,
- Intent(context, TasksWidgetAdapter::class.java)
- .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
- .setData(cacheBuster)
- )
setPendingIntentTemplate(R.id.list_view, getPendingIntentTemplate(context))
}
+
+ val builder = TasksWidgetBuilder(
+ subtasksHelper = subtasksHelper,
+ widgetPreferences = widgetPreferences,
+ filter = filter,
+ context = context,
+ widgetId = id,
+ taskDao = taskDao,
+ chipProvider = chipProvider,
+ markdown = markdownProvider.markdown(false),
+ headerFormatter = headerFormatter,
+ )
+ val items = runBlocking { builder.buildItems() }
+ RemoteViewsCompat.setRemoteAdapter(
+ context = context,
+ remoteViews = remoteViews,
+ appWidgetId = id,
+ viewId = R.id.list_view,
+ items = items
+ )
+
+ return remoteViews
}
private fun RemoteViews.setupHeader(
diff --git a/app/src/main/java/org/tasks/widget/TasksWidgetAdapter.kt b/app/src/main/java/org/tasks/widget/TasksWidgetAdapter.kt
deleted file mode 100644
index 7412b9e32..000000000
--- a/app/src/main/java/org/tasks/widget/TasksWidgetAdapter.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package org.tasks.widget
-
-import android.appwidget.AppWidgetManager
-import android.content.Context
-import android.content.Intent
-import android.widget.RemoteViewsService
-import com.todoroo.astrid.subtasks.SubtasksHelper
-import dagger.hilt.android.AndroidEntryPoint
-import dagger.hilt.android.qualifiers.ApplicationContext
-import kotlinx.coroutines.runBlocking
-import org.tasks.data.dao.TaskDao
-import org.tasks.markdown.MarkdownProvider
-import org.tasks.preferences.DefaultFilterProvider
-import org.tasks.preferences.Preferences
-import org.tasks.tasklist.HeaderFormatter
-import timber.log.Timber
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class TasksWidgetAdapter : RemoteViewsService() {
- @ApplicationContext @Inject lateinit var context: Context
- @Inject lateinit var defaultFilterProvider: DefaultFilterProvider
- @Inject lateinit var taskDao: TaskDao
- @Inject lateinit var preferences: Preferences
- @Inject lateinit var subtasksHelper: SubtasksHelper
- @Inject lateinit var chipProvider: WidgetChipProvider
- @Inject lateinit var markdownProvider: MarkdownProvider
- @Inject lateinit var headerFormatter: HeaderFormatter
-
- override fun onGetViewFactory(intent: Intent): RemoteViewsFactory? {
- val widgetId = intent.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return null
- val widgetPreferences = WidgetPreferences(context, preferences, widgetId)
- val filterId = widgetPreferences.filterId
- val filter = runBlocking {
- defaultFilterProvider.getFilterFromPreference(filterId)
- }
- Timber.d("onGetViewFactory $filter")
- return TasksWidgetViewFactory(
- subtasksHelper,
- widgetPreferences,
- filter,
- filterId,
- applicationContext,
- widgetId,
- taskDao,
- chipProvider,
- markdownProvider.markdown(false),
- headerFormatter,
- )
- }
-}
diff --git a/app/src/main/java/org/tasks/widget/TasksWidgetViewFactory.kt b/app/src/main/java/org/tasks/widget/TasksWidgetBuilder.kt
similarity index 82%
rename from app/src/main/java/org/tasks/widget/TasksWidgetViewFactory.kt
rename to app/src/main/java/org/tasks/widget/TasksWidgetBuilder.kt
index e1be769c6..0a44dea89 100644
--- a/app/src/main/java/org/tasks/widget/TasksWidgetViewFactory.kt
+++ b/app/src/main/java/org/tasks/widget/TasksWidgetBuilder.kt
@@ -2,10 +2,10 @@ package org.tasks.widget
import android.content.Context
import android.content.Intent
+import android.text.format.Formatter
import android.view.View
import android.widget.RemoteViews
-import android.widget.RemoteViewsService.RemoteViewsFactory
-import com.todoroo.andlib.utility.AndroidUtilities.atLeastAndroid16
+import androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems
import com.todoroo.astrid.core.SortHelper
import com.todoroo.astrid.subtasks.SubtasksHelper
import kotlinx.coroutines.runBlocking
@@ -18,6 +18,7 @@ import org.tasks.data.hasNotes
import org.tasks.data.isHidden
import org.tasks.data.isOverdue
import org.tasks.extensions.Context.is24HourFormat
+import org.tasks.extensions.estimateParcelSize
import org.tasks.extensions.setBackgroundResource
import org.tasks.extensions.setColorFilter
import org.tasks.extensions.setMaxLines
@@ -40,26 +41,23 @@ import org.tasks.ui.CheckBoxProvider.Companion.getCheckboxRes
import timber.log.Timber
import kotlin.math.max
-internal class TasksWidgetViewFactory(
+internal class TasksWidgetBuilder(
private val subtasksHelper: SubtasksHelper,
private val widgetPreferences: WidgetPreferences,
private val filter: Filter,
- private val filterId: String?,
private val context: Context,
private val widgetId: Int,
private val taskDao: TaskDao,
private val chipProvider: WidgetChipProvider,
private val markdown: Markdown,
private val headerFormatter: HeaderFormatter,
-) : RemoteViewsFactory {
- private val taskLimit = if (atLeastAndroid16()) 50 + 1 else Int.MAX_VALUE
+) {
private val indentPadding = (20 * context.resources.displayMetrics.density).toInt()
private val settings = widgetPreferences.getWidgetListSettings()
private val hPad = context.resources.getDimension(R.dimen.widget_padding).toInt()
private val disableGroups = !filter.supportsSorting()
|| (filter.supportsManualSort() && widgetPreferences.isManualSort)
|| (filter is AstridOrderingFilter && widgetPreferences.isAstridSort)
- private var tasks = SectionedDataSource()
private val onSurface = context.getColor(if (settings.isDark) R.color.white_87 else R.color.black_87)
private val onSurfaceVariant = context.getColor(if (settings.isDark) R.color.white_60 else R.color.black_60)
@@ -67,70 +65,6 @@ internal class TasksWidgetViewFactory(
chipProvider.isDark = settings.isDark
}
- override fun onCreate() {
- Timber.d("onCreate widgetId:$widgetId filter:$filter")
- }
-
- override fun onDataSetChanged() {
- Timber.v("onDataSetChanged $filter")
- if (widgetPreferences.filterId != filterId) {
- Timber.d("Skipping stale factory: expected $filterId, current ${widgetPreferences.filterId}")
- return
- }
- try {
- runBlocking {
- val collapsed = widgetPreferences.collapsed
- tasks = SectionedDataSource(
- taskDao.fetchTasks(getQuery(filter)),
- disableGroups,
- settings.groupMode,
- widgetPreferences.subtaskMode,
- collapsed,
- widgetPreferences.completedTasksAtBottom,
- )
- collapsed.toMutableSet().let {
- if (it.retainAll(tasks.getSectionValues().toSet())) {
- widgetPreferences.collapsed = it
- }
- }
- }
- } catch (e: InterruptedException) {
- Timber.w("Widget update interrupted")
- }
- }
-
- override fun onDestroy() {
- Timber.d("onDestroy widgetId:$widgetId")
- }
-
- override fun getCount() = tasks.size.coerceAtMost(taskLimit)
-
- override fun getViewAt(position: Int): RemoteViews? = tasks.let {
- when {
- position == taskLimit - 1 && it.size > taskLimit -> buildFooter()
- it.isHeader(position) -> buildHeader(it.getSection(position))
- position < it.size -> buildUpdate(it.getItem(position))
- else -> null
- }
- }
-
- override fun getLoadingView(): RemoteViews = newRemoteView()
-
- override fun getViewTypeCount(): Int = 3
-
- override fun getItemId(position: Int) = tasks.let {
- when {
- position == taskLimit - 1 && it.size > taskLimit -> 0
- it.isHeader(position) -> it.getSection(position).value
- position < it.size -> it.getItem(position).id
- else -> 0
- }
- }
-
- override fun hasStableIds(): Boolean = true
-
- private fun newRemoteView() = RemoteViews(BuildConfig.APPLICATION_ID, R.layout.widget_row)
-
private fun buildFooter(): RemoteViews {
return RemoteViews(BuildConfig.APPLICATION_ID, R.layout.widget_footer).apply {
setTextSize(R.id.widget_view_more, settings.textSize)
@@ -196,7 +130,7 @@ internal class TasksWidgetViewFactory(
!settings.showDueDates && task.isOverdue -> context.getColor(R.color.overdue)
else -> onSurface
}
- newRemoteView().apply {
+ RemoteViews(BuildConfig.APPLICATION_ID, R.layout.widget_row).apply {
strikethrough(R.id.widget_text, task.isCompleted)
setTextSize(R.id.widget_text, settings.textSize)
if (settings.showDueDates) {
@@ -304,8 +238,7 @@ internal class TasksWidgetViewFactory(
private suspend fun getQuery(filter: Filter): String {
subtasksHelper.applySubtasksToWidgetFilter(filter, widgetPreferences)
- val limit = if (taskLimit == Int.MAX_VALUE) null else taskLimit
- return getQuery(widgetPreferences, filter, limit)
+ return getQuery(widgetPreferences, filter, MAX_ITEMS)
}
private fun formatDueDate(row: RemoteViews, task: TaskContainer) = with(row) {
@@ -354,4 +287,72 @@ internal class TasksWidgetViewFactory(
setViewVisibility(dueDateRes, View.GONE)
}
}
+
+ suspend fun buildItems(): RemoteCollectionItems {
+ var totalParcelSize = 0
+ val collapsed = widgetPreferences.collapsed
+ val tasks = SectionedDataSource(
+ taskDao.fetchTasks(getQuery(filter)),
+ disableGroups,
+ settings.groupMode,
+ widgetPreferences.subtaskMode,
+ collapsed,
+ widgetPreferences.completedTasksAtBottom,
+ )
+ collapsed.toMutableSet().let {
+ if (it.retainAll(tasks.getSectionValues().toSet())) {
+ widgetPreferences.collapsed = it
+ }
+ }
+ Timber.d("buildItems loaded ${tasks.size} items for widget $widgetId")
+
+ data class WidgetItem(val id: Long, val view: RemoteViews, val isHeader: Boolean)
+ val items = mutableListOf()
+ var truncatedDueToSize = false
+
+ for (position in 0 until tasks.size) {
+ val isHeader = tasks.isHeader(position)
+ val id = if (isHeader) {
+ tasks.getSection(position).value
+ } else {
+ tasks.getItem(position).id
+ }
+ val view = if (isHeader) {
+ buildHeader(tasks.getSection(position))
+ } else {
+ buildUpdate(tasks.getItem(position))
+ } ?: continue
+
+ val viewSize = view.estimateParcelSize()
+ if (totalParcelSize + viewSize > MAX_PARCEL_SIZE) {
+ Timber.d("Stopping at position $position due to size limit")
+ truncatedDueToSize = true
+ break
+ }
+
+ items.add(WidgetItem(id, view, isHeader))
+ totalParcelSize += viewSize
+ }
+
+ val builder = RemoteCollectionItems.Builder()
+ .setHasStableIds(true)
+ .setViewTypeCount(VIEW_TYPE_COUNT)
+
+ items.forEach { builder.addItem(it.id, it.view) }
+
+ if (truncatedDueToSize) {
+ builder.addItem(FOOTER_ID, buildFooter())
+ }
+
+ Timber.d("Built ${items.size} items, totalSize=${Formatter.formatShortFileSize(context, totalParcelSize.toLong())}, truncated=$truncatedDueToSize")
+
+ return builder.build()
+ }
+
+ companion object {
+ const val VIEW_TYPE_COUNT = 3
+ const val MAX_ITEMS = 100
+ const val MAX_PARCEL_SIZE = 200_000 // 200KB
+ const val FOOTER_ID = 0L
+ }
}
diff --git a/deps_fdroid.txt b/deps_fdroid.txt
index 45eeb7b44..1cbbb780e 100644
--- a/deps_fdroid.txt
+++ b/deps_fdroid.txt
@@ -1369,6 +1369,10 @@
+| | \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.20 -> 2.2.21 (c)
+| +--- com.google.dagger:hilt-android:2.54 -> 2.57.2 (*)
+| \--- org.jspecify:jspecify:1.0.0
+++--- androidx.core:core-remoteviews:1.1.0
++| +--- androidx.annotation:annotation:1.2.0 -> 1.9.1 (*)
++| +--- androidx.core:core:1.8.0 -> 1.16.0 (*)
++| \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.2.21 (*)
++--- androidx.core:core-splashscreen:1.2.0
+| +--- androidx.annotation:annotation:1.8.1 -> 1.9.1 (*)
+| +--- androidx.appcompat:appcompat-resources:1.7.0 -> 1.7.1 (*)
diff --git a/deps_googleplay.txt b/deps_googleplay.txt
index bb60624ac..4ef98c625 100644
--- a/deps_googleplay.txt
+++ b/deps_googleplay.txt
@@ -1761,6 +1761,10 @@
+| | \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.20 -> 2.2.21 (c)
+| +--- com.google.dagger:hilt-android:2.54 -> 2.57.2 (*)
+| \--- org.jspecify:jspecify:1.0.0
+++--- androidx.core:core-remoteviews:1.1.0
++| +--- androidx.annotation:annotation:1.2.0 -> 1.9.1 (*)
++| +--- androidx.core:core:1.8.0 -> 1.16.0 (*)
++| \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.2.21 (*)
++--- androidx.core:core-splashscreen:1.2.0
+| +--- androidx.annotation:annotation:1.8.1 -> 1.9.1 (*)
+| +--- androidx.appcompat:appcompat-resources:1.7.0 -> 1.7.1 (*)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index be8cdc08b..b90787a37 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -70,6 +70,7 @@ androidx-compose = { module = "androidx.compose:compose-bom", version.ref = "com
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout-android", version = "1.2.0" }
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
+androidx-core-remoteviews = { group = "androidx.core", name = "core-remoteviews", version = "1.1.0" }
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version = "1.2.0" }
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version = "1.2.0" }
androidx-fragment-compose = { module = "androidx.fragment:fragment-compose", version = "1.8.9" }