Skip to content
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

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

mixmasala
Copy link

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
StartForeground, 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

@mixmasala
Copy link
Author

I've tested this with my application; but feel that there could be better integration with the notification library gioui.org/x/notify, so that the foreground service can share the same notification channels, in particular so that the text of the background notification can be updated. I'm not sure that this change should depend on that package though, but I think that the Service class needs to be declared in the application manifest. Any help resolving/improving the above details is very much appreciated.

@mixmasala mixmasala marked this pull request as draft November 18, 2021 11:09
@mixmasala mixmasala marked this pull request as ready for review November 18, 2021 11:09
@eliasnaur
Copy link
Contributor

Nice work, thank you.

I've tested this with my application; but feel that there could be better integration with the notification library gioui.org/x/notify, so that the foreground service can share the same notification channels, in particular so that the text of the background notification can be updated. I'm not sure that this change should depend on that package though, but I think that the Service class needs to be declared in the application manifest. Any help resolving/improving the above details is very much appreciated.

Why share notifications with niotify? This change is for foreground work, niotify is for notifications. However, see my review comments for a suggestion to customize foreground notification content text.

app/GioForegroundService.java Outdated Show resolved Hide resolved
app/GioForegroundService.java Show resolved Hide resolved
app/GioForegroundService.java Show resolved Hide resolved
app/GioForegroundService.java Outdated Show resolved Hide resolved
app/GioForegroundService.java Outdated Show resolved Hide resolved
app/GioForegroundService.java Outdated Show resolved Hide resolved
app/permission/foreground/android.go Outdated Show resolved Hide resolved
app/permission/foreground/main.go Outdated Show resolved Hide resolved
app/permission/foreground/android.go Outdated Show resolved Hide resolved
app/permission/foreground/android.go Outdated Show resolved Hide resolved
@eliasnaur
Copy link
Contributor

Oh, and remember to sign-off your change and squash commits.

@mixmasala
Copy link
Author

Nice work, thank you.

I've tested this with my application; but feel that there could be better integration with the notification library gioui.org/x/notify, so that the foreground service can share the same notification channels, in particular so that the text of the background notification can be updated. I'm not sure that this change should depend on that package though, but I think that the Service class needs to be declared in the application manifest. Any help resolving/improving the above details is very much appreciated.

Why share notifications with niotify? This change is for foreground work, niotify is for notifications. However, see my review comments for a suggestion to customize foreground notification content text.

well, the foreground service requires a notification - the code here just sets a static string which isn't very appealing. Android applications using foreground services tend to have some kind of content displayed in the service notification and it would be useful to have a clean way to interact with the notification - possibly the StartForeground method should return or accept this as an argument - and this is where it starts to overlap with the functionality provided by niotify.

@mixmasala mixmasala marked this pull request as draft November 19, 2021 12:43
@mixmasala
Copy link
Author

mixmasala commented Nov 19, 2021

edit: fixed

@mixmasala mixmasala marked this pull request as ready for review November 26, 2021 11:49
@mixmasala
Copy link
Author

I've refactored this PR to address issues raised above and changed the API to include setting the notification ID, title, and text.

app/os_android.go Outdated Show resolved Hide resolved
app/os_android.go Outdated Show resolved Hide resolved
app/os_android.go Outdated Show resolved Hide resolved
app/os_android.go Outdated Show resolved Hide resolved
app/os_android.go Outdated Show resolved Hide resolved
app/os_android.go Outdated Show resolved Hide resolved
app/permission/foreground/main.go Outdated Show resolved Hide resolved
cmd/gogio/androidbuild.go Outdated Show resolved Hide resolved
cmd/gogio/androidbuild.go Outdated Show resolved Hide resolved
cmd/gogio/androidbuild.go Outdated Show resolved Hide resolved
@mixmasala mixmasala force-pushed the add_foreground_service branch 4 times, most recently from c65a7be to 917b1da Compare November 29, 2021 16:22
@mixmasala
Copy link
Author

edit: stopped using startForegroundService because I saw some crash with resuming the application from background with same error as: transistorsoft/nativescript-background-geolocation-lt#121 (comment) . Instead startService is used, which fixes this crash.

app/GioForegroundService.java Outdated Show resolved Hide resolved
app/GioForegroundService.java Outdated Show resolved Hide resolved
app/GioForegroundService.java Outdated Show resolved Hide resolved
app/os_android.go Outdated Show resolved Hide resolved
app/os_android.go Outdated Show resolved Hide resolved
app/os_android.go Outdated Show resolved Hide resolved
app/os_android.go Outdated Show resolved Hide resolved
app/permission/foreground/main.go Outdated Show resolved Hide resolved
cmd/gogio/permission.go Outdated Show resolved Hide resolved
cmd/gogio/androidbuild.go Outdated Show resolved Hide resolved
@mixmasala mixmasala force-pushed the add_foreground_service branch 6 times, most recently from 206d76d to 37f30b7 Compare December 9, 2021 23:32
Copy link
Contributor

@eliasnaur eliasnaur left a comment

Choose a reason for hiding this comment

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

Leaving a few review comments for your follow-ups.

app/GioForegroundService.java Outdated Show resolved Hide resolved
app/GioForegroundService.java Outdated Show resolved Hide resolved
cmd/gogio/androidbuild.go Outdated Show resolved Hide resolved
cmd/gogio/androidbuild.go Outdated Show resolved Hide resolved
app/os_android.go Outdated Show resolved Hide resolved
app/GioForegroundService.java Outdated Show resolved Hide resolved
app/GioForegroundService.java Outdated Show resolved Hide resolved
app/GioForegroundService.java Outdated Show resolved Hide resolved
app/Gio.java Outdated Show resolved Hide resolved
@mixmasala mixmasala force-pushed the add_foreground_service branch 2 times, most recently from f0f3c2a to 84b41ef Compare December 10, 2021 09:16
@mixmasala mixmasala force-pushed the add_foreground_service branch 2 times, most recently from 698540a to 61f8a00 Compare October 8, 2022 11:58
Copy link
Contributor

@eliasnaur eliasnaur left a comment

Choose a reason for hiding this comment

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

Looks pretty good! I have a handful of comments, but it seems we can soon merge this.

app/permission/foreground/main.go Outdated Show resolved Hide resolved
app/permission/foreground/android.go Outdated Show resolved Hide resolved

// Start is a no-op on Linux, Windows, macOS; Android will
// display a notification during background work; iOS isn't supported.
func Start(title, text string) (func(), error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's just delete this function, leaving just app.Start and this package only for the permission. Note in the documentation for app.Start that this package must be imported for Start to work. Also, make sure Start returns an error if the user forgets the import.

Copy link
Author

Choose a reason for hiding this comment

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

Ok, I moved code out of the app/permission path and added os_nandroid.go to add a no-op Start() method for platforms other than android so that calls to Start() and its returned stop method have no effect.


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?

app/GioForegroundService.java Outdated Show resolved Hide resolved
app/os_android.go Outdated Show resolved Hide resolved
var foregroundService struct {
intent C.jobject
mu sync.Mutex
count int
Copy link
Contributor

Choose a reason for hiding this comment

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

This is much better than before, but users can still call their stop functions more than once. I suggest a map[*int]bool where each call to app.Start registers a new(int) token in the map. The stop function then returns eearly if the map no longer contains the token.

Copy link
Author

Choose a reason for hiding this comment

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

I'm afraid I don't see how users can call their stop functions more than once as the returned function checks count while ohlding the lock in order to prevent stopService from being called more than once.

There should be only one instance of the GioForegroundService, so multiple calls to Start() only indicate that some part of the application is doing something that needs to continue running, and when all of the associated stop func() have been called should the service be actually stopped.

I don't see why a map is useful here, where a counter suffices.

Copy link
Contributor

Choose a reason for hiding this comment

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

A program may have two unrelated background jobs, say (a) uploading an image and (b) checking for new messages. If they run concurrently, the job count is incremented to 2. A programming mistake causes (a) to calls its stop function twice, decrementing the count to 0, even though (b) is still in progress. The resulting missing notification is very hard to notice.

A map allows us to detect and ignore multiple calls to any particular stop function. In other words, it is no longer a (subtle) mistake to call your stop function twice.

Copy link
Author

Choose a reason for hiding this comment

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

ok, this makes sense to me. Thank you for the explanation.

Copy link
Author

Choose a reason for hiding this comment

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

(changes made)

@mixmasala mixmasala force-pushed the add_foreground_service branch 4 times, most recently from fece051 to e2f1cfa Compare November 10, 2022 12:53
@mixmasala
Copy link
Author

For some reason github wont let me comment on the change requests directly any more.

I moved code out of gioui.org/app/permissions, but I was not able to find a solution to check that gioui.org/app/permissions/foreground was imported when built. For example, I thought that BuildInfo might have package-level resolution, but it does not:

+func hasPermission(permission string) bool {
+       permissionPath := filepath.Join("gioui.org/app/permission/", permission)
+       if info, ok := debug.ReadBuildInfo(); ok {
+               for _, module := range info.Deps {
+                       fmt.Println("Imports: ", module.Path)
+                       // XXX: we do not have a way to get the list of included go packages at runtime
+                       if module.Path == permissionPath {
+                               return true
+                       }
+               }
+       }
+       return false
+}

@eliasnaur
Copy link
Contributor

For some reason github wont let me comment on the change requests directly any more.

I moved code out of gioui.org/app/permissions, but I was not able to find a solution to check that gioui.org/app/permissions/foreground was imported when built. For example, I thought that BuildInfo might have package-level resolution, but it does not:

+func hasPermission(permission string) bool {
+       permissionPath := filepath.Join("gioui.org/app/permission/", permission)
+       if info, ok := debug.ReadBuildInfo(); ok {
+               for _, module := range info.Deps {
+                       fmt.Println("Imports: ", module.Path)
+                       // XXX: we do not have a way to get the list of included go packages at runtime
+                       if module.Path == permissionPath {
+                               return true
+                       }
+               }
+       }
+       return false
+}

I don't think you need to check for the import. It should be enough to detect the Java exception/error when trying to open a foreground notification.

@mixmasala
Copy link
Author

I don't think you need to check for the import. It should be enough to detect the Java exception/error when trying to open a foreground notification.

Good Idea. I tried to catch java.lang.SecurityException, but the exception is not raised to the caller of android.startForegroundService, as it is thrown in the method onStartCommand which is called by the android environment after the service is started. Do you have any ideas how to handle this?

@eliasnaur
Copy link
Contributor

I don't think you need to check for the import. It should be enough to detect the Java exception/error when trying to open a foreground notification.

Good Idea. I tried to catch java.lang.SecurityException, but the exception is not raised to the caller of android.startForegroundService, as it is thrown in the method onStartCommand which is called by the android environment after the service is started. Do you have any ideas how to handle this?

One idea is to make onStartCommand report back to Gio whether the foreground notification succeeded or not. I think you'll need to wrap a chan error or similar with cgo.NewHandle to pass it to the Intent argument to onStartCommand. Then, in app.StartForeground wait for the channel to return an error (and free the handle).

@mixmasala mixmasala force-pushed the add_foreground_service branch 2 times, most recently from 7fd3b23 to 079448b Compare January 2, 2023 15:52
Copy link
Contributor

@eliasnaur eliasnaur left a comment

Choose a reason for hiding this comment

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

I somehow failed to send this review out after writing it. I apologize.


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.

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.

foregroundService.intent = C.jni_NewGlobalRef(env, foregroundService.intent)

} else {
panic(err)
Copy link
Contributor

Choose a reason for hiding this comment

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

You should propagate this error to the caller. In particular when the XXX issue above is resolved so that startForegroundService can fail for configuration reasons.

Comment on lines 1464 to 1476
// Start starts the foreground service
func Start(title, text string) (stop func(), err error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The documentation needs expanding. To make the documentation visible for everyone, rename this function startForeground and add a documented StartForeground in package app that calls it.

Copy link
Author

Choose a reason for hiding this comment

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

done

package app

// Start is a no-op on platforms other than android
func Start(title, text string) (stop func(), err error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

As described above, rename this function to startForeground.

Copy link
Author

Choose a reason for hiding this comment

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

done

This adds the permission android.permission.FOREGROUND_SERVICE and adds
GioForegroundService which creates the tray Notification necessary to
implement the Foreground Service.

This adds the method Start to package app, which 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.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants