Skip to content

Commit 917b1da

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 917b1da

File tree

7 files changed

+262
-0
lines changed

7 files changed

+262
-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: 105 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,8 @@ import (
131139
"gioui.org/unit"
132140
)
133141

142+
const foregroundService = "org/gioui/GioForegroundService"
143+
134144
type window struct {
135145
callbacks *callbacks
136146

@@ -774,6 +784,59 @@ func getObjectClass(env *C.JNIEnv, obj C.jobject) C.jclass {
774784
return cls
775785
}
776786

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

9531016
func (_ ViewEvent) ImplementsEvent() {}
1017+
1018+
// start a foreground service
1019+
func Java_org_gioui_StartForeground(id int, title, text string) (chan struct{}, error) {
1020+
closeChan := make(chan struct{})
1021+
errChan := make(chan error)
1022+
1023+
// get a handle on the startService, stopService methods of GioForegroundService
1024+
// run everything in a goroutine so that start/stop calls are from the same thread
1025+
go func() {
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+
errChan <- err
1031+
return
1032+
}
1033+
1034+
// set the notification text
1035+
setforegroundIntentExtras(env, intent, id, title, text)
1036+
1037+
// locate the methods to start and stop our service
1038+
cls := getObjectClass(env, android.appCtx)
1039+
startServiceMethod := getMethodID(env, cls, "startService", "(Landroid/content/Intent;)Landroid/content/ComponentName;")
1040+
stopServiceMethod := getMethodID(env, cls, "stopService", "(Landroid/content/Intent;)Z")
1041+
1042+
// Call startService with the above intent
1043+
_, err = callObjectMethod(env, android.appCtx, startServiceMethod, jvalue(intent))
1044+
if err != nil {
1045+
errChan <- err
1046+
return
1047+
}
1048+
1049+
// wait for the channel to close, and then halt the foreground service
1050+
// create an intent and set it to the foregroundService
1051+
errChan <- nil
1052+
<-closeChan
1053+
callVoidMethod(env, android.appCtx, stopServiceMethod, jvalue(intent))
1054+
})
1055+
}()
1056+
1057+
return closeChan, <-errChan
1058+
}
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{}, error) {
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{}, error) {
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{}, error) {
8+
return nil, nil
9+
}

cmd/gogio/androidbuild.go

Lines changed: 17 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 (
@@ -68,6 +69,7 @@ const (
6869
<item name="android:statusBarColor">#40000000</item>
6970
</style>
7071
</resources>`
72+
foregroundPermission = "android.permission.FOREGROUND_SERVICE"
7173
)
7274

7375
func init() {
@@ -446,6 +448,7 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe
446448
Features: features,
447449
IconSnip: iconSnip,
448450
AppName: appName,
451+
HasService: hasForegroundPermission(permissions),
449452
}
450453
tmpl, err := template.New("test").Parse(
451454
`<?xml version="1.0" encoding="utf-8"?>
@@ -467,6 +470,11 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe
467470
<category android:name="android.intent.category.LAUNCHER" />
468471
</intent-filter>
469472
</activity>
473+
{{if .HasService}}
474+
<service android:name="org.gioui.GioForegroundService"
475+
android:stopWithTask="true">
476+
</service>
477+
{{end}}
470478
</application>
471479
</manifest>`)
472480
var manifestBuffer bytes.Buffer
@@ -867,6 +875,15 @@ func getPermissions(ps []string) ([]string, []string) {
867875
return permissions, features
868876
}
869877

878+
func hasForegroundPermission(permissions []string) bool {
879+
for _, p := range permissions {
880+
if p == foregroundPermission {
881+
return true
882+
}
883+
}
884+
return false
885+
}
886+
870887
func latestPlatform(sdk string) (string, error) {
871888
allPlats, err := filepath.Glob(filepath.Join(sdk, "platforms", "android-*"))
872889
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)