diff --git a/app/build.gradle b/app/build.gradle
index e00b562..de7d26a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -2,6 +2,8 @@ plugins {
id "com.diffplug.gradle.spotless" version "3.26.1"
}
apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-android'
android {
compileSdkVersion 29
@@ -54,6 +56,8 @@ dependencies {
implementation 'io.noties.markwon:image-picasso:4.3.1'
implementation 'io.noties.markwon:image:4.3.1'
implementation 'io.noties.markwon:ext-tables:4.3.1'
+ implementation "androidx.core:core-ktx:+"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
configurations {
@@ -70,3 +74,6 @@ spotless {
importOrder('', 'static *')
}
}
+repositories {
+ mavenCentral()
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 314b2b1..0100d91 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -47,6 +47,8 @@
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
new file mode 100644
index 0000000..9e4a983
--- /dev/null
+++ b/app/src/main/java/com/github/gotify/service/Constants.kt
@@ -0,0 +1,20 @@
+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.
+ */
+const val MSG_REGISTER_CLIENT = 1
+const val MSG_UNREGISTER_CLIENT = 2
+const val MSG_START = 3
+const val MSG_GET_INFO = 4
+const val MSG_NEW_URL = 5
+const val MSG_NOTIFICATION = 6
+const val MSG_IS_REGISTERED = 7
+/**
+ * ERRORS
+ */
+const val ERROR_NOT_CONNECTED = 1
+const val ERROR_NULL_OR_BLANK = 2
+const val ERROR_ALREADY_REGISTERED = 3
\ No newline at end of file
diff --git a/app/src/main/java/com/github/gotify/service/GotifyMessengerService.kt b/app/src/main/java/com/github/gotify/service/GotifyMessengerService.kt
new file mode 100644
index 0000000..90cb7a9
--- /dev/null
+++ b/app/src/main/java/com/github/gotify/service/GotifyMessengerService.kt
@@ -0,0 +1,189 @@
+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.ClientFactory
+import com.github.gotify.client.api.ApplicationApi
+import com.github.gotify.log.Log
+import java.io.IOException
+
+/**
+ * THIS SERVICE IS USED BY OTHER APPS TO REGISTER
+ */
+// TODO : in the app, implement forceUnregisterApp
+
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+class GotifyMessengerService : Service() {
+ /** Keeps track of all current registered clients. */
+ private val db = MessagingDatabase(this)
+ private lateinit var settings: Settings //= Settings(this)
+
+ /**
+ * Handler of incoming messages from clients.
+ */
+ internal inner class gHandler : Handler() {
+
+ override fun handleMessage(msg: Message) {
+ when (msg.what) {
+ MSG_START -> simpleAnswer(msg, MSG_START,0)
+ MSG_REGISTER_CLIENT -> {
+ val uid = msg.sendingUid
+ val msgData = msg.data
+ val thread = object : Thread() {
+ override fun run() {
+ registerApp(msgData, uid)
+ }
+ }
+ thread.start()
+ thread.join()
+ simpleAnswer(msg, MSG_REGISTER_CLIENT,0)
+ }
+ MSG_UNREGISTER_CLIENT -> {
+ unregisterApp(msg.data,msg.sendingUid,true)
+ simpleAnswer(msg, MSG_UNREGISTER_CLIENT,0)
+ }
+ MSG_GET_INFO -> sendInfo(msg)
+ MSG_IS_REGISTERED -> {
+ var rep =0
+ if(db.strictIsRegistered(msg.data.getString("package").toString(),msg.sendingUid)){
+ rep = 1
+ }
+ simpleAnswer(msg, MSG_IS_REGISTERED,rep)
+ }
+ else -> super.handleMessage(msg)
+ }
+ }
+
+ private fun simpleAnswer(msg: Message, what: Int, arg1: Int) {
+ try {
+ msg.replyTo?.send(Message.obtain(null, what, arg1, 0))
+ } catch (e: RemoteException) {
+ }
+ }
+
+ private fun unregisterApp(msg: Bundle, clientUid: Int, deleteApp: Boolean) {
+ 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")
+ db.unregisterApp(clientPackageName, clientUid)
+ if(deleteApp){
+ // TODO : delete app on the server,
+ // it can be done manually for the moment
+ }
+ }
+ }
+
+ 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,false)
+ }
+ // 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
+ }
+ var app = getApp(clientPackageName)
+ if (app == null){
+ Log.i("$clientPackageName isn't registered to the server, creating a new one")
+ app = createApp(clientPackageName)
+ }
+ if(app == null){
+ Log.w("Cannot find the AppId neither 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)) {
+ val token = db.getToken(clientPackageName)
+ try {
+ val answer = Message.obtain(null, MSG_GET_INFO, 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 getApp(appName: String): com.github.gotify.client.model.Application? {
+ val client = ClientFactory.clientToken(settings.url(), settings.sslSettings(), settings.token())
+ var applications: List? = null
+ try{
+ applications = client.createService(ApplicationApi::class.java).apps.execute().body()
+ }catch(e: IOException){
+ e.printStackTrace()
+ }
+ if(applications.isNullOrEmpty()) {
+ Log.i("There isn't any app registered to the server")
+ return null
+ }
+ for (app in applications) {
+ if (app.name == appName) return app
+ }
+ return null
+ }
+
+ 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
+ app.description = "automatically created"
+ try{
+ return client.createService(ApplicationApi::class.java).createApp(app).execute().body()
+ }catch(e: IOException){
+ e.printStackTrace()
+ }
+ return null
+ }
+}
\ 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
new file mode 100644
index 0000000..5f7e25c
--- /dev/null
+++ b/app/src/main/java/com/github/gotify/service/GotifyPushNotification.kt
@@ -0,0 +1,141 @@
+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
+
+/**
+ * THIS CLASS IS USED TO PUSH NOTIFICATIONS TO OTHER APPS
+ * It is called from the thread in WebSocketService
+ */
+
+
+//TODO : delete the notification once delivered
+//TODO: Continue to implement the URl change
+// For the moment, re-registering the client is enough
+
+/**
+ * Function to notify client
+ */
+fun notifyClient(context: Context,app: String, msg: com.github.gotify.client.model.Message){
+ val gotif = GotifyPushNotification(context,app,msg)
+ gotif.start()
+}
+
+open class GotifyPushNotification(private val context: Context,
+ private val clientPackage: String,
+ private val message: com.github.gotify.client.model.Message){
+ /** Messenger for communicating with service. */
+ private var gService: Messenger? = null
+ /** To known if it if bound to the service */
+ private var gIsBound = false
+ /** Handler of incoming messages from service. */
+ private val gHandlerThread = HandlerThread(clientPackage)
+ private var gHandler: Handler? = null
+ /** Target we publish for client apps to send messages to gHandler. */
+ private var gMessenger: Messenger? = null
+
+ fun start(){
+ gHandlerThread.start()
+ gHandler = Handler(gHandlerThread.looper)
+ gMessenger = Messenger(gHandler)
+ doBindService()
+ }
+
+ /**
+ * Class for interacting with the main interface of the service.
+ */
+ private val gConnection: ServiceConnection = object : ServiceConnection {
+ override fun onServiceConnected(className: ComponentName,
+ service: IBinder) {
+ gService = Messenger(service)
+ gIsBound = true
+ Log.i("Remote service connected")
+ /** We're connected, now we send the message !**/
+ doNotifyClient()
+ }
+
+ override fun onServiceDisconnected(className: ComponentName) {
+ // This is called when the connection with the service has been
+ // unexpectedly disconnected -- that is, its process crashed.
+ gService = null
+ doUnbindService()
+ Log.i("Remote service disconnected")
+ }
+ }
+
+ private fun doBindService() {
+ val intent = Intent()
+ val db = MessagingDatabase(context)
+ val service = db.getServiceName(clientPackage)
+ if (service.isBlank()){
+ Log.w("No service found for $clientPackage")
+ doUnbindService()
+ return
+ }
+ intent.component = ComponentName(clientPackage , service)
+ context.bindService( intent, gConnection, Context.BIND_AUTO_CREATE )
+ // We don't do if(true){gIsBound = true} now because
+ // we consider it is bound only when the service is connected
+ }
+
+ private fun doUnbindService() {
+ Log.i("Unbinding")
+ if (gIsBound) {
+ // Detach our existing connection.
+ context.unbindService(gConnection)
+ gIsBound = false
+ }
+ gHandlerThread.quit()
+ gHandler = null
+ gMessenger = null
+ }
+
+ private fun doChangeURL(url: String){
+ if(!gIsBound){
+ Log.w("You need to bind fisrt")
+ doUnbindService()
+ return
+ }
+ try {
+ val msg = Message.obtain(null,
+ MSG_NEW_URL, 0, 0)
+ msg.replyTo = gMessenger
+ msg.data = bundleOf("url" to url)
+ gService!!.send(msg)
+ } catch (e: RemoteException) {
+ // There is nothing special we need to do if the service
+ // has crashed.
+ }
+ //done : unbinding
+ doUnbindService()
+ }
+ private fun doNotifyClient() {
+ if (!gIsBound) {
+ Log.w("You need to bind first")
+ doUnbindService()
+ return
+ }
+ try {
+ val msg = Message.obtain(
+ null,
+ MSG_NOTIFICATION, 0, 0
+ )
+ msg.replyTo = gMessenger
+ msg.data = bundleOf("message" to message.message,
+ "title" to message.title,
+ "priority" to message.priority.toInt())
+ gService!!.send(msg)
+ Log.i("Notification sent")
+ } catch (e: RemoteException) {
+ // There is nothing special we need to do if the service
+ // has crashed.
+ }
+ /** Once the notification is delivered, we can unbind **/
+ doUnbindService()
+ }
+}
\ 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
new file mode 100644
index 0000000..633d3e1
--- /dev/null
+++ b/app/src/main/java/com/github/gotify/service/MessagingDatabase.kt
@@ -0,0 +1,154 @@
+package com.github.gotify.service
+
+import android.content.ContentValues
+import android.content.Context
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteOpenHelper
+
+private val DB_NAME = "gotify_service"
+private 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"
+
+ override fun onCreate(db: SQLiteDatabase){
+ db.execSQL(CREATE_TABLE_APPS);
+ }
+
+ override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
+ throw IllegalStateException("Upgrades not supported")
+ }
+
+ fun registerApp(packageName: String, uid: Int, serviceName: String, appId :Int, 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)
+ }
+ db.insert(TABLE_APPS,null,values)
+ }
+
+ fun unregisterApp(packageName: String, uid: Int){
+ val db = writableDatabase
+ val selection = "$FIELD_PACKAGE_NAME = ? AND $FIELD_UID = ?"
+ val selectionArgs = arrayOf(packageName,uid.toString())
+ db.delete(TABLE_APPS,selection,selectionArgs)
+ }
+
+ fun forceUnregisterApp(packageName: String){
+ val db = writableDatabase
+ val selection = "$FIELD_PACKAGE_NAME = ?"
+ val selectionArgs = arrayOf(packageName)
+ db.delete(TABLE_APPS,selection,selectionArgs)
+ }
+
+ fun isRegistered(packageName: String): Boolean {
+ val db = readableDatabase
+ val selection = "$FIELD_PACKAGE_NAME = ?"
+ val selectionArgs = arrayOf(packageName)
+ val cursor = db.query(
+ TABLE_APPS,
+ null,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ )
+ return (cursor != null && cursor.count > 0)
+ }
+
+ fun strictIsRegistered(packageName: String, uid: Int): Boolean {
+ val db = readableDatabase
+ val selection = "$FIELD_PACKAGE_NAME = ? AND $FIELD_UID = ?"
+ val selectionArgs = arrayOf(packageName,uid.toString())
+ val cursor = db.query(
+ TABLE_APPS,
+ null,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ )
+ return (cursor != null && cursor.count > 0)
+ }
+
+ fun getServiceName(packageName: String): String{
+ val db = readableDatabase
+ val projection = arrayOf(FIELD_SERVICE_NAME)
+ val selection = "$FIELD_PACKAGE_NAME = ?"
+ val selectionArgs = arrayOf(packageName)
+ val cursor = db.query(
+ TABLE_APPS,
+ projection,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ )
+ var res = ""
+ if(cursor.moveToFirst()){
+ res = cursor.getString(cursor.getColumnIndex(FIELD_SERVICE_NAME))
+ }
+ return res
+ }
+
+ fun getAppFromId(appId: Int): String{
+ val db = readableDatabase
+ val projection = arrayOf(FIELD_PACKAGE_NAME)
+ val selection = "$FIELD_APP_ID = ?"
+ val selectionArgs = arrayOf(appId.toString())
+ val cursor = db.query(
+ TABLE_APPS,
+ projection,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ )
+ var res = ""
+ if(cursor.moveToFirst()){
+ res = cursor.getString(cursor.getColumnIndex(FIELD_PACKAGE_NAME))
+ }
+ return res
+ }
+
+ fun getToken(packageName: String): String{
+ val db = readableDatabase
+ val projection = arrayOf(FIELD_TOKEN)
+ val selection = "$FIELD_PACKAGE_NAME = ?"
+ val selectionArgs = arrayOf(packageName)
+ val cursor = db.query(
+ TABLE_APPS,
+ projection,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ )
+ var res = ""
+ if(cursor.moveToFirst()){
+ res = cursor.getString(cursor.getColumnIndex(FIELD_TOKEN))
+ }
+ return res
+ }
+}
\ 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 480fc01..5dda07d 100644
--- a/app/src/main/java/com/github/gotify/service/WebSocketService.java
+++ b/app/src/main/java/com/github/gotify/service/WebSocketService.java
@@ -172,12 +172,20 @@ public class WebSocketService extends Service {
lastReceivedMessage.set(message.getId());
}
broadcast(message);
- showNotification(
- message.getId(),
- message.getTitle(),
- message.getMessage(),
- message.getPriority(),
- message.getExtras());
+ MessagingDatabase db = new MessagingDatabase(this);
+ String registeredAppName = db.getAppFromId(message.getAppid());
+ db.close();
+ if(!registeredAppName.isEmpty() && registeredAppName != "") {
+ Log.i("Notification to " + registeredAppName);
+ GotifyPushNotificationKt.notifyClient(this, registeredAppName, message);
+ }else {
+ showNotification(
+ message.getId(),
+ message.getTitle(),
+ message.getMessage(),
+ message.getPriority(),
+ message.getExtras());
+ }
}
private void broadcast(Message message) {
@@ -300,4 +308,4 @@ public class WebSocketService extends Service {
(NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(-5, b.build());
}
-}
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index b0cb412..a508aa4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,14 +1,16 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
-
+ ext.kotlin_version = '1.3.72'
+
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
-
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files