I am not sure of the terminology for what I'm looking to do, so sorry in advance!
I've found a FilePicker plugin for Xamarin.Forms (https://github.com/Studyxnet/FilePicker-Plugin-for-Xamarin-and-Windows) that implements device-specific functionality for selecting files via the CrossFilePicker class.
The way to use leverage this functionality would be something like
CrossFilePicker.Current.OpenFile("Filename.txt");
The most important part of this for me is that CrossFilePicker.Current is static and can be accessible from anywhere in the shared layer of my Xamarin.Forms app.
I need to implement a class with the same characteristics. I want to leverage device Accessibility functionality (i.e. determining if a screen reader is enabled) and I need to be able to do so with a static class.
My eventual plan is to then wrap this static class so that I can use it for unit tests too.
I don't want to import device libraries into my shared project.
TLDR: I need a static class that implements device-specific functionality.
Any help would be greatly appreciated! Thank you :)
EDIT:
Here are the files I have currently implemented in my project
IAccessibilityService Located in the shared .NET project
namespace Bitspace.Services
{
public interface IAccessibilityService
{
public bool IsScreenReaderEnabled();
public void Announcement(string message);
public void NavigationAnnouncement(string message);
}
}
DeviceAccessibility.cs Located in the shared .NET project
using System;
namespace Bitspace.Services
{
public class DeviceAccessibility
{
private static Lazy<IAccessibilityService> Implementation = new Lazy<IAccessibilityService>(() => CreateAccessibilityService(), System.Threading.LazyThreadSafetyMode.PublicationOnly);
public static IAccessibilityService Current
{
get
{
var curr = Implementation.Value;
if (curr == null)
{
throw new Exception();
}
return curr;
}
}
private static IAccessibilityService CreateAccessibilityService()
{
return new DeviceAccessibilityImplementation();
}
}
}
DeviceAccessibilityImplementation.cs Located in the Android project
using Android.Runtime;
namespace Bitspace.Services
{
[Preserve (AllMembers = true)]
public class DeviceAccessibilityImplementation : IAccessibilityService
{
public bool IsScreenReaderEnabled()
{
return true;
}
public void Announcement(string message)
{
}
public void NavigationAnnouncement(string message)
{
}
}
}
If I try to build the project, I get an error on the return new DeviceAccessibilityImplementation(); line in DeviceAccessibility.cs that says DeviceAccessibility.cs(25, 24): [CS0246] The type or namespace name 'DeviceAccessibilityImplementation' could not be found (are you missing a using directive or an assembly reference?)
However, CTRL Clicking on that line takes me to the DeviceAccessibilityImplementation.cs
If I understand correctly:
When we run an Android app built with Flutter, it first goes to the AndroidManfiest.xml file, looks for the LAUNCHER activity and launches it. This activity is MainActivity.kt by default that extends FlutterActivity.
But the Flutter part of the app begins when the main() method in main.dart gets called.
My question is, who calls this main() method?
For Android, is it the MainActivity that extends FlutterActivity? Or is there some logic in the FlutterActivity itself? Or is there some other mechanism altogether and I'm completely missing the point?
The same question applies to iOS too with FlutterViewController instead of FlutterActivity.
A link to the source code for this that just clarifies when does main() get called would be great.
On iOS
A main.m file was autogenerated in your Flutter projects ios/Runner directory, which defines the regular, main function which a C program implements, int main(int argc, char* argv[]);.
One compiled output can only have one main method, which the compiler will run immediately when the program is started. The following code creates a UIApplicationMain, which "Creates the application object and the application delegate and sets up the event cycle":
#import "AppDelegate.h"
int main(int argc, char* argv[]) {
#autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
It's simpler in Swift, just annotate AppDelegate with #UIApplicationMain.
The AppDelegate is the class which launches Flutter, because it extends FlutterAppDelegate. When FlutterAppDelegate is instantiated, iOS will create the FlutterViewController, which creates a FlutterEngine. It creates FlutterViewController because the FlutterViewController is configured in the Main.storyboard, which has been specified in the applications Info.plist. So technically, Flutter apps are Storyboard apps 😂.
Anyway, when the storyboard is created by iOS, the window property is set on the AppDelegate. You can get the FlutterViewController in the AppDelegate using window.rootViewController. An Objective-C++ file, FlutterViewController.mm's sharedSetupWithProject method creates a FlutterEngine using [[FlutterEngine alloc]initWithName:...:
- (void)sharedSetupWithProject:(nullable FlutterDartProject*)project
initialRoute:(nullable NSString*)initialRoute {
// Need the project to get settings for the view. Initializing it here means
// the Engine class won't initialize it later.
if (!project) {
project = [[[FlutterDartProject alloc] init] autorelease];
}
FlutterView.forceSoftwareRendering = project.settings.enable_software_rendering;
auto engine = fml::scoped_nsobject<FlutterEngine>{[[FlutterEngine alloc]
initWithName:#"io.flutter"
project:project
allowHeadlessExecution:self.engineAllowHeadlessExecution
restorationEnabled:[self restorationIdentifier] != nil]};
if (!engine) {
return;
}
_viewOpaque = YES;
_weakFactory = std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(self);
_engine = std::move(engine);
_flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]);
[_engine.get() createShell:nil libraryURI:nil initialRoute:initialRoute];
_engineNeedsLaunch = YES;
_ongoingTouches.reset([[NSMutableSet alloc] init]);
[self loadDefaultSplashScreenView];
[self performCommonViewControllerInitialization];
}
Eventually, in FlutterEngine.mm, launchEngine is called, with the entrypoint (your dart's main function.)
- (void)launchEngine:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil {
// Launch the Dart application with the inferred run configuration.
self.shell.RunEngine([_dartProject.get() runConfigurationForEntrypoint:entrypoint
libraryOrNil:libraryOrNil]);
}
Shell::RunEngine is a C++ function implemented in shell.cc. I'm going to stop there for now. This is probably where the event loop starts 😃, using platform_runner->PostTask(...).
On Android
In a generated Flutter project's android directory, MainActivity is declared in the AndroidManifest.xml to be the application which launches from the home screen.
When the activity is launched, Android will call the activity's onCreate method. Because MainActivity extends FlutterActivity, this onCreate method is called. FlutterActivityAndFragmentDelegate is instantiated, and its onAttach method is called. Eventually, a Java representation of FlutterEngine is created using:
flutterEngine =
new FlutterEngine(
host.getContext(),
host.getFlutterShellArgs().toArray(),
/*automaticallyRegisterPlugins=*/ false,
/*willProvideRestorationData=*/ host.shouldRestoreAndSaveState());
This communicates with the C++ FlutterEngine library using FlutterJNI.
/**
* Interface between Flutter embedding's Java code and Flutter engine's C/C++ code.
*
* <p>Flutter's engine is built with C/C++. The Android Flutter embedding is responsible for
* coordinating Android OS events and app user interactions with the C/C++ engine. Such coordination
* requires messaging from an Android app in Java code to the C/C++ engine code. This communication
* requires a JNI (Java Native Interface) API to cross the Java/native boundary.
*
Later, in FlutterActivityAndFragmentDelegate, doInitialFlutterViewRun is called, which creates a DartEntryPoint, your main method again, based on the FlutterActivity's ActivityInfo. This gets the entrpoint function name using the following, but it will default to "main":
#NonNull
public String getDartEntrypointFunctionName() {
try {
Bundle metaData = getMetaData();
String desiredDartEntrypoint =
metaData != null ? metaData.getString(DART_ENTRYPOINT_META_DATA_KEY) : null;
return desiredDartEntrypoint != null ? desiredDartEntrypoint : DEFAULT_DART_ENTRYPOINT;
} catch (PackageManager.NameNotFoundException e) {
return DEFAULT_DART_ENTRYPOINT;
}
}
Then, flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint); is called. So your main method has "been called". But this uses the FlutterJNI and the FlutterEngine C++ code. First, flutterJNI.runBundleAndSnapshotFromLibrary is called, and finally this JNI native method:
private native void nativeRunBundleAndSnapshotFromLibrary(
long nativeShellHolderId,
#NonNull String bundlePath,
#Nullable String entrypointFunctionName,
#Nullable String pathToEntrypointFunction,
#NonNull AssetManager manager);
This native method is defined in platform_view_adroid_jni_impl.cc:
{
.name = "nativeRunBundleAndSnapshotFromLibrary",
.signature = "(JLjava/lang/String;Ljava/lang/String;"
"Ljava/lang/String;Landroid/content/res/AssetManager;)V",
.fnPtr = reinterpret_cast<void*>(&RunBundleAndSnapshotFromLibrary),
},
Where RunBundleAndSnapshotFromLibrary is a C++ method:
static void RunBundleAndSnapshotFromLibrary(JNIEnv* env,
jobject jcaller,
jlong shell_holder,
jstring jBundlePath,
jstring jEntrypoint,
jstring jLibraryUrl,
jobject jAssetManager) {
auto asset_manager = std::make_shared<flutter::AssetManager>();
asset_manager->PushBack(std::make_unique<flutter::APKAssetProvider>(
env, // jni environment
jAssetManager, // asset manager
fml::jni::JavaStringToString(env, jBundlePath)) // apk asset dir
);
auto entrypoint = fml::jni::JavaStringToString(env, jEntrypoint);
auto libraryUrl = fml::jni::JavaStringToString(env, jLibraryUrl);
ANDROID_SHELL_HOLDER->Launch(asset_manager, entrypoint, libraryUrl);
}
Where AndroidShellHolder::Launch is:
void AndroidShellHolder::Launch(std::shared_ptr<AssetManager> asset_manager,
const std::string& entrypoint,
const std::string& libraryUrl) {
if (!IsValid()) {
return;
}
asset_manager_ = asset_manager;
auto config = BuildRunConfiguration(asset_manager, entrypoint, libraryUrl);
if (!config) {
return;
}
shell_->RunEngine(std::move(config.value()));
}
Just like iOS, Shell::RunEngine is called.
This is pretty basic, but I'm not getting success on implementing it from the official Unity docs and YouTube video tutorials, so I asked.
I want to access methods from already made aar plug-in. Its checked as android plug-in by default. The plug-in has following code:
package alarm.company.com.unity;
public class myPlugin {
public String myTest(int i){
return "Hello your string number is: "+i;
}
}
the compiled aar plug-in is copied into the Unity folder.
Here's the c# code to access that method:
const string PluginName = "alarm.company.com.unity.myPlugin";
static AndroidJavaClass _pluginClass;
static AndroidJavaClass _pluginInstance;
public Text text;
// Start is called before the first frame update
void Start()
{
AndroidJavaClass plugin = new AndroidJavaClass(PluginName);
string s = plugin.CallStatic<string>("myTest", 9);
print(s);
text.text = s;
}
text is assigned and this script is put on the main camera. But I'm not getting any text back or the text object is not changed, while running in emulator. How can this be solved?
Unity Plugin development is a bit tricky to get setup at first but once you are able to it is pretty powerful.
Couple things.
1) You are using an AndroidJavaClass and trying to call a non static method in java. Your method in your plugin should be static if you are wanting to use that class.
2) Otherwise you will need to use an AndroidJavaObject to construct your class and then you should be able to call your function.
Hope this helps!
Example Using non Static Code:
Unity Code
using (AndroidJavaObject leetPlugin = new AndroidJavaObject("alarm.company.com.unity.myPlugin")){
string s = leetPlugin.Call("myTest", 9);
print(s);
text.text = s;
}
Android Code
public void myTest(int value) {
return "Your value is " + value;
}
Some advises when you work with plugins for unity:
Use gradle build system. It generates mainTemplate file in Plugins/Android. You can edit this file and set source java code to your java classes so you don't need to generate aar every time you want to build:
sourceSets
{
main
{
java
{
srcDirs = ['src']
srcDirs += ['E:/AndroidStudioProjects/PathToJavaClasses']
}
}
}
If you want to send something from java code to unity you should extend your activity from unity and call method UnitySendMessage
public class MyUnityPlayerActivity extends UnityPlayerActivity{
public void SendUnityMessage()
{
UnityPlayer.UnitySendMessage(UnityGameObjectName, UNITY_FUNCTION_NAME,
params);
}
}
I am diving into the ndk stuff and I've managed to make a simple library and call it from my activity.
In order to test it, in my Activity I have used:
static {
System.loadLibrary("my-lib");
}
public native static String callNativeFunction(Context context);
This means that calling MyActivity.callNativeFunction(context) does return the String value from my Cpp function.
I have 2 questions:
The loadLibrary is made in my main activity, however I want to be able for instance, to call the callNativeFunction function from an IntentService when the activity may be closed or from other places of my app. How can I properly load the library and have it available from all places of the app?
Since this is a simple function that I'll use in my project, is there anything else specific to do on release? From https://developer.android.com/studio/projects/add-native-code.html it seems that Gradle builds for all supported abis and adds them into the apk.
You need to load the library once. This can also be done in your Application class. Once the library is loaded, you can call the defined native methods from the appropriate classes defined in your cpp class
Application class :
public class SRApplication extends Application {
static {
System.loadLibrary("my-lib");
}
#Override
public void onCreate() {
super.onCreate();
}
}
CPP :
extern "C"
jstring
Java_com_example_service_ExampleService_getNativeString (
JNIEnv* env,
jobject /* this */) {
return env->NewStringUTF("yo");
}
Your Service :
package com.example.service.ExampleService;
public class ExampleService extends Service {
#Nullable
#Override
public IBinder onBind(Intent intent) {
String nativeString = getNativeString();
return null;
}
public native String getNativeString();
}
If you don't have specific code for different cpu variants, you don't need to explicitly handle anything, the default
externalNativeBuild {
cmake {
cppFlags ""
}
}
is sufficient.
I'm trying to write a Unity plugin that will work both on iOS and Android.
I have two point of entry to my plugin, one class for iOS and another for Android.
The problem is when I use only the iOS class and try to build the XCode project from Unity I get this error:
The class UnityEngine.AndroidJavaObject could not be loaded, used in UnityEngine
AndroidJavaObject class is used only in the Android class which is never referenced by the unity scripts.
The problem is I want to provide one plugin that will "just work" for both iOS and Android. The plugin is compiled so I can't use any of the unity pre-processor flags.
How can I solve this issue?
I see no alternative to conditional compilation if you need access to the native API. I would suggest to use encapsulate plugin access by using an interface, for example:
public interface IMyPlugin
{
void Do ();
}
public class MyIOSConnector : IMyPlugin
{
public void Do () {
#if UNITY_IOS
// iOS implementation
#endif
}
}
public class MyAndroidConnector : IMyPlugin
{
public void Do () {
#if UNITY_ANDROID
// Android implementation
#endif
}
}
public static class MyPlugin
{
static IMyPlugin _myPlugin = null;
public static IMyPlugin Access {
get {
if (_myPlugin == null) {
#if UNITY_EDITOR
// maybe this needs special handling e.g. a dummy implementation
#endif
switch (Application.platform) {
case RuntimePlatform.IPhonePlayer:
_myPlugin = new MyIOSConnector ();
break;
case RuntimePlatform.Android:
_myPlugin = new MyAndroidConnector ();
break;
default:
break;
}
}
return _myPlugin;
}
}
}
From other classes you call then:
MyPlugin.Access.Do ();