package android import ( "fmt" "sync" "gioui.org/app" "git.wow.st/gmp/jni" ) // Importance represents the priority of notifications sent over a particular NotificationChannel. // You MUST use one of the constants defined here when specifying an importance for a channel. // These constants map to different values within the JVM. type Importance int const ( ImportanceDefault Importance = iota ImportanceHigh ImportanceLow ImportanceMax ImportanceMin ImportanceNone ImportanceUnspecified importanceEnd // compile-time hack to track the number of importance constants and size the // array holding their values correctly. If new constants need to be added, add them above // this. ) // value returns the JVM value for this importance constant. It must not be invoked before the // importances have been resolved. func (i Importance) value() int32 { return importances[i] } const ( helperClass = "ht/sr/git/whereswaldon/niotify/NotificationHelper" importanceDefaultName = "IMPORTANCE_DEFAULT" importanceHighName = "IMPORTANCE_HIGH" importanceLowName = "IMPORTANCE_LOW" importanceMaxName = "IMPORTANCE_MAX" importanceMinName = "IMPORTANCE_MIN" importanceNoneName = "IMPORTANCE_NONE" importanceUnspecifiedName = "IMPORTANCE_UNSPECIFIED" ) var ( // idlock protects the nextNotificationID to ensure that no notification is ever // sent with a duplicate id. // // BUG(whereswaldon): Notification ID generation does not handle 32 bit integer // overflow. Sending more than 2 billion notifications results in undefined // behavior. idlock sync.Mutex nextNotificationID int32 // jvmConstLock protects the mapping of JVM constants that must be resolved at runtime jvmConstLock sync.Once // importances tracks the IMPORTANCE_* constants from the JVM's values. Since they must // be resolved at runtime, this array tracks their actual runtime values and the exported // constants are simply indicies into this array. importances [importanceEnd]int32 // map the JVM constant name to the index in the array importancesMap = map[string]Importance{ importanceDefaultName: ImportanceDefault, importanceHighName: ImportanceHigh, importanceLowName: ImportanceLow, importanceMaxName: ImportanceMax, importanceMinName: ImportanceMin, importanceNoneName: ImportanceNone, importanceUnspecifiedName: ImportanceUnspecified, } ) // nextID safely returns the next unused notification id number. This function should // always be used to get a notificationID. func nextID() int32 { idlock.Lock() defer idlock.Unlock() id := nextNotificationID nextNotificationID++ return id } // NotificationChannel represents a stream of notifications that an application // provisions on android. Such streams can be selectively enabled and disabled // by the user, and should be used for different purposes. type NotificationChannel struct { id string } // NewChannel creates a new notification channel identified by the provided id // and with the given user-visible name and description. The importance field // specifies how the android system should prioritize notifications sent over this // channel, and the value provided MUST be one of the constants declared by this // package. The actual value of the importance constant is translated into the // java value at runtime. func NewChannel(importance Importance, id, name, description string) (*NotificationChannel, error) { if err := jni.Do(jni.JVMFor(app.JavaVM()), func(env jni.Env) error { appCtx := jni.Object(app.AppContext()) classLoader := jni.ClassLoaderFor(env, appCtx) notifyClass, err := jni.LoadClass(env, classLoader, helperClass) if err != nil { return err } jvmConstLock.Do(func() { var managerClass jni.Class managerClass, err = jni.LoadClass(env, classLoader, "android/app/NotificationManager") if err != nil { return } for name, index := range importancesMap { fieldID := jni.GetStaticFieldID(env, managerClass, name, "I") importances[index] = jni.GetStaticIntField(env, managerClass, fieldID) } }) newChannelMethod := jni.GetStaticMethodID(env, notifyClass, "newChannel", "(Landroid/content/Context;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V") jname := jni.Value(jni.JavaString(env, name)) jdescription := jni.Value(jni.JavaString(env, description)) jID := jni.Value(jni.JavaString(env, id)) jimportance := jni.Value(importance.value()) err = jni.CallStaticVoidMethod(env, notifyClass, newChannelMethod, jni.Value(app.AppContext()), jimportance, jID, jname, jdescription) if err != nil { return err } return nil }); err != nil { return nil, fmt.Errorf("failed creating notification channel: %w", err) } nc := &NotificationChannel{ id: id, } return nc, nil } // Notification represents a notification that has been requested to be shown to the user. // This type provides methods to cancel or update the contents of the notification. type Notification struct { id int32 } // Send creates a new Notification and requests that it be displayed on this channel. func (nc *NotificationChannel) Send(title, text string) (*Notification, error) { notificationID := nextID() if err := jni.Do(jni.JVMFor(app.JavaVM()), func(env jni.Env) error { appCtx := jni.Object(app.AppContext()) classLoader := jni.ClassLoaderFor(env, appCtx) notifyClass, err := jni.LoadClass(env, classLoader, helperClass) if err != nil { return err } newChannelMethod := jni.GetStaticMethodID(env, notifyClass, "sendNotification", "(Landroid/content/Context;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V") jtitle := jni.Value(jni.JavaString(env, title)) jtext := jni.Value(jni.JavaString(env, text)) jID := jni.Value(jni.JavaString(env, nc.id)) err = jni.CallStaticVoidMethod(env, notifyClass, newChannelMethod, jni.Value(app.AppContext()), jID, jni.Value(notificationID), jtitle, jtext) if err != nil { return err } return nil }); err != nil { return nil, fmt.Errorf("failed sending notification: %w", err) } return &Notification{ id: notificationID, }, nil } // Cancel removes a previously created notification from display. func (n *Notification) Cancel() error { notificationID := n.id if err := jni.Do(jni.JVMFor(app.JavaVM()), func(env jni.Env) error { appCtx := jni.Object(app.AppContext()) classLoader := jni.ClassLoaderFor(env, appCtx) notifyClass, err := jni.LoadClass(env, classLoader, helperClass) if err != nil { return err } newChannelMethod := jni.GetStaticMethodID(env, notifyClass, "cancelNotification", "(Landroid/content/Context;I)V") return jni.CallStaticVoidMethod(env, notifyClass, newChannelMethod, jni.Value(app.AppContext()), jni.Value(notificationID)) }); err != nil { return fmt.Errorf("failed cancelling notification: %w", err) } return nil }