Skip to content

Commit f0f3c2a

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 f0f3c2a

File tree

8 files changed

+283
-0
lines changed

8 files changed

+283
-0
lines changed

app/Gio.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import android.content.ClipboardManager;
66
import android.content.ClipData;
77
import android.content.Context;
8+
import android.content.Intent;
89
import android.os.Handler;
910
import android.os.Looper;
1011

@@ -65,4 +66,17 @@ static void wakeupMainThread() {
6566
}
6667

6768
static private native void scheduleMainFuncs();
69+
70+
static Intent startForegroundService(Context ctx, String activityClassName, String title, String text) {
71+
Intent intent = new Intent();
72+
try {
73+
intent.setClass(ctx, ctx.getClassLoader().loadClass(activityClassName));
74+
} catch (ClassNotFoundException e) {
75+
throw new RuntimeException(e);
76+
}
77+
intent.putExtra("channelName", title);
78+
intent.putExtra("channelDesc", text);
79+
ctx.startService(intent);
80+
return intent;
81+
}
6882
}

app/GioForegroundService.java

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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.ComponentName;
13+
import android.content.Intent;
14+
import android.content.pm.PackageManager;
15+
import android.os.IBinder;
16+
import android.os.Bundle;
17+
18+
// GioForegroundService implements a Service required to use the FOREGROUND_SERVICE
19+
// permission on Android, in order to run an application in the background.
20+
// See https://developer.android.com/guide/components/foreground-services for
21+
// more details. To add this permission to your application, import
22+
// gioui.org/app/permission/foreground and use the Start method from that
23+
// package to control this service.
24+
public class GioForegroundService extends Service {
25+
private String channelID;
26+
private String channelName;
27+
private String channelDesc;
28+
private int notificationID;
29+
30+
@Override public int onStartCommand(Intent intent, int flags, int startId) {
31+
// Get the channel parameters from intent extras and package metadata.
32+
Bundle extras = intent.getExtras();
33+
Context ctx = getApplicationContext();
34+
try {
35+
ComponentName svc = new ComponentName(this, this.getClass());
36+
Bundle metadata = getPackageManager().getServiceInfo(svc, PackageManager.GET_META_DATA).metaData;
37+
if (metadata != null) {
38+
channelID = metadata.getString("org.gioui.GioForegroundChannelID");
39+
notificationID = metadata.getInt("org.gioui.GioForegroundNotificationID", 0x42424242);
40+
}
41+
} catch (Exception e) {
42+
throw new RuntimeException(e);
43+
}
44+
channelName = extras.getString("channelName");
45+
channelDesc = extras.getString("channelDesc");
46+
notificationID = extras.getInt("notificationID", notificationID);
47+
this.createNotificationChannel();
48+
49+
// Create the Intent that will bring GioActivity to foreground.
50+
Intent resultIntent = new Intent(ctx, GioActivity.class);
51+
PendingIntent pending = PendingIntent.getActivity(ctx, notificationID, resultIntent, Intent.FLAG_ACTIVITY_CLEAR_TASK);
52+
Notification.Builder builder = new Notification.Builder(ctx, channelID)
53+
.setContentTitle(channelName)
54+
.setSmallIcon(getResources().getIdentifier("@mipmap/ic_launcher_adaptive", "drawable", getPackageName()))
55+
.setContentText(channelDesc)
56+
.setContentIntent(pending)
57+
.setPriority(Notification.PRIORITY_MIN);
58+
startForeground(notificationID, builder.build());
59+
return START_NOT_STICKY;
60+
}
61+
62+
@Override public IBinder onBind(Intent intent) {
63+
return null;
64+
}
65+
66+
@Override public void onCreate() {
67+
super.onCreate();
68+
}
69+
70+
@Override
71+
public void onTaskRemoved(Intent rootIntent) {
72+
super.onTaskRemoved(rootIntent);
73+
this.deleteNotificationChannel();
74+
stopForeground(true);
75+
this.stopSelf();
76+
}
77+
78+
@Override public void onDestroy() {
79+
this.deleteNotificationChannel();
80+
}
81+
82+
private void deleteNotificationChannel() {
83+
NotificationManager notificationManager = getSystemService(NotificationManager.class);
84+
notificationManager.deleteNotificationChannel(channelName);
85+
}
86+
87+
private void createNotificationChannel() {
88+
// https://developer.android.com/training/notify-user/build-notification#java
89+
NotificationChannel channel = new NotificationChannel(channelID, channelName, NotificationManager.IMPORTANCE_LOW);
90+
channel.setDescription(channelDesc);
91+
NotificationManager notificationManager = getSystemService(NotificationManager.class);
92+
notificationManager.createNotificationChannel(channel);
93+
}
94+
}

app/os_android.go

Lines changed: 98 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
}
@@ -774,6 +782,59 @@ func getObjectClass(env *C.JNIEnv, obj C.jobject) C.jclass {
774782
return cls
775783
}
776784

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

9531014
func (_ ViewEvent) ImplementsEvent() {}
1015+
1016+
// StartForeground starts a foreground service and returns a channel that stops the service when closed or error.
1017+
func StartForeground(title, text string) (chan struct{}, error) {
1018+
var closeChan chan struct{}
1019+
errChan := make(chan error)
1020+
1021+
// get a handle on the startService, stopService methods of GioForegroundService
1022+
// run everything in a goroutine so that start/stop calls are from the same thread
1023+
go func() {
1024+
runInJVM(javaVM(), func(env *C.JNIEnv) {
1025+
startForegroundService := getStaticMethodID(env, android.gioCls,
1026+
"startForegroundService", "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;")
1027+
1028+
intent, err := callStaticObjectMethod(env, android.gioCls,
1029+
startForegroundService,
1030+
jvalue(android.appCtx),
1031+
jvalue(javaString(env, "org/gioui/GioForegroundService")),
1032+
jvalue(javaString(env, title)),
1033+
jvalue(javaString(env, text)),
1034+
)
1035+
if err != nil {
1036+
errChan <- err
1037+
return
1038+
}
1039+
1040+
// wait for the channel to close, and then halt the foreground service
1041+
// create an intent and set it to the foregroundService
1042+
errChan <- nil
1043+
<-closeChan
1044+
cls := getObjectClass(env, android.appCtx)
1045+
stopServiceMethod := getMethodID(env, cls, "stopService", "(Landroid/content/Intent;)Z")
1046+
callVoidMethod(env, android.appCtx, stopServiceMethod, jvalue(intent))
1047+
})
1048+
}()
1049+
1050+
return closeChan, <-errChan
1051+
}
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(title, text string) (chan struct{}, error) {
22+
return app.StartForeground(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(title, text string) (chan struct{}, error) {
22+
return start(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(title, text string) (chan struct{}, error) {
8+
return nil, nil
9+
}

cmd/gogio/androidbuild.go

Lines changed: 19 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: stringsContains(permissions, foregroundPermission),
449452
}
450453
tmpl, err := template.New("test").Parse(
451454
`<?xml version="1.0" encoding="utf-8"?>
@@ -467,6 +470,13 @@ 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+
<meta-data android:name="org.gioui.GioForegroundChannelID"
477+
android:value="GioForegroundService" />
478+
</service>
479+
{{end}}
470480
</application>
471481
</manifest>`)
472482
var manifestBuffer bytes.Buffer
@@ -867,6 +877,15 @@ func getPermissions(ps []string) ([]string, []string) {
867877
return permissions, features
868878
}
869879

880+
func stringsContains(strings []string, str string) bool {
881+
for _, s := range strings {
882+
if str == s {
883+
return true
884+
}
885+
}
886+
return false
887+
}
888+
870889
func latestPlatform(sdk string) (string, error) {
871890
allPlats, err := filepath.Glob(filepath.Join(sdk, "platforms", "android-*"))
872891
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)