cwtch-ui/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt

353 lines
20 KiB
Kotlin
Raw Permalink Normal View History

2021-06-24 23:10:45 +00:00
package im.cwtch.flwtch
2022-06-29 17:21:54 +00:00
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
2021-06-24 23:10:45 +00:00
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
2022-06-29 17:21:54 +00:00
import android.net.Uri
2021-06-24 23:10:45 +00:00
import android.os.Build
import android.util.Log
import android.content.pm.ServiceInfo
2021-06-24 23:10:45 +00:00
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
2022-06-29 17:21:54 +00:00
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
2021-06-24 23:10:45 +00:00
import cwtch.Cwtch
import io.flutter.FlutterInjector
import org.json.JSONObject
2021-09-27 19:53:21 +00:00
import java.nio.file.Files
import java.nio.file.Paths
import java.io.FileInputStream
import java.io.File
2021-06-24 23:10:45 +00:00
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
private var notificationSimple: String? = null
private var notificationConversationInfo: String? = null
private val TAG: String = "FlwtchWorker.kt"
2021-06-24 23:10:45 +00:00
override suspend fun doWork(): Result {
// Hack to uncomment and deploy if your device has zombie workers you need to kill
// We need a proper solution but this will clear those out for now
/*if (notificationSimple == null) {
Log.e("FlwtchWorker", "doWork found notificationSimple is null, app has not started, this is a stale thread, terminating")
return Result.failure()
}*/
2021-06-24 23:10:45 +00:00
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 = "Cwtch is keeping Tor running in the background" // TODO: translate
2021-06-24 23:10:45 +00:00
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 fun handleCwtch(method: String, args: String): Result {
if (method != "Start") {
if (Cwtch.started() != 1.toLong()) {
Log.e(TAG, "libCwtch-go reports it is not initialized yet")
return Result.failure()
}
}
val a = JSONObject(args)
when (method) {
"Start" -> {
Log.i(TAG, "handleAppInfo Start")
val appDir = (a.get("appDir") as? String) ?: ""
val torPath = (a.get("torPath") as? String) ?: "tor"
Log.i(TAG, "appDir: '$appDir' torPath: '$torPath'")
2021-06-24 23:10:45 +00:00
if (Cwtch.startCwtch(appDir, torPath) != 0.toLong()) return Result.failure()
2021-06-24 23:10:45 +00:00
Log.i(TAG, "startCwtch success, starting coroutine AppbusEvent loop...")
val downloadIDs = mutableMapOf<String, Int>()
val downloadFinishedIDs = mutableMapOf<String, Int>()
2022-06-29 17:21:54 +00:00
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags = flags or PendingIntent.FLAG_IMMUTABLE
}
while (true) {
try {
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
// TODO replace this notification block with the NixNotification manager in dart as it has access to contact names and also needs less working around
if (evt.EventType == "NewMessageFromPeer" || evt.EventType == "NewMessageFromGroup") {
val data = JSONObject(evt.Data)
val handle = data.getString("RemotePeer");
val conversationId = data.getInt("ConversationID").toString();
val notificationChannel = if (evt.EventType == "NewMessageFromPeer") handle else conversationId
if (data["RemotePeer"] != data["ProfileOnion"]) {
val notification = data["notification"]
2021-06-25 00:59:54 +00:00
if (notification == "SimpleEvent") {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createMessageNotificationChannel("Cwtch", "Cwtch")
} 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)
""
}
2022-02-08 17:35:25 +00:00
val clickIntent = Intent(applicationContext, MainActivity::class.java).also { intent ->
intent.action = Intent.ACTION_RUN
intent.putExtra("EventType", "NotificationClicked")
}
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
.setContentTitle("Cwtch")
.setContentText(notificationSimple ?: "New Message")
.setSmallIcon(R.mipmap.knott_transparent)
2022-06-29 17:21:54 +00:00
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, flags))
.setAutoCancel(true)
.build()
2022-02-08 17:35:25 +00:00
notificationManager.notify(getNotificationID("Cwtch", "Cwtch"), newNotification)
} else if (notification == "ContactInfo") {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createMessageNotificationChannel(notificationChannel, notificationChannel)
} 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()
Log.i(TAG, "notification for " + evt.EventType + " " + handle + " " + conversationId + " " + channelId)
Log.i(TAG, data.toString());
val key = loader.getLookupKeyForAsset(data.getString("picture"))//"assets/profiles/001-centaur.png")
var fh : java.io.InputStream? = null;
try {
fh = applicationContext.assets.open(key)
} catch (e: Exception) {
Log.d("FlwtchWorker->ContactInfo", e.toString() + " :: " + e.getStackTrace());
}
try {
val file = File(data.getString("picture"))
fh = FileInputStream(file)
} catch (e: Exception) {
Log.d("FlwtchWorker->ContactInfo", e.toString() + " :: " + e.getStackTrace());
}
2022-02-08 17:35:25 +00:00
val clickIntent = Intent(applicationContext, MainActivity::class.java).also { intent ->
intent.action = Intent.ACTION_RUN
intent.putExtra("EventType", "NotificationClicked")
intent.putExtra("ProfileOnion", data.getString("ProfileOnion"))
intent.putExtra("Handle", handle)
}
val image : android.graphics.Bitmap? = if (fh != null) BitmapFactory.decodeStream(fh ) else null;
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
.setContentTitle(data.getString("Nick"))
.setContentText((notificationConversationInfo
?: "New Message From %1").replace("%1", data.getString("Nick")))
.setLargeIcon(image)
.setSmallIcon(R.mipmap.knott_transparent)
2022-06-29 17:21:54 +00:00
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, flags))
.setAutoCancel(true)
.build()
2022-02-08 17:35:25 +00:00
notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), channelId), newNotification)
if (fh != null) {
fh.close()
}
}
2022-02-08 16:32:05 +00:00
}
} else if (evt.EventType == "FileDownloadProgressUpdate") {
try {
val data = JSONObject(evt.Data);
val fileKey = data.getString("FileKey");
val title = data.getString("NameSuggestion");
val progress = data.getString("Progress").toInt();
val progressMax = data.getString("FileSizeInChunks").toInt();
// if we have seen a download finished update for this key then ignore it
if (!downloadFinishedIDs.containsKey(fileKey)) {
if (!downloadIDs.containsKey(fileKey)) {
downloadIDs.put(fileKey, downloadIDs.count());
}
var dlID = downloadIDs.get(fileKey);
if (dlID == null) {
dlID = 0;
}
if (progress >= 0) {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createDownloadNotificationChannel(fileKey, fileKey)
} 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 newNotification = NotificationCompat.Builder(applicationContext, channelId)
.setOngoing(true)
.setContentTitle("Downloading")//todo: translate
.setContentText(title)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setProgress(progressMax, progress, false)
.setSound(null)
//.setSilent(true)
.build();
notificationManager.notify(dlID, newNotification);
}
}
} catch (e: Exception) {
Log.d("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace());
}
} else if (evt.EventType == "FileDownloaded") {
Log.d(TAG, "file downloaded!");
val data = JSONObject(evt.Data);
val tempFile = data.getString("TempFile");
val fileKey = data.getString("FileKey");
if (tempFile != "" && tempFile != data.getString("FilePath")) {
val filePath = data.getString("FilePath");
Log.i(TAG, "moving " + tempFile + " to " + filePath);
val sourcePath = Paths.get(tempFile);
val targetUri = Uri.parse(filePath);
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
val bytesWritten = Files.copy(sourcePath, os);
Log.d("TAG", "copied " + bytesWritten.toString() + " bytes");
if (bytesWritten != 0L) {
os?.flush();
os?.close();
Files.delete(sourcePath);
}
}
// Suppress future notifications...
downloadFinishedIDs.put(fileKey, downloadIDs.count());
if (downloadIDs.containsKey(fileKey)) {
notificationManager.cancel(downloadIDs.get(fileKey) ?: 0);
}
2021-11-04 22:31:50 +00:00
}
Intent().also { intent ->
intent.action = "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)
}
if (evt.EventType == "Shutdown") {
Log.i(TAG, "processing shutdown event, exiting FlwtchWorker/Start()...");
return Result.success()
}
} catch (e: Exception) {
Log.e(TAG, "Error in handleCwtch: " + e.toString() + " :: " + e.getStackTrace());
}
2021-06-24 23:10:45 +00:00
}
}
// Event passing translations from Flutter to Kotlin worker scope so the worker can use them
"L10nInit" -> {
notificationSimple = (a.get("notificationSimple") as? String) ?: "New Message"
notificationConversationInfo = (a.get("notificationConversationInfo") as? String)
?: "New Message From "
}
else -> {
Log.i(TAG, "unknown command: " + method);
return Result.failure()
}
2021-09-27 19:53:21 +00:00
}
return Result.success()
2021-06-24 23:10:45 +00:00
}
// 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" // TODO: change
val cancel = "Shut down" // TODO: translate
2021-06-24 23:10:45 +00:00
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createForegroundNotificationChannel(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)
""
}
2021-06-25 00:59:54 +00:00
val cancelIntent = Intent(applicationContext, MainActivity::class.java).also { intent ->
intent.action = Intent.ACTION_RUN
intent.putExtra("EventType", "ShutdownClicked")
}
2022-06-29 17:21:54 +00:00
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags = flags or PendingIntent.FLAG_IMMUTABLE
}
2021-06-24 23:10:45 +00:00
val notification = NotificationCompat.Builder(applicationContext, channelId)
.setContentTitle(title)
.setTicker(title)
.setContentText(progress)
2021-07-15 20:03:57 +00:00
.setSmallIcon(R.mipmap.knott_transparent)
2021-06-24 23:10:45 +00:00
.setOngoing(true)
// Add the cancel action to the notification which can
// be used to cancel the worker
2022-06-29 17:21:54 +00:00
.addAction(android.R.drawable.ic_delete, cancel, PendingIntent.getActivity(applicationContext, 2, cancelIntent, flags))
2021-06-24 23:10:45 +00:00
.build()
return ForegroundInfo(101, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
2021-06-24 23:10:45 +00:00
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createForegroundNotificationChannel(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
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createDownloadNotificationChannel(channelId: String, channelName: String): String{
val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW)
chan.lightColor = Color.MAGENTA
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
notificationManager.createNotificationChannel(chan)
return channelId
}
2021-06-24 23:10:45 +00:00
companion object {
const val KEY_METHOD = "KEY_METHOD"
const val KEY_ARGS = "KEY_ARGS"
}
}