Merge pull request #652 from KotlinGeekDev/oss-push-notifications

Oss push notifications(using UnifiedPush)
This commit is contained in:
Vitor Pamplona 2023-10-26 17:16:44 -04:00 committed by GitHub
commit 98bb30fa59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 356 additions and 8 deletions

View File

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

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".Amethyst">
<receiver
android:exported="true"
android:enabled="true"
android:name=".service.notifications.PushMessageReceiver">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/>
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED"/>
</intent-filter>
</receiver>
</application>
</manifest>

View File

@ -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<String>
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<String> {
return unifiedPush.getDistributors(appContext)
}
fun formattedDistributorNames(): List<String> {
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)
}
}

View File

@ -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<String, String>(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
}
}

View File

@ -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<AccountInfo>) {
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.")
}
}
}

View File

@ -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<String>,
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)
}
}
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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

View File

@ -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
)

View File

@ -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 {