mirror of
https://github.com/lone-cloud/prism-android
synced 2026-06-03 19:54:44 -07:00
manual app fixes and improvements
This commit is contained in:
parent
423d91dc1f
commit
874444c6be
9 changed files with 115 additions and 165 deletions
|
|
@ -9,7 +9,6 @@
|
||||||
package app.lonecloud.prism
|
package app.lonecloud.prism
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
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 app.lonecloud.prism.utils.DescriptionParser
|
import app.lonecloud.prism.utils.DescriptionParser
|
||||||
|
|
@ -49,26 +48,15 @@ object Distributor : UnifiedPushDistributor() {
|
||||||
) = backendRegisterNewChannelId(context, packageName, channelId, title, vapid, description)
|
) = backendRegisterNewChannelId(context, packageName, channelId, title, vapid, description)
|
||||||
|
|
||||||
override fun backendUnregisterChannelId(context: Context, channelId: String) {
|
override fun backendUnregisterChannelId(context: Context, channelId: String) {
|
||||||
Log.d("Distributor", "backendUnregisterChannelId called with channelId: $channelId")
|
|
||||||
|
|
||||||
val db = getDb(context)
|
val db = getDb(context)
|
||||||
|
|
||||||
val channelVapidPair = db.listChannelIdVapid().find { it.first == channelId }
|
val channelVapidPair = db.listChannelIdVapid().find { it.first == channelId }
|
||||||
Log.d("Distributor", "Found vapidKey for channelId: ${channelVapidPair?.second}")
|
|
||||||
|
|
||||||
if (channelVapidPair != null) {
|
if (channelVapidPair != null) {
|
||||||
val app = db.listApps().find { it.vapidKey == channelVapidPair.second }
|
val app = db.listApps().find { it.vapidKey == channelVapidPair.second }
|
||||||
Log.d(
|
|
||||||
"Distributor",
|
|
||||||
"Found app: ${app?.title}, isManual: ${DescriptionParser.isManualApp(app?.description)}, connectorToken: ${app?.connectorToken}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (app != null && DescriptionParser.isManualApp(app.description)) {
|
if (app != null && DescriptionParser.isManualApp(app.description)) {
|
||||||
Log.d("Distributor", "Calling PrismServerClient.deleteApp with connectorToken: ${app.connectorToken}")
|
|
||||||
PrismServerClient.deleteApp(context, app.connectorToken)
|
PrismServerClient.deleteApp(context, app.connectorToken)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.w("Distributor", "No vapidKey found for channelId: $channelId")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageSender.send(
|
MessageSender.send(
|
||||||
|
|
|
||||||
|
|
@ -146,8 +146,7 @@ class PrismPreferences(context: Context) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getRegisteredEndpoint(connectorToken: String): String? =
|
fun getRegisteredEndpoint(connectorToken: String): String? = sharedPreferences.getString("registered_endpoint_$connectorToken", null)
|
||||||
sharedPreferences.getString("registered_endpoint_$connectorToken", null)
|
|
||||||
|
|
||||||
fun removeRegisteredEndpoint(connectorToken: String) {
|
fun removeRegisteredEndpoint(connectorToken: String) {
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
|
|
@ -161,8 +160,7 @@ class PrismPreferences(context: Context) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getVapidPrivateKey(connectorToken: String): String? =
|
fun getVapidPrivateKey(connectorToken: String): String? = sharedPreferences.getString("vapid_private_$connectorToken", null)
|
||||||
sharedPreferences.getString("vapid_private_$connectorToken", null)
|
|
||||||
|
|
||||||
fun removeVapidPrivateKey(connectorToken: String) {
|
fun removeVapidPrivateKey(connectorToken: String) {
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
|
|
@ -170,37 +168,6 @@ class PrismPreferences(context: Context) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setRegistrationAddedAt(connectorToken: String, timestampMs: Long) {
|
|
||||||
sharedPreferences.edit {
|
|
||||||
putLong("registration_added_at_$connectorToken", timestampMs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getRegistrationAddedAt(connectorToken: String): Long? {
|
|
||||||
val timestamp = sharedPreferences.getLong("registration_added_at_$connectorToken", -1L)
|
|
||||||
return if (timestamp == -1L) null else timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeRegistrationAddedAt(connectorToken: String) {
|
|
||||||
sharedPreferences.edit {
|
|
||||||
remove("registration_added_at_$connectorToken")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cleanupLegacyRegistrationAddedAtIfNeeded() {
|
|
||||||
if (sharedPreferences.getBoolean(PREF_ADDED_AT_CLEANED_UP, false)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val keysToRemove = sharedPreferences.all.keys
|
|
||||||
.filter { it.startsWith("registration_added_at_") }
|
|
||||||
|
|
||||||
sharedPreferences.edit {
|
|
||||||
keysToRemove.forEach { remove(it) }
|
|
||||||
putBoolean(PREF_ADDED_AT_CLEANED_UP, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addPendingManualToken(connectorToken: String) {
|
fun addPendingManualToken(connectorToken: String) {
|
||||||
val tokens = sharedPreferences.getStringSet(PREF_PENDING_MANUAL_TOKENS, emptySet())
|
val tokens = sharedPreferences.getStringSet(PREF_PENDING_MANUAL_TOKENS, emptySet())
|
||||||
?.toMutableSet()
|
?.toMutableSet()
|
||||||
|
|
@ -242,6 +209,5 @@ class PrismPreferences(context: Context) :
|
||||||
private const val PREF_PRISM_API_KEY = "prism_api_key"
|
private const val PREF_PRISM_API_KEY = "prism_api_key"
|
||||||
private const val PREF_INTRO_COMPLETED = "intro_completed"
|
private const val PREF_INTRO_COMPLETED = "intro_completed"
|
||||||
private const val PREF_PENDING_MANUAL_TOKENS = "pending_manual_tokens"
|
private const val PREF_PENDING_MANUAL_TOKENS = "pending_manual_tokens"
|
||||||
private const val PREF_ADDED_AT_CLEANED_UP = "added_at_cleaned_up"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ object PrismServerClient {
|
||||||
onError(error)
|
onError(error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val validatedVapidPrivateKey = vapidPrivateKey ?: return
|
val validatedVapidPrivateKey = requireNotNull(vapidPrivateKey)
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
|
|
@ -297,7 +297,11 @@ object PrismServerClient {
|
||||||
return app.endpoint
|
return app.endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun persistSubscriptionIdInDb(context: Context, connectorToken: String, subscriptionId: String) {
|
private fun persistSubscriptionIdInDb(
|
||||||
|
context: Context,
|
||||||
|
connectorToken: String,
|
||||||
|
subscriptionId: String
|
||||||
|
) {
|
||||||
val db = DatabaseFactory.getDb(context)
|
val db = DatabaseFactory.getDb(context)
|
||||||
val app = db.listApps().find { it.connectorToken == connectorToken } ?: return
|
val app = db.listApps().find { it.connectorToken == connectorToken } ?: return
|
||||||
val channelId = db.listChannelIdVapid()
|
val channelId = db.listChannelIdVapid()
|
||||||
|
|
@ -321,9 +325,8 @@ object PrismServerClient {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getVapidPrivateKeyFromDescription(description: String?): String? {
|
private fun getVapidPrivateKeyFromDescription(description: String?): String? =
|
||||||
return DescriptionParser.extractValue(description, VAPID_PRIVATE_DESC_PREFIX)
|
DescriptionParser.extractValue(description, VAPID_PRIVATE_DESC_PREFIX)
|
||||||
}
|
|
||||||
|
|
||||||
private fun isValidVapidPrivateKey(privateKey: String): Boolean = try {
|
private fun isValidVapidPrivateKey(privateKey: String): Boolean = try {
|
||||||
Base64.decode(privateKey, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP).size == 32
|
Base64.decode(privateKey, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP).size == 32
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,6 @@ class MainViewModel(
|
||||||
|
|
||||||
fun refreshRegistrations() {
|
fun refreshRegistrations() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
application?.let { app ->
|
|
||||||
PrismPreferences(app).cleanupLegacyRegistrationAddedAtIfNeeded()
|
|
||||||
}
|
|
||||||
val apps = messenger?.sendIMessageL(InternalOpcode.REG_LIST, "apps", App::class.java)
|
val apps = messenger?.sendIMessageL(InternalOpcode.REG_LIST, "apps", App::class.java)
|
||||||
registrationsViewModel.state = RegistrationListState(apps ?: emptyList())
|
registrationsViewModel.state = RegistrationListState(apps ?: emptyList())
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +103,6 @@ class MainViewModel(
|
||||||
intent.setPackage(app.packageName)
|
intent.setPackage(app.packageName)
|
||||||
intent.putExtra("token", token)
|
intent.putExtra("token", token)
|
||||||
app.sendBroadcast(intent)
|
app.sendBroadcast(intent)
|
||||||
PrismPreferences(app).removeRegistrationAddedAt(token)
|
|
||||||
PrismPreferences(app).removeVapidPrivateKey(token)
|
PrismPreferences(app).removeVapidPrivateKey(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,12 +130,10 @@ class MainViewModel(
|
||||||
|
|
||||||
fun getEndpoint(token: String): String? = getApp(token)?.endpoint
|
fun getEndpoint(token: String): String? = getApp(token)?.endpoint
|
||||||
|
|
||||||
fun getSubscriptionId(token: String): String? = application?.let { app ->
|
fun getChannelId(token: String): String? = application?.let { app ->
|
||||||
PrismPreferences(app).getSubscriptionId(token)
|
val db = DatabaseFactory.getDb(app)
|
||||||
}
|
val appEntry = db.listApps().find { it.connectorToken == token } ?: return@let null
|
||||||
|
db.listChannelIdVapid().find { (_, vapid) -> vapid == appEntry.vapidKey }?.first
|
||||||
fun getRegistrationAddedAt(token: String): Long? = application?.let { app ->
|
|
||||||
PrismPreferences(app).getRegistrationAddedAt(token)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideAppDetails() {
|
fun hideAppDetails() {
|
||||||
|
|
@ -230,7 +224,6 @@ class MainViewModel(
|
||||||
val packageName = targetPackageName.ifBlank { app.packageName }
|
val packageName = targetPackageName.ifBlank { app.packageName }
|
||||||
val preferences = PrismPreferences(app)
|
val preferences = PrismPreferences(app)
|
||||||
preferences.addPendingManualToken(connectorToken)
|
preferences.addPendingManualToken(connectorToken)
|
||||||
preferences.setRegistrationAddedAt(connectorToken, System.currentTimeMillis())
|
|
||||||
preferences.setVapidPrivateKey(connectorToken, vapidKeys.privateKey)
|
preferences.setVapidPrivateKey(connectorToken, vapidKeys.privateKey)
|
||||||
|
|
||||||
val db = DatabaseFactory.getDb(app)
|
val db = DatabaseFactory.getDb(app)
|
||||||
|
|
@ -267,7 +260,6 @@ class MainViewModel(
|
||||||
intent.setPackage(app.packageName)
|
intent.setPackage(app.packageName)
|
||||||
intent.putExtra("token", token)
|
intent.putExtra("token", token)
|
||||||
app.sendBroadcast(intent)
|
app.sendBroadcast(intent)
|
||||||
PrismPreferences(app).removeRegistrationAddedAt(token)
|
|
||||||
PrismPreferences(app).removeVapidPrivateKey(token)
|
PrismPreferences(app).removeVapidPrivateKey(token)
|
||||||
if (selectedRegistrationToken == token) {
|
if (selectedRegistrationToken == token) {
|
||||||
clearSelectedRegistration()
|
clearSelectedRegistration()
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,17 @@ import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.material.icons.filled.Delete
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|
@ -46,8 +46,6 @@ 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 java.text.DateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import org.unifiedpush.android.distributor.ipc.subscribeUiActions
|
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
|
import org.unifiedpush.android.distributor.ui.vm.DistribMigrationViewModel
|
||||||
|
|
@ -324,22 +322,15 @@ fun App(
|
||||||
val selectedRegistration = mainViewModel.selectedRegistrationToken?.let { token ->
|
val selectedRegistration = mainViewModel.selectedRegistrationToken?.let { token ->
|
||||||
mainViewModel.registrationsViewModel.state.list.find { item -> item.token == token }
|
mainViewModel.registrationsViewModel.state.list.find { item -> item.token == token }
|
||||||
}
|
}
|
||||||
val subscriptionId = mainViewModel.selectedRegistrationToken?.let { token ->
|
val channelId = mainViewModel.selectedRegistrationToken?.let { token ->
|
||||||
mainViewModel.getSubscriptionId(token)
|
mainViewModel.getChannelId(token)
|
||||||
}
|
}
|
||||||
val addedDate = mainViewModel.selectedRegistrationToken
|
|
||||||
?.let { token -> mainViewModel.getRegistrationAddedAt(token) }
|
|
||||||
?.let { timestamp ->
|
|
||||||
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT)
|
|
||||||
.format(Date(timestamp))
|
|
||||||
}
|
|
||||||
|
|
||||||
RegistrationDetailsScreen(
|
RegistrationDetailsScreen(
|
||||||
appName = selectedRegistration?.app?.title,
|
appName = selectedRegistration?.app?.title,
|
||||||
packageId = selectedRegistration?.app?.packageName,
|
packageId = selectedRegistration?.app?.packageName,
|
||||||
totalMessages = selectedRegistration?.msgCount,
|
totalMessages = selectedRegistration?.msgCount,
|
||||||
subscriptionId = subscriptionId,
|
channelId = channelId,
|
||||||
addedDate = addedDate,
|
|
||||||
isManual = selectedRegistration?.token?.startsWith("manual_app_") == true,
|
isManual = selectedRegistration?.token?.startsWith("manual_app_") == true,
|
||||||
icon = selectedRegistration?.app?.icon
|
icon = selectedRegistration?.app?.icon
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,7 @@ fun RegistrationDetailsScreen(
|
||||||
appName: String?,
|
appName: String?,
|
||||||
packageId: String?,
|
packageId: String?,
|
||||||
totalMessages: Int?,
|
totalMessages: Int?,
|
||||||
subscriptionId: String?,
|
channelId: String?,
|
||||||
addedDate: String?,
|
|
||||||
isManual: Boolean,
|
isManual: Boolean,
|
||||||
icon: android.graphics.drawable.Drawable?
|
icon: android.graphics.drawable.Drawable?
|
||||||
) {
|
) {
|
||||||
|
|
@ -101,11 +100,6 @@ fun RegistrationDetailsScreen(
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
Text(
|
|
||||||
text = packageId,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -140,18 +134,12 @@ fun RegistrationDetailsScreen(
|
||||||
label = stringResource(R.string.registration_details_messages),
|
label = stringResource(R.string.registration_details_messages),
|
||||||
value = totalMessages.toString()
|
value = totalMessages.toString()
|
||||||
)
|
)
|
||||||
RowDivider()
|
|
||||||
|
|
||||||
DetailRow(
|
|
||||||
label = stringResource(R.string.registration_details_added),
|
|
||||||
value = addedDate ?: stringResource(R.string.registration_details_not_available)
|
|
||||||
)
|
|
||||||
if (isManual) {
|
if (isManual) {
|
||||||
RowDivider()
|
RowDivider()
|
||||||
|
|
||||||
DetailRow(
|
DetailRow(
|
||||||
label = stringResource(R.string.registration_details_subscription_id),
|
label = stringResource(R.string.registration_details_subscription_id),
|
||||||
value = subscriptionId ?: stringResource(R.string.registration_details_not_available)
|
value = channelId ?: stringResource(R.string.registration_details_not_available)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ class ServerConnection(private val context: Context, private val releaseLock: ()
|
||||||
is ServerMessage.Notification -> onNotification(webSocket, message)
|
is ServerMessage.Notification -> onNotification(webSocket, message)
|
||||||
ServerMessage.Ping -> onPing(webSocket)
|
ServerMessage.Ping -> onPing(webSocket)
|
||||||
is ServerMessage.Register -> onRegister(message)
|
is ServerMessage.Register -> onRegister(message)
|
||||||
is ServerMessage.Unregister -> onUnregister(message)
|
is ServerMessage.Unregister -> onUnregister(webSocket, message)
|
||||||
is ServerMessage.Urgency -> {
|
is ServerMessage.Urgency -> {
|
||||||
Log.d(TAG, "Urgency status=${message.status}")
|
Log.d(TAG, "Urgency status=${message.status}")
|
||||||
}
|
}
|
||||||
|
|
@ -237,7 +237,6 @@ class ServerConnection(private val context: Context, private val releaseLock: ()
|
||||||
handleManualRegistrationFailure(
|
handleManualRegistrationFailure(
|
||||||
appTitle = app.title,
|
appTitle = app.title,
|
||||||
connectorToken = app.connectorToken,
|
connectorToken = app.connectorToken,
|
||||||
channelId = message.channelID,
|
|
||||||
error = "Missing VAPID private key for ${app.title ?: "app"}. Delete and re-add the app."
|
error = "Missing VAPID private key for ${app.title ?: "app"}. Delete and re-add the app."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -262,7 +261,6 @@ class ServerConnection(private val context: Context, private val releaseLock: ()
|
||||||
handleManualRegistrationFailure(
|
handleManualRegistrationFailure(
|
||||||
appTitle = app.title,
|
appTitle = app.title,
|
||||||
connectorToken = app.connectorToken,
|
connectorToken = app.connectorToken,
|
||||||
channelId = message.channelID,
|
|
||||||
error = "Failed to persist encryption keys for channel ${message.channelID}"
|
error = "Failed to persist encryption keys for channel ${message.channelID}"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -288,7 +286,6 @@ class ServerConnection(private val context: Context, private val releaseLock: ()
|
||||||
handleManualRegistrationFailure(
|
handleManualRegistrationFailure(
|
||||||
appTitle = app.title,
|
appTitle = app.title,
|
||||||
connectorToken = app.connectorToken,
|
connectorToken = app.connectorToken,
|
||||||
channelId = message.channelID,
|
|
||||||
error = error
|
error = error
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -301,19 +298,22 @@ class ServerConnection(private val context: Context, private val releaseLock: ()
|
||||||
private fun handleManualRegistrationFailure(
|
private fun handleManualRegistrationFailure(
|
||||||
appTitle: String?,
|
appTitle: String?,
|
||||||
connectorToken: String,
|
connectorToken: String,
|
||||||
channelId: String,
|
|
||||||
error: String
|
error: String
|
||||||
) {
|
) {
|
||||||
Log.e(TAG, "Failed to register '$appTitle' with Prism server: $error")
|
Log.e(TAG, "Failed to register '$appTitle' with Prism server: $error")
|
||||||
|
|
||||||
PrismPreferences(context).removePendingManualToken(connectorToken)
|
val preferences = PrismPreferences(context)
|
||||||
|
val isInitialPendingRegistration = preferences.isPendingManualToken(connectorToken)
|
||||||
|
|
||||||
Distributor.deleteApp(context, connectorToken)
|
if (isInitialPendingRegistration) {
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Rolling back pending manual app '$appTitle' after registration failure"
|
||||||
|
)
|
||||||
|
rollbackPendingManualAppRegistration(connectorToken)
|
||||||
|
}
|
||||||
|
|
||||||
MessageSender.send(
|
preferences.removePendingManualToken(connectorToken)
|
||||||
context,
|
|
||||||
ClientMessage.Unregister(channelID = channelId)
|
|
||||||
)
|
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
|
|
@ -326,7 +326,38 @@ class ServerConnection(private val context: Context, private val releaseLock: ()
|
||||||
sendUiAction(context, "RefreshRegistrations")
|
sendUiAction(context, "RefreshRegistrations")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onUnregister(message: ServerMessage.Unregister) {
|
private fun rollbackPendingManualAppRegistration(connectorToken: String) {
|
||||||
|
val db = DatabaseFactory.getDb(context)
|
||||||
|
val app = db.listApps().find { it.connectorToken == connectorToken }
|
||||||
|
val channelId = app?.let {
|
||||||
|
db.listChannelIdVapid().find { (_, vapid) -> vapid == it.vapidKey }?.first
|
||||||
|
}
|
||||||
|
|
||||||
|
channelId?.let { EncryptionKeyStore(context).deleteKeys(it) }
|
||||||
|
db.unregisterApp(connectorToken)
|
||||||
|
|
||||||
|
val preferences = PrismPreferences(context)
|
||||||
|
preferences.removeSubscriptionId(connectorToken)
|
||||||
|
preferences.removeRegisteredEndpoint(connectorToken)
|
||||||
|
preferences.removeVapidPrivateKey(connectorToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onUnregister(webSocket: WebSocket, message: ServerMessage.Unregister) {
|
||||||
|
val db = DatabaseFactory.getDb(context)
|
||||||
|
val channelVapidPair = db.listChannelIdVapid().find { (channelId, _) -> channelId == message.channelID }
|
||||||
|
val appForChannel = channelVapidPair?.let { (_, vapid) ->
|
||||||
|
db.listApps().find { app -> app.vapidKey == vapid }
|
||||||
|
}
|
||||||
|
val isManualChannel = appForChannel?.let { DescriptionParser.isManualApp(it.description) } == true
|
||||||
|
|
||||||
|
if (isManualChannel) {
|
||||||
|
Log.w(TAG, "Received unregister for manual channel ${message.channelID}; re-registering instead of deleting app")
|
||||||
|
ClientMessage.Register(
|
||||||
|
channelID = message.channelID,
|
||||||
|
key = channelVapidPair!!.second
|
||||||
|
).send(webSocket)
|
||||||
|
return
|
||||||
|
}
|
||||||
Distributor.deleteChannelFromServer(context, message.channelID)
|
Distributor.deleteChannelFromServer(context, message.channelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,9 @@ import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.drawable.BitmapDrawable
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.Person
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import app.lonecloud.prism.DatabaseFactory
|
import app.lonecloud.prism.DatabaseFactory
|
||||||
import app.lonecloud.prism.R
|
import app.lonecloud.prism.R
|
||||||
import app.lonecloud.prism.api.data.NotificationAction
|
import app.lonecloud.prism.api.data.NotificationAction
|
||||||
|
|
@ -46,24 +41,39 @@ object ManualAppNotifications {
|
||||||
val channelId = "manual_app_${app.connectorToken}"
|
val channelId = "manual_app_${app.connectorToken}"
|
||||||
val appTitle = app.title ?: "Unknown App"
|
val appTitle = app.title ?: "Unknown App"
|
||||||
createNotificationChannel(context, channelId, appTitle)
|
createNotificationChannel(context, channelId, appTitle)
|
||||||
val displayTitle = payload.title.ifBlank { appTitle }
|
|
||||||
|
val hasTitle = payload.title.isNotBlank()
|
||||||
|
val hasMessage = payload.message.isNotBlank()
|
||||||
|
val sender = if (hasTitle && hasMessage) payload.title else null
|
||||||
|
val bodyText = when {
|
||||||
|
hasMessage -> payload.message
|
||||||
|
hasTitle -> payload.title
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
val notificationId = getNotificationId(payload.tag)
|
val notificationId = getNotificationId(payload.tag)
|
||||||
val packageName = resolveTargetPackage(app)
|
val packageName = resolveTargetPackage(app)
|
||||||
val appPerson = buildNotificationPerson(context, appTitle, packageName)
|
|
||||||
val messageStyle = NotificationCompat.MessagingStyle(appPerson)
|
val contentText = sender?.let { "$it: $bodyText" } ?: bodyText
|
||||||
.setConversationTitle(displayTitle)
|
val bigTextStyle = NotificationCompat.BigTextStyle()
|
||||||
.addMessage(payload.message, System.currentTimeMillis(), appPerson)
|
.bigText(bodyText)
|
||||||
|
.also { style ->
|
||||||
|
sender?.let { style.setSummaryText(it) }
|
||||||
|
}
|
||||||
|
|
||||||
val notificationBuilder = NotificationCompat.Builder(context, channelId)
|
val notificationBuilder = NotificationCompat.Builder(context, channelId)
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setContentTitle(displayTitle)
|
.setContentTitle(appTitle)
|
||||||
.setContentText(payload.message)
|
.setContentText(contentText)
|
||||||
.setStyle(messageStyle)
|
.setStyle(bigTextStyle)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setGroup(app.connectorToken)
|
.setGroup(app.connectorToken)
|
||||||
|
|
||||||
|
resolveAppIconBitmap(context, packageName)?.let { appIcon ->
|
||||||
|
notificationBuilder.setLargeIcon(appIcon)
|
||||||
|
}
|
||||||
|
|
||||||
val contentIntent = createContentIntent(context, packageName, notificationId)
|
val contentIntent = createContentIntent(context, packageName, notificationId)
|
||||||
if (contentIntent != null) {
|
if (contentIntent != null) {
|
||||||
notificationBuilder.setContentIntent(contentIntent)
|
notificationBuilder.setContentIntent(contentIntent)
|
||||||
|
|
@ -88,12 +98,35 @@ object ManualAppNotifications {
|
||||||
}
|
}
|
||||||
|
|
||||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
notificationManager.notify(payload.tag, notificationId, notificationBuilder.build())
|
notificationManager.notify(
|
||||||
|
payload.tag,
|
||||||
|
notificationId,
|
||||||
|
notificationBuilder.build()
|
||||||
|
)
|
||||||
|
|
||||||
incrementMessageCount(context, app)
|
incrementMessageCount(context, app)
|
||||||
refreshMessageCount(context)
|
refreshMessageCount(context)
|
||||||
|
|
||||||
Log.d(TAG, "Displayed notification for manual app '${app.title}': ${payload.title} (tag: ${payload.tag})")
|
val previewSender = sender ?: ""
|
||||||
|
val logMessage =
|
||||||
|
"Displayed notification for manual app '${app.title}' " +
|
||||||
|
"sender='$previewSender' body='${bodyText.take(120)}' (tag: ${payload.tag})"
|
||||||
|
Log.d(TAG, logMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveAppIconBitmap(context: Context, packageName: String?): android.graphics.Bitmap? {
|
||||||
|
if (packageName.isNullOrBlank()) return null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
context.packageManager.getApplicationIcon(packageName).toBitmap()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Could not resolve app icon for package: $packageName",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissNotification(context: Context, tag: String) {
|
fun dismissNotification(context: Context, tag: String) {
|
||||||
|
|
@ -205,45 +238,4 @@ object ManualAppNotifications {
|
||||||
private fun refreshMessageCount(context: Context) {
|
private fun refreshMessageCount(context: Context) {
|
||||||
MainRegistrationCounter.onCountRefreshed(context)
|
MainRegistrationCounter.onCountRefreshed(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildNotificationPerson(
|
|
||||||
context: Context,
|
|
||||||
appTitle: String,
|
|
||||||
packageName: String?
|
|
||||||
): Person {
|
|
||||||
val personBuilder = Person.Builder()
|
|
||||||
.setName(appTitle)
|
|
||||||
|
|
||||||
resolveAppIconBitmap(context, packageName)?.let { iconBitmap ->
|
|
||||||
personBuilder.setIcon(IconCompat.createWithBitmap(iconBitmap))
|
|
||||||
}
|
|
||||||
|
|
||||||
return personBuilder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveAppIconBitmap(context: Context, packageName: String?): Bitmap? {
|
|
||||||
if (packageName == null) return null
|
|
||||||
|
|
||||||
return try {
|
|
||||||
val drawable = context.packageManager.getApplicationIcon(packageName)
|
|
||||||
drawable.toBitmap()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Could not resolve app icon for package: $packageName", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Drawable.toBitmap(): Bitmap {
|
|
||||||
if (this is BitmapDrawable && bitmap != null) {
|
|
||||||
return bitmap
|
|
||||||
}
|
|
||||||
|
|
||||||
val width = intrinsicWidth.takeIf { it > 0 } ?: 128
|
|
||||||
val height = intrinsicHeight.takeIf { it > 0 } ?: 128
|
|
||||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
||||||
val canvas = Canvas(bitmap)
|
|
||||||
setBounds(0, 0, canvas.width, canvas.height)
|
|
||||||
draw(canvas)
|
|
||||||
return bitmap
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,7 @@
|
||||||
<string name="registration_details_not_found">Registration not found.</string>
|
<string name="registration_details_not_found">Registration not found.</string>
|
||||||
<string name="registration_details_package_id">Package ID</string>
|
<string name="registration_details_package_id">Package ID</string>
|
||||||
<string name="registration_details_messages">Total Messages</string>
|
<string name="registration_details_messages">Total Messages</string>
|
||||||
<string name="registration_details_added">Added</string>
|
<string name="registration_details_subscription_id">Channel ID</string>
|
||||||
<string name="registration_details_subscription_id">Subscription ID</string>
|
|
||||||
<string name="registration_details_not_available">Not available</string>
|
<string name="registration_details_not_available">Not available</string>
|
||||||
<string name="registration_details_type">Registration Type</string>
|
<string name="registration_details_type">Registration Type</string>
|
||||||
<string name="registration_details_type_manual">Manual</string>
|
<string name="registration_details_type_manual">Manual</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue