Fixed
Status Update
Comments
il...@google.com <il...@google.com>
jb...@google.com <jb...@google.com>
ap...@google.com <ap...@google.com> #2
Project: platform/frameworks/support
Branch: androidx-master-dev
commit d0fdd32e0bbd6bb79aeac439be93f477ab5fad13
Author: Ian Lake <ilake@google.com>
Date: Tue Mar 24 11:48:48 2020
Wait until CREATED before generating an ActivityResult key
Fragments use their internal unique key (mWho)
as part of the key they pass to ActivityResultRegistry.
This key is saved by FragmentManager and restored immediately
after the Fragment is constructed, but often prepareCall()
is called when declaring a member variable, which means it
runs as part of that initialization and before the mWho
is properly restored.
By using the wrong key on recreation, the Fragment's callback
is not properly hooked up to the ActivityResultRegistry
when onActivityResult() is fired.
Hardened the ActivityResultRegistry to store the result
correctly rather than crash with a NullPointerException.
Test: ran in sample app
BUG: 152137004
Change-Id: I40a6a935a47ec642129147c176276f467b66d10e
M activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
M fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
https://android-review.googlesource.com/1267626
Branch: androidx-master-dev
commit d0fdd32e0bbd6bb79aeac439be93f477ab5fad13
Author: Ian Lake <ilake@google.com>
Date: Tue Mar 24 11:48:48 2020
Wait until CREATED before generating an ActivityResult key
Fragments use their internal unique key (mWho)
as part of the key they pass to ActivityResultRegistry.
This key is saved by FragmentManager and restored immediately
after the Fragment is constructed, but often prepareCall()
is called when declaring a member variable, which means it
runs as part of that initialization and before the mWho
is properly restored.
By using the wrong key on recreation, the Fragment's callback
is not properly hooked up to the ActivityResultRegistry
when onActivityResult() is fired.
Hardened the ActivityResultRegistry to store the result
correctly rather than crash with a NullPointerException.
Test: ran in sample app
BUG: 152137004
Change-Id: I40a6a935a47ec642129147c176276f467b66d10e
M activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
M fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
il...@google.com <il...@google.com> #3
This has been fixed internally and will be available in the upcoming Fragment 1.3.0-alpha03
and Activity 1.2.0-alpah03
.
Description
Version used: androidx.fragment:fragment-ktx:1.3.0-alpha02
Devices/Android versions reproduced on: Emulator API level 29
When using the Activity Result API in a fragment, it crashes when receiving a result form an activity in a different configuration than when it was launched. The steps are like this:
1. Launch first activity.
2. Call second activity for result
3. Reorient the device/emulator
4. Finish the second activity (back button)
5. Crash with this stack:
```
2020-03-22 08:55:07.407 6338-6338/my.testapp E/AndroidRuntime: FATAL EXCEPTION: main
Process: my.testapp, PID: 6338
java.lang.RuntimeException: Unable to resume activity {my.testapp/my.testapp.activity.main.MainActivity}: java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=0, result=0, data=null} to activity {my.testapp/my.testapp.activity.main.MainActivity}: java.lang.NullPointerException: Attempt to read from field 'androidx.activity.result.ActivityResultCallback androidx.activity.result.ActivityResultRegistry$CallbackAndContract.mCallback' on a null object reference
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4205)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4237)
at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
Caused by: java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=0, result=0, data=null} to activity {my.testapp/my.testapp.activity.main.MainActivity}: java.lang.NullPointerException: Attempt to read from field 'androidx.activity.result.ActivityResultCallback androidx.activity.result.ActivityResultRegistry$CallbackAndContract.mCallback' on a null object reference
at android.app.ActivityThread.deliverResults(ActivityThread.java:4845)
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4192)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4237)
at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
Caused by: java.lang.NullPointerException: Attempt to read from field 'androidx.activity.result.ActivityResultCallback androidx.activity.result.ActivityResultRegistry$CallbackAndContract.mCallback' on a null object reference
at androidx.activity.result.ActivityResultRegistry.doDispatch(ActivityResultRegistry.java:281)
at androidx.activity.result.ActivityResultRegistry.dispatchResult(ActivityResultRegistry.java:275)
at androidx.activity.ComponentActivity.onActivityResult(ComponentActivity.java:444)
at androidx.fragment.app.FragmentActivity.onActivityResult(FragmentActivity.java:181)
at android.app.Activity.dispatchActivityResult(Activity.java:8110)
at android.app.ActivityThread.deliverResults(ActivityThread.java:4838)
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4192)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4237)
at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
```
There are no problems if there was no configuration change. Also, oddly, there are no problems if there were two configuration changes (e.g. portrait -> landscape -> portrait).
Code for the fragment:
```kotlin
class MainFragment : Fragment() {
companion object {
fun newInstance() = MainFragment()
}
private val viewModel: MainViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = MainFragmentBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observeViewModel()
}
private fun observeViewModel() {
// This is just effectively a listener on button press delivered through a BroadcastChannel
LifecycleChannelListener(viewLifecycleOwner.lifecycle, viewModel.navigateToFirebaseUi) {
onSignInButtonClicked()
}
}
private val onSignInButtonClicked = prepareCall(StartActivity()) { result ->
Log.d("@@@@", "R: $result")
}
private inner class StartActivity: ActivityResultContract<Unit, Int>() {
override fun createIntent(input: Unit?): Intent {
return Intent(requireContext(), MainActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): Int {
return 0
}
}
}
```