Skip to content

Android bridge without the Java wrapper, harness on a device - #36

Merged
x401om merged 12 commits into
devfrom
feature/android-bridge-without-wrapper
Nov 13, 2026
Merged

x401om merged 12 commits into
devfrom
feature/android-bridge-without-wrapper

Conversation

@yauch-dev

@yauch-dev yauch-dev commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

What

  • Android bridge calls CrossplatformHelper from C#. AdaptyAndroid.cs reaches io.adapty.internal:crossplatform through JNI and implements its four callback interfaces as AndroidJavaProxy. The adaptyandroidwrapper Gradle project, the unity-wrapper AAR under Plugins/Android/Local and AdaptyAndroidCallbackAction are gone; a native bump is one line in the androidlib build.gradle. R8 needs no rule: android-sdk's consumer rules keep com.adapty.**. The activity provider is set on every initialisation, as in the other cross-platform SDKs.
  • Callbacks go through the Unity thread's native looper (AdaptyAndroidLooper, an eventfd on the thread's ALooper). A flow is an Activity of its own and Unity's player loop is paused underneath it; a SynchronizationContext post would wait for the frame that comes only once the flow is gone, so the close action would reach the listener with nothing left to dismiss. Unlike a Handler on Looper.myLooper(), the native looper is there under GameActivity too, where Unity's thread has no Java Looper and 4.1.0–4.1.2 delivered nothing. RunOnMainThread, which carries the permission respond and the observer-mode reports from app code, posts through the same looper on Android, so they reach a flow that is still on screen.
  • SDK harness on an Android device. harness_build_android and AndroidHarnessServer (the stock Pipeline runtime server writes its descriptor next to the APK and stops); the run steps are in the harness AGENTS.md. Pipeline is bumped to 0.6.0-exp.1, which starts the runtime server from Project Settings rather than a scene component; the scene's stale RuntimePipelineManager is removed. Enable In Builds stays off in the project, because in 0.6 it puts the server into any player built while it is on, release builds included; both harness builds turn it on for the build and restore it, with every other setting they touch.
  • CHANGELOG: a .unitypackage install deletes Assets/AdaptySDK/Plugins/Android/Local before importing, or the old wrapper's AAR stays in the build.
  • Demo orientation fixed to portrait.

Verified

  • JSON layer suite, all four legs.
  • Harness on an Android 16 device, A/B against the wrapper build: activation, profile, a flow with its Play products, creating, presenting and dismissing its view, and flow_view_did_perform_action (system_back) delivered to C# within 3 s while the flow is on screen — identical on both builds.
  • After the review changes, on an Android 15 device: the harness build still starts the runtime server with Enable In Builds off in the project, and in Observer mode the purchase the harness reports from a pool thread reaches the native SDK 1–2 ms later (observerModeHandler: onStartPurchase called) while the flow is on screen; system_back still arrives. On the iOS simulator the harness build runs the same calls and the observer purchase.
  • Both harness builds leave RuntimePipelineConfig.json and ProjectSettings.asset unchanged.
  • Not exercised: a permission respond (no test flow asks for one; it takes the same RunOnMainThread path), and a purchase through the flow outside Observer mode.

@StanislavMayorov

Copy link
Copy Markdown
Contributor

@codex

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-30T10:30:04.897369Z 6e142ac Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6e142ac2aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@yauch-dev
yauch-dev force-pushed the feature/android-bridge-without-wrapper branch 2 times, most recently from 0582d75 to 1af8ed3 Compare October 28, 2026 15:33
@yauch-dev
yauch-dev removed this pull request from stack #38 October 28, 2026 15:36
@yauch-dev
yauch-dev added this pull request to stack #40 October 28, 2026 15:36
@yauch-dev
yauch-dev force-pushed the feature/android-bridge-without-wrapper branch from 1af8ed3 to 828aa5d Compare October 28, 2026 18:51
@yauch-dev
yauch-dev force-pushed the feature/android-bridge-without-wrapper branch 2 times, most recently from 5989dc2 to 161f587 Compare November 9, 2026 20:27
Base automatically changed from feature/testing-tools to dev November 13, 2026 13:45
AdaptyAndroid.cs reaches io.adapty.internal:crossplatform through JNI and
implements its four callback interfaces as AndroidJavaProxy, so the package
ships no Java of its own: the adaptyandroidwrapper Gradle project, the
unity-wrapper AAR under Plugins/Android/Local and AdaptyAndroidCallbackAction
go away, and a native bump is one line in the androidlib build.gradle.

Callbacks are posted to a Handler on the Unity thread's Looper, as the
wrapper did, not through RunOnMainThread: a flow is an Activity of its own,
Unity's player loop is paused underneath it, and a SynchronizationContext post
would wait for the frame that comes only once the flow is gone - the close
action would reach the listener with nothing left to dismiss. Measured on a
device with the SDK harness against the wrapper this replaces.

R8 needs no rule of its own: android-sdk's consumer rules keep com.adapty.**.
harness_build_android builds the harness scene as a Development ARM64 player.
The stock Pipeline runtime server cannot run on a device - it writes its
descriptor next to Application.dataPath, which is the APK - so
AndroidHarnessServer turns the driver's autostart off and runs a subclass that
writes the descriptor into the app's internal files directory, with a token of
its own. The host pulls it with run-as, rewrites pid and heartbeat to what the
CLI accepts, and bridges the port with adb forward; the harness AGENTS.md has
the steps and the pause behaviour under a flow.

Pipeline 0.6.0-exp.1 starts the runtime server from Project Settings >
Pipeline > Runtime rather than a scene component; the settings file enables it
on port 7900.
…nced

Pipeline 0.6.0 has no such component; the runtime server is configured in
Project Settings > Pipeline > Runtime, and the scene showed a missing script.
Under GameActivity - the default entry point of a new Unity 6 project - Unity's
scripting thread has no Java Looper, so the bridge threw on its first use and
no call reached the SDK; 4.1.0-4.1.2 with the Java wrapper registered nothing
and dropped every completion and event instead. While a flow is on screen
GameActivity also stops the player loop, SynchronizationContext and
UnityPlayer.invokeOnMainThread alike, so a RunOnMainThread fallback would leave
a flow that cannot be closed.

AdaptyAndroidLooper registers an eventfd on the thread's native ALooper through
P/Invoke into libc and libandroid, and drains its queue in the looper callback.
That looper keeps turning through the pause on both entry points - Looper.loop()
polls it under UnityPlayerActivity, the activity glue under GameActivity - so
it replaces the Handler outright; a Handler on Looper.myLooper() would also
bind to a Looper nobody loops if something called Looper.prepare() there.

Checked on an Android 17 emulator: a flow closed from the flow's close action
and from system Back while Unity was paused, on both entry points with Unity
6000.4, GameActivity also on 6000.0 and 6000.6. The AGENTS.md #if count is
corrected to what the package holds.
The Pipeline 0.6 build processor reads EditorUserBuildSettings.development
rather than the build options, on every platform. Only the Android harness
build set it, so an iOS harness build from an Editor with the flag off
stopped on the processor's release-build dialog, which nothing driving the
Editor from the CLI can answer.
BuildAndroid did not set the flag the linker processor reads, so the
Android harness was built with an empty list and the linker could strip
what the harness reaches only by reflection.
They still said there was no Android build, that the scene referenced
RuntimePipelineManager, and that the 120 s request timeout was the
scene's; it now comes from RuntimePipelineConfig.json. An iOS device is
what stays out of reach.
…ess's

Pipeline 0.6 puts no define constraint on its runtime assembly, so with
Enable In Builds on in the project every player got the server - a
release build in batchmode with only a warning, the demo's and
StrippingBuild's included - and AndroidHarnessServer, which lives in
Assembly-CSharp, started it on any Android player.

The project now keeps Enable In Builds off. Both harness builds turn it
on for the build, with the Development flag, and restore both together
with the iOS SDK, the Android architectures and the application id they
set. Without a driver AndroidHarnessServer returns quietly, since that
is every player that is not a harness build. The settings file is
committed as RuntimePipelineConfig.Save writes it, so a harness build
leaves it unchanged.
…ndroid

A permission respond and the observer-mode reports are app code that
may run on any thread, and RunOnMainThread posted them through the
SynchronizationContext. On Android a flow pauses Unity's player loop,
so the answer waited for the flow it was meant for to close. They now
go through AdaptyAndroidLooper, like the bridge's own callbacks.

The activity provider is set on every initialisation of the helper, as
in the other cross-platform SDKs, not only on the process's first. The
changelog tells a .unitypackage install to delete Plugins/Android/Local,
where the old wrapper's AAR would otherwise stay in the build.
…thread

An app answers a permission request from the OS callback and reports an
observer-mode purchase from the store's, neither of them on Unity's
thread. The harness answered both inline on the main thread, so it never
took the path RunOnMainThread exists for; now it answers from a pool
thread and logs that it did, and a run on an Android device checks that
the answer reaches a flow that is on screen.
…e Editor

The post-processor that files the local StoreKit configuration is
compiled for the platform the Editor is on. After an Android harness
build in a live Editor, harness_build produced a project without it and
purchases went to the sandbox with nothing saying why; it now refuses to
run unless the Editor is on iOS.

AndroidHarnessServer was guarded by UNITY_ANDROID alone, so in Play Mode
on the Android platform with Enable In Builds on it took over the
driver's server and then threw on AndroidJavaClass. It is now compiled
for Android players only.
@x401om
x401om force-pushed the feature/android-bridge-without-wrapper branch from 161f587 to 2ded1cb Compare November 13, 2026 13:45
@x401om
x401om merged commit 13462ee into dev Nov 13, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants