Skip to content

Commit d295b22

Browse files
committed
app, cmd/gogio: add android foreground permission and service
This adds the permission android.permission.FOREGROUND_SERVICE and adds GioForegroundService which creates the tray Notification necessary to implement the Foreground Service. The package foreground includes the method Start, which on android, notifies the system that the program will perform background work and that it shouldn't be killed. It returns a channel that should be closed when the background work is complete. See https://developer.android.com/guide/components/foreground-services and https://developer.android.com/training/notify-user/build-notification Signed-off-by: Masala <[email protected]>
1 parent a699f77 commit d295b22

File tree

7 files changed

+264
-0
lines changed

7 files changed

+264
-0
lines changed

app/GioForegroundService.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// SPDX-License-Identifier: Unlicense OR MIT
2+
3+
package org.gioui;
4+
import android.app.Notification;
5+
import android.app.Service;
6+
import android.app.Notification;
7+
import android.app.Notification.Builder;
8+
import android.app.NotificationChannel;
9+
import android.app.NotificationManager;
10+
import android.app.PendingIntent;
11+
import android.content.Context;
12+
import android.content.Intent;
13+
import android.os.IBinder;
14+
15+
// GioForegroundService implements a Service required to use the FOREGROUND_SERVICE
16+
// permission on Android, in order to run an application in the background.
17+
// See https://developer.android.com/guide/components/foreground-services for
18+
// more details. To add this permission to your application, import
19+
// gioui.org/app/permission/foreground and use the Start method from that
20+
// package to control this service.
21+
public class GioForegroundService extends Service {
22+
private String channelID;
23+
private String channelName;
24+
private String channelDesc;
25+
private int notificationID = 0x42424242;
26+
27+
@Override public int onStartCommand (Intent intent, int flags, int startId) {
28+
// get the channel parameters from intent extras
29+
channelID = intent.getStringExtra("channelID");
30+
channelName = intent.getStringExtra("channelName");
31+
channelDesc = intent.getStringExtra("channelDesc");
32+
notificationID = intent.getIntExtra("notificationID", notificationID);
33+
this.createNotificationChannel();
34+
35+
// create the Intent that will bring GioActivity to foreground
36+
Context ctx = getApplicationContext();
37+
Intent resultIntent = new Intent(ctx, GioActivity.class);
38+
PendingIntent pending = PendingIntent.getActivity(ctx, notificationID, resultIntent, Intent.FLAG_ACTIVITY_CLEAR_TASK);
39+
// create the Notification that Android will display on the tray for this service
40+
Notification.Builder builder = new Notification.Builder(ctx, channelID)
41+
.setContentTitle(channelName)
42+
.setSmallIcon(getResources().getIdentifier("@mipmap/ic_launcher_adaptive", "drawable", getPackageName()))
43+
.setContentText(channelDesc)
44+
.setContentIntent(pending)
45+
.setPriority(Notification.PRIORITY_MIN);
46+
startForeground(notificationID, builder.build());
47+
return START_NOT_STICKY;
48+
}
49+
50+
@Override public IBinder onBind(Intent intent) {
51+
return null;
52+
}
53+
54+
@Override public void onCreate() {
55+
super.onCreate();
56+
}
57+
58+
@Override
59+
public void onTaskRemoved(Intent rootIntent) {
60+
super.onTaskRemoved(rootIntent);
61+
this.deleteNotificationChannel();
62+
stopForeground(true);
63+
this.stopSelf();
64+
}
65+
66+
@Override public void onDestroy() {
67+
this.deleteNotificationChannel();
68+
}
69+
70+
private void deleteNotificationChannel() {
71+
NotificationManager notificationManager = getSystemService(NotificationManager.class);
72+
notificationManager.deleteNotificationChannel(channelName);
73+
}
74+
75+
private void createNotificationChannel() {
76+
// https://developer.android.com/training/notify-user/build-notification#java
77+
NotificationChannel channel = new NotificationChannel(channelID, channelName, NotificationManager.IMPORTANCE_LOW);
78+
channel.setDescription(channelDesc);
79+
NotificationManager notificationManager = getSystemService(NotificationManager.class);
80+
notificationManager.createNotificationChannel(channel);
81+
}
82+
}

app/os_android.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ static jclass jni_GetObjectClass(JNIEnv *env, jobject obj) {
4040
return (*env)->GetObjectClass(env, obj);
4141
}
4242
43+
static jclass jni_FindClass(JNIEnv *env, const char *name) {
44+
return (*env)->FindClass(env, name);
45+
}
46+
47+
static jobject jni_NewObject(JNIEnv *env, jclass clazz, jmethodID methodID) {
48+
return (*env)->NewObject(env, clazz, methodID);
49+
}
50+
4351
static jmethodID jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
4452
return (*env)->GetMethodID(env, clazz, name, sig);
4553
}
@@ -131,6 +139,10 @@ import (
131139
"gioui.org/unit"
132140
)
133141

142+
const (
143+
foregroundService = "org/gioui/GioForegroundService"
144+
)
145+
134146
type window struct {
135147
callbacks *callbacks
136148

@@ -774,6 +786,59 @@ func getObjectClass(env *C.JNIEnv, obj C.jobject) C.jclass {
774786
return cls
775787
}
776788

789+
// getIntent loads class and returns an android.content.Intent or error
790+
func getIntent(env *C.JNIEnv, obj C.jobject, class string) (C.jobject, error) {
791+
cls := getObjectClass(env, obj) // android.app.Application
792+
getClassLoader := getMethodID(env, cls, "getClassLoader", "()Ljava/lang/ClassLoader;")
793+
ldr, err := callObjectMethod(env, obj, getClassLoader)
794+
if err != nil {
795+
return 0, err
796+
}
797+
loadClassMethod := getMethodID(env, getObjectClass(env, ldr), "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;")
798+
loaded, err := callObjectMethod(env, ldr, loadClassMethod, jvalue(javaString(env, class)))
799+
if err != nil {
800+
return 0, err
801+
}
802+
c := C.CString("android/content/Intent")
803+
defer C.free(unsafe.Pointer(c))
804+
intentClass := C.jni_FindClass(env, c)
805+
if err := exception(env); err != nil {
806+
return 0, err
807+
}
808+
intentInit := getMethodID(env, intentClass, "<init>", "()V")
809+
intent := C.jni_NewObject(env, intentClass, intentInit)
810+
setClassMethod := getMethodID(env, intentClass, "setClass", "(Landroid/content/Context;Ljava/lang/Class;)Landroid/content/Intent;")
811+
_, err = callObjectMethod(env, intent, setClassMethod, jvalue(obj), jvalue(C.jclass(loaded)))
812+
if err != nil {
813+
return 0, err
814+
}
815+
816+
return intent, nil
817+
}
818+
819+
// setforegroundIntentExtras adds the notification text to the intent.
820+
func setforegroundIntentExtras(env *C.JNIEnv, intent C.jobject, id int, name, desc string) {
821+
cls := getObjectClass(env, intent)
822+
putExtraStringMethod := getMethodID(env, cls, "putExtra", "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;")
823+
putExtraIntMethod := getMethodID(env, cls, "putExtra", "(Ljava/lang/String;I)Landroid/content/Intent;")
824+
_, err := callObjectMethod(env, intent, putExtraStringMethod, jvalue(javaString(env, "channelID")), jvalue(javaString(env, name)))
825+
if err != nil {
826+
panic(err)
827+
}
828+
_, err = callObjectMethod(env, intent, putExtraStringMethod, jvalue(javaString(env, "channelName")), jvalue(javaString(env, name)))
829+
if err != nil {
830+
panic(err)
831+
}
832+
_, err = callObjectMethod(env, intent, putExtraStringMethod, jvalue(javaString(env, "channelDesc")), jvalue(javaString(env, desc)))
833+
if err != nil {
834+
panic(err)
835+
}
836+
_, err = callObjectMethod(env, intent, putExtraIntMethod, jvalue(javaString(env, "notificationID")), jvalue(id))
837+
if err != nil {
838+
panic(err)
839+
}
840+
}
841+
777842
// goString converts the JVM jstring to a Go string.
778843
func goString(env *C.JNIEnv, str C.jstring) string {
779844
if str == 0 {
@@ -951,3 +1016,44 @@ func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) {
9511016
}
9521017

9531018
func (_ ViewEvent) ImplementsEvent() {}
1019+
1020+
// start a foreground service
1021+
func Java_org_gioui_StartForeground(id int, title, text string) chan struct{} {
1022+
closeChan := make(chan struct{})
1023+
1024+
// get a handle on the startForeground, stopForeground methods of GioForegroundService
1025+
// run everything in a goroutine so that start/stop calls are from the same thread
1026+
runInJVM(javaVM(), func(env *C.JNIEnv) {
1027+
// create an intent and set it to the foregroundService
1028+
intent, err := getIntent(env, android.appCtx, foregroundService)
1029+
if err != nil {
1030+
panic(err)
1031+
}
1032+
1033+
// set the notification text
1034+
setforegroundIntentExtras(env, intent, id, title, text)
1035+
1036+
// locate the methods to start our service
1037+
cls := getObjectClass(env, android.appCtx)
1038+
startForegroundServiceMethod := getMethodID(env, cls, "startForegroundService", "(Landroid/content/Intent;)Landroid/content/ComponentName;")
1039+
// Call startForegroundService with the above intent
1040+
callObjectMethod(env, android.appCtx, startForegroundServiceMethod, jvalue(intent))
1041+
})
1042+
1043+
go func() {
1044+
// wait for the channel to close, and then halt the foreground service
1045+
<-closeChan
1046+
runInJVM(javaVM(), func(env *C.JNIEnv) {
1047+
// create an intent and set it to the foregroundService
1048+
intent, err := getIntent(env, android.appCtx, foregroundService)
1049+
if err != nil {
1050+
panic(err)
1051+
}
1052+
cls := getObjectClass(env, android.appCtx)
1053+
stopService := getMethodID(env, cls, "stopService", "(Landroid/content/Intent;)Z")
1054+
callVoidMethod(env, android.appCtx, stopService, jvalue(intent))
1055+
})
1056+
}()
1057+
1058+
return closeChan
1059+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// SPDX-License-Identifier: Unlicense OR MIT
2+
3+
//+build android
4+
5+
/*
6+
Package foreground implements permissions to run a foreground service.
7+
See https://developer.android.com/guide/components/foreground-services.
8+
9+
The following entries will be added to AndroidManifest.xml:
10+
11+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
12+
13+
*/
14+
15+
package foreground
16+
17+
import (
18+
"gioui.org/app"
19+
)
20+
21+
func start(id int, title, text string) chan struct{} {
22+
return app.Java_org_gioui_StartForeground(id, title, text)
23+
}

app/permission/foreground/main.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// SPDX-License-Identifier: Unlicense OR MIT
2+
3+
/*
4+
Package foreground implements permissions to run a foreground service.
5+
See https://developer.android.com/guide/components/foreground-services.
6+
7+
The following entries will be added to AndroidManifest.xml:
8+
9+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
10+
11+
*/
12+
13+
package foreground
14+
15+
// Start notifies the system that the program will perform
16+
// background work and that it shouldn't be killed. It returns a channel
17+
// that should be closed when the background work is complete.
18+
19+
// Start is a no-op on Linux, Windows, macOS; Android will
20+
// display a notification during background work; iOS isn't supported.
21+
func Start(id int, title, text string) chan struct{} {
22+
return start(id, title, text)
23+
}

app/permission/foreground/other.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// SPDX-License-Identifier: Unlicense OR MIT
2+
3+
//+build !android
4+
5+
package foreground
6+
7+
func start(id int, title, text string) chan struct{} {
8+
return nil
9+
}

cmd/gogio/androidbuild.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type manifestData struct {
4949
Features []string
5050
IconSnip string
5151
AppName string
52+
HasService bool
5253
}
5354

5455
const (
@@ -446,6 +447,7 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe
446447
Features: features,
447448
IconSnip: iconSnip,
448449
AppName: appName,
450+
HasService: hasForegroundPermission(permissions),
449451
}
450452
tmpl, err := template.New("test").Parse(
451453
`<?xml version="1.0" encoding="utf-8"?>
@@ -467,6 +469,11 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe
467469
<category android:name="android.intent.category.LAUNCHER" />
468470
</intent-filter>
469471
</activity>
472+
{{if .HasService}}
473+
<service android:name="org.gioui.GioForegroundService"
474+
android:stopWithTask="true">
475+
</service>
476+
{{end}}
470477
</application>
471478
</manifest>`)
472479
var manifestBuffer bytes.Buffer
@@ -867,6 +874,17 @@ func getPermissions(ps []string) ([]string, []string) {
867874
return permissions, features
868875
}
869876

877+
func hasForegroundPermission(permissions []string) bool {
878+
if fgPerms, ok := AndroidPermissions["foreground"]; ok {
879+
for _, p := range permissions {
880+
if p == fgPerms[0] { // []string, len 1
881+
return true
882+
}
883+
}
884+
}
885+
return false
886+
}
887+
870888
func latestPlatform(sdk string) (string, error) {
871889
allPlats, err := filepath.Glob(filepath.Join(sdk, "platforms", "android-*"))
872890
if err != nil {

cmd/gogio/permission.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ var AndroidPermissions = map[string][]string{
1919
"android.permission.READ_EXTERNAL_STORAGE",
2020
"android.permission.WRITE_EXTERNAL_STORAGE",
2121
},
22+
"foreground": {
23+
"android.permission.FOREGROUND_SERVICE",
24+
},
2225
}
2326

2427
var AndroidFeatures = map[string][]string{

0 commit comments

Comments
 (0)