Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UnsatisfiedLinkError when trying to override RegisterReceiver in Application inheritor #9334

Open
aliyailina opened this issue Sep 26, 2024 · 7 comments
Assignees
Labels
Area: App Runtime Issues in `libmonodroid.so`. bug Component does not function as intended.
Milestone

Comments

@aliyailina
Copy link

aliyailina commented Sep 26, 2024

Android framework version

net8.0-android

Affected platform version

.NET 8.0.303
Tried on Android 12 (Samsung) and Android 14 (Pixel)

Description

Not a lot to describe: trying override RegisterReceiver(BroadcastReceiver? receiver, IntentFilter? filter) results in java.lang.UnsatisfiedLinkError.
The full message is:
java.lang.UnsatisfiedLinkError: No implementation found for android.content.Intent crc645671ac291747a43a.SampleApplication.n_registerReceiver(android.content.BroadcastReceiver, android.content.IntentFilter) (tried Java_crc645671ac291747a43a_SampleApplication_n_1registerReceiver and Java_crc645671ac291747a43a_SampleApplication_n_1registerReceiver__Landroid_content_BroadcastReceiver_2Landroid_content_IntentFilter_2) - is the library loaded, e.g. System.loadLibrary?

Steps to Reproduce

Create new Android application, add Application class inheritor and override RegisterReceiver(BroadcastReceiver? receiver, IntentFilter? filter) method.
Anyway, you can try the attached sample

Did you find any workaround?

Any workaround would be appreciated.

Relevant log output

java.lang.UnsatisfiedLinkError: No implementation found for android.content.Intent crc645671ac291747a43a.SampleApplication.n_registerReceiver(android.content.BroadcastReceiver, android.content.IntentFilter) (tried Java_crc645671ac291747a43a_SampleApplication_n_1registerReceiver and Java_crc645671ac291747a43a_SampleApplication_n_1registerReceiver__Landroid_content_BroadcastReceiver_2Landroid_content_IntentFilter_2) - is the library loaded, e.g. System.loadLibrary?
 	at crc645671ac291747a43a.SampleApplication.n_registerReceiver(Native Method)
 	at crc645671ac291747a43a.SampleApplication.registerReceiver(SampleApplication.java:34)
 	at mono.MonoPackageManager.LoadApplication(MonoPackageManager.java:39)
 	at mono.MonoRuntimeProvider.attachInfo(MonoRuntimeProvider.java:35)
 	at android.app.ActivityThread.installProvider(ActivityThread.java:8220)
 	at android.app.ActivityThread.installContentProviders(ActivityThread.java:7728)
 	at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7482)
 	at android.app.ActivityThread.access$1600(ActivityThread.java:310)
 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2281)
 	at android.os.Handler.dispatchMessage(Handler.java:106)
 	at android.os.Looper.loopOnce(Looper.java:226)
 	at android.os.Looper.loop(Looper.java:313)
 	at android.app.ActivityThread.main(ActivityThread.java:8663)
 	at java.lang.reflect.Method.invoke(Native Method)
 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571)
 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1135)
@aliyailina aliyailina added Area: App Runtime Issues in `libmonodroid.so`. needs-triage Issues that need to be assigned. labels Sep 26, 2024
@jonpryor
Copy link
Member

The cause of this crash is straightforward: context.registerReceiver():

context.registerReceiver (new mono.android.app.NotifyTimeZoneChanges (), timezoneChangedFilter);

needs to be called after ApplicationRegistration.registerApplications():

mono.android.app.ApplicationRegistration.registerApplications ();

Which would fix this immediate problem.

The larger problem is that all virtual context. method invocations within MonoPackageManager.LoadApplication() are now suspect. This includes context.getFilesDir(), context.getCacheDir(), and context.getClassLoader() (among others), as these are all bound as virtual members, and thus could also be overridden by the Application, a'la:

[Application]
partial class MyApp : Android.App.Application {
    public override Java.IO.File? FilesDir => "/data/local/tmp"; // sure, why not!
}

We can't call ApplicationRegistration.registerApplications() until after Runtime.initInternal() is called, and Runtime.initInternal() pulls in data from the context, which is the Application subclass.

(Behold the joy of circular dependencies?)

We could instead move those context. method invocations into libxamarin-app.so, which would allow us to perform non-virtual method invocations via JNIEnv::CallNonvirtualObjectMethod() against android.app.Application. This should fix the crash, but also means that these properties would be ignored when overridden in C#.

I'm not sure what this would do to startup time. JNI invocations are generally slower than invocations from Java.

Alternatively, we rework Runtime.initInternal() to be a "two-step" process, where the initial step doesn't require context method invocations, we call ApplicationRegistration.registerApplications(), and then we call Runtime.finishInit() using the context method invocations.

I'm not sure that this is possible, but it certainly should be explored.

@aliyailina
Copy link
Author

aliyailina commented Sep 26, 2024

@jonpryor Thank you for investigation! I hope I haven't open any hell portal with that issue.

Is there anything I can do to override this method without crashing? Because, from what I read, it looks impossible.

@jonpryor
Copy link
Member

@aliyailina asked:

Is there anything I can do to override this method without crashing?

Yes. Override…in Java. Via the glory that is @(AndroidJavaSource), with %(AndroidJavaSource.Bind)=True, you should be able to subclass Application from Java, and then have your C# class inherit your Java class:

// java
package kludge;
public class MyIntermediateApp extends android.app.Application {
    @Override Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        // …
    }
}
// C#
public sealed partial class SampleApplication : Kludge.MyIntermediateApp {
    // …
}

The problem with this idea is that presumably you want to call C# code from RegisterReceiver(). That complicates things, but you could have MyIntermediateApp.registerReceiver() "store" the calls in a List, and then have a method to query that list within SampleApplication.OnCreate(). (I think OnCreate() is called later, anyway…)

@aliyailina
Copy link
Author

aliyailina commented Sep 27, 2024

@jonpryor That might work for this sample, where SampleApplication inherits exactly Android's Application class, but in real app I have already inherited my application class from third-party provided base class (to be precise, MvvmCross). Anything for such case?

@jonpryor
Copy link
Member

@aliyailina wrote:

That might work for this sample, where SampleApplication inherits exactly Android's Application class, but in real app I have already inherited my application class from third-party provided base class (to be precise, MvvmCross). Anything for such case?

As my college Statics teacher used to say, "it's the same but different." You should still be able to use an intermediary, but instead of having the Java code subclass android.app.Application, it would subclass the Java name of the C# class you're using. (Yes, Java code should be able to subclass C# types!)

The problem will be determining the Java name of the C# class.

For example, if we create a new MVVM-using app:

% dotnet new android -n android-mvx
% cd android-mvx
% git apply --ignore-whitespace <<EOF
diff --git a/android-mvx.csproj b/android-mvx.csproj
index a1999d9..73b8237 100644
--- a/android-mvx.csproj
+++ b/android-mvx.csproj
@@ -10,4 +10,21 @@
     <ApplicationVersion>1</ApplicationVersion>
     <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
   </PropertyGroup>
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions">
+      <Version>5.0.0</Version>
+    </PackageReference>
+    <PackageReference Include="MvvmCross">
+      <Version>8.0.2</Version>
+    </PackageReference>
+    <PackageReference Include="MvvmCross.DroidX.Material">
+      <Version>8.0.2</Version>
+    </PackageReference>
+    <PackageReference Include="Serilog.Extensions.Logging">
+      <Version>3.0.1</Version>
+    </PackageReference>
+    <PackageReference Include="Xamarin.AndroidX.ConstraintLayout">
+      <Version>2.1.1.2</Version>
+    </PackageReference>
+  </ItemGroup>
 </Project>
\ No newline at end of file
EOF

% dotnet build

The Xamarin MVVX MainApplication template inherits MvxAndroidApplication<Setup, App>:

#if DEBUG
    [Application(Debuggable = true)]
#else
    [Application(Debuggable = false)]
#endif
    public class MainApplication : MvxAndroidApplication<Setup, App>
    {
        public MainApplication(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
        {
        }
    }

Once dotnet build has finished, look for the generated Java Callable Wrappers:

% find obj -iname \*MvxAndroidApplication\*.java
./obj/Debug/net8.0-android/android/src/crc6466d8e86b1ec8bfa8/MvxAndroidApplication.java
./obj/Debug/net8.0-android/android/src/crc6466d8e86b1ec8bfa8/MvxAndroidApplication_2.java

The _2 comes from the "arty" of MvxAndroidApplication<Setup, App>, but generics get all "weird", so I'd suggest forgetting that MvxAndroidApplication<Setup, App> exists, and instead stick to MvxAndroidApplication.

Now that you've figured out the Java Callable Wrapper name, just use it from your Java code:

package kludge;
public class MyIntermediateApp extends crc6466d8e86b1ec8bfa8.MvxAndroidApplication {
    @Override Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        // …
    }
}

Then change your C# code from:

public partial class MainApplication : MvxAndroidApplication<Setup, App> {
}

to

public partial class MainApplication : Kludge.MyIntermediateApp {
}

You lose generics, but retain the inheritance relationship.

@jonpryor jonpryor added this to the .NET 10 milestone Sep 30, 2024
@aliyailina
Copy link
Author

aliyailina commented Oct 3, 2024

@jonpryor Many thanks for your help!

I tried what you suggest, created .java file in my project, inherited my Java class from MvxAndroidApplication referencing the package from JCW, but it only says "package does not exist" when referencing to package where MvxAndroidApplication is. Am I missing something?
I checked the path to JCW for MvxAndroidApplication and file definitely exists at the same path as yours.
It also doesn't work with clear sample project.

@aliyailina
Copy link
Author

aliyailina commented Oct 4, 2024

@jonpryor Here is the build log, and here is the sample I used.
I'm not very familiar with Android build process, but, from what I understand, the build task that creates JCWs is _GenerateJavaStubs and it is not called here (while it is called if I will exclude .java file), so there is no way my .java file access MvxAndroidApplication from JCW, because it is not generated at the point when .java file is being bound.

upd

It looks like a bug. Created new issue #9374.

upd2

So I was right, generated Java types are not available when AndroidJavaSource binding happens. According to this comment it is not a bug, but expected behavior. So no workaround for now.

@jpobst jpobst added bug Component does not function as intended. and removed needs-triage Issues that need to be assigned. labels Oct 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: App Runtime Issues in `libmonodroid.so`. bug Component does not function as intended.
Projects
None yet
Development

No branches or pull requests

4 participants