diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 6806f5a8..bc3e8c4a 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -19,15 +19,19 @@
+
+
+
+
@@ -37,6 +41,9 @@
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 3848bcfc..57cc75e0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -17,8 +17,8 @@ android {
applicationId = "com.eva.recorderapp"
minSdk = 29
targetSdk = 35
- versionCode = 4
- versionName = "1.1.2"
+ versionCode = 5
+ versionName = "1.2.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -40,11 +40,11 @@ android {
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = "1.8"
+ jvmTarget = "17"
}
buildFeatures {
compose = true
@@ -121,6 +121,12 @@ dependencies {
implementation(libs.androidx.datastore)
implementation(libs.protobuf.javalite)
implementation(libs.protobuf.kotlin.lite)
+ //glance
+ implementation(libs.androidx.glance)
+ implementation(libs.androidx.glance.appwidget)
+ implementation(libs.androidx.glance.material3)
+ implementation(libs.androidx.glance.preview)
+ implementation(libs.androidx.glance.appwidget.preview)
// tests
testImplementation(libs.junit)
testImplementation(libs.turbine)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 588d4460..f5f3accf 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -67,6 +67,30 @@
android:exported="false"
android:foregroundServiceType="microphone" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Unit
+ content: @Composable() () -> Unit,
) {
val context = LocalContext.current
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/data/player/AudioFilePlayerListener.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/data/player/AudioFilePlayerListener.kt
index 0f33ad42..39131072 100644
--- a/app/src/main/java/com/eva/recorderapp/voice_recorder/data/player/AudioFilePlayerListener.kt
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/data/player/AudioFilePlayerListener.kt
@@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.update
@@ -94,7 +95,8 @@ class AudioFilePlayerListener(
private fun computeMusicTrackInfo(state: PlayerState): Flow {
return flow {
Log.d(TAG, "CURRENT PLAYER STATE: $state")
-
+ // an empty data to prefill the entry
+ emit(PlayerTrackData())
// If the player can advertise positions ie, its ready or play or paused
// then continue the loop
while (state.canAdvertiseCurrentPosition) {
@@ -102,17 +104,15 @@ class AudioFilePlayerListener(
val current = player.currentPosition.milliseconds
val total = player.duration.milliseconds
- if (current.isNegative() && total.isNegative())
+ if (current.isNegative() || total.isNegative())
continue
- val trackData = PlayerTrackData(
- current = player.currentPosition.milliseconds,
- total = player.duration.milliseconds,
- )
+ val trackData = PlayerTrackData(current = current, total = total)
emit(trackData)
delay(100.milliseconds)
}
- }.distinctUntilChanged()
+ }.filter { it.allPositiveAndFinite }
+ .distinctUntilChanged()
}
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/data/service/VoiceRecorderService.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/data/service/VoiceRecorderService.kt
index 6979f6df..8ff262cc 100644
--- a/app/src/main/java/com/eva/recorderapp/voice_recorder/data/service/VoiceRecorderService.kt
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/data/service/VoiceRecorderService.kt
@@ -19,6 +19,7 @@ import com.eva.recorderapp.voice_recorder.domain.recorder.emums.RecorderAction
import com.eva.recorderapp.voice_recorder.domain.recorder.emums.RecorderState
import com.eva.recorderapp.voice_recorder.domain.use_cases.BluetoothScoUseCase
import com.eva.recorderapp.voice_recorder.domain.use_cases.PhoneStateObserverUsecase
+import com.eva.recorderapp.voice_recorder.domain.util.AppWidgetsRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -40,6 +41,7 @@ import kotlinx.coroutines.launch
import kotlinx.datetime.LocalTime
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
private const val LOGGER_TAG = "RECORDER_SERVICE"
@@ -61,6 +63,9 @@ class VoiceRecorderService : LifecycleService() {
@Inject
lateinit var bookmarksProvider: RecordingBookmarksProvider
+ @Inject
+ lateinit var widgetFacade: AppWidgetsRepository
+
private val binder = LocalBinder()
@@ -69,9 +74,12 @@ class VoiceRecorderService : LifecycleService() {
@OptIn(ExperimentalCoroutinesApi::class)
val bookMarks = _bookMarks.mapLatest { bookMarks ->
// convert it to set such that common items are subtracted
- bookMarks.map(LocalTime::roundToClosestSeconds).toSet().toImmutableList()
+ bookMarks.map(LocalTime::roundToClosestSeconds)
+ .toImmutableList()
}.stateIn(
- scope = lifecycleScope, started = SharingStarted.Lazily, initialValue = persistentListOf()
+ scope = lifecycleScope,
+ started = SharingStarted.Lazily,
+ initialValue = persistentListOf()
)
private val _amplitudes = MutableStateFlow(emptyList>())
@@ -89,6 +97,13 @@ class VoiceRecorderService : LifecycleService() {
private val notificationTimer: Flow
get() = voiceRecorder.recorderTimer.sample(800.milliseconds)
+ /**
+ * Recorder timer sampled per 1 seconds
+ */
+ @OptIn(FlowPreview::class)
+ private val widgetTimer: Flow
+ get() = voiceRecorder.recorderTimer.sample(1.seconds)
+
inner class LocalBinder : Binder() {
fun getService(): VoiceRecorderService = this@VoiceRecorderService
@@ -103,12 +118,11 @@ class VoiceRecorderService : LifecycleService() {
override fun onCreate() {
super.onCreate()
try {
- // read phone states
// preparing the recorder
voiceRecorder.createRecorder()
// check use case
bluetoothScoUseCase.startConnectionIfAllowed()
- // listen to changes
+ // read phone states
observeChangingPhoneState()
// inform bt connect
showBluetoothConnectedToast()
@@ -116,7 +130,8 @@ class VoiceRecorderService : LifecycleService() {
readAmplitudes()
// update the notification
readTimerAndUpdateNotification()
-
+ // update widget state
+ updateRecorderWidgetState()
Log.i(LOGGER_TAG, "SERVICE CREATED WITH OBSERVERS")
} catch (e: Exception) {
e.printStackTrace()
@@ -175,6 +190,13 @@ class VoiceRecorderService : LifecycleService() {
}.launchIn(lifecycleScope)
}
+ private fun updateRecorderWidgetState() {
+ combine(widgetTimer, recorderState) { time, state ->
+ // update the widget
+ widgetFacade.recorderWidgetUpdate(state, time)
+ }.launchIn(lifecycleScope)
+ }
+
private fun onStartRecording() {
//start the recorder
lifecycleScope.launch { voiceRecorder.startRecording() }.invokeOnCompletion {
@@ -212,6 +234,8 @@ class VoiceRecorderService : LifecycleService() {
voiceRecorder.cancelRecording()
//clear bookmarks
clearBookMarks()
+ //update widget
+ widgetFacade.resetRecorderWidget()
}.invokeOnCompletion {
// stop the foreground
stopForeground(STOP_FOREGROUND_REMOVE)
@@ -238,6 +262,8 @@ class VoiceRecorderService : LifecycleService() {
else -> {}
}
+ //update widget
+ widgetFacade.resetRecorderWidget()
}.invokeOnCompletion {
//clear and save bookmarks
// stop the foreground
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/di/AppUtilsModule.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/di/AppUtilsModule.kt
index 3fcc8b89..0637609c 100644
--- a/app/src/main/java/com/eva/recorderapp/voice_recorder/di/AppUtilsModule.kt
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/di/AppUtilsModule.kt
@@ -9,9 +9,11 @@ import com.eva.recorderapp.voice_recorder.data.util.ShareRecordingsUtilImpl
import com.eva.recorderapp.voice_recorder.domain.bookmarks.ExportBookMarkUriProvider
import com.eva.recorderapp.voice_recorder.domain.recorder.RecorderActionHandler
import com.eva.recorderapp.voice_recorder.domain.util.AppShortcutFacade
+import com.eva.recorderapp.voice_recorder.domain.util.AppWidgetsRepository
import com.eva.recorderapp.voice_recorder.domain.util.BluetoothScoConnect
import com.eva.recorderapp.voice_recorder.domain.util.PhoneStateObserver
import com.eva.recorderapp.voice_recorder.domain.util.ShareRecordingsUtil
+import com.eva.recorderapp.voice_recorder.widgets.data.AppWidgetsRepoImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -26,7 +28,7 @@ object AppUtilsModule {
@Provides
@Singleton
fun providesRecorderActionHandler(
- @ApplicationContext context: Context
+ @ApplicationContext context: Context,
): RecorderActionHandler = RecorderActionHandlerImpl(context)
@Provides
@@ -55,4 +57,9 @@ object AppUtilsModule {
exportBookMarkUriProvider: ExportBookMarkUriProvider,
): ShareRecordingsUtil = ShareRecordingsUtilImpl(context, exportBookMarkUriProvider)
+ @Provides
+ @Singleton
+ fun providesWidgetUtils(@ApplicationContext context: Context): AppWidgetsRepository =
+ AppWidgetsRepoImpl(context)
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/di/RecorderRecordingsProvider.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/di/RecorderRecordingsProvider.kt
index 76631fc0..b049cc4e 100644
--- a/app/src/main/java/com/eva/recorderapp/voice_recorder/di/RecorderRecordingsProvider.kt
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/di/RecorderRecordingsProvider.kt
@@ -15,6 +15,8 @@ import com.eva.recorderapp.voice_recorder.domain.recordings.provider.RecordingCa
import com.eva.recorderapp.voice_recorder.domain.recordings.provider.RecordingsSecondaryDataProvider
import com.eva.recorderapp.voice_recorder.domain.recordings.provider.TrashRecordingsProvider
import com.eva.recorderapp.voice_recorder.domain.recordings.provider.VoiceRecordingsProvider
+import com.eva.recorderapp.voice_recorder.domain.use_cases.GetRecordingsOfCurrentAppUseCase
+import com.eva.recorderapp.voice_recorder.domain.util.AppWidgetsRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -66,4 +68,13 @@ object RecorderRecordingsProvider {
): RecordingsSecondaryDataProvider =
RecordingSecondaryDataProviderImpl(context = context, recordingsDao = recordingsMetadataDao)
+ @Provides
+ @Singleton
+ fun providesOwnerRecordingsUseCase(
+ recordings: VoiceRecordingsProvider,
+ secondaryRecordingsData: RecordingsSecondaryDataProvider,
+ widgetFacade: AppWidgetsRepository,
+ ): GetRecordingsOfCurrentAppUseCase =
+ GetRecordingsOfCurrentAppUseCase(recordings, secondaryRecordingsData, widgetFacade)
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/domain/player/model/PlayerTrackData.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/domain/player/model/PlayerTrackData.kt
index 7546d0ee..1b875c0c 100644
--- a/app/src/main/java/com/eva/recorderapp/voice_recorder/domain/player/model/PlayerTrackData.kt
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/domain/player/model/PlayerTrackData.kt
@@ -25,6 +25,9 @@ data class PlayerTrackData(
return ratio.coerceIn(0f, 1f)
}
+ val allPositiveAndFinite: Boolean
+ get() = current.isFinite() && current.isPositive() && total.isFinite() && total.isPositive()
+
fun calculateSeekAmount(seek: Float): Duration {
try {
require(value = seek in 0f..1f)
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/domain/use_cases/GetRecordingsOfCurrentAppUseCase.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/domain/use_cases/GetRecordingsOfCurrentAppUseCase.kt
new file mode 100644
index 00000000..c56ad7c0
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/domain/use_cases/GetRecordingsOfCurrentAppUseCase.kt
@@ -0,0 +1,60 @@
+package com.eva.recorderapp.voice_recorder.domain.use_cases
+
+import com.eva.recorderapp.common.Resource
+import com.eva.recorderapp.voice_recorder.domain.recordings.provider.ExtraRecordingMetaDataList
+import com.eva.recorderapp.voice_recorder.domain.recordings.provider.RecordingsSecondaryDataProvider
+import com.eva.recorderapp.voice_recorder.domain.recordings.provider.ResourcedVoiceRecordingModels
+import com.eva.recorderapp.voice_recorder.domain.recordings.provider.VoiceRecordingModels
+import com.eva.recorderapp.voice_recorder.domain.recordings.provider.VoiceRecordingsProvider
+import com.eva.recorderapp.voice_recorder.domain.util.AppWidgetsRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.onEach
+
+class GetRecordingsOfCurrentAppUseCase(
+ private val recordings: VoiceRecordingsProvider,
+ private val secondaryRecordings: RecordingsSecondaryDataProvider,
+ private val widgetRepository: AppWidgetsRepository,
+) {
+ operator fun invoke(): Flow {
+ return combine(
+ recordings.voiceRecordingsOnlyThisApp,
+ secondaryRecordings.providesRecordingMetaData
+ ) { resource, metadata ->
+ when (resource) {
+ is Resource.Success -> {
+ // emit the merged data
+ val combinedData = combineMetadata(resource.data, metadata)
+ Resource.Success(combinedData)
+ }
+ // on other cases emit that res only
+ else -> resource
+ }
+ }.onEach {
+ // update the widget on each emit
+ widgetRepository.recordingsWidgetUpdate()
+ }
+ }
+
+ private fun combineMetadata(
+ recordings: VoiceRecordingModels,
+ otherMetadata: ExtraRecordingMetaDataList,
+ ): VoiceRecordingModels {
+
+ val recordingsIdWithExtraMetadata = otherMetadata.associateBy { it.recordingId }
+ val recordingsKeys = recordingsIdWithExtraMetadata.keys
+
+ return recordings.map { model ->
+ // if not found return model
+ if (!recordingsKeys.contains(model.id)) return@map model
+ // if found add the extra data
+ recordingsIdWithExtraMetadata.getOrDefault(model.id, null)
+ ?.let { extraData ->
+ model.copy(
+ isFavorite = extraData.isFavourite,
+ categoryId = extraData.categoryId
+ )
+ } ?: model
+ }.sortedBy { it.recordedAt }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/domain/util/AppWidgetsRepository.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/domain/util/AppWidgetsRepository.kt
new file mode 100644
index 00000000..6e233760
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/domain/util/AppWidgetsRepository.kt
@@ -0,0 +1,22 @@
+package com.eva.recorderapp.voice_recorder.domain.util
+
+import com.eva.recorderapp.voice_recorder.domain.recorder.emums.RecorderState
+import kotlinx.datetime.LocalTime
+
+interface AppWidgetsRepository {
+
+ /**
+ * Updates Recordings widget with the current data
+ */
+ suspend fun recordingsWidgetUpdate()
+
+ /**
+ * Updates the recorder widget with [RecorderState] and stopwatch time as [LocalTime]
+ */
+ suspend fun recorderWidgetUpdate(state: RecorderState, time: LocalTime? = null)
+
+ /**
+ * Reset the recorder widget to [RecorderState.IDLE]
+ */
+ suspend fun resetRecorderWidget()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/data/AppWidgetsRepoImpl.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/data/AppWidgetsRepoImpl.kt
new file mode 100644
index 00000000..4ab954f4
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/data/AppWidgetsRepoImpl.kt
@@ -0,0 +1,74 @@
+package com.eva.recorderapp.voice_recorder.widgets.data
+
+import android.content.Context
+import android.util.Log
+import androidx.glance.appwidget.updateAll
+import com.eva.recorderapp.voice_recorder.domain.recorder.emums.RecorderState
+import com.eva.recorderapp.voice_recorder.domain.util.AppWidgetsRepository
+import com.eva.recorderapp.voice_recorder.widgets.recorder.AppRecorderWidget
+import com.eva.recorderapp.voice_recorder.widgets.recorder.RecorderWidgetDefinition
+import com.eva.recorderapp.voice_recorder.widgets.recordings.AppRecordingsWidget
+import com.google.protobuf.Duration
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.datetime.LocalTime
+
+private const val TAG = "UPDATE_APP_WIDGETS"
+
+class AppWidgetsRepoImpl(private val context: Context) : AppWidgetsRepository {
+
+ override suspend fun recordingsWidgetUpdate() {
+ withContext(Dispatchers.Default) {
+ Log.d(TAG, "RECORDINGS_WIDGET_UPDATED")
+ AppRecordingsWidget().updateAll(context)
+ }
+ }
+
+ override suspend fun recorderWidgetUpdate(state: RecorderState, time: LocalTime?) {
+ val duration = Duration.newBuilder()
+ .setSeconds(time?.second?.toLong() ?: 0L).build()
+
+ val recordingMode = when (state) {
+ RecorderState.RECORDING -> RecordingModeProto.RECORDING
+ RecorderState.PAUSED -> RecordingModeProto.PAUSED
+ else -> RecordingModeProto.IDLE_OR_COMPLETED
+ }
+
+ withContext(Dispatchers.IO) {
+ // update the content
+ RecorderWidgetDefinition.getDataStore(context, RecorderWidgetDefinition.FILE_LOCATION)
+ .updateData { content ->
+ content.toBuilder()
+ .setMode(recordingMode)
+ .setDuration(duration)
+ .build()
+ }
+ }
+ // update the widget
+ withContext(Dispatchers.Default) {
+ Log.d(TAG, "RECORDER_WIDGET_UPDATED ")
+ AppRecorderWidget().updateAll(context)
+ }
+ }
+
+ override suspend fun resetRecorderWidget() {
+ val duration = RecorderWidgetDefinition.ZERO_DURATION
+ val recordingMode = RecordingModeProto.IDLE_OR_COMPLETED
+
+ withContext(Dispatchers.IO) {
+ // update the content
+ RecorderWidgetDefinition.getDataStore(context, RecorderWidgetDefinition.FILE_LOCATION)
+ .updateData { content ->
+ content.toBuilder()
+ .setMode(recordingMode)
+ .setDuration(duration)
+ .build()
+ }
+ }
+ // update the widget
+ withContext(Dispatchers.Default) {
+ Log.d(TAG, "RECORDER_WIDGET_RESET ")
+ AppRecorderWidget().updateAll(context)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/data/ProtoToModel.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/data/ProtoToModel.kt
new file mode 100644
index 00000000..7b4c4a2e
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/data/ProtoToModel.kt
@@ -0,0 +1,13 @@
+package com.eva.recorderapp.voice_recorder.widgets.data
+
+import com.eva.recorderapp.voice_recorder.domain.recorder.emums.RecorderState
+import kotlinx.datetime.LocalTime
+
+fun RecorderWidgetDataProto.toModel(): RecorderWidgetModel = RecorderWidgetModel(
+ state = when (mode) {
+ RecordingModeProto.RECORDING -> RecorderState.RECORDING
+ RecordingModeProto.PAUSED -> RecorderState.PAUSED
+ else -> RecorderState.IDLE
+ },
+ time = LocalTime.fromSecondOfDay(duration.seconds.toInt())
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/data/RecorderWidgetModel.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/data/RecorderWidgetModel.kt
new file mode 100644
index 00000000..52a2c23a
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/data/RecorderWidgetModel.kt
@@ -0,0 +1,9 @@
+package com.eva.recorderapp.voice_recorder.widgets.data
+
+import com.eva.recorderapp.voice_recorder.domain.recorder.emums.RecorderState
+import kotlinx.datetime.LocalTime
+
+data class RecorderWidgetModel(
+ val state: RecorderState,
+ val time: LocalTime,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recorder/AppRecorderWidget.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recorder/AppRecorderWidget.kt
new file mode 100644
index 00000000..e8cee47a
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recorder/AppRecorderWidget.kt
@@ -0,0 +1,81 @@
+package com.eva.recorderapp.voice_recorder.widgets.recorder
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.Intent
+import android.widget.RemoteViews
+import androidx.core.content.getSystemService
+import androidx.glance.GlanceId
+import androidx.glance.GlanceModifier
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.SizeMode
+import androidx.glance.appwidget.action.actionStartActivity
+import androidx.glance.appwidget.provideContent
+import androidx.glance.currentState
+import androidx.glance.state.GlanceStateDefinition
+import com.eva.recorderapp.MainActivity
+import com.eva.recorderapp.R
+import com.eva.recorderapp.voice_recorder.presentation.navigation.util.NavDeepLinks
+import com.eva.recorderapp.voice_recorder.widgets.data.RecorderWidgetDataProto
+import com.eva.recorderapp.voice_recorder.widgets.data.toModel
+import com.eva.recorderapp.voice_recorder.widgets.utils.RecorderAppWidgetTheme
+
+
+class AppRecorderWidget : GlanceAppWidget() {
+
+ override val sizeMode: SizeMode
+ get() = SizeMode.Exact
+
+ override val stateDefinition: GlanceStateDefinition
+ get() = RecorderWidgetDefinition
+
+
+ override suspend fun provideGlance(context: Context, id: GlanceId) {
+ provideContent {
+
+ val protoState = currentState()
+
+ RecorderAppWidgetTheme {
+ RecorderWidgetContent(
+ model = protoState.toModel(),
+ modifier = GlanceModifier
+ .clickable(
+ onClick = actionStartActivity(
+ Intent(
+ Intent.ACTION_VIEW,
+ NavDeepLinks.recorderDestinationUri,
+ context,
+ MainActivity::class.java
+ ).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ ),
+ )
+ )
+ }
+ }
+ }
+
+ override fun onCompositionError(
+ context: Context,
+ glanceId: GlanceId,
+ appWidgetId: Int,
+ throwable: Throwable,
+ ) {
+ // print stacktrace
+ throwable.printStackTrace()
+ // update the layout
+ val widgetManager = context.getSystemService() ?: return
+
+ val remoteView = RemoteViews(context.packageName, R.layout.widget_loading_failed_layout)
+ .apply {
+ val message = throwable.message ?: context.getString(R.string.widget_error_text)
+ setTextViewText(R.id.widget_error_description, message)
+ }
+
+ // show the error on the widget
+ widgetManager.updateAppWidget(appWidgetId, remoteView)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recorder/RecorderWidgetContent.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recorder/RecorderWidgetContent.kt
new file mode 100644
index 00000000..4b1226d5
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recorder/RecorderWidgetContent.kt
@@ -0,0 +1,133 @@
+package com.eva.recorderapp.voice_recorder.widgets.recorder
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.glance.ColorFilter
+import androidx.glance.GlanceComposable
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.LocalContext
+import androidx.glance.appwidget.components.Scaffold
+import androidx.glance.appwidget.cornerRadius
+import androidx.glance.background
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.Column
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.height
+import androidx.glance.layout.padding
+import androidx.glance.layout.size
+import androidx.glance.layout.width
+import androidx.glance.layout.wrapContentSize
+import androidx.glance.preview.ExperimentalGlancePreviewApi
+import androidx.glance.preview.Preview
+import androidx.glance.text.FontWeight
+import androidx.glance.text.Text
+import androidx.glance.text.TextStyle
+import com.eva.recorderapp.R
+import com.eva.recorderapp.common.LocalTimeFormats
+import com.eva.recorderapp.voice_recorder.domain.recorder.emums.RecorderState
+import com.eva.recorderapp.voice_recorder.widgets.data.RecorderWidgetModel
+import com.eva.recorderapp.voice_recorder.widgets.utils.RecorderAppWidgetTheme
+import kotlinx.datetime.LocalTime
+import kotlinx.datetime.format
+
+@Composable
+@GlanceComposable
+fun RecorderWidgetContent(
+ model: RecorderWidgetModel,
+ modifier: GlanceModifier = GlanceModifier,
+) {
+ val context = LocalContext.current
+
+ Scaffold(
+ backgroundColor = GlanceTheme.colors.widgetBackground,
+ modifier = modifier,
+ horizontalPadding = 12.dp
+ ) {
+ Row(
+ modifier = GlanceModifier
+ .fillMaxSize()
+ .padding(vertical = 12.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Box(
+ modifier = GlanceModifier
+ .size(48.dp)
+ .cornerRadius(8.dp)
+ .background(GlanceTheme.colors.primaryContainer)
+ .padding(4.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Image(
+ provider = ImageProvider(R.drawable.ic_mic_variant),
+ contentDescription = context.getString(R.string.widget_recorder_widget),
+ modifier = GlanceModifier.size(32.dp),
+ colorFilter = ColorFilter.tint(colorProvider = GlanceTheme.colors.onPrimaryContainer)
+ )
+ }
+ Spacer(modifier = GlanceModifier.width(20.dp))
+ Column(
+ modifier = GlanceModifier.wrapContentSize(),
+ horizontalAlignment = Alignment.Start
+ ) {
+ Text(
+ text = model.time.format(LocalTimeFormats.LOCALTIME_FORMAT_MM_SS),
+ style = TextStyle(
+ color = GlanceTheme.colors.onBackground,
+ fontWeight = FontWeight.Medium,
+ fontSize = 18.sp
+ )
+ )
+ Spacer(modifier = GlanceModifier.height(4.dp))
+
+ val currentState = when (model.state) {
+ RecorderState.RECORDING -> context.getString(R.string.recorder_state_recording)
+ RecorderState.PAUSED -> context.getString(R.string.recorder_state_paused)
+ else -> null
+ }
+
+ currentState?.let {
+ Text(
+ text = it,
+ style = TextStyle(
+ color = GlanceTheme.colors.onSurfaceVariant,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp
+ ),
+ )
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalGlancePreviewApi::class)
+@Preview(heightDp = 130, widthDp = 245)
+@Composable
+private fun RecorderWidgetPreview() = RecorderAppWidgetTheme {
+ RecorderWidgetContent(
+ model = RecorderWidgetModel(
+ state = RecorderState.RECORDING,
+ time = LocalTime.fromSecondOfDay(5)
+ )
+ )
+}
+
+@OptIn(ExperimentalGlancePreviewApi::class)
+@Preview(heightDp = 130, widthDp = 245)
+@Composable
+private fun RecorderWidgetRecordingPreview() = RecorderAppWidgetTheme {
+ RecorderWidgetContent(
+ model = RecorderWidgetModel(
+ state = RecorderState.PAUSED,
+ time = LocalTime.fromSecondOfDay(5)
+ )
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recorder/RecorderWidgetDefinition.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recorder/RecorderWidgetDefinition.kt
new file mode 100644
index 00000000..c6bbea53
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recorder/RecorderWidgetDefinition.kt
@@ -0,0 +1,56 @@
+package com.eva.recorderapp.voice_recorder.widgets.recorder
+
+import android.content.Context
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.Serializer
+import androidx.datastore.dataStore
+import androidx.datastore.dataStoreFile
+import androidx.glance.state.GlanceStateDefinition
+import com.eva.recorderapp.voice_recorder.widgets.data.RecorderWidgetDataProto
+import com.eva.recorderapp.voice_recorder.widgets.data.RecordingModeProto
+import com.eva.recorderapp.voice_recorder.widgets.data.recorderWidgetDataProto
+import com.google.protobuf.Duration
+import com.google.protobuf.InvalidProtocolBufferException
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+
+
+object RecorderWidgetDefinition : GlanceStateDefinition {
+
+ const val FILE_LOCATION = "recorder_data.proto"
+ val ZERO_DURATION: Duration = Duration.newBuilder()
+ .setSeconds(0).build()
+
+ private val Context.recorderData by dataStore(FILE_LOCATION, RecorderDatastoreData)
+
+ override suspend fun getDataStore(
+ context: Context,
+ fileKey: String,
+ ): DataStore = context.recorderData
+
+ override fun getLocation(context: Context, fileKey: String): File {
+ return context.dataStoreFile(FILE_LOCATION)
+ }
+
+ private object RecorderDatastoreData : Serializer {
+
+ override val defaultValue: RecorderWidgetDataProto = recorderWidgetDataProto {
+ mode = RecordingModeProto.IDLE_OR_COMPLETED
+ duration = ZERO_DURATION
+ }
+
+ override suspend fun readFrom(input: InputStream): RecorderWidgetDataProto {
+ try {
+ return RecorderWidgetDataProto.parseFrom(input)
+ } catch (exception: InvalidProtocolBufferException) {
+ throw CorruptionException("Cannot read .proto file", exception)
+ }
+ }
+
+ override suspend fun writeTo(t: RecorderWidgetDataProto, output: OutputStream) =
+ t.writeTo(output)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recorder/RecorderWidgetReceiver.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recorder/RecorderWidgetReceiver.kt
new file mode 100644
index 00000000..3bb4d96a
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recorder/RecorderWidgetReceiver.kt
@@ -0,0 +1,10 @@
+package com.eva.recorderapp.voice_recorder.widgets.recorder
+
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+
+class RecorderWidgetReceiver : GlanceAppWidgetReceiver() {
+
+ override val glanceAppWidget: GlanceAppWidget
+ get() = AppRecorderWidget()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/AppRecordingsWidget.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/AppRecordingsWidget.kt
new file mode 100644
index 00000000..94336713
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/AppRecordingsWidget.kt
@@ -0,0 +1,113 @@
+package com.eva.recorderapp.voice_recorder.widgets.recordings
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import android.widget.RemoteViews
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.core.content.getSystemService
+import androidx.glance.GlanceId
+import androidx.glance.GlanceModifier
+import androidx.glance.action.ActionParameters
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.SizeMode
+import androidx.glance.appwidget.action.ActionCallback
+import androidx.glance.appwidget.action.actionRunCallback
+import androidx.glance.appwidget.action.actionStartActivity
+import androidx.glance.appwidget.provideContent
+import com.eva.recorderapp.MainActivity
+import com.eva.recorderapp.R
+import com.eva.recorderapp.common.Resource
+import com.eva.recorderapp.voice_recorder.domain.use_cases.GetRecordingsOfCurrentAppUseCase
+import com.eva.recorderapp.voice_recorder.presentation.navigation.util.NavDeepLinks
+import com.eva.recorderapp.voice_recorder.widgets.recordings.composables.RecordingsWidgetContent
+import com.eva.recorderapp.voice_recorder.widgets.utils.RecorderAppWidgetTheme
+import dagger.hilt.EntryPoint
+import dagger.hilt.EntryPoints
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+private const val TAG = "APP_RECORDINGS_WIDGET"
+
+class AppRecordingsWidget : GlanceAppWidget(R.layout.widget_loading_failed_layout) {
+
+ @EntryPoint
+ @InstallIn(SingletonComponent::class)
+ interface GlanceWidgetEntryPoint {
+ // a custom entry point.
+ fun providesOwnRecordingsUseCase(): GetRecordingsOfCurrentAppUseCase
+ }
+
+ override val sizeMode: SizeMode
+ get() = SizeMode.Exact
+
+ override suspend fun provideGlance(context: Context, id: GlanceId) {
+
+ val entryPoint =
+ EntryPoints.get(context.applicationContext, GlanceWidgetEntryPoint::class.java)
+
+ val recordingsProvider = entryPoint.providesOwnRecordingsUseCase()
+
+ provideContent {
+
+ val resource by recordingsProvider.invoke().collectAsState(initial = Resource.Loading)
+
+ RecorderAppWidgetTheme {
+ RecordingsWidgetContent(
+ resource = resource,
+ onRefresh = { actionRunCallback() },
+ modifier = GlanceModifier.clickable(
+ onClick = actionStartActivity(
+ Intent(
+ Intent.ACTION_VIEW,
+ NavDeepLinks.recordingsDestinationUri,
+ context,
+ MainActivity::class.java
+ ).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ )
+ )
+ )
+ }
+ }
+ }
+
+ override fun onCompositionError(
+ context: Context,
+ glanceId: GlanceId,
+ appWidgetId: Int,
+ throwable: Throwable,
+ ) {
+ // print stacktrace
+ throwable.printStackTrace()
+ // update the layout
+ val widgetManager = context.getSystemService() ?: return
+
+ val remoteView = RemoteViews(
+ context.packageName,
+ R.layout.widget_loading_failed_layout
+ ).apply {
+ val message = throwable.message ?: context.getString(R.string.widget_error_text)
+ setTextViewText(R.id.widget_error_description, message)
+ }
+
+ // show the error on the widget
+ widgetManager.updateAppWidget(appWidgetId, remoteView)
+ }
+}
+
+
+private class RefreshAction : ActionCallback {
+ override suspend fun onAction(
+ context: Context,
+ glanceId: GlanceId,
+ parameters: ActionParameters,
+ ) {
+ Log.d(TAG, "REFRESH CALLED")
+ AppRecordingsWidget().update(context, glanceId)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/RecordingsWidgetReceiver.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/RecordingsWidgetReceiver.kt
new file mode 100644
index 00000000..51443ad1
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/RecordingsWidgetReceiver.kt
@@ -0,0 +1,11 @@
+package com.eva.recorderapp.voice_recorder.widgets.recordings
+
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+
+
+class RecordingsWidgetReceiver : GlanceAppWidgetReceiver() {
+
+ override val glanceAppWidget: GlanceAppWidget
+ get() = AppRecordingsWidget()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/GlancePreviewRecordings.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/GlancePreviewRecordings.kt
new file mode 100644
index 00000000..37df4cb2
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/GlancePreviewRecordings.kt
@@ -0,0 +1,8 @@
+package com.eva.recorderapp.voice_recorder.widgets.recordings.composables
+
+import androidx.glance.preview.ExperimentalGlancePreviewApi
+import androidx.glance.preview.Preview
+
+@OptIn(ExperimentalGlancePreviewApi::class)
+@Preview(heightDp = 276, widthDp = 306)
+annotation class GlancePreviewRecordings
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/RecordingWidgetCard.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/RecordingWidgetCard.kt
new file mode 100644
index 00000000..08a3d2f9
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/RecordingWidgetCard.kt
@@ -0,0 +1,109 @@
+package com.eva.recorderapp.voice_recorder.widgets.recordings.composables
+
+import android.text.format.Formatter
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.glance.ColorFilter
+import androidx.glance.GlanceComposable
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.LocalContext
+import androidx.glance.background
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.Column
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.padding
+import androidx.glance.layout.size
+import androidx.glance.layout.width
+import androidx.glance.layout.wrapContentHeight
+import androidx.glance.text.FontWeight
+import androidx.glance.text.Text
+import androidx.glance.text.TextStyle
+import com.eva.recorderapp.R
+import com.eva.recorderapp.common.LocalTimeFormats.NOTIFICATION_TIMER_TIME_FORMAT
+import com.eva.recorderapp.voice_recorder.domain.recordings.models.RecordedVoiceModel
+import com.eva.recorderapp.voice_recorder.widgets.utils.maybeCornerRadius
+import kotlinx.datetime.format
+
+@GlanceComposable
+@Composable
+fun RecordingWidgetCard(
+ model: RecordedVoiceModel,
+ modifier: GlanceModifier = GlanceModifier,
+) {
+
+ val context = LocalContext.current
+
+ val fileSize = remember(model.sizeInBytes) {
+ Formatter.formatShortFileSize(context, model.sizeInBytes)
+ }
+
+ Row(
+ modifier = modifier
+ .padding(horizontal = 4.dp, vertical = 2.dp)
+ .maybeCornerRadius(16.dp, resId = R.drawable.rounded_shape_primary_cont_color)
+ .background(GlanceTheme.colors.primaryContainer),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = GlanceModifier
+ .size(28.dp)
+ .maybeCornerRadius(8.dp, resId = R.drawable.rounded_shape_primary_color)
+ .background(GlanceTheme.colors.primary)
+ .padding(4.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Image(
+ provider = ImageProvider(R.drawable.ic_widget_mic),
+ contentDescription = null,
+ modifier = GlanceModifier.size(24.dp),
+ colorFilter = ColorFilter.tint(colorProvider = GlanceTheme.colors.onPrimary)
+ )
+ }
+ Spacer(modifier = GlanceModifier.width(12.dp))
+ Column(modifier = GlanceModifier.defaultWeight()) {
+ Text(
+ text = model.title,
+ style = TextStyle(
+ color = GlanceTheme.colors.onPrimaryContainer,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ ),
+ maxLines = 1,
+ )
+ Row(modifier = GlanceModifier.wrapContentHeight()) {
+ Text(
+ text = model.durationAsLocaltime.format(NOTIFICATION_TIMER_TIME_FORMAT),
+ style = TextStyle(
+ color = GlanceTheme.colors.onPrimaryContainer,
+ fontWeight = FontWeight.Normal,
+ fontSize = 10.sp
+ ),
+ )
+ Spacer(modifier = GlanceModifier.width(12.dp))
+ Text(
+ text = fileSize,
+ style = TextStyle(
+ color = GlanceTheme.colors.onPrimaryContainer,
+ fontWeight = FontWeight.Normal,
+ fontSize = 10.sp
+ ),
+ )
+ }
+ }
+ if (model.isFavorite) {
+ Image(
+ provider = ImageProvider(R.drawable.ic_star_outlined),
+ contentDescription = context.getString(R.string.menu_option_favourite),
+ modifier = GlanceModifier.size(16.dp),
+ colorFilter = ColorFilter.tint(colorProvider = GlanceTheme.colors.onPrimaryContainer)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/RecordingsList.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/RecordingsList.kt
new file mode 100644
index 00000000..ed34cc37
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/RecordingsList.kt
@@ -0,0 +1,112 @@
+package com.eva.recorderapp.voice_recorder.widgets.recordings.composables
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.glance.ColorFilter
+import androidx.glance.GlanceComposable
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.LocalContext
+import androidx.glance.appwidget.lazy.LazyColumn
+import androidx.glance.appwidget.lazy.itemsIndexed
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.Column
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.fillMaxWidth
+import androidx.glance.layout.height
+import androidx.glance.layout.padding
+import androidx.glance.layout.size
+import androidx.glance.text.FontWeight
+import androidx.glance.text.Text
+import androidx.glance.text.TextStyle
+import com.eva.recorderapp.R
+import com.eva.recorderapp.voice_recorder.domain.recordings.models.RecordedVoiceModel
+import com.eva.recorderapp.voice_recorder.presentation.util.PreviewFakes
+import com.eva.recorderapp.voice_recorder.widgets.utils.RecorderAppWidgetTheme
+
+@GlanceComposable
+@Composable
+fun RecordingsList(
+ recordings: List,
+ modifier: GlanceModifier = GlanceModifier,
+) {
+ val context = LocalContext.current
+
+ if (recordings.isEmpty()) {
+ Column(
+ modifier = modifier,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ provider = ImageProvider(R.drawable.ic_recorder),
+ contentDescription = context.getString(R.string.no_recordings),
+ colorFilter = ColorFilter.tint(GlanceTheme.colors.secondary),
+ modifier = GlanceModifier.size(48.dp)
+ )
+ Spacer(modifier = GlanceModifier.height(4.dp))
+ Text(
+ text = context.getString(R.string.widget_recordings_no_recordings),
+ style = TextStyle(
+ color = GlanceTheme.colors.tertiary,
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp
+ ),
+ )
+ }
+ } else {
+ LazyColumn(modifier = modifier) {
+ itemsIndexed(
+ items = recordings,
+ itemId = { _, item -> item.id },
+ ) { _, item ->
+ Box(
+ modifier = GlanceModifier
+ .padding(vertical = 4.dp)
+ .fillMaxWidth()
+ ) {
+ RecordingWidgetCard(
+ model = item,
+ modifier = GlanceModifier.fillMaxWidth()
+ )
+ }
+ }
+ }
+ }
+}
+
+
+@GlancePreviewRecordings
+@Composable
+private fun RecordingsContentPreviewWithoutFavourite() =
+ RecorderAppWidgetTheme {
+ RecordingsList(
+ recordings = PreviewFakes.FAKE_VOICE_RECORDING_MODELS.map { it.recoding },
+ modifier = GlanceModifier.fillMaxSize()
+ )
+ }
+
+@GlancePreviewRecordings
+@Composable
+private fun RecordingsContentPreviewWithFavourite() =
+ RecorderAppWidgetTheme {
+ RecordingsList(
+ recordings = PreviewFakes.FAKE_VOICE_RECORDINGS_SELECTED.map { it.recoding },
+ modifier = GlanceModifier.fillMaxSize()
+ )
+ }
+
+@GlancePreviewRecordings
+@Composable
+private fun RecordingsContentPreviewEmpty() =
+ RecorderAppWidgetTheme {
+ RecordingsList(
+ recordings = emptyList(),
+ modifier = GlanceModifier.fillMaxSize()
+ )
+ }
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/RecordingsLoadError.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/RecordingsLoadError.kt
new file mode 100644
index 00000000..b6b98130
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/RecordingsLoadError.kt
@@ -0,0 +1,69 @@
+package com.eva.recorderapp.voice_recorder.widgets.recordings.composables
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.glance.ColorFilter
+import androidx.glance.GlanceComposable
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.LocalContext
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Column
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.height
+import androidx.glance.layout.size
+import androidx.glance.text.FontWeight
+import androidx.glance.text.Text
+import androidx.glance.text.TextStyle
+import com.eva.recorderapp.R
+import com.eva.recorderapp.voice_recorder.widgets.utils.RecorderAppWidgetTheme
+
+@Composable
+@GlanceComposable
+fun RecordingsLoadError(
+ message: String?, modifier: GlanceModifier = GlanceModifier,
+) {
+
+ val context = LocalContext.current
+
+ Column(
+ modifier = modifier,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ provider = ImageProvider(R.drawable.ic_widget_error),
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(colorProvider = GlanceTheme.colors.secondary),
+ modifier = GlanceModifier.size(28.dp)
+ )
+ Spacer(modifier = GlanceModifier.height(8.dp))
+ Text(
+ text = context.getString(R.string.recordings_load_failed),
+ style = TextStyle(
+ color = GlanceTheme.colors.primary,
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp
+ )
+ )
+ Spacer(modifier = GlanceModifier.height(4.dp))
+ Text(
+ text = message ?: context.getString(R.string.widget_error_text),
+ style = TextStyle(
+ color = GlanceTheme.colors.onSurfaceVariant,
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp
+ )
+ )
+ }
+}
+
+@GlancePreviewRecordings
+@Composable
+private fun RecordingsLoadErrorPreview() = RecorderAppWidgetTheme {
+ RecordingsLoadError(message = "Failed to load", modifier = GlanceModifier.fillMaxSize())
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/RecordingsWidgetContent.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/RecordingsWidgetContent.kt
new file mode 100644
index 00000000..3d6f5562
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/recordings/composables/RecordingsWidgetContent.kt
@@ -0,0 +1,85 @@
+package com.eva.recorderapp.voice_recorder.widgets.recordings.composables
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import androidx.glance.GlanceComposable
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.ImageProvider
+import androidx.glance.LocalContext
+import androidx.glance.appwidget.CircularProgressIndicator
+import androidx.glance.appwidget.components.CircleIconButton
+import androidx.glance.appwidget.components.Scaffold
+import androidx.glance.appwidget.components.TitleBar
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.fillMaxSize
+import com.eva.recorderapp.R
+import com.eva.recorderapp.common.Resource
+import com.eva.recorderapp.voice_recorder.domain.recordings.provider.ResourcedVoiceRecordingModels
+import com.eva.recorderapp.voice_recorder.presentation.util.PreviewFakes
+import com.eva.recorderapp.voice_recorder.widgets.utils.RecorderAppWidgetTheme
+
+@Composable
+@GlanceComposable
+fun RecordingsWidgetContent(
+ resource: ResourcedVoiceRecordingModels,
+ onRefresh: () -> Unit = {},
+ modifier: GlanceModifier = GlanceModifier,
+) {
+ val context = LocalContext.current
+
+ Scaffold(
+ titleBar = {
+ TitleBar(
+ startIcon = ImageProvider(R.drawable.ic_recorder),
+ title = context.getString(R.string.recording_top_bar_title),
+ actions = {
+ CircleIconButton(
+ imageProvider = ImageProvider(R.drawable.ic_widget_refresh),
+ contentDescription = context.getString(R.string.widget_refresh),
+ onClick = onRefresh,
+ backgroundColor = GlanceTheme.colors.widgetBackground,
+ contentColor = GlanceTheme.colors.primary,
+ )
+ },
+ iconColor = GlanceTheme.colors.primary,
+ textColor = GlanceTheme.colors.onSurface,
+ )
+ },
+ backgroundColor = GlanceTheme.colors.widgetBackground,
+ horizontalPadding = 10.dp,
+ modifier = modifier,
+ ) {
+ when (resource) {
+ Resource.Loading -> {
+ Box(
+ modifier = GlanceModifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(color = GlanceTheme.colors.secondary)
+ }
+ }
+
+ is Resource.Error -> RecordingsLoadError(
+ message = resource.message,
+ modifier = GlanceModifier.fillMaxSize()
+ )
+
+ is Resource.Success -> RecordingsList(
+ recordings = resource.data,
+ modifier = GlanceModifier.fillMaxSize()
+ )
+ }
+ }
+}
+
+
+@GlancePreviewRecordings
+@Composable
+private fun RecordingsContentPreviewResourceSuccess() = RecorderAppWidgetTheme {
+ RecordingsWidgetContent(
+ resource = Resource.Success(data = PreviewFakes.FAKE_VOICE_RECORDING_MODELS.map { it.recoding }),
+ onRefresh = {},
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/utils/MaybeCornerRadius.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/utils/MaybeCornerRadius.kt
new file mode 100644
index 00000000..81475baf
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/utils/MaybeCornerRadius.kt
@@ -0,0 +1,22 @@
+package com.eva.recorderapp.voice_recorder.widgets.utils
+
+import android.os.Build
+import androidx.annotation.DrawableRes
+import androidx.compose.ui.unit.Dp
+import androidx.glance.GlanceModifier
+import androidx.glance.ImageProvider
+import androidx.glance.appwidget.cornerRadius
+import androidx.glance.background
+
+fun GlanceModifier.maybeCornerRadius(cornerRadius: Dp, @DrawableRes resId: Int)
+ : GlanceModifier {
+ val modifier = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
+ cornerRadius(cornerRadius)
+ else GlanceModifier.background(ImageProvider(resId))
+ return then(modifier)
+}
+
+fun GlanceModifier.maybeCornerRadius(@DrawableRes resId: Int)
+ : GlanceModifier =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) GlanceModifier
+ else then(GlanceModifier.background(ImageProvider(resId)))
\ No newline at end of file
diff --git a/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/utils/RecorderAppWidgetTheme.kt b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/utils/RecorderAppWidgetTheme.kt
new file mode 100644
index 00000000..71cd8806
--- /dev/null
+++ b/app/src/main/java/com/eva/recorderapp/voice_recorder/widgets/utils/RecorderAppWidgetTheme.kt
@@ -0,0 +1,30 @@
+package com.eva.recorderapp.voice_recorder.widgets.utils
+
+import android.os.Build
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.glance.GlanceTheme
+import androidx.glance.LocalContext
+import androidx.glance.material3.ColorProviders
+import com.eva.recorderapp.ui.theme.darkScheme
+import com.eva.recorderapp.ui.theme.lightScheme
+
+@Composable
+fun RecorderAppWidgetTheme(
+ dynamicColor: Boolean = true,
+ content: @Composable() () -> Unit,
+) {
+ val context = LocalContext.current
+
+ val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && dynamicColor)
+ ColorProviders(
+ light = dynamicLightColorScheme(context),
+ dark = dynamicDarkColorScheme(context)
+ ) else ColorProviders(light = lightScheme, dark = darkScheme)
+
+ GlanceTheme(
+ colors = colorScheme,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/proto/RecorderWidgetData.proto b/app/src/main/proto/RecorderWidgetData.proto
new file mode 100644
index 00000000..048f9bb7
--- /dev/null
+++ b/app/src/main/proto/RecorderWidgetData.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+option java_package = "com.eva.recorderapp.voice_recorder.widgets.data";
+option java_multiple_files = true;
+option java_generate_equals_and_hash = true;
+
+import "google/protobuf/duration.proto";
+
+message RecorderWidgetDataProto{
+ RecordingModeProto mode = 1;
+ google.protobuf.Duration duration = 2;
+}
+
+enum RecordingModeProto{
+ IDLE_OR_COMPLETED = 0;
+ RECORDING = 1;
+ PAUSED = 2;
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_mic_variant.xml b/app/src/main/res/drawable/ic_mic_variant.xml
new file mode 100644
index 00000000..8218deff
--- /dev/null
+++ b/app/src/main/res/drawable/ic_mic_variant.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_recording_load_failed.xml b/app/src/main/res/drawable/ic_recording_load_failed.xml
new file mode 100644
index 00000000..c7326677
--- /dev/null
+++ b/app/src/main/res/drawable/ic_recording_load_failed.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_widget_error.xml b/app/src/main/res/drawable/ic_widget_error.xml
new file mode 100644
index 00000000..3ea248d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_widget_error.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_widget_mic.xml b/app/src/main/res/drawable/ic_widget_mic.xml
new file mode 100644
index 00000000..051a6873
--- /dev/null
+++ b/app/src/main/res/drawable/ic_widget_mic.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_widget_refresh.xml b/app/src/main/res/drawable/ic_widget_refresh.xml
new file mode 100644
index 00000000..d37c9fb5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_widget_refresh.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_shape_primary_color.xml b/app/src/main/res/drawable/rounded_shape_primary_color.xml
new file mode 100644
index 00000000..1c485885
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_shape_primary_color.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/rounded_shape_primary_cont_color.xml b/app/src/main/res/drawable/rounded_shape_primary_cont_color.xml
new file mode 100644
index 00000000..7f17d943
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_shape_primary_cont_color.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/widget_base_shape.xml b/app/src/main/res/drawable/widget_base_shape.xml
new file mode 100644
index 00000000..893fbc96
--- /dev/null
+++ b/app/src/main/res/drawable/widget_base_shape.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/recorder_widget_preview_layout.xml b/app/src/main/res/layout/recorder_widget_preview_layout.xml
new file mode 100644
index 00000000..d10276ff
--- /dev/null
+++ b/app/src/main/res/layout/recorder_widget_preview_layout.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/recordings_widget_preview_layout.xml b/app/src/main/res/layout/recordings_widget_preview_layout.xml
new file mode 100644
index 00000000..3bed02fa
--- /dev/null
+++ b/app/src/main/res/layout/recordings_widget_preview_layout.xml
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_loading_failed_layout.xml b/app/src/main/res/layout/widget_loading_failed_layout.xml
new file mode 100644
index 00000000..a834bf47
--- /dev/null
+++ b/app/src/main/res/layout/widget_loading_failed_layout.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night-v31/colors.xml b/app/src/main/res/values-night-v31/colors.xml
index 47455534..5c349605 100644
--- a/app/src/main/res/values-night-v31/colors.xml
+++ b/app/src/main/res/values-night-v31/colors.xml
@@ -1,7 +1,9 @@
@android:color/system_neutral1_900
-
+ @android:color/system_neutral1_100
+ @android:color/system_accent1_200
+ @android:color/system_accent1_800
@android:color/system_accent1_700
@android:color/system_accent1_100
\ No newline at end of file
diff --git a/app/src/main/res/values-night-v34/colors.xml b/app/src/main/res/values-night-v34/colors.xml
index 94a5ed16..5b71504f 100644
--- a/app/src/main/res/values-night-v34/colors.xml
+++ b/app/src/main/res/values-night-v34/colors.xml
@@ -1,7 +1,9 @@
@android:color/system_surface_container_dark
-
+ @android:color/system_on_surface_dark
+ @android:color/system_primary_dark
+ @android:color/system_on_primary_dark
@android:color/system_primary_container_dark
@android:color/system_on_primary_container_dark
\ No newline at end of file
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 1f3b78d1..f82ef557 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -1,7 +1,11 @@
@color/white
+
#FF1A1B1F
+ #FFE4E3D7
+ #FF8FD5AF
+ #FF003823
#FF005235
#FFAAF2CA
diff --git a/app/src/main/res/values-v31/colors.xml b/app/src/main/res/values-v31/colors.xml
index 046b1acc..a197f61d 100644
--- a/app/src/main/res/values-v31/colors.xml
+++ b/app/src/main/res/values-v31/colors.xml
@@ -1,7 +1,9 @@
@color/white
-
+ @android:color/system_neutral1_900
+ @android:color/system_accent1_600
+ @android:color/system_accent1_0
@android:color/system_accent1_100
@android:color/system_accent1_900
diff --git a/app/src/main/res/values-v34/colors.xml b/app/src/main/res/values-v34/colors.xml
index 63eaf25b..906f7ed8 100644
--- a/app/src/main/res/values-v34/colors.xml
+++ b/app/src/main/res/values-v34/colors.xml
@@ -1,7 +1,9 @@
@android:color/system_surface_container_light
-
+ @android:color/system_on_surface_light
+ @android:color/system_primary_light
+ @android:color/system_on_primary_light
@android:color/system_primary_container_light
@android:color/system_on_primary_container_light
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 0ad2bee9..8ca2f801 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -6,8 +6,9 @@
#DCDCDC
#FFFDFBFF
-
-
+ #FF1B1C15
+ #FF246A4B
+ #FFFFFFFF
#FFAAF2CA
#FF002113
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index aaa4484d..9362f929 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -15,4 +15,15 @@
560dp
24dp
300dp
+
+ 109dp
+ 306dp
+ 115dp
+ 276dp
+
+ 109dp
+ 48dp
+ 306dp
+ 130dp
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 197f3b43..2296777a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -202,4 +202,18 @@
Write request accepted
Rejected
Other App
+
+ Refresh
+ Cannot load the widget due to something
+ Failed to load widget
+ Cannot find any recordings to show
+ Sneak peek at your recording at a glance
+ No recordings
+ Recording_001
+ 00:00
+ Cannot load recordings
+ Start recording with a button click
+ Recording
+ Paused
+ Recorder Widget
\ No newline at end of file
diff --git a/app/src/main/res/xml-v31/recorder_widget.xml b/app/src/main/res/xml-v31/recorder_widget.xml
new file mode 100644
index 00000000..12f9a43c
--- /dev/null
+++ b/app/src/main/res/xml-v31/recorder_widget.xml
@@ -0,0 +1,14 @@
+
+
+
diff --git a/app/src/main/res/xml-v31/recordings_widget_provider.xml b/app/src/main/res/xml-v31/recordings_widget_provider.xml
new file mode 100644
index 00000000..ffa0611f
--- /dev/null
+++ b/app/src/main/res/xml-v31/recordings_widget_provider.xml
@@ -0,0 +1,15 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/recorder_widget.xml b/app/src/main/res/xml/recorder_widget.xml
new file mode 100644
index 00000000..98e7681a
--- /dev/null
+++ b/app/src/main/res/xml/recorder_widget.xml
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/recordings_widget_provider.xml b/app/src/main/res/xml/recordings_widget_provider.xml
new file mode 100644
index 00000000..13b4cdbd
--- /dev/null
+++ b/app/src/main/res/xml/recordings_widget_provider.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5628fd20..9688860c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,6 +3,7 @@ agp = "8.6.1"
assertk = "0.28.1"
datastore = "1.1.1"
coreSplashscreen = "1.0.1"
+glance = "1.1.0"
graphicsShapes = "1.0.1"
hiltNavigation = "1.2.0"
kotlin = "2.0.20"
@@ -17,16 +18,16 @@ kotlinxDatetime = "0.6.1"
kotlinxSerializationJson = "1.7.3"
lifecycleRuntimeKtx = "2.8.6"
activityCompose = "1.9.2"
-composeBom = "2024.09.02"
+composeBom = "2024.09.03"
ksp = "2.0.20-1.0.25"
hilt = "2.52"
-materialIconsExtendedVersion = "1.7.2"
+materialIconsExtendedVersion = "1.7.3"
media3Common = "1.4.1"
-navigationCompose = "2.8.1"
+navigationCompose = "2.8.2"
playServicesLocationVersion = "21.3.0"
roomCompiler = "2.6.1"
turbine = "1.1.0"
-uiTextGoogleFonts = "1.7.2"
+uiTextGoogleFonts = "1.7.3"
workRuntimeKtxVersion = "2.9.1"
hiltWork = "1.2.0"
protobufJavalite = "4.28.2"
@@ -35,6 +36,11 @@ protobuf_version = "0.9.4"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
+androidx-glance = { module = "androidx.glance:glance", version.ref = "glance" }
+androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" }
+androidx-glance-appwidget-preview = { module = "androidx.glance:glance-appwidget-preview", version.ref = "glance" }
+androidx-glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "glance" }
+androidx-glance-preview = { module = "androidx.glance:glance-preview", version.ref = "glance" }
androidx-graphics-shapes = { module = "androidx.graphics:graphics-shapes", version.ref = "graphicsShapes" }
androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltNavigation" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigation" }