Add VAPID support

This commit is contained in:
sim 2024-11-04 19:13:14 +00:00
parent b093c7869b
commit 6ee69319fd
7 changed files with 100 additions and 38 deletions

View File

@ -10,6 +10,21 @@ import java.util.concurrent.atomic.AtomicReference
class Database(val context: Context) : class Database(val context: Context) :
SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
/** No record has been found. */
class NoRecordException : Throwable() {
private fun readResolve(): Any = WrongPackageNameException()
}
/** A record has been found for another packageName. */
class WrongPackageNameException : Throwable() {
private fun readResolve(): Any = WrongPackageNameException()
}
/** A record has been found with another vapid key. */
class WrongVapidException : Exception() {
private fun readResolve(): Any = WrongVapidException()
}
override fun onCreate(db: SQLiteDatabase) { override fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_TABLE_APPS) db.execSQL(CREATE_TABLE_APPS)
} }
@ -20,19 +35,21 @@ class Database(val context: Context) :
while (v < newVersion) { while (v < newVersion) {
when (v) { when (v) {
1 -> db?.execSQL(UPGRADE_1_2) 1 -> db?.execSQL(UPGRADE_1_2)
2 -> db?.execSQL(UPGRADE_2_3)
else -> throw IllegalStateException("Upgrade not supported") else -> throw IllegalStateException("Upgrade not supported")
} }
v++ v++
} }
} }
fun registerApp(packageName: String, connectorToken: String, appToken: String, title: String = "") { fun registerApp(packageName: String, connectorToken: String, appToken: String, title: String?, vapidKey: String?) {
val db = writableDatabase val db = writableDatabase
val values = ContentValues().apply { val values = ContentValues().apply {
put(FIELD_PACKAGE_NAME, packageName) put(FIELD_PACKAGE_NAME, packageName)
put(FIELD_CONNECTOR_TOKEN, connectorToken) put(FIELD_CONNECTOR_TOKEN, connectorToken)
put(FIELD_APP_TOKEN, appToken) put(FIELD_APP_TOKEN, appToken)
put(FIELD_NOTIFICATION_TITLE, title) // Used for non-UnifiedPush notif put(FIELD_NOTIFICATION_TITLE, title) // Used for non-UnifiedPush notif
put(FIELD_VAPID, vapidKey)
} }
db.insert(TABLE_APPS, null, values) db.insert(TABLE_APPS, null, values)
RegistrationCountCache.refresh(context) RegistrationCountCache.refresh(context)
@ -46,20 +63,32 @@ class Database(val context: Context) :
RegistrationCountCache.refresh(context) RegistrationCountCache.refresh(context)
} }
fun isRegistered(packageName: String, connectorToken: String): Boolean { /**
* Assert [connectorToken] is registered for [packageName] with the [vapidKey].
*
* @throws [NoRecordException] if the [connectorToken] isn't registered
* @throws [WrongPackageNameException] if the [packageName] does not match the record
* @throws [WrongVapidException] if the [vapidKey] does not match the record
*/
fun assertIsRegistered(connectorToken: String, packageName: String, vapidKey: String?) {
val db = readableDatabase val db = readableDatabase
val selection = "$FIELD_PACKAGE_NAME = ? AND $FIELD_CONNECTOR_TOKEN = ?" val projection = arrayOf(FIELD_PACKAGE_NAME, FIELD_VAPID)
val selectionArgs = arrayOf(packageName, connectorToken) val selection = "$FIELD_CONNECTOR_TOKEN = ?"
val selectionArgs = arrayOf(connectorToken)
return db.query( return db.query(
TABLE_APPS, TABLE_APPS,
null, projection,
selection, selection,
selectionArgs, selectionArgs,
null, null,
null, null,
null null
).use { cursor -> ).use { cursor ->
(cursor != null && cursor.count > 0) val packageNameColumn = cursor.getColumnIndex(FIELD_PACKAGE_NAME)
val vapidColumn = cursor.getColumnIndex(FIELD_VAPID)
if (!cursor.moveToFirst() || packageNameColumn < 0 || vapidColumn < 0) throw NoRecordException()
if (cursor.getString(packageNameColumn) != packageName) throw WrongPackageNameException()
if (cursor.getString(vapidColumn) != vapidKey) throw WrongVapidException()
} }
} }
@ -169,14 +198,17 @@ class Database(val context: Context) :
private const val FIELD_CONNECTOR_TOKEN = "connectorToken" private const val FIELD_CONNECTOR_TOKEN = "connectorToken"
private const val FIELD_APP_TOKEN = "appToken" private const val FIELD_APP_TOKEN = "appToken"
private const val FIELD_NOTIFICATION_TITLE = "notificationTitle" // Used for non-UnifiedPush notif private const val FIELD_NOTIFICATION_TITLE = "notificationTitle" // Used for non-UnifiedPush notif
private const val FIELD_VAPID = "vapid"
private const val CREATE_TABLE_APPS = "CREATE TABLE $TABLE_APPS (" + private const val CREATE_TABLE_APPS = "CREATE TABLE $TABLE_APPS (" +
"$FIELD_PACKAGE_NAME TEXT," + "$FIELD_PACKAGE_NAME TEXT," +
"$FIELD_CONNECTOR_TOKEN TEXT," + "$FIELD_CONNECTOR_TOKEN TEXT," +
"$FIELD_APP_TOKEN TEXT," + "$FIELD_APP_TOKEN TEXT," +
"$FIELD_NOTIFICATION_TITLE TEXT," + "$FIELD_NOTIFICATION_TITLE TEXT," +
"$FIELD_VAPID TEXT," +
"PRIMARY KEY ($FIELD_CONNECTOR_TOKEN));" "PRIMARY KEY ($FIELD_CONNECTOR_TOKEN));"
private const val UPGRADE_1_2 = "ALTER TABLE $TABLE_APPS ADD COLUMN $FIELD_NOTIFICATION_TITLE TEXT" private const val UPGRADE_1_2 = "ALTER TABLE $TABLE_APPS ADD COLUMN $FIELD_NOTIFICATION_TITLE TEXT"
private const val UPGRADE_2_3 = "ALTER TABLE $TABLE_APPS ADD COLUMN $FIELD_VAPID TEXT"
private val db: AtomicReference<Database?> = AtomicReference(null) private val db: AtomicReference<Database?> = AtomicReference(null)

View File

@ -8,9 +8,9 @@ import java.util.UUID
object LocalNotification { object LocalNotification {
fun createChannel(context: Context, title: String, block: () -> Unit) { fun createChannel(context: Context, title: String, block: () -> Unit) {
Api(context).apiCreateApp(context.getString(R.string.local_notif_title).format(title)) { nextpushToken -> Api(context).apiCreateApp(context.getString(R.string.local_notif_title).format(title), null) { nextpushToken ->
nextpushToken?.let { nextpushToken?.let {
getDb(context).registerApp(context.packageName, UUID.randomUUID().toString(), it, title) getDb(context).registerApp(context.packageName, UUID.randomUUID().toString(), it, title, null)
} }
block() block()
} }

View File

@ -147,6 +147,7 @@ class Api(context: Context) {
fun apiCreateApp( fun apiCreateApp(
appName: String, appName: String,
vapid: String?,
block: (String?) -> Unit block: (String?) -> Unit
) { ) {
tryWithDeviceId { deviceId -> tryWithDeviceId { deviceId ->
@ -155,6 +156,9 @@ class Api(context: Context) {
"deviceId" to deviceId, "deviceId" to deviceId,
"appName" to appName "appName" to appName
) )
vapid?.let {
parameters.put("vapid", it)
}
try { try {
withApiProvider { apiProvider, then -> withApiProvider { apiProvider, then ->
apiProvider.createApp(parameters) apiProvider.createApp(parameters)

View File

@ -3,6 +3,7 @@ package org.unifiedpush.distributor.nextpush.distributor
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log import android.util.Log
import org.unifiedpush.distributor.nextpush.Database
import org.unifiedpush.distributor.nextpush.Database.Companion.getDb import org.unifiedpush.distributor.nextpush.Database.Companion.getDb
import org.unifiedpush.distributor.nextpush.LocalNotification import org.unifiedpush.distributor.nextpush.LocalNotification
import org.unifiedpush.distributor.nextpush.account.AccountFactory.getAccount import org.unifiedpush.distributor.nextpush.account.AccountFactory.getAccount
@ -90,19 +91,23 @@ object Distributor {
} }
/** /**
* Check if the [connectorToken] is known and correspond to the [app]. * Check if the [connectorToken] is known and correspond to the [app], or if the vapid as been
* updated.
* *
* @return [ConnectorTokenValidity] * @return [RegistrationStatus]
*/ */
fun checkToken(context: Context, connectorToken: String, app: String): ConnectorTokenValidity { fun checkRegistration(context: Context, connectorToken: String, app: String, vapid: String?): RegistrationStatus {
val db = getDb(context) val db = getDb(context)
if (connectorToken !in db.listTokens()) { try {
return ConnectorTokenValidity.TOKEN_NEW db.assertIsRegistered(connectorToken, app, vapid)
} catch (e: Database.NoRecordException) {
return RegistrationStatus.NEW
} catch (e: Database.WrongPackageNameException) {
return RegistrationStatus.ERROR
} catch (e: Database.WrongVapidException) {
return RegistrationStatus.UPDATED
} }
if (db.isRegistered(app, connectorToken)) { return RegistrationStatus.REGISTERED_OK
return ConnectorTokenValidity.TOKEN_REGISTERED_OK
}
return ConnectorTokenValidity.TOKEN_NOK
} }
fun deleteDevice(context: Context, block: () -> Unit = {}) { fun deleteDevice(context: Context, block: () -> Unit = {}) {
@ -119,17 +124,17 @@ object Distributor {
* *
* [block]'s parameter is `true` if we have successfully created the registration. * [block]'s parameter is `true` if we have successfully created the registration.
*/ */
fun createApp(context: Context, appName: String, connectorToken: String, block: (Boolean) -> Unit) { fun createApp(context: Context, appName: String, connectorToken: String, vapid: String?, block: (Boolean) -> Unit) {
Api(context).apiCreateApp(appName) { nextpushToken -> Api(context).apiCreateApp(appName, vapid) { nextpushToken ->
nextpushToken?.let { nextpushToken?.let {
getDb(context).registerApp(appName, connectorToken, it) getDb(context).registerApp(appName, connectorToken, it, null, vapid)
block(true) block(true)
} ?: block(false) } ?: block(false)
} }
} }
fun deleteApp(context: Context, connectorToken: String, block: () -> Unit) { fun deleteApp(context: Context, connectorToken: String, sendIntent: Boolean = true, block: () -> Unit) {
sendUnregistered(context, connectorToken) if (sendIntent) sendUnregistered(context, connectorToken)
val db = getDb(context) val db = getDb(context)
db.getAppToken( db.getAppToken(
connectorToken connectorToken

View File

@ -3,13 +3,16 @@ package org.unifiedpush.distributor.nextpush.distributor
/** /**
* Validity of a connection token received during registration. * Validity of a connection token received during registration.
*/ */
enum class ConnectorTokenValidity { enum class RegistrationStatus {
/** This is a new token. */ /** This is a new token. */
TOKEN_NEW, NEW,
/** This is a known token, which match the provided application. */ /** This is a known token, which match the provided application. */
TOKEN_REGISTERED_OK, REGISTERED_OK,
/** The registration need to be updated, vapid doesn't match */
UPDATED,
/** This is a known token, but it doesn't match the application. */ /** This is a known token, but it doesn't match the application. */
TOKEN_NOK ERROR
} }

View File

@ -16,6 +16,7 @@ const val ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER"
const val EXTRA_APPLICATION = "application" const val EXTRA_APPLICATION = "application"
const val EXTRA_PI = "pi" const val EXTRA_PI = "pi"
const val EXTRA_TOKEN = "token" const val EXTRA_TOKEN = "token"
const val EXTRA_VAPID = "vapid"
const val EXTRA_ENDPOINT = "endpoint" const val EXTRA_ENDPOINT = "endpoint"
const val EXTRA_FAILED_REASON = "reason" const val EXTRA_FAILED_REASON = "reason"
const val EXTRA_BYTES_MESSAGE = "bytesMessage" const val EXTRA_BYTES_MESSAGE = "bytesMessage"

View File

@ -13,7 +13,7 @@ import org.unifiedpush.distributor.nextpush.Database.Companion.getDb
import org.unifiedpush.distributor.nextpush.WakeLock import org.unifiedpush.distributor.nextpush.WakeLock
import org.unifiedpush.distributor.nextpush.account.AccountFactory import org.unifiedpush.distributor.nextpush.account.AccountFactory
import org.unifiedpush.distributor.nextpush.distributor.* // ktlint-disable no-wildcard-imports import org.unifiedpush.distributor.nextpush.distributor.* // ktlint-disable no-wildcard-imports
import org.unifiedpush.distributor.nextpush.distributor.Distributor.checkToken import org.unifiedpush.distributor.nextpush.distributor.Distributor.checkRegistration
import org.unifiedpush.distributor.nextpush.distributor.Distributor.createApp import org.unifiedpush.distributor.nextpush.distributor.Distributor.createApp
import org.unifiedpush.distributor.nextpush.distributor.Distributor.deleteApp import org.unifiedpush.distributor.nextpush.distributor.Distributor.deleteApp
import org.unifiedpush.distributor.nextpush.distributor.Distributor.sendEndpoint import org.unifiedpush.distributor.nextpush.distributor.Distributor.sendEndpoint
@ -112,7 +112,8 @@ class RegisterBroadcastReceiver : BroadcastReceiver() {
Log.w(TAG, "Trying to register an app without packageName") Log.w(TAG, "Trying to register an app without packageName")
return return
} }
onRegister(context, connectorToken, application) val vapid = intent.getStringExtra(EXTRA_VAPID)?.trim()?.takeIf { it.length == 87 }
onRegister(context, connectorToken, application, vapid)
} }
ACTION_UNREGISTER -> { ACTION_UNREGISTER -> {
Log.i(TAG, "UNREGISTER") Log.i(TAG, "UNREGISTER")
@ -126,12 +127,13 @@ class RegisterBroadcastReceiver : BroadcastReceiver() {
/** /**
* Register the app on the server and send the new endpoint * Register the app on the server and send the new endpoint
*/ */
private fun onRegister(context: Context, connectorToken: String, application: String) { private fun onRegister(context: Context, connectorToken: String, application: String, vapid: String?) {
if (!AppCompanion.createQueue.containsTokenElseAdd(connectorToken)) { if (!AppCompanion.createQueue.containsTokenElseAdd(connectorToken)) {
when (checkToken(context, connectorToken, application)) { when (checkRegistration(context, connectorToken, application, vapid)) {
ConnectorTokenValidity.TOKEN_REGISTERED_OK -> onRegisterKnownToken(context, connectorToken) RegistrationStatus.REGISTERED_OK -> onRegisterKnownToken(context, connectorToken)
ConnectorTokenValidity.TOKEN_NOK -> onRegisterNokToken(context, connectorToken, application) RegistrationStatus.ERROR -> onRegisterNokToken(context, connectorToken, application)
ConnectorTokenValidity.TOKEN_NEW -> onRegisterNewToken(context, connectorToken, application) RegistrationStatus.UPDATED -> onRegisterUpdatedToken(context, connectorToken, application, vapid)
RegistrationStatus.NEW -> onRegisterNewToken(context, connectorToken, application, vapid)
} }
AppCompanion.createQueue.removeToken(connectorToken) AppCompanion.createQueue.removeToken(connectorToken)
} else { } else {
@ -160,11 +162,23 @@ class RegisterBroadcastReceiver : BroadcastReceiver() {
} }
/** /**
* Registering a new token. * Update a know token.
*
* Remove the previous app and register a new one.
*/
private fun onRegisterUpdatedToken(context: Context, connectorToken: String, application: String, vapid: String?) {
Log.d(TAG, "Updating registration for $application")
deleteApp(context, connectorToken, false) {
onRegisterNewToken(context, connectorToken, application, vapid, toastOnSuccess = false)
}
}
/**
* Register a new token.
* *
* If we are not connected to Nextcloud, we send registration failed with [FailedReason.ACTION_REQUIRED] * If we are not connected to Nextcloud, we send registration failed with [FailedReason.ACTION_REQUIRED]
*/ */
private fun onRegisterNewToken(context: Context, connectorToken: String, application: String) { private fun onRegisterNewToken(context: Context, connectorToken: String, application: String, vapid: String?, toastOnSuccess: Boolean = true) {
val appName = context.getApplicationName(application) ?: application val appName = context.getApplicationName(application) ?: application
when { when {
AccountFactory.getAccount(context)?.connected != true -> registrationFailedWithToast( AccountFactory.getAccount(context)?.connected != true -> registrationFailedWithToast(
@ -184,14 +198,17 @@ class RegisterBroadcastReceiver : BroadcastReceiver() {
else -> createApp( else -> createApp(
context, context,
application, application,
connectorToken connectorToken,
vapid
) { success -> ) { success ->
when (success) { when (success) {
true -> { true -> {
sendEndpoint(context, connectorToken) sendEndpoint(context, connectorToken)
if (toastOnSuccess) {
Toast.makeText(context, "$appName registered.", Toast.LENGTH_SHORT) Toast.makeText(context, "$appName registered.", Toast.LENGTH_SHORT)
.show() .show()
} }
}
false -> registrationFailedWithToast( false -> registrationFailedWithToast(
context, context,