Skip to content

[mono] Add Android app sample using mono runtime #3944

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

Merged
merged 18 commits into from
Dec 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/publish-mono-samples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ on:
branches: [ master ]

env:
DOTNET_INSTALLER_CHANNEL: '6.0.100-alpha.1.20531.2'
DOTNET_INSTALLER_CHANNEL: '6.0.100-alpha.1.20567.4'
DOTNET_DO_INSTALL: 'true'

jobs:
Expand All @@ -35,4 +35,10 @@ jobs:
- name: Publish mono iOS sample
run: |
cd ./core/mono-samples/iOS
dotnet publish /p:RunningOnCI=True
- name: Publish mono Android sample
run: |
cd ./core/mono-samples/Android
export ANDROID_SDK_ROOT=${HOME}/Library/Android/sdk
export ANDROID_NDK_ROOT=${HOME}/Library/Android/sdk/ndk-bundle
dotnet publish /p:RunningOnCI=True
40 changes: 40 additions & 0 deletions core/mono-samples/Android/AndroidSampleApp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.InteropServices;

public static class AndroidSampleApp
{
[DllImport("native-lib")]
private extern static int myNum();

[DllImport("native-lib")]
private extern static void androidRegisterCounterIncrement(Func<string> del);

[DllImport("native-lib")]
private extern static void androidRegisterNameGreet(Func<string,string> del);

private static int counter = 1;

// Called by native code, see native-lib.c
private static string IncrementCounter()
{
return $"Clicked {counter++} times!";
}

private static string GreetName(string name)
{
return $"Hello {name}!\nRunning on mono runtime\nUsing C#";
}

public static int Main(string[] args)
{
androidRegisterCounterIncrement(IncrementCounter);
androidRegisterNameGreet(GreetName);
Console.WriteLine("Hello, Android!"); // logcat
return myNum();
}
}
73 changes: 73 additions & 0 deletions core/mono-samples/Android/AndroidSampleApp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<Project Sdk="Microsoft.NET.Sdk" DefaultTargets="BuildApp">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetArchitecture>x64</TargetArchitecture>
<RuntimeIdentifier>android-$(TargetArchitecture)</RuntimeIdentifier>
<OutputType>Exe</OutputType>
<PublishTrimmed>True</PublishTrimmed>
<TrimMode>link</TrimMode>
<Configuration>Release</Configuration>
<OutputPath>$(MSBuildThisFileDirectory)bin/$(Configuration)/$(TargetFramework)/$(RuntimeIdentifier)/publish</OutputPath>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Runtime.Android.Sample.Mono" Version="$(BundledNETCoreAppPackageVersion)" GeneratePathProperty="true" />
</ItemGroup>

<Target Name="BuildNativeLib" BeforeTargets="Publish">
<PropertyGroup>
<abi Condition="'$(TargetArchitecture)' == x64">x86_64</abi>
<abi Condition="'$(TargetArchitecture)' == x86">x86</abi>
<abi Condition="'$(TargetArchitecture)' == arm">armeabi-v7a</abi>
<abi Condition="'$(TargetArchitecture)' == arm64">arm64-v8a</abi>
</PropertyGroup>
<Exec Command="cmake -DCMAKE_TOOLCHAIN_FILE=$(ANDROID_NDK_ROOT)/build/cmake/android.toolchain.cmake -DANDROID_ABI=$(abi) -DANDROID_STL=none -DANDROID_NATIVE_API_LEVEL=21 -B monodroid -DCMAKE_BUILD_TYPE=MinSizeRel -DCMAKE_C_FLAGS=%22-s -Wno-unused-command-line-argument%22" />
<Exec Command="cmake --build monodroid --config MinSizeRel" />

<ItemGroup>
<NativeLibFiles Include="$(MSBuildThisFileDirectory)monodroid/libnative-lib.so" />
</ItemGroup>

<Copy SourceFiles="@(NativeLibFiles)" DestinationFolder="$(OutputPath)" />

<RemoveDir Directories="$(MSBuildThisFileDirectory)monodroid" />
</Target>

<UsingTask TaskName="AndroidAppBuilderTask"
AssemblyFile="$(PkgMicrosoft_NET_Runtime_Android_Sample_Mono)\tools\net6.0\AndroidAppBuilder.dll" />

<Target Name="BuildApp" AfterTargets="Publish">
<PropertyGroup>
<ApkDir>$(OutputPath)apk\</ApkDir>
<AdbTool>$(ANDROID_SDK_ROOT)\platform-tools\adb</AdbTool>
</PropertyGroup>

<ItemGroup>
<AndroidRuntimePack Include="@(ResolvedRuntimePack)" Exclude="Microsoft.AspNetCore.App.Runtime.linux-$(TargetArchitecture)" />
</ItemGroup>

<RemoveDir Directories="$(ApkDir)" />

<AndroidAppBuilderTask
RuntimeIdentifier="$(RuntimeIdentifier)"
SourceDir="$(OutputPath)"
MonoRuntimeHeaders="%(AndroidRuntimePack.PackageDirectory)\runtimes\$(RuntimeIdentifier)\native\include\mono-2.0"
MainLibraryFileName="$(AssemblyName).dll"
ProjectName="HelloAndroid"
OutputDir="$(ApkDir)"
StripDebugSymbols="True"
KeyStorePath="$(HOME)/.android/"
NativeMainSource="$(MSBuildThisFileDirectory)MainActivity.java">
<Output TaskParameter="ApkBundlePath" PropertyName="ApkBundlePath" />
<Output TaskParameter="ApkPackageId" PropertyName="ApkPackageId" />
</AndroidAppBuilderTask>
</Target>

<Target Name="LaunchApp" AfterTargets="BuildApp" Condition="'$(RunningOnCI)' == ''">
<Exec Command="$(AdbTool) kill-server" />
<Exec Command="$(AdbTool) start-server" />
<Exec Command="$(AdbTool) logcat -c" ContinueOnError="WarnAndContinue" />
<Exec Command="$(AdbTool) install $(ApkDir)/bin/HelloAndroid.apk" />
<Exec Command="$(AdbTool) shell am start -n net.dot.HelloAndroid/net.dot.MainActivity" />
</Target>
</Project>
12 changes: 12 additions & 0 deletions core/mono-samples/Android/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
cmake_minimum_required(VERSION 3.10)

project(buildnativelib)

add_library(
native-lib
SHARED
native-lib.c)

target_link_libraries(
native-lib
log)
123 changes: 123 additions & 0 deletions core/mono-samples/Android/MainActivity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

package net.dot;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.graphics.Color;

public class MainActivity extends Activity
{
static
{
System.loadLibrary("native-lib");
}

// Called from JNI
public static int getNum()
{
return 42;
}

@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);

RelativeLayout rootLayout = new RelativeLayout(this);

final TextView welcomeMsg = new TextView(this);
welcomeMsg.setId(1);
welcomeMsg.setTextSize(20);
RelativeLayout.LayoutParams welcomeMsgParams =
new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT);
welcomeMsgParams.addRule(RelativeLayout.CENTER_HORIZONTAL);
welcomeMsgParams.addRule(RelativeLayout.ALIGN_TOP);
rootLayout.addView(welcomeMsg, welcomeMsgParams);

final TextView clickCounter = new TextView(this);
clickCounter.setId(2);
clickCounter.setTextSize(20);
RelativeLayout.LayoutParams clickCounterParams =
new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT);
clickCounterParams.addRule(RelativeLayout.CENTER_HORIZONTAL);
clickCounterParams.addRule(RelativeLayout.CENTER_VERTICAL);
rootLayout.addView(clickCounter, clickCounterParams);

Button button = new Button(this);
button.setId(3);
button.setText("Click me");
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
clickCounter.setText(incrementCounter());
}
});
RelativeLayout.LayoutParams buttonParams =
new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT);
buttonParams.addRule(RelativeLayout.BELOW, clickCounter.getId());
buttonParams.addRule(RelativeLayout.CENTER_HORIZONTAL);
rootLayout.addView(button, buttonParams);

EditText textField = new EditText(this);
textField.setId(4);
textField.setTextSize(20);
RelativeLayout.LayoutParams textFieldParams =
new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT);
textFieldParams.addRule(RelativeLayout.CENTER_HORIZONTAL);

Button enterName = new Button(this);
enterName.setId(5);
enterName.setText("Enter");
enterName.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
welcomeMsg.setText(greetName(textField.getText().toString()));
}
});
RelativeLayout.LayoutParams enterNameParams =
new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT);
enterNameParams.addRule(RelativeLayout.CENTER_HORIZONTAL);
enterNameParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
rootLayout.addView(enterName, enterNameParams);

textFieldParams.addRule(RelativeLayout.ABOVE, enterName.getId());
rootLayout.addView(textField, textFieldParams);

setContentView(rootLayout);

final String entryPointLibName = "AndroidSampleApp.dll";
welcomeMsg.setText("Running " + entryPointLibName + "...");
clickCounter.setText("Hello World!");
final Activity ctx = this;
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
int retcode = MonoRunner.initialize(entryPointLibName, ctx);
welcomeMsg.setText("Hello Android " + retcode + "!\nRunning on mono runtime\nUsing C#");
}
}, 1000);
}

public native String incrementCounter();

public native String greetName(String text);
}
6 changes: 6 additions & 0 deletions core/mono-samples/Android/NuGet.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="dotnet6" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json" />
</packageSources>
</configuration>
32 changes: 32 additions & 0 deletions core/mono-samples/Android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
languages:
- csharp
products:
- dotnet-core
page_type: sample
name: "Android Sample: Simple greeting and counter (C#)"
description: "An Android application that contains an example of embedding the mono runtime to invoke unmanaged code with C#."
urlFragment: "mono-android-csharp"
---

# Android Sample: Simple greeting and counter (C#)

In this sample, the mono runtime is used to invoke C unmanaged code (native-lib.c) from the C# managed side (AndroidSampleApp.cs) and vice versa. Then through the Java Native Interface (JNI), the java code (MainActivity.java) can call and be called by the C unamanged code. With the sample running, you can enter your name and click the corresponding button to modify the greeting message as well as clicking a button to increment a counter.

[!NOTE]
The purpose of this sample is to demonstrate the concept of building an Android application on top of the mono runtime. The mono runtime headers should be supplied through the build process.
If you wish to use your own activity file, set `NativeMainSource` in `AndroidSampleApp.csproj` to point at your activity file, and the class is currently restricted to be called `MainActivity`.
When running an activity in a non-UI thread, `FindClass` in `native-lib.c` will not work.

## Sample Prerequisites

- ANDROID_NDK & ANDROID_SDK (<https://github.com/dotnet/runtime/blob/master/docs/workflow/testing/libraries/testing-android.md> right under `Testing Libraries on Android`).
- Android simulator API 21 or greater.
- Dotnet sdk 6.0.100-alpha.1.20567.4 (Installation instructions in parent directory).

## Building the sample

The source code includes an MSBuild project file for C# (a _.csproj_ file) that targets .NET 6.0. After downloading the _.zip_ file, be sure to have the Android simulator open. Mdify the `TargetArchitecture` property in `AndroidSampleApp.csproj` to your simulator's architecture (x64, x86, arm, arm64). To run the sample, open the command line, navigate to the downloaded folder, and run `dotnet publish`.

[!NOTE]
To view the application's logs, use `<YOUR_ANDROID_SDK_ROOT>\platform-tools\adb logcat -d` after publishing.
67 changes: 67 additions & 0 deletions core/mono-samples/Android/native-lib.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#include <jni.h>

char *(*incrementHandlerPtr)(void);
char *(*greetHandlerPtr)(const char *);

JNIEXPORT jstring JNICALL
Java_net_dot_MainActivity_incrementCounter(
JNIEnv* env,
jobject thisObject) {
char *s = "Hello";
if (incrementHandlerPtr)
s = incrementHandlerPtr();
return (*env) -> NewStringUTF(env, s);
}

JNIEXPORT jstring JNICALL
Java_net_dot_MainActivity_greetName(
JNIEnv* env,
jobject thisObject,
jstring string) {
char *s = "Hello Android!\nRunning on mono runtime\nUsing C#";
const char *name = (*env)->GetStringUTFChars(env, string, NULL);
if (greetHandlerPtr)
s = greetHandlerPtr(name);
return (*env) -> NewStringUTF(env, s);
}

static JavaVM *gJvm;

JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved)
{
(void)reserved;
gJvm = vm;
return JNI_VERSION_1_6;
}

static JNIEnv* GetJniEnv()
{
JNIEnv *env;
(*gJvm)->GetEnv(gJvm, (void**)&env, JNI_VERSION_1_6);
if (env)
return env;
(*gJvm)->AttachCurrentThread(gJvm, &env, NULL);
return env;
}

void
androidRegisterCounterIncrement (void* ptr)
{
incrementHandlerPtr = ptr;
}

void
androidRegisterNameGreet (void* ptr)
{
greetHandlerPtr = ptr;
}

int myNum() {
JNIEnv* jniEnv = GetJniEnv();

jclass mainActivity = (*jniEnv)->FindClass (jniEnv, "net/dot/MainActivity");
jmethodID getNum = (*jniEnv)->GetStaticMethodID(jniEnv, mainActivity, "getNum", "()I");
jint val = (*jniEnv)->CallStaticIntMethod(jniEnv, mainActivity, getNum);
return val;
}
Loading