I have a fragment in my activity, that it will get some data on its onActivityResult(), And try to update the UI. This is my onActivityResult() code:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
GET_USER_AUDIO_TO_TEXT_REQUEST_CODE -> {
if (resultCode == RESULT_OK) {
data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.let {
val receivedCityName = it[0]
binding.homeCityNameEditTextView.setText(receivedCityName)//Update UI at this line
viewModel.fetchWeatherCity(receivedCityName)
}
} else {
Toast.makeText(activity, R.string.error_while_getting_data_from_recognizer, Toast.LENGTH_SHORT).show()
}
}
}
}
So I tried to create some Instrumentation test with the Espresso for this part as bellow:
#Test
fun checkActionsAfterReceivingDataInOnActivityResult() {
val intent = Intent()
intent.putStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS, arrayListOf(DEFAULT_CITY_NAME))
activityRule.activity.supportFragmentManager
.findFragmentByTag(HomeFragment.CLASS_NAME)
?.onActivityResult(
HomeFragment.GET_USER_AUDIO_TO_TEXT_REQUEST_CODE,
Activity.RESULT_OK,
intent
)
}
But I will get an error after running this Test method that says:
This error is because of my ViewModel methods that will run. by calling this line in onActivityResult():
viewModel.fetchWeatherCity(receivedCityName)
This is its error:
java.lang.IllegalStateException: Cannot invoke setValue on a background thread
at android.arch.lifecycle.LiveData.assertMainThread(LiveData.java:435)
at android.arch.lifecycle.LiveData.setValue(LiveData.java:279)
And because of updating my UI by this line:
binding.homeCityNameEditTextView.setText(receivedCityName)//Update UI at this line
I will get this error too:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
Test instrumentation runs on a different thread, so wrap up the test method-body with run on UI thread.
activityRule.activity.runOnUiThread {
// the test method body goes in here
}
In your case:-
activityRule.activity.runOnUiThread {
activityRule.activity.supportFragmentManager
.findFragmentByTag(HomeFragment.CLASS_NAME)
?.onActivityResult(
HomeFragment.GET_USER_AUDIO_TO_TEXT_REQUEST_CODE,
Activity.RESULT_OK,
intent)
}
You shouldn't invoke methods such as onActivityResult directly in your Espresso test. You will always get the same error because these tests are executed on different threads. If you want to test it correctly, you should consider using Espresso-Intents:
#Test
fun checkActionsAfterReceivingDataInOnActivityResult() {
val intent = Intent()
intent.putStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS, arrayListOf(DEFAULT_CITY_NAME))
intending(hasComponent(YourActivity.class.getName())).respondWith(ActivityResult(Activity.RESULT_OK, intent))
// onView(...).perform(click())
// First do something to cause your app to launch the intended activity
// onView(...).check(matches(...))
// Then test something on activity result
}
Related
I'm working on a library that has a couple of ready-made activities.
So far i have my activities in the library, and in the main app, i call it normally with registerForActivityResult to start it.
this means whoever is using my library would be able to see the whole activity.
what i would like to do, is to have the developer call a method in the library class and ask it to do an action, and in the library that method would on its own start the activity, register it for result, and return the result to the calling class through an interface.
the below is what i tried but it gives me error LifecycleOwner is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED
private fun launchScannerActivity(activity: FragmentActivity, callback: ScannerCallback) {
val scanResult =
activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
callback.onResult(it.data?.getStringExtra("Some Key") ?: "")
} else {
callback.onFail()
}
}
val intent = Intent(activity, ScannerActivity::class.java)
scanResult.launch(intent);
}
why do i need this:
This library would be an SDK for a SAAS product, so we would like to abstract and obfuscate as much of the implementation as possible from our clients.
You can't really communicate between Activities using interfaces, at least not in a way that is somewhat concise and isn't very prone to leaking. What you can do is expose your own Activity result contract. Then your API could be as simple as some of the ones in ActivityResultContracts. You can look at the source code there to see how to implement it.
Maybe something like this:
class ScannerResultContract : ActivityResultContract<Unit, String?>() {
override fun createIntent(context: Context, input: Unit?): Intent {
return Intent(context, ScannerActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): String? {
return if (resultCode == Activity.RESULT_OK) {
intent?.getStringExtra("Some Key")
} else {
null
}
}
}
Client usage:
// In activity or fragment:
val getScannerResult = registerForActivityResult(ScannerResultContract()) { resultString ->
if (resultString != null) {
// use it
} else {
// log no result returned
}
}
//elsewhere:
someListener.setOnClickListener {
getScannerResult.launch()
}
I am working on NGenious payment SDK and trying to implement it in my Kotlin project but when the SDK shows up in Fragment, after 2 to 3 seconds the screen automatically got white and nothing is being shown.
This is the payment SDK
Method to launch payment Fragment
activity?.let {
PaymentClient(it).launchCardPayment(
CardPaymentRequest.builder()
.gatewayUrl(paymentOption.paymentCreateOrderResponse.data?.links?.paymentAuthorization?.href?:"")
.code(Constants.currencyCode)
.build(), CARD_PAYMENT_REQUEST_CODE)
}
onActivityResult Method
override fun onActivityResult(
requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.e("onActivityResult","onActivityResult" + resultCode)
if (requestCode == CARD_PAYMENT_REQUEST_CODE) {
when (resultCode) {
Activity.RESULT_OK -> onCardPaymentResponse(CardPaymentData.getFromIntent(data!!))
Activity.RESULT_CANCELED -> onCardPaymentCancelled()
}
Log.e("resultCode CCPF","" + resultCode)
}
Log.e("CPRC CCPF","" + CARD_PAYMENT_REQUEST_CODE)
}
As I review your CardPaymentRequest builder you are sending the Currency Code in the code field thats why they throwing an exception on it.
pass payment code in the code field, you can get the code by this field.
paymentOption.paymentCreateOrderResponse.data?.links?.payment?.href?.split("=")!!.get(1)
Now run the code like that.
activity?.let {
PaymentClient(it).launchCardPayment(request = CardPaymentRequest.builder()
.gatewayUrl(paymentOption.paymentCreateOrderResponse.data?.links?.paymentAuthorization?.href?:"")
.code(paymentOption.paymentCreateOrderResponse.data?.links?.payment?.href?.split("=")!!.get(1))
.build(), requestCode = CARD_PAYMENT_REQUEST_CODE)
}
I have 2 apps, one a very simple toy app that exists to call the other:
const val AUTHENTICATE_CODE = 42
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
fab.setOnClickListener {
Intent(Intent.ACTION_VIEW, Uri.parse("testapp://hello.world/")) //2nd app has intent filter to intercept this.
.also { intent -> startActivityForResult(intent, AUTHENTICATE_CODE) }
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val textView = findViewById<TextView>(R.id.hello_text)
if (requestCode == AUTHENTICATE_CODE && resultCode == Activity.RESULT_OK) {
requireNotNull(data) {
textView.text = "Error: Intent was null. Data lost."
return#onActivityResult
}
val dataExtra = data.getStringExtra("com.example.app.DATA")
requireNotNull(dataExtra){
textView.text = "Error: Intent did not contain data."
return#onActivityResult
}
Log.d("TestAppPlsIgnore", "Result Intent received")
textView.text = "Success! $dataExtra"
} else {
textView.text = "Something went wrong. Request = $requestCode; Result = $resultCode"
}
}
//...
}
The other app is a little more involved:
The activity in app 2 that the toy app launches implements the navigation library from Jetpack.
Most of the fragments that are in that activity's nav graph implement the same ViewModel. i.e. private val mainViewModel by activityViewModels<MainActivityViewModel>()
Inside the MainActivityViewModel is a LiveData<String> that we'll call data. The MainActivity of app 2 has an observer watching data similar to this:
val dataObserver = Observer<String> { data ->
val result = Intent()
result.putExtra("com.example.app.DATA", data)
Log.d("MainActivity.DataObserver", "Sending data $data")
setResult(Activity.RESULT_OK, result)
finish()
}
mainViewModel.data.observe(this, dataObserver)
In the general flow to get to a point where a string is put into data, the navigation view of the main activity will likely navigate between one or more fragments.
The expected result: When a string is added to data in app 2, the observer will create the result intent, set it as the result, and finish app 2. App 1 will receive the result and call onActivityResult, and we should display "Success!" plus some data.
What I get: The observer does work. The log statement shows the correct data was received by the observer. App 2 finishes. And app 1's onActivityResult displays the fail case, showing the correct request code, but a response code == Activity.RESULT_CANCELLED. If the requireNotNull(data) statement is moved outside the if statement, app 1 will instead show that the intent returned was null.
My questions:
RESULT_CANCELLED is not being explicitly returned, and I am attempting to return an intent with data. So that should only leave the activity crashing as a reason why RESULT_CANCELLED is being returned. Navigating across a nav graph will inevitably cause some fragments to reach the end of their lifecycle. Would Android confuse that for an activity crashing?
Why is there a null intent when onActivityResult is being called? For the most part, I'm just following what's outlined in the documentation, if a bit more verbosely.
Is this not the right way to send a simple string between two specific apps? I don't want to use share intents, because this is meant to be a more direct communication between specific apps rather than a broad communication between my app and a category of apps.
It turns out that you should not call finishAfterTransition() elsewhere in an app if you plan to use an Observer-based setup like mine to send data through startActivityForResult(). finishAfterTransition() causes a conflict with any calls to finish(), and you'll send a null result and a ResultCode of RESULT_CANCELLED.
I am trying to capture an image from the camera and display it on the imageView. I tried but getting the error "override method should call super.onActivityResult", you can see my code below. Please let me know if I am doing it right.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == TAKE_PICTURE && resultCode == Activity.RESULT_OK) {
try{
val file= File(currentPath)
val uri = Uri.fromFile(file)
val imageView = findViewById<ImageView>(R.id.imageView)
imageView.setImageURI(uri)
}catch(e:IOException){
e.printStackTrace()
}
}
if (requestCode == PICK_PICTURE && resultCode == Activity.RESULT_OK) {
try{
val uri = data!!.data
val imageView = findViewById<ImageView>(R.id.imageView)
imageView.setImageURI(uri)
}catch(e:IOException){
e.printStackTrace()
}
}
}
add super.onActivityResult(requestCode, resultCode, data); as first line on the onActivityResult method.
What does it mean?
When a class extends another, say you have class A and class B : A
In this context, class A is the super class of B.
So if class A has a method called:
fun someMethod()
you can do:
var myB = B()
myB.someMethod()
this effectively calls the code in A, because B extends A, and someMethod is not private.
Now if you want to modify someMethod's behavior in B, you override it...
override fun someMethod() {
// b does something different here
}
now, you can mark A's someMethod as #CallSuper (an annotation coming from here.
In which case you get the warning/error that B must call its super (know as Parent too) class too...
so B must now do:
override fun someMethod() {
super.someMethod()
// your B code too..
}
There's no rule to call it at the beginning you can call it at any time, as long as you do before the end of the function. In some instances, it's desired to call it as the first thing (if, for instance, A does something you need in B as well), and in some other cases, you want to wait for B to do something before calling super.... As long as you do.
onActivityResult is marked as such, and therefore you must call super.
I have a thirdparty Android control that can't be used in Flutter directly. I put it in an Android activity. Then, using information from https://flutter.io/docs/development/platform-integration/platform-channels#step-3b-add-an-android-platform-specific-implementation-using-kotlin, I can successfully launch that activity and perform some actions there. The only part that doesn't work is sending results back from the activity.
Flutter code:
void showDialog() async
{
try {
final Map<String, List<double>> result = await platform.invokeMethod('show_dialog',
<String, String>{
'address': widget.user.address
});
widget.user.address = result.keys.toList()[0];
} on PlatformException catch (e) {
print('Failed to pick address: ${e.message}.');
}
}
Android code:
class MainActivity: FlutterActivity() {
private val CHANNEL = "dialog"
private lateinit var _result: MethodChannel.Result
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "show_dialog") {
_result = result
val intent = Intent(this#MainActivity, DialogActivity::class.java)
intent.putExtra("address", call.argument<String>("address"))
startActivityForResult(intent, 9689)
}
else result.notImplemented()
}
}
override fun onActivityResult(requestCode: Int, result: Int, intent: Intent?) {
if(requestCode != 9689)
return super.onActivityResult(requestCode, result, intent)
if (result == Activity.RESULT_OK) {
_result.success(mapOf(intent!!.getStringExtra("address") to
listOf(intent.getDoubleExtra("latitude", 0.0),
intent.getDoubleExtra("longitude", 0.0))))
}
else
_result.success(null)
}
}
What's the problem? Breakpoint on the line widget.user.address = result.keys.toList()[0]; is never reached, suggesting the result is never sent back.
Turns out the code was almost correct. The Android side didn't need any changes, but on the Flutter side I had to make this change:
turn
final Map<String, List<double>> result = await platform.invokeMethod(
into
final result = await platform.invokeMethod(
i. e. simply remove explicit type from the variable, because the return value of platform.invokeMethod was some kind of an internal hash map (in particular, it's name started with an underscore) rather than that of Map as specified. Flutter didn't show any errors in the console output, because for some reason it only captures Android log with debugger attached to the Android part of the application, and once I figured how to debug Android code in Android Studio, I immediately found the reason.