diff --git a/app/build.gradle b/app/build.gradle index 8cf0ca9a8..b330bcb9d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -185,6 +185,9 @@ dependencies { playImplementation platform('com.google.firebase:firebase-bom:32.3.1') playImplementation 'com.google.firebase:firebase-messaging-ktx' + //PushNotifications(FDroid) + fdroidImplementation 'com.github.UnifiedPush:android-connector:2.2.0' + // Charts implementation "com.patrykandpatrick.vico:core:${vico_version}" implementation "com.patrykandpatrick.vico:compose:${vico_version}" diff --git a/app/src/fdroid/AndroidManifest.xml b/app/src/fdroid/AndroidManifest.xml new file mode 100644 index 000000000..c4ef213f9 --- /dev/null +++ b/app/src/fdroid/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt new file mode 100644 index 000000000..0219c6c55 --- /dev/null +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt @@ -0,0 +1,76 @@ +package com.vitorpamplona.amethyst.service.notifications + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import com.vitorpamplona.amethyst.Amethyst +import org.unifiedpush.android.connector.UnifiedPush + +interface PushDistributorActions { + fun getSavedDistributor(): String + fun getInstalledDistributors(): List + fun saveDistributor(distributor: String) + fun removeSavedDistributor() +} +object PushDistributorHandler : PushDistributorActions { + private val appContext = Amethyst.instance.applicationContext + private val unifiedPush: UnifiedPush = UnifiedPush + + private var endpointInternal = "" + val endpoint = endpointInternal + + fun getSavedEndpoint() = endpoint + fun setEndpoint(newEndpoint: String) { + endpointInternal = newEndpoint + Log.d("PushHandler", "New endpoint saved : $endpointInternal") + } + + fun removeEndpoint() { + endpointInternal = "" + } + + override fun getSavedDistributor(): String { + return unifiedPush.getDistributor(appContext) + } + + fun savedDistributorExists(): Boolean = getSavedDistributor().isNotEmpty() + + override fun getInstalledDistributors(): List { + return unifiedPush.getDistributors(appContext) + } + + fun formattedDistributorNames(): List { + val distributorsArray = getInstalledDistributors().toTypedArray() + val distributorsNameArray = distributorsArray.map { + try { + val ai = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + appContext.packageManager.getApplicationInfo( + it, + PackageManager.ApplicationInfoFlags.of( + PackageManager.GET_META_DATA.toLong() + ) + ) + } else { + appContext.packageManager.getApplicationInfo(it, 0) + } + appContext.packageManager.getApplicationLabel(ai) + } catch (e: PackageManager.NameNotFoundException) { + it + } as String + }.toTypedArray() + return distributorsNameArray.toList() + } + + override fun saveDistributor(distributor: String) { + unifiedPush.saveDistributor(appContext, distributor) + unifiedPush.registerApp(appContext) + } + + override fun removeSavedDistributor() { + unifiedPush.safeRemoveDistributor(appContext) + } + fun forceRemoveDistributor(context: Context) { + unifiedPush.forceRemoveDistributor(context) + } +} diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt new file mode 100644 index 000000000..0263c6fe4 --- /dev/null +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt @@ -0,0 +1,92 @@ +package com.vitorpamplona.amethyst.service.notifications + +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.util.Log +import android.util.LruCache +import androidx.core.content.ContextCompat +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel +import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.GiftWrapEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import org.unifiedpush.android.connector.MessagingReceiver + +class PushMessageReceiver : MessagingReceiver() { + private val TAG = "Amethyst-OSSPushReceiver" + private val appContext = Amethyst.instance.applicationContext + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val eventCache = LruCache(100) + private val pushHandler = PushDistributorHandler + + override fun onMessage(context: Context, message: ByteArray, instance: String) { + val messageStr = String(message) + Log.d(TAG, "New message ${message.decodeToString()} for Instance: $instance") + scope.launch { + try { + parseMessage(messageStr)?.let { + receiveIfNew(it) + } + } catch (e: Exception) { + Log.d(TAG, "Message could not be parsed: ${e.message}") + } + } + } + + private suspend fun parseMessage(message: String): GiftWrapEvent? { + (Event.fromJson(message) as? GiftWrapEvent)?.let { + return it + } + return null + } + + private suspend fun receiveIfNew(event: GiftWrapEvent) { + if (eventCache.get(event.id) == null) { + eventCache.put(event.id, event.id) + EventNotificationConsumer(appContext).consume(event) + } + } + + override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { + Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance") + val sanitizedEndpoint = endpoint.dropLast(5) + pushHandler.setEndpoint(sanitizedEndpoint) + scope.launch(Dispatchers.IO) { + RegisterAccounts(LocalPreferences.allSavedAccounts()).go(sanitizedEndpoint) + notificationManager().getOrCreateZapChannel(appContext) + notificationManager().getOrCreateDMChannel(appContext) + } + } + + override fun onReceive(context: Context, intent: Intent) { + val intentData = intent.dataString + val intentAction = intent.action.toString() + Log.d(TAG, "Intent Data:- $intentData Intent Action: $intentAction") + super.onReceive(context, intent) + } + + override fun onRegistrationFailed(context: Context, instance: String) { + Log.d(TAG, "Registration failed for Instance: $instance") + scope.cancel() + pushHandler.forceRemoveDistributor(context) + } + + override fun onUnregistered(context: Context, instance: String) { + val removedEndpoint = pushHandler.endpoint + Log.d(TAG, "Endpoint: $removedEndpoint removed for Instance: $instance") + Log.d(TAG, "App is unregistered. ") + pushHandler.forceRemoveDistributor(context) + pushHandler.removeEndpoint() + } + + fun notificationManager(): NotificationManager { + return ContextCompat.getSystemService(appContext, NotificationManager::class.java) as NotificationManager + } +} diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index 2855d5e5a..6e153d28f 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -1,9 +1,21 @@ package com.vitorpamplona.amethyst.service.notifications +import android.util.Log import com.vitorpamplona.amethyst.AccountInfo object PushNotificationUtils { - var hasInit: Boolean = true + var hasInit: Boolean = false + private val pushHandler = PushDistributorHandler suspend fun init(accounts: List) { + if (hasInit || pushHandler.savedDistributorExists()) { + return + } + try { + if (pushHandler.savedDistributorExists()) { + RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint()) + } + } catch (e: Exception) { + Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.") + } } } diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CustomNotificationScreen.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CustomNotificationScreen.kt new file mode 100644 index 000000000..a91cf4984 --- /dev/null +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CustomNotificationScreen.kt @@ -0,0 +1,117 @@ +package com.vitorpamplona.amethyst.ui.screen.loggedIn + +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.TextButton +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.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.window.Dialog +import com.halilibo.richtext.markdown.Markdown +import com.halilibo.richtext.ui.RichTextStyle +import com.halilibo.richtext.ui.material3.Material3RichText +import com.halilibo.richtext.ui.resolveDefaults +import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler +import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel +import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel +import com.vitorpamplona.amethyst.ui.theme.HalfPadding +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun CustomNotificationScreen( + notifFeedViewModel: NotificationViewModel, + userReactionsStatsModel: UserReactionsViewModel, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { + val pushHandler = PushDistributorHandler + var distributorPresent by remember { + mutableStateOf(pushHandler.savedDistributorExists()) + } + val list = pushHandler.getInstalledDistributors() + val readableList = pushHandler.formattedDistributorNames() + if (!distributorPresent) { + SelectPushDistributor( + distrbutorList = readableList.toImmutableList(), + onDistributorSelected = { index, name -> + val fullDistributorName = list[index] + pushHandler.saveDistributor(fullDistributorName) + Log.d("Amethyst", "NotificationScreen: Distributor registered.") + }, + onDismiss = { + distributorPresent = true + Log.d("Amethyst", "NotificationScreen: Distributor dialog dismissed.") + } + ) + } else { + val currentDistributor = pushHandler.getSavedDistributor() + pushHandler.saveDistributor(currentDistributor) + } + + NotificationScreen( + notifFeedViewModel = notifFeedViewModel, + userReactionsStatsModel = userReactionsStatsModel, + accountViewModel = accountViewModel, + nav = nav + ) +} + +@Composable +fun SelectPushDistributor( + distrbutorList: ImmutableList, + onDistributorSelected: (Int, String) -> Unit, + onDismiss: () -> Unit +) { + Dialog(onDismissRequest = onDismiss) { + Surface( + modifier = Modifier + .border( + width = Dp.Hairline, + color = MaterialTheme.colorScheme.background, + shape = CircleShape + ) + .fillMaxWidth() + ) { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + ) { + Box(modifier = Modifier.align(CenterHorizontally)) { + Material3RichText( + style = RichTextStyle().resolveDefaults() + ) { + Markdown(content = "### Select a distributor") + } + } + distrbutorList.forEachIndexed { index, distributor -> + TextButton( + onClick = { + onDistributorSelected(index, distributor) + onDismiss() + }, + modifier = HalfPadding.fillMaxWidth() + ) { + Material3RichText( + style = RichTextStyle().resolveDefaults() + ) { + Markdown(content = distributor) + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index 9d023231b..d0394692c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -62,7 +62,12 @@ class RegisterAccounts( it.isSuccessful } } catch (e: java.lang.Exception) { - Log.e("FirebaseMsgService", "Unable to register with push server", e) + val tag = if (BuildConfig.FLAVOR == "play") { + "FirebaseMsgService" + } else { + "UnifiedPushService" + } + Log.e(tag, "Unable to register with push server", e) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 3dd0c08ff..57cba335e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -38,13 +38,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreenByAuthor import com.vitorpamplona.amethyst.ui.screen.loggedIn.CommunityScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.CustomNotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.DiscoverScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRedirectScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.SettingsScreen @@ -157,7 +157,7 @@ fun AppNavigation( Route.Notification.let { route -> composable(route.route, route.arguments, content = { - NotificationScreen( + CustomNotificationScreen( notifFeedViewModel = notifFeedViewModel, userReactionsStatsModel = userReactionsStatsModel, accountViewModel = accountViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt index ccdac5450..5e56c5f0f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -32,12 +32,9 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.patrykandpatrick.vico.compose.axis.axisLabelComponent -import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis import com.patrykandpatrick.vico.compose.axis.horizontal.rememberBottomAxis -import com.patrykandpatrick.vico.compose.axis.vertical.endAxis import com.patrykandpatrick.vico.compose.axis.vertical.rememberEndAxis import com.patrykandpatrick.vico.compose.axis.vertical.rememberStartAxis -import com.patrykandpatrick.vico.compose.axis.vertical.startAxis import com.patrykandpatrick.vico.compose.chart.Chart import com.patrykandpatrick.vico.compose.chart.line.lineChart import com.patrykandpatrick.vico.compose.component.shape.shader.fromBrush diff --git a/app/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CustomNotificationScreen.kt b/app/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CustomNotificationScreen.kt new file mode 100644 index 000000000..fd56ec9ae --- /dev/null +++ b/app/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CustomNotificationScreen.kt @@ -0,0 +1,18 @@ +package com.vitorpamplona.amethyst.ui.screen.loggedIn + +import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel +import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel + +@Composable +fun CustomNotificationScreen( + notifFeedViewModel: NotificationViewModel, + userReactionsStatsModel: UserReactionsViewModel, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) = NotificationScreen( + notifFeedViewModel = notifFeedViewModel, + userReactionsStatsModel = userReactionsStatsModel, + accountViewModel = accountViewModel, + nav = nav +) diff --git a/settings.gradle b/settings.gradle index 017aa10c6..d535b492f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,12 @@ pluginManagement { gradlePluginPortal() google() mavenCentral() - maven { url "https://jitpack.io" } + maven { + url "https://jitpack.io" + content { + includeModule 'com.github.UnifiedPush', 'android-connector' + } + } } } dependencyResolutionManagement {