I've just released an update to my app which replaces the deprecated StartActivityForResult
with ActivityResultLauncher
and, despite a couple of weeks of beta testing and a few months of internal testing, I am getting crashes reported in the Google Play Dev Console.
Teh crash has happened on fiour different devices ( 3 x Samsung, 1 x Motorola) and on Android 11, 12 and 13. Google's stats say it is affecting 30% of users which is obviously very, very worrying!
Previous to this release I would see a single crash maybe once every couple of weeks and they were never to do with the method in question.
I am using ActivityResultLauncher
to open a new Activity
that contains a web browser, and to take the URL of the last browsed page as the result passed back to the calling activity.
The ActivityResultLauncher
and the Intent that it launches are declared at the class level with:
private final Intent goto_browse_scr = new Intent();
ActivityResultLauncher<Intent> browseActivityResultLauncher;
In onCreate()
I then register the Activity for the result with:
browseActivityResultLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
String browsedURL = "";
Intent _data = result.getData();
if (_data != null && _data.getStringExtra("browsedURL") != null) browsedURL = _data.getStringExtra("browsedURL");
if (result.getResultCode() == RESULT_OK) viewBinding.sourceString.setText(browsedURL);
}
);
The crash is happening in the following method, which is called from a UI button click:
private void startBrowseActivity() {
closeDrawerIfOpen();
goto_browse_scr.setAction(Intent.ACTION_VIEW);
goto_browse_scr.setClass(getApplicationContext(), BrowseActivity.class);
// Work out what to start with
String startURL;
if ( *logic to determine start URL* ) {
// snipped
} else {
startURL = "";
}
goto_browse_scr.putExtra("startURL",startURL);
browseActivityResultLauncher.launch(goto_browse_scr);
}
The stack trace doesn't give me much info, other than that the last line here is causing the NPE.
Exception java.lang.RuntimeException:
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:3502)
at android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:3670)
at android.app.servertransaction.LaunchActivityItem.execute (LaunchActivityItem.java:85)
at android.app.servertransaction.TransactionExecutor.executeCallbacks (TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2126)
at android.os.Handler.dispatchMessage (Handler.java:106)
at android.os.Looper.loop (Looper.java:250)
at android.app.ActivityThread.main (ActivityThread.java:7766)
at java.lang.reflect.Method.invoke (Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:604)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:958)
Caused by java.lang.NullPointerException:
at androidx.activity.result.ActivityResultLauncher.launch (ActivityResultLauncher.java:102)
at com.myAppID.MainActivity.startBrowseActivity (MainActivity.java:102)
at com.myAppID.MainActivity.onCreate (MainActivity.java:4)
at android.app.Activity.performCreate (Activity.java:8098)
at android.app.Activity.performCreate (Activity.java:8082)
at android.app.Instrumentation.callActivityOnCreate (Instrumentation.java:1309)
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:3475)
I considered moving the Intent
declaration into the startBrowseActivity
method as this is the only place the Intent is used (the ActivityResultLauncher is used in onCreate
so that can't move (or maybe it can?)).
My big problem is that I can't replicate the crash myself. I have tried on a couple of physical devices and three AVDs, but the browse activity launches and completes as expected every time.
So, I am left with trying to figure out where I could possibly have a null pointer in this call.
In Android Studio, adding a null check to goto_browse_scr
just before the .launch
call shows a warning that this object is never null.
However, there is no such warning for browseActivityResultLauncher
- so could this really be the culprit? How could this be null when it is set up in the onCreate()
method? And if this is not likely to be the culprit, what else could be?
CodePudding user response:
With a custom ActivityResultContract that accepts a non-null Unit input, you may utilize the parameter-free launch() extension method. Because we internally call launch() with 'null' rather than 'Unit,' this call now fails.
Note: "A 'NullPointerException' will no longer be thrown when using the launch() method extension with a custom 'ActivityResultContract' that contains a 'Unit' input.
*/
public fun ActivityResultLauncher<Void?>.launch(options: ActivityOptionsCompat? = null) {
launch(null, options)
}
/**
* Convenience method to launch a no-argument registered call without needing to pass in `Unit`.
*/
@JvmName("launchUnit")
public fun ActivityResultLauncher<Unit>.launch(options: ActivityOptionsCompat? = null) {
launch(Unit, options)
}
CodePudding user response:
package androidx.activity.result
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.app.ActivityOptionsCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class ActivityResultLauncherTest {
@Test
fun testUnitLaunch() {
val expectedResult = "result"
val registry = object : ActivityResultRegistry() {
override fun <I : Any?, O : Any?> onLaunch(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?
) {
contract.createIntent(InstrumentationRegistry.getInstrumentation().context, input)
dispatchResult(requestCode, expectedResult)
}
}
val contract = object : ActivityResultContract<Unit, String?>() {
override fun createIntent(context: Context, input: Unit) = Intent()
override fun parseResult(resultCode: Int, intent: Intent?) = ""
}
var actualResult: String? = null
val launcher = registry.register("key", contract) {
actualResult = it
}
launcher.launch()
assertThat(actualResult).isEqualTo(expectedResult)
}
}