·10 min read

HsdpShimActivity: targetPackageName is null

A Crashlytics crash inside Google's hsdp library, reproduced on an emulator and fixed by forcing hsdp 2.2.0. Three more like it point to a tester.

androidadmobcapacitorgradle

Contents

Crashlytics showed me a crash in ASMR Logo Craft Block that had none of my code in it:

Fatal Exception: java.lang.IllegalStateException: targetPackageName is null
  at com.google.android.play.core.hsdp.service.HsdpShimActivity.zzd(com.google.android.play:hsdp@@2.0.1:5)
  at com.google.android.play.core.hsdp.service.HsdpShimActivity.onAttachedToWindow(com.google.android.play:hsdp@@2.0.1:3)
  at com.android.internal.policy.DecorView.onAttachedToWindow(DecorView.java:1712)
  at android.view.View.dispatchAttachedToWindow(View.java:20483)
  ...

The game is a Capacitor app, a web game in a WebView with native plugins for ads, billing and Play Games. I had never heard of hsdp. Working through the crash list after it, I found three more issues of the same flavour, all in Google libraries. The hsdp crash got a fix. The others got a theory about where all of them came from, and the theory matters more than the fix, so this post is about both.

Where hsdp comes from

It is not something I added. play-services-ads brings it in through play-services-ads-api, and the dependency tree shows the chain:

play-services-ads:25.4.0 → play-services-ads-api:25.4.0 → com.google.android.play:hsdp:2.0.1

Its job is the Play install overlay that opens when a player taps an app-install ad. If you want to know which hsdp a build actually ships, the APK says so:

unzip -p app-release.apk hsdp.properties
# version=2.0.1

HsdpShimActivity is declared android:exported="false" in the library's manifest. Nothing outside the app can start it, and inside the app only Google's ads SDK does.

What Crashlytics' AI said

Crashlytics attaches a Gemini analysis to each issue. For this one it offered three causes: the Play Core SDK was given the wrong Context and "lost its reference to the package manager", R8 stripped something it needed, or manifest merging dropped the activity. The fixes followed from those: pass applicationContext to AppUpdateManagerFactory, add -keep class com.google.android.play.core.hsdp.** { *; }, and wrap the calls in a try/catch.

None of it fits, and you can tell from the library itself. I had Claude Code pull the class out of the Gradle cache and disassemble it with javap. zzd() does this, in order:

  1. get the window token, throw windowToken is null if there isn't one
  2. read the string extra target_package_name from the intent that started the activity
  3. throw targetPackageName is null if that extra is missing

So targetPackageName is the package of the advertised app, the one the player would install from the ad, read out of an intent extra. It has nothing to do with my package name or which context anything was given. The activity is present in the merged manifest, or Android could not have started it. And there is nothing to wrap in a try/catch: the throw happens in onAttachedToWindow, a callback Android makes into Google's activity, with no frame of mine on the stack.

The analysis reads well and names real Android failure modes. It just names the ones that usually go with an error like this, not the ones this bytecode can produce. One of its suggestions, "update Play Core dependencies", turned out to be right, but not in the way it meant.

Reproducing it on purpose

If the crash is "this activity started without its extra", I can start it without its extra. adb's shell user isn't allowed to start a non-exported activity, so I made a temporary emulator from a google_apis system image (the Play Store images won't let adb root through), switched adb to root, installed that day's build with hsdp 2.0.1 in it, and launched the activity by name:

adb root
adb install -r app-before.apk
adb shell am start -n com.example.game/com.google.android.play.core.hsdp.service.HsdpShimActivity
I/HsdpShimActivity( 3159): shim activity onAttachedToWindow
E/AndroidRuntime( 3159): FATAL EXCEPTION: main
E/AndroidRuntime( 3159): java.lang.IllegalStateException: targetPackageName is null
E/AndroidRuntime( 3159):   at com.google.android.play.core.hsdp.service.HsdpShimActivity.zzd(com.google.android.play:hsdp@@2.0.1:5)
E/AndroidRuntime( 3159):   at com.google.android.play.core.hsdp.service.HsdpShimActivity.onAttachedToWindow(com.google.android.play:hsdp@@2.0.1:3)

Same exception, same two frames as the Crashlytics report. That build was a release-style build with R8 on and every class in place, which also rules out the shrinking theory by experiment rather than by argument.

Forcing hsdp 2.2.0

Google's Maven repository had hsdp 2.1.0 and 2.2.0, the latter published at the start of October. Disassembling 2.2.0 shows the fix: onCreate now checks the extras and calls finish() if they are missing, and onAttachedToWindow returns early while the activity is finishing.

The catch is that no ads SDK asks for it. I checked the POM of the newest one, play-services-ads-api 25.5.0, and it still pins hsdp 2.0.1. Upgrading AdMob does not get you the fix; you have to raise hsdp yourself.

Forcing a transitive dependency to a newer version is only safe if nothing the caller uses has gone. So before changing anything I listed the public classes and methods of both versions with javap. Everything the ads SDK calls (HsdpDeepLinkServiceFactory.create, the HsdpDeepLinkService open, prewarm and end-session methods, the prewarm request builder) is unchanged in 2.2.0. The named API is a strict superset: two create(Activity, …) overloads were added. What changed is hsdp's own obfuscated zz* internals, which nothing outside it touches.

The change is a Gradle constraint. A simplified version of it:

// android/app/build.gradle — simplified example
dependencies {
    constraints {
        implementation('com.google.android.play:hsdp:2.2.0') {
            because 'play-services-ads pins 2.0.1, which throws targetPackageName is null'
        }
    }
}

A constraint rather than a dependency, so it only raises hsdp where the ads SDK already brings it and adds nothing to a build without ads. It went into our shared ads package so every game picks it up, with a note to delete it once play-services-ads depends on 2.2.0 or later. Resolution in all three build types:

releaseRuntimeClasspath: com.google.android.play:hsdp:2.0.1 -> 2.2.0
qaRuntimeClasspath: com.google.android.play:hsdp:2.0.1 -> 2.2.0
debugRuntimeClasspath: com.google.android.play:hsdp:2.0.1 -> 2.2.0

Then the same launch on the same emulator, with the new build:

E/HsdpShimActivity( 5086): Missing required intent extras in onCreate, finishing shim activity.
I/HsdpShimActivity( 5086): shim activity onDestroy
--- process alive: 5086

The activity closes itself and the game keeps running. Ads still worked afterwards: the test banner showed, and the interstitial and app open ads loaded. A rewarded ad timed out once and retried, and the old build did exactly the same on that emulator, which was sharing a heavily loaded machine with three others.

What I could not test: tapping a real app-install ad. Google's test ads don't open the Play install overlay, so the path hsdp exists for never ran in any of this.

Three more crashes with the same shape

Two of the next three Crashlytics issues had the same shape: a Google activity, started without the extras its own library always passes. The third was in Google's consent SDK:

Where Library The failure
GamesResolutionActivity play-services-games-v2 22.1.0 checkNotNull on the resultReceiver and pendingIntent extras
ProxyBillingActivity billing 9.1.0 Attempt to invoke virtual method 'android.content.IntentSender android.app.PendingIntent.getIntentSender()' on a null object reference
UMP metrics ping user-messaging-platform 4.0.0 java.util.NoSuchElementException, no message

The first two are exported="false", like the hsdp shim. Both libraries were already Google's latest release, and neither has a guard in its newest version to pick up. The UMP one is different in kind: it is a background thread, not an activity. Reading its bytecode, the metrics reporter POSTs to Google and, on any response other than 200 or 204, reads the error body with a Scanner and calls next(). On an empty body that throws NoSuchElementException, and the method only catches IOException. That is a reading of the bytecode; I did not reproduce it. UMP 4.0.0 is also the latest version.

So I made no change for any of the three. Swapping Google's activities in the manifest for subclasses of my own would work, and I am not maintaining that.

What made me stop treating them as player crashes was putting them side by side:

  • All five are build 1.0.0 (1), the game's first build, on Android 11. The framework frames carry the same line numbers in every trace (ActivityThread.java:3406, Thread.java:923).

  • The Play Games, billing and hsdp crashes all happened on the same day. Billing and hsdp are 53 seconds apart. A fifth crash in the same build, in Google's SignInHubActivity, came 52 seconds after the hsdp one. That is one Google activity after another, about a minute apart.

  • Four of the five traces have frames like these on an unrelated thread:

    at com.mojito.utility.MojitoMessage.fillCallStack(MojitoMessage.java:345)
    at com.mojito.utility.MojitoLogger.logMessage(MojitoLogger.java:128)
    at java.security.MessageDigest.digest_aroundBody13$advice(MessageDigest.java:78)
    

    _aroundBody…$advice is what AspectJ weaving leaves behind. Something on that device had hooked MessageDigest and Locale in the system framework and was logging the calls. The billing trace lacks those frames, but they only show up when some thread happens to be inside a hooked call at the moment of the crash.

Starting a non-exported activity takes the app's own code or system-level access. Google's own callers always pass the extras. Something with that access walking the manifest and launching each activity cold would produce exactly this list.

I want to be plain about how far that goes. It is an inference from the pattern. I don't know what the device was; a search for com.mojito turned up nothing. Crashlytics session ids looked like they might group the crashes, but they start with a hex timestamp, so they say when a crash happened and nothing about which device it came from.

The fifth one, and a second wrong analysis

The SignInHubActivity crash (play-services-auth 20.7.0) came with its own Gemini analysis: update play-services-auth to 21.x, add keep rules for auth.api.signin.**, and check how the app builds its sign-in intent.

The app does not build a sign-in intent. Build 1.0.0 still carried @capacitor-firebase/authentication and an old cloud-save package from an earlier version of the game, both of which sign in with a Google account. Both were removed before 1.0.1, and the merged manifest from the current release build has no SignInHubActivity at all. Every suggestion would have added code to fix a library that is no longer in the app. That issue got closed instead; if it ever comes back on a newer build, Crashlytics will reopen it as a regression.

My own first explanation was wrong too

Before I had the other crashes, I wrote the hsdp fix up as "the extra goes missing when Android restores the activity". That doesn't hold up: Android restores an activity with the intent it was started with. I only dropped it once the timestamps and the hooks made a better explanation. The fix survived because it never depended on the story. hsdp 2.2.0 closes the crash for any launch without extras, whoever does the launching, and it cost one constraint.

What I do with these now

When a crash sits entirely inside a Google library activity and the cause is a missing extra:

  • Check whether a newer version of that library guards it, even if the SDK that brings it in hasn't moved to it yet. hsdp did; billing and Play Games didn't.
  • Look at the build, the Android version and the timestamps across issues before fixing anything, and look at the non-crashing threads for hooks a normal phone wouldn't have.
  • Leave the rest alone unless the same crash shows up from several devices without those frames. Crashlytics shows the user and device count per issue, so that is one glance. If it happens, it goes to Google's issue tracker, because the fix has to come from them.

And read Crashlytics' AI analysis as a list of things that often cause an error with that wording. It works from the stack trace, and the answer here was in the bytecode, the manifest and the dependency tree. A few minutes with javap and one am start on a rooted emulator answered what it guessed at. If the Crashlytics side of a Capacitor game is new to you, I wrote up getting Analytics and Crashlytics to report anything at all in the same game.

More posts

New writing when there is something worth saying. By email or by RSS, whichever you prefer.