diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 76868df..3dfbef8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -47,8 +47,12 @@ android:label="@string/title_activity_settings" /> - - + + + + + + diff --git a/app/src/main/java/com/github/gotify/service/Constants.kt b/app/src/main/java/com/github/gotify/service/Constants.kt index ece1fc6..59bf7e2 100644 --- a/app/src/main/java/com/github/gotify/service/Constants.kt +++ b/app/src/main/java/com/github/gotify/service/Constants.kt @@ -1,14 +1,14 @@ package com.github.gotify.service /** - * Command to the service to register a client, receiving callbacks - * from the service. The Message's replyTo field must be a Messenger of - * the client where callbacks should be sent. + * Constants found here: + * https://github.com/UnifiedPush/UP-lib/blob/main/src/main/java/org/unifiedpush/connector/Constants.kt */ -const val TYPE_CLIENT_STARTED = 1 -const val TYPE_REGISTER_CLIENT = 2 -const val TYPE_REGISTERED_CLIENT = 3 -const val TYPE_UNREGISTER_CLIENT = 4 -const val TYPE_UNREGISTERED_CLIENT = 5 -const val TYPE_MESSAGE = 6 -const val TYPE_CHANGED_URL = 7 \ No newline at end of file + +const val NEW_ENDPOINT = "org.unifiedpush.android.connector.NEW_ENDPOINT" +const val UNREGISTERED = "org.unifiedpush.android.connector.UNREGISTERED" +const val MESSAGE = "org.unifiedpush.android.connector.MESSAGE" + +const val REGISTER = "org.unifiedpush.android.distributor.REGISTER" +const val UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER" +const val MESSAGE_ACK = "org.unifiedpush.android.distributor.MESSAGE_ACK" \ No newline at end of file diff --git a/app/src/main/java/com/github/gotify/service/GotifyPushNotification.kt b/app/src/main/java/com/github/gotify/service/GotifyPushNotification.kt deleted file mode 100644 index 8ec733d..0000000 --- a/app/src/main/java/com/github/gotify/service/GotifyPushNotification.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.github.gotify.service - -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.* -import androidx.core.os.bundleOf -import com.github.gotify.log.Log -import com.google.gson.Gson - -/** - * THIS FUNC IS USED TO PUSH NOTIFICATIONS TO OTHER APPS - * It is called from the thread in WebSocketService - */ - -/** - * Function to notify client - */ -fun notifyClient(context: Context, clientPackage: String, message: com.github.gotify.client.model.Message){ - val db = MessagingDatabase(context) - val service = db.getServiceName(clientPackage) - if (service.isBlank()) { - Log.w("No service found for $clientPackage") - return - } - val gHandlerThread = HandlerThread(clientPackage) - gHandlerThread.start() - - val gConnection: ServiceConnection = object : ServiceConnection { - override fun onServiceConnected(className: ComponentName, - service: IBinder) { - val gService = Messenger(service) - Log.i("Remote service connected") - try { - val msg = Message.obtain( - null, - TYPE_MESSAGE, 0, 0 - ) - msg.data = bundleOf("json" to Gson().toJson(message) ) - gService.send(msg) - Log.i("Notification sent") - } catch(e: RemoteException) { - Log.e("Something went wrong", e) - } finally { - context.unbindService(this) - gHandlerThread.quit() - } - } - - override fun onServiceDisconnected(className: ComponentName) { - gHandlerThread.quit() - } - } - - val intent = Intent() - intent.component = ComponentName(clientPackage, service) - context.bindService(intent, gConnection, Context.BIND_AUTO_CREATE) -} \ No newline at end of file diff --git a/app/src/main/java/com/github/gotify/service/GotifyRegisterService.kt b/app/src/main/java/com/github/gotify/service/GotifyRegisterService.kt deleted file mode 100644 index e306295..0000000 --- a/app/src/main/java/com/github/gotify/service/GotifyRegisterService.kt +++ /dev/null @@ -1,175 +0,0 @@ -package com.github.gotify.service - -import android.app.Service -import android.content.Intent -import android.os.* -import androidx.annotation.RequiresApi -import androidx.core.os.bundleOf -import com.github.gotify.Settings -import com.github.gotify.api.Api -import com.github.gotify.api.ApiException -import com.github.gotify.api.ClientFactory -import com.github.gotify.client.api.ApplicationApi -import com.github.gotify.log.Log -import java.text.SimpleDateFormat -import java.util.Date -import kotlin.concurrent.thread - -/** - * THIS SERVICE IS USED BY OTHER APPS TO REGISTER - */ - -@RequiresApi(Build.VERSION_CODES.LOLLIPOP) -class GotifyRegisterService : Service() { - /** Keeps track of all current registered clients. */ - private val db = MessagingDatabase(this) - private lateinit var settings: Settings - - /** - * Handler of incoming messages from clients. - */ - internal inner class gHandler : Handler() { - - override fun handleMessage(msg: Message) { - when (msg.what) { - TYPE_CLIENT_STARTED -> simpleAnswer(msg, TYPE_CLIENT_STARTED) - TYPE_REGISTER_CLIENT -> { - val uid = msg.sendingUid - val msgData = msg.data - thread(start = true) { - registerApp(msgData, uid) - Log.i("Registration is finished") - }.join() - sendInfo(msg) - } - TYPE_UNREGISTER_CLIENT -> { - val uid = msg.sendingUid - val msgData = msg.data - thread(start = true) { - unregisterApp(msgData, uid) - Log.i("Unregistration is finished") - } - simpleAnswer(msg, TYPE_UNREGISTERED_CLIENT) - } - else -> super.handleMessage(msg) - } - } - - private fun simpleAnswer(msg: Message, what: Int) { - try { - msg.replyTo?.send(Message.obtain(null, what, 0, 0)) - } catch (e: RemoteException) { - } - } - - private fun unregisterApp(msg: Bundle, clientUid: Int) { - val clientPackageName = msg.getString("package").toString() - // we only trust unregistered demands from the uid who registered the app - if (db.strictIsRegistered(clientPackageName, clientUid)) { - Log.i("Unregistering $clientPackageName uid: $clientUid") - deleteApp(clientPackageName) - db.unregisterApp(clientPackageName, clientUid) - } - } - - private fun registerApp(msg: Bundle,clientUid: Int) { - val clientPackageName = msg.getString("package").toString() - if (clientPackageName.isBlank()) { - Log.w("Trying to register an app without packageName") - return - } - Log.i("registering $clientPackageName uid: $clientUid") - // The app is registered with the same uid : we re-register it - // the client may need to create a new app in the server - if (db.strictIsRegistered(clientPackageName, clientUid)) { - Log.i("$clientPackageName already registered : unregistering to register again") - unregisterApp(msg,clientUid) - } - // The app is registered with a new uid. - // User should unregister this app manually - // to avoid an app to impersonate another one - if (db.isRegistered(clientPackageName)) { - Log.w("$clientPackageName already registered with a different uid") - return - } - - val clientService = msg.getString("service").toString() - if (clientService.isBlank()) { - Log.w("Cannot find the service for $clientPackageName") - return - } - val app = createApp(clientPackageName) - if(app == null){ - Log.w("Cannot create a new app to register") - return - } - Log.i("registering : $clientPackageName $clientUid $clientService ${app.id} ${app.token}") - db.registerApp(clientPackageName, clientUid, clientService, app.id, app.token) - } - - private fun sendInfo(msg: Message){ - val clientPackageName = msg.data?.getString("package").toString() - Log.i("$clientPackageName is asking for its token and url") - // we only trust unregistered demands from the uid who registered the app - if (db.strictIsRegistered(clientPackageName, msg.sendingUid)) { - // db.getToken also remove the token in the db - val token = db.getToken(clientPackageName) - try { - val answer = Message.obtain(null, TYPE_REGISTERED_CLIENT, 0, 0) - answer.data = bundleOf("url" to settings.url(), - "token" to token) - msg.replyTo?.send(answer) - } catch (e: RemoteException) { - } - }else{ - Log.w("Client isn't registered or has a different uid") - } - } - - } - - /** - * Target we publish for clients to send messages to gHandler. - */ - private val gMessenger = Messenger(gHandler()) - - /** - * When binding to the service, we return an interface to our messenger - * for sending messages to the service. - */ - override fun onBind(intent: Intent): IBinder? { - settings = Settings(this) - return gMessenger.binder - } - - override fun onDestroy() { - db.close() - super.onDestroy() - } - - private fun createApp(appName: String): com.github.gotify.client.model.Application? { - val client = ClientFactory.clientToken(settings.url(), settings.sslSettings(), settings.token()) - val app = com.github.gotify.client.model.Application() - app.name = appName - val date = SimpleDateFormat("dd-MM-yyyy HH:mm:ss").format(Date()) - app.description = "(auto) $date" - try { - Log.i("Creating app") - return Api.execute(client.createService(ApplicationApi::class.java).createApp(app)); - } catch (e: ApiException) { - Log.e("Could not create app.", e); - } - return null - } - - private fun deleteApp(appName: String){ - val client = ClientFactory.clientToken(settings.url(), settings.sslSettings(), settings.token()) - try { - val appId = db.getAppId(appName) - Log.i("Deleting app with appId=$appId") - Api.execute(client.createService(ApplicationApi::class.java).deleteApp(appId)); - } catch (e: ApiException) { - Log.e("Could not delete app.", e); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/gotify/service/MessagingDatabase.kt b/app/src/main/java/com/github/gotify/service/MessagingDatabase.kt index cb16385..27d4070 100644 --- a/app/src/main/java/com/github/gotify/service/MessagingDatabase.kt +++ b/app/src/main/java/com/github/gotify/service/MessagingDatabase.kt @@ -9,19 +9,17 @@ private const val DB_NAME = "gotify_service" private const val DB_VERSION = 1 class MessagingDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION){ - private val CREATE_TABLE_APPS = "CREATE TABLE apps (" + - "package_name TEXT," + - "uid INT," + - "service_name TEXT," + - "app_id INT," + - "token TEXT," + - "PRIMARY KEY (package_name));" private val TABLE_APPS = "apps" private val FIELD_PACKAGE_NAME = "package_name" - private val FIELD_UID = "uid" - private val FIELD_SERVICE_NAME = "service_name" private val FIELD_APP_ID = "app_id" - private val FIELD_TOKEN = "token" + private val FIELD_CONNECTOR_TOKEN = "connector_token" + private val FIELD_GOTIFY_TOKEN = "gotify_token" + private val CREATE_TABLE_APPS = "CREATE TABLE $TABLE_APPS (" + + "$FIELD_PACKAGE_NAME TEXT," + + "$FIELD_APP_ID INT," + + "$FIELD_GOTIFY_TOKEN TEXT," + + "$FIELD_CONNECTOR_TOKEN TEXT," + + "PRIMARY KEY ($FIELD_PACKAGE_NAME));" override fun onCreate(db: SQLiteDatabase){ db.execSQL(CREATE_TABLE_APPS) @@ -31,22 +29,21 @@ class MessagingDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, n throw IllegalStateException("Upgrades not supported") } - fun registerApp(packageName: String, uid: Int, serviceName: String, appId :Int, token: String){ + fun registerApp(packageName: String, appId :Int, gotify_token: String, connector_token: String){ val db = writableDatabase val values = ContentValues().apply { put(FIELD_PACKAGE_NAME, packageName) - put(FIELD_UID, uid.toString()) - put(FIELD_SERVICE_NAME,serviceName) put(FIELD_APP_ID,appId.toString()) - put(FIELD_TOKEN,token) + put(FIELD_GOTIFY_TOKEN,gotify_token) + put(FIELD_CONNECTOR_TOKEN,connector_token) } db.insert(TABLE_APPS,null,values) } - fun unregisterApp(packageName: String, uid: Int){ + fun unregisterApp(packageName: String, connector_token: String){ val db = writableDatabase - val selection = "$FIELD_PACKAGE_NAME = ? AND $FIELD_UID = ?" - val selectionArgs = arrayOf(packageName,uid.toString()) + val selection = "$FIELD_PACKAGE_NAME = ? AND $FIELD_CONNECTOR_TOKEN = ?" + val selectionArgs = arrayOf(packageName,connector_token) db.delete(TABLE_APPS,selection,selectionArgs) } @@ -74,10 +71,10 @@ class MessagingDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, n } } - fun strictIsRegistered(packageName: String, uid: Int): Boolean { + fun strictIsRegistered(packageName: String, connector_token: String): Boolean { val db = readableDatabase - val selection = "$FIELD_PACKAGE_NAME = ? AND $FIELD_UID = ?" - val selectionArgs = arrayOf(packageName,uid.toString()) + val selection = "$FIELD_PACKAGE_NAME = ? AND $FIELD_CONNECTOR_TOKEN = ?" + val selectionArgs = arrayOf(packageName,connector_token) return db.query( TABLE_APPS, null, @@ -91,24 +88,6 @@ class MessagingDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, n } } - fun getServiceName(packageName: String): String{ - val db = readableDatabase - val projection = arrayOf(FIELD_SERVICE_NAME) - val selection = "$FIELD_PACKAGE_NAME = ?" - val selectionArgs = arrayOf(packageName) - return db.query( - TABLE_APPS, - projection, - selection, - selectionArgs, - null, - null, - null - ).use { cursor -> - if (cursor.moveToFirst()) cursor.getString(cursor.getColumnIndex(FIELD_SERVICE_NAME)) else "" - } - } - fun getAppFromId(appId: Int): String{ val db = readableDatabase val projection = arrayOf(FIELD_PACKAGE_NAME) @@ -145,9 +124,9 @@ class MessagingDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, n } } - fun getToken(packageName: String): String{ + fun getGotifyToken(packageName: String, remove: Boolean): String{ val db = readableDatabase - val projection = arrayOf(FIELD_TOKEN) + val projection = arrayOf(FIELD_GOTIFY_TOKEN) val selection = "$FIELD_PACKAGE_NAME = ?" val selectionArgs = arrayOf(packageName) val token = db.query( @@ -159,19 +138,57 @@ class MessagingDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, n null, null ).use { cursor -> - if (cursor.moveToFirst()) cursor.getString(cursor.getColumnIndex(FIELD_TOKEN)) else "" + if (cursor.moveToFirst()) cursor.getString(cursor.getColumnIndex(FIELD_GOTIFY_TOKEN)) else "" } - removeToken(token) + if (remove) + removeGotifyToken(token) return token } - private fun removeToken(packageName: String){ + private fun removeGotifyToken(packageName: String){ val db = writableDatabase val values = ContentValues().apply { - put(FIELD_TOKEN,"null") + put(FIELD_GOTIFY_TOKEN,"null") } val selection = "$FIELD_PACKAGE_NAME = ?" val selectionArgs = arrayOf(packageName) db.update(TABLE_APPS,values,selection,selectionArgs) } + + fun getConnectorToken(packageName: String): String{ + val db = readableDatabase + val projection = arrayOf(FIELD_CONNECTOR_TOKEN) + val selection = "$FIELD_PACKAGE_NAME = ?" + val selectionArgs = arrayOf(packageName) + val token = db.query( + TABLE_APPS, + projection, + selection, + selectionArgs, + null, + null, + null + ).use { cursor -> + if (cursor.moveToFirst()) cursor.getString(cursor.getColumnIndex(FIELD_CONNECTOR_TOKEN)) else "" + } + return token + } + + fun listApps(): List{ + val db = readableDatabase + val projection = arrayOf(FIELD_PACKAGE_NAME) + return db.query( + TABLE_APPS, + projection, + null, + null, + null, + null, + null + ).use{ cursor -> + generateSequence { if (cursor.moveToNext()) cursor else null } + .map{ it.getString(it.getColumnIndex(FIELD_PACKAGE_NAME)) } + .toList() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/gotify/service/PushNotification.kt b/app/src/main/java/com/github/gotify/service/PushNotification.kt new file mode 100644 index 0000000..8bf11bc --- /dev/null +++ b/app/src/main/java/com/github/gotify/service/PushNotification.kt @@ -0,0 +1,53 @@ +package com.github.gotify.service + + +import android.content.Context +import android.content.Intent +import android.util.Log + +/** + * These functions are used to send messages to other apps + */ + +fun sendMessage(context: Context, application: String, message: String){ + val token = getToken(context,application)!! + val broadcastIntent = Intent() + broadcastIntent.`package` = application + broadcastIntent.action = MESSAGE + broadcastIntent.putExtra("token", token) + broadcastIntent.putExtra("message", message) + context.sendBroadcast(broadcastIntent) +} + +fun sendEndpoint(context: Context, application: String, endpoint: String) { + val token = getToken(context,application)!! + val broadcastIntent = Intent() + broadcastIntent.`package` = application + broadcastIntent.action = NEW_ENDPOINT + broadcastIntent.putExtra("token", token) + broadcastIntent.putExtra("endpoint", endpoint) + context.sendBroadcast(broadcastIntent) +} + +fun sendUnregistered(context: Context, application: String, needToken: Boolean){ + val token = getToken(context,application).let{ + if(it.isNullOrEmpty() and needToken) return else it + } + + val broadcastIntent = Intent() + broadcastIntent.`package` = application + broadcastIntent.action = UNREGISTERED + broadcastIntent.putExtra("token", token) + context.sendBroadcast(broadcastIntent) +} + +fun getToken(context: Context, application: String): String?{ + val db = MessagingDatabase(context) + val token = db.getConnectorToken(application) + db.close() + if (token.isBlank()) { + Log.w("notifyClient", "No token found for $application") + return null + } + return token +} \ No newline at end of file diff --git a/app/src/main/java/com/github/gotify/service/RegisterBroadcastReceiver.kt b/app/src/main/java/com/github/gotify/service/RegisterBroadcastReceiver.kt new file mode 100644 index 0000000..c637b8b --- /dev/null +++ b/app/src/main/java/com/github/gotify/service/RegisterBroadcastReceiver.kt @@ -0,0 +1,118 @@ +package com.github.gotify.service + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import com.github.gotify.Settings +import com.github.gotify.api.Api +import com.github.gotify.api.ApiException +import com.github.gotify.api.ClientFactory +import com.github.gotify.client.api.ApplicationApi +import java.text.SimpleDateFormat +import java.util.Date +import kotlin.concurrent.thread + +/** + * THIS SERVICE IS USED BY OTHER APPS TO REGISTER + */ + +class RegisterBroadcastReceiver: BroadcastReceiver() { + + private lateinit var settings: Settings + + private fun unregisterApp(db: MessagingDatabase, application: String, token: String) { + // we only trust unregistered demands from the uid who registered the app + if (db.strictIsRegistered(application, token)) { + Log.i("RegisterService","Unregistering $application token: $token") + deleteApp(db, application) + db.unregisterApp(application, token) + } + } + + private fun registerApp(db: MessagingDatabase, application: String, connector_token: String) { + if (application.isBlank()) { + Log.w("RegisterService","Trying to register an app without packageName") + return + } + Log.i("RegisterService","registering $application token: $connector_token") + // The app is registered with the same token : we re-register it + // the client may need its endpoint again + if (db.strictIsRegistered(application, connector_token)) { + Log.i("RegisterService","$application already registered : unregistering to register again") + unregisterApp(db,application,connector_token) + } + // The app is registered with a new token. + // User should unregister this app manually + // to avoid an app to impersonate another one + if (db.isRegistered(application)) { + Log.w("RegisterService","$application already registered with a different token") + return + } + val app = createApp(application) + if(app == null){ + Log.w("RegisterService","Cannot create a new app to register") + return + } + db.registerApp(application, app.id, app.token, connector_token) + } + + private fun createApp(appName: String): com.github.gotify.client.model.Application? { + val client = ClientFactory.clientToken(settings.url(), settings.sslSettings(), settings.token()) + val app = com.github.gotify.client.model.Application() + app.name = appName + val date = SimpleDateFormat("dd-MM-yyyy HH:mm:ss").format(Date()) + app.description = "(auto) $date" + try { + Log.i("RegisterService","Creating app") + return Api.execute(client.createService(ApplicationApi::class.java).createApp(app)) + } catch (e: ApiException) { + Log.e("RegisterService","Could not create app.", e) + } + return null + } + + private fun deleteApp(db: MessagingDatabase, appName: String){ + val client = ClientFactory.clientToken(settings.url(), settings.sslSettings(), settings.token()) + try { + val appId = db.getAppId(appName) + Log.i("RegisterService","Deleting app with appId=$appId") + Api.execute(client.createService(ApplicationApi::class.java).deleteApp(appId)) + } catch (e: ApiException) { + Log.e("RegisterService","Could not delete app.", e) + } + } + + override fun onReceive(context: Context?, intent: Intent?) { + settings = Settings(context) + when (intent!!.action) { + REGISTER ->{ + val db = MessagingDatabase(context!!) + Log.i("Register","REGISTER") + val internalToken = intent.getStringExtra("token")?: "" + val application = intent.getStringExtra("application")?: "" + thread(start = true) { + registerApp(db, application, internalToken) + Log.i("RegisterService","Registration is finished") + }.join() + val token = db.getGotifyToken(application, false) + val endpoint = settings.url() + + "/message?token=$token" + sendEndpoint(context,application,endpoint) + db.close() + } + UNREGISTER ->{ + Log.i("Register","UNREGISTER") + val token = intent.getStringExtra("token")?: "" + val application = intent.getStringExtra("application")?: "" + thread(start = true) { + val db = MessagingDatabase(context!!) + unregisterApp(db,application, token) + db.close() + Log.i("RegisterService","Unregistration is finished") + } + sendUnregistered(context!!,application,false) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/gotify/service/WebSocketService.java b/app/src/main/java/com/github/gotify/service/WebSocketService.java index 6f38bb4..b395793 100644 --- a/app/src/main/java/com/github/gotify/service/WebSocketService.java +++ b/app/src/main/java/com/github/gotify/service/WebSocketService.java @@ -177,7 +177,7 @@ public class WebSocketService extends Service { db.close(); if (!registeredAppName.isEmpty()) { Log.i("Forward message to " + registeredAppName); - GotifyPushNotificationKt.notifyClient(this, registeredAppName, message); + PushNotificationKt.sendMessage(this, registeredAppName, message.getMessage()); return; } }