From a99a00de309f2ca9d1ca4f4ad9f08a9a0bdf0766 Mon Sep 17 00:00:00 2001 From: erinn Date: Fri, 28 May 2021 15:56:45 -0700 Subject: [PATCH 1/7] WIP: partial android service migration --- android/app/build.gradle | 29 +++++ android/app/src/main/AndroidManifest.xml | 2 + .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 122 ++++++++++++++++++ .../kotlin/im/cwtch/flwtch/MainActivity.kt | 37 +++--- pubspec.lock | 4 +- 5 files changed, 177 insertions(+), 17 deletions(-) create mode 100644 android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt diff --git a/android/app/build.gradle b/android/app/build.gradle index 4b49db2..2786507 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -69,6 +69,15 @@ android { signingConfig signingConfigs.release } } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } } flutter { @@ -82,4 +91,24 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2" implementation "com.airbnb.android:lottie:3.5.0" implementation "com.android.support.constraint:constraint-layout:2.0.4" + + // WorkManager + + // (Java only) + //implementation("androidx.work:work-runtime:$work_version") + + // Kotlin + coroutines + implementation("androidx.work:work-runtime-ktx:2.5.0") + + // optional - RxJava2 support + //implementation("androidx.work:work-rxjava2:$work_version") + + // optional - GCMNetworkManager support + //implementation("androidx.work:work-gcm:$work_version") + + // optional - Test helpers + //androidTestImplementation("androidx.work:work-testing:$work_version") + + // optional - Multiprocess support + implementation "androidx.work:work-multiprocess:2.5.0" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 43ed283..1162d40 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -42,4 +42,6 @@ + + diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt new file mode 100644 index 0000000..b14af9f --- /dev/null +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -0,0 +1,122 @@ +package im.cwtch.flwtch + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.graphics.Color +import android.os.Build +import android.util.Log + +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.ForegroundInfo +import androidx.work.WorkerParameters +import androidx.work.WorkManager + +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +import org.json.JSONObject + +import cwtch.Cwtch + +class FlwtchWorker(context: Context, parameters: WorkerParameters) : + CoroutineWorker(context, parameters) { + private val CWTCH_EVENTBUS = "test.flutter.dev/eventBus" + + private val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as + NotificationManager + + override suspend fun doWork(): Result { + val appDir = inputData.getString(KEY_APP_DIR) + ?: return Result.failure() + val torPath = inputData.getString(KEY_TOR_PATH) + ?: return Result.failure() + // Mark the Worker as important + val progress = "Trying to do a Flwtch" + setForeground(createForegroundInfo(progress)) + download(appDir, torPath) + return Result.success() + } + + private suspend fun download(appDir: String, torPath: String) { + Cwtch.startCwtch(appDir, torPath) + // seperate coroutine to poll event bus and send to dart + //Log.i("FlwtchWorker.kt", "got event chan: " + eventbus_chan + " launching corouting...") + GlobalScope.launch(Dispatchers.IO) { + while(true) { + val evt = AppbusEvent(Cwtch.getAppBusEvent()) + Log.i("FlwtchWorker.kt", "got appbusEvent: " + evt) + launch(Dispatchers.Main) { + //todo: this elides evt.EventID which may be needed at some point? + val flutterEngine = FlutterEngine(applicationContext) + val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) + eventbus_chan.invokeMethod(evt.EventType, evt.Data) + } + } + } + } + + // Creates an instance of ForegroundInfo which can be used to update the + // ongoing notification. + private fun createForegroundInfo(progress: String): ForegroundInfo { + val id = "flwtch" + val title = "Flwtch" + val cancel = "Nevermind"//todo: translate + // This PendingIntent can be used to cancel the worker + val intent = WorkManager.getInstance(applicationContext) + .createCancelPendingIntent(getId()) + + val channelId = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel(id, id) + } else { + // If earlier version channel ID is not used + // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) + "" + } + + val notification = NotificationCompat.Builder(applicationContext, id) + .setContentTitle(title) + .setTicker(title) + .setContentText(progress) + .setSmallIcon(R.mipmap.knott) + .setOngoing(true) + // Add the cancel action to the notification which can + // be used to cancel the worker + .addAction(android.R.drawable.ic_delete, cancel, intent) + .build() + + return ForegroundInfo(101, notification) + } + + + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel(channelId: String, channelName: String): String{ + val chan = NotificationChannel(channelId, + channelName, NotificationManager.IMPORTANCE_NONE) + chan.lightColor = Color.BLUE + chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + notificationManager.createNotificationChannel(chan) + return channelId + } + + companion object { + const val KEY_APP_DIR = "KEY_APP_DIR" + const val KEY_TOR_PATH = "KEY_TOR_PATH" + } + + class AppbusEvent(json: String) : JSONObject(json) { + val EventType = this.optString("EventType") + val EventID = this.optString("EventID") + val Data = this.optString("Data") + } +} diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index c82a32e..dbac7d8 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -7,6 +7,11 @@ import android.os.Bundle import android.os.Looper import android.util.Log +import androidx.work.Data +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkRequest + import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -44,8 +49,6 @@ class MainActivity: FlutterActivity() { // Note: this methods are invoked on the main thread. MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_APP_INFO).setMethodCallHandler { call, result -> handleAppInfo(call, result) } MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_CWTCH).setMethodCallHandler { call, result -> handleCwtch(call, result) } - - } private fun handleAppInfo(@NonNull call: MethodCall, @NonNull result: Result) { @@ -70,21 +73,25 @@ class MainActivity: FlutterActivity() { val appDir = (call.argument("appDir") as? String) ?: ""; val torPath = (call.argument("torPath") as? String) ?: "tor"; Log.i("MainActivity.kt", " appDir: '" + appDir + "' torPath: '" + torPath + "'") - Cwtch.startCwtch(appDir, torPath) + + //Cwtch.startCwtch(appDir, torPath)// todo + val data: Data = Data.Builder().putString(FlwtchWorker.KEY_APP_DIR, appDir).putString(FlwtchWorker.KEY_TOR_PATH, torPath).build() + val uploadWorkRequest: WorkRequest = OneTimeWorkRequestBuilder().setInputData(data).build() + WorkManager.getInstance(this).enqueue(uploadWorkRequest) // seperate coroutine to poll event bus and send to dart - val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) - Log.i("MainActivity.kt", "got event chan: " + eventbus_chan + " launching corouting...") - GlobalScope.launch(Dispatchers.IO) { - while(true) { - val evt = AppbusEvent(Cwtch.getAppBusEvent()) - Log.i("MainActivity.kt", "got appbusEvent: " + evt) - launch(Dispatchers.Main) { - //todo: this elides evt.EventID which may be needed at some point? - eventbus_chan.invokeMethod(evt.EventType, evt.Data) - } - } - } +// val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) +// Log.i("MainActivity.kt", "got event chan: " + eventbus_chan + " launching corouting...") +// GlobalScope.launch(Dispatchers.IO) { +// while(true) { +// val evt = AppbusEvent(Cwtch.getAppBusEvent()) +// Log.i("MainActivity.kt", "got appbusEvent: " + evt) +// launch(Dispatchers.Main) { +// //todo: this elides evt.EventID which may be needed at some point? +// eventbus_chan.invokeMethod(evt.EventType, evt.Data) +// } +// } +// } } "SelectProfile" -> { val onion = (call.argument("profile") as? String) ?: ""; diff --git a/pubspec.lock b/pubspec.lock index 8c01490..011cd94 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -14,7 +14,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.5.0" boolean_selector: dependency: transitive description: @@ -371,7 +371,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "6.2.0" + version: "6.1.0+1" webdriver: dependency: transitive description: From 599993d2d2892549a155b1ae383702a103dc1298 Mon Sep 17 00:00:00 2001 From: erinn Date: Tue, 1 Jun 2021 14:01:47 -0700 Subject: [PATCH 2/7] WIP: partial android service migration --- .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 152 ++++++++++++++- .../kotlin/im/cwtch/flwtch/MainActivity.kt | 174 ++++-------------- android/build.gradle | 6 +- pubspec.lock | 2 +- 4 files changed, 184 insertions(+), 150 deletions(-) diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index b14af9f..c7e5ef3 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -36,18 +36,156 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : NotificationManager override suspend fun doWork(): Result { - val appDir = inputData.getString(KEY_APP_DIR) + val method = inputData.getString(KEY_METHOD) ?: return Result.failure() - val torPath = inputData.getString(KEY_TOR_PATH) + val args = inputData.getString(KEY_ARGS) ?: return Result.failure() + Log.i("FlwtchWorker.kt", "got call $method with args $args") // Mark the Worker as important val progress = "Trying to do a Flwtch" setForeground(createForegroundInfo(progress)) - download(appDir, torPath) + handleCwtch(method, args) return Result.success() } - private suspend fun download(appDir: String, torPath: String) { + private suspend fun handleCwtch(method: String, args: String) { + var a = JSONObject(args); + when (method) { + "Start" -> { + Log.i("FlwtchWorker.kt", "handleAppInfo Start") + val appDir = (a.get("appDir") as? String) ?: ""; + val torPath = (a.get("torPath") as? String) ?: "tor"; + Log.i("FlwtchWorker.kt", " appDir: '" + appDir + "' torPath: '" + torPath + "'") + + Cwtch.startCwtch(appDir, torPath) + + // infinite coroutine :) + while(true) { + val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent()) + Log.i("FlwtchWorker.kt", "got appbusEvent: " + evt) + if (isStopped) { + Log.i("FlwtchWorker.kt", "COROUTINEWORKER DOT ISSTOPPED TRUE OH MY") + } + //todo: this elides evt.EventID which may be needed at some point? + val data = Data.Builder().putString("EventType", evt.EventType).putString("Data", evt.Data).build() + setProgress(data) + Thread.sleep(200) + } + } + "SelectProfile" -> { + val onion = (a.get("profile") as? String) ?: ""; + Cwtch.selectProfile(onion) + } + "CreateProfile" -> { + val nick = (a.get("nick") as? String) ?: ""; + val pass = (a.get("pass") as? String) ?: ""; + Cwtch.createProfile(nick, pass) + } + "LoadProfiles" -> { + val pass = (a.get("pass") as? String) ?: ""; + Cwtch.loadProfiles(pass) + } + "GetProfiles" -> Result.success(Data.Builder().putString("result", Cwtch.getProfiles()).build()) + // "ACNEvents" -> result.success(Cwtch.acnEvents()) + "ContactEvents" -> Result.success(Data.Builder().putString("result", Cwtch.contactEvents()).build()) + "NumMessages" -> { + val profile = (a.get("profile") as? String) ?: ""; + val handle = (a.get("contact") as? String) ?: ""; + Result.success(Data.Builder().putLong("result", Cwtch.numMessages(profile, handle)).build()) + } + "GetMessage" -> { + //Log.i("MainActivivity.kt", (a.get("index"))); + +// var args : HashMap = a.gets(); +// Log.i("FlwtchWorker.kt", args); + + + val profile = (a.get("profile") as? String) ?: ""; + val handle = (a.get("contact") as? String) ?: ""; + val indexI = a.getInt("index") ?: 0; + Log.i("FlwtchWorker.kt", "indexI = " + indexI) + Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build()) + } + "GetMessages" -> { + val profile = (a.get("profile") as? String) ?: ""; + val handle = (a.get("contact") as? String) ?: ""; + val start = (a.get("start") as? Long) ?: 0; + val end = (a.get("end") as? Long) ?: 0; + Result.success(Data.Builder().putString("result", Cwtch.getMessages(profile, handle, start, end)).build()) + } + "AcceptContact" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val handle = (a.get("handle") as? String) ?: ""; + Cwtch.acceptContact(profile, handle); + } + "BlockContact" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val handle = (a.get("handle") as? String) ?: ""; + Cwtch.blockContact(profile, handle); + } + "DebugResetContact" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val handle = (a.get("handle") as? String) ?: ""; + Cwtch.debugResetContact(profile, handle); + } + "SendMessage" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val handle = (a.get("handle") as? String) ?: ""; + val message = (a.get("message") as? String) ?: ""; + Cwtch.sendMessage(profile, handle, message); + } + "SendInvitation" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val handle = (a.get("handle") as? String) ?: ""; + val target = (a.get("target") as? String) ?: ""; + Cwtch.sendInvitation(profile, handle, target); + } + "SendProfileEvent" -> { + val onion = (a.get("onion") as? String) ?: ""; + val jsonEvent = (a.get("jsonEvent") as? String) ?: ""; + Cwtch.sendProfileEvent(onion, jsonEvent); + } + "SendAppEvent" -> { + val jsonEvent = (a.get("jsonEvent") as? String) ?: ""; + Cwtch.sendAppEvent(jsonEvent); + } + "ResetTor" -> { + Cwtch.resetTor(); + } + "ImportBundle" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val bundle = (a.get("bundle") as? String) ?: ""; + Cwtch.importBundle(profile, bundle); + } + "SetGroupAttribute" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val groupHandle = (a.get("groupHandle") as? String) ?: ""; + val key = (a.get("key") as? String) ?: ""; + val value = (a.get("value") as? String) ?: ""; + Cwtch.setGroupAttribute(profile, groupHandle, key, value); + } + "CreateGroup" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val server = (a.get("server") as? String) ?: ""; + val groupName = (a.get("groupname") as? String) ?: ""; + Cwtch.createGroup(profile, server, groupName); + } + "LeaveGroup" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val groupHandle = (a.get("groupHandle") as? String) ?: ""; + Log.i("FlwtchWorker.kt", "LeaveGroup: need to recompile aar and uncomment this line")//todo + //Cwtch.leaveGroup(profile, groupHandle); + } + "RejectInvite" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val groupHandle = (a.get("groupHandle") as? String) ?: ""; + Cwtch.rejectInvite(profile, groupHandle); + } + else -> Result.failure() + } + } + + private suspend fun launchCwtch(appDir: String, torPath: String) { Cwtch.startCwtch(appDir, torPath) // seperate coroutine to poll event bus and send to dart //Log.i("FlwtchWorker.kt", "got event chan: " + eventbus_chan + " launching corouting...") @@ -103,15 +241,15 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : private fun createNotificationChannel(channelId: String, channelName: String): String{ val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_NONE) - chan.lightColor = Color.BLUE + chan.lightColor = Color.MAGENTA chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE notificationManager.createNotificationChannel(chan) return channelId } companion object { - const val KEY_APP_DIR = "KEY_APP_DIR" - const val KEY_TOR_PATH = "KEY_TOR_PATH" + const val KEY_METHOD = "KEY_METHOD" + const val KEY_ARGS = "KEY_ARGS" } class AppbusEvent(json: String) : JSONObject(json) { diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index e1aa843..e2dccaf 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -6,11 +6,8 @@ import android.content.pm.PackageManager import android.os.Bundle import android.os.Looper import android.util.Log - -import androidx.work.Data -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager -import androidx.work.WorkRequest +import androidx.lifecycle.Observer +import androidx.work.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -67,141 +64,40 @@ class MainActivity: FlutterActivity() { } private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) { - when (call.method) { - "Start" -> { - Log.i("MainActivity.kt", "handleAppInfo Start") - val appDir = (call.argument("appDir") as? String) ?: ""; - val torPath = (call.argument("torPath") as? String) ?: "tor"; - Log.i("MainActivity.kt", " appDir: '" + appDir + "' torPath: '" + torPath + "'") + val argmap: Map = call.arguments as Map + val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build() + var tag = "" + if (call.method == "Start") { + tag = "cwtchEventBus" + } + val workRequest: WorkRequest = OneTimeWorkRequestBuilder().setInputData(data).addTag(tag).build() + WorkManager.getInstance(this).enqueue(workRequest) + if (call.method == "Start") { + val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) + WorkManager.getInstance(applicationContext) + // requestId is the WorkRequest id + .getWorkInfosByTagLiveData("cwtchEventBus") + .observeForever(Observer> { listOfWorkInfo -> + if (listOfWorkInfo.isNullOrEmpty()) { + return@Observer + } - //Cwtch.startCwtch(appDir, torPath)// todo - val data: Data = Data.Builder().putString(FlwtchWorker.KEY_APP_DIR, appDir).putString(FlwtchWorker.KEY_TOR_PATH, torPath).build() - val uploadWorkRequest: WorkRequest = OneTimeWorkRequestBuilder().setInputData(data).build() - WorkManager.getInstance(this).enqueue(uploadWorkRequest) - - // seperate coroutine to poll event bus and send to dart -// val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) -// Log.i("MainActivity.kt", "got event chan: " + eventbus_chan + " launching corouting...") -// GlobalScope.launch(Dispatchers.IO) { -// while(true) { -// val evt = AppbusEvent(Cwtch.getAppBusEvent()) -// Log.i("MainActivity.kt", "got appbusEvent: " + evt) -// launch(Dispatchers.Main) { -// //todo: this elides evt.EventID which may be needed at some point? -// eventbus_chan.invokeMethod(evt.EventType, evt.Data) -// } -// } -// } - } - "SelectProfile" -> { - val onion = (call.argument("profile") as? String) ?: ""; - Cwtch.selectProfile(onion) - } - "CreateProfile" -> { - val nick = (call.argument("nick") as? String) ?: ""; - val pass = (call.argument("pass") as? String) ?: ""; - Cwtch.createProfile(nick, pass) - } - "LoadProfiles" -> { - val pass = (call.argument("pass") as? String) ?: ""; - Cwtch.loadProfiles(pass) - } - "GetProfiles" -> result.success(Cwtch.getProfiles()) - // "ACNEvents" -> result.success(Cwtch.acnEvents()) - "ContactEvents" -> result.success(Cwtch.contactEvents()) - "NumMessages" -> { - val profile = (call.argument("profile") as? String) ?: ""; - val handle = (call.argument("contact") as? String) ?: ""; - result.success(Cwtch.numMessages(profile, handle)) - } - "GetMessage" -> { - //Log.i("MainActivivity.kt", (call.argument("index"))); - -// var args : HashMap = call.arguments(); -// Log.i("MainActivity.kt", args); - - - val profile = (call.argument("profile") as? String) ?: ""; - val handle = (call.argument("contact") as? String) ?: ""; - val indexI = call.argument("index") ?: 0; - Log.i("MainActivity.kt", "indexI = " + indexI) - result.success(Cwtch.getMessage(profile, handle, indexI.toLong())) - } - "GetMessages" -> { - val profile = (call.argument("profile") as? String) ?: ""; - val handle = (call.argument("contact") as? String) ?: ""; - val start = (call.argument("start") as? Long) ?: 0; - val end = (call.argument("end") as? Long) ?: 0; - result.success(Cwtch.getMessages(profile, handle, start, end)) - } - "AcceptContact" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val handle = (call.argument("handle") as? String) ?: ""; - Cwtch.acceptContact(profile, handle); - } - "BlockContact" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val handle = (call.argument("handle") as? String) ?: ""; - Cwtch.blockContact(profile, handle); - } - "DebugResetContact" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val handle = (call.argument("handle") as? String) ?: ""; - Cwtch.debugResetContact(profile, handle); - } - "SendMessage" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val handle = (call.argument("handle") as? String) ?: ""; - val message = (call.argument("message") as? String) ?: ""; - Cwtch.sendMessage(profile, handle, message); - } - "SendInvitation" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val handle = (call.argument("handle") as? String) ?: ""; - val target = (call.argument("target") as? String) ?: ""; - Cwtch.sendInvitation(profile, handle, target); - } - "SendProfileEvent" -> { - val onion = (call.argument("onion") as? String) ?: ""; - val jsonEvent = (call.argument("jsonEvent") as? String) ?: ""; - Cwtch.sendProfileEvent(onion, jsonEvent); - } - "SendAppEvent" -> { - val jsonEvent = (call.argument("jsonEvent") as? String) ?: ""; - Cwtch.sendAppEvent(jsonEvent); - } - "ResetTor" -> { - Cwtch.resetTor(); - } - "ImportBundle" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val bundle = (call.argument("bundle") as? String) ?: ""; - Cwtch.importBundle(profile, bundle); - } - "SetGroupAttribute" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val groupHandle = (call.argument("groupHandle") as? String) ?: ""; - val key = (call.argument("key") as? String) ?: ""; - val value = (call.argument("value") as? String) ?: ""; - Cwtch.setGroupAttribute(profile, groupHandle, key, value); - } - "CreateGroup" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val server = (call.argument("server") as? String) ?: ""; - val groupName = (call.argument("groupname") as? String) ?: ""; - Cwtch.createGroup(profile, server, groupName); - } - "LeaveGroup" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val groupHandle = (call.argument("groupHandle") as? String) ?: ""; - Cwtch.leaveGroup(profile, groupHandle); - } - "RejectInvite" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val groupHandle = (call.argument("groupHandle") as? String) ?: ""; - Cwtch.rejectInvite(profile, groupHandle); - } - else -> result.notImplemented() + for (workInfo in listOfWorkInfo) { + if (workInfo != null) { + val progress = workInfo.progress + val eventType = progress.getString("EventType") ?: "" + val eventData = progress.getString("Data") + Log.i("MainActivity.kt", "got event $progress $eventType $eventData") + try { + eventbus_chan.invokeMethod(eventType, eventData) + } catch (e: Exception) { + Log.i("MainActivity.kt", "event bus exception") + } + } else { + Log.i("MainActivity.kt", "got null workInfo") + } + } + }) } } diff --git a/android/build.gradle b/android/build.gradle index 361fb7a..5685ae0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -27,6 +27,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { - delete rootProject.buildDir -} +//task clean(type: Delete) { +// delete rootProject.buildDir +//} diff --git a/pubspec.lock b/pubspec.lock index c7f8be5..58bca12 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -105,7 +105,7 @@ packages: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.1.1" + version: "6.1.0" flutter: dependency: "direct main" description: flutter From 6972028ecf00fc1ed87b253186c301dab2e55791 Mon Sep 17 00:00:00 2001 From: erinn Date: Wed, 2 Jun 2021 13:21:23 -0700 Subject: [PATCH 3/7] WIP: partial android service migration --- android/app/build.gradle | 5 +++++ .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 18 +++++++----------- .../kotlin/im/cwtch/flwtch/MainActivity.kt | 9 ++++++--- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 2786507..91220d1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -111,4 +111,9 @@ dependencies { // optional - Multiprocess support implementation "androidx.work:work-multiprocess:2.5.0" + + // end of workmanager deps + + // ipc + implementation "io.reactivex:rxkotlin:1.x.y" } diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index c7e5ef3..1ff7a92 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -7,25 +7,17 @@ import android.content.Context import android.graphics.Color import android.os.Build import android.util.Log - import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat -import androidx.work.CoroutineWorker -import androidx.work.Data -import androidx.work.ForegroundInfo -import androidx.work.WorkerParameters -import androidx.work.WorkManager - +import androidx.work.* +import cwtch.Cwtch import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel - import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch - import org.json.JSONObject -import cwtch.Cwtch class FlwtchWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) { @@ -67,7 +59,11 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : Log.i("FlwtchWorker.kt", "COROUTINEWORKER DOT ISSTOPPED TRUE OH MY") } //todo: this elides evt.EventID which may be needed at some point? - val data = Data.Builder().putString("EventType", evt.EventType).putString("Data", evt.Data).build() + val data = Data.Builder() + .putString("EventType", evt.EventType) + .putString("Data", evt.Data) + .putString("EventID", evt.EventID) + .build() setProgress(data) Thread.sleep(200) } diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index e2dccaf..1c416c5 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -77,7 +77,7 @@ class MainActivity: FlutterActivity() { WorkManager.getInstance(applicationContext) // requestId is the WorkRequest id .getWorkInfosByTagLiveData("cwtchEventBus") - .observeForever(Observer> { listOfWorkInfo -> + .observe(this, Observer> { listOfWorkInfo -> if (listOfWorkInfo.isNullOrEmpty()) { return@Observer } @@ -87,9 +87,12 @@ class MainActivity: FlutterActivity() { val progress = workInfo.progress val eventType = progress.getString("EventType") ?: "" val eventData = progress.getString("Data") - Log.i("MainActivity.kt", "got event $progress $eventType $eventData") + val output = progress.keyValueMap.toString() try { - eventbus_chan.invokeMethod(eventType, eventData) + if (eventType != "") { + Log.i("MainActivity.kt", "got event $output $eventType $eventData") + eventbus_chan.invokeMethod(eventType, eventData) + } } catch (e: Exception) { Log.i("MainActivity.kt", "event bus exception") } From 9c69275fe64f6ab8eb29ac806fe8970e8559375c Mon Sep 17 00:00:00 2001 From: erinn Date: Fri, 11 Jun 2021 14:28:20 -0700 Subject: [PATCH 4/7] android service and notification support take one. ACTION --- android/app/build.gradle | 5 +- android/app/src/main/AndroidManifest.xml | 3 + .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 145 ++++++++++++------ .../kotlin/im/cwtch/flwtch/MainActivity.kt | 145 +++++++++++++----- lib/cwtch/cwtch.dart | 4 +- lib/cwtch/ffi.dart | 14 +- lib/cwtch/gomobile.dart | 10 +- lib/main.dart | 37 ++++- lib/model.dart | 1 + 9 files changed, 268 insertions(+), 96 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 91220d1..58459a0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -114,6 +114,7 @@ dependencies { // end of workmanager deps - // ipc - implementation "io.reactivex:rxkotlin:1.x.y" + // needed to prevent a ListenableFuture dependency conflict/bug + // see https://github.com/google/ExoPlayer/issues/7905#issuecomment-692637059 + implementation 'com.google.guava:guava:any' } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a0ea053..ec51df3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -45,4 +45,7 @@ + + + diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index 1ff7a92..97c3024 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -1,21 +1,19 @@ package im.cwtch.flwtch -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager +import android.app.* import android.content.Context +import android.content.Context.ACTIVITY_SERVICE +import android.content.Intent +import android.graphics.BitmapFactory import android.graphics.Color import android.os.Build import android.util.Log import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat +import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.work.* import cwtch.Cwtch -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.MethodChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import io.flutter.FlutterInjector import org.json.JSONObject @@ -27,6 +25,9 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private var notificationID: MutableMap = mutableMapOf() + private var notificationIDnext: Int = 1; + override suspend fun doWork(): Result { val method = inputData.getString(KEY_METHOD) ?: return Result.failure() @@ -36,38 +37,96 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : // Mark the Worker as important val progress = "Trying to do a Flwtch" setForeground(createForegroundInfo(progress)) - handleCwtch(method, args) - return Result.success() + return handleCwtch(method, args) } - private suspend fun handleCwtch(method: String, args: String) { - var a = JSONObject(args); + private fun getNotificationID(profile: String, contact: String): Int { + val k = "$profile $contact" + if (!notificationID.containsKey(k)) { + notificationID[k] = notificationIDnext++ + } + return notificationID[k] ?: -1 + } + + private suspend fun handleCwtch(method: String, args: String): Result { + val a = JSONObject(args); when (method) { "Start" -> { Log.i("FlwtchWorker.kt", "handleAppInfo Start") - val appDir = (a.get("appDir") as? String) ?: ""; - val torPath = (a.get("torPath") as? String) ?: "tor"; - Log.i("FlwtchWorker.kt", " appDir: '" + appDir + "' torPath: '" + torPath + "'") + val appDir = (a.get("appDir") as? String) ?: "" + val torPath = (a.get("torPath") as? String) ?: "tor" + Log.i("FlwtchWorker.kt", "appDir: '$appDir' torPath: '$torPath'") - Cwtch.startCwtch(appDir, torPath) + if (Cwtch.startCwtch(appDir, torPath) != 0.toByte()) return Result.failure() // infinite coroutine :) while(true) { val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent()) - Log.i("FlwtchWorker.kt", "got appbusEvent: " + evt) + //Log.i("FlwtchWorker.kt", "got appbusEvent: " + evt) if (isStopped) { Log.i("FlwtchWorker.kt", "COROUTINEWORKER DOT ISSTOPPED TRUE OH MY") } - //todo: this elides evt.EventID which may be needed at some point? - val data = Data.Builder() - .putString("EventType", evt.EventType) - .putString("Data", evt.Data) - .putString("EventID", evt.EventID) + + if (evt.EventType == "NewMessageFromPeer") { + val data = JSONObject(evt.Data) + +// val appProcesses: List = (applicationContext.getSystemService(ACTIVITY_SERVICE) as ActivityManager).runningAppProcesses +// for (appProcess in appProcesses) { +// if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { +// Log.i("Foreground App", appProcess.processName) +// } +// } + val channelId = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createMessageNotificationChannel(data.getString("RemotePeer"), data.getString("RemotePeer")) + } else { + // If earlier version channel ID is not used + // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) + "" + } + + val loader = FlutterInjector.instance().flutterLoader() + val key = loader.getLookupKeyForAsset("assets/"+data.getString("Picture"))//"assets/profiles/001-centaur.png") + val fh = applicationContext.assets.open(key) + + val clickIntent = Intent(applicationContext, MainActivity::class.java).also { intent -> + intent.setAction(Intent.ACTION_RUN)//("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS") + //intent.setClassName("im.cwtch.flwtch", "MainActivity") + intent.putExtra("EventType", "NotificationClicked") + intent.putExtra("ProfileOnion", data.getString("ProfileOnion")) + intent.putExtra("RemotePeer", data.getString("RemotePeer")) + } + + val newNotification = NotificationCompat.Builder(applicationContext, channelId) + .setContentTitle(data.getString("Nick")) + .setContentText("New message") + .setLargeIcon(BitmapFactory.decodeStream(fh)) + .setSmallIcon(R.mipmap.knott) + .setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT)) + .setAutoCancel(true) .build() - setProgress(data) - Thread.sleep(200) + notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), data.getString("RemotePeer")), newNotification) + } + //todo: this elides evt.EventID which may be needed at some point? +// val data = Data.Builder() +// .putString("EventType", evt.EventType) +// .putString("Data", evt.Data) +// .putString("EventID", evt.EventID) +// .build() + //setProgress(data)//progress can only hold a single undelivered value so it's possible for observers to miss rapidfire updates + //Thread.sleep(200)//this is a kludge to make it mostly-work until a proper channel is implemented + Intent().also { intent -> + intent.setAction("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS") + intent.putExtra("EventType", evt.EventType) + intent.putExtra("Data", evt.Data) + intent.putExtra("EventID", evt.EventID) + LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent) + } } } + "ReconnectCwtchForeground" -> { + Cwtch.reconnectCwtchForeground() + } "SelectProfile" -> { val onion = (a.get("profile") as? String) ?: ""; Cwtch.selectProfile(onion) @@ -87,7 +146,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : "NumMessages" -> { val profile = (a.get("profile") as? String) ?: ""; val handle = (a.get("contact") as? String) ?: ""; - Result.success(Data.Builder().putLong("result", Cwtch.numMessages(profile, handle)).build()) + return Result.success(Data.Builder().putLong("result", Cwtch.numMessages(profile, handle)).build()) } "GetMessage" -> { //Log.i("MainActivivity.kt", (a.get("index"))); @@ -100,14 +159,14 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val handle = (a.get("contact") as? String) ?: ""; val indexI = a.getInt("index") ?: 0; Log.i("FlwtchWorker.kt", "indexI = " + indexI) - Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build()) + return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build()) } "GetMessages" -> { val profile = (a.get("profile") as? String) ?: ""; val handle = (a.get("contact") as? String) ?: ""; val start = (a.get("start") as? Long) ?: 0; val end = (a.get("end") as? Long) ?: 0; - Result.success(Data.Builder().putString("result", Cwtch.getMessages(profile, handle, start, end)).build()) + return Result.success(Data.Builder().putString("result", Cwtch.getMessages(profile, handle, start, end)).build()) } "AcceptContact" -> { val profile = (a.get("ProfileOnion") as? String) ?: ""; @@ -177,26 +236,9 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val groupHandle = (a.get("groupHandle") as? String) ?: ""; Cwtch.rejectInvite(profile, groupHandle); } - else -> Result.failure() - } - } - - private suspend fun launchCwtch(appDir: String, torPath: String) { - Cwtch.startCwtch(appDir, torPath) - // seperate coroutine to poll event bus and send to dart - //Log.i("FlwtchWorker.kt", "got event chan: " + eventbus_chan + " launching corouting...") - GlobalScope.launch(Dispatchers.IO) { - while(true) { - val evt = AppbusEvent(Cwtch.getAppBusEvent()) - Log.i("FlwtchWorker.kt", "got appbusEvent: " + evt) - launch(Dispatchers.Main) { - //todo: this elides evt.EventID which may be needed at some point? - val flutterEngine = FlutterEngine(applicationContext) - val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) - eventbus_chan.invokeMethod(evt.EventType, evt.Data) - } - } + else -> return Result.failure() } + return Result.success() } // Creates an instance of ForegroundInfo which can be used to update the @@ -243,6 +285,17 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : return channelId } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createMessageNotificationChannel(channelId: String, channelName: String): String{ + val chan = NotificationChannel(channelId, + channelName, NotificationManager.IMPORTANCE_HIGH) + chan.lightColor = Color.MAGENTA + chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + notificationManager.createNotificationChannel(chan) + return channelId + } + companion object { const val KEY_METHOD = "KEY_METHOD" const val KEY_ARGS = "KEY_ARGS" diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index d50dc49..3820914 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -1,12 +1,20 @@ package im.cwtch.flwtch import SplashView +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import androidx.annotation.NonNull import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import android.os.Looper import android.util.Log +import android.widget.Toast +import androidx.annotation.RequiresApi import androidx.lifecycle.Observer +import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.work.* import kotlinx.coroutines.Dispatchers @@ -27,6 +35,8 @@ import kotlin.concurrent.thread import org.json.JSONObject import java.io.File +import java.time.Duration +import java.util.concurrent.TimeUnit class MainActivity: FlutterActivity() { @@ -42,11 +52,32 @@ class MainActivity: FlutterActivity() { // Channel to send eventbus events on private val CWTCH_EVENTBUS = "test.flutter.dev/eventBus" + // Channel to trigger contactview when an external notification is clicked + private val CHANNEL_NOTIF_CLICK = "im.cwtch.flwtch/notificationClickHandler" + + private var methodChan: MethodChannel? = null + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + if (methodChan == null || intent.extras == null) return + if (!intent.extras!!.containsKey("ProfileOnion") || !intent.extras!!.containsKey("RemotePeer")) { + Log.i("onNewIntent", "got intent with no onions") + return + } + val profile = intent.extras!!.getString("ProfileOnion") + val handle = intent.extras!!.getString("RemotePeer") + val mappo = mapOf("ProfileOnion" to profile, "RemotePeer" to handle) + Log.i("MainActivity.kt", "onNewIntent($profile, $handle)") + val j = JSONObject(mappo) + methodChan!!.invokeMethod("NotificationClicked", j.toString()) + } + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) // Note: this methods are invoked on the main thread. MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_APP_INFO).setMethodCallHandler { call, result -> handleAppInfo(call, result) } MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_CWTCH).setMethodCallHandler { call, result -> handleCwtch(call, result) } + methodChan = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NOTIF_CLICK) } private fun handleAppInfo(@NonNull call: MethodCall, @NonNull result: Result) { @@ -61,49 +92,72 @@ class MainActivity: FlutterActivity() { val ainfo = this.applicationContext.packageManager.getApplicationInfo( "im.cwtch.flwtch", // Must be app name PackageManager.GET_SHARED_LIBRARY_FILES) - return ainfo.nativeLibraryDir } + // receives messages from the ForegroundService (which provides, ironically enough, the backend) private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) { + var method = call.method val argmap: Map = call.arguments as Map - val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build() - var tag = "" - if (call.method == "Start") { - tag = "cwtchEventBus" - } - val workRequest: WorkRequest = OneTimeWorkRequestBuilder().setInputData(data).addTag(tag).build() - WorkManager.getInstance(this).enqueue(workRequest) - if (call.method == "Start") { - val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) - WorkManager.getInstance(applicationContext) - // requestId is the WorkRequest id - .getWorkInfosByTagLiveData("cwtchEventBus") - .observe(this, Observer> { listOfWorkInfo -> - if (listOfWorkInfo.isNullOrEmpty()) { - return@Observer - } - for (workInfo in listOfWorkInfo) { - if (workInfo != null) { - val progress = workInfo.progress - val eventType = progress.getString("EventType") ?: "" - val eventData = progress.getString("Data") - val output = progress.keyValueMap.toString() - try { - if (eventType != "") { - Log.i("MainActivity.kt", "got event $output $eventType $eventData") - eventbus_chan.invokeMethod(eventType, eventData) - } - } catch (e: Exception) { - Log.i("MainActivity.kt", "event bus exception") - } - } else { - Log.i("MainActivity.kt", "got null workInfo") - } - } - }) + // the frontend calls Start every time it fires up, but we don't want to *actually* call Cwtch.Start() + // in case the ForegroundService is still running. in both cases, however, we *do* want to re-register + // the eventbus listener. + if (call.method == "Start") { + val workerTag = "cwtchEventBusWorker" + val uniqueTag = argmap["torPath"] ?: "nullEventBus" + + // note: because the ForegroundService is specified as UniquePeriodicWork, it can't actually get + // accidentally duplicated. however, we still need to manually check if it's running or not, so + // that we can divert this method call to ReconnectCwtchForeground instead if so. + val works = WorkManager.getInstance(this).getWorkInfosByTag(workerTag).get() + var alreadyRunning = false + for (workInfo in works) { + if (workInfo.tags.contains(uniqueTag)) { + if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) { + alreadyRunning = true + } + } else { + WorkManager.getInstance(this).cancelWorkById(workInfo.id) + } + } + + // register our eventbus listener. note that we observe any/all work according to its tag, which + // results in an implicit "reconnection" to old service threads even after frontend restarts + val mc = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) + val filter = IntentFilter("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS") + LocalBroadcastManager.getInstance(applicationContext).registerReceiver(MyBroadcastReceiver(mc), filter) + + if (alreadyRunning) { + Log.i("MainActivity.kt", "diverting Start -> Reconnect") + method = "ReconnectCwtchForeground" + } else { + Log.i("MainActivity.kt", "Start() launching foregroundservice") + // this is where the eventbus ForegroundService gets launched. WorkManager should keep it alive after this + val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build() + // 15 minutes is the shortest interval you can request + val workRequest = PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES).setInputData(data).addTag(workerTag).addTag(uniqueTag).build() + val workResult = WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.KEEP, workRequest) + return + } } + + // ...otherwise fallthru to a normal ffi method call (and return the result using the result callback) + val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build() + val workRequest = OneTimeWorkRequestBuilder().setInputData(data).build() + val workResult = WorkManager.getInstance(this).enqueue(workRequest) + WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(workRequest.id).observe( + this, Observer { workInfo -> + if (workInfo.state == WorkInfo.State.SUCCEEDED) { + val res = workInfo.outputData.keyValueMap.toString() + //Log.i("MainActivity.kt", "method $method returned SUCCESS($res)") + result.success(workInfo.outputData.getString("result")) + } else { + val idk = workInfo.state.toString() + //Log.i("MainActivity.kt", "method $method returned $idk") + } + } + ) } // source: https://web.archive.org/web/20210203022531/https://stackoverflow.com/questions/41928803/how-to-parse-json-in-kotlin/50468095 @@ -125,4 +179,23 @@ class MainActivity: FlutterActivity() { val EventID = this.optString("EventID") val Data = this.optString("Data") } + + class MyBroadcastReceiver(mc: MethodChannel) : BroadcastReceiver() { + val eventBus: MethodChannel = mc + + override fun onReceive(context: Context, intent: Intent) { +// StringBuilder().apply { +// append("Action: ${intent.action}\n") +// append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\n") +// toString().also { log -> +// Log.d("MyBroadcastReceiver", log) +// } +// } + val evtType = intent.getStringExtra("EventType") ?: "" + val evtData = intent.getStringExtra("Data") ?: "" + //val evtID = intent.getStringExtra("EventID") ?: ""//todo? + eventBus.invokeMethod(evtType, evtData) + } + } + } diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index cb2b0cd..3b22cde 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -1,6 +1,8 @@ abstract class Cwtch { // ignore: non_constant_identifier_names - Future Start(); + Future Start(); + // ignore: non_constant_identifier_names + Future ReconnectCwtchForeground(); // ignore: non_constant_identifier_names void SelectProfile(String onion); diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 410ffba..f335178 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -15,8 +15,8 @@ import '../config.dart'; /// Cwtch API /// ///////////////////// -typedef start_cwtch_function = Void Function(Pointer str, Int32 length, Pointer str2, Int32 length2); -typedef StartCwtchFn = void Function(Pointer dir, int len, Pointer tor, int torLen); +typedef start_cwtch_function = Int8 Function(Pointer str, Int32 length, Pointer str2, Int32 length2); +typedef StartCwtchFn = int Function(Pointer dir, int len, Pointer tor, int torLen); typedef void_from_string_string_function = Void Function(Pointer, Int32, Pointer, Int32); typedef VoidFromStringStringFn = void Function(Pointer, int, Pointer, int); @@ -76,7 +76,7 @@ class CwtchFfi implements Cwtch { } // ignore: non_constant_identifier_names - Future Start() async { + Future Start() async { String home = ""; String bundledTor = ""; Map envVars = Platform.environment; @@ -109,6 +109,14 @@ class CwtchFfi implements Cwtch { }); } + // ignore: non_constant_identifier_names + Future ReconnectCwtchForeground() async { + var reconnectCwtch = library.lookup>("c_ReconnectCwtchForeground"); + // ignore: non_constant_identifier_names + final ReconnectCwtchForeground = reconnectCwtch.asFunction(); + ReconnectCwtchForeground(); + } + // Called on object being disposed to (presumably on app close) to close the isolate that's listening to libcwtch-go events @override void dispose() { diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 210eb42..940fd0c 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -43,7 +43,7 @@ class CwtchGomobile implements Cwtch { } // ignore: non_constant_identifier_names - Future Start() async { + Future Start() async { print("gomobile.dart: Start()..."); var cwtchDir = path.join((await androidHomeDirectory).path, ".cwtch"); if (EnvironmentConfig.BUILD_VER == dev_version) { @@ -51,7 +51,13 @@ class CwtchGomobile implements Cwtch { } String torPath = path.join(await androidLibraryDir, "libtor.so"); print("gomobile.dart: Start invokeMethod Start($cwtchDir, $torPath)..."); - cwtchPlatform.invokeMethod("Start", {"appDir": cwtchDir, "torPath": torPath}); + return cwtchPlatform.invokeMethod("Start", {"appDir": cwtchDir, "torPath": torPath}); + } + + @override + // ignore: non_constant_identifier_names + Future ReconnectCwtchForeground() async { + cwtchPlatform.invokeMethod("ReconnectCwtchForeground", {}); } // Handle libcwtch-go events (received via kotlin) and dispatch to the cwtchNotifier diff --git a/lib/main.dart b/lib/main.dart index bfde1c1..d2da036 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,7 @@ +import 'dart:convert'; + import 'package:cwtch/notification_manager.dart'; +import 'package:cwtch/views/messageview.dart'; import 'package:cwtch/widgets/rightshiftfixer.dart'; import 'package:flutter/foundation.dart'; import 'package:cwtch/cwtch/ffi.dart'; @@ -8,6 +11,7 @@ import 'package:cwtch/errorHandler.dart'; import 'package:cwtch/settings.dart'; import 'package:cwtch/torstatus.dart'; import 'package:cwtch/views/triplecolview.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'cwtch/cwtch.dart'; import 'cwtch/cwtchNotifier.dart'; @@ -44,6 +48,8 @@ class FlwtchState extends State { var columns = [1]; // default or 'single column' mode //var columns = [1, 1, 2]; late ProfileListState profs; + final MethodChannel notificationClickChannel = MethodChannel('im.cwtch.flwtch/notificationClickHandler'); + final GlobalKey navKey = GlobalKey(); @override initState() { @@ -51,6 +57,7 @@ class FlwtchState extends State { cwtchInit = false; profs = ProfileListState(); + notificationClickChannel.setMethodCallHandler(_externalNotificationClicked); if (Platform.isAndroid) { var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager()); @@ -63,10 +70,9 @@ class FlwtchState extends State { cwtch = CwtchFfi(cwtchNotifier); } - cwtch.Start().then((val) { - setState(() { - cwtchInit = true; - }); + cwtch.Start(); + setState(() { + cwtchInit = true; }); } @@ -92,13 +98,12 @@ class FlwtchState extends State { return Consumer( builder: (context, settings, child) => MaterialApp( key: Key('app'), + navigatorKey: navKey, locale: settings.locale, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, title: 'Cwtch', theme: mkThemeData(settings), - // from dan: home: cwtchInit == true ? ProfileMgrView(cwtch) : SplashView(), - // from erinn: home: columns.length == 3 ? TripleColumnView() : ProfileMgrView(), home: cwtchInit == true ? (columns.length == 3 ? TripleColumnView() : ShiftRightFixer(child: ProfileMgrView())) : SplashView(), ), ); @@ -106,6 +111,26 @@ class FlwtchState extends State { ); } + Future _externalNotificationClicked(MethodCall call) async { + var args = jsonDecode(call.arguments); + var profile = profs.getProfile(args["ProfileOnion"])!; + var contact = profile.contactList.getContact(args["RemotePeer"])!; + contact.unreadMessages = 0; + navKey.currentState?.push( + MaterialPageRoute( + builder: (BuildContext builderContext) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: profile), + ChangeNotifierProvider.value(value: contact), + ], + builder: (context, child) => MessageView(), + ); + }, + ), + ); + } + @override void dispose() { cwtch.dispose(); diff --git a/lib/model.dart b/lib/model.dart index 2752e26..f0f3ef8 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -426,6 +426,7 @@ class MessageState extends ChangeNotifier { void tryLoad(BuildContext context) { Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, messageIndex).then((jsonMessage) { try { + print("debug messageJson $jsonMessage"); dynamic messageWrapper = jsonDecode(jsonMessage); if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { this._senderOnion = profileOnion; From 97f1e43009d6f543e7d7c5d1f2288894043cb73f Mon Sep 17 00:00:00 2001 From: erinn Date: Fri, 11 Jun 2021 14:51:35 -0700 Subject: [PATCH 5/7] remove debugging cruft --- .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 35 ++----------------- .../kotlin/im/cwtch/flwtch/MainActivity.kt | 16 ++------- android/build.gradle | 1 + lib/model.dart | 1 - pubspec.lock | 4 +-- 5 files changed, 8 insertions(+), 49 deletions(-) diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index 97c3024..5759fbe 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -19,8 +19,6 @@ import org.json.JSONObject class FlwtchWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) { - private val CWTCH_EVENTBUS = "test.flutter.dev/eventBus" - private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -33,9 +31,8 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : ?: return Result.failure() val args = inputData.getString(KEY_ARGS) ?: return Result.failure() - Log.i("FlwtchWorker.kt", "got call $method with args $args") // Mark the Worker as important - val progress = "Trying to do a Flwtch" + val progress = "Trying to do a Flwtch"//todo:translate setForeground(createForegroundInfo(progress)) return handleCwtch(method, args) } @@ -62,20 +59,8 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : // infinite coroutine :) while(true) { val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent()) - //Log.i("FlwtchWorker.kt", "got appbusEvent: " + evt) - if (isStopped) { - Log.i("FlwtchWorker.kt", "COROUTINEWORKER DOT ISSTOPPED TRUE OH MY") - } - if (evt.EventType == "NewMessageFromPeer") { val data = JSONObject(evt.Data) - -// val appProcesses: List = (applicationContext.getSystemService(ACTIVITY_SERVICE) as ActivityManager).runningAppProcesses -// for (appProcess in appProcesses) { -// if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { -// Log.i("Foreground App", appProcess.processName) -// } -// } val channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createMessageNotificationChannel(data.getString("RemotePeer"), data.getString("RemotePeer")) @@ -90,8 +75,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val fh = applicationContext.assets.open(key) val clickIntent = Intent(applicationContext, MainActivity::class.java).also { intent -> - intent.setAction(Intent.ACTION_RUN)//("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS") - //intent.setClassName("im.cwtch.flwtch", "MainActivity") + intent.setAction(Intent.ACTION_RUN) intent.putExtra("EventType", "NotificationClicked") intent.putExtra("ProfileOnion", data.getString("ProfileOnion")) intent.putExtra("RemotePeer", data.getString("RemotePeer")) @@ -107,14 +91,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : .build() notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), data.getString("RemotePeer")), newNotification) } - //todo: this elides evt.EventID which may be needed at some point? -// val data = Data.Builder() -// .putString("EventType", evt.EventType) -// .putString("Data", evt.Data) -// .putString("EventID", evt.EventID) -// .build() - //setProgress(data)//progress can only hold a single undelivered value so it's possible for observers to miss rapidfire updates - //Thread.sleep(200)//this is a kludge to make it mostly-work until a proper channel is implemented + Intent().also { intent -> intent.setAction("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS") intent.putExtra("EventType", evt.EventType) @@ -149,12 +126,6 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : return Result.success(Data.Builder().putLong("result", Cwtch.numMessages(profile, handle)).build()) } "GetMessage" -> { - //Log.i("MainActivivity.kt", (a.get("index"))); - -// var args : HashMap = a.gets(); -// Log.i("FlwtchWorker.kt", args); - - val profile = (a.get("profile") as? String) ?: ""; val handle = (a.get("contact") as? String) ?: ""; val indexI = a.getInt("index") ?: 0; diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index 3820914..4db21d5 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -67,7 +67,6 @@ class MainActivity: FlutterActivity() { val profile = intent.extras!!.getString("ProfileOnion") val handle = intent.extras!!.getString("RemotePeer") val mappo = mapOf("ProfileOnion" to profile, "RemotePeer" to handle) - Log.i("MainActivity.kt", "onNewIntent($profile, $handle)") val j = JSONObject(mappo) methodChan!!.invokeMethod("NotificationClicked", j.toString()) } @@ -137,7 +136,7 @@ class MainActivity: FlutterActivity() { val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build() // 15 minutes is the shortest interval you can request val workRequest = PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES).setInputData(data).addTag(workerTag).addTag(uniqueTag).build() - val workResult = WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.KEEP, workRequest) + WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.KEEP, workRequest) return } } @@ -145,16 +144,12 @@ class MainActivity: FlutterActivity() { // ...otherwise fallthru to a normal ffi method call (and return the result using the result callback) val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build() val workRequest = OneTimeWorkRequestBuilder().setInputData(data).build() - val workResult = WorkManager.getInstance(this).enqueue(workRequest) + WorkManager.getInstance(this).enqueue(workRequest) WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(workRequest.id).observe( this, Observer { workInfo -> if (workInfo.state == WorkInfo.State.SUCCEEDED) { val res = workInfo.outputData.keyValueMap.toString() - //Log.i("MainActivity.kt", "method $method returned SUCCESS($res)") result.success(workInfo.outputData.getString("result")) - } else { - val idk = workInfo.state.toString() - //Log.i("MainActivity.kt", "method $method returned $idk") } } ) @@ -184,13 +179,6 @@ class MainActivity: FlutterActivity() { val eventBus: MethodChannel = mc override fun onReceive(context: Context, intent: Intent) { -// StringBuilder().apply { -// append("Action: ${intent.action}\n") -// append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\n") -// toString().also { log -> -// Log.d("MyBroadcastReceiver", log) -// } -// } val evtType = intent.getStringExtra("EventType") ?: "" val evtData = intent.getStringExtra("Data") ?: "" //val evtID = intent.getStringExtra("EventID") ?: ""//todo? diff --git a/android/build.gradle b/android/build.gradle index 5685ae0..c887697 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -27,6 +27,7 @@ subprojects { project.evaluationDependsOn(':app') } +//removed due to gradle namespace conflicts that are beyond erinn's mere mortal understanding //task clean(type: Delete) { // delete rootProject.buildDir //} diff --git a/lib/model.dart b/lib/model.dart index 2100b84..a07fb07 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -436,7 +436,6 @@ class MessageState extends ChangeNotifier { void tryLoad(BuildContext context) { Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, messageIndex).then((jsonMessage) { try { - print("debug messageJson $jsonMessage"); dynamic messageWrapper = jsonDecode(jsonMessage); if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { this._senderOnion = profileOnion; diff --git a/pubspec.lock b/pubspec.lock index 9588274..3db29ce 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -105,7 +105,7 @@ packages: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.1.0" + version: "6.1.1" flutter: dependency: "direct main" description: flutter @@ -413,7 +413,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "6.1.0+1" + version: "6.2.0" webdriver: dependency: transitive description: From 5c8d448a37125b439ccae73c6564039ca667129e Mon Sep 17 00:00:00 2001 From: erinn Date: Fri, 11 Jun 2021 15:05:38 -0700 Subject: [PATCH 6/7] update lcg version --- LIBCWTCH-GO.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index d12319f..d21cb28 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -v0.0.2-58-gfddfd41-2021-06-10-18-36 \ No newline at end of file +v0.0.2-63-g033de73-2021-06-11-21-41 \ No newline at end of file From 2abdcdeae02be017f79118926dc5668d4772e9a4 Mon Sep 17 00:00:00 2001 From: erinn Date: Fri, 11 Jun 2021 15:15:42 -0700 Subject: [PATCH 7/7] updatemessageflags got mismerged oops --- .../app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index 5759fbe..7ab5f2c 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -139,6 +139,13 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val end = (a.get("end") as? Long) ?: 0; return Result.success(Data.Builder().putString("result", Cwtch.getMessages(profile, handle, start, end)).build()) } + "UpdateMessageFlags" -> { + val profile = (a.get("profile") as? String) ?: ""; + val handle = (a.get("contact") as? String) ?: ""; + val midx = (a.get("midx") as? Long) ?: 0; + val flags = (a.get("flags") as? Long) ?: 0; + Cwtch.updateMessageFlags(profile, handle, midx, flags); + } "AcceptContact" -> { val profile = (a.get("ProfileOnion") as? String) ?: ""; val handle = (a.get("handle") as? String) ?: "";