diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f10f9d77f..622addad43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - [Feature] Infrared Editor process error - [Feature] Optimization FapHub by compose metrics +- [Feature] Firmware update notification - [FIX] Splashscreen WearOS icon - [FIX] Handle expired link diff --git a/build.gradle.kts b/build.gradle.kts index 5721505269..4e818fcf52 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,5 +6,6 @@ plugins { alias(libs.plugins.kotlin.ksp) apply false alias(libs.plugins.square.anvil) apply false alias(libs.plugins.protobuf) apply false + alias(libs.plugins.google.gms) apply false id("flipper.lint") } diff --git a/components/bottombar/impl/build.gradle.kts b/components/bottombar/impl/build.gradle.kts index 9b38b9423d..0eec4cedea 100644 --- a/components/bottombar/impl/build.gradle.kts +++ b/components/bottombar/impl/build.gradle.kts @@ -24,10 +24,12 @@ dependencies { implementation(projects.components.hub.api) implementation(projects.components.deeplink.api) implementation(projects.components.unhandledexception.api) + implementation(projects.components.notification.api) implementation(libs.kotlin.serialization.json) implementation(libs.appcompat) + implementation(libs.tangle.viewmodel.compose) implementation(libs.tangle.viewmodel.api) anvil(libs.tangle.viewmodel.compiler) diff --git a/components/bottombar/impl/src/main/java/com/flipperdevices/bottombar/impl/api/BottomNavigationFeatureEntryImpl.kt b/components/bottombar/impl/src/main/java/com/flipperdevices/bottombar/impl/api/BottomNavigationFeatureEntryImpl.kt index 04bb7dc68d..4e4f1bc882 100644 --- a/components/bottombar/impl/src/main/java/com/flipperdevices/bottombar/impl/api/BottomNavigationFeatureEntryImpl.kt +++ b/components/bottombar/impl/src/main/java/com/flipperdevices/bottombar/impl/api/BottomNavigationFeatureEntryImpl.kt @@ -18,6 +18,7 @@ import com.flipperdevices.core.di.provideDelegate import com.flipperdevices.core.ui.navigation.AggregateFeatureEntry import com.flipperdevices.core.ui.navigation.ComposableFeatureEntry import com.flipperdevices.inappnotification.api.InAppNotificationRenderer +import com.flipperdevices.notification.api.FlipperAppNotificationDialogApi import com.flipperdevices.unhandledexception.api.UnhandledExceptionRenderApi import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesMultibinding @@ -35,7 +36,8 @@ class BottomNavigationFeatureEntryImpl @Inject constructor( composableEntriesProvider: Provider>, private val connectionApi: ConnectionApi, private val notificationRenderer: InAppNotificationRenderer, - private val unhandledExceptionRendererApi: UnhandledExceptionRenderApi + private val unhandledExceptionRendererApi: UnhandledExceptionRenderApi, + private val appNotificationApi: FlipperAppNotificationDialogApi ) : BottomNavigationFeatureEntry, BottomNavigationHandleDeeplink { private val featureEntriesMutable by featureEntriesProvider private val composableEntriesMutable by composableEntriesProvider @@ -53,6 +55,7 @@ class BottomNavigationFeatureEntryImpl @Inject constructor( featureEntries = featureEntriesMutable.toPersistentSet(), composableEntries = composableEntriesMutable.toPersistentSet(), notificationRenderer = notificationRenderer, + appNotificationApi = appNotificationApi, unhandledExceptionRendererApi = unhandledExceptionRendererApi, navController = childNavController, onTabClick = { tab, force -> onChangeTab(tab, force) } diff --git a/components/bottombar/impl/src/main/java/com/flipperdevices/bottombar/impl/composable/ComposableMainScreen.kt b/components/bottombar/impl/src/main/java/com/flipperdevices/bottombar/impl/composable/ComposableMainScreen.kt index 3ba77a07ef..4204a4140e 100644 --- a/components/bottombar/impl/src/main/java/com/flipperdevices/bottombar/impl/composable/ComposableMainScreen.kt +++ b/components/bottombar/impl/src/main/java/com/flipperdevices/bottombar/impl/composable/ComposableMainScreen.kt @@ -23,6 +23,7 @@ import com.flipperdevices.core.ui.navigation.AggregateFeatureEntry import com.flipperdevices.core.ui.navigation.ComposableFeatureEntry import com.flipperdevices.core.ui.theme.LocalPallet import com.flipperdevices.inappnotification.api.InAppNotificationRenderer +import com.flipperdevices.notification.api.FlipperAppNotificationDialogApi import com.flipperdevices.unhandledexception.api.UnhandledExceptionRenderApi import com.google.accompanist.systemuicontroller.rememberSystemUiController import kotlinx.collections.immutable.ImmutableSet @@ -33,6 +34,7 @@ fun ComposableMainScreen( connectionApi: ConnectionApi, notificationRenderer: InAppNotificationRenderer, unhandledExceptionRendererApi: UnhandledExceptionRenderApi, + appNotificationApi: FlipperAppNotificationDialogApi, featureEntries: ImmutableSet, composableEntries: ImmutableSet, navController: NavHostController, @@ -88,6 +90,7 @@ fun ComposableMainScreen( notificationRenderer = notificationRenderer ) connectionApi.CheckAndShowUnsupportedDialog() + appNotificationApi.NotificationDialog() unhandledExceptionRendererApi.ComposableUnhandledExceptionRender(Modifier) } } diff --git a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/utils/PermissionHelper.kt b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/utils/PermissionHelper.kt index 04038fce6d..6a1c518b5b 100644 --- a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/utils/PermissionHelper.kt +++ b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/utils/PermissionHelper.kt @@ -21,7 +21,7 @@ object PermissionHelper { } /** - * @return true if all permissions granted + * @return empty if all permissions granted */ fun getUngrantedPermission(context: Context, permissions: Array): List { val ungrantedPermission = mutableListOf() diff --git a/components/core/activityholder/build.gradle.kts b/components/core/activityholder/build.gradle.kts index 81ea1bb7c6..50934aa55d 100644 --- a/components/core/activityholder/build.gradle.kts +++ b/components/core/activityholder/build.gradle.kts @@ -5,4 +5,5 @@ plugins { android.namespace = "com.flipperdevices.core.activityholder" dependencies { + implementation(libs.appcompat) } diff --git a/components/core/permission/api/.gitignore b/components/core/permission/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/components/core/permission/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/components/core/permission/api/build.gradle.kts b/components/core/permission/api/build.gradle.kts new file mode 100644 index 0000000000..43e460f023 --- /dev/null +++ b/components/core/permission/api/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("flipper.android-lib") +} + +android.namespace = "com.flipperdevices.core.permission.api" + +dependencies { +} diff --git a/components/core/permission/api/src/main/java/com/flipperdevices/core/permission/api/PermissionRequestHandler.kt b/components/core/permission/api/src/main/java/com/flipperdevices/core/permission/api/PermissionRequestHandler.kt new file mode 100644 index 0000000000..cddf4401be --- /dev/null +++ b/components/core/permission/api/src/main/java/com/flipperdevices/core/permission/api/PermissionRequestHandler.kt @@ -0,0 +1,7 @@ +package com.flipperdevices.core.permission.api + +typealias PermissionListener = (String, Boolean) -> Unit + +interface PermissionRequestHandler { + fun requestPermission(vararg permissions: String, listener: PermissionListener) +} diff --git a/components/core/permission/impl/.gitignore b/components/core/permission/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/components/core/permission/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/components/core/permission/impl/build.gradle.kts b/components/core/permission/impl/build.gradle.kts new file mode 100644 index 0000000000..e87b714cc8 --- /dev/null +++ b/components/core/permission/impl/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("flipper.android-lib") + id("flipper.anvil") +} + +android.namespace = "com.flipperdevices.core.permission.impl" + +dependencies { + implementation(projects.components.core.di) + implementation(projects.components.core.permission.api) + + implementation(libs.dagger) + implementation(libs.appcompat) + + implementation(libs.ktx.activity) +} diff --git a/components/core/permission/impl/src/main/java/com/flipperdevices/core/permission/impl/PermissionRequestHandlerImpl.kt b/components/core/permission/impl/src/main/java/com/flipperdevices/core/permission/impl/PermissionRequestHandlerImpl.kt new file mode 100644 index 0000000000..0a0070c705 --- /dev/null +++ b/components/core/permission/impl/src/main/java/com/flipperdevices/core/permission/impl/PermissionRequestHandlerImpl.kt @@ -0,0 +1,73 @@ +package com.flipperdevices.core.permission.impl + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.core.permission.api.PermissionListener +import com.flipperdevices.core.permission.api.PermissionRequestHandler +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +@ContributesBinding(AppGraph::class, PermissionRequestHandler::class) +class PermissionRequestHandlerImpl @Inject constructor() : + PermissionRequestHandler, + Application.ActivityLifecycleCallbacks, + ActivityResultCallback> { + + private var permissionRequest: ActivityResultLauncher>? = null + private val permissionPendingListeners = mutableMapOf>() + + fun register(application: Application) { + application.registerActivityLifecycleCallbacks(this) + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + if (activity is AppCompatActivity) { + permissionRequest = activity.registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions(), this + ) + } + } + + override fun onActivityDestroyed(activity: Activity) { + permissionRequest = null + permissionPendingListeners.clear() + } + + override fun onActivityResult(result: Map) { + result.forEach { (permission, result) -> + permissionPendingListeners.remove(permission)?.forEach { listener -> + listener.invoke(permission, result) + } + } + } + + override fun requestPermission(vararg permissions: String, listener: PermissionListener) { + val request = permissionRequest + if (request == null) { + permissions.forEach { + listener(it, false) + } + return + } + permissions.forEach { permission -> + val currentList = permissionPendingListeners.getOrDefault(permission, listOf()) + permissionPendingListeners[permission] = currentList + listener + } + request.launch(permissions.toList().toTypedArray()) + } + + // Unused fun + override fun onActivityResumed(activity: Activity) = Unit + override fun onActivityStarted(activity: Activity) = Unit + override fun onActivityPaused(activity: Activity) = Unit + override fun onActivityStopped(activity: Activity) = Unit + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit +} diff --git a/components/core/preference/src/main/proto/settings.proto b/components/core/preference/src/main/proto/settings.proto index 59f394f8a7..a23913c10b 100644 --- a/components/core/preference/src/main/proto/settings.proto +++ b/components/core/preference/src/main/proto/settings.proto @@ -57,4 +57,6 @@ message Settings { bool use_new_infrared = 24; SelectedCatalogSort selected_catalog_sort = 25; bool fatal_ble_security_exception_happens = 26; + bool notification_topic_update_enabled = 27; + bool notification_dialog_shown = 28; } \ No newline at end of file diff --git a/components/inappnotification/api/src/main/java/com/flipperdevices/inappnotification/api/model/InAppNotification.kt b/components/inappnotification/api/src/main/java/com/flipperdevices/inappnotification/api/model/InAppNotification.kt index ba1cc5eaed..241b781731 100644 --- a/components/inappnotification/api/src/main/java/com/flipperdevices/inappnotification/api/model/InAppNotification.kt +++ b/components/inappnotification/api/src/main/java/com/flipperdevices/inappnotification/api/model/InAppNotification.kt @@ -1,15 +1,21 @@ package com.flipperdevices.inappnotification.api.model +import androidx.annotation.StringRes + private const val NOTIFICATION_UPDATE_MS = 5000L private const val NOTIFICATION_REPORT_APP_MS = 3000L private const val NOTIFICATION_HIDE_APP_MS = 5000L +private const val NOTIFICATION_DURATION_MS = 3 * 1000L sealed class InAppNotification { abstract val durationMs: Long - data class SavedKey( - val title: String, - override val durationMs: Long + data class Successful( + val title: String? = null, + @StringRes val titleId: Int? = null, + val desc: String? = null, + @StringRes val descId: Int? = null, + override val durationMs: Long = NOTIFICATION_DURATION_MS ) : InAppNotification() data object ReportApp : InAppNotification() { @@ -33,4 +39,12 @@ sealed class InAppNotification { val action: () -> Unit, override val durationMs: Long = NOTIFICATION_HIDE_APP_MS ) : InAppNotification() + + data class Error( + @StringRes val titleId: Int, + @StringRes val descId: Int, + @StringRes val actionTextId: Int?, + val action: (() -> Unit)?, + override val durationMs: Long = NOTIFICATION_HIDE_APP_MS + ) : InAppNotification() } diff --git a/components/inappnotification/impl/src/main/java/com/flipperdevices/inappnotification/impl/composable/ComposableInAppNotification.kt b/components/inappnotification/impl/src/main/java/com/flipperdevices/inappnotification/impl/composable/ComposableInAppNotification.kt index d1e1bbd23c..1e61d6353c 100644 --- a/components/inappnotification/impl/src/main/java/com/flipperdevices/inappnotification/impl/composable/ComposableInAppNotification.kt +++ b/components/inappnotification/impl/src/main/java/com/flipperdevices/inappnotification/impl/composable/ComposableInAppNotification.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.flipperdevices.core.ui.theme.LocalPallet import com.flipperdevices.inappnotification.api.model.InAppNotification +import com.flipperdevices.inappnotification.impl.composable.type.ComposableInAppNotificationError import com.flipperdevices.inappnotification.impl.composable.type.ComposableInAppNotificationHideApp import com.flipperdevices.inappnotification.impl.composable.type.ComposableInAppNotificationReportApp import com.flipperdevices.inappnotification.impl.composable.type.ComposableInAppNotificationSavedKey @@ -55,6 +56,14 @@ private fun ComposableInAppNotificationCard( LaunchedEffect(notification) { visibleState = true } + + val onClickAction = remember(onNotificationHidden) { + { + visibleState = false + actionClicked = true + onNotificationHidden() + } + } AnimatedVisibility( modifier = modifier, visible = visibleState, @@ -69,14 +78,10 @@ private fun ComposableInAppNotificationCard( backgroundColor = LocalPallet.current.notificationCard ) { when (notification) { - is InAppNotification.SavedKey -> ComposableInAppNotificationSavedKey(notification) + is InAppNotification.Successful -> ComposableInAppNotificationSavedKey(notification) is InAppNotification.SelfUpdateReady -> - ComposableInAppNotificationSelfUpdateReady(notification) { - visibleState = false - actionClicked = true - onNotificationHidden() - } + ComposableInAppNotificationSelfUpdateReady(notification, onClickAction) is InAppNotification.SelfUpdateError -> ComposableInAppNotificationSelfUpdateError() @@ -87,11 +92,12 @@ private fun ComposableInAppNotificationCard( InAppNotification.ReportApp -> ComposableInAppNotificationReportApp() is InAppNotification.HiddenApp -> ComposableInAppNotificationHideApp( notification = notification, - onClickAction = { - visibleState = false - actionClicked = true - onNotificationHidden() - } + onClickAction = onClickAction + ) + + is InAppNotification.Error -> ComposableInAppNotificationError( + notification, + onClickAction ) } } diff --git a/components/inappnotification/impl/src/main/java/com/flipperdevices/inappnotification/impl/composable/type/ComposableInAppNotificationError.kt b/components/inappnotification/impl/src/main/java/com/flipperdevices/inappnotification/impl/composable/type/ComposableInAppNotificationError.kt new file mode 100644 index 0000000000..d34004cd11 --- /dev/null +++ b/components/inappnotification/impl/src/main/java/com/flipperdevices/inappnotification/impl/composable/type/ComposableInAppNotificationError.kt @@ -0,0 +1,84 @@ +package com.flipperdevices.inappnotification.impl.composable.type + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.theme.FlipperThemeInternal +import com.flipperdevices.core.ui.theme.LocalPallet +import com.flipperdevices.core.ui.theme.LocalTypography +import com.flipperdevices.inappnotification.api.model.InAppNotification +import com.flipperdevices.inappnotification.impl.R + +@Composable +internal fun ComposableInAppNotificationError( + error: InAppNotification.Error, + onClickAction: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + modifier = Modifier.size(24.dp), + painter = painterResource(id = R.drawable.pic_update_error), + contentDescription = stringResource(error.titleId), + ) + Column(Modifier.weight(1f)) { + Text( + text = stringResource(error.titleId), + style = LocalTypography.current.subtitleB12 + ) + Text( + text = stringResource(error.descId), + style = LocalTypography.current.subtitleR12 + ) + } + val actionTextId = error.actionTextId + val action = error.action + if (actionTextId != null && action != null) { + Text( + modifier = Modifier + .padding(start = 12.dp) + .clickable { + action() + onClickAction() + }, + text = stringResource(actionTextId), + style = LocalTypography.current.subtitleM12, + color = LocalPallet.current.accentSecond + ) + } + } +} + +@Preview +@Composable +private fun ComposableInAppNotificationErrorPreview() { + FlipperThemeInternal { + ComposableInAppNotificationError( + error = InAppNotification.Error( + titleId = R.string.hide_app_title, + descId = R.string.hide_app_desc, + actionTextId = null, + action = null + ), + onClickAction = {} + ) + } +} diff --git a/components/inappnotification/impl/src/main/java/com/flipperdevices/inappnotification/impl/composable/type/ComposableInAppNotificationSavedKey.kt b/components/inappnotification/impl/src/main/java/com/flipperdevices/inappnotification/impl/composable/type/ComposableInAppNotificationSavedKey.kt index 2674ca3af8..9c42b1b9f4 100644 --- a/components/inappnotification/impl/src/main/java/com/flipperdevices/inappnotification/impl/composable/type/ComposableInAppNotificationSavedKey.kt +++ b/components/inappnotification/impl/src/main/java/com/flipperdevices/inappnotification/impl/composable/type/ComposableInAppNotificationSavedKey.kt @@ -20,19 +20,25 @@ import com.flipperdevices.inappnotification.impl.R @Composable internal fun ComposableInAppNotificationSavedKey( - notification: InAppNotification.SavedKey + notification: InAppNotification.Successful ) { Row { ComposableSaveIcon() Column(modifier = Modifier.padding(top = 9.dp, bottom = 9.dp, end = 12.dp)) { - Text( - text = notification.title, - style = LocalTypography.current.subtitleB12 - ) - Text( - text = stringResource(R.string.saved_key_desc), - style = LocalTypography.current.subtitleR12 - ) + val title = notification.title ?: notification.titleId?.let { stringResource(it) } + if (title != null) { + Text( + text = title, + style = LocalTypography.current.subtitleB12 + ) + } + val desc = notification.desc ?: notification.descId?.let { stringResource(it) } + if (desc != null) { + Text( + text = desc, + style = LocalTypography.current.subtitleR12 + ) + } } } } diff --git a/components/inappnotification/impl/src/main/res/values/strings.xml b/components/inappnotification/impl/src/main/res/values/strings.xml index 893063a591..6c9e2d0fdd 100644 --- a/components/inappnotification/impl/src/main/res/values/strings.xml +++ b/components/inappnotification/impl/src/main/res/values/strings.xml @@ -4,7 +4,6 @@ New app version is ready to install Update - saved to Archive Successfully App report has been sent diff --git a/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/LimboKeyProcessor.kt b/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/LimboKeyProcessor.kt index 8b644c02b1..8504339b2b 100644 --- a/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/LimboKeyProcessor.kt +++ b/components/keyedit/impl/src/main/java/com/flipperdevices/keyedit/impl/viewmodel/processors/LimboKeyProcessor.kt @@ -10,14 +10,13 @@ import com.flipperdevices.bridge.synchronization.api.SynchronizationApi import com.flipperdevices.inappnotification.api.InAppNotificationStorage import com.flipperdevices.inappnotification.api.model.InAppNotification import com.flipperdevices.keyedit.api.NotSavedFlipperKey +import com.flipperdevices.keyedit.impl.R import com.flipperdevices.keyedit.impl.model.EditableKey import com.flipperdevices.keyedit.impl.model.KeyEditState import com.flipperdevices.keyparser.api.KeyParser import com.flipperdevices.singleactivity.api.SingleActivityApi import javax.inject.Inject -private const val NOTIFICATION_DURATION_MS = 3 * 1000L - class LimboKeyProcessor @Inject constructor( private val parser: KeyParser, private val utilsKeyApi: UtilsKeyApi, @@ -69,9 +68,9 @@ class LimboKeyProcessor @Inject constructor( simpleKeyApi.insertKey(newKey) synchronizationApi.startSynchronization(force = true) inAppNotificationStorage.addNotification( - InAppNotification.SavedKey( + InAppNotification.Successful( title = newKey.path.nameWithExtension, - durationMs = NOTIFICATION_DURATION_MS + descId = R.string.saved_key_desc ) ) singleActivityApi.open() diff --git a/components/keyedit/impl/src/main/res/values/strings.xml b/components/keyedit/impl/src/main/res/values/strings.xml index 8970fe0014..8014dde86b 100644 --- a/components/keyedit/impl/src/main/res/values/strings.xml +++ b/components/keyedit/impl/src/main/res/values/strings.xml @@ -4,6 +4,7 @@ Key name Note: Enter your note + saved to Archive Cancel Editing diff --git a/components/notification/api/.gitignore b/components/notification/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/components/notification/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/components/notification/api/build.gradle.kts b/components/notification/api/build.gradle.kts new file mode 100644 index 0000000000..3fcb587a35 --- /dev/null +++ b/components/notification/api/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("flipper.android-compose") +} + +android.namespace = "com.flipperdevices.notification.api" + +dependencies { + implementation(libs.kotlin.coroutines) + + // Compose + implementation(libs.compose.ui) +} diff --git a/components/notification/api/src/main/java/com/flipperdevices/notification/api/FlipperAppNotificationApi.kt b/components/notification/api/src/main/java/com/flipperdevices/notification/api/FlipperAppNotificationApi.kt new file mode 100644 index 0000000000..04cb53af61 --- /dev/null +++ b/components/notification/api/src/main/java/com/flipperdevices/notification/api/FlipperAppNotificationApi.kt @@ -0,0 +1,17 @@ +package com.flipperdevices.notification.api + +import com.flipperdevices.notification.model.UpdateNotificationState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow + +interface FlipperAppNotificationApi { + fun init() + + fun isSubscribedToUpdateNotificationTopic(scope: CoroutineScope): StateFlow + + fun setSubscribeToUpdateAsync( + isSubscribe: Boolean, + scope: CoroutineScope, + withNotificationSuccess: Boolean = false + ) +} diff --git a/components/notification/api/src/main/java/com/flipperdevices/notification/api/FlipperAppNotificationDialogApi.kt b/components/notification/api/src/main/java/com/flipperdevices/notification/api/FlipperAppNotificationDialogApi.kt new file mode 100644 index 0000000000..959eb78605 --- /dev/null +++ b/components/notification/api/src/main/java/com/flipperdevices/notification/api/FlipperAppNotificationDialogApi.kt @@ -0,0 +1,10 @@ +package com.flipperdevices.notification.api + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable + +@Immutable +interface FlipperAppNotificationDialogApi { + @Composable + fun NotificationDialog() +} diff --git a/components/notification/api/src/main/java/com/flipperdevices/notification/model/UpdateNotificationState.kt b/components/notification/api/src/main/java/com/flipperdevices/notification/model/UpdateNotificationState.kt new file mode 100644 index 0000000000..f5b5a3a9a4 --- /dev/null +++ b/components/notification/api/src/main/java/com/flipperdevices/notification/model/UpdateNotificationState.kt @@ -0,0 +1,7 @@ +package com.flipperdevices.notification.model + +enum class UpdateNotificationState { + ENABLED, + DISABLED, + IN_PROGRESS +} diff --git a/components/notification/impl/.gitignore b/components/notification/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/components/notification/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/components/notification/impl/build.gradle.kts b/components/notification/impl/build.gradle.kts new file mode 100644 index 0000000000..78d39e9f34 --- /dev/null +++ b/components/notification/impl/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + id("flipper.android-compose") + id("flipper.anvil") +} + +android.namespace = "com.flipperdevices.notification.impl" + +dependencies { + implementation(projects.components.notification.api) + + implementation(projects.components.core.di) + implementation(projects.components.core.log) + implementation(projects.components.core.ktx) + implementation(projects.components.core.preference) + implementation(projects.components.core.permission.api) + implementation(projects.components.core.ui.res) + implementation(projects.components.core.ui.ktx) + implementation(projects.components.core.ui.theme) + implementation(projects.components.core.ui.dialog) + + implementation(projects.components.inappnotification.api) + + // Compose + implementation(libs.compose.ui) + implementation(libs.compose.tooling) + implementation(libs.compose.foundation) + implementation(libs.compose.material) + implementation(libs.compose.navigation) + + implementation(libs.gms.firebase) + implementation(libs.kotlin.coroutines.play.services) + implementation(libs.kotlin.coroutines) + + implementation(libs.appcompat) + + implementation(libs.tangle.viewmodel.compose) + implementation(libs.tangle.viewmodel.api) + anvil(libs.tangle.viewmodel.compiler) +} diff --git a/components/notification/impl/src/main/AndroidManifest.xml b/components/notification/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..685e093a63 --- /dev/null +++ b/components/notification/impl/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/components/notification/impl/src/main/java/com/flipperdevices/notification/api/FlipperAppNotificationApiImpl.kt b/components/notification/impl/src/main/java/com/flipperdevices/notification/api/FlipperAppNotificationApiImpl.kt new file mode 100644 index 0000000000..6196604889 --- /dev/null +++ b/components/notification/impl/src/main/java/com/flipperdevices/notification/api/FlipperAppNotificationApiImpl.kt @@ -0,0 +1,252 @@ +package com.flipperdevices.notification.api + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.provider.Settings +import androidx.core.content.ContextCompat +import androidx.datastore.core.DataStore +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.core.di.provideDelegate +import com.flipperdevices.core.ktx.jre.withLock +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.error +import com.flipperdevices.core.log.info +import com.flipperdevices.inappnotification.api.InAppNotificationStorage +import com.flipperdevices.inappnotification.api.model.InAppNotification +import com.flipperdevices.notification.impl.R +import com.flipperdevices.notification.model.ChannelBlockedException +import com.flipperdevices.notification.model.NotificationPermissionState +import com.flipperdevices.notification.model.UpdateNotificationState +import com.flipperdevices.notification.model.UpdateNotificationStateInternal +import com.flipperdevices.notification.utils.NotificationPermissionHelper +import com.google.firebase.Firebase +import com.google.firebase.messaging.messaging +import com.squareup.anvil.annotations.ContributesBinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.withContext +import java.io.IOException +import java.net.UnknownHostException +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton + +private const val COUNT_PERMISSION_DENIED = 3 +private const val TOPIC_UPDATE_FIRMWARE = "flipper_update_firmware_release" +private const val UPDATE_TOPIC_NOTIFICATION_CHANNEL = "flipper_update_firmware_channel" + +@Singleton +@ContributesBinding(AppGraph::class, FlipperAppNotificationApi::class) +class FlipperAppNotificationApiImpl @Inject constructor( + dataStoreProvider: Provider>, + permissionHelperProvider: Provider, + inAppNotificationStorageProvider: Provider, + private val context: Context +) : FlipperAppNotificationApi, + LogTagProvider { + override val TAG = "FlipperAppNotificationApi" + + private val settingsDataStore by dataStoreProvider + private val permissionHelper by permissionHelperProvider + private val inAppNotification by inAppNotificationStorageProvider + private val updateNotificationStateInternalFlow = MutableStateFlow( + UpdateNotificationStateInternal.READY + ) + private val mutex = Mutex() + private var permissionRequestDenied = 0 + + override fun isSubscribedToUpdateNotificationTopic(scope: CoroutineScope): StateFlow { + return combine( + settingsDataStore.data, + updateNotificationStateInternalFlow + ) { settings, updateNotificationStateInternal -> + when (updateNotificationStateInternal) { + UpdateNotificationStateInternal.IN_PROGRESS -> UpdateNotificationState.IN_PROGRESS + UpdateNotificationStateInternal.READY -> if ( + settings.notificationTopicUpdateEnabled + ) { + when (permissionHelper.isPermissionGranted(UPDATE_TOPIC_NOTIFICATION_CHANNEL)) { + NotificationPermissionState.GRANTED -> UpdateNotificationState.ENABLED + NotificationPermissionState.DISABLED, + NotificationPermissionState.DISABLED_CHANNEL -> UpdateNotificationState.DISABLED + } + } else { + UpdateNotificationState.DISABLED + } + } + }.stateIn(scope, SharingStarted.WhileSubscribed(), UpdateNotificationState.IN_PROGRESS) + } + + override fun setSubscribeToUpdateAsync( + isSubscribe: Boolean, + scope: CoroutineScope, + withNotificationSuccess: Boolean + ) { + scope.launch(Dispatchers.Default) { + setSubscribeToUpdate(isSubscribe, onRetry = { + setSubscribeToUpdateAsync(isSubscribe, scope) + }, withNotificationSuccess) + } + } + + @Suppress("LongMethod") + private suspend fun setSubscribeToUpdate( + isSubscribe: Boolean, + onRetry: () -> Unit, + withNotificationSuccess: Boolean + ) = withLock(mutex, "set_update") { + try { + createNotificationChannel() + setSubscribeToUpdateInternal(isSubscribe) + if (withNotificationSuccess) { + inAppNotification.addNotification( + InAppNotification.Successful( + titleId = R.string.notification_sucs_title, + descId = R.string.notification_sucs_desc + ) + ) + } + } catch (uhe: UnknownHostException) { + error(uhe) { "Failed subscribe to topic" } + inAppNotification.addNotification( + InAppNotification.Error( + titleId = R.string.notification_error_internet_title, + descId = R.string.notification_error_internet_desc, + actionTextId = R.string.notification_error_action_retry, + action = onRetry + ) + ) + } catch (ioException: IOException) { + error(ioException) { "Failed subscribe to topic" } + inAppNotification.addNotification( + InAppNotification.Error( + titleId = R.string.notification_error_server_title, + descId = R.string.notification_error_server_desc, + actionTextId = R.string.notification_error_action_retry, + action = onRetry + ) + ) + } catch (securityException: SecurityException) { + error(securityException) { "Failed grant permission" } + if (++permissionRequestDenied >= COUNT_PERMISSION_DENIED) { + inAppNotification.addNotification( + InAppNotification.Error( + titleId = R.string.notification_error_permission_title, + descId = R.string.notification_error_permission_desc, + actionTextId = R.string.notification_error_action_go_to_settings, + action = { openNotificationSettings(withChannel = false) } + ) + ) + } else { + inAppNotification.addNotification( + InAppNotification.Error( + titleId = R.string.notification_error_permission_title, + descId = R.string.notification_error_permission_desc, + actionTextId = R.string.notification_error_action_retry, + action = onRetry + ) + ) + } + } catch (channelBlocked: ChannelBlockedException) { + error(channelBlocked) { "Failed grant permission" } + inAppNotification.addNotification( + InAppNotification.Error( + titleId = R.string.notification_error_permission_title, + descId = R.string.notification_error_permission_desc, + actionTextId = R.string.notification_error_action_go_to_settings, + action = { openNotificationSettings(withChannel = true) } + ) + ) + } catch (generalError: Throwable) { + error(generalError) { "Failed subscribe to topic" } + inAppNotification.addNotification( + InAppNotification.Error( + titleId = R.string.notification_error_general_title, + descId = R.string.notification_error_general_desc, + actionTextId = R.string.notification_error_action_retry, + action = onRetry + ) + ) + } finally { + withContext(NonCancellable) { + updateNotificationStateInternalFlow.emit(UpdateNotificationStateInternal.READY) + } + } + } + + private suspend fun setSubscribeToUpdateInternal(isSubscribe: Boolean) { + updateNotificationStateInternalFlow.emit(UpdateNotificationStateInternal.IN_PROGRESS) + + val task = if (isSubscribe) { + Firebase.messaging.subscribeToTopic(TOPIC_UPDATE_FIRMWARE) + } else { + Firebase.messaging.unsubscribeFromTopic(TOPIC_UPDATE_FIRMWARE) + } + task.await() + + if (isSubscribe) { + when (permissionHelper.isPermissionGranted(UPDATE_TOPIC_NOTIFICATION_CHANNEL)) { + NotificationPermissionState.GRANTED -> {} + NotificationPermissionState.DISABLED -> if (!permissionHelper.requestPermission()) { + throw SecurityException() + } + + NotificationPermissionState.DISABLED_CHANNEL -> throw ChannelBlockedException() + } + } + + settingsDataStore.updateData { + it.toBuilder() + .setNotificationTopicUpdateEnabled(isSubscribe) + .build() + } + } + + private fun openNotificationSettings(withChannel: Boolean) { + var settingsIntent: Intent = + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + if (withChannel) { + settingsIntent = settingsIntent.putExtra( + Settings.EXTRA_CHANNEL_ID, + UPDATE_TOPIC_NOTIFICATION_CHANNEL + ) + } + context.startActivity(settingsIntent) + } + + private fun createNotificationChannel() { + val name = context.getString(R.string.notification_channel_title) + val descriptionText = context.getString(R.string.notification_channel_desc) + val importance = NotificationManager.IMPORTANCE_DEFAULT + val mChannel = NotificationChannel(UPDATE_TOPIC_NOTIFICATION_CHANNEL, name, importance) + mChannel.description = descriptionText + val notificationManager = + ContextCompat.getSystemService(context, NotificationManager::class.java) + notificationManager?.createNotificationChannel(mChannel) + } + + override fun init() { + Firebase.messaging.token.addOnCompleteListener { task -> + if (!task.isSuccessful) { + error(task.exception) { "Can't init FCM registration token" } + return@addOnCompleteListener + } + val token = task.result + + info { "Init FCM success with token $token " } + } + } +} diff --git a/components/notification/impl/src/main/java/com/flipperdevices/notification/api/FlipperAppNotificationDialogApiImpl.kt b/components/notification/impl/src/main/java/com/flipperdevices/notification/api/FlipperAppNotificationDialogApiImpl.kt new file mode 100644 index 0000000000..3772c18add --- /dev/null +++ b/components/notification/impl/src/main/java/com/flipperdevices/notification/api/FlipperAppNotificationDialogApiImpl.kt @@ -0,0 +1,59 @@ +package com.flipperdevices.notification.api + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.core.ui.dialog.composable.multichoice.FlipperMultiChoiceDialog +import com.flipperdevices.core.ui.dialog.composable.multichoice.FlipperMultiChoiceDialogModel +import com.flipperdevices.core.ui.ktx.image.painterResourceByKey +import com.flipperdevices.notification.impl.R +import com.flipperdevices.notification.viewmodel.NotificationDialogViewModel +import com.squareup.anvil.annotations.ContributesBinding +import tangle.viewmodel.compose.tangleViewModel +import javax.inject.Inject + +@ContributesBinding(AppGraph::class, FlipperAppNotificationDialogApi::class) +class FlipperAppNotificationDialogApiImpl @Inject constructor() : FlipperAppNotificationDialogApi { + @Composable + override fun NotificationDialog() { + val dialogViewModel = tangleViewModel() + val isDialogShown by dialogViewModel.isNotificationShown().collectAsState() + + if (isDialogShown) { + val imageId = if (MaterialTheme.colors.isLight) { + R.drawable.pic_notification_light + } else { + R.drawable.pic_notification_dark + } + val dialog = remember(imageId, dialogViewModel) { + FlipperMultiChoiceDialogModel.Builder() + .setImage { + Image( + modifier = Modifier.fillMaxWidth(), + painter = painterResourceByKey(imageId), + contentDescription = stringResource(R.string.notification_dialog_title) + ) + }.setTitle(R.string.notification_dialog_title) + .setDescription(R.string.notification_dialog_desc).addButton( + textId = R.string.notification_dialog_btn_enable, + onClick = dialogViewModel::onEnableNotification, + isActive = true + ).addButton( + textId = R.string.notification_dialog_btn_skip, + onClick = dialogViewModel::onDismiss + ) + .setCloseOnClickOutside(true) + .setOnDismissRequest(dialogViewModel::onDismiss) + .build() + } + FlipperMultiChoiceDialog(model = dialog) + } + } +} diff --git a/components/notification/impl/src/main/java/com/flipperdevices/notification/model/ChannelBlockedException.kt b/components/notification/impl/src/main/java/com/flipperdevices/notification/model/ChannelBlockedException.kt new file mode 100644 index 0000000000..7f77eeb801 --- /dev/null +++ b/components/notification/impl/src/main/java/com/flipperdevices/notification/model/ChannelBlockedException.kt @@ -0,0 +1,3 @@ +package com.flipperdevices.notification.model + +class ChannelBlockedException : Throwable() diff --git a/components/notification/impl/src/main/java/com/flipperdevices/notification/model/NotificationPermissionState.kt b/components/notification/impl/src/main/java/com/flipperdevices/notification/model/NotificationPermissionState.kt new file mode 100644 index 0000000000..5d218a9444 --- /dev/null +++ b/components/notification/impl/src/main/java/com/flipperdevices/notification/model/NotificationPermissionState.kt @@ -0,0 +1,7 @@ +package com.flipperdevices.notification.model + +enum class NotificationPermissionState { + GRANTED, + DISABLED, + DISABLED_CHANNEL +} diff --git a/components/notification/impl/src/main/java/com/flipperdevices/notification/model/UpdateNotificationStateInternal.kt b/components/notification/impl/src/main/java/com/flipperdevices/notification/model/UpdateNotificationStateInternal.kt new file mode 100644 index 0000000000..d4e8ef62b1 --- /dev/null +++ b/components/notification/impl/src/main/java/com/flipperdevices/notification/model/UpdateNotificationStateInternal.kt @@ -0,0 +1,6 @@ +package com.flipperdevices.notification.model + +enum class UpdateNotificationStateInternal { + IN_PROGRESS, + READY +} diff --git a/components/notification/impl/src/main/java/com/flipperdevices/notification/utils/NotificationPermissionHelper.kt b/components/notification/impl/src/main/java/com/flipperdevices/notification/utils/NotificationPermissionHelper.kt new file mode 100644 index 0000000000..406f5b4231 --- /dev/null +++ b/components/notification/impl/src/main/java/com/flipperdevices/notification/utils/NotificationPermissionHelper.kt @@ -0,0 +1,53 @@ +package com.flipperdevices.notification.utils + +import android.Manifest +import android.app.NotificationManager +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.content.ContextCompat +import com.flipperdevices.core.permission.api.PermissionRequestHandler +import com.flipperdevices.notification.model.NotificationPermissionState +import kotlinx.coroutines.CompletableDeferred +import javax.inject.Inject + +class NotificationPermissionHelper @Inject constructor( + private val context: Context, + private val permissionRequestHandler: PermissionRequestHandler +) { + suspend fun requestPermission(): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return true + } + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + return true + } + val result = CompletableDeferred() + permissionRequestHandler.requestPermission(Manifest.permission.POST_NOTIFICATIONS) { _, isGranted -> + result.complete(isGranted) + } + return result.await() + } + + fun isPermissionGranted(channelId: String): NotificationPermissionState { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + return NotificationPermissionState.DISABLED + } + } + val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val channel = manager.getNotificationChannel(channelId) + if (channel.importance == NotificationManager.IMPORTANCE_NONE) { + return NotificationPermissionState.DISABLED_CHANNEL + } + return NotificationPermissionState.GRANTED + } +} diff --git a/components/notification/impl/src/main/java/com/flipperdevices/notification/viewmodel/NotificationDialogViewModel.kt b/components/notification/impl/src/main/java/com/flipperdevices/notification/viewmodel/NotificationDialogViewModel.kt new file mode 100644 index 0000000000..7c75f4d831 --- /dev/null +++ b/components/notification/impl/src/main/java/com/flipperdevices/notification/viewmodel/NotificationDialogViewModel.kt @@ -0,0 +1,48 @@ +package com.flipperdevices.notification.viewmodel + +import androidx.datastore.core.DataStore +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.flipperdevices.core.di.provideDelegate +import com.flipperdevices.core.preference.pb.Settings +import com.flipperdevices.notification.api.FlipperAppNotificationApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.runBlocking +import tangle.viewmodel.VMInject +import javax.inject.Provider + +class NotificationDialogViewModel @VMInject constructor( + settingsProvider: Provider>, + flipperNotificationApiProvider: Provider, + coroutineScopeProvider: Provider +) : ViewModel() { + private val settings by settingsProvider + private val flipperNotificationApi by flipperNotificationApiProvider + private val coroutineScope by coroutineScopeProvider + fun isNotificationShown(): StateFlow = settings.data.map { + it.notificationDialogShown.not() + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) + + fun onEnableNotification() { + flipperNotificationApi.setSubscribeToUpdateAsync( + isSubscribe = true, + scope = coroutineScope, + withNotificationSuccess = true + ) + onDismiss() + } + + fun onDismiss() { + runBlocking { + settings.updateData { + it.toBuilder() + .setNotificationDialogShown(true) + .build() + } + } + } +} diff --git a/components/notification/impl/src/main/res/drawable/pic_notification_dark.xml b/components/notification/impl/src/main/res/drawable/pic_notification_dark.xml new file mode 100644 index 0000000000..1eedfe1fa1 --- /dev/null +++ b/components/notification/impl/src/main/res/drawable/pic_notification_dark.xml @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/notification/impl/src/main/res/drawable/pic_notification_light.xml b/components/notification/impl/src/main/res/drawable/pic_notification_light.xml new file mode 100644 index 0000000000..a7d8beec96 --- /dev/null +++ b/components/notification/impl/src/main/res/drawable/pic_notification_light.xml @@ -0,0 +1,830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/notification/impl/src/main/res/values/strings.xml b/components/notification/impl/src/main/res/values/strings.xml new file mode 100644 index 0000000000..ada7750603 --- /dev/null +++ b/components/notification/impl/src/main/res/values/strings.xml @@ -0,0 +1,21 @@ + + + Notifications enabled + You will be notified about firmware releases + No Internet Connection + Check your internet connection and try again + Can\'t Connect to the Server + Unable to change this setting. Try again later. + Something Went Wrong + Try again later + Retry + Go to Settings + Notifications not enabled + You should provide permission for notifications + Firmware update + Firmware update description + Enable Push Notifications + App will notify you about new firmware releases + Enable + Skip + \ No newline at end of file diff --git a/components/notification/noop/.gitignore b/components/notification/noop/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/components/notification/noop/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/components/notification/noop/build.gradle.kts b/components/notification/noop/build.gradle.kts new file mode 100644 index 0000000000..35a62f975d --- /dev/null +++ b/components/notification/noop/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("flipper.android-compose") + id("flipper.anvil") +} + +android.namespace = "com.flipperdevices.notification.noop" + +dependencies { + implementation(projects.components.core.di) + + implementation(projects.components.notification.api) + implementation(projects.components.inappnotification.api) + + // Compose + implementation(libs.compose.ui) + + implementation(libs.kotlin.coroutines) +} diff --git a/components/notification/noop/src/main/java/com/flipperdevices/notification/noop/FlipperAppNotificationApiNoopImpl.kt b/components/notification/noop/src/main/java/com/flipperdevices/notification/noop/FlipperAppNotificationApiNoopImpl.kt new file mode 100644 index 0000000000..23ad256ff5 --- /dev/null +++ b/components/notification/noop/src/main/java/com/flipperdevices/notification/noop/FlipperAppNotificationApiNoopImpl.kt @@ -0,0 +1,38 @@ +package com.flipperdevices.notification.noop + +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.inappnotification.api.InAppNotificationStorage +import com.flipperdevices.inappnotification.api.model.InAppNotification +import com.flipperdevices.notification.api.FlipperAppNotificationApi +import com.flipperdevices.notification.model.UpdateNotificationState +import com.squareup.anvil.annotations.ContributesBinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ContributesBinding(AppGraph::class, FlipperAppNotificationApi::class) +class FlipperAppNotificationApiNoopImpl @Inject constructor( + private val inAppNotificationStorage: InAppNotificationStorage +) : FlipperAppNotificationApi { + override fun init() = Unit + + override fun isSubscribedToUpdateNotificationTopic(scope: CoroutineScope): StateFlow { + return MutableStateFlow(UpdateNotificationState.DISABLED) + } + + override fun setSubscribeToUpdateAsync( + isSubscribe: Boolean, + scope: CoroutineScope, + withNotificationSuccess: Boolean + ) { + inAppNotificationStorage.addNotification( + InAppNotification.Error( + titleId = R.string.notification_disabled_title, + descId = R.string.notification_disabled_desc, + actionTextId = null, + action = null + ) + ) + } +} diff --git a/components/notification/noop/src/main/java/com/flipperdevices/notification/noop/FlipperAppNotificationDialogApiNoopImpl.kt b/components/notification/noop/src/main/java/com/flipperdevices/notification/noop/FlipperAppNotificationDialogApiNoopImpl.kt new file mode 100644 index 0000000000..5db1fe570b --- /dev/null +++ b/components/notification/noop/src/main/java/com/flipperdevices/notification/noop/FlipperAppNotificationDialogApiNoopImpl.kt @@ -0,0 +1,14 @@ +package com.flipperdevices.notification.noop + +import androidx.compose.runtime.Composable +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.notification.api.FlipperAppNotificationDialogApi +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject + +@ContributesBinding(AppGraph::class, FlipperAppNotificationDialogApi::class) +class FlipperAppNotificationDialogApiNoopImpl @Inject constructor() : + FlipperAppNotificationDialogApi { + @Composable + override fun NotificationDialog() = Unit +} diff --git a/components/notification/noop/src/main/res/values/strings.xml b/components/notification/noop/src/main/res/values/strings.xml new file mode 100644 index 0000000000..be57ff2d7d --- /dev/null +++ b/components/notification/noop/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Notifications not enabled + You need to have GMS services and a version from Google Play to enable notifications + diff --git a/components/settings/impl/build.gradle.kts b/components/settings/impl/build.gradle.kts index 23be0cf3a6..bc82fbf4b0 100644 --- a/components/settings/impl/build.gradle.kts +++ b/components/settings/impl/build.gradle.kts @@ -31,6 +31,8 @@ dependencies { implementation(projects.components.nfc.mfkey32.api) implementation(projects.components.faphub.installation.all.api) implementation(projects.components.selfupdater.api) + implementation(projects.components.notification.api) + implementation(projects.components.inappnotification.api) implementation(projects.components.bridge.api) implementation(projects.components.bridge.dao.api) diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/ComposableSettings.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/ComposableSettings.kt index 0da1e6db82..3159574824 100644 --- a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/ComposableSettings.kt +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/ComposableSettings.kt @@ -16,12 +16,13 @@ import androidx.navigation.NavController import com.flipperdevices.core.ui.ktx.OrangeAppBar import com.flipperdevices.core.ui.theme.LocalPallet import com.flipperdevices.settings.impl.R +import com.flipperdevices.settings.impl.composable.category.AppCategory import com.flipperdevices.settings.impl.composable.category.DebugCategory import com.flipperdevices.settings.impl.composable.category.ExperimentalCategory import com.flipperdevices.settings.impl.composable.category.ExportKeysCategory import com.flipperdevices.settings.impl.composable.category.OtherSettingsCategory -import com.flipperdevices.settings.impl.composable.category.ThemeCategory import com.flipperdevices.settings.impl.composable.category.VersionCategory +import com.flipperdevices.settings.impl.viewmodels.NotificationViewModel import com.flipperdevices.settings.impl.viewmodels.SettingsViewModel import tangle.viewmodel.compose.tangleViewModel @@ -29,13 +30,15 @@ import tangle.viewmodel.compose.tangleViewModel fun ComposableCommonSettings( navController: NavController, modifier: Modifier = Modifier, - settingsViewModel: SettingsViewModel = tangleViewModel() + settingsViewModel: SettingsViewModel = tangleViewModel(), + notificationViewModel: NotificationViewModel = tangleViewModel() ) { val context = LocalContext.current val settings by settingsViewModel.getState().collectAsState() val s2rInitialized by settingsViewModel.getShake2ReportInitializationState().collectAsState() val exportState by settingsViewModel.getExportState().collectAsState() + val notificationState by notificationViewModel.getNotificationToggleState().collectAsState() Column( modifier = modifier @@ -45,9 +48,11 @@ fun ComposableCommonSettings( verticalArrangement = Arrangement.spacedBy(14.dp) ) { OrangeAppBar(R.string.options, onBack = navController::popBackStack) - ThemeCategory( + AppCategory( theme = settings.selectedTheme, - onSelectTheme = settingsViewModel::onChangeSelectedTheme + onSelectTheme = settingsViewModel::onChangeSelectedTheme, + notificationState = notificationState, + onChangeNotificationState = notificationViewModel::switchToggle ) if (settings.expertMode) { DebugCategory( diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/AppCategory.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/AppCategory.kt new file mode 100644 index 0000000000..03d3be1bff --- /dev/null +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/AppCategory.kt @@ -0,0 +1,41 @@ +package com.flipperdevices.settings.impl.composable.category + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.preference.pb.SelectedTheme +import com.flipperdevices.core.ui.theme.LocalTypography +import com.flipperdevices.notification.model.UpdateNotificationState +import com.flipperdevices.settings.impl.R +import com.flipperdevices.settings.impl.composable.components.GrayDivider +import com.flipperdevices.settings.impl.composable.elements.PushNotificationElement +import com.flipperdevices.settings.impl.composable.elements.ThemeChangerElement + +@Composable +fun AppCategory( + theme: SelectedTheme, + onSelectTheme: (SelectedTheme) -> Unit, + notificationState: UpdateNotificationState, + onChangeNotificationState: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + CardCategory(modifier = modifier) { + Text( + modifier = Modifier.padding(all = 12.dp), + text = stringResource(R.string.app_title), + style = LocalTypography.current.buttonB16 + ) + ThemeChangerElement( + theme = theme, + onSelectTheme = onSelectTheme + ) + GrayDivider() + PushNotificationElement( + notificationState = notificationState, + onChangeNotificationState = onChangeNotificationState + ) + } +} diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/DebugCategory.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/DebugCategory.kt index 930511c6c5..a0083781bf 100644 --- a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/DebugCategory.kt +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/DebugCategory.kt @@ -5,11 +5,11 @@ import androidx.compose.ui.Modifier import androidx.navigation.NavController import com.flipperdevices.core.preference.pb.Settings import com.flipperdevices.settings.impl.R -import com.flipperdevices.settings.impl.composable.elements.CategoryElement -import com.flipperdevices.settings.impl.composable.elements.ClickableElement -import com.flipperdevices.settings.impl.composable.elements.GrayDivider -import com.flipperdevices.settings.impl.composable.elements.SimpleElement -import com.flipperdevices.settings.impl.composable.elements.SwitchableElement +import com.flipperdevices.settings.impl.composable.components.CategoryElement +import com.flipperdevices.settings.impl.composable.components.ClickableElement +import com.flipperdevices.settings.impl.composable.components.GrayDivider +import com.flipperdevices.settings.impl.composable.components.SimpleElement +import com.flipperdevices.settings.impl.composable.components.SwitchableElement import com.flipperdevices.settings.impl.model.DebugSettingAction import com.flipperdevices.settings.impl.model.DebugSettingSwitch import com.flipperdevices.settings.impl.viewmodels.DebugViewModel diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/ExperimentalCategory.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/ExperimentalCategory.kt index 41090e3f94..847f1d4567 100644 --- a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/ExperimentalCategory.kt +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/ExperimentalCategory.kt @@ -5,8 +5,8 @@ import androidx.compose.ui.Modifier import androidx.navigation.NavController import com.flipperdevices.core.preference.pb.Settings import com.flipperdevices.settings.impl.R -import com.flipperdevices.settings.impl.composable.elements.CategoryElement -import com.flipperdevices.settings.impl.composable.elements.ClickableElement +import com.flipperdevices.settings.impl.composable.components.CategoryElement +import com.flipperdevices.settings.impl.composable.components.ClickableElement import com.flipperdevices.settings.impl.viewmodels.ExperimentalViewModel import tangle.viewmodel.compose.tangleViewModel diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/ExportKeysCategory.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/ExportKeysCategory.kt index 2ee6eb5697..f2e76541b0 100644 --- a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/ExportKeysCategory.kt +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/ExportKeysCategory.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp import com.flipperdevices.core.ui.ktx.clickableRipple import com.flipperdevices.core.ui.theme.LocalPallet import com.flipperdevices.settings.impl.R -import com.flipperdevices.settings.impl.composable.elements.SimpleElement +import com.flipperdevices.settings.impl.composable.components.SimpleElement import com.flipperdevices.settings.impl.model.ExportState import com.flipperdevices.core.ui.res.R as DesignSystem diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/OtherSettingsCategory.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/OtherSettingsCategory.kt index 77870793be..a2f771289b 100644 --- a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/OtherSettingsCategory.kt +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/OtherSettingsCategory.kt @@ -3,9 +3,9 @@ package com.flipperdevices.settings.impl.composable.category import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.flipperdevices.settings.impl.R -import com.flipperdevices.settings.impl.composable.elements.ClickableElement -import com.flipperdevices.settings.impl.composable.elements.GrayDivider -import com.flipperdevices.settings.impl.composable.elements.UrlElement +import com.flipperdevices.settings.impl.composable.components.ClickableElement +import com.flipperdevices.settings.impl.composable.components.GrayDivider +import com.flipperdevices.settings.impl.composable.components.UrlElement @Composable fun OtherSettingsCategory( diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/ThemeCategory.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/ThemeCategory.kt deleted file mode 100644 index 3673370e76..0000000000 --- a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/ThemeCategory.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.flipperdevices.settings.impl.composable.category - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material.DropdownMenu -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.flipperdevices.core.preference.pb.SelectedTheme -import com.flipperdevices.core.ui.ktx.clickableRipple -import com.flipperdevices.core.ui.theme.LocalPallet -import com.flipperdevices.core.ui.theme.LocalTypography -import com.flipperdevices.settings.impl.R -import com.flipperdevices.settings.impl.composable.elements.SimpleElement -import com.flipperdevices.core.ui.res.R as DesignSystem - -@Composable -fun ThemeCategory( - theme: SelectedTheme, - onSelectTheme: (SelectedTheme) -> Unit, - modifier: Modifier = Modifier, -) { - val themes = SelectedTheme.entries.filter { it != SelectedTheme.UNRECOGNIZED } - - val nameTheme = stringResource(id = getNameBySelectedTheme(theme)) - var showMenu by remember { mutableStateOf(false) } - - CardCategory(modifier = modifier) { - Row( - modifier = Modifier.clickableRipple { showMenu = true }, - verticalAlignment = Alignment.CenterVertically - ) { - SimpleElement( - modifier = Modifier.weight(weight = 1f), - titleId = R.string.theme_options, - descriptionId = R.string.theme_options_desc, - titleTextStyle = LocalTypography.current.buttonB16 - ) - - Box { - Row( - modifier = Modifier.padding(12.dp), - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { - Text( - text = nameTheme, - style = LocalTypography.current.buttonM16 - ) - - Icon( - painter = painterResource(DesignSystem.drawable.ic_more), - contentDescription = nameTheme, - tint = LocalPallet.current.iconTint30 - ) - } - - DropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false }, - modifier = Modifier.background(LocalPallet.current.backgroundDialog) - ) { - themes.forEach { - DropdownMenuItem(onClick = { - showMenu = false - onSelectTheme(it) - }) { - Text(text = stringResource(id = getNameBySelectedTheme(it))) - } - } - } - } - } - } -} - -@Composable -fun getNameBySelectedTheme(theme: SelectedTheme): Int { - return when (theme) { - SelectedTheme.DARK -> R.string.theme_options_dark - SelectedTheme.LIGHT -> R.string.theme_options_light - else -> R.string.theme_options_system - } -} diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/VersionCategory.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/VersionCategory.kt index ee35ef029a..1bd4963550 100644 --- a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/VersionCategory.kt +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/category/VersionCategory.kt @@ -24,7 +24,7 @@ import com.flipperdevices.core.ui.ktx.clickableRipple import com.flipperdevices.core.ui.theme.LocalPallet import com.flipperdevices.core.ui.theme.LocalTypography import com.flipperdevices.settings.impl.R -import com.flipperdevices.settings.impl.composable.elements.SimpleElement +import com.flipperdevices.settings.impl.composable.components.SimpleElement import com.flipperdevices.settings.impl.viewmodels.VersionViewModel import tangle.viewmodel.compose.tangleViewModel diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/AnimatedSwitch.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/AnimatedSwitch.kt new file mode 100644 index 0000000000..e12b59bc02 --- /dev/null +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/AnimatedSwitch.kt @@ -0,0 +1,93 @@ +package com.flipperdevices.settings.impl.composable.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Button +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Switch +import androidx.compose.material.SwitchDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.theme.FlipperThemeInternal +import com.flipperdevices.core.ui.theme.LocalPallet +import com.flipperdevices.notification.model.UpdateNotificationState + +@Composable +fun AnimatedSwitch( + state: UpdateNotificationState, + onSwitch: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) = Box(modifier) { + AnimatedVisibility( + state == UpdateNotificationState.ENABLED || state == UpdateNotificationState.DISABLED, + enter = fadeIn(), + exit = fadeOut(), + ) { + Switch( + checked = state == UpdateNotificationState.ENABLED, + onCheckedChange = onSwitch, + colors = SwitchDefaults.colors( + uncheckedThumbColor = LocalPallet.current.disableSwitch, + uncheckedTrackColor = LocalPallet.current.disableBackgroundSwitch, + uncheckedTrackAlpha = 0.5f + ) + ) + } + AnimatedVisibility( + state == UpdateNotificationState.IN_PROGRESS, + enter = fadeIn(), + exit = fadeOut(), + ) { + CircularProgressIndicator( + modifier = Modifier + .padding(12.dp) + .size(24.dp), + color = LocalPallet.current.accent, + strokeWidth = 2.dp + ) + } +} + +@Preview( + showBackground = true, + showSystemUi = true +) +@Composable +private fun AnimatedSwitchPreview() { + FlipperThemeInternal { + var selectedState by remember { + mutableStateOf(UpdateNotificationState.DISABLED) + } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Row { + UpdateNotificationState.entries.forEach { state -> + Button(onClick = { selectedState = state }) { + Text(text = state.name) + } + } + } + AnimatedSwitch(state = selectedState, onSwitch = {}) + UpdateNotificationState.entries.forEach { state -> + AnimatedSwitch(state = state, onSwitch = {}) + } + } + } +} diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/CategoryElement.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/CategoryElement.kt similarity index 86% rename from components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/CategoryElement.kt rename to components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/CategoryElement.kt index ee8f2be4b4..567b82c605 100644 --- a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/CategoryElement.kt +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/CategoryElement.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.settings.impl.composable.elements +package com.flipperdevices.settings.impl.composable.components import androidx.annotation.StringRes import androidx.compose.foundation.layout.Row @@ -26,6 +26,6 @@ fun CategoryElement( descriptionId, titleTextStyle = LocalTypography.current.buttonB16 ) - Switch(state = state, onSwitchState = onSwitchState) + FlipperSwitch(state = state, onSwitchState = onSwitchState) } } diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/ClickableElement.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/ClickableElement.kt similarity index 97% rename from components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/ClickableElement.kt rename to components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/ClickableElement.kt index 7d84d6a36d..5d19a68e05 100644 --- a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/ClickableElement.kt +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/ClickableElement.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.settings.impl.composable.elements +package com.flipperdevices.settings.impl.composable.components import androidx.annotation.DrawableRes import androidx.annotation.StringRes diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/Switch.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/FlipperSwitch.kt similarity index 52% rename from components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/Switch.kt rename to components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/FlipperSwitch.kt index f5a87ab7ff..32b072f96d 100644 --- a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/Switch.kt +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/FlipperSwitch.kt @@ -1,19 +1,23 @@ -package com.flipperdevices.settings.impl.composable.elements +package com.flipperdevices.settings.impl.composable.components +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.material.Switch import androidx.compose.material.SwitchDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.theme.FlipperThemeInternal import com.flipperdevices.core.ui.theme.LocalPallet @Composable -fun Switch( +fun FlipperSwitch( state: Boolean, modifier: Modifier = Modifier, onSwitchState: (Boolean) -> Unit ) { - androidx.compose.material.Switch( + Switch( modifier = modifier.padding(horizontal = 12.dp), checked = state, onCheckedChange = onSwitchState, @@ -24,3 +28,24 @@ fun Switch( ) ) } + +@Preview( + showSystemUi = true, + showBackground = true +) +@Composable +private fun SwitchPreview() { + FlipperThemeInternal { + Column { + FlipperSwitch( + state = true, + onSwitchState = {} + ) + + FlipperSwitch( + state = false, + onSwitchState = {} + ) + } + } +} diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/GrayDivider.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/GrayDivider.kt similarity index 88% rename from components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/GrayDivider.kt rename to components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/GrayDivider.kt index 8500f5afab..23fbe178fd 100644 --- a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/GrayDivider.kt +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/GrayDivider.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.settings.impl.composable.elements +package com.flipperdevices.settings.impl.composable.components import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.Divider diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/SimpleElement.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/SimpleElement.kt similarity index 96% rename from components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/SimpleElement.kt rename to components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/SimpleElement.kt index cef18a9d5e..4c26e598e5 100644 --- a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/SimpleElement.kt +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/SimpleElement.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.settings.impl.composable.elements +package com.flipperdevices.settings.impl.composable.components import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement @@ -18,7 +18,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.flipperdevices.core.ktx.jre.then import com.flipperdevices.core.ui.ktx.clickableRipple import com.flipperdevices.core.ui.theme.LocalPallet import com.flipperdevices.core.ui.theme.LocalTypography diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/SwitchableElement.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/SwitchableElement.kt similarity index 84% rename from components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/SwitchableElement.kt rename to components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/SwitchableElement.kt index 388251a7c0..31f22815ce 100644 --- a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/SwitchableElement.kt +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/SwitchableElement.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.settings.impl.composable.elements +package com.flipperdevices.settings.impl.composable.components import androidx.annotation.StringRes import androidx.compose.foundation.layout.Row @@ -24,6 +24,6 @@ fun SwitchableElement( titleId, descriptionId ) - Switch(state = state, onSwitchState = onSwitchState) + FlipperSwitch(state = state, onSwitchState = onSwitchState) } } diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/UrlElement.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/UrlElement.kt similarity index 96% rename from components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/UrlElement.kt rename to components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/UrlElement.kt index 0ec9c072fd..77370a8395 100644 --- a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/UrlElement.kt +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/components/UrlElement.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.settings.impl.composable.elements +package com.flipperdevices.settings.impl.composable.components import androidx.annotation.DrawableRes import androidx.annotation.StringRes diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/PushNotificationElement.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/PushNotificationElement.kt new file mode 100644 index 0000000000..7ce43c7aed --- /dev/null +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/PushNotificationElement.kt @@ -0,0 +1,44 @@ +package com.flipperdevices.settings.impl.composable.elements + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.ktx.clickableRipple +import com.flipperdevices.core.ui.theme.LocalTypography +import com.flipperdevices.notification.model.UpdateNotificationState +import com.flipperdevices.settings.impl.R +import com.flipperdevices.settings.impl.composable.components.AnimatedSwitch +import com.flipperdevices.settings.impl.composable.components.SimpleElement + +@Composable +fun PushNotificationElement( + notificationState: UpdateNotificationState, + modifier: Modifier = Modifier, + onChangeNotificationState: (Boolean) -> Unit +) { + Row( + modifier = modifier.clickableRipple { + when (notificationState) { + UpdateNotificationState.ENABLED -> onChangeNotificationState(false) + UpdateNotificationState.DISABLED -> onChangeNotificationState(true) + UpdateNotificationState.IN_PROGRESS -> {} + } + }, + verticalAlignment = Alignment.CenterVertically + ) { + SimpleElement( + modifier = Modifier.weight(weight = 1f), + titleId = R.string.app_notification_title, + descriptionId = R.string.app_notification_desc, + titleTextStyle = LocalTypography.current.buttonB16 + ) + AnimatedSwitch( + modifier = Modifier.padding(all = 12.dp), + state = notificationState, + onSwitch = onChangeNotificationState + ) + } +} diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/ThemeChangerElement.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/ThemeChangerElement.kt new file mode 100644 index 0000000000..0026f36536 --- /dev/null +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/composable/elements/ThemeChangerElement.kt @@ -0,0 +1,94 @@ +package com.flipperdevices.settings.impl.composable.elements + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.preference.pb.SelectedTheme +import com.flipperdevices.core.ui.ktx.clickableRipple +import com.flipperdevices.core.ui.theme.LocalPallet +import com.flipperdevices.core.ui.theme.LocalTypography +import com.flipperdevices.settings.impl.R +import com.flipperdevices.settings.impl.composable.components.SimpleElement +import com.flipperdevices.core.ui.res.R as DesignSystem + +@Composable +fun ThemeChangerElement( + theme: SelectedTheme, + modifier: Modifier = Modifier, + onSelectTheme: (SelectedTheme) -> Unit, +) { + val themes = SelectedTheme.entries.filter { it != SelectedTheme.UNRECOGNIZED } + + val nameTheme = stringResource(id = getNameBySelectedTheme(theme)) + var showMenu by remember { mutableStateOf(false) } + + Row( + modifier = modifier.clickableRipple { showMenu = true }, + verticalAlignment = Alignment.CenterVertically + ) { + SimpleElement( + modifier = Modifier.weight(weight = 1f), + titleId = R.string.app_theme_options, + descriptionId = R.string.app_theme_options_desc, + titleTextStyle = LocalTypography.current.buttonB16 + ) + + Box { + Row( + modifier = Modifier.padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = nameTheme, + style = LocalTypography.current.buttonM16 + ) + + Icon( + painter = painterResource(DesignSystem.drawable.ic_more), + contentDescription = nameTheme, + tint = LocalPallet.current.iconTint30 + ) + } + + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + modifier = Modifier.background(LocalPallet.current.backgroundDialog) + ) { + themes.forEach { + DropdownMenuItem(onClick = { + showMenu = false + onSelectTheme(it) + }) { + Text(text = stringResource(id = getNameBySelectedTheme(it))) + } + } + } + } + } +} + +@Composable +fun getNameBySelectedTheme(theme: SelectedTheme): Int { + return when (theme) { + SelectedTheme.DARK -> R.string.app_theme_options_dark + SelectedTheme.LIGHT -> R.string.app_theme_options_light + else -> R.string.app_theme_options_system + } +} diff --git a/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/viewmodels/NotificationViewModel.kt b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/viewmodels/NotificationViewModel.kt new file mode 100644 index 0000000000..add701820a --- /dev/null +++ b/components/settings/impl/src/main/java/com/flipperdevices/settings/impl/viewmodels/NotificationViewModel.kt @@ -0,0 +1,20 @@ +package com.flipperdevices.settings.impl.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.notification.api.FlipperAppNotificationApi +import tangle.viewmodel.VMInject + +class NotificationViewModel @VMInject constructor( + private val notificationApi: FlipperAppNotificationApi +) : ViewModel(), LogTagProvider { + override val TAG = "NotificationViewModel" + + fun getNotificationToggleState() = notificationApi + .isSubscribedToUpdateNotificationTopic(viewModelScope) + + fun switchToggle(newState: Boolean) { + notificationApi.setSubscribeToUpdateAsync(newState, viewModelScope) + } +} diff --git a/components/settings/impl/src/main/res/values/strings.xml b/components/settings/impl/src/main/res/values/strings.xml index d646a038fe..d30b2212f8 100644 --- a/components/settings/impl/src/main/res/values/strings.xml +++ b/components/settings/impl/src/main/res/values/strings.xml @@ -2,12 +2,15 @@ Options - Color Theme - System/Light/Dark - System - Light - Dark - Unknown + Color Theme + System/Light/Dark + System + Light + Dark + Unknown + App + Push Notifications + Notify about new firmware releases Debug Options Features for developers and testing diff --git a/components/share/receive/src/main/java/com/flipperdevices/share/receive/helpers/ReceiveKeyActionHelper.kt b/components/share/receive/src/main/java/com/flipperdevices/share/receive/helpers/ReceiveKeyActionHelper.kt index 75e0763d1b..7cae6a42e9 100644 --- a/components/share/receive/src/main/java/com/flipperdevices/share/receive/helpers/ReceiveKeyActionHelper.kt +++ b/components/share/receive/src/main/java/com/flipperdevices/share/receive/helpers/ReceiveKeyActionHelper.kt @@ -8,10 +8,9 @@ import com.flipperdevices.inappnotification.api.InAppNotificationStorage import com.flipperdevices.inappnotification.api.model.InAppNotification import com.flipperdevices.keyparser.api.KeyParser import com.flipperdevices.keyparser.api.model.FlipperKeyParsed +import com.flipperdevices.share.receive.R import javax.inject.Inject -private const val NOTIFICATION_DURATION_MS = 3 * 1000L - class ReceiveKeyActionHelper @Inject constructor( private val notificationStorage: InAppNotificationStorage, private val simpleKeyApi: SimpleKeyApi, @@ -21,9 +20,9 @@ class ReceiveKeyActionHelper @Inject constructor( suspend fun saveKey(key: FlipperKey): Result = runCatching { simpleKeyApi.insertKey(key) notificationStorage.addNotification( - InAppNotification.SavedKey( + InAppNotification.Successful( title = key.path.nameWithoutExtension, - durationMs = NOTIFICATION_DURATION_MS + descId = R.string.saved_key_desc ) ) } diff --git a/components/share/receive/src/main/res/values/strings.xml b/components/share/receive/src/main/res/values/strings.xml index e4349a6e29..fcec8a72a1 100644 --- a/components/share/receive/src/main/res/values/strings.xml +++ b/components/share/receive/src/main/res/values/strings.xml @@ -5,6 +5,8 @@ Edit Error when saving. A key with this name already exists + saved to Archive + Retry Not Internet Connection Unable to download this key diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6547f714ac..fd287e0f37 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,6 +76,10 @@ detekt-ruleset-compiler = "0.0.2" # https://github.com/BraisGabin/detekt-compile detekt-ruleset-ktlint = "0.50.0" # https://github.com/pinterest/ktlint/releases detekt-ruleset-compose = "0.3.0" # https://github.com/mrmans0n/compose-rules/releases +# Firebase +google-gms-gradle = "4.4.0" # https://developers.google.com/android/guides/google-services-plugin +google-gms-firebase = "23.3.1" # https://firebase.google.com/docs/android/setup#kotlin:~:text=com.google.firebase%3Afirebase%2Dmessaging + [libraries] # Gradle - Core android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" } @@ -110,7 +114,7 @@ detekt-ruleset-ktlint = { module = "io.gitlab.arturbosch.detekt:detekt-formattin detekt-ruleset-compose = { module = "io.nlopez.compose.rules:detekt", version.ref = "detekt-ruleset-compose" } # UI -splashscreen = { module ="androidx.core:core-splashscreen", version.ref = "splashscreen"} +splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" } compose-activity = { module = "androidx.activity:activity-compose", version.ref = "ktx-activity" } material = { module = "com.google.android.material:material", version.ref = "material" } compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-ui" } @@ -243,7 +247,7 @@ work-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work" } # Google play core app-update = { module = "com.google.android.play:app-update", version.ref = "play-core" } app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "play-core" } - +gms-firebase = { module = "com.google.firebase:firebase-messaging", version.ref = "google-gms-firebase" } [plugins] @@ -254,4 +258,5 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin-ge kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-general" } protobuf = { id = "com.google.protobuf", version.ref = "protobuf-gradle" } -square-anvil = { id = "com.squareup.anvil", version.ref = "anvil" } \ No newline at end of file +square-anvil = { id = "com.squareup.anvil", version.ref = "anvil" } +google-gms = { id = "com.google.gms.google-services", version.ref = "google-gms-gradle" } diff --git a/instances/app/build.gradle.kts b/instances/app/build.gradle.kts index 15990f9494..e6a83856b0 100644 --- a/instances/app/build.gradle.kts +++ b/instances/app/build.gradle.kts @@ -8,6 +8,7 @@ plugins { id("flipper.android-app") id("com.squareup.anvil") id("kotlin-kapt") + alias(libs.plugins.google.gms) } android.namespace = "com.flipperdevices.app" @@ -18,6 +19,8 @@ dependencies { implementation(projects.components.core.log) implementation(projects.components.core.preference) implementation(projects.components.core.activityholder) + implementation(projects.components.core.permission.api) + implementation(projects.components.core.permission.impl) implementation(projects.components.core.ui.ktx) implementation(projects.components.core.ui.res) implementation(projects.components.core.ui.lifecycle) @@ -214,8 +217,10 @@ dependencies { implementation(projects.components.selfupdater.thirdparty.api) implementation(projects.components.selfupdater.thirdparty.github) } + SourceInstall.DEBUG -> implementation(projects.components.selfupdater.debug) + else -> implementation(projects.components.selfupdater.unknown) } @@ -223,6 +228,13 @@ dependencies { implementation(projects.components.unhandledexception.api) implementation(projects.components.unhandledexception.impl) + implementation(projects.components.notification.api) + if (IS_GOOGLE_FEATURE_AVAILABLE) { + implementation(projects.components.notification.impl) + } else { + implementation(projects.components.notification.noop) + } + implementation(libs.ktor.client) implementation(libs.annotations) diff --git a/instances/app/google-services.json b/instances/app/google-services.json new file mode 100644 index 0000000000..2823a1eed2 --- /dev/null +++ b/instances/app/google-services.json @@ -0,0 +1,39 @@ +{ + "project_info": { + "project_number": "25283638650", + "project_id": "flipper-zero-app", + "storage_bucket": "flipper-zero-app.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:25283638650:android:442ac89403612b0f7812c3", + "android_client_info": { + "package_name": "com.flipperdevices.app" + } + }, + "oauth_client": [ + { + "client_id": "25283638650-dpff71677fpd6hq2t9k09qmr6gdeotj9.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCs_esXinP9RYOCaHIcJXe_MTRxC2BjgZ4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "25283638650-dpff71677fpd6hq2t9k09qmr6gdeotj9.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/instances/app/src/main/java/com/flipperdevices/app/FlipperApplication.kt b/instances/app/src/main/java/com/flipperdevices/app/FlipperApplication.kt index 6a094bd98a..538a6a16e6 100644 --- a/instances/app/src/main/java/com/flipperdevices/app/FlipperApplication.kt +++ b/instances/app/src/main/java/com/flipperdevices/app/FlipperApplication.kt @@ -10,12 +10,20 @@ import com.flipperdevices.core.activityholder.CurrentActivityHolder import com.flipperdevices.core.di.ApplicationParams import com.flipperdevices.core.di.ComponentHolder import com.flipperdevices.core.di.provideDelegate +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.error import com.flipperdevices.core.log.info import com.flipperdevices.singleactivity.impl.SingleActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import tangle.inject.TangleGraph import timber.log.Timber -class FlipperApplication : Application(), ImageLoaderFactory { +class FlipperApplication : Application(), ImageLoaderFactory, LogTagProvider { + override val TAG = "FlipperApplication" + + private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) override fun onCreate() { super.onCreate() @@ -25,6 +33,7 @@ class FlipperApplication : Application(), ImageLoaderFactory { .create( context = this, application = this, + scope = applicationScope, ApplicationParams( startApplicationClass = SingleActivity::class, version = BuildConfig.VERSION_NAME @@ -48,6 +57,13 @@ class FlipperApplication : Application(), ImageLoaderFactory { val component = ComponentHolder.component() val synchronizationApi by component.synchronizationApi synchronizationApi.startSynchronization() + try { + val notificationApi by component.notificationApi + notificationApi.init() + } catch (e: Exception) { + error(e) { "Failed init notification api" } + } + component.permissionRequestHandlerImpl.get().register(this) } override fun newImageLoader(): ImageLoader { diff --git a/instances/app/src/main/java/com/flipperdevices/app/di/AppComponent.kt b/instances/app/src/main/java/com/flipperdevices/app/di/AppComponent.kt index 9abf762cb5..847d3373fc 100644 --- a/instances/app/src/main/java/com/flipperdevices/app/di/AppComponent.kt +++ b/instances/app/src/main/java/com/flipperdevices/app/di/AppComponent.kt @@ -7,6 +7,7 @@ import com.flipperdevices.core.di.ApplicationParams import com.squareup.anvil.annotations.MergeComponent import dagger.BindsInstance import dagger.Component +import kotlinx.coroutines.CoroutineScope import javax.inject.Singleton /** @@ -23,6 +24,7 @@ interface AppComponent { fun create( @BindsInstance context: Context, @BindsInstance application: Application, + @BindsInstance scope: CoroutineScope, @BindsInstance applicationParams: ApplicationParams ): AppComponent } diff --git a/instances/app/src/main/java/com/flipperdevices/app/di/MainComponent.kt b/instances/app/src/main/java/com/flipperdevices/app/di/MainComponent.kt index 0c820395d4..c280c67335 100644 --- a/instances/app/src/main/java/com/flipperdevices/app/di/MainComponent.kt +++ b/instances/app/src/main/java/com/flipperdevices/app/di/MainComponent.kt @@ -2,6 +2,8 @@ package com.flipperdevices.app.di import com.flipperdevices.bridge.synchronization.api.SynchronizationApi import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.core.permission.impl.PermissionRequestHandlerImpl +import com.flipperdevices.notification.api.FlipperAppNotificationApi import com.flipperdevices.shake2report.api.Shake2ReportApi import com.squareup.anvil.annotations.ContributesTo import javax.inject.Provider @@ -10,4 +12,6 @@ import javax.inject.Provider interface MainComponent { val shake2report: Provider val synchronizationApi: Provider + val notificationApi: Provider + val permissionRequestHandlerImpl: Provider } diff --git a/settings.gradle.kts b/settings.gradle.kts index cb72b2d263..0cd1829608 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -67,6 +67,8 @@ include( ":components:core:share", ":components:core:pager", ":components:core:progress", + ":components:core:permission:api", + ":components:core:permission:impl", ":components:bottombar:api", ":components:bottombar:impl", @@ -226,5 +228,9 @@ include( ":components:selfupdater:thirdparty:github", ":components:unhandledexception:api", - ":components:unhandledexception:impl" + ":components:unhandledexception:impl", + + ":components:notification:api", + ":components:notification:impl", + ":components:notification:noop", )