You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
732 lines
26 KiB
732 lines
26 KiB
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> |
|
<meta name="viewport" content="width=1024, user-scalable=no"> |
|
|
|
<title>Your deck.js Presentation</title> |
|
|
|
<!-- Required stylesheet --> |
|
<link rel="stylesheet" media="screen" href="core/deck.core.css"> |
|
|
|
<!-- Extension CSS files go here. Remove or add as needed. --> |
|
<link rel="stylesheet" media="screen" href="extensions/goto/deck.goto.css"> |
|
<link rel="stylesheet" media="screen" href="extensions/menu/deck.menu.css"> |
|
<link rel="stylesheet" media="screen" href="extensions/navigation/deck.navigation.css"> |
|
<link rel="stylesheet" media="screen" href="extensions/status/deck.status.css"> |
|
<!-- <link rel="stylesheet" media="screen" href="extensions/scale/deck.scale.css"> --> |
|
|
|
<!-- Style theme. More available in /themes/style/ or create your own. --> |
|
<link rel="stylesheet" media="screen" href="themes/style/swiss.css"> |
|
|
|
<!-- Transition theme. More available in /themes/transition/ or create your own. --> |
|
<link rel="stylesheet" media="screen" href="themes/transition/horizontal-slide.css"> |
|
|
|
<!-- Basic black and white print styles --> |
|
<link rel="stylesheet" media="print" href="core/print.css"> |
|
|
|
<!-- Required Modernizr file --> |
|
<script src="modernizr.custom.js"></script> |
|
</head> |
|
<body style="margin:0px;padding:0px;overflow:hidden"> |
|
<div class="deck-container"> |
|
|
|
<!-- Begin slides. Just make elements with a class of slide. --> |
|
|
|
<section class="slide"> |
|
<h2>Android for Gio programmers</h2> <br></br> |
|
<h3>Native Development Kit (NDK): direct access from cgo</h3> |
|
<ul><li>limited Android API access</li></ul> |
|
<h3>Java Native Interface: call Java code via cgo</h3> |
|
<ul><li>custom Java classes</li></ul> |
|
<h3>Java fragments: extend the main Gio Activity</h3> |
|
<ul><li>permissions and application lifecycle</li></ul> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>NDK — sensor.h</h2> |
|
<p>$ANDROID_HOME/ndk-bundle/sysroot/usr/include/android/sensor.h</p> |
|
<pre style="font-size: 90%"><code># 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; |
|
</code></pre> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>NDK — nswrap</h2> |
|
<img src="nswrap-git.png"></img> |
|
</section> |
|
|
|
<!-- |
|
<section class="slide"> |
|
<h2>NDK android27.yaml</h2> |
|
<p>nswrap configuration: android27.yaml</p> |
|
<pre style="font-size: 65%"><code>package: android27 |
|
libraries: [android, log, nativewindow, m] |
|
inputfiles: |
|
- $ANDROID_HOME/ndk-bundle/sysroot/usr/include/android/native_window.h |
|
- $ANDROID_HOME/ndk-bundle/sysroot/usr/include/android/log.h |
|
- $ANDROID_HOME/ndk-bundle/sysroot/usr/include/android/sensor.h |
|
- $ANDROID_HOME/ndk-bundle/sysroot/usr/include/jni.h |
|
- $ANDROID_HOME/ndk-bundle/sysroot/usr/include/sys/stat.h |
|
sysimports: |
|
- android/native_window.h |
|
- android/log.h |
|
- android/sensor.h |
|
- jni.h |
|
- stdlib.h |
|
- sys/stat.h |
|
functions: |
|
- .* |
|
functionignore: |
|
- JNI_CreateJavaVM |
|
- JNI_GetCreatedJavaVMs |
|
- JNI_GetDefaultJavaVMInitArgs |
|
- JNI_OnLoad |
|
- JNI_OnUnload |
|
- AHardwareBuffer_lockPlanes |
|
- wcstoimax |
|
- wcstoumax |
|
- imaxdiv |
|
# - __system_property_get |
|
enums: |
|
- .* |
|
typesubs: |
|
Stat: Stat_t |
|
Stat64: Stat64_t |
|
clang: $ANDROID_HOME/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android27-clang |
|
</code></pre> |
|
</section> |
|
|
|
<section class = "slide"> |
|
<h2>NDK — Clang AST</h2> |
|
<p>nswrap reads the Clang ast</p> |
|
<pre><code>... |
|
|-TypedefDecl 0x7fac6e838220 <line:480:1, col:31> col:31 referenced ASensorManager 'struct ASensorManager':'struct ASensorManager' |
|
| |-ElaboratedType 0x7fac6e8371d0 'struct ASensorManager' sugar |
|
| | `-RecordType 0x7fac6e837180 'struct ASensorManager' |
|
| | `-Record 0x7fac6e837100 'ASensorManager' |
|
| `-FullComment 0x7fac6e8468a0 <line:465:3, col:10> |
|
| |-ParagraphComment 0x7fac6e8465a0 <col:3, col:4> |
|
| | `-TextComment 0x7fac6e846570 <col:3, col:4> Text=" {" |
|
| `-VerbatimBlockComment 0x7fac6e8465c0 <col:5, col:10> Name="link" CloseName="" |
|
| |-VerbatimBlockLineComment 0x7fac6e846610 <col:10, col:66> Text=" ASensorManager} is an opaque type to manage sensors and" |
|
| |-VerbatimBlockLineComment 0x7fac6e846630 <line:466:3, col:18> Text=" events queues." |
|
... |
|
|-FunctionDecl 0x7fac6e838b28 <line:568:1, versioning.h:19:89> sensor.h:568:17 ASensorManager_getInstanceForPackage 'ASensorManager *(const char *)' |
|
| |-ParmVarDecl 0x7fac6e838a00 <col:54, col:66> col:66 packageName 'const char *' |
|
| |-AnnotateAttr 0x7fac6e838bc8 <versioning.h:19:51, col:87> "introduced_in=26" |
|
</code></pre> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>NDK — nswrap output</h2> |
|
<p>nswrap generates 1270 lines of Go code</p> |
|
<p>android27/main.go</p> |
|
<pre style="font-size:75%"><code>package android27 |
|
... |
|
type ASensorManager = C.struct_ASensorManager |
|
|
|
const ASENSOR_TYPE_INVALID = C.ASENSOR_TYPE_INVALID |
|
const ASENSOR_TYPE_ACCELEROMETER = C.ASENSOR_TYPE_ACCELEROMETER |
|
const ASENSOR_TYPE_MAGNETIC_FIELD = C.ASENSOR_TYPE_MAGNETIC_FIELD |
|
const ASENSOR_TYPE_GYROSCOPE = C.ASENSOR_TYPE_GYROSCOPE |
|
... |
|
|
|
func ASensorManagerGetInstanceForPackage(packageName *Char) *ASensorManager { |
|
ret := (*ASensorManager)(unsafe.Pointer(C.ASensorManager_getInstanceForPackage((*C.char)(packageName)))) |
|
return ret |
|
} |
|
|
|
func ASensorManagerGetDefaultSensor(manager *ASensorManager, type_ Int) *ASensor { |
|
ret := (*ASensor)(unsafe.Pointer(C.ASensorManager_getDefaultSensor((*C.ASensorManager)(manager), (C.int)(type_)))) |
|
return ret |
|
} |
|
</code></pre> |
|
</section> |
|
--> |
|
|
|
<section class="slide"> |
|
<h2>NDK — Android-specific Go code</h2> |
|
<p>os_android.go</p> |
|
<pre style="font-size: 65%"><code>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() |
|
... |
|
</code></pre> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>NDK — Android main polling loop</h2> |
|
<p>os_android.go (86 total lines)</p> |
|
<pre style="font-size:75%"><code>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())} |
|
} |
|
} |
|
</code></pre> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>NDK — Gio app setup</h2> |
|
<p>main.go</p> |
|
<pre style="font-size: 70%"><code>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 |
|
} |
|
... |
|
</code></pre> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>NDK — Gio app main loop</h2> |
|
<p>main.go (174 total lines)</p> |
|
<pre style="font-size:65%"><code> ... |
|
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) |
|
} |
|
} |
|
} |
|
</code></pre> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>NDK — Building an APK</h2> |
|
<pre><code>$ gogio -target android -minsdk 27 . |
|
$ adb install sensors.apk |
|
</code></pre> |
|
<img src="accel.jpg" style="width:60%"></img> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>JNI — Java Native Interface</h2> |
|
<p>Use JNI from cgo to:</p> |
|
<ul> |
|
<li>Access Android Java APIs (with limitations)</li> |
|
<li>Call methods on your own Java classes</li> |
|
</ul> |
|
<p>To use it: |
|
<ul> |
|
<li>create (or acquire) a Java Virtual Machine (JavaVM)</li> |
|
<li>use the JavaVM to obtain a JNI Native Interface struct (JNIEnv)</li> |
|
<li>call function pointers inside the JNIEnv struct</li> |
|
</ul> |
|
<p>$ANDROID_HOME/ndk-bundle/sysroot/usr/include/jni.h</p> |
|
<pre style="font-size:65%"><code>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); |
|
... |
|
</code></pre> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>JNI — git.wow.st/gmp/jni</h2> |
|
<iframe frameborder="0" src="jni/jni.html" style="transform:scale(1.5); transform-origin: 50% 0%;" width=800 height=13300></iframe> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>JNI — Basics</h2> |
|
<p>my/java/AClass.java:</p> |
|
<pre style="font-size:60%"><code>package my.java; |
|
|
|
public class AClass { |
|
public int Num() { |
|
return 17; |
|
} |
|
} |
|
</code></pre> |
|
|
|
<p>main.go:</p> |
|
<pre style="font-size:60%"><code>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()) |
|
} |
|
} |
|
</code></pre> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>JNI — Using JNI on Android</h2> |
|
<p><code>gogio</code> looks for <code>.jar</code> files in your package |
|
directory and across all imports. Every class from these <code>.jar</code> files |
|
will be bundled with your <code>apk</code></p> |
|
<pre style="font-size:65%"><code>$ 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 |
|
... |
|
</code></pre> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>JNI — JNI Limitations on Android</h2> |
|
<p>Key limitation: cannot override <code>Activity</code> methods:</p> |
|
<ul> |
|
<li><code>onActivityResult</code></li> |
|
<li><code>onRequestPermissionResult</code></li> |
|
<li>Lifecycle methods: <code>onCreate</code>, <code>onDestroy</code>, etc.</li> |
|
</ul> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>Java Fragments</h2> |
|
<p>There are lots of gaps in the NDK and limitations with JNI. |
|
Fragments can access the rest.</p> |
|
<img src="fragment.png"></img> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>Java Fragments — Basics</h2> |
|
<p>os_android.go</p> |
|
<pre style="font-size:61%"><code>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 { ... } |
|
}) |
|
} |
|
</code></pre> |
|
<p>AFrag.java</p> |
|
<pre style="font-size:61%"><code>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); |
|
} |
|
} |
|
</code></pre> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>Java Fragments — Permissions (Java)</h2> |
|
<p>Here we are going to connect to Bluetooth low-energy peripherals using a |
|
library called Blessed (<a href="https://github.com/weliem/blessed-android">https://github.com/weliem/blessed-android</a>)</p> |
|
<p>We need permission to access Bluetooth hardware, which, on Android, |
|
requies <code>ACCESS_FINE_LOCATION</code>, which is a "dangerous" permission. See: |
|
<a href="https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION">developer.android.com/reference/android/Manifest.permission</a></p> |
|
<p>We need to request access to this permission at runtime.</p> |
|
<p>BlessedConnect.java:</p> |
|
<pre style="font-size:57%"><code>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); |
|
} |
|
</code></pre> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>Java Fragments — Permissions (Go)</h2> |
|
<p>We need to ask <code>gogio</code> to add all required permissions to |
|
AndroidManifest.xml</p> |
|
<p>os_android.go:</p> |
|
<pre style="font-size:70%"><code>import ( |
|
"gioui.org/app" |
|
_ "gioui.org/app/permission/bluetooth" |
|
) |
|
</code></pre> |
|
<p>AndroidManifest.xml:</p> |
|
<pre style="font-size:70%"><code> |
|
<?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> |
|
</code></pre> |
|
</section> |
|
|
|
<section class ="slide"> |
|
<h2>Java Fragments — Gio app</h2> |
|
<p>main.go is 482 lines. No platform-specific code is required in the Gio app.</p> |
|
<pre style="font-size:65%"><code>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) |
|
} |
|
} |
|
} |
|
</code></pre> |
|
</section> |
|
|
|
<section class="slide"> |
|
<h2>Java Fragments — BLE API</h2> |
|
<pre><code>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) |
|
</code></pre> |
|
<img src="ble.png" style="width:70%"></img> |
|
</section> |
|
|
|
<section class ="slide"> |
|
<h2>Links</h2> |
|
<ol> |
|
<p><a href="https://git.wow.st/gmp/nswrap">git.wow.st/gmp/nswrap</a></p> |
|
<p><a href="https://git.wow.st/gmp/android-go">git.wow.st/gmp/android-go</a></p> |
|
<p><a href="https://git.wow.st/gmp/hrm">git.wow.st/gmp/jni</a></p> |
|
<p><a href="https://git.wow.st/gmp/ble">git.wow.st/gmp/ble</a></p> |
|
<p><a href="https://git.wow.st/gmp/hrm">git.wow.st/gmp/hrm</a></p> |
|
<p><a href="https://git.wow.st/gmp/passgo">git.wow.st/gmp/passgo</a></p> |
|
</ol> |
|
</section> |
|
<!-- End slides. --> |
|
|
|
<!-- Begin extension snippets. Add or remove as needed. --> |
|
|
|
<!-- deck.navigation snippet --> |
|
<div aria-role="navigation"> |
|
<a href="#" class="deck-prev-link" title="Previous">←</a> |
|
<a href="#" class="deck-next-link" title="Next">→</a> |
|
</div> |
|
|
|
<!-- deck.status snippet --> |
|
<p class="deck-status" aria-role="status"> |
|
<span class="deck-status-current"></span> |
|
/ |
|
<span class="deck-status-total"></span> |
|
</p> |
|
|
|
<!-- deck.goto snippet --> |
|
<form action="." method="get" class="goto-form"> |
|
<label for="goto-slide">Go to slide:</label> |
|
<input type="text" name="slidenum" id="goto-slide" list="goto-datalist"> |
|
<datalist id="goto-datalist"></datalist> |
|
<input type="submit" value="Go"> |
|
</form> |
|
|
|
<!-- End extension snippets. --> |
|
</div> |
|
|
|
<!-- Required JS files. --> |
|
<script src="jquery.min.js"></script> |
|
<script src="core/deck.core.js"></script> |
|
|
|
<!-- Extension JS files. Add or remove as needed. --> |
|
<script src="extensions/menu/deck.menu.js"></script> |
|
<script src="extensions/goto/deck.goto.js"></script> |
|
<script src="extensions/status/deck.status.js"></script> |
|
<script src="extensions/navigation/deck.navigation.js"></script> |
|
<!-- <script src="extensions/scale/deck.scale.js"></script> --> |
|
|
|
<!-- Initialize the deck. You can put this in an external file if desired. --> |
|
<script> |
|
$(function() { |
|
$.deck('.slide'); |
|
}); |
|
</script> |
|
</body> |
|
</html>
|
|
|