How to start an activity from flutter plugin using an API - android

So I am making a Flutter plugin and I am attempting to run Kotlin code on Android. The problem is, this code runs a method which attempts to start an activity without the FLAG_ACTIVITY_NEW_TASK flag on the intent. The problem with this is that it also does NOT have a way to give it an intent instance as it attempts to instantiate an instance inside the method itself. The method expects to be called from a button or other method that is stored on the UI and called from it. However, since it is called from the onMethodCall method in the Flutter plugin, it does not seem to work. I have attempted many workarounds such as adding a method inside the Activity and running the code inside while calling it from the flutter plugin class. I have also tried using the UIThread and no luck either. Any workarounds?
Note: I have not provided any code due to keeping this API hidden. It should only be known that I am running the code from the onMethodCall event.
Error: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

You can extend your plugin to implement ActivityAware in your plugin class, when you implement it, you get a couple of callbacks that gives you the current activity. Like this :
lateinit activity: Activity? = null
override fun onDetachedFromActivity() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
After that you can just startActivity from the assigned activity variable.
Let me know if you need further help.

As you mentioned, For Flutter plugin any platform-dependent logics should be kept in the subclass of FlutterActivity which was used to show flutter module/screens inside a native module. Now you can launch intent from that subclass without any additional flags.
#note - Subclass of FlutterActvity should be kept in the native module.
class FlutterResponseActivity : FlutterActivity() {
private var methodResult: Result? = null
override fun provideFlutterEngine(context: Context): FlutterEngine? {
return MyApplication.mContext.flutterEngine //Pre-warmed flutter engine
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"startMainActivity" -> {
startMainActivity()
result.success(true)
}
else -> result.notImplemented()
}
}
}
private fun startMainActivity() {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
}

Related

How to fix android run time error in Stripe Terminal discover device code build using kotlin

I am trying to integrate stripe terminal code with my android app build using kotlin, unfortunately I am getting the following run time error which I could not able to fix
java.lang.IllegalStateException: initTerminal must be called before attempting to get the instance
The code I have added is used below
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pay_screen)
onDiscoverReaders()
}
fun onDiscoverReaders() {
val config = DiscoveryConfiguration(
timeout = 0,
discoveryMethod = DiscoveryMethod.LOCAL_MOBILE,
isSimulated = false,
location = "xxxxxxxxxxxxxxx"
)
// Save this cancelable to an instance variable
discoveryCancelable = Terminal.getInstance().discoverReaders(config,
discoveryListener = object : DiscoveryListener {
override fun onUpdateDiscoveredReaders(readers: List<Reader>) {
}
}
, object : Callback {
override fun onSuccess() {
println("Finished discovering readers")
}
override fun onFailure(e: TerminalException) {
e.printStackTrace()
}
})
}
I have added this to one of my activity and my intention is to check if my phone is supporting stripe tap on mobile
I guess the issue could be calling onDiscoverReaders() from a wrong place, someone please help me to fix this issue
Thanks in advance
In stripe docs you can check
// Create your listener object. Override any methods that you want to be notified about
val listener = object : TerminalListener {
}
// Choose the level of messages that should be logged to your console
val logLevel = LogLevel.VERBOSE
// Create your token provider.
val tokenProvider = TokenProvider()
// Pass in the current application context, your desired logging level, your token provider, and the listener you created
if (!Terminal.isInitialized()) {
Terminal.initTerminal(applicationContext, logLevel, tokenProvider, listener)
}
// Since the Terminal is a singleton, you can call getInstance whenever you need it
Terminal.getInstance()
might be you missed to initialise terminal before getting Instance so try add above code before onDiscoverReaders()
The error speaks for itself - first you need to initialize the api terminal, and then call the terminal instance.
Based on the documentation, we follow the following steps to get started with the api terminal:
Initialize the terminal application in the application class of the
application
class App : Application() {
override fun onCreate() {
super.onCreate()
TerminalApplicationDelegate.onCreate(this)
}
}
We request the necessary permissions for correct work with the
terminal search (bluetooth, geolocation), if everything is provided,
we call the init terminal with parameters like that:
Terminal.initTerminal(
context = context,
logLevel = LogLevel.VERBOSE,
tokenProvider = TokenProvider(),
listener = object : TerminalListener {
override fun onUnexpectedReaderDisconnect(reader: Reader) {
Log.d("log", "onUnexpectedReaderDisconnect")
}
override fun onConnectionStatusChange(status: ConnectionStatus) {
super.onConnectionStatusChange(status)
Log.d("log", "onConnectionStatusChange")
}
override fun onPaymentStatusChange(status: PaymentStatus) {
super.onPaymentStatusChange(status)
Log.d("log", "onPaymentStatusChange")
}
}
)
After this initialization, you can call the terminal instance and
work with it.

FlutterActivity not working when using cached engine

I am trying to open FlutterActivity in my existing android application. Before I was creating new flutter engine every time I was opening activity like this:
FlutterActivity
.withNewEngine()
.build(context)
And everything was working fine besides a little lag while opening the activity. To get rid of the lag I wanted to switch to using cached engine. I followed this official tutorial: LINK
And ended up with smething like this:
In my Application class:
class App : Application() {
lateinit var flutterEngine: FlutterEngine
override fun onCreate() {
...
flutterEngine = FlutterEngine(this)
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
FlutterEngineCache
.getInstance()
.put("myEngineId", flutterEngine)
}
}
And later in my application on the button click, in the same place that I was successfully opening FlutterActivity:
FlutterActivity
.withCachedEngine("myEngineId")
.build(context)
So I basically followed the all the instructions but the effect that I get now is after the button click there is even longer lag than before and then there is only black screen being displayed. My flutter screen is not displayed and application is kind of frozen I can't go back or do anything. There is also no error or any useful info in the logs. I have no idea what is going on. What am I doing wrong?
To Use cached FlutterEngine
In FlutterActivity you must declare provideFlutterEngine method.
class DemoActivity : FlutterActivity() {
override fun provideFlutterEngine(context: Context): FlutterEngine? =
FlutterEngineCache.getInstance().get(FlutterConstants.ENGINE_ID)
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "demo-channel")
.setMethodCallHandler { call, result ->
if (call.method == "demo-method") {
demoMethod()
result.success(null)
} else {
result.notImplemented()
}
}
}
private fun demoMethod() {
// Do native code
}
}

registerForActivityResult() outside onCreate() with Compose

So I am trying to launch the intent Intent.ACTION_OPEN_DOCUMENT. I first tried with startActivityForResult but I noticed it was depreciated so I tried to find another way to do this. So I found the registerForActivityResult method but it turns out it must run after onCreate() has finished :
Note: While it is safe to call registerForActivityResult() before your fragment or activity is created, you cannot launch the ActivityResultLauncher until the fragment or activity's Lifecycle has reached CREATED.
Since I am using Jetpack Compose and setContent is in onCreate() my Activity has actually never finished creating because all my Composables functions are run in the setContent of my MainActivity
So how can I achieve this ?
Using the latest version of activity-compose you can use rememberLauncherForActivityResult() to register a request to Activity#startActivityForResult.
Something like:
val result = remember { mutableStateOf<Uri?>(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
result.value = it
}
Button(onClick = { launcher.launch(arrayOf("application/pdf")) }) {
Text(text = "Open Document")
}
result.value?.let {
//...
}

#BeforeAll not functioning as intended in JUnit5

I'm running UI testing on Android devices using Appium. We recently migrated to JUnit5 and I'm attempting to utilize the #BeforeAll class to make sure the app is in a good state before we continue to the next class.
Currently, the tooltip in Android studio is indicating that the function is never used. In the log I'm seeing a junitException saying that the method must be static. I haven't implemented #TestInstance yet, I'd like to be able to use beforeAll without it for now. I'm just confused why it isn't working since my #beforeEach and #afterEach are both working. The error and code are below.
org.junit.platform.commons.JUnitException: #BeforeAll method 'public final void com.bypass.automation.BaseTest.healthcheck()' must be static unless the test class is annotated with #TestInstance(Lifecycle.PER_CLASS).
open class BaseTest {
lateinit var driver: AndroidDriver<MobileElement>
private val capabilities = DesiredCapabilities().apply {
setCapability(APPIUM_VERSION, "1.19.1")
setCapability(PLATFORM_NAME, "Android")
setCapability(DEVICE_NAME, "Android")
setCapability("appPackage", "com.ourpackage")
setCapability("appActivity", "com.ourpackage.PassthroughHomeActivity")
setCapability("automationName", "uiautomator2")
setCapability("skipDeviceInitialization", true)
setCapability("noReset", true)
setCapability("full-reset", false)
setCapability("enableMultiWindows", false)
setCapability("unlockType", "pin")
setCapability("unlockKey", "0000")
setCapability("newCommandTimeout", "120")
}
#BeforeAll
fun healthcheck() {
val currentActivity = driver.currentActivity()
println("Current activity is $currentActivity")
if (currentActivity.contains("StationSecurePayActivity")) {
println("Exiting Station Pay")
CreditCardEntryView(driver).clickBackButton()
}
when {
currentActivity.contains("kiosk") -> {
Thread.sleep(2000)
println("Exiting Kiosk")
KioskView(driver).exitKiosk()
println("Logging out")
LogInProviderUtil(driver).logOut()
}
currentActivity != ".LoginActivity" -> {
println("Logging out")
LogInProviderUtil(driver).logOut()
}
currentActivity.contains(".LoginActivity") -> {
println("Session was properly logged out. No action taken.")
}
}
}
#BeforeEach
fun setup() {
driver = AndroidDriver(URL("http://127.0.0.1:4750/wd/hub"), capabilities)
driver.manage()?.timeouts()?.implicitlyWait(30, SECONDS)
if (LogInProviderUtil(driver).isLoggedIn()){
LogInProviderUtil(driver).logOut()
}
}
#AfterEach
fun teardown() {
if (LogInProviderUtil(driver).isLoggedIn()){
LogInProviderUtil(driver).logOut()
driver.quit()
}
else {
driver.quit()
}
}
}
It will work. I believe that any method annotated with #BeforeAll must be static (unless the "per-class" test instance lifecycle is used). So it sounds to me like you should switch to that by adding this annotation to your test class: #TestInstance(Lifecycle.PER_CLASS)
Also, it is usual practice to make your setup and teardown methods public. Also, I recommend use of Selenium-Jupiter framework (https://github.com/bonigarcia/selenium-jupiter/blob/master/README.md#appium) . Good luck.
If you want to have an initialization block you may put it simply into
init{} method. And you don't have to annotate it.

Could registerForActivityResult be used in a coroutine?

The problem with this as I see it is that you have to guarantee that registerForActivityResult() is called before your own activity's OnCreate() completes. OnCreate() is obviously not a suspending function, so I can't wrap registerForActivityResult() and ActivityResultLauncher.launch() in a suspendCoroutine{} to wait for the callback, as I can't launch the suspendCoroutine from OnCreate and wait for it to finish before letting OnCreate complete...
...which I did think I might be able to do using runBlocking{}, but I have found that invoking runBlocking inside OnCreate causes the app to hang forever without ever running the code inside the runBlocking{} block.
So my question is whether runBlocking{} is the correct answer but I am using it wrong, or whether there is some other way to use registerForActivityResult() in a coroutine, or whether it is simply not possible at all.
You can do something like this.
Please refer to the implementation below.
class RequestPermission(activity: ComponentActivity) {
private var requestPermissionContinuation: CancellableContinuation<Boolean>? = null
#SuppressLint("MissingPermission")
private val requestFineLocationPermissionLauncher =
activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
requestPermissionContinuation?.resumeWith(Result.success(isGranted))
}
suspend operator fun invoke(permission: String) = suspendCancellableCoroutine<Boolean> { continuation ->
requestPermissionContinuation = continuation
requestFineLocationPermissionLauncher.launch(permission)
continuation.invokeOnCancellation {
requestPermissionContinuation = null
}
}
}
Make sure you initialize this class before onStart of the activity. registerForActivityResult API should be called before onStart of the activity. Refer to the sample below
class SampleActivity : AppCompatActivity() {
val requestPermission: RequestPermission = RequestPermission(this)
override fun onResume() {
super.onResume()
lifecycleScope.launch {
val isGranted = requestPermission(Manifest.permission.ACCESS_FINE_LOCATION)
//Do your actions here
}
}
}

Categories

Resources