Skip to content

Commit

Permalink
feat(notification): implement Android and iOS APIs (tauri-apps#340)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog authored May 5, 2023
1 parent 1397172 commit be1c775
Show file tree
Hide file tree
Showing 25 changed files with 3,701 additions and 91 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions plugins/notification/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
rand = "0.8"
time = { version = "0.3", features = ["serde", "parsing", "formatting"] }
url = { version = "2", features = ["serde"] }
serde_repr = "0.1"

[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
notify-rust = "4.5"
Expand Down
4 changes: 2 additions & 2 deletions plugins/notification/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ plugins {

android {
namespace = "app.tauri.notification"
compileSdk = 32
compileSdk = 33

defaultConfig {
minSdk = 24
targetSdk = 32
targetSdk = 33

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
Expand Down
21 changes: 19 additions & 2 deletions plugins/notification/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.tauri.notification">

<application>
<receiver android:name="app.tauri.notification.TimedNotificationPublisher" />
<receiver android:name="app.tauri.notification.NotificationDismissReceiver" />
<receiver
android:name="app.tauri.notification.NotificationRestoreReceiver"
android:directBootAware="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
</application>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
</manifest>
25 changes: 25 additions & 0 deletions plugins/notification/android/src/main/java/AssetUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package app.tauri.notification

import android.annotation.SuppressLint
import android.content.Context

class AssetUtils {
companion object {
const val RESOURCE_ID_ZERO_VALUE = 0

@SuppressLint("DiscouragedApi")
fun getResourceID(context: Context, resourceName: String?, dir: String?): Int {
return context.resources.getIdentifier(resourceName, dir, context.packageName)
}

fun getResourceBaseName(resPath: String?): String? {
if (resPath == null) return null
if (resPath.contains("/")) {
return resPath.substring(resPath.lastIndexOf('/') + 1)
}
return if (resPath.contains(".")) {
resPath.substring(0, resPath.lastIndexOf('.'))
} else resPath
}
}
}
150 changes: 150 additions & 0 deletions plugins/notification/android/src/main/java/ChannelManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package app.tauri.notification

import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.ContentResolver
import android.content.Context
import android.graphics.Color
import android.media.AudioAttributes
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import app.tauri.Logger
import app.tauri.plugin.Invoke
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject

private const val CHANNEL_ID = "id"
private const val CHANNEL_NAME = "name"
private const val CHANNEL_DESCRIPTION = "description"
private const val CHANNEL_IMPORTANCE = "importance"
private const val CHANNEL_VISIBILITY = "visibility"
private const val CHANNEL_SOUND = "sound"
private const val CHANNEL_VIBRATE = "vibration"
private const val CHANNEL_USE_LIGHTS = "lights"
private const val CHANNEL_LIGHT_COLOR = "lightColor"

class ChannelManager(private var context: Context) {
private var notificationManager: NotificationManager? = null

init {
notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?
}

fun createChannel(invoke: Invoke) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = JSObject()
if (invoke.getString(CHANNEL_ID) != null) {
channel.put(CHANNEL_ID, invoke.getString(CHANNEL_ID))
} else {
invoke.reject("Channel missing identifier")
return
}
if (invoke.getString(CHANNEL_NAME) != null) {
channel.put(CHANNEL_NAME, invoke.getString(CHANNEL_NAME))
} else {
invoke.reject("Channel missing name")
return
}
channel.put(
CHANNEL_IMPORTANCE,
invoke.getInt(CHANNEL_IMPORTANCE, NotificationManager.IMPORTANCE_DEFAULT)
)
channel.put(CHANNEL_DESCRIPTION, invoke.getString(CHANNEL_DESCRIPTION, ""))
channel.put(
CHANNEL_VISIBILITY,
invoke.getInt(CHANNEL_VISIBILITY, NotificationCompat.VISIBILITY_PUBLIC)
)
channel.put(CHANNEL_SOUND, invoke.getString(CHANNEL_SOUND))
channel.put(CHANNEL_VIBRATE, invoke.getBoolean(CHANNEL_VIBRATE, false))
channel.put(CHANNEL_USE_LIGHTS, invoke.getBoolean(CHANNEL_USE_LIGHTS, false))
channel.put(CHANNEL_LIGHT_COLOR, invoke.getString(CHANNEL_LIGHT_COLOR))
createChannel(channel)
invoke.resolve()
} else {
invoke.reject("channel not available")
}
}

private fun createChannel(channel: JSObject) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
channel.getString(CHANNEL_ID),
channel.getString(CHANNEL_NAME),
channel.getInteger(CHANNEL_IMPORTANCE)!!
)
notificationChannel.description = channel.getString(CHANNEL_DESCRIPTION)
notificationChannel.lockscreenVisibility = channel.getInteger(CHANNEL_VISIBILITY, android.app.Notification.VISIBILITY_PRIVATE)
notificationChannel.enableVibration(channel.getBoolean(CHANNEL_VIBRATE, false))
notificationChannel.enableLights(channel.getBoolean(CHANNEL_USE_LIGHTS, false))
val lightColor = channel.getString(CHANNEL_LIGHT_COLOR)
if (lightColor.isNotEmpty()) {
try {
notificationChannel.lightColor = Color.parseColor(lightColor)
} catch (ex: IllegalArgumentException) {
Logger.error(
Logger.tags("NotificationChannel"),
"Invalid color provided for light color.",
null
)
}
}
var sound = channel.getString(CHANNEL_SOUND)
if (sound.isNotEmpty()) {
if (sound.contains(".")) {
sound = sound.substring(0, sound.lastIndexOf('.'))
}
val audioAttributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build()
val soundUri =
Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/raw/" + sound)
notificationChannel.setSound(soundUri, audioAttributes)
}
notificationManager?.createNotificationChannel(notificationChannel)
}
}

fun deleteChannel(invoke: Invoke) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = invoke.getString("id")
notificationManager?.deleteNotificationChannel(channelId)
invoke.resolve()
} else {
invoke.reject("channel not available")
}
}

fun listChannels(invoke: Invoke) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannels: List<NotificationChannel> =
notificationManager?.notificationChannels ?: listOf()
val channels = JSArray()
for (notificationChannel in notificationChannels) {
val channel = JSObject()
channel.put(CHANNEL_ID, notificationChannel.id)
channel.put(CHANNEL_NAME, notificationChannel.name)
channel.put(CHANNEL_DESCRIPTION, notificationChannel.description)
channel.put(CHANNEL_IMPORTANCE, notificationChannel.importance)
channel.put(CHANNEL_VISIBILITY, notificationChannel.lockscreenVisibility)
channel.put(CHANNEL_SOUND, notificationChannel.sound)
channel.put(CHANNEL_VIBRATE, notificationChannel.shouldVibrate())
channel.put(CHANNEL_USE_LIGHTS, notificationChannel.shouldShowLights())
channel.put(
CHANNEL_LIGHT_COLOR, String.format(
"#%06X",
0xFFFFFF and notificationChannel.lightColor
)
)
channels.put(channel)
}
val result = JSObject()
result.put("channels", channels)
invoke.resolve(result)
} else {
invoke.reject("channel not available")
}
}
}
Loading

0 comments on commit be1c775

Please sign in to comment.