Skip to content

Commit 84b41ef

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 <masala@riseup.net>
1 parent a699f77 commit 84b41ef

8 files changed

Lines changed: 230 additions & 0 deletions

File tree

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+
// ForegroundService 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 ForegroundService 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.ForegroundChannelID");
39+
notificationID = metadata.getInt("org.gioui.ForegroundNotificationID", 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: 45 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
}
@@ -951,3 +959,40 @@ func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) {
951959
}
952960

953961
func (_ ViewEvent) ImplementsEvent() {}
962+
963+
// StartForeground starts a foreground service and returns a channel that stops the service when closed or error.
964+
func StartForeground(title, text string) (chan struct{}, error) {
965+
var closeChan chan struct{}
966+
errChan := make(chan error)
967+
968+
// Get a handle on the startService, stopService methods of ForegroundService.
969+
// run everything in a goroutine so that start/stop calls are from the same thread
970+
go func() {
971+
runInJVM(javaVM(), func(env *C.JNIEnv) {
972+
startForegroundService := getStaticMethodID(env, android.gioCls,
973+
"startForegroundService", "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;")
974+
975+
intent, err := callStaticObjectMethod(env, android.gioCls,
976+
startForegroundService,
977+
jvalue(android.appCtx),
978+
jvalue(javaString(env, "org/gioui/GioForegroundService")),
979+
jvalue(javaString(env, title)),
980+
jvalue(javaString(env, text)),
981+
)
982+
if err != nil {
983+
errChan <- err
984+
return
985+
}
986+
987+
// wait for the channel to close, and then halt the foreground service
988+
// create an intent and set it to the foregroundService
989+
errChan <- nil
990+
<-closeChan
991+
cls := getObjectClass(env, android.appCtx)
992+
stopServiceMethod := getMethodID(env, cls, "stopService", "(Landroid/content/Intent;)Z")
993+
callVoidMethod(env, android.appCtx, stopServiceMethod, jvalue(intent))
994+
})
995+
}()
996+
997+
return closeChan, <-errChan
998+
}
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.ForegroundService"
475+
android:stopWithTask="true">
476+
<meta-data android:name="org.gioui.ForegroundChannelID"
477+
android:value="ForegroundService" />
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)