## Android for Gio programmers

### Native Development Kit (NDK): direct access from cgo

- limited Android API access

### Java Native Interface: call Java code via cgo

- custom Java classes

### Java fragments: extend the main Gio Activity

- permissions and application lifecycle

## NDK — sensor.h

$ANDROID_HOME/ndk-bundle/sysroot/usr/include/android/sensor.h

    # android/sensor.h
    #if __ANDROID_API__ >= 26
    /**
     * Get a reference to the sensor manager. ASensorManager is a singleton
     * per package as different packages may have access to different sensors.
     *
     * Example:
     *
     *     ASensorManager* sensorManager = ASensorManager_getInstanceForPackage("foo.bar .baz");
     *
     */
    ASensorManager* ASensorManager_getInstanceForPackage(const char* packageName) __INTRODUCED_IN(26);
    #endif

    /**
     * Returns the default sensor for the given type, or NULL if no sensor
     * of that type exists.
     */
    ASensor const* ASensorManager_getDefaultSensor(ASensorManager* manager, int type);

    /**
     * {@link ASensorManager} is an opaque type to manage sensors and
     * events queues.
     *
     * {@link ASensorManager} is a singleton that can be obtained using
     * ASensorManager_getInstance().
     *
     * This file provides a set of functions that uses {@link
     * ASensorManager} to access and list hardware sensors, and
     * create and destroy event queues:
     * - ASensorManager_getSensorList()
     * - ASensorManager_getDefaultSensor()
     * - ASensorManager_getDefaultSensorEx()
     * - ASensorManager_createEventQueue()
     * - ASensorManager_destroyEventQueue()
     */
    typedef struct ASensorManager ASensorManager;

## NDK — nswrap

## NDK — Android-specific Go code

os_android.go

    import ndk "git.wow.st/gmp/android-go/android27"

    func sensorLoop() {
        // the Android Looper is thread-local
        runtime.LockOSThread()

        pkg := ndk.CharWithGoString("st.wow.git.sensors")
        manager := ndk.ASensorManagerGetInstanceForPackage(pkg)
        pkg.Free()

        sens := ndk.ASensorManagerGetDefaultSensor(manager, ndk.ASENSOR_TYPE_ACCELEROMETER)
        stp := ndk.ASensorGetStringType(sens)
        labchan <- "sensor" + stp.String()

        var looper *ndk.ALooper
        var queue *ndk.ASensorEventQueue
        looper_id := 1
        var rate ndk.Int32_t = 60

        setup := func() {
            looper = ndk.ALooperForThread()
            if (looper == nil) {
                labchan <- "no looper for thread"
                looper = ndk.ALooperPrepare(ndk.ALOOPER_PREPARE_ALLOW_NON_CALLBACKS)
            }

            queue = ndk.ASensorManagerCreateEventQueue(manager, looper, looper_id)

            ndk.ASensorEventQueueEnableSensor(queue, sens)
            ndk.ASensorEventQueueSetEventRate(queue, sens, 1000000/rate) // microseconds
        }

        setup()
    ...

## NDK — Android main polling loop

os_android.go (86 total lines)

    func sensorLoop() {
       ...

       for {
            var zero ndk.Int
            id := (int)(ndk.ALooperPollOnce(-1, &zero, &zero, (*unsafe.Pointer)(unsafe.Pointer(nil)))) // poll forever
            if (id == ndk.ALOOPER_POLL_ERROR) { // set up a new looper
                labchan <- "getting a new looper"
                setup()
                continue
            }
            if (id == looper_id) {
                var event ndk.ASensorEvent
                if (ndk.ASensorEventQueueGetEvents(queue, &event, 1) != 1) {
                    continue
                }
                accel := (&event).Acceleration()
                senschan <- vector{float64(accel.X()), float64(accel.Y()), float64(accel.Z())}
            }
        }

## NDK — Gio app setup

main.go

    func eventloop() {
        w := app.NewWindow( app.Size(unit.Dp(400), unit.Dp(400)), app.Title("Hello"))
        th := material.NewTheme(gofont.Collection())
        var ops op.Ops

        var accel vector

        var cx, cy, cvx, cvy, cs float32     // circle position, velocity and radius
        cx = 200
        cy = 200
        cs = 50                    // circle size (radius)

        circle := func(gtx C, width, height float32) D {
            // clip circle position and bounce off of the edges
            if (cx < cs) {   cx = cs;   cvx = (-0.5) * cvx }
            if (cy < cs) {   cy = cs;   cvy = (-0.5) * cvy }
            if (cx > width - cs) {  cx = width - cs;  cvx = (-0.5) * cvx }
            if (cy > height - cs) { cy = height - cs; cvy = (-0.5) * cvy }

            blue := color.RGBA{0x3f, 0x51, 0xb5, 0x80}
            r1 := f32.Rectangle{f32.Point{cx - cs, cy - cs}, f32.Point{cx + cs, cy + cs}}
            clip.Rect{ Rect: r1, NE: cs, NW: cs, SE: cs, SW: cs}.Op(gtx.Ops).Add(gtx.Ops)
            paint.ColorOp{Color: blue}.Add(gtx.Ops)
            paint.PaintOp{Rect: r1}.Add(gtx.Ops)

            var ret D
            ret.Size.X = int(cs * 2)
            ret.Size.Y = int(cs * 2)
            return ret
        }
        ...

## NDK — Gio app main loop

main.go (174 total lines)

        ...
        ticker := time.NewTicker(time.Second / 60)

        var t, told int64
        t = time.Now().UnixNano()

        for {
            select {
            case <- ticker.C:
                told = t
                t = time.Now().UnixNano()
                elapsed := float32((t - told) / 1000000)

                cvx = cvx - float32(accel.x/1000) * elapsed
                cvy = cvy + float32(accel.y/1000) * elapsed

                cx = cx + cvx * elapsed
                cy = cy + cvy * elapsed
                w.Invalidate()

            case x := <-senschan:
                accel = x

            case e := <-w.Events():
                switch e := e.(type) {
                case system.FrameEvent:
                    gtx := layout.NewContext(&ops, e)
                    ... // draw everything
                    e.Frame(gtx.Ops)
                }
            }
        }

## NDK — Building an APK

    $ gogio -target android -minsdk 27 .
    $ adb install sensors.apk

## JNI — Java Native Interface

Use JNI from cgo to:

- Access Android Java APIs (with limitations)
- Call methods on your own Java classes

To use it:

- create (or acquire) a Java Virtual Machine (JavaVM)
- use the JavaVM to obtain a JNI Native Interface struct (JNIEnv)
- call function pointers inside the JNIEnv struct

$ANDROID_HOME/ndk-bundle/sysroot/usr/include/jni.h

    typedef const struct JNINativeInterface* JNIEnv;
    typedef const struct JNIInvokeInterface* JavaVM;

    /*
     * Table of interface function pointers.
     */
    struct JNINativeInterface {
        jobject     (*NewObject)(JNIEnv*, jclass, jmethodID, ...);
        jclass      (*GetObjectClass)(JNIEnv*, jobject);
        jboolean    (*IsInstanceOf)(JNIEnv*, jobject, jclass);
        jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);

        jobject     (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);
        jboolean    (*CallBooleanMethod)(JNIEnv*, jobject, jmethodID, ...);
        jbyte       (*CallByteMethod)(JNIEnv*, jobject, jmethodID, ...);
        jchar       (*CallCharMethod)(JNIEnv*, jobject, jmethodID, ...);
        jshort      (*CallShortMethod)(JNIEnv*, jobject, jmethodID, ...);
        jint        (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
        jlong       (*CallLongMethod)(JNIEnv*, jobject, jmethodID, ...);
        jfloat      (*CallFloatMethod)(JNIEnv*, jobject, jmethodID, ...);
        jdouble     (*CallDoubleMethod)(JNIEnv*, jobject, jmethodID, ...);
        void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);

        jobject     (*GetObjectField)(JNIEnv*, jobject, jfieldID);
        jboolean    (*GetBooleanField)(JNIEnv*, jobject, jfieldID);
        jbyte       (*GetByteField)(JNIEnv*, jobject, jfieldID);
        jchar       (*GetCharField)(JNIEnv*, jobject, jfieldID);
        jshort      (*GetShortField)(JNIEnv*, jobject, jfieldID);
        jint        (*GetIntField)(JNIEnv*, jobject, jfieldID);
        jlong       (*GetLongField)(JNIEnv*, jobject, jfieldID);
        jfloat      (*GetFloatField)(JNIEnv*, jobject, jfieldID);
        jdouble     (*GetDoubleField)(JNIEnv*, jobject, jfieldID);
       ...

## JNI — git.wow.st/gmp/jni

## JNI — Basics

my/java/AClass.java:

    package my.java;

    public class AClass {
            public int Num() {
                    return 17;
            }
    }

main.go:

    package main
    //go:generate javac my/java/AClass.java

    import (
            "fmt"
            "git.wow.st/gmp/jni"
    )

    func main() {
            vm := jni.CreateJavaVM()
            err := jni.Do(vm, func(env jni.Env) error {
                    cls := jni.FindClass(env, "my/java/AClass")
                    if cls == 0 {
                            return fmt.Errorf("Class not found")
                    }
                    mid := jni.GetMethodID(env, cls, "<init>", "()V")
                    if mid == nil {
                            return fmt.Errorf("Initializer not found")
                    }
                    inst, err := jni.NewObject(env, cls, mid)
                    if err != nil {
                            return err
                    }
                    mid = jni.GetMethodID(env, cls, "Num", "()I")
                    if mid == nil {
                            return fmt.Errorf("Method not found")
                    }
                    res, err := jni.CallIntMethod(env, inst, mid)
                    if err != nil {
                            return err
                    }
                    fmt.Printf("Result: %d\n", res)
                    return nil
            })
            if err != nil {
                    fmt.Printf(err.Error())
            }
    }

## JNI — Using JNI on Android

`gogio` looks for `.jar` files in your package directory and across all imports. Every class from these `.jar` files will be bundled with your `apk`

    $ apktool d hrm.apk
    $ find hrm/smali -type f
    hrm/smali/org/gioui/GioView$1.smali
    hrm/smali/org/gioui/GioView.smali
    hrm/smali/org/gioui/GioView$3.smali
    hrm/smali/org/gioui/GioView$4.smali
    hrm/smali/org/gioui/GioView$2.smali
    hrm/smali/org/gioui/Gio.smali
    hrm/smali/org/gioui/GioActivity.smali
    hrm/smali/org/gioui/Gio$1.smali
    hrm/smali/org/gioui/GioView$InputConnection.smali
    hrm/smali/st/wow/git/ble/BlessedConnect$3.smali
    hrm/smali/st/wow/git/ble/BlessedConnect$1.smali
    hrm/smali/st/wow/git/ble/BlessedConnect$2.smali
    hrm/smali/st/wow/git/ble/BlessedConnect.smali
    hrm/smali/timber/log/Timber.smali
    hrm/smali/timber/log/Timber$Tree.smali
    hrm/smali/timber/log/Timber$DebugTree.smali
    hrm/smali/timber/log/Timber$1.smali
    hrm/smali/com/welie/blessed/BluetoothPeripheral$1$9.smali
    hrm/smali/com/welie/blessed/BluetoothPeripheral$11.smali
    hrm/smali/com/welie/blessed/BluetoothCentral$4.smali
    ...

## JNI — JNI Limitations on Android

Key limitation: cannot override `Activity` methods:

- `onActivityResult`
- `onRequestPermissionResult`
- Lifecycle methods: `onCreate`, `onDestroy`, etc.

## Java Fragments

There are lots of gaps in the NDK and limitations with JNI. Fragments can access the rest.

## Java Fragments — Basics

os_android.go

    import (
    	"gioui.org/app"
    	"git.wow.st/gmp/jni"
    )

    func registerFragment(w *app.Window) {
            vm := jni.JVMFor(app.JavaVM())

            w.Do(func(view uintptr) {
                    err := jni.Do(vm, func(env jni.Env) error {
                            cls := jni.FindClass(env, "st/wow/git/fragment/AFrag")
                            mth := jni.GetMethodID(env, cls, "<init>", "()V");
                            inst, err := jni.NewObject(env, cls, mth)
                            mth = jni.GetMethodID(env, cls, "register", "(Landroid/view/View;)V");
                            jni.CallVoidMethod(env, inst, mth, jni.Value(view))
                            return nil
                    })
    		if err != nil { ... }
            })
    }

AFrag.java

    package com.afrag.AFrag;

    import android.app.Activity;
    import android.app.Fragment;
    import android.app.FragmentTransaction;
    import android.content.Context;
    import android.util.Log;
    import android.view.View;

    public class AFrag extends Fragment {
            public void register(View view) {
                    Activity act = (Activity)view.getContext();
                    FragmentTransaction ft = act.getFragmentManager().beginTransaction();
                    ft.add(this, "AFrag");
                    ft.commitNow();
            }

            @Override public void onAttach(Context ctx) {
                    super.onAttach(ctx);
    	}
    }

## Java Fragments — Permissions (Java)

Here we are going to connect to Bluetooth low-energy peripherals using a library called Blessed (https://github.com/weliem/blessed-android)

We need permission to access Bluetooth hardware, which, on Android, requies `ACCESS_FINE_LOCATION`, which is a "dangerous" permission. See: developer.android.com/reference/android/Manifest.permission

We need to request access to this permission at runtime.

BlessedConnect.java:

    package st.wow.git.ble;

    import android.app.Fragment;

    import com.welie.blessed.BluetoothCentral;
    import com.welie.blessed.BluetoothCentralCallback;
    import com.welie.blessed.BluetoothPeripheral;
    import com.welie.blessed.BluetoothPeripheralCallback;

    public class BlessedConnect extends Fragment {
        final int PERMISSION_REQUEST = 1;
        final int REQUEST_ENABLE_BT = 2;

        @Override public void onAttach(Context ctx) {
            super.onAttach(ctx);
            if (ctx.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQUEST);
            }
            System.loadLibrary("gio");
            installComplete(this);
        }

        public void onRequestPermissionsResult (int requestCode, String[] permissions, int[] grantResults) {
            if (requestCode == PERMISSION_REQUEST) {
                boolean granted = true;
                for (int x : grantResults) {
                    if (x == PackageManager.PERMISSION_DENIED) {
                        granted = false;
                        break;
                    }
                }
            }
        }

        static private native void installComplete(BlessedConnect p);
    }

## Java Fragments — Permissions (Go)

We need to ask `gogio` to add all required permissions to AndroidManifest.xml

os_android.go:

    import (
        "gioui.org/app"
        _ "gioui.org/app/permission/bluetooth"
    )

AndroidManifest.xml:

    <?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="29" android:compileSdkVersionCodename="10" package="git.wow.st.hrm" platformBuildVersionCode="29" platformBuildVersionName="10">
        <uses-permission android:name="android.permission.BLUETOOTH"/>
        <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
        <uses-feature android:glEsVersion="0x00030000" android:required="false"/>
        <uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
        <uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
        <application android:icon="@mipmap/ic_launcher" android:label="Hrm">
            <activity android:configChanges="keyboardHidden|orientation" android:label="Hrm" android:name="org.gioui.GioActivity" android:theme="@style/Theme.GioApp" android:windowSoftInputMode="adjustResize">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN"/>
                    <category android:name="android.intent.category.LAUNCHER"/>
                </intent-filter>
            </activity>
        </application>
    </manifest>

## Java Fragments — Gio app

main.go is 482 lines. No platform-specific code is required in the Gio app.

    import (
        "gioui.org/app"
        "git.wow.st/gmp/ble"
    )

    func eventloop()
        w := app.NewWindow()
        ...
        b := ble.NewBLE()
        b.Enable(w)

        select {
            case e := <-b.Events():
                switch e := e.(type) {
                case ble.UpdateStateEvent: ...
                case ble.DiscoverPeripheralEvent: ...
                case ble.ConnectEvent: ...
                case ble.ConnectTimeoutEvent: ...
                case ble.DiscoverServiceEvent: ...
                case ble.DiscoverCharacteristicEvent: ...
                case ble.UpdateValueEvent: ...
                }
                w.Invalidate()
            case e := <-w.Events():
                switch e := e.(type) {
                case system.DestroyEvent:
                    return
                case system.FrameEvent:
                    gtx := layout.NewContext(&ops, e)
                    ... // draw everything
                    e.Frame(gtx.Ops)
                }
            }
        }

## Java Fragments — BLE API

    func (b *BLE) Enable(w *app.Window)
    func (b *BLE) State() string
    func (b *BLE) Events() chan interface{}
    func (b *BLE) Scan()
    func (b *BLE) StopScan()
    func (p Peripheral) DiscoverServices()
    func (p Peripheral) DiscoverCharacteristics(serv Service)
    func (p Peripheral) SetNotifyValue(c Characteristic)
    func (b *BLE) Connect(p Peripheral) bool
    func CancelConnection(p Peripheral)
    func Disconnect(p Peripheral)

## Links

git.wow.st/gmp/nswrap

git.wow.st/gmp/android-go

git.wow.st/gmp/jni

git.wow.st/gmp/ble

git.wow.st/gmp/hrm

git.wow.st/gmp/passgo

← →

/

Go to slide: