Skip to content

Commit

Permalink
Add push notification about new updates (#735)
Browse files Browse the repository at this point in the history
**Background**

Right now users want recieve notification about new updates in firmware
channel

**Changes**

- Add notification enable dialog
- Add row in settings app category

**Test plan**

Try enable notification via app and via settings
  • Loading branch information
LionZXY authored Nov 13, 2023
1 parent aff3e94 commit a6533b5
Show file tree
Hide file tree
Showing 74 changed files with 2,991 additions and 169 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
2 changes: 2 additions & 0 deletions components/bottombar/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,7 +36,8 @@ class BottomNavigationFeatureEntryImpl @Inject constructor(
composableEntriesProvider: Provider<MutableSet<ComposableFeatureEntry>>,
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
Expand All @@ -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) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,6 +34,7 @@ fun ComposableMainScreen(
connectionApi: ConnectionApi,
notificationRenderer: InAppNotificationRenderer,
unhandledExceptionRendererApi: UnhandledExceptionRenderApi,
appNotificationApi: FlipperAppNotificationDialogApi,
featureEntries: ImmutableSet<AggregateFeatureEntry>,
composableEntries: ImmutableSet<ComposableFeatureEntry>,
navController: NavHostController,
Expand Down Expand Up @@ -88,6 +90,7 @@ fun ComposableMainScreen(
notificationRenderer = notificationRenderer
)
connectionApi.CheckAndShowUnsupportedDialog()
appNotificationApi.NotificationDialog()
unhandledExceptionRendererApi.ComposableUnhandledExceptionRender(Modifier)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ object PermissionHelper {
}

/**
* @return true if all permissions granted
* @return empty if all permissions granted
*/
fun getUngrantedPermission(context: Context, permissions: Array<String>): List<String> {
val ungrantedPermission = mutableListOf<String>()
Expand Down
1 change: 1 addition & 0 deletions components/core/activityholder/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ plugins {
android.namespace = "com.flipperdevices.core.activityholder"

dependencies {
implementation(libs.appcompat)
}
1 change: 1 addition & 0 deletions components/core/permission/api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
8 changes: 8 additions & 0 deletions components/core/permission/api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
plugins {
id("flipper.android-lib")
}

android.namespace = "com.flipperdevices.core.permission.api"

dependencies {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.flipperdevices.core.permission.api

typealias PermissionListener = (String, Boolean) -> Unit

interface PermissionRequestHandler {
fun requestPermission(vararg permissions: String, listener: PermissionListener)
}
1 change: 1 addition & 0 deletions components/core/permission/impl/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
16 changes: 16 additions & 0 deletions components/core/permission/impl/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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<Map<String, @JvmSuppressWildcards Boolean>> {

private var permissionRequest: ActivityResultLauncher<Array<String>>? = null
private val permissionPendingListeners = mutableMapOf<String, List<PermissionListener>>()

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<String, Boolean>) {
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
}
2 changes: 2 additions & 0 deletions components/core/preference/src/main/proto/settings.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand All @@ -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
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = {}
)
}
}
Loading

0 comments on commit a6533b5

Please sign in to comment.