mirror of
https://github.com/gotify/android.git
synced 2026-06-07 03:29:32 +08:00
Service to permit other app to register and forward the notifications to theses apps
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@
|
||||
android:label="@string/title_activity_settings" />
|
||||
|
||||
<service android:name=".service.WebSocketService" />
|
||||
<service android:name=".service.GotifyPushNotification" />
|
||||
<service android:name=".service.GotifyMessengerService" android:exported="true" />
|
||||
|
||||
<receiver android:name=".init.BootCompletedReceiver">
|
||||
<intent-filter>
|
||||
|
||||
20
app/src/main/java/com/github/gotify/service/Constants.kt
Normal file
20
app/src/main/java/com/github/gotify/service/Constants.kt
Normal file
@@ -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
|
||||
@@ -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<com.github.gotify.client.model.Application>? = 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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
154
app/src/main/java/com/github/gotify/service/MessagingDatabase.kt
Normal file
154
app/src/main/java/com/github/gotify/service/MessagingDatabase.kt
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user