migrate to distributor v0.7.x

This commit is contained in:
Egor 2026-02-12 03:47:20 -08:00
parent befcaf840c
commit b2ae8c3e0c
39 changed files with 439 additions and 495 deletions

View file

@ -97,7 +97,9 @@ android {
dependencies {
implementation(libs.unifiedpush.distributor)
implementation(libs.unifiedpush.distributor.base)
implementation(libs.unifiedpush.distributor.ui)
implementation(libs.accompanist.permissions)
implementation(libs.tink.android)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)

View file

@ -21,6 +21,7 @@
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".activities.MainActivity"
android:process=":ui"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
@ -33,7 +34,16 @@
android:enabled="true"
android:foregroundServiceType="specialUse">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="This service needs to be constantly connected to the server, to get Server-Sent Events messages from it."/>
android:value="This service needs to be constantly connected to the server via WebSocket to receive push notifications."/>
</service>
<service
android:name=".services.PrismInternalService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="org.unifiedpush.distributor.internal.service"/>
</intent-filter>
</service>
<receiver
@ -45,6 +55,15 @@
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver
android:name=".receivers.PrismConfigReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="app.lonecloud.prism.SET_PRISM_SERVER_URL" />
<action android:name="app.lonecloud.prism.SET_PRISM_API_KEY" />
</intent-filter>
</receiver>
<receiver
android:name=".receivers.RegisterBroadcastReceiver"
android:enabled="true"

View file

@ -2,8 +2,8 @@ package app.lonecloud.prism
import android.content.Context
import androidx.core.content.edit
import org.unifiedpush.distributor.MigrationManager
import org.unifiedpush.distributor.Store
import org.unifiedpush.android.distributor.MigrationManager
import org.unifiedpush.android.distributor.Store
class AppStore(context: Context) :
Store(context, PREF_NAME),

View file

@ -3,7 +3,7 @@ package app.lonecloud.prism
import android.content.Context
import app.lonecloud.prism.services.MainRegistrationCounter
import java.util.concurrent.atomic.AtomicReference
import org.unifiedpush.distributor.Database as Database
import org.unifiedpush.android.distributor.Database as Database
object DatabaseFactory {
class MainDatabase(context: Context) : Database(context) {

View file

@ -3,14 +3,11 @@ package app.lonecloud.prism
import android.content.Context
import app.lonecloud.prism.api.MessageSender
import app.lonecloud.prism.api.data.ClientMessage
import org.unifiedpush.distributor.Database
import org.unifiedpush.distributor.UnifiedPushDistributor
import org.unifiedpush.android.distributor.Database
import org.unifiedpush.android.distributor.UnifiedPushDistributor
/**
* These functions are used to send messages to other apps
*/
object Distributor : UnifiedPushDistributor() {
override val receiverComponentName = "app.lonecloud.prism.receivers.RegisterBroadcastReceiver"
override val receiverComponent = app.lonecloud.prism.receivers.RegisterBroadcastReceiver::class.java
override fun getDb(context: Context): Database = DatabaseFactory.getDb(context)

View file

@ -1,27 +0,0 @@
package app.lonecloud.prism
import kotlin.coroutines.coroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterIsInstance
object EventBus {
val mutEvents: MutableSharedFlow<Any> = MutableSharedFlow()
val events = mutEvents.asSharedFlow()
suspend inline fun <reified T : Any> publish(event: T) {
if (mutEvents.subscriptionCount.value > 0) {
mutEvents.emit(event)
}
}
suspend inline fun <reified T> subscribe(crossinline onEvent: (T) -> Unit) {
events.filterIsInstance<T>()
.collectLatest { event ->
coroutineContext.ensureActive()
onEvent(event)
}
}
}

View file

@ -0,0 +1,18 @@
package app.lonecloud.prism
import org.unifiedpush.android.distributor.ui.AppConfig
import org.unifiedpush.android.distributor.ui.InAppNotifsConfig
import org.unifiedpush.android.distributor.ui.MigrationConfig
import org.unifiedpush.android.distributor.ui.NoLoginConfig
import org.unifiedpush.android.distributor.ui.PrivacyPolicy
object PrismConfig : AppConfig {
override val appName = R.string.app_name
override val restartableService = true
override val privacyPolicy: PrivacyPolicy? = null
override val loginConfig = NoLoginConfig(canChangeUrl = true)
override val migrationConfig = object : MigrationConfig {
override val supportTempFallback = true
}
override val inAppNotifsConfig: InAppNotifsConfig? = null
}

View file

@ -1,106 +0,0 @@
package app.lonecloud.prism.activities
import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.lonecloud.prism.AppStore
import app.lonecloud.prism.DatabaseFactory
import app.lonecloud.prism.Distributor
import app.lonecloud.prism.EventBus
import app.lonecloud.prism.PrismServerClient
import app.lonecloud.prism.services.FgService
import app.lonecloud.prism.services.MigrationManager
import app.lonecloud.prism.services.RestartWorker
import app.lonecloud.prism.services.SourceManager
import app.lonecloud.prism.utils.TAG
import kotlinx.coroutines.launch
class AppAction(private val action: Action) {
sealed class Action {
data object RestartService : Action()
class ShowToasts(val enable: Boolean) : Action()
class DeleteRegistration(val registrations: List<String>) : Action()
data object FallbackIntroShown : Action()
class FallbackDistribSelected(val distributor: String?) : Action()
class MigrateToDistrib(val distributor: String) : Action()
data object ReactivateUnifiedPush : Action()
data object RegisterPrismServer : Action()
}
fun handle(context: Context) {
when (action) {
is Action.RestartService -> restartService(context)
is Action.ShowToasts -> showToasts(context, action)
is Action.DeleteRegistration -> deleteRegistration(context, action)
is Action.FallbackIntroShown -> fallbackIntroShown(context)
is Action.FallbackDistribSelected -> fallbackDistribSelected(context, action)
is Action.MigrateToDistrib -> migrateToDistrib(context, action)
is Action.ReactivateUnifiedPush -> reactivateUnifiedPush(context)
is Action.RegisterPrismServer -> registerPrismServer(context)
}
}
private fun restartService(context: Context) {
Log.d(TAG, "Restarting the Listener")
SourceManager.clearFails()
FgService.stopService {
RestartWorker.run(context, delay = 0)
}
}
private fun showToasts(context: Context, action: Action.ShowToasts) {
AppStore(context).showToasts = action.enable
}
private fun deleteRegistration(context: Context, action: Action.DeleteRegistration) {
action.registrations.forEach { token ->
val db = DatabaseFactory.getDb(context)
val dbApp = db.listApps().find { it.connectorToken == token }
if (dbApp?.description?.startsWith("target:") == true) {
val appName = dbApp.title ?: dbApp.packageName
PrismServerClient.deleteApp(context, appName)
}
Distributor.deleteApp(context, token)
}
}
private fun fallbackIntroShown(context: Context) {
MigrationManager().setFallbackIntroShown(context)
}
/**
* Save fallback service
*
* If fallback is disabled and we have already send TEMP_UNAVAILABLE:
* we send the endpoint again
*/
private fun fallbackDistribSelected(context: Context, action: Action.FallbackDistribSelected) {
MigrationManager()
.selectFallbackService(
context,
action.distributor,
SourceManager.shouldSendFallback
)
}
private fun migrateToDistrib(context: Context, action: Action.MigrateToDistrib) {
MigrationManager().migrate(context, action.distributor)
}
private fun reactivateUnifiedPush(context: Context) {
MigrationManager().reactivate(context)
}
private fun registerPrismServer(context: Context) {
app.lonecloud.prism.PrismServerClient.registerAllApps(context)
}
}
fun ViewModel.publishAction(action: AppAction) {
viewModelScope.launch {
EventBus.publish(action)
}
}

View file

@ -1,25 +0,0 @@
package app.lonecloud.prism.activities
import android.content.Context
import org.unifiedpush.android.distributor.ui.compose.state.ApplicationRowState
import org.unifiedpush.distributor.utils.appInfoForMetadata
import org.unifiedpush.distributor.utils.getApplicationIcon
import org.unifiedpush.distributor.utils.getApplicationName
fun Context.applicationRowState(packageName: String, description: String? = null): ApplicationRowState {
val ai = appInfoForMetadata(packageName)
val title = ai?.let { getApplicationName(it) } ?: packageName
val icon = getApplicationIcon(packageName)
val description = if (title == packageName) {
description ?: ""
} else {
description?.let { "$it$packageName" }
?: packageName
}
return ApplicationRowState(
icon = icon,
title = title,
packageName = packageName,
description = description
)
}

View file

@ -1,72 +0,0 @@
package app.lonecloud.prism.activities
import android.app.Application
import android.content.Context
import app.lonecloud.prism.AppStore
import org.unifiedpush.android.distributor.ui.compose.DistribMigrationViewModel as UPDistribMigrationViewModel
import org.unifiedpush.android.distributor.ui.compose.state.DistribMigrationState
import org.unifiedpush.distributor.utils.listOtherDistributors
class DistribMigrationViewModel(state: DistribMigrationState, val application: Application? = null) : UPDistribMigrationViewModel(state) {
constructor(application: Application) : this(
stateFrom(application),
application
)
override fun onFallbackDistribSelected(distributor: String?) {
publishAction(
AppAction(AppAction.Action.FallbackDistribSelected(distributor))
)
}
override fun onMigrationDistributorSelected(distributor: String) {
publishAction(
AppAction(AppAction.Action.MigrateToDistrib(distributor))
)
}
override fun onFallbackIntroShown() {
publishAction(
AppAction(AppAction.Action.FallbackIntroShown)
)
}
override fun onServiceReactivated() {
publishAction(
AppAction(AppAction.Action.ReactivateUnifiedPush)
)
}
override fun refreshDistributors() {
application?.let { context ->
refreshDistributors {
val store = AppStore(context)
val fallbackDistrib = store.fallbackService
return@refreshDistributors context.listOtherDistributors()
.map { packageName ->
context.applicationRowState(packageName).copy(
selected = fallbackDistrib == packageName
)
}.toSet()
}
}
}
companion object {
fun stateFrom(context: Context): DistribMigrationState {
val store = AppStore(context)
val fallbackDistrib = store.fallbackService
val distributors = context.listOtherDistributors().map { packageName ->
context.applicationRowState(packageName).copy(
selected = fallbackDistrib == packageName
)
}.toSet()
return DistribMigrationState(
distributors,
store.fallbackIntroShown,
migrated = store.migrated,
featureEnabled = false
)
}
}
}

View file

@ -1,56 +1,36 @@
package app.lonecloud.prism.activities
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.viewmodel.compose.viewModel
import app.lonecloud.prism.EventBus
import app.lonecloud.prism.activities.ThemeViewModel
import app.lonecloud.prism.activities.ui.App
import app.lonecloud.prism.activities.ui.theme.AppTheme
import app.lonecloud.prism.services.RestartWorker
import app.lonecloud.prism.utils.TAG
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.unifiedpush.android.distributor.ipc.InternalMessenger
class MainActivity : ComponentActivity() {
private var jobs: MutableList<Job> = emptyList<Job>().toMutableList()
private lateinit var messenger: InternalMessenger
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
RestartWorker.startPeriodic(this)
messenger = InternalMessenger(this)
enableEdgeToEdge()
setContent {
val factory = ViewModelFactory(this.application)
val factory = ViewModelFactory(this.application, messenger)
val themeViewModel = viewModel<ThemeViewModel>(factory = factory)
AppTheme(
dynamicColor = themeViewModel.dynamicColors
) {
App(factory, themeViewModel)
}
subscribeActions()
}
}
private fun subscribeActions() {
Log.d(TAG, "Subscribing to actions")
jobs += CoroutineScope(Dispatchers.IO).launch {
EventBus.subscribe<AppAction> { it.handle(this@MainActivity) }
}
}
override fun onDestroy() {
Log.d(TAG, "Destroy")
jobs.removeAll {
it.cancel()
true
}
super.onDestroy()
}
}

View file

@ -25,25 +25,31 @@ import app.lonecloud.prism.utils.VapidKeyGenerator
import app.lonecloud.prism.utils.WebPushEncryptionKeys
import java.util.UUID
import kotlinx.coroutines.launch
import org.unifiedpush.android.distributor.ui.compose.BatteryOptimisationViewModel
import org.unifiedpush.android.distributor.ui.compose.RegistrationsViewModel
import org.unifiedpush.android.distributor.ui.compose.state.RegistrationListState
import org.unifiedpush.android.distributor.data.App
import org.unifiedpush.android.distributor.ipc.InternalMessenger
import org.unifiedpush.android.distributor.ipc.InternalOpcode
import org.unifiedpush.android.distributor.ui.state.RegistrationListState
import org.unifiedpush.android.distributor.ui.vm.BatteryOptimisationViewModel
import org.unifiedpush.android.distributor.ui.vm.RegistrationsViewModel
class MainViewModel(
mainUiState: MainUiState,
val batteryOptimisationViewModel: BatteryOptimisationViewModel,
val registrationsViewModel: RegistrationsViewModel,
val messenger: InternalMessenger?,
val application: Application? = null
) : ViewModel() {
constructor(application: Application) : this(
constructor(requireBatteryOpt: Boolean, messenger: InternalMessenger?, application: Application) : this(
mainUiState = MainUiState(
prismServerConfigured = !AppStore(application).prismServerUrl.isNullOrBlank() &&
!AppStore(application).prismApiKey.isNullOrBlank()
),
batteryOptimisationViewModel = BatteryOptimisationViewModel(application),
batteryOptimisationViewModel = BatteryOptimisationViewModel(requireBatteryOpt, messenger),
registrationsViewModel = RegistrationsViewModel(
getRegistrationListState(application)
RegistrationListState(emptyList<App>()),
messenger
),
messenger,
application
)
@ -69,9 +75,8 @@ class MainViewModel(
fun refreshRegistrations() {
viewModelScope.launch {
application?.let {
registrationsViewModel.state = getRegistrationListState(it)
}
val apps = messenger?.sendIMessageL(InternalOpcode.REG_LIST, "apps", App::class.java)
registrationsViewModel.state = RegistrationListState(apps ?: emptyList())
}
}
@ -79,9 +84,7 @@ class MainViewModel(
viewModelScope.launch {
val state = registrationsViewModel.state
val tokenList = state.list.filter { it.selected }.map { it.token }
publishAction(
AppAction(AppAction.Action.DeleteRegistration(tokenList))
)
messenger?.sendIMessage(InternalOpcode.REG_DELETE, "regs" to tokenList)
registrationsViewModel.state = RegistrationListState(
list = state.list.filter {
!it.selected
@ -130,7 +133,9 @@ class MainViewModel(
}
fun restartService() {
publishAction(AppAction(AppAction.Action.RestartService))
viewModelScope.launch {
messenger?.sendIMessage(InternalOpcode.WORKER_RESTART, 0)
}
}
private fun hasUnifiedPushSupport(pm: PackageManager, packageName: String): Boolean {
@ -238,7 +243,6 @@ class MainViewModel(
}
}
// Timeout
Log.e(TAG, "Endpoint timeout after 30 seconds for token: $connectorToken")
}
}

View file

@ -1,50 +0,0 @@
package app.lonecloud.prism.activities
import android.content.Context
import app.lonecloud.prism.DatabaseFactory
import org.unifiedpush.android.distributor.ui.compose.state.RegistrationListState
import org.unifiedpush.android.distributor.ui.compose.state.RegistrationState
import org.unifiedpush.distributor.Database
fun getRegistrationListState(context: Context): RegistrationListState = RegistrationListState(
list = DatabaseFactory.getDb(context).listApps().map { app ->
getRegistrationState(context, app)
}
)
fun getRegistrationState(context: Context, app: Database.App): RegistrationState {
// Parse manual app format: "target:package.name|optional_description"
val isManualApp = app.description?.startsWith("target:") == true
val targetPackage = app.description?.takeIf { isManualApp }
?.substringAfter("target:")?.substringBefore("|")
?.takeIf { it.isNotBlank() }
val cleanDescription = if (isManualApp) {
app.description?.substringAfter("|", "")?.takeIf { it.isNotBlank() }
} else {
app.description
}
val displayPackage = targetPackage ?: app.packageName
val displayDescription = if (isManualApp && targetPackage == null) null else cleanDescription
val baseAppState = context.applicationRowState(displayPackage, displayDescription)
// For manual apps without target, use custom title instead of package-derived one
val displayTitle = if (isManualApp && targetPackage == null) {
app.title ?: baseAppState.title
} else {
baseAppState.title
}
val finalAppState = baseAppState.copy(
title = displayTitle
)
return RegistrationState(
app = finalAppState,
msgCount = app.msgCount,
token = app.connectorToken,
copyable = false
)
}

View file

@ -1,18 +1,29 @@
package app.lonecloud.prism.activities
import android.app.Application
import android.content.Intent
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.lonecloud.prism.AppStore
import app.lonecloud.prism.PrismServerClient
import app.lonecloud.prism.activities.ui.SettingsState
import app.lonecloud.prism.receivers.PrismConfigReceiver
import kotlinx.coroutines.launch
import org.unifiedpush.android.distributor.ipc.InternalMessenger
import org.unifiedpush.android.distributor.ipc.InternalOpcode
import org.unifiedpush.android.distributor.ipc.sendUiAction
class SettingsViewModel(state: SettingsState, val application: Application? = null) : ViewModel() {
constructor(application: Application) : this(
class SettingsViewModel(
state: SettingsState,
val messenger: InternalMessenger?,
val application: Application? = null
) : ViewModel() {
constructor(messenger: InternalMessenger?, application: Application) : this(
SettingsState.from(application),
messenger,
application
)
@ -22,7 +33,8 @@ class SettingsViewModel(state: SettingsState, val application: Application? = nu
fun toggleShowToasts() {
viewModelScope.launch {
state = state.copy(showToasts = !state.showToasts)
publishAction(AppAction(AppAction.Action.ShowToasts(state.showToasts)))
application?.let { AppStore(it).showToasts = state.showToasts }
messenger?.sendIMessage(InternalOpcode.SHOW_TOASTS_SET, if (state.showToasts) 1 else 0)
}
}
@ -33,11 +45,17 @@ class SettingsViewModel(state: SettingsState, val application: Application? = nu
application?.let {
AppStore(it).prismServerUrl = trimmedUrl.ifBlank { null }
val intent = Intent(PrismConfigReceiver.ACTION_SET_PRISM_SERVER_URL).apply {
putExtra(PrismConfigReceiver.EXTRA_URL, trimmedUrl)
setPackage(it.packageName)
}
it.sendBroadcast(intent)
if (trimmedUrl.isNotBlank() && state.prismApiKey.isNotBlank()) {
publishAction(AppAction(AppAction.Action.RegisterPrismServer))
PrismServerClient.registerAllApps(it)
}
UiAction.publish(UiAction.Action.UpdatePrismServerConfigured)
sendUiAction(it, "UpdatePrismServerConfigured")
}
}
}
@ -49,16 +67,24 @@ class SettingsViewModel(state: SettingsState, val application: Application? = nu
application?.let {
AppStore(it).prismApiKey = trimmedKey.ifBlank { null }
val intent = Intent(PrismConfigReceiver.ACTION_SET_PRISM_API_KEY).apply {
putExtra(PrismConfigReceiver.EXTRA_API_KEY, trimmedKey)
setPackage(it.packageName)
}
it.sendBroadcast(intent)
if (state.prismServerUrl.isNotBlank() && trimmedKey.isNotBlank()) {
publishAction(AppAction(AppAction.Action.RegisterPrismServer))
PrismServerClient.registerAllApps(it)
}
UiAction.publish(UiAction.Action.UpdatePrismServerConfigured)
sendUiAction(it, "UpdatePrismServerConfigured")
}
}
}
fun restartService() {
publishAction(AppAction(AppAction.Action.RestartService))
viewModelScope.launch {
messenger?.sendIMessage(InternalOpcode.WORKER_RESTART, 0)
}
}
}

View file

@ -8,18 +8,17 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.lonecloud.prism.AppStore
import kotlinx.coroutines.launch
import org.unifiedpush.android.distributor.ipc.InternalMessenger
import org.unifiedpush.android.distributor.ipc.InternalOpcode
class ThemeViewModel(val application: Application? = null) : ViewModel() {
var dynamicColors by mutableStateOf(
application?.let { AppStore(it).dynamicColors } ?: false
)
class ThemeViewModel(val messenger: InternalMessenger?, val application: Application?) : ViewModel() {
var dynamicColors by mutableStateOf(application?.let { AppStore(it).dynamicColors } ?: false)
fun toggleDynamicColors() {
viewModelScope.launch {
dynamicColors = !dynamicColors
application?.run {
AppStore(this).dynamicColors = dynamicColors
}
application?.let { AppStore(it).dynamicColors = dynamicColors }
messenger?.sendIMessage(InternalOpcode.THEME_DYN_SET, if (dynamicColors) 1 else 0)
}
}
}

View file

@ -1,25 +0,0 @@
package app.lonecloud.prism.activities
import app.lonecloud.prism.EventBus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class UiAction(val action: Action) {
enum class Action {
RefreshRegistrations,
UpdatePrismServerConfigured
}
fun handle(action: (Action) -> Unit) {
action(this.action)
}
companion object {
fun publish(type: Action) {
CoroutineScope(Dispatchers.IO).launch {
EventBus.publish(UiAction(type))
}
}
}
}

View file

@ -2,24 +2,31 @@ package app.lonecloud.prism.activities
import android.app.Application
import android.content.Context
import android.os.PowerManager
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import app.lonecloud.prism.activities.ThemeViewModel
import app.lonecloud.prism.PrismConfig
import app.lonecloud.prism.activities.ui.MainUiState
import app.lonecloud.prism.activities.ui.SettingsState
import org.unifiedpush.android.distributor.ui.compose.BatteryOptimisationViewModel
import org.unifiedpush.android.distributor.ui.compose.previewRegistrationsViewModel
import org.unifiedpush.android.distributor.ui.compose.state.DistribMigrationState
import org.unifiedpush.android.distributor.ipc.InternalMessenger
import org.unifiedpush.android.distributor.ui.state.DistribMigrationState
import org.unifiedpush.android.distributor.ui.vm.DistribMigrationViewModel
class ViewModelFactory(val application: Application, val messenger: InternalMessenger) : ViewModelProvider.Factory {
private val requireBatteryOptimization =
!(application.getSystemService(Context.POWER_SERVICE) as PowerManager)
.isIgnoringBatteryOptimizations(application.packageName)
class ViewModelFactory(val application: Application) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = when {
modelClass.isAssignableFrom(MainViewModel::class.java) -> MainViewModel(application)
modelClass.isAssignableFrom(SettingsViewModel::class.java) -> SettingsViewModel(
application
modelClass.isAssignableFrom(MainViewModel::class.java) -> MainViewModel(requireBatteryOptimization, messenger, application)
modelClass.isAssignableFrom(SettingsViewModel::class.java) -> SettingsViewModel(messenger, application)
modelClass.isAssignableFrom(ThemeViewModel::class.java) -> ThemeViewModel(messenger, application)
modelClass.isAssignableFrom(DistribMigrationViewModel::class.java) -> DistribMigrationViewModel(
DistribMigrationState(),
PrismConfig,
messenger
)
modelClass.isAssignableFrom(ThemeViewModel::class.java) -> ThemeViewModel(application)
modelClass.isAssignableFrom(DistribMigrationViewModel::class.java) -> DistribMigrationViewModel(application)
else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
} as T
}
@ -30,8 +37,15 @@ class PreviewFactory(val context: Context) : ViewModelProvider.Factory {
modelClass.isAssignableFrom(MainViewModel::class.java) -> {
MainViewModel(
MainUiState(),
BatteryOptimisationViewModel(true),
previewRegistrationsViewModel(context)
org.unifiedpush.android.distributor.ui.vm.BatteryOptimisationViewModel(false, null),
org.unifiedpush.android.distributor.ui.vm.RegistrationsViewModel(
org.unifiedpush.android.distributor.ui.state.RegistrationListState(
emptyList<org.unifiedpush.android.distributor.data.App>()
),
null
),
null,
null
)
}
modelClass.isAssignableFrom(SettingsViewModel::class.java) -> {
@ -40,12 +54,18 @@ class PreviewFactory(val context: Context) : ViewModelProvider.Factory {
showToasts = false,
prismServerUrl = "",
prismApiKey = ""
)
),
null,
null
)
}
modelClass.isAssignableFrom(ThemeViewModel::class.java) -> ThemeViewModel()
modelClass.isAssignableFrom(ThemeViewModel::class.java) -> ThemeViewModel(null, null)
modelClass.isAssignableFrom(DistribMigrationViewModel::class.java) -> {
DistribMigrationViewModel(DistribMigrationState())
DistribMigrationViewModel(
DistribMigrationState(),
PrismConfig,
null
)
}
else -> throw IllegalArgumentException("Unknown ViewModel class")
} as T

View file

@ -33,17 +33,17 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import app.lonecloud.prism.R
import app.lonecloud.prism.activities.DistribMigrationViewModel
import app.lonecloud.prism.activities.MainViewModel
import app.lonecloud.prism.activities.PreviewFactory
import app.lonecloud.prism.activities.SettingsViewModel
import app.lonecloud.prism.activities.ThemeViewModel
import org.unifiedpush.android.distributor.ui.R as LibR
import org.unifiedpush.android.distributor.ipc.subscribeUiActions
import org.unifiedpush.android.distributor.ui.compose.AppBar
import org.unifiedpush.android.distributor.ui.vm.DistribMigrationViewModel
enum class AppScreen(@param:StringRes val title: Int) {
Main(R.string.app_name),
Settings(LibR.string.settings)
Settings(R.string.settings)
}
@OptIn(ExperimentalMaterial3Api::class)
@ -84,11 +84,13 @@ fun App(
themeViewModel: ThemeViewModel = viewModel<ThemeViewModel>(factory = factory),
navController: NavHostController = rememberNavController()
) {
val context = LocalContext.current
val uiActionsFlow = subscribeUiActions(context)
val backStackEntry by navController.currentBackStackEntryAsState()
val currentScreen = AppScreen.valueOf(
backStackEntry?.destination?.route ?: AppScreen.Main.name
)
// shared with all views, no need to scope it
val migrationViewModel = viewModel<DistribMigrationViewModel>(factory = factory)
val mainViewModel = viewModel<MainViewModel>(factory = factory)
@ -147,7 +149,8 @@ fun App(
) {
MainScreen(
mainViewModel,
migrationViewModel
migrationViewModel,
uiActionsFlow
)
}
composable(

View file

@ -25,13 +25,9 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import app.lonecloud.prism.AppStore
import app.lonecloud.prism.EventBus
import app.lonecloud.prism.R
import app.lonecloud.prism.activities.DistribMigrationViewModel
import app.lonecloud.prism.activities.MainViewModel
import app.lonecloud.prism.activities.PreviewFactory
import app.lonecloud.prism.activities.UiAction
import org.unifiedpush.android.distributor.ui.R as LibR
import org.unifiedpush.android.distributor.ui.compose.AppBar
import org.unifiedpush.android.distributor.ui.compose.CardDisableBatteryOptimisation
import org.unifiedpush.android.distributor.ui.compose.CardDisabledForMigration
@ -40,6 +36,7 @@ import org.unifiedpush.android.distributor.ui.compose.PermissionsUi
import org.unifiedpush.android.distributor.ui.compose.RegistrationList
import org.unifiedpush.android.distributor.ui.compose.RegistrationListHeading
import org.unifiedpush.android.distributor.ui.compose.UnregisterBarUi
import org.unifiedpush.android.distributor.ui.vm.DistribMigrationViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -69,7 +66,7 @@ fun MainAppBar(onGoToSettings: () -> Unit) {
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = stringResource(LibR.string.settings)
contentDescription = stringResource(R.string.settings)
)
}
}
@ -77,21 +74,23 @@ fun MainAppBar(onGoToSettings: () -> Unit) {
}
@Composable
fun MainScreen(viewModel: MainViewModel, migrationViewModel: DistribMigrationViewModel) {
fun MainScreen(
viewModel: MainViewModel,
migrationViewModel: DistribMigrationViewModel,
uiActionsFlow: kotlinx.coroutines.flow.Flow<String>?
) {
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(Unit) {
EventBus.subscribe<UiAction> {
it.handle { type ->
when (type) {
UiAction.Action.RefreshRegistrations -> viewModel.refreshRegistrations()
UiAction.Action.UpdatePrismServerConfigured -> {
viewModel.application?.let { app ->
val store = AppStore(app)
viewModel.updatePrismServerConfigured(
!store.prismServerUrl.isNullOrBlank() &&
!store.prismApiKey.isNullOrBlank()
)
}
uiActionsFlow?.collect { action ->
when (action) {
"RefreshRegistrations" -> viewModel.refreshRegistrations()
"UpdatePrismServerConfigured" -> {
viewModel.application?.let { app ->
val store = AppStore(app)
viewModel.updatePrismServerConfigured(
!store.prismServerUrl.isNullOrBlank() &&
!store.prismApiKey.isNullOrBlank()
)
}
}
}
@ -133,7 +132,7 @@ fun MainScreen(viewModel: MainViewModel, migrationViewModel: DistribMigrationVie
)
}
RegistrationList(viewModel.registrationsViewModel) {}
RegistrationList(viewModel.registrationsViewModel)
}
if (viewModel.mainUiState.showPermissionDialog) {
PermissionsUi {
@ -155,7 +154,7 @@ fun MainScreen(viewModel: MainViewModel, migrationViewModel: DistribMigrationVie
}
)
}
if (migrationViewModel.state.showMigrations) {
if (migrationViewModel.state.canMigrate) {
DistribMigrationUi(migrationViewModel)
}
}
@ -166,5 +165,5 @@ fun MainPreview() {
val factory = PreviewFactory(LocalContext.current)
val mainVM = viewModel<MainViewModel>(factory = factory)
val migrationVM = viewModel<DistribMigrationViewModel>(factory = factory)
MainScreen(mainVM, migrationVM)
MainScreen(mainVM, migrationVM, null)
}

View file

@ -29,7 +29,11 @@ import androidx.compose.ui.unit.dp
import app.lonecloud.prism.R
@Composable
fun PrismServerConfigButton(currentUrl: String, onConfigure: (url: String, apiKey: String) -> Unit) {
fun PrismServerConfigButton(
currentUrl: String,
currentApiKey: String,
onConfigure: (url: String, apiKey: String) -> Unit
) {
var showDialog by remember { mutableStateOf(false) }
Surface(
@ -61,6 +65,7 @@ fun PrismServerConfigButton(currentUrl: String, onConfigure: (url: String, apiKe
if (showDialog) {
PrismServerConfigDialog(
initialUrl = currentUrl,
initialApiKey = currentApiKey,
onDismiss = { showDialog = false },
onSave = { url, apiKey ->
onConfigure(url, apiKey)
@ -73,11 +78,12 @@ fun PrismServerConfigButton(currentUrl: String, onConfigure: (url: String, apiKe
@Composable
fun PrismServerConfigDialog(
initialUrl: String,
initialApiKey: String,
onDismiss: () -> Unit,
onSave: (url: String, apiKey: String) -> Unit
) {
var url by remember { mutableStateOf(initialUrl) }
var apiKey by remember { mutableStateOf("") }
var apiKey by remember { mutableStateOf(initialApiKey) }
var isTesting by remember { mutableStateOf(false) }
var testResult by remember { mutableStateOf<String?>(null) }
var showServerChangeWarning by remember { mutableStateOf(false) }

View file

@ -14,12 +14,11 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import app.lonecloud.prism.R
import app.lonecloud.prism.activities.DistribMigrationViewModel
import app.lonecloud.prism.activities.PreviewFactory
import app.lonecloud.prism.activities.SettingsViewModel
import app.lonecloud.prism.activities.ThemeViewModel
import org.unifiedpush.android.distributor.ui.compose.DistribMigrationUi
import org.unifiedpush.android.distributor.ui.compose.MigrationPreferences
import org.unifiedpush.android.distributor.ui.vm.DistribMigrationViewModel
@Composable
fun SettingsScreen(
@ -39,6 +38,7 @@ fun SettingsScreen(
) {
PrismServerConfigButton(
currentUrl = viewModel.state.prismServerUrl,
currentApiKey = viewModel.state.prismApiKey,
onConfigure = { url, apiKey ->
viewModel.updatePrismServerUrl(url)
viewModel.updatePrismApiKey(apiKey)
@ -57,13 +57,11 @@ fun SettingsScreen(
onCheckedChange = { themeViewModel.toggleDynamicColors() }
)
MigrationPreferences(migrationViewModel)
RestartServicesPreference {
viewModel.restartService()
}
}
if (migrationViewModel.state.showMigrations) {
if (migrationViewModel.state.canMigrate) {
DistribMigrationUi(migrationViewModel)
}
}

View file

@ -34,7 +34,6 @@ sealed class ApiUrlCandidate {
instance.set(New(url))
DatabaseFactory.getDb(context).run {
if (countApps() == 0) {
// registerApp update the counter which restart the service
registerApp(
context.packageName,
FAKE_TOKEN,
@ -44,9 +43,7 @@ sealed class ApiUrlCandidate {
null
)
} else {
// Else
SourceManager.setFailOnce()
// We restart in 1sec if it hasn't been replaced until then, as setFailOnce should call RestartWorker.run
RestartWorker.run(context, delay = 1_000)
}
}

View file

@ -19,7 +19,6 @@ object MessageSender {
fun send(context: Context, message: ClientMessage) {
synchronized(this) {
websocket?.let {
// Log.d(TAG, "Sending: ${message.serialize()}")
Log.d(TAG, "Sending: ${message::class.java.simpleName}")
message.send(it)
} ?: run {

View file

@ -11,6 +11,7 @@ import app.lonecloud.prism.DatabaseFactory
import app.lonecloud.prism.Distributor
import app.lonecloud.prism.Distributor.sendMessage
import app.lonecloud.prism.EncryptionKeyStore
import app.lonecloud.prism.R
import app.lonecloud.prism.api.data.ClientMessage
import app.lonecloud.prism.api.data.ServerMessage
import app.lonecloud.prism.callback.NetworkCallbackFactory
@ -27,8 +28,7 @@ import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import org.unifiedpush.android.distributor.ui.R as LibR
import org.unifiedpush.distributor.ChannelCreationStatus
import org.unifiedpush.android.distributor.ChannelCreationStatus
class ServerConnection(private val context: Context, private val releaseLock: () -> Unit) : WebSocketListener() {
@ -90,7 +90,7 @@ class ServerConnection(private val context: Context, private val releaseLock: ()
Handler(Looper.getMainLooper()).post {
Toast.makeText(
context,
context.getString(LibR.string.toast_url_candidate_success, it),
context.getString(R.string.toast_url_candidate_success, it),
Toast.LENGTH_SHORT
).show()
}
@ -239,7 +239,7 @@ class ServerConnection(private val context: Context, private val releaseLock: ()
Handler(Looper.getMainLooper()).post {
Toast.makeText(
context,
context.getString(LibR.string.toast_url_candidate_fail, url),
context.getString(R.string.toast_url_candidate_fail, url),
Toast.LENGTH_SHORT
).show()
}

View file

@ -1,8 +1,8 @@
package app.lonecloud.prism.callback
import android.content.Context
import org.unifiedpush.distributor.callback.BatteryCallback
import org.unifiedpush.distributor.callback.CallbackFactory
import org.unifiedpush.android.distributor.callback.BatteryCallback
import org.unifiedpush.android.distributor.callback.CallbackFactory
/**
* Battery callback - disabled since URGENCY feature is not supported by the Mozilla server

View file

@ -4,8 +4,8 @@ import android.content.Context
import app.lonecloud.prism.services.MainRegistrationCounter
import app.lonecloud.prism.services.RestartWorker
import app.lonecloud.prism.services.SourceManager
import org.unifiedpush.distributor.callback.CallbackFactory
import org.unifiedpush.distributor.callback.NetworkCallback
import org.unifiedpush.android.distributor.callback.CallbackFactory
import org.unifiedpush.android.distributor.callback.NetworkCallback
object NetworkCallbackFactory : CallbackFactory<NetworkCallbackFactory.MainNetworkCallback>() {
class MainNetworkCallback(val context: Context) : NetworkCallback() {

View file

@ -0,0 +1,31 @@
package app.lonecloud.prism.receivers
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import app.lonecloud.prism.AppStore
class PrismConfigReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val store = AppStore(context)
when (intent.action) {
ACTION_SET_PRISM_SERVER_URL -> {
val url = intent.getStringExtra(EXTRA_URL) ?: ""
store.prismServerUrl = url
}
ACTION_SET_PRISM_API_KEY -> {
val apiKey = intent.getStringExtra(EXTRA_API_KEY) ?: ""
store.prismApiKey = apiKey
}
}
}
companion object {
const val ACTION_SET_PRISM_SERVER_URL = "app.lonecloud.prism.SET_PRISM_SERVER_URL"
const val ACTION_SET_PRISM_API_KEY = "app.lonecloud.prism.SET_PRISM_API_KEY"
const val EXTRA_URL = "url"
const val EXTRA_API_KEY = "api_key"
}
}

View file

@ -6,19 +6,13 @@ import android.content.Context
import app.lonecloud.prism.AppStore
import app.lonecloud.prism.Distributor
import app.lonecloud.prism.callback.NetworkCallbackFactory
import org.unifiedpush.distributor.receiver.DistributorReceiver
import org.unifiedpush.android.distributor.receiver.DistributorReceiver
/**
* THIS SERVICE IS USED BY OTHER APPS TO REGISTER
*/
class RegisterBroadcastReceiver : DistributorReceiver() {
override val distributor = Distributor
override fun isConnected(context: Context): Boolean {
// We don't have to care about login
return true
}
override fun isConnected(context: Context): Boolean = true
override fun hasInternet(context: Context): Boolean = NetworkCallbackFactory.hasInternet()

View file

@ -10,8 +10,8 @@ import app.lonecloud.prism.utils.ForegroundNotification
import app.lonecloud.prism.utils.NOTIFICATION_ID_FOREGROUND
import app.lonecloud.prism.utils.TAG
import java.util.concurrent.atomic.AtomicReference
import org.unifiedpush.distributor.service.ForegroundService
import org.unifiedpush.distributor.service.ForegroundServiceFactory
import org.unifiedpush.android.distributor.service.ForegroundService
import org.unifiedpush.android.distributor.service.ForegroundServiceFactory
class FgService : ForegroundService() {

View file

@ -2,10 +2,10 @@ package app.lonecloud.prism.services
import android.content.Context
import app.lonecloud.prism.DatabaseFactory
import app.lonecloud.prism.activities.UiAction
import app.lonecloud.prism.utils.ForegroundNotification
import org.unifiedpush.distributor.Database
import org.unifiedpush.distributor.RegistrationCounter
import org.unifiedpush.android.distributor.Database
import org.unifiedpush.android.distributor.RegistrationCounter
import org.unifiedpush.android.distributor.ipc.sendUiAction
object MainRegistrationCounter : RegistrationCounter() {
@ -15,7 +15,7 @@ object MainRegistrationCounter : RegistrationCounter() {
override fun onCountRefreshed(context: Context) {
ForegroundNotification(context).update()
UiAction.publish(UiAction.Action.RefreshRegistrations)
sendUiAction(context, "RefreshRegistrations")
}
override fun getDb(context: Context): Database = DatabaseFactory.getDb(context)

View file

@ -3,7 +3,7 @@ package app.lonecloud.prism.services
import android.content.Context
import app.lonecloud.prism.AppStore
import app.lonecloud.prism.Distributor
import org.unifiedpush.distributor.MigrationManager as MManager
import org.unifiedpush.android.distributor.MigrationManager as MManager
class MigrationManager : MManager() {
override val distrib = Distributor

View file

@ -0,0 +1,125 @@
package app.lonecloud.prism.services
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.core.graphics.drawable.toBitmap
import app.lonecloud.prism.AppStore
import app.lonecloud.prism.DatabaseFactory
import app.lonecloud.prism.Distributor
import app.lonecloud.prism.PrismServerClient
import org.unifiedpush.android.distributor.Database
import org.unifiedpush.android.distributor.MigrationManager
import org.unifiedpush.android.distributor.SourceManager
import org.unifiedpush.android.distributor.UnifiedPushDistributor
import org.unifiedpush.android.distributor.WorkerCompanion
import org.unifiedpush.android.distributor.data.App
import org.unifiedpush.android.distributor.data.Description
import org.unifiedpush.android.distributor.ipc.handler.IAccount
import org.unifiedpush.android.distributor.ipc.handler.IApi
import org.unifiedpush.android.distributor.ipc.handler.IRegistrations
import org.unifiedpush.android.distributor.service.ForegroundServiceFactory
import org.unifiedpush.android.distributor.service.InternalService
import org.unifiedpush.android.distributor.utils.getApplicationIcon
class PrismInternalService : InternalService() {
override val sourceManager: SourceManager<*> = SourceManager
override val restartWorker: WorkerCompanion = RestartWorker
override val startService: ForegroundServiceFactory = FgService
override val migrationManager: org.unifiedpush.android.distributor.MigrationManager = app.lonecloud.prism.services.MigrationManager()
override val distributor: UnifiedPushDistributor = Distributor
override val db: Database by lazy { DatabaseFactory.getDb(this) }
private val appStore by lazy { AppStore(this) }
override var themeDynamicColors: Boolean
get() = appStore.dynamicColors
set(value) {
appStore.dynamicColors = value
}
override var showToasts: Boolean
get() = appStore.showToasts
set(value) {
appStore.showToasts = value
}
override fun getDebugInfo(): String = "Prism Distributor"
override fun runAppMigration() {
// No app migration needed for Prism currently
}
override fun account(): IAccount = object : IAccount {
override fun get(): String? = null
override fun logout() {}
override fun login(data: Bundle) {}
}
override fun api(): IApi = object : IApi {
override fun newPushServer(url: String?) {
// Prism uses fixed Mozilla server, but can be customized
}
override fun getUrl(): String = appStore.apiUrl
}
override fun registrations() = object : IRegistrations {
override fun delete(registrations: List<String>) {
registrations.forEach { token ->
// Prism's custom logic: delete from Prism server if it's a "target:" app
val dbApp = db.listApps().find { it.connectorToken == token }
if (dbApp?.description?.startsWith("target:") == true) {
val appName = dbApp.title ?: dbApp.packageName
PrismServerClient.deleteApp(context, appName)
}
distributor.deleteApp(context, token)
}
}
override fun list(): List<App> = db
.listApps().map {
val pm = context.packageManager
val isManualApp = it.description?.startsWith("target:") == true
val targetPackage = if (isManualApp) {
it.description?.substringAfter("target:")?.substringBefore("|")?.takeIf { pkg -> pkg.isNotBlank() }
} else {
null
}
val packageToResolve = (targetPackage ?: it.packageName) ?: ""
val appName = try {
val appInfo = pm.getApplicationInfo(packageToResolve, PackageManager.GET_META_DATA)
pm.getApplicationLabel(appInfo).toString()
} catch (e: PackageManager.NameNotFoundException) {
packageToResolve
}
val displayTitle = it.title ?: appName
App(
connectorToken = it.connectorToken,
packageName = it.packageName,
endpoint = it.endpoint,
vapidKey = it.vapidKey,
title = displayTitle,
msgCount = it.msgCount,
description = if (it.packageName == context.packageName) {
Description.LocalChannel
} else {
Description.StringDescription(packageToResolve)
},
icon = getApplicationIcon(packageToResolve)?.toBitmap(),
isLocal = it.packageName == context.packageName
)
}
override fun copyEndpoint(token: String?) {
super@PrismInternalService.registrations().copyEndpoint(token)
}
override fun addLocal(title: String) {
super@PrismInternalService.registrations().addLocal(title)
}
}
}

View file

@ -10,7 +10,7 @@ import app.lonecloud.prism.Distributor
import app.lonecloud.prism.api.MessageSender
import app.lonecloud.prism.callback.NetworkCallbackFactory
import app.lonecloud.prism.utils.TAG
import org.unifiedpush.distributor.WorkerCompanion
import org.unifiedpush.android.distributor.WorkerCompanion
class RestartWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

View file

@ -5,8 +5,8 @@ import app.lonecloud.prism.utils.DisconnectedNotification
import okhttp3.Request
import okhttp3.WebSocket
import okio.ByteString
import org.unifiedpush.distributor.AppNotification
import org.unifiedpush.distributor.SourceManager as SManager
import org.unifiedpush.android.distributor.AppNotification
import org.unifiedpush.android.distributor.SourceManager as SManager
object SourceManager : SManager<WebSocket>() {
override val foregroundService = FgService.service

View file

@ -4,11 +4,9 @@ import android.app.NotificationManager
import android.content.Context
import androidx.core.app.NotificationCompat
import app.lonecloud.prism.R
import app.lonecloud.prism.activities.MainActivity
import app.lonecloud.prism.services.MainRegistrationCounter
import java.util.concurrent.atomic.AtomicBoolean
import org.unifiedpush.android.distributor.ui.R as LibR
import org.unifiedpush.distributor.AppNotification
import org.unifiedpush.android.distributor.AppNotification
const val NOTIFICATION_ID_FOREGROUND = 51115
private const val NOTIFICATION_ID_WARNING = 51215
@ -25,16 +23,15 @@ class MainNotificationData(
text = text,
ticker = ticker,
priority = priority,
ongoing = ongoing,
activity = MainActivity::class.java
ongoing = ongoing
)
private val Context.warningChannelData: AppNotification.ChannelData
get() = AppNotification.ChannelData(
"Warning",
this.getString(LibR.string.warning),
this.getString(R.string.warning),
NotificationManager.IMPORTANCE_HIGH,
this.resources.getString(LibR.string.warning_notif_description).format(this.getString(R.string.app_name))
this.resources.getString(R.string.warning_notif_description).format(this.getString(R.string.app_name))
)
class DisconnectedNotification(context: Context) :
@ -44,10 +41,10 @@ class DisconnectedNotification(context: Context) :
NOTIFICATION_ID_WARNING,
MainNotificationData(
context.getString(R.string.app_name),
context.getString(LibR.string.warning_notif_content).format(
context.getString(R.string.warning_notif_content).format(
context.getString(R.string.app_name)
),
context.getString(LibR.string.warning),
context.getString(R.string.warning),
NotificationCompat.PRIORITY_HIGH,
true
),
@ -63,20 +60,20 @@ class ForegroundNotification(context: Context) :
context.getString(R.string.app_name),
if (MainRegistrationCounter.oneOrMore(context)) {
MainRegistrationCounter.getCount(context).let {
context.resources.getQuantityString(LibR.plurals.foreground_notif_content_with_reg, it, it)
context.resources.getQuantityString(R.plurals.foreground_notif_content_with_reg, it, it)
}
} else {
context.getString(LibR.string.foreground_notif_content_no_reg)
context.getString(R.string.foreground_notif_content_no_reg)
},
context.getString(LibR.string.foreground_service),
context.getString(R.string.foreground_service),
NotificationCompat.PRIORITY_LOW,
true
),
ChannelData(
"Foreground",
context.getString(LibR.string.foreground_service),
context.getString(R.string.foreground_service),
NotificationManager.IMPORTANCE_LOW,
context.getString(LibR.string.foreground_notif_description)
context.getString(R.string.foreground_notif_description)
)
)

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#999999" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#FF000000" android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z"/>
</vector>

View file

@ -1,33 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="prism_server_url_label">Prism Server URL</string>
<string name="prism_server_url_placeholder">https://your-prism-server.com</string>
<string name="prism_api_key_label">Prism API Key</string>
<string name="prism_api_key_placeholder">Your API key</string>
<string name="restart_service_button">Restart Push Service</string>
<string name="configure_server">Configure Server</string>
<string name="prism_server_configured">Prism Server: %s</string>
<string name="prism_server_not_configured">Not configured</string>
<string name="debug_title">Debug</string>
<string name="add_custom_app_title">Add Custom App</string>
<string name="app_name_label">App Name</string>
<string name="app_name_placeholder">e.g., notifications</string>
<string name="add_button">Add</string>
<string name="cancel_button">Cancel</string>
<string name="select_target_app_title">Select Target App</string>
<string name="search_apps_label">Search apps</string>
<string name="search_apps_placeholder">App name or package</string>
<string name="testing_connection">Testing connection…</string>
<string name="connection_successful">Connection successful!</string>
<string name="connection_failed">Connection failed: %s</string>
<string name="test_and_save_button">Test &amp; Save</string>
<string name="target_app_label">Target App</string>
<string name="select_an_app">Select an app</string>
<string name="app_dropdown_show_toasts">Notify about new registrations</string>
<string name="dynamic_colors_title">Dynamic Colors</string>
<string name="add_manual_app_content_description">Add Manual App</string>
<!-- Preview strings for distributor-ui library -->
<string name="preview_app_name" translatable="false">Distrib name</string>
<string name="preview_privacy_policy" translatable="false">Distrib privacy policy\ </string>
<string name="preview_in_app_notif_link" translatable="false">on the Internet.</string>
<!-- UnifiedPush library strings (required for translations) -->
<!-- Library strings referenced by Prism code -->
<string name="toast_url_candidate_fail">Fail to use %1$s</string>
<string name="toast_url_candidate_success">Successfully using %1$s</string>
<string name="warning">Warning</string>
<string name="warning_notif_content">%1$s is disconnected</string>
<string name="warning_notif_description">Warn when %1$s is disconnected or an issue occurred.</string>
<string name="warning_notif_ticker">Prism Warning</string>
<string name="foreground_service">Foreground Service</string>
<string name="foreground_notif_description">Notification to run in the foreground</string>
<string name="foreground_notif_content_no_reg">Waiting for registration to connect</string>
<string name="foreground_notif_ticker">Prism</string>
<string name="bar_unregister_title">%d selected</string>
<string name="dialog_unregistering_content">Are you sure to delete this registration?</string>
<plurals name="bar_unregister_title">
<item quantity="one">%d selected</item>
<item quantity="other">%d selected</item>
@ -36,6 +26,38 @@
<item quantity="one">Are you sure to delete this registration?</item>
<item quantity="other">Are you sure to delete %d registrations?</item>
</plurals>
<string name="foreground_notif_ticker">Foreground Service</string>
<string name="warning_notif_ticker">Warning</string>
<plurals name="foreground_notif_content_with_reg">
<item quantity="one">Connected for %d registration</item>
<item quantity="other">Connected for %d registrations</item>
</plurals>
<!-- Prism-specific strings -->
<string name="app_name">Prism</string>
<string name="settings">Settings</string>
<string name="add_custom_app_title">Add Custom App</string>
<string name="app_name_label">App Name</string>
<string name="app_name_placeholder">Enter app name</string>
<string name="target_app_label">Target App (Optional)</string>
<string name="select_an_app">Select an app</string>
<string name="add_button">Add</string>
<string name="cancel_button">Cancel</string>
<string name="select_target_app_title">Select Target App</string>
<string name="search_apps_label">Search</string>
<string name="search_apps_placeholder">Search for apps</string>
<string name="add_manual_app_content_description">Add manual app</string>
<string name="debug_title">Debug Information</string>
<string name="configure_server">Configure Prism Server</string>
<string name="prism_server_configured">Prism server configured</string>
<string name="prism_server_not_configured">Prism server not configured</string>
<string name="prism_server_url_label">Server URL</string>
<string name="prism_server_url_placeholder">https://prism.example.com</string>
<string name="prism_api_key_label">API Key</string>
<string name="prism_api_key_placeholder">Enter API key</string>
<string name="testing_connection">Testing connection…</string>
<string name="connection_successful">Connection successful</string>
<string name="connection_failed">Connection failed</string>
<string name="test_and_save_button">Test and Save</string>
<string name="restart_service_button">Restart Service</string>
<string name="app_dropdown_show_toasts">Notify when apps register</string>
<string name="dynamic_colors_title">Dynamic Colors</string>
</resources>

View file

@ -4,8 +4,9 @@ androidx-activityCompose = "1.12.1"
androidx-lifecycle = "2.10.0"
androidx-work = "2.11.0"
appcompat = "1.7.1"
unifiedpush_distributor = "0.5.6"
unifiedpush_distributor_ui = "0.5.5"
unifiedpush_distributor = "0.7.1"
unifiedpush_distributor_base = "0.7.0"
accompanist_permissions = "0.37.3"
tink = "1.15.0"
kotlin = "2.2.20"
kotlinx_serializationJson = "1.9.0"
@ -29,7 +30,9 @@ androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "u
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" }
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
unifiedpush-distributor = { module = "org.unifiedpush.android:distributor", version.ref = "unifiedpush_distributor" }
unifiedpush-distributor-ui = { module = "org.unifiedpush.android:distributor-ui", version.ref = "unifiedpush_distributor_ui" }
unifiedpush-distributor-base = { module = "org.unifiedpush.android:distributor-base", version.ref = "unifiedpush_distributor_base" }
unifiedpush-distributor-ui = { module = "org.unifiedpush.android:distributor-ui", version.ref = "unifiedpush_distributor" }
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist_permissions" }
tink-android = { module = "com.google.crypto.tink:tink-android", version.ref = "tink" }
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx_serializationJson" }