diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/Database.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/Database.kt index c7cc58f..c97f39d 100644 --- a/app/src/main/java/org/unifiedpush/distributor/nextpush/Database.kt +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/Database.kt @@ -10,6 +10,21 @@ import java.util.concurrent.atomic.AtomicReference class Database(val context: Context) : 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) { db.execSQL(CREATE_TABLE_APPS) } @@ -20,19 +35,21 @@ class Database(val context: Context) : while (v < newVersion) { when (v) { 1 -> db?.execSQL(UPGRADE_1_2) + 2 -> db?.execSQL(UPGRADE_2_3) else -> throw IllegalStateException("Upgrade not supported") } 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 values = ContentValues().apply { put(FIELD_PACKAGE_NAME, packageName) put(FIELD_CONNECTOR_TOKEN, connectorToken) put(FIELD_APP_TOKEN, appToken) put(FIELD_NOTIFICATION_TITLE, title) // Used for non-UnifiedPush notif + put(FIELD_VAPID, vapidKey) } db.insert(TABLE_APPS, null, values) RegistrationCountCache.refresh(context) @@ -46,20 +63,32 @@ class Database(val context: 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 selection = "$FIELD_PACKAGE_NAME = ? AND $FIELD_CONNECTOR_TOKEN = ?" - val selectionArgs = arrayOf(packageName, connectorToken) + val projection = arrayOf(FIELD_PACKAGE_NAME, FIELD_VAPID) + val selection = "$FIELD_CONNECTOR_TOKEN = ?" + val selectionArgs = arrayOf(connectorToken) return db.query( TABLE_APPS, - null, + projection, selection, selectionArgs, null, null, null ).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_APP_TOKEN = "appToken" 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 (" + "$FIELD_PACKAGE_NAME TEXT," + "$FIELD_CONNECTOR_TOKEN TEXT," + "$FIELD_APP_TOKEN TEXT," + "$FIELD_NOTIFICATION_TITLE TEXT," + + "$FIELD_VAPID TEXT," + "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_2_3 = "ALTER TABLE $TABLE_APPS ADD COLUMN $FIELD_VAPID TEXT" private val db: AtomicReference = AtomicReference(null) diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/LocalNotification.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/LocalNotification.kt index 4f61be7..bf6259a 100644 --- a/app/src/main/java/org/unifiedpush/distributor/nextpush/LocalNotification.kt +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/LocalNotification.kt @@ -8,9 +8,9 @@ import java.util.UUID object LocalNotification { 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 { - getDb(context).registerApp(context.packageName, UUID.randomUUID().toString(), it, title) + getDb(context).registerApp(context.packageName, UUID.randomUUID().toString(), it, title, null) } block() } diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/api/Api.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/api/Api.kt index 047bdb0..c972027 100644 --- a/app/src/main/java/org/unifiedpush/distributor/nextpush/api/Api.kt +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/api/Api.kt @@ -147,6 +147,7 @@ class Api(context: Context) { fun apiCreateApp( appName: String, + vapid: String?, block: (String?) -> Unit ) { tryWithDeviceId { deviceId -> @@ -155,6 +156,9 @@ class Api(context: Context) { "deviceId" to deviceId, "appName" to appName ) + vapid?.let { + parameters.put("vapid", it) + } try { withApiProvider { apiProvider, then -> apiProvider.createApp(parameters) diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/distributor/Distributor.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/distributor/Distributor.kt index 7123667..61be9d4 100644 --- a/app/src/main/java/org/unifiedpush/distributor/nextpush/distributor/Distributor.kt +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/distributor/Distributor.kt @@ -3,6 +3,7 @@ package org.unifiedpush.distributor.nextpush.distributor import android.content.Context import android.content.Intent import android.util.Log +import org.unifiedpush.distributor.nextpush.Database import org.unifiedpush.distributor.nextpush.Database.Companion.getDb import org.unifiedpush.distributor.nextpush.LocalNotification 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) - if (connectorToken !in db.listTokens()) { - return ConnectorTokenValidity.TOKEN_NEW + try { + 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 ConnectorTokenValidity.TOKEN_REGISTERED_OK - } - return ConnectorTokenValidity.TOKEN_NOK + return RegistrationStatus.REGISTERED_OK } fun deleteDevice(context: Context, block: () -> Unit = {}) { @@ -119,17 +124,17 @@ object Distributor { * * [block]'s parameter is `true` if we have successfully created the registration. */ - fun createApp(context: Context, appName: String, connectorToken: String, block: (Boolean) -> Unit) { - Api(context).apiCreateApp(appName) { nextpushToken -> + fun createApp(context: Context, appName: String, connectorToken: String, vapid: String?, block: (Boolean) -> Unit) { + Api(context).apiCreateApp(appName, vapid) { nextpushToken -> nextpushToken?.let { - getDb(context).registerApp(appName, connectorToken, it) + getDb(context).registerApp(appName, connectorToken, it, null, vapid) block(true) } ?: block(false) } } - fun deleteApp(context: Context, connectorToken: String, block: () -> Unit) { - sendUnregistered(context, connectorToken) + fun deleteApp(context: Context, connectorToken: String, sendIntent: Boolean = true, block: () -> Unit) { + if (sendIntent) sendUnregistered(context, connectorToken) val db = getDb(context) db.getAppToken( connectorToken diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/distributor/ConnectorTokenValidity.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/distributor/RegistrationStatus.kt similarity index 67% rename from app/src/main/java/org/unifiedpush/distributor/nextpush/distributor/ConnectorTokenValidity.kt rename to app/src/main/java/org/unifiedpush/distributor/nextpush/distributor/RegistrationStatus.kt index 5d25022..65538cb 100644 --- a/app/src/main/java/org/unifiedpush/distributor/nextpush/distributor/ConnectorTokenValidity.kt +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/distributor/RegistrationStatus.kt @@ -3,13 +3,16 @@ package org.unifiedpush.distributor.nextpush.distributor /** * Validity of a connection token received during registration. */ -enum class ConnectorTokenValidity { +enum class RegistrationStatus { /** This is a new token. */ - TOKEN_NEW, + NEW, /** 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. */ - TOKEN_NOK + ERROR } diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/distributor/UnifiedPushConstants.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/distributor/UnifiedPushConstants.kt index e2f6a39..35779a4 100644 --- a/app/src/main/java/org/unifiedpush/distributor/nextpush/distributor/UnifiedPushConstants.kt +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/distributor/UnifiedPushConstants.kt @@ -16,6 +16,7 @@ const val ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER" const val EXTRA_APPLICATION = "application" const val EXTRA_PI = "pi" const val EXTRA_TOKEN = "token" +const val EXTRA_VAPID = "vapid" const val EXTRA_ENDPOINT = "endpoint" const val EXTRA_FAILED_REASON = "reason" const val EXTRA_BYTES_MESSAGE = "bytesMessage" diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/receivers/RegisterBroadcastReceiver.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/receivers/RegisterBroadcastReceiver.kt index d0dc91f..3bae650 100644 --- a/app/src/main/java/org/unifiedpush/distributor/nextpush/receivers/RegisterBroadcastReceiver.kt +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/receivers/RegisterBroadcastReceiver.kt @@ -13,7 +13,7 @@ import org.unifiedpush.distributor.nextpush.Database.Companion.getDb import org.unifiedpush.distributor.nextpush.WakeLock import org.unifiedpush.distributor.nextpush.account.AccountFactory 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.deleteApp 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") return } - onRegister(context, connectorToken, application) + val vapid = intent.getStringExtra(EXTRA_VAPID)?.trim()?.takeIf { it.length == 87 } + onRegister(context, connectorToken, application, vapid) } ACTION_UNREGISTER -> { Log.i(TAG, "UNREGISTER") @@ -126,12 +127,13 @@ class RegisterBroadcastReceiver : BroadcastReceiver() { /** * 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)) { - when (checkToken(context, connectorToken, application)) { - ConnectorTokenValidity.TOKEN_REGISTERED_OK -> onRegisterKnownToken(context, connectorToken) - ConnectorTokenValidity.TOKEN_NOK -> onRegisterNokToken(context, connectorToken, application) - ConnectorTokenValidity.TOKEN_NEW -> onRegisterNewToken(context, connectorToken, application) + when (checkRegistration(context, connectorToken, application, vapid)) { + RegistrationStatus.REGISTERED_OK -> onRegisterKnownToken(context, connectorToken) + RegistrationStatus.ERROR -> onRegisterNokToken(context, connectorToken, application) + RegistrationStatus.UPDATED -> onRegisterUpdatedToken(context, connectorToken, application, vapid) + RegistrationStatus.NEW -> onRegisterNewToken(context, connectorToken, application, vapid) } AppCompanion.createQueue.removeToken(connectorToken) } 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] */ - 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 when { AccountFactory.getAccount(context)?.connected != true -> registrationFailedWithToast( @@ -184,13 +198,16 @@ class RegisterBroadcastReceiver : BroadcastReceiver() { else -> createApp( context, application, - connectorToken + connectorToken, + vapid ) { success -> when (success) { true -> { sendEndpoint(context, connectorToken) - Toast.makeText(context, "$appName registered.", Toast.LENGTH_SHORT) - .show() + if (toastOnSuccess) { + Toast.makeText(context, "$appName registered.", Toast.LENGTH_SHORT) + .show() + } } false -> registrationFailedWithToast(