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

notif: Switch to our own Pigeon-based plugin for showing a notification #592

Merged
merged 13 commits into from
Mar 28, 2024
Merged
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
5 changes: 4 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# Suppress noisy generated files in diffs.

# Dart files generated from the files next to them:
# Dart files generated from the files next to them, or by Pigeon:
*.g.dart -diff

# Generated files for testing migrations:
test/model/schemas/*.dart -diff
test/model/schemas/*.json -diff

# Kotlin files generated by, e.g., Pigeon:
*.g.kt -diff

# On the other hand, keep diffs for pubspec.lock. It contains
# information independent of any non-generated file in the tree.
# And thankfully it's much less verbose than, say, a yarn.lock.
Expand Down
38 changes: 14 additions & 24 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.

# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
# For docs on this file, see:
# https://dart.dev/guides/language/analysis-options
include: package:flutter_lints/flutter.yaml

analyzer:
exclude:
# Skip analysis on Pigeon-generated code, because it currently
# triggers some stylistic lints and they aren't actionable for us.
# (Some lints could signal a bug, which would be good to catch...
# but typically those are lints Flutter upstream keeps enabled, so
# if Pigeon tripped them it'd immediately get caught.)
# TODO(pigeon) re-enable lints once clean: https://github.com/flutter/flutter/issues/145633
- lib/host/*.g.dart

linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
# For a list of all available lints, with docs, see:
# https://dart-lang.github.io/linter/lints/index.html.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
no_literal_bool_comparisons: true
prefer_relative_imports: true

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
167 changes: 167 additions & 0 deletions android/app/src/main/kotlin/com/zulip/flutter/Notifications.g.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Autogenerated from Pigeon (v17.2.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon

package com.zulip.flutter

import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer

private fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}

private fun wrapError(exception: Throwable): List<Any?> {
if (exception is FlutterError) {
return listOf(
exception.code,
exception.message,
exception.details
)
} else {
return listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
)
}
}

/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()

/**
* Corresponds to `android.app.PendingIntent`.
*
* See: https://developer.android.com/reference/android/app/PendingIntent
*
* Generated class from Pigeon that represents data sent in messages.
*/
data class PendingIntent (
val requestCode: Long,
/**
* A value set on an extra on the Intent, and passed to
* the on-notification-opened callback.
*/
val intentPayload: String,
/**
* A combination of flags from [PendingIntent.flags], and others associated
* with `Intent`; see Android docs for `PendingIntent.getActivity`.
*/
val flags: Long

) {
companion object {
@Suppress("UNCHECKED_CAST")
fun fromList(list: List<Any?>): PendingIntent {
val requestCode = list[0].let { if (it is Int) it.toLong() else it as Long }
val intentPayload = list[1] as String
val flags = list[2].let { if (it is Int) it.toLong() else it as Long }
return PendingIntent(requestCode, intentPayload, flags)
}
}
fun toList(): List<Any?> {
return listOf<Any?>(
requestCode,
intentPayload,
flags,
)
}
}
@Suppress("UNCHECKED_CAST")
private object AndroidNotificationHostApiCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
128.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
PendingIntent.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) {
is PendingIntent -> {
stream.write(128)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
}

/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface AndroidNotificationHostApi {
/**
* Corresponds to `android.app.NotificationManager.notify`,
* combined with `androidx.core.app.NotificationCompat.Builder`.
*
* The arguments `tag` and `id` go to the `notify` call.
* The rest go to method calls on the builder.
*
* The `color` should be in the form 0xAARRGGBB.
* This is the form returned by [Color.value].
*
* The `smallIconResourceName` is passed to `android.content.res.Resources.getIdentifier`
* to get a resource ID to pass to `Builder.setSmallIcon`.
* Whatever name is passed there must appear in keep.xml too:
* see https://github.com/zulip/zulip-flutter/issues/528 .
*
* See:
* https://developer.android.com/reference/kotlin/android/app/NotificationManager.html#notify
* https://developer.android.com/reference/androidx/core/app/NotificationCompat.Builder
*/
fun notify(tag: String?, id: Long, channelId: String, color: Long?, contentIntent: PendingIntent?, contentText: String?, contentTitle: String?, extras: Map<String?, String?>?, smallIconResourceName: String?)

companion object {
/** The codec used by AndroidNotificationHostApi. */
val codec: MessageCodec<Any?> by lazy {
AndroidNotificationHostApiCodec
}
/** Sets up an instance of `AndroidNotificationHostApi` to handle messages through the `binaryMessenger`. */
@Suppress("UNCHECKED_CAST")
fun setUp(binaryMessenger: BinaryMessenger, api: AndroidNotificationHostApi?) {
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.notify", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val tagArg = args[0] as String?
val idArg = args[1].let { if (it is Int) it.toLong() else it as Long }
val channelIdArg = args[2] as String
val colorArg = args[3].let { if (it is Int) it.toLong() else it as Long? }
val contentIntentArg = args[4] as PendingIntent?
val contentTextArg = args[5] as String?
val contentTitleArg = args[6] as String?
val extrasArg = args[7] as Map<String?, String?>?
val smallIconResourceNameArg = args[8] as String?
var wrapped: List<Any?>
try {
api.notify(tagArg, idArg, channelIdArg, colorArg, contentIntentArg, contentTextArg, contentTitleArg, extrasArg, smallIconResourceNameArg)
wrapped = listOf<Any?>(null)
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
80 changes: 80 additions & 0 deletions android/app/src/main/kotlin/com/zulip/flutter/ZulipPlugin.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.zulip.flutter

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.annotation.Keep
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import io.flutter.embedding.engine.plugins.FlutterPlugin

private const val TAG = "ZulipPlugin"

private class AndroidNotificationHost(val context: Context)
: AndroidNotificationHostApi {
@SuppressLint(
// If permission is missing, `notify` will throw an exception.
// Which hopefully will propagate to Dart, and then it's up to Dart code to handle it.
"MissingPermission",
// For `getIdentifier`. TODO make a cleaner API.
"DiscouragedApi")
override fun notify(
tag: String?,
id: Long,
channelId: String,
color: Long?,
contentIntent: PendingIntent?,
contentText: String?,
contentTitle: String?,
extras: Map<String?, String?>?,
smallIconResourceName: String?
) {
val notification = NotificationCompat.Builder(context, channelId).apply {
color?.let { setColor(it.toInt()) }
contentIntent?.let { setContentIntent(
android.app.PendingIntent.getActivity(context,
it.requestCode.toInt(),
Intent(context, MainActivity::class.java).apply {
// This action name and extra name are special to
// FlutterLocalNotificationsPlugin, which handles receiving the Intent.
// TODO take care of receiving the notification-opened Intent ourselves
action = "SELECT_NOTIFICATION"
putExtra("payload", it.intentPayload)
},
it.flags.toInt())
) }
contentText?.let { setContentText(it) }
contentTitle?.let { setContentTitle(it) }
extras?.let { setExtras(
Bundle().apply { it.forEach { (k, v) -> putString(k, v) } } ) }
smallIconResourceName?.let { setSmallIcon(context.resources.getIdentifier(
it, "drawable", context.packageName)) }
}.build()
NotificationManagerCompat.from(context).notify(tag, id.toInt(), notification)
}
}

/** A Flutter plugin for the Zulip app's ad-hoc needs. */
// @Keep is needed because this class is used only
// from ZulipShimPlugin, via reflection.
@Keep
class ZulipPlugin : FlutterPlugin { // TODO ActivityAware too?
private var notificationHost: AndroidNotificationHost? = null

override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
Log.d(TAG, "Attaching to Flutter engine.")
notificationHost = AndroidNotificationHost(binding.applicationContext)
AndroidNotificationHostApi.setUp(binding.binaryMessenger, notificationHost)
}

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
if (notificationHost == null) {
Log.wtf(TAG, "Already detached from the engine.")
return
}
AndroidNotificationHostApi.setUp(binding.binaryMessenger, null)
notificationHost = null
}
}
1 change: 1 addition & 0 deletions android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "$agpVersion" apply false
id "com.android.library" version "$agpVersion" apply false
id "org.jetbrains.kotlin.android" version "$kotlinVersion" apply false
}

Expand Down
27 changes: 27 additions & 0 deletions lib/host/android_notifications.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export './android_notifications.g.dart';

/// For use in [PendingIntent.flags].
///
/// See: https://developer.android.com/reference/android/app/PendingIntent#constants_1
abstract class PendingIntentFlag {
/// Corresponds to `FLAG_ONE_SHOT`.
static const oneShot = 1 << 30;

/// Corresponds to `FLAG_NO_CREATE`.
static const noCreate = 1 << 29;

/// Corresponds to `FLAG_CANCEL_CURRENT`.
static const cancelCurrent = 1 << 28;

/// Corresponds to `FLAG_UPDATE_CURRENT`.
static const updateCurrent = 1 << 27;

/// Corresponds to `FLAG_IMMUTABLE`.
static const immutable = 1 << 26;

/// Corresponds to `FLAG_MUTABLE`.
static const mutable = 1 << 25;

/// Corresponds to `FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT`.
static const allowUnsafeImplicitIntent = 1 << 24;
}
Loading
Loading