8000 app, cmd/gogio: add android foreground permission and service by mixmasala · Pull Request #67 · gioui/gio · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

app, cmd/gogio: add android foreground permission and service #67

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions app/Gio.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import android.content.ClipboardManager;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;

Expand Down Expand Up @@ -65,4 +67,21 @@ static void wakeupMainThread() {
}

static private native void scheduleMainFuncs();

static Intent startForegroundService(Context ctx, String title, String text) throws ClassNotFoundException {
Intent intent = new Intent();
intent.setClass(ctx, ctx.getClassLoader().loadClass("org/gioui/GioForegroundService"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just GioForegroundService.class instead of loadClass...?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that I tried doing that but the class doesn't exist unless it was first loaded.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please post the error message you get. It doesn't make sense to me that org.gioui.Gio can be loaded but org.gioui.GioForegroundService can't.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ActivityManager
Unable to start service Intent
{ cmp=org.mixnetworks.katzen/
org.gioui.GioForegroundServie (has extras) } U=0:
not found

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There must be more to the error than what you pasted. A stack trace or similar that can tell us why the class cannot be loaded.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you know how to get android to produce a stack trace here?

intent.putExtra("title", title);
intent.putExtra("text", text);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Use startForegroundService for API level 26 and later
ctx.startForegroundService(intent);
} else {
// Use startService for API level 25 and earlier
ctx.startService(intent);
}

return intent;
}
}
108 changes: 108 additions & 0 deletions app/GioForegroundService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: Unlicense OR MIT

package org.gioui;
import android.app.Notification;
import android.app.Service;
import android.app.Notification;
import android.app.Notification.Builder;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.IBinder;
import android.os.Build;
import android.os.Bundle;

// GioForegroundService implements a Service required to use the FOREGROUND_SERVICE
// permission on Android, in order to run an application in the background.
// See https://developer.android.com/guide/components/foreground-services for
// more details. To add this permission to your application, import
// gioui.org/app/permission/foreground and use the Start method from that
// package to control this service.
public class GioForegroundService extends Service {
private String channelName;

// ForegroundNotificationID is a default unique ID for the tray Notification of this service, as it must be nonzero.
private int ForegroundNotificationID = 0x42424242;

@Override public int onStartCommand(Intent intent, int flags, int startId) {
// Get the channel parameters from intent extras and package metadata.
Bundle extras = intent.getExtras();
String title = extras.getString("title");
String text = extras.getString("text");
Context ctx = getApplicationContext();
try {
ComponentName svc = new ComponentName(this, this.getClass());
Bundle metadata = getPackageManager().getServiceInfo(svc, PackageManager.GET_META_DATA).metaData;
if (metadata == null) {
throw new RuntimeException("No ForegroundService MetaData found");
}
channelName = metadata.getString("org.gioui.ForegroundChannelName");
String channelDesc = metadata.getString("org.gioui.ForegroundChannelDesc", "");
String channelID = metadata.getString("org.gioui.ForegroundChannelID");
int notificationID = metadata.getInt("org.gioui.ForegroundNotificationID", ForegroundNotificationID);
this.createNotificationChannel(channelDesc, channelID, channelName);
Intent launchIntent = getPackageManager().getLaunchIntentForPackage(ctx.getPackageName());

PendingIntent pending = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
pending = PendingIntent.getActivity(ctx, notificationID, launchIntent, Intent.FLAG_ACTIVITY_CLEAR_TASK|PendingIntent.FLAG_IMMUTABLE);
} else {
pending = PendingIntent.getActivity(ctx, notificationID, launchIntent, Intent.FLAG_ACTIVITY_CLEAR_TASK);
}
Notification.Builder builder = new Notification.Builder(ctx, channelID)
.setContentTitle(title)
.setContentText(text)
.setSmallIcon(getResources().getIdentifier("@mipmap/ic_launcher_adaptive", "drawable", getPackageName()))
.setContentIntent(pending)
.setPriority(Notification.PRIORITY_MIN);
startForeground(notificationID, builder.build());
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
} catch (java.lang.SecurityException e) {
// XXX: notify the caller of Start that the service has failed
throw new RuntimeException(e);
}
return START_NOT_STICKY;
}

@Override public IBinder onBind(Intent intent) {
return null;
}

@Override public void onCreate() {
super.onCreate();
}

@Override
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
this.deleteNotificationChannel();
stopForeground(true);
this.stopSelf();
}

@Override public void onDestroy() {
this.deleteNotificationChannel();
}

private void deleteNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.deleteNotificationChannel(channelName);
}
}

private void createNotificationChannel(String channelDesc, String channelID, String channelName) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// https://developer.android.com/training/notify-user/build-notification#java
NotificationChannel channel = new NotificationChannel(channelID, channelName, NotificationManager.IMPORTANCE_LOW);
channel.setDescription(channelDesc);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
}
9 changes: 9 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ func DataDir() (string, error) {
return dataDir()
}

// Start starts the foreground service on android, notifies the system that the
// program will perform background work and that it shouldn't be killed. The
// foreground service is stopped using the cancel function returned by Start().
// If multiple calls to Start are made, the foreground service will not be
// stopped until the final cancel function has been called.
func Start(title, text string) (stop func(), err error) {
return startForeground(title, text)
}

// Main must be called last from the program main function.
// On most platforms Main blocks forever, for Android and
// iOS it returns immediately to give control of the main
Expand Down
69 changes: 66 additions & 3 deletions app/os_android.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ static jclass jni_GetObjectClass(JNIEnv *env, jobject obj) {
return (*env)->GetObjectClass(env, obj);
}

static jobject jni_NewObject(JNIEnv *env, jclass clazz, jmethodID methodID) {
return (*env)->NewObject(env, clazz, methodID);
}

static jmethodID jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
return (*env)->GetMethodID(env, clazz, name, sig);
}
Expand Down Expand Up @@ -231,9 +235,11 @@ var android struct {
// gioCls is the class of the Gio class.
gioCls C.jclass

mwriteClipboard C.jmethodID
mreadClipboard C.jmethodID
mwakeupMainThread C.jmethodID
mwriteClipboard C.jmethodID
mreadClipboard C.jmethodID
mwakeupMainThread C.jmethodID
startForegroundService C.jmethodID
stopService C.jmethodID

// android.view.accessibility.AccessibilityNodeInfo class.
accessibilityNodeInfo struct {
Expand Down Expand Up @@ -434,6 +440,10 @@ func initJVM(env *C.JNIEnv, gio C.jclass, ctx C.jobject) {
android.mreadClipboard = getStaticMethodID(env, gio, "readClipboard", "(Landroid/content/Context;)Ljava/lang/String;")
android.mwakeupMainThread = getStaticMethodID(env, gio, "wakeupMainThread", "()V")

cls = getObjectClass(env, android.appCtx)
android.stopService = getMethodID(env, cls, "stopService", "(Landroid/content/Intent;)Z")
android.startForegroundService = getStaticMethodID(env, gio, "startForegroundService", "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;")

intern := func(s string) C.jstring {
ref := C.jni_NewGlobalRef(env, C.jobject(javaString(env, s)))
return C.jstring(ref)
Expand Down Expand Up @@ -1495,3 +1505,56 @@ func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) {

func (AndroidViewEvent) implementsViewEvent() {}
func (AndroidViewEvent) ImplementsEvent() {}

var foregroundService struct {
intent C.jobject
mu sync.Mutex
stop map[*int]bool
}

// startForeground starts the foreground service on android
// it returns a method that stops the foreground service, or an error
func startForeground(title, text string) (func(), error) {
foregroundService.mu.Lock()
defer foregroundService.mu.Unlock()

var intent C.jobject
var err error
if len(foregroundService.stop) == 0 {
runInJVM(javaVM(), func(env *C.JNIEnv) {
intent, err = callStaticObjectMethod(env, android.gioCls,
android.startForegroundService,
jvalue(android.appCtx),
jvalue(javaString(env, title)),
jvalue(javaString(env, text)),
)
if err == nil {
// get a reference across JNI sessions to the returned intent
foregroundService.intent = C.jni_NewGlobalRef(env, intent)
}
})
}
if err != nil {
return nil, err
}
ref := new(int)
foregroundService.stop[ref] = true
return func() {
foregroundService.mu.Lock()
defer foregroundService.mu.Unlock()
delete(foregroundService.stop, ref)
// Each call to Start returns a stop() method; once the last stop() is called, stop the service.
if len(foregroundService.stop) == 0 {
runInJVM(javaVM(), func(env *C.JNIEnv) {
callVoidMethod(env, android.appCtx, android.stopService, jvalue(foregroundService.intent))
C.jni_DeleteGlobalRef(env, foregroundService.intent)
foregroundService.intent = 0
})
}
}, nil

}

func init() {
foregroundService.stop = make(map[*int]bool)
}
11 changes: 11 additions & 0 deletions app/os_nandroid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: Unlicense OR MIT

//go:build !android
// +build !android

package app

// app.Start is a no-op on platforms other than android
func startForeground(title, text string) (stop func(), err error) {
return func() {}, nil
}
13 changes: 13 additions & 0 deletions app/permission/foreground/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: Unlicense OR MIT

/*
Package foreground implements permissions to run a foreground service.
See https://developer.android.com/guide/components/foreground-services.

The following entries will be added to AndroidManifest.xml:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

*/

package foreground
0