registerForActivityResult not working when second activity orientation changes - android

I am calling activity B from activity A using the ActivityResultLauncher and setting the result from activity B when the task is done. This works perfectly if orientation is not changed. The problem is when I change orientation from activity B and then setting the result, then registerForActivityResult of activity A is not called. Could someone let me know, what could be the issue?
Note: I do not face this issue if I am using startActivityForResult and onActivityResult. I changed this to ActivityResultLauncher as startActivityForResult became deprecated.
activity A sample code:
private lateinit var launcher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(layout)
setLauncherResult()
}
private fun setLauncherResult() {
launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
//Do operations here
}
}
//On button click starting activity B using launcher
val intent = Intent(activityA, activityB)
launcher.launch(intent)
}
activity B sample code:
//setting result
val intent = Intent()
//set intent extras
setResult(Activity.RESULT_OK, intent)
finish()

You can save the value in onSaveInstanceState() and use setResult in onRestoreInstanceState().

Try moving the registerForActivityResult assignment of launcher to onCreateView() (override it if you haven't already). Putting it here, I'm not getting the "is attempting to register while current state is STARTED" RuntimeException in OnCreateView() as it would with onResume() or later. The advantage of OnCreateView() over OnCreate() seems to be that the launcher variable will get re-set on theme day/night changes as well as just orientation changes.
Also see the Configuration change & Process death section here for more discussion.
I know the official dox say it's safe to set it before anything else in the class, but theme changes broke .launch(), and this location appears to work for me.

Everytime orientation changes it calls onCreate and it resets your views & Other variables like launcher are re-assigned. So you can follow the example shown in official document here: https://developer.android.com/training/basics/intents/result#launch
And pull your launcher assignment out of onCreate as following (This worked for me):
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
//do your stuff here
binding.tv.text = result.data?.getStringExtra("text")
}
}
companion object {
var number : Int = 0
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tv.setOnClickListener {
val intent = Intent(this, ActivityB::class.java)
startForResult.launch(intent)
}
}
}
If you don't want that onCreate should be called again when orientation changes. Then in your Manifest you can add configChanges:
<activity android:name=".MainActivity"
android:configChanges="orientation|screenSize">

Related

Implementing registerForActivityResult from Fragment to dismiss Activity class

I have a calling like
There is a
-> FragmentMain which calls ActivityA
-> ActivityA consists of ActivityA_FragmentA
-> ActivityA_FragmentA calls ActivityB
-> ActivityB consists of ActivityB_FragmentA , ActivityB_FragmentB , ActivityB_FragmentC
ActivityB_FragmentC is final fragment on whose Ok button , I need to dismiss and return to FragmentMain
Current Implementation:
-> On dismiss of ActivityB_FragmentC updates LiveData to which is observed on ActivityB
-> This LiveData on ActivityB Used setResult(1000) in ActivityB and then finishActivity
-> Using onActivityResult() in ActivityA (result code used to compare 1000) {if result code is 1000 then dismiss ActivityA to show FragmentMain }
How to implement this using . registerForActivityResult()
I checked the document for it Doc Link
It says to use Observer and has explanation only for camera fetch .
I needed a simple call where I can pass result code back .. How can I achieve this ?
I want to use registerForActivityResult() in place of onActivityResult() in ActivityA
Trial 1 :
class MyLifecycleObserver(private val registry :
ActivityResultRegistry)
: DefaultLifecycleObserver {
lateinit var getContent : ActivityResultLauncher
override fun onCreate(owner: LifecycleOwner) {
getContent = registry.register("key", owner, ActivityResultContracts.GetContent()) { uri ->
// Handle the returned Uri
}
}
fun selectImage() {
getContent.launch("image/*")
} }
Am trying to pass result/ call from ActivityA_FragmentA to ActivityA but am able to do it using
ActivityResultContracts.StartActivityForResult()

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 {
//...
}

Android seems to be ignoring my setResult call for onActivityResult

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.

Kotlin overridden onBackPressed() function is never called when Action Bar back button is pressed

I've seen this question posed all over the place but I have not seen where anyone has been able to answer why the function doesn't get called.
I've run the app in debug mode with a break point set at the onBackPressed function and it completely ignores it. The compiler even recognizes and changes my red break point into a circle with a line through it.
I have a ContractSelectAdapter with a setOnClickListener passing a selected contract to ContractMenuActivity using putExtra(). Once in the ContractMenuActivity there is a button to open the activity below, UploadImageActivity and while in the activity I want a user to be able to hit the back button to reopen the ContractMenuActivity but within the onCreate function of ContractMenuActivity is a getStringExtra call that causing it to crash because there is no "extra". Which is why I'm trying to use the onBackPressed. In theory it should work perfectly.
I've tried putting a call to the onBackPressed function manually in the onCreate function of UploadImageActivity just to make sure it works and it does. It immediately opens ContractMenuActivity. So the function works it just never gets called for some reason. Extremely frustrating.
the onBackPressed function is at the very end of the UploadImageActivity class (first code excerpt)
Any help would be GREATLY appreciated.
class UploadImageActivity : AppCompatActivity() {
var prefs: Prefs? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_upload_image)
prefs = Prefs(this)
}
override fun onBackPressed()
{
super.onBackPressed()
val returnIntent = Intent(this, ContractMenuActivity::class.java)
returnIntent.putExtra(SELECTED_CONTRACT, prefs!!.rmsAppContractID)
startActivity(returnIntent)
}
}
class ContractMenuActivity : AppCompatActivity()
{
var selectedContract: String = ""
var prefs: Prefs? = null
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_contract_menu)
prefs = Prefs(this)
val btnUploadImage = findViewById<Button>(R.id.btnUploadImage)
selectedContract = intent.getStringExtra(SELECTED_CONTRACT)
prefs!!.rmsAppContractID = this.selectedContract
var contractSelectMsg = "Contract ${prefs!!.rmsAppContractID} selected"
val duration = Toast.LENGTH_SHORT
Toast.makeText(this, contractSelectMsg, duration).show()
btnUploadImage.setOnClickListener {this.openUploadImage()}
}
public fun openUploadImage()
{
val registered = prefs!!.rmsAppRegistered
var message: String
if(registered)
{
val intent = Intent(this, UploadImageActivity::class.java)
startActivity(intent);
}
else
{
message = "You must Pair this device before you can upload an image."
val intent = Intent(this, PostResponseMessageActivity::class.java).apply {
putExtra(POST_RESPONSE_MESSAGE, message)
}
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}
}
Parent name in manifest:
<activity
android:name=".ContractMenuActivity"
android:label="#string/contractMenuLabel"
android:parentActivityName=".ContractSelectActivity">
</activity>
<activity
android:name=".UploadImageActivity"
android:label="#string/uploadImageLabel"
android:parentActivityName=".ContractMenuActivity">
</activity>
super.onBackPressed()
Will finish the current activity. If you want to do some action after user press back button, You must to past your code to top of super.onBackPressed().

Activity screen still here after finish()

I have a particular case where I need to ask something to the user when he starts a printer service.
So from the onStartPrinterDiscovery (so a service), I start an activity to display the dialog and when the action is done, I send a new intent which calls finish() nd so I see that onDestoy() is called.
Unfortunately when I hit the apps history button, I still see my activity's screen behind:
Could you tell me why and how to fix it please?
androidManifest.xml:
<activity
android:name=".DialogActivity"
android:noHistory="true"
android:theme="#android:style/Theme.Dialog"
android:launchMode="singleTop">
</activity>
DialogActivity:
class DialogActivity : Activity() {
var activity:Activity = this
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(R.layout.floatingactivity)
setFinishOnTouchOutside(false)
onNewIntent(intent)
}
override fun onDestroy() {
super.onDestroy()
isDisplayed = false
}
override fun onNewIntent(intent: Intent) {
val extras = intent.extras
val action = extras.getString("action")
when (action) {
"showDialog" -> {
if (!isDisplayed) {
tvMessage.text = getString(R.string.ask_for_action)
isDisplayed = true
}
}
"showErrorDialog" -> {
if (!isDisplayed) {
tvMessage.text = getString(R.string.error_action)
isDisplayed = true
}
}
"dismissDialog" -> { activity.finish() }
else -> {}
}
if (isDisplayed) {
btCancel.setOnClickListener {
activity.finish()
}
}
}
companion object {
var isDisplayed = false
}
}
EDIT I add how I currently create my Intent because of one answer which could be a solution:
val intent = Intent(applicationContext, DialogActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
intent.putExtra("action","showDialog")
startActivity(intent)
Add the android:excludeFromRecents to your activity attributes, like so:
<activity
android:name=".DialogActivity"
android:theme="#android:style/Theme.Dialog"
android:excludeFromRecents="true"
android:launchMode="singleTop">
</activity>
This is from the docs of the android:excludeFromRecents:
Whether or not the task initiated by this activity should be excluded from the list of recently used applications, the overview screen. That is, when this activity is the root activity of a new task, this attribute determines whether the task should not appear in the list of recent apps. Set "true" if the task should be excluded from the list; set "false" if it should be included. The default value is "false".
The android:noHistory="true" is for different purposes. This is from the docs of the android:noHistory:
A value of "true" means that the activity will not leave a historical trace. It will not remain in the activity stack for the task, so the user will not be able to return to it. In this case, onActivityResult() is never called if you start another activity for a result from this activity.
Try this:-
Intent i = new Intent(this,YourFirstActivity.Class);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(i);
finish();
Or
use finishAffinity() suitable for >= API 16.

Categories

Resources