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" }