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
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 4b49db2..58459a0 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,30 @@ 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"
+
+ // end of workmanager deps
+
+ // 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 6445594..ec51df3 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -43,4 +43,9 @@
+
+
+
+
+
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..7ab5f2c
--- /dev/null
+++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt
@@ -0,0 +1,287 @@
+package im.cwtch.flwtch
+
+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.FlutterInjector
+import org.json.JSONObject
+
+
+class FlwtchWorker(context: Context, parameters: WorkerParameters) :
+ CoroutineWorker(context, parameters) {
+ private val notificationManager =
+ 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()
+ val args = inputData.getString(KEY_ARGS)
+ ?: return Result.failure()
+ // Mark the Worker as important
+ val progress = "Trying to do a Flwtch"//todo:translate
+ setForeground(createForegroundInfo(progress))
+ return handleCwtch(method, 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'")
+
+ if (Cwtch.startCwtch(appDir, torPath) != 0.toByte()) return Result.failure()
+
+ // infinite coroutine :)
+ while(true) {
+ val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
+ if (evt.EventType == "NewMessageFromPeer") {
+ val data = JSONObject(evt.Data)
+ 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)
+ 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()
+ notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), data.getString("RemotePeer")), newNotification)
+ }
+
+ 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)
+ }
+ "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) ?: "";
+ return Result.success(Data.Builder().putLong("result", Cwtch.numMessages(profile, handle)).build())
+ }
+ "GetMessage" -> {
+ 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)
+ 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;
+ 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) ?: "";
+ 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 -> return Result.failure()
+ }
+ return Result.success()
+ }
+
+ // 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.MAGENTA
+ chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
+ notificationManager.createNotificationChannel(chan)
+ 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"
+ }
+
+ 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 dc34259..4db21d5 100644
--- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt
+++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt
@@ -1,11 +1,21 @@
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
import kotlinx.coroutines.GlobalScope
@@ -25,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() {
@@ -40,13 +52,31 @@ 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)
+ 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,150 +91,68 @@ 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) {
- 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 + "'")
- Cwtch.startCwtch(appDir, torPath)
+ var method = call.method
+ val argmap: Map = call.arguments as Map
- // 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)
- }
+ // 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)
}
}
- "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);
+ // 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)
-
- 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()))
+ 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()
+ WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.KEEP, workRequest)
+ return
}
- "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))
- }
- "UpdateMessageFlags" -> {
- val profile = (call.argument("profile") as? String) ?: "";
- val handle = (call.argument("contact") as? String) ?: "";
- val midx = (call.argument("midx") as? Long) ?: 0;
- val flags = (call.argument("flags") as? Long) ?: 0;
- Cwtch.updateMessageFlags(profile, handle, midx, flags);
- }
- "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()
}
+
+ // ...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()
+ 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()
+ result.success(workInfo.outputData.getString("result"))
+ }
+ }
+ )
}
// source: https://web.archive.org/web/20210203022531/https://stackoverflow.com/questions/41928803/how-to-parse-json-in-kotlin/50468095
@@ -226,4 +174,16 @@ 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) {
+ val evtType = intent.getStringExtra("EventType") ?: ""
+ val evtData = intent.getStringExtra("Data") ?: ""
+ //val evtID = intent.getStringExtra("EventID") ?: ""//todo?
+ eventBus.invokeMethod(evtType, evtData)
+ }
+ }
+
}
diff --git a/android/build.gradle b/android/build.gradle
index 361fb7a..c887697 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -27,6 +27,7 @@ subprojects {
project.evaluationDependsOn(':app')
}
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
+//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/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart
index 816fa65..7c51a94 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 df0e163..f0c8786 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);
@@ -79,7 +79,7 @@ class CwtchFfi implements Cwtch {
}
// ignore: non_constant_identifier_names
- Future Start() async {
+ Future Start() async {
String home = "";
String bundledTor = "";
Map envVars = Platform.environment;
@@ -112,6 +112,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 d89f38e..a193811 100644
--- a/lib/cwtch/gomobile.dart
+++ b/lib/cwtch/gomobile.dart
@@ -42,7 +42,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) {
@@ -50,7 +50,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();