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 { dependencies {
implementation(libs.unifiedpush.distributor) implementation(libs.unifiedpush.distributor)
implementation(libs.unifiedpush.distributor.base)
implementation(libs.unifiedpush.distributor.ui) implementation(libs.unifiedpush.distributor.ui)
implementation(libs.accompanist.permissions)
implementation(libs.tink.android) implementation(libs.tink.android)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.compose)

View file

@ -21,6 +21,7 @@
android:networkSecurityConfig="@xml/network_security_config"> android:networkSecurityConfig="@xml/network_security_config">
<activity <activity
android:name=".activities.MainActivity" android:name=".activities.MainActivity"
android:process=":ui"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
@ -33,7 +34,16 @@
android:enabled="true" android:enabled="true"
android:foregroundServiceType="specialUse"> android:foregroundServiceType="specialUse">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" <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> </service>
<receiver <receiver
@ -45,6 +55,15 @@
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter> </intent-filter>
</receiver> </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 <receiver
android:name=".receivers.RegisterBroadcastReceiver" android:name=".receivers.RegisterBroadcastReceiver"
android:enabled="true" android:enabled="true"

View file

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

View file

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

View file

@ -3,14 +3,11 @@ package app.lonecloud.prism
import android.content.Context import android.content.Context
import app.lonecloud.prism.api.MessageSender import app.lonecloud.prism.api.MessageSender
import app.lonecloud.prism.api.data.ClientMessage import app.lonecloud.prism.api.data.ClientMessage
import org.unifiedpush.distributor.Database import org.unifiedpush.android.distributor.Database
import org.unifiedpush.distributor.UnifiedPushDistributor import org.unifiedpush.android.distributor.UnifiedPushDistributor
/**
* These functions are used to send messages to other apps
*/
object Distributor : UnifiedPushDistributor() { 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) 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 package app.lonecloud.prism.activities
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.viewmodel.compose.viewModel 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.App
import app.lonecloud.prism.activities.ui.theme.AppTheme import app.lonecloud.prism.activities.ui.theme.AppTheme
import app.lonecloud.prism.services.RestartWorker import org.unifiedpush.android.distributor.ipc.InternalMessenger
import app.lonecloud.prism.utils.TAG
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private var jobs: MutableList<Job> = emptyList<Job>().toMutableList() private lateinit var messenger: InternalMessenger
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
RestartWorker.startPeriodic(this)
messenger = InternalMessenger(this)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
val factory = ViewModelFactory(this.application) val factory = ViewModelFactory(this.application, messenger)
val themeViewModel = viewModel<ThemeViewModel>(factory = factory) val themeViewModel = viewModel<ThemeViewModel>(factory = factory)
AppTheme( AppTheme(
dynamicColor = themeViewModel.dynamicColors dynamicColor = themeViewModel.dynamicColors
) { ) {
App(factory, themeViewModel) 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() { override fun onDestroy() {
Log.d(TAG, "Destroy")
jobs.removeAll {
it.cancel()
true
}
super.onDestroy() super.onDestroy()
} }
} }

View file

@ -25,25 +25,31 @@ import app.lonecloud.prism.utils.VapidKeyGenerator
import app.lonecloud.prism.utils.WebPushEncryptionKeys import app.lonecloud.prism.utils.WebPushEncryptionKeys
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.unifiedpush.android.distributor.ui.compose.BatteryOptimisationViewModel import org.unifiedpush.android.distributor.data.App
import org.unifiedpush.android.distributor.ui.compose.RegistrationsViewModel import org.unifiedpush.android.distributor.ipc.InternalMessenger
import org.unifiedpush.android.distributor.ui.compose.state.RegistrationListState 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( class MainViewModel(
mainUiState: MainUiState, mainUiState: MainUiState,
val batteryOptimisationViewModel: BatteryOptimisationViewModel, val batteryOptimisationViewModel: BatteryOptimisationViewModel,
val registrationsViewModel: RegistrationsViewModel, val registrationsViewModel: RegistrationsViewModel,
val messenger: InternalMessenger?,
val application: Application? = null val application: Application? = null
) : ViewModel() { ) : ViewModel() {
constructor(application: Application) : this( constructor(requireBatteryOpt: Boolean, messenger: InternalMessenger?, application: Application) : this(
mainUiState = MainUiState( mainUiState = MainUiState(
prismServerConfigured = !AppStore(application).prismServerUrl.isNullOrBlank() && prismServerConfigured = !AppStore(application).prismServerUrl.isNullOrBlank() &&
!AppStore(application).prismApiKey.isNullOrBlank() !AppStore(application).prismApiKey.isNullOrBlank()
), ),
batteryOptimisationViewModel = BatteryOptimisationViewModel(application), batteryOptimisationViewModel = BatteryOptimisationViewModel(requireBatteryOpt, messenger),
registrationsViewModel = RegistrationsViewModel( registrationsViewModel = RegistrationsViewModel(
getRegistrationListState(application) RegistrationListState(emptyList<App>()),
messenger
), ),
messenger,
application application
) )
@ -69,9 +75,8 @@ class MainViewModel(
fun refreshRegistrations() { fun refreshRegistrations() {
viewModelScope.launch { viewModelScope.launch {
application?.let { val apps = messenger?.sendIMessageL(InternalOpcode.REG_LIST, "apps", App::class.java)
registrationsViewModel.state = getRegistrationListState(it) registrationsViewModel.state = RegistrationListState(apps ?: emptyList())
}
} }
} }
@ -79,9 +84,7 @@ class MainViewModel(
viewModelScope.launch { viewModelScope.launch {
val state = registrationsViewModel.state val state = registrationsViewModel.state
val tokenList = state.list.filter { it.selected }.map { it.token } val tokenList = state.list.filter { it.selected }.map { it.token }
publishAction( messenger?.sendIMessage(InternalOpcode.REG_DELETE, "regs" to tokenList)
AppAction(AppAction.Action.DeleteRegistration(tokenList))
)
registrationsViewModel.state = RegistrationListState( registrationsViewModel.state = RegistrationListState(
list = state.list.filter { list = state.list.filter {
!it.selected !it.selected
@ -130,7 +133,9 @@ class MainViewModel(
} }
fun restartService() { fun restartService() {
publishAction(AppAction(AppAction.Action.RestartService)) viewModelScope.launch {
messenger?.sendIMessage(InternalOpcode.WORKER_RESTART, 0)
}
} }
private fun hasUnifiedPushSupport(pm: PackageManager, packageName: String): Boolean { 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") 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 package app.lonecloud.prism.activities
import android.app.Application import android.app.Application
import android.content.Intent
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.lonecloud.prism.AppStore import app.lonecloud.prism.AppStore
import app.lonecloud.prism.PrismServerClient
import app.lonecloud.prism.activities.ui.SettingsState import app.lonecloud.prism.activities.ui.SettingsState
import app.lonecloud.prism.receivers.PrismConfigReceiver
import kotlinx.coroutines.launch 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() { class SettingsViewModel(
constructor(application: Application) : this( state: SettingsState,
val messenger: InternalMessenger?,
val application: Application? = null
) : ViewModel() {
constructor(messenger: InternalMessenger?, application: Application) : this(
SettingsState.from(application), SettingsState.from(application),
messenger,
application application
) )
@ -22,7 +33,8 @@ class SettingsViewModel(state: SettingsState, val application: Application? = nu
fun toggleShowToasts() { fun toggleShowToasts() {
viewModelScope.launch { viewModelScope.launch {
state = state.copy(showToasts = !state.showToasts) 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 { application?.let {
AppStore(it).prismServerUrl = trimmedUrl.ifBlank { null } 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()) { 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 { application?.let {
AppStore(it).prismApiKey = trimmedKey.ifBlank { null } 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()) { if (state.prismServerUrl.isNotBlank() && trimmedKey.isNotBlank()) {
publishAction(AppAction(AppAction.Action.RegisterPrismServer)) PrismServerClient.registerAllApps(it)
} }
UiAction.publish(UiAction.Action.UpdatePrismServerConfigured) sendUiAction(it, "UpdatePrismServerConfigured")
} }
} }
} }
fun restartService() { 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 androidx.lifecycle.viewModelScope
import app.lonecloud.prism.AppStore import app.lonecloud.prism.AppStore
import kotlinx.coroutines.launch 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() { class ThemeViewModel(val messenger: InternalMessenger?, val application: Application?) : ViewModel() {
var dynamicColors by mutableStateOf( var dynamicColors by mutableStateOf(application?.let { AppStore(it).dynamicColors } ?: false)
application?.let { AppStore(it).dynamicColors } ?: false
)
fun toggleDynamicColors() { fun toggleDynamicColors() {
viewModelScope.launch { viewModelScope.launch {
dynamicColors = !dynamicColors dynamicColors = !dynamicColors
application?.run { application?.let { AppStore(it).dynamicColors = dynamicColors }
AppStore(this).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.app.Application
import android.content.Context import android.content.Context
import android.os.PowerManager
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider 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.MainUiState
import app.lonecloud.prism.activities.ui.SettingsState import app.lonecloud.prism.activities.ui.SettingsState
import org.unifiedpush.android.distributor.ui.compose.BatteryOptimisationViewModel import org.unifiedpush.android.distributor.ipc.InternalMessenger
import org.unifiedpush.android.distributor.ui.compose.previewRegistrationsViewModel import org.unifiedpush.android.distributor.ui.state.DistribMigrationState
import org.unifiedpush.android.distributor.ui.compose.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") @Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = when { override fun <T : ViewModel> create(modelClass: Class<T>): T = when {
modelClass.isAssignableFrom(MainViewModel::class.java) -> MainViewModel(application) modelClass.isAssignableFrom(MainViewModel::class.java) -> MainViewModel(requireBatteryOptimization, messenger, application)
modelClass.isAssignableFrom(SettingsViewModel::class.java) -> SettingsViewModel( modelClass.isAssignableFrom(SettingsViewModel::class.java) -> SettingsViewModel(messenger, application)
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}") else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
} as T } as T
} }
@ -30,8 +37,15 @@ class PreviewFactory(val context: Context) : ViewModelProvider.Factory {
modelClass.isAssignableFrom(MainViewModel::class.java) -> { modelClass.isAssignableFrom(MainViewModel::class.java) -> {
MainViewModel( MainViewModel(
MainUiState(), MainUiState(),
BatteryOptimisationViewModel(true), org.unifiedpush.android.distributor.ui.vm.BatteryOptimisationViewModel(false, null),
previewRegistrationsViewModel(context) 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) -> { modelClass.isAssignableFrom(SettingsViewModel::class.java) -> {
@ -40,12 +54,18 @@ class PreviewFactory(val context: Context) : ViewModelProvider.Factory {
showToasts = false, showToasts = false,
prismServerUrl = "", prismServerUrl = "",
prismApiKey = "" prismApiKey = ""
) ),
null,
null
) )
} }
modelClass.isAssignableFrom(ThemeViewModel::class.java) -> ThemeViewModel() modelClass.isAssignableFrom(ThemeViewModel::class.java) -> ThemeViewModel(null, null)
modelClass.isAssignableFrom(DistribMigrationViewModel::class.java) -> { modelClass.isAssignableFrom(DistribMigrationViewModel::class.java) -> {
DistribMigrationViewModel(DistribMigrationState()) DistribMigrationViewModel(
DistribMigrationState(),
PrismConfig,
null
)
} }
else -> throw IllegalArgumentException("Unknown ViewModel class") else -> throw IllegalArgumentException("Unknown ViewModel class")
} as T } as T

View file

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

View file

@ -25,13 +25,9 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import app.lonecloud.prism.AppStore import app.lonecloud.prism.AppStore
import app.lonecloud.prism.EventBus
import app.lonecloud.prism.R import app.lonecloud.prism.R
import app.lonecloud.prism.activities.DistribMigrationViewModel
import app.lonecloud.prism.activities.MainViewModel import app.lonecloud.prism.activities.MainViewModel
import app.lonecloud.prism.activities.PreviewFactory 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.AppBar
import org.unifiedpush.android.distributor.ui.compose.CardDisableBatteryOptimisation import org.unifiedpush.android.distributor.ui.compose.CardDisableBatteryOptimisation
import org.unifiedpush.android.distributor.ui.compose.CardDisabledForMigration 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.RegistrationList
import org.unifiedpush.android.distributor.ui.compose.RegistrationListHeading import org.unifiedpush.android.distributor.ui.compose.RegistrationListHeading
import org.unifiedpush.android.distributor.ui.compose.UnregisterBarUi import org.unifiedpush.android.distributor.ui.compose.UnregisterBarUi
import org.unifiedpush.android.distributor.ui.vm.DistribMigrationViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -69,7 +66,7 @@ fun MainAppBar(onGoToSettings: () -> Unit) {
) { ) {
Icon( Icon(
imageVector = Icons.Default.Settings, imageVector = Icons.Default.Settings,
contentDescription = stringResource(LibR.string.settings) contentDescription = stringResource(R.string.settings)
) )
} }
} }
@ -77,21 +74,23 @@ fun MainAppBar(onGoToSettings: () -> Unit) {
} }
@Composable @Composable
fun MainScreen(viewModel: MainViewModel, migrationViewModel: DistribMigrationViewModel) { fun MainScreen(
viewModel: MainViewModel,
migrationViewModel: DistribMigrationViewModel,
uiActionsFlow: kotlinx.coroutines.flow.Flow<String>?
) {
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
EventBus.subscribe<UiAction> { uiActionsFlow?.collect { action ->
it.handle { type -> when (action) {
when (type) { "RefreshRegistrations" -> viewModel.refreshRegistrations()
UiAction.Action.RefreshRegistrations -> viewModel.refreshRegistrations() "UpdatePrismServerConfigured" -> {
UiAction.Action.UpdatePrismServerConfigured -> { viewModel.application?.let { app ->
viewModel.application?.let { app -> val store = AppStore(app)
val store = AppStore(app) viewModel.updatePrismServerConfigured(
viewModel.updatePrismServerConfigured( !store.prismServerUrl.isNullOrBlank() &&
!store.prismServerUrl.isNullOrBlank() && !store.prismApiKey.isNullOrBlank()
!store.prismApiKey.isNullOrBlank() )
)
}
} }
} }
} }
@ -133,7 +132,7 @@ fun MainScreen(viewModel: MainViewModel, migrationViewModel: DistribMigrationVie
) )
} }
RegistrationList(viewModel.registrationsViewModel) {} RegistrationList(viewModel.registrationsViewModel)
} }
if (viewModel.mainUiState.showPermissionDialog) { if (viewModel.mainUiState.showPermissionDialog) {
PermissionsUi { PermissionsUi {
@ -155,7 +154,7 @@ fun MainScreen(viewModel: MainViewModel, migrationViewModel: DistribMigrationVie
} }
) )
} }
if (migrationViewModel.state.showMigrations) { if (migrationViewModel.state.canMigrate) {
DistribMigrationUi(migrationViewModel) DistribMigrationUi(migrationViewModel)
} }
} }
@ -166,5 +165,5 @@ fun MainPreview() {
val factory = PreviewFactory(LocalContext.current) val factory = PreviewFactory(LocalContext.current)
val mainVM = viewModel<MainViewModel>(factory = factory) val mainVM = viewModel<MainViewModel>(factory = factory)
val migrationVM = viewModel<DistribMigrationViewModel>(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 import app.lonecloud.prism.R
@Composable @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) } var showDialog by remember { mutableStateOf(false) }
Surface( Surface(
@ -61,6 +65,7 @@ fun PrismServerConfigButton(currentUrl: String, onConfigure: (url: String, apiKe
if (showDialog) { if (showDialog) {
PrismServerConfigDialog( PrismServerConfigDialog(
initialUrl = currentUrl, initialUrl = currentUrl,
initialApiKey = currentApiKey,
onDismiss = { showDialog = false }, onDismiss = { showDialog = false },
onSave = { url, apiKey -> onSave = { url, apiKey ->
onConfigure(url, apiKey) onConfigure(url, apiKey)
@ -73,11 +78,12 @@ fun PrismServerConfigButton(currentUrl: String, onConfigure: (url: String, apiKe
@Composable @Composable
fun PrismServerConfigDialog( fun PrismServerConfigDialog(
initialUrl: String, initialUrl: String,
initialApiKey: String,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onSave: (url: String, apiKey: String) -> Unit onSave: (url: String, apiKey: String) -> Unit
) { ) {
var url by remember { mutableStateOf(initialUrl) } var url by remember { mutableStateOf(initialUrl) }
var apiKey by remember { mutableStateOf("") } var apiKey by remember { mutableStateOf(initialApiKey) }
var isTesting by remember { mutableStateOf(false) } var isTesting by remember { mutableStateOf(false) }
var testResult by remember { mutableStateOf<String?>(null) } var testResult by remember { mutableStateOf<String?>(null) }
var showServerChangeWarning by remember { mutableStateOf(false) } var showServerChangeWarning by remember { mutableStateOf(false) }

View file

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

View file

@ -34,7 +34,6 @@ sealed class ApiUrlCandidate {
instance.set(New(url)) instance.set(New(url))
DatabaseFactory.getDb(context).run { DatabaseFactory.getDb(context).run {
if (countApps() == 0) { if (countApps() == 0) {
// registerApp update the counter which restart the service
registerApp( registerApp(
context.packageName, context.packageName,
FAKE_TOKEN, FAKE_TOKEN,
@ -44,9 +43,7 @@ sealed class ApiUrlCandidate {
null null
) )
} else { } else {
// Else
SourceManager.setFailOnce() 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) RestartWorker.run(context, delay = 1_000)
} }
} }

View file

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

View file

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

View file

@ -1,8 +1,8 @@
package app.lonecloud.prism.callback package app.lonecloud.prism.callback
import android.content.Context import android.content.Context
import org.unifiedpush.distributor.callback.BatteryCallback import org.unifiedpush.android.distributor.callback.BatteryCallback
import org.unifiedpush.distributor.callback.CallbackFactory import org.unifiedpush.android.distributor.callback.CallbackFactory
/** /**
* Battery callback - disabled since URGENCY feature is not supported by the Mozilla server * 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.MainRegistrationCounter
import app.lonecloud.prism.services.RestartWorker import app.lonecloud.prism.services.RestartWorker
import app.lonecloud.prism.services.SourceManager import app.lonecloud.prism.services.SourceManager
import org.unifiedpush.distributor.callback.CallbackFactory import org.unifiedpush.android.distributor.callback.CallbackFactory
import org.unifiedpush.distributor.callback.NetworkCallback import org.unifiedpush.android.distributor.callback.NetworkCallback
object NetworkCallbackFactory : CallbackFactory<NetworkCallbackFactory.MainNetworkCallback>() { object NetworkCallbackFactory : CallbackFactory<NetworkCallbackFactory.MainNetworkCallback>() {
class MainNetworkCallback(val context: Context) : NetworkCallback() { 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.AppStore
import app.lonecloud.prism.Distributor import app.lonecloud.prism.Distributor
import app.lonecloud.prism.callback.NetworkCallbackFactory 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() { class RegisterBroadcastReceiver : DistributorReceiver() {
override val distributor = Distributor override val distributor = Distributor
override fun isConnected(context: Context): Boolean { override fun isConnected(context: Context): Boolean = true
// We don't have to care about login
return true
}
override fun hasInternet(context: Context): Boolean = NetworkCallbackFactory.hasInternet() 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.NOTIFICATION_ID_FOREGROUND
import app.lonecloud.prism.utils.TAG import app.lonecloud.prism.utils.TAG
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import org.unifiedpush.distributor.service.ForegroundService import org.unifiedpush.android.distributor.service.ForegroundService
import org.unifiedpush.distributor.service.ForegroundServiceFactory import org.unifiedpush.android.distributor.service.ForegroundServiceFactory
class FgService : ForegroundService() { class FgService : ForegroundService() {

View file

@ -2,10 +2,10 @@ package app.lonecloud.prism.services
import android.content.Context import android.content.Context
import app.lonecloud.prism.DatabaseFactory import app.lonecloud.prism.DatabaseFactory
import app.lonecloud.prism.activities.UiAction
import app.lonecloud.prism.utils.ForegroundNotification import app.lonecloud.prism.utils.ForegroundNotification
import org.unifiedpush.distributor.Database import org.unifiedpush.android.distributor.Database
import org.unifiedpush.distributor.RegistrationCounter import org.unifiedpush.android.distributor.RegistrationCounter
import org.unifiedpush.android.distributor.ipc.sendUiAction
object MainRegistrationCounter : RegistrationCounter() { object MainRegistrationCounter : RegistrationCounter() {
@ -15,7 +15,7 @@ object MainRegistrationCounter : RegistrationCounter() {
override fun onCountRefreshed(context: Context) { override fun onCountRefreshed(context: Context) {
ForegroundNotification(context).update() ForegroundNotification(context).update()
UiAction.publish(UiAction.Action.RefreshRegistrations) sendUiAction(context, "RefreshRegistrations")
} }
override fun getDb(context: Context): Database = DatabaseFactory.getDb(context) 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 android.content.Context
import app.lonecloud.prism.AppStore import app.lonecloud.prism.AppStore
import app.lonecloud.prism.Distributor import app.lonecloud.prism.Distributor
import org.unifiedpush.distributor.MigrationManager as MManager import org.unifiedpush.android.distributor.MigrationManager as MManager
class MigrationManager : MManager() { class MigrationManager : MManager() {
override val distrib = Distributor 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.api.MessageSender
import app.lonecloud.prism.callback.NetworkCallbackFactory import app.lonecloud.prism.callback.NetworkCallbackFactory
import app.lonecloud.prism.utils.TAG 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) { 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.Request
import okhttp3.WebSocket import okhttp3.WebSocket
import okio.ByteString import okio.ByteString
import org.unifiedpush.distributor.AppNotification import org.unifiedpush.android.distributor.AppNotification
import org.unifiedpush.distributor.SourceManager as SManager import org.unifiedpush.android.distributor.SourceManager as SManager
object SourceManager : SManager<WebSocket>() { object SourceManager : SManager<WebSocket>() {
override val foregroundService = FgService.service override val foregroundService = FgService.service

View file

@ -4,11 +4,9 @@ import android.app.NotificationManager
import android.content.Context import android.content.Context
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import app.lonecloud.prism.R import app.lonecloud.prism.R
import app.lonecloud.prism.activities.MainActivity
import app.lonecloud.prism.services.MainRegistrationCounter import app.lonecloud.prism.services.MainRegistrationCounter
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import org.unifiedpush.android.distributor.ui.R as LibR import org.unifiedpush.android.distributor.AppNotification
import org.unifiedpush.distributor.AppNotification
const val NOTIFICATION_ID_FOREGROUND = 51115 const val NOTIFICATION_ID_FOREGROUND = 51115
private const val NOTIFICATION_ID_WARNING = 51215 private const val NOTIFICATION_ID_WARNING = 51215
@ -25,16 +23,15 @@ class MainNotificationData(
text = text, text = text,
ticker = ticker, ticker = ticker,
priority = priority, priority = priority,
ongoing = ongoing, ongoing = ongoing
activity = MainActivity::class.java
) )
private val Context.warningChannelData: AppNotification.ChannelData private val Context.warningChannelData: AppNotification.ChannelData
get() = AppNotification.ChannelData( get() = AppNotification.ChannelData(
"Warning", "Warning",
this.getString(LibR.string.warning), this.getString(R.string.warning),
NotificationManager.IMPORTANCE_HIGH, 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) : class DisconnectedNotification(context: Context) :
@ -44,10 +41,10 @@ class DisconnectedNotification(context: Context) :
NOTIFICATION_ID_WARNING, NOTIFICATION_ID_WARNING,
MainNotificationData( MainNotificationData(
context.getString(R.string.app_name), 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(R.string.app_name)
), ),
context.getString(LibR.string.warning), context.getString(R.string.warning),
NotificationCompat.PRIORITY_HIGH, NotificationCompat.PRIORITY_HIGH,
true true
), ),
@ -63,20 +60,20 @@ class ForegroundNotification(context: Context) :
context.getString(R.string.app_name), context.getString(R.string.app_name),
if (MainRegistrationCounter.oneOrMore(context)) { if (MainRegistrationCounter.oneOrMore(context)) {
MainRegistrationCounter.getCount(context).let { 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 { } 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, NotificationCompat.PRIORITY_LOW,
true true
), ),
ChannelData( ChannelData(
"Foreground", "Foreground",
context.getString(LibR.string.foreground_service), context.getString(R.string.foreground_service),
NotificationManager.IMPORTANCE_LOW, 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"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="prism_server_url_label">Prism Server URL</string> <!-- Preview strings for distributor-ui library -->
<string name="prism_server_url_placeholder">https://your-prism-server.com</string> <string name="preview_app_name" translatable="false">Distrib name</string>
<string name="prism_api_key_label">Prism API Key</string> <string name="preview_privacy_policy" translatable="false">Distrib privacy policy\ </string>
<string name="prism_api_key_placeholder">Your API key</string> <string name="preview_in_app_notif_link" translatable="false">on the Internet.</string>
<string name="restart_service_button">Restart Push Service</string>
<string name="configure_server">Configure Server</string> <!-- Library strings referenced by Prism code -->
<string name="prism_server_configured">Prism Server: %s</string> <string name="toast_url_candidate_fail">Fail to use %1$s</string>
<string name="prism_server_not_configured">Not configured</string> <string name="toast_url_candidate_success">Successfully using %1$s</string>
<string name="debug_title">Debug</string> <string name="warning">Warning</string>
<string name="add_custom_app_title">Add Custom App</string> <string name="warning_notif_content">%1$s is disconnected</string>
<string name="app_name_label">App Name</string> <string name="warning_notif_description">Warn when %1$s is disconnected or an issue occurred.</string>
<string name="app_name_placeholder">e.g., notifications</string> <string name="warning_notif_ticker">Prism Warning</string>
<string name="add_button">Add</string> <string name="foreground_service">Foreground Service</string>
<string name="cancel_button">Cancel</string> <string name="foreground_notif_description">Notification to run in the foreground</string>
<string name="select_target_app_title">Select Target App</string> <string name="foreground_notif_content_no_reg">Waiting for registration to connect</string>
<string name="search_apps_label">Search apps</string> <string name="foreground_notif_ticker">Prism</string>
<string name="search_apps_placeholder">App name or package</string> <string name="bar_unregister_title">%d selected</string>
<string name="testing_connection">Testing connection…</string> <string name="dialog_unregistering_content">Are you sure to delete this registration?</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>
<!-- UnifiedPush library strings (required for translations) -->
<plurals name="bar_unregister_title"> <plurals name="bar_unregister_title">
<item quantity="one">%d selected</item> <item quantity="one">%d selected</item>
<item quantity="other">%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="one">Are you sure to delete this registration?</item>
<item quantity="other">Are you sure to delete %d registrations?</item> <item quantity="other">Are you sure to delete %d registrations?</item>
</plurals> </plurals>
<string name="foreground_notif_ticker">Foreground Service</string> <plurals name="foreground_notif_content_with_reg">
<string name="warning_notif_ticker">Warning</string> <item quantity="one">Connected for %d registration</item>
</resources> <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-lifecycle = "2.10.0"
androidx-work = "2.11.0" androidx-work = "2.11.0"
appcompat = "1.7.1" appcompat = "1.7.1"
unifiedpush_distributor = "0.5.6" unifiedpush_distributor = "0.7.1"
unifiedpush_distributor_ui = "0.5.5" unifiedpush_distributor_base = "0.7.0"
accompanist_permissions = "0.37.3"
tink = "1.15.0" tink = "1.15.0"
kotlin = "2.2.20" kotlin = "2.2.20"
kotlinx_serializationJson = "1.9.0" 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" } androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" }
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
unifiedpush-distributor = { module = "org.unifiedpush.android:distributor", version.ref = "unifiedpush_distributor" } 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" } 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" } 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" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx_serializationJson" }