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();