2021-06-24 23:10:45 +00:00
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.util.Log
import android.view.Window
import androidx.lifecycle.Observer
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.work.*
import io.flutter.embedding.android.SplashScreen
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel.Result
2021-09-27 19:53:21 +00:00
import io.flutter.plugin.common.ErrorLogResult
2021-06-24 23:10:45 +00:00
import org.json.JSONObject
import java.util.concurrent.TimeUnit
2021-12-14 23:50:08 +00:00
import java.io.File
2021-12-15 01:13:13 +00:00
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
2021-06-24 23:10:45 +00:00
2021-09-27 19:53:21 +00:00
import android.net.Uri
import android.provider.DocumentsContract
import android.content.ContentUris
import android.os.Build
import android.os.Environment
import android.database.Cursor
import android.provider.MediaStore
2021-06-24 23:10:45 +00:00
class MainActivity : FlutterActivity ( ) {
override fun provideSplashScreen ( ) : SplashScreen ? = SplashView ( )
// Channel to get app info
private val CHANNEL _APP _INFO = " test.flutter.dev/applicationInfo "
private val CALL _APP _INFO = " getNativeLibDir "
// Channel to get cwtch api calls on
private val CHANNEL _CWTCH = " cwtch "
// Channel to send eventbus events on
private val CWTCH _EVENTBUS = " test.flutter.dev/eventBus "
2021-06-25 00:59:54 +00:00
// Channels to trigger actions when an external notification is clicked
2021-06-24 23:10:45 +00:00
private val CHANNEL _NOTIF _CLICK = " im.cwtch.flwtch/notificationClickHandler "
2021-06-25 00:59:54 +00:00
private val CHANNEL _SHUTDOWN _CLICK = " im.cwtch.flwtch/shutdownClickHandler "
2021-06-24 23:10:45 +00:00
// WorkManager tag applied to all Start() infinite coroutines
val WORKER _TAG = " cwtchEventBusWorker "
private var myReceiver : MyBroadcastReceiver ? = null
2021-06-25 00:59:54 +00:00
private var notificationClickChannel : MethodChannel ? = null
private var shutdownClickChannel : MethodChannel ? = null
2021-06-24 23:10:45 +00:00
2021-09-27 19:53:21 +00:00
// "Download to..." prompt extra arguments
2021-10-01 19:38:06 +00:00
private val FILEPICKER _REQUEST _CODE = 234
2021-12-14 23:50:08 +00:00
private val PREVIEW _EXPORT _REQUEST _CODE = 235
2022-03-10 21:29:28 +00:00
private val PROFILE _EXPORT _REQUEST _CODE = 236
2021-09-27 19:53:21 +00:00
private var dlToProfile = " "
private var dlToHandle = " "
private var dlToFileKey = " "
2021-12-14 23:50:08 +00:00
private var exportFromPath = " "
2021-09-27 19:53:21 +00:00
2021-09-30 00:16:00 +00:00
// handles clicks received from outside the app (ie, notifications)
2021-06-24 23:10:45 +00:00
override fun onNewIntent ( intent : Intent ) {
super . onNewIntent ( intent )
2021-06-25 00:59:54 +00:00
if ( notificationClickChannel == null || intent . extras == null ) return
if ( intent . extras !! . getString ( " EventType " ) == " NotificationClicked " ) {
2021-06-29 18:49:29 +00:00
if ( !in tent . extras !! . containsKey ( " ProfileOnion " ) || !in tent . extras !! . containsKey ( " Handle " ) ) {
2021-06-25 00:59:54 +00:00
Log . i ( " onNewIntent " , " got notification clicked intent with no onions " )
return
}
val profile = intent . extras !! . getString ( " ProfileOnion " )
2021-06-29 18:49:29 +00:00
val handle = intent . extras !! . getString ( " Handle " )
val mappo = mapOf ( " ProfileOnion " to profile , " Handle " to handle )
2021-06-25 00:59:54 +00:00
val j = JSONObject ( mappo )
notificationClickChannel !! . invokeMethod ( " NotificationClicked " , j . toString ( ) )
} else if ( intent . extras !! . getString ( " EventType " ) == " ShutdownClicked " ) {
shutdownClickChannel !! . invokeMethod ( " ShutdownClicked " , " " )
} else {
print ( " warning: received intent with unknown method; ignoring " )
2021-06-24 23:10:45 +00:00
}
}
2021-09-30 00:16:00 +00:00
// handles return values from the system file picker
2021-09-27 19:53:21 +00:00
override fun onActivityResult ( requestCode : Int , result : Int , intent : Intent ? ) {
2021-10-01 19:38:06 +00:00
super . onActivityResult ( requestCode , result , intent ) ;
2021-09-27 19:53:21 +00:00
if ( intent == null || intent !! . getData ( ) == null ) {
Log . i ( " MainActivity:onActivityResult " , " user canceled activity " ) ;
return ;
}
2021-10-01 19:38:06 +00:00
if ( requestCode == FILEPICKER _REQUEST _CODE ) {
val filePath = intent !! . getData ( ) . toString ( ) ;
val manifestPath = StringBuilder ( ) . append ( this . applicationContext . cacheDir ) . append ( " / " ) . append ( this . dlToFileKey ) . toString ( ) ;
handleCwtch ( MethodCall ( " DownloadFile " , mapOf (
" ProfileOnion " to this . dlToProfile ,
" handle " to this . dlToHandle ,
" filepath " to filePath ,
" manifestpath " to manifestPath ,
" filekey " to this . dlToFileKey
) ) , ErrorLogResult ( " " ) ) ; //placeholder; this Result is never actually invoked
2021-12-14 23:50:08 +00:00
} else if ( requestCode == PREVIEW _EXPORT _REQUEST _CODE ) {
val targetPath = intent !! . getData ( ) . toString ( )
2021-12-15 01:13:13 +00:00
val sourcePath = Paths . get ( this . exportFromPath ) ;
val targetUri = Uri . parse ( targetPath ) ;
val os = this . applicationContext . getContentResolver ( ) . openOutputStream ( targetUri ) ;
val bytesWritten = Files . copy ( sourcePath , os ) ;
Log . d ( " MainActivity:PREVIEW_EXPORT " , " copied " + bytesWritten . toString ( ) + " bytes " ) ;
if ( bytesWritten != 0L ) {
os ?. flush ( ) ;
os ?. close ( ) ;
//Files.delete(sourcePath);
}
2022-03-10 21:29:28 +00:00
} else if ( requestCode == PROFILE _EXPORT _REQUEST _CODE ) {
val targetPath = intent !! . getData ( ) . toString ( )
val srcFile = StringBuilder ( ) . append ( this . applicationContext . cacheDir ) . append ( " / " ) . append ( this . exportFromPath ) . toString ( ) ;
Log . i ( " MainActivity:PREVIEW_EXPORT " , " exporting previewed file " + srcFile ) ;
val sourcePath = Paths . get ( srcFile ) ;
val targetUri = Uri . parse ( targetPath ) ;
val os = this . applicationContext . getContentResolver ( ) . openOutputStream ( targetUri ) ;
val bytesWritten = Files . copy ( sourcePath , os ) ;
Log . d ( " MainActivity:PREVIEW_EXPORT " , " copied " + bytesWritten . toString ( ) + " bytes " ) ;
if ( bytesWritten != 0L ) {
os ?. flush ( ) ;
os ?. close ( ) ;
//Files.delete(sourcePath);
}
2021-10-01 19:38:06 +00:00
}
2021-09-27 19:53:21 +00:00
}
2021-06-24 23:10:45 +00:00
override fun configureFlutterEngine ( @NonNull flutterEngine : FlutterEngine ) {
super . configureFlutterEngine ( flutterEngine )
// Note: this methods are invoked on the main thread.
//note to self: ask someone if this does anything ^ea
requestWindowFeature ( Window . FEATURE _NO _TITLE )
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 ) }
2021-06-25 00:59:54 +00:00
notificationClickChannel = MethodChannel ( flutterEngine . dartExecutor . binaryMessenger , CHANNEL _NOTIF _CLICK )
shutdownClickChannel = MethodChannel ( flutterEngine . dartExecutor . binaryMessenger , CHANNEL _SHUTDOWN _CLICK )
2021-06-24 23:10:45 +00:00
}
2022-03-11 00:10:28 +00:00
// MethodChannel CHANNEL_APP_INFO handler (Flutter Channel for requests for Android environment info)
2021-06-24 23:10:45 +00:00
private fun handleAppInfo ( @NonNull call : MethodCall , @NonNull result : Result ) {
when ( call . method ) {
CALL _APP _INFO -> result . success ( getNativeLibDir ( ) )
?: result . error ( " Unavailable " , " nativeLibDir not available " , null ) ;
else -> result . notImplemented ( )
}
}
private fun getNativeLibDir ( ) : String {
val ainfo = this . applicationContext . packageManager . getApplicationInfo (
" im.cwtch.flwtch " , // Must be app name
PackageManager . GET _SHARED _LIBRARY _FILES )
return ainfo . nativeLibraryDir
}
// receives messages from the ForegroundService (which provides, ironically enough, the backend)
private fun handleCwtch ( @NonNull call : MethodCall , @NonNull result : Result ) {
var method = call . method
val argmap : Map < String , String > = call . arguments as Map < String , String >
// 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 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 ( WORKER _TAG ) . get ( )
for ( workInfo in works ) {
2021-12-03 19:28:10 +00:00
WorkManager . getInstance ( this ) . cancelWorkById ( workInfo . id )
2021-06-24 23:10:45 +00:00
}
WorkManager . getInstance ( this ) . pruneWork ( )
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 ( WORKER _TAG ) . addTag ( uniqueTag ) . build ( )
WorkManager . getInstance ( this ) . enqueueUniquePeriodicWork ( " req_ $uniqueTag " , ExistingPeriodicWorkPolicy . REPLACE , workRequest )
return
2021-09-27 19:53:21 +00:00
} else if ( call . method == " CreateDownloadableFile " ) {
this . dlToProfile = argmap [ " ProfileOnion " ] ?: " "
this . dlToHandle = argmap [ " handle " ] ?: " "
val suggestedName = argmap [ " filename " ] ?: " filename.ext "
this . dlToFileKey = argmap [ " filekey " ] ?: " "
val intent = Intent ( Intent . ACTION _CREATE _DOCUMENT ) . apply {
addCategory ( Intent . CATEGORY _OPENABLE )
type = " application/octet-stream "
putExtra ( Intent . EXTRA _TITLE , suggestedName )
}
2021-10-01 19:38:06 +00:00
startActivityForResult ( intent , FILEPICKER _REQUEST _CODE )
2021-09-27 19:53:21 +00:00
return
2021-12-14 23:50:08 +00:00
} else if ( call . method == " ExportPreviewedFile " ) {
this . exportFromPath = argmap [ " Path " ] ?: " "
val suggestion = argmap [ " FileName " ] ?: " filename.ext "
var imgType = " jpeg "
if ( suggestion . endsWith ( " png " ) ) {
imgType = " png "
} else if ( suggestion . endsWith ( " webp " ) ) {
imgType = " webp "
} else if ( suggestion . endsWith ( " bmp " ) ) {
imgType = " bmp "
} else if ( suggestion . endsWith ( " gif " ) ) {
imgType = " gif "
}
val intent = Intent ( Intent . ACTION _CREATE _DOCUMENT ) . apply {
addCategory ( Intent . CATEGORY _OPENABLE )
type = " image/ " + imgType
putExtra ( Intent . EXTRA _TITLE , suggestion )
}
startActivityForResult ( intent , PREVIEW _EXPORT _REQUEST _CODE )
return
2022-03-10 21:29:28 +00:00
} else if ( call . method == " ExportProfile " ) {
this . exportFromPath = argmap [ " file " ] ?: " "
val intent = Intent ( Intent . ACTION _CREATE _DOCUMENT ) . apply {
addCategory ( Intent . CATEGORY _OPENABLE )
type = " application/gzip "
putExtra ( Intent . EXTRA _TITLE , argmap [ " file " ] )
}
startActivityForResult ( intent , PROFILE _EXPORT _REQUEST _CODE )
2021-06-24 23:10:45 +00:00
}
// ...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 ->
2022-02-16 21:43:46 +00:00
if ( workInfo != null && workInfo . state == WorkInfo . State . SUCCEEDED ) {
2021-06-24 23:10:45 +00:00
val res = workInfo . outputData . keyValueMap . toString ( )
result . success ( workInfo . outputData . getString ( " result " ) )
}
}
)
}
// using onresume/onstop for broadcastreceiver because of extended discussion on https://stackoverflow.com/questions/7439041/how-to-unregister-broadcastreceiver
override fun onResume ( ) {
super . onResume ( )
Log . i ( " MainActivity.kt " , " onResume " )
if ( myReceiver == null ) {
Log . i ( " MainActivity.kt " , " onResume registering local broadcast receiver / event bus forwarder " )
val mc = MethodChannel ( flutterEngine ?. dartExecutor ?. binaryMessenger , CWTCH _EVENTBUS )
val filter = IntentFilter ( " im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS " )
myReceiver = MyBroadcastReceiver ( mc )
LocalBroadcastManager . getInstance ( applicationContext ) . registerReceiver ( myReceiver !! , filter )
}
// ReconnectCwtchForeground which will resync counters and settings...
// We need to do this here because after a "pause" flutter is still running
// but we might have lost sync with the background process...
Log . i ( " MainActivity.kt " , " Call ReconnectCwtchForeground " )
val data : Data = Data . Builder ( ) . putString ( FlwtchWorker . KEY _METHOD , " ReconnectCwtchForeground " ) . putString ( FlwtchWorker . KEY _ARGS , " {} " ) . build ( )
val workRequest = OneTimeWorkRequestBuilder < FlwtchWorker > ( ) . setInputData ( data ) . build ( )
WorkManager . getInstance ( applicationContext ) . enqueue ( workRequest )
}
override fun onStop ( ) {
super . onStop ( )
Log . i ( " MainActivity.kt " , " onStop " )
if ( myReceiver != null ) {
LocalBroadcastManager . getInstance ( applicationContext ) . unregisterReceiver ( myReceiver !! ) ;
myReceiver = null ;
}
}
override fun onDestroy ( ) {
super . onDestroy ( )
Log . i ( " MainActivity.kt " , " onDestroy - cancelling all WORKER_TAG and pruning old work " )
WorkManager . getInstance ( this ) . cancelAllWorkByTag ( WORKER _TAG )
WorkManager . getInstance ( this ) . pruneWork ( )
}
class AppbusEvent ( json : String ) : JSONObject ( json ) {
val EventType = this . optString ( " EventType " )
val EventID = this . optString ( " EventID " )
val Data = this . optString ( " Data " )
}
// MainActivity.MyBroadcastReceiver receives events from the Cwtch service via im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS Android local broadcast intents
// then it forwards them to the flutter ui engine using the CWTCH_EVENTBUS methodchannel
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 )
}
}
}