Merge pull request 'android service and notifications' (#167) from androidservice into trunk
continuous-integration/drone/push Build is passing Details

Reviewed-on: #167
This commit is contained in:
Sarah Jamie Lewis 2021-06-11 15:25:53 -07:00
commit c606002541
10 changed files with 479 additions and 150 deletions

View File

@ -1 +1 @@
v0.0.2-58-gfddfd41-2021-06-10-18-36
v0.0.2-63-g033de73-2021-06-11-21-41

View File

@ -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'
}

View File

@ -43,4 +43,9 @@
<!--Needed to access Tor socket-->
<uses-permission android:name="android.permission.INTERNET" />
<!--Needed to run in background (lol)-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
<uses-permission android:name="android.permission.GET_TASKS" />
</manifest>

View File

@ -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<String, Int> = 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")
}
}

View File

@ -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<String, String> = call.arguments as Map<String, String>
// 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<String, dynamic> = 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<Int>("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<FlwtchWorker>(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<FlwtchWorker>().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)
}
}
}

View File

@ -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
//}

View File

@ -1,6 +1,8 @@
abstract class Cwtch {
// ignore: non_constant_identifier_names
Future<void> Start();
Future<dynamic> Start();
// ignore: non_constant_identifier_names
Future<void> ReconnectCwtchForeground();
// ignore: non_constant_identifier_names
void SelectProfile(String onion);

View File

@ -15,8 +15,8 @@ import '../config.dart';
/// Cwtch API ///
/////////////////////
typedef start_cwtch_function = Void Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
typedef StartCwtchFn = void Function(Pointer<Utf8> dir, int len, Pointer<Utf8> tor, int torLen);
typedef start_cwtch_function = Int8 Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
typedef StartCwtchFn = int Function(Pointer<Utf8> dir, int len, Pointer<Utf8> tor, int torLen);
typedef void_from_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
@ -79,7 +79,7 @@ class CwtchFfi implements Cwtch {
}
// ignore: non_constant_identifier_names
Future<void> Start() async {
Future<dynamic> Start() async {
String home = "";
String bundledTor = "";
Map<String, String> envVars = Platform.environment;
@ -112,6 +112,14 @@ class CwtchFfi implements Cwtch {
});
}
// ignore: non_constant_identifier_names
Future<void> ReconnectCwtchForeground() async {
var reconnectCwtch = library.lookup<NativeFunction<Void Function()>>("c_ReconnectCwtchForeground");
// ignore: non_constant_identifier_names
final ReconnectCwtchForeground = reconnectCwtch.asFunction<void Function()>();
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() {

View File

@ -42,7 +42,7 @@ class CwtchGomobile implements Cwtch {
}
// ignore: non_constant_identifier_names
Future<void> Start() async {
Future<dynamic> 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<void> ReconnectCwtchForeground() async {
cwtchPlatform.invokeMethod("ReconnectCwtchForeground", {});
}
// Handle libcwtch-go events (received via kotlin) and dispatch to the cwtchNotifier

View File

@ -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<Flwtch> {
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<NavigatorState> navKey = GlobalKey<NavigatorState>();
@override
initState() {
@ -51,6 +57,7 @@ class FlwtchState extends State<Flwtch> {
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<Flwtch> {
cwtch = CwtchFfi(cwtchNotifier);
}
cwtch.Start().then((val) {
setState(() {
cwtchInit = true;
});
cwtch.Start();
setState(() {
cwtchInit = true;
});
}
@ -92,13 +98,12 @@ class FlwtchState extends State<Flwtch> {
return Consumer<Settings>(
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<Flwtch> {
);
}
Future<void> _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<void>(
builder: (BuildContext builderContext) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: contact),
],
builder: (context, child) => MessageView(),
);
},
),
);
}
@override
void dispose() {
cwtch.dispose();