manual app fixes and improvements

This commit is contained in:
Egor 2026-02-18 13:54:40 -08:00
parent 423d91dc1f
commit 874444c6be
9 changed files with 115 additions and 165 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(
MessageSender.send( TAG,
context, "Rolling back pending manual app '$appTitle' after registration failure"
ClientMessage.Unregister(channelID = channelId)
) )
rollbackPendingManualAppRegistration(connectorToken)
}
preferences.removePendingManualToken(connectorToken)
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)
} }

View file

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

View file

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