mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 00:06:23 +00:00
feat: play the Involved sound for new mail
This commit is contained in:
@@ -153,8 +153,10 @@ class BulwarkFcmModule(reactContext: ReactApplicationContext)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(ctx, BulwarkMessagingService.CHANNEL_ID)
|
val builder = NotificationCompat.Builder(ctx, BulwarkMessagingService.channelId(ctx))
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
// Android 7 has no channels: the notification names its sound.
|
||||||
|
.setSound(BulwarkMessagingService.soundUri(ctx))
|
||||||
.setLargeIcon(largeIcon)
|
.setLargeIcon(largeIcon)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
// body is the subject with the "(No subject)" fallback; an empty
|
// body is the subject with the "(No subject)" fallback; an empty
|
||||||
@@ -284,7 +286,7 @@ class BulwarkFcmModule(reactContext: ReactApplicationContext)
|
|||||||
intent,
|
intent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||||
)
|
)
|
||||||
val summary = NotificationCompat.Builder(ctx, BulwarkMessagingService.CHANNEL_ID)
|
val summary = NotificationCompat.Builder(ctx, BulwarkMessagingService.channelId(ctx))
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setContentTitle(groupTitle)
|
.setContentTitle(groupTitle)
|
||||||
.setContentText(if (count == 1) "1 new message" else "$count new messages")
|
.setContentText(if (count == 1) "1 new message" else "$count new messages")
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import android.app.NotificationManager
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.media.AudioAttributes
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
@@ -49,7 +51,10 @@ class BulwarkMessagingService : FirebaseMessagingService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CHANNEL_ID = "bulwark_mail"
|
// Plays the Involved sound; see channelId().
|
||||||
|
const val CHANNEL_ID = "bulwark_mail_involved"
|
||||||
|
// The first mail channel, with the system's default sound.
|
||||||
|
const val LEGACY_CHANNEL_ID = "bulwark_mail"
|
||||||
private const val TAG = "BulwarkPush"
|
private const val TAG = "BulwarkPush"
|
||||||
private const val START_WAKE_LOCK_MS = 10_000L
|
private const val START_WAKE_LOCK_MS = 10_000L
|
||||||
|
|
||||||
@@ -103,10 +108,39 @@ class BulwarkMessagingService : FirebaseMessagingService() {
|
|||||||
startWakeLock = null
|
startWakeLock = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel new-mail notifications go to. A channel's sound can't
|
||||||
|
* change after it is created, so the Involved sound (the webmail's
|
||||||
|
* default, lib/notification-sound.ts) needs a channel of its own; the
|
||||||
|
* old one stays as the user left it. Someone who turned mail
|
||||||
|
* notifications off in the old channel keeps them off: their
|
||||||
|
* notifications keep going there.
|
||||||
|
*/
|
||||||
|
fun channelId(context: Context): String {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return CHANNEL_ID
|
||||||
|
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
val legacy = manager.getNotificationChannel(LEGACY_CHANNEL_ID)
|
||||||
|
return if (legacy != null && legacy.importance == NotificationManager.IMPORTANCE_NONE) {
|
||||||
|
LEGACY_CHANNEL_ID
|
||||||
|
} else {
|
||||||
|
CHANNEL_ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The Involved sound (res/raw, kept from the shrinker by res/raw/keep.xml). */
|
||||||
|
fun soundUri(context: Context): Uri =
|
||||||
|
Uri.parse("android.resource://${context.packageName}/raw/involved_notification")
|
||||||
|
|
||||||
fun ensureChannel(context: Context) {
|
fun ensureChannel(context: Context) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
if (channelId(context) != CHANNEL_ID) return
|
||||||
if (manager.getNotificationChannel(CHANNEL_ID) != null) return
|
if (manager.getNotificationChannel(CHANNEL_ID) != null) return
|
||||||
|
val sound = soundUri(context)
|
||||||
|
val attributes = AudioAttributes.Builder()
|
||||||
|
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
|
.build()
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
"Mail notifications",
|
"Mail notifications",
|
||||||
@@ -114,6 +148,7 @@ class BulwarkMessagingService : FirebaseMessagingService() {
|
|||||||
).apply {
|
).apply {
|
||||||
description = "Incoming email alerts"
|
description = "Incoming email alerts"
|
||||||
enableVibration(true)
|
enableVibration(true)
|
||||||
|
setSound(sound, attributes)
|
||||||
}
|
}
|
||||||
manager.createNotificationChannel(channel)
|
manager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- The calendar reminder channel names its sound by resource name from JS
|
<!-- The calendar reminder channel (src/lib/calendar-notifications.ts) and the
|
||||||
(src/lib/calendar-notifications.ts), which the release resource shrinker
|
mail channel (BulwarkMessagingService.kt) name their sound by resource
|
||||||
can't see. -->
|
name, which the release resource shrinker can't see. -->
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools"
|
<resources xmlns:tools="http://schemas.android.com/tools"
|
||||||
tools:keep="@raw/involved_notification" />
|
tools:keep="@raw/involved_notification" />
|
||||||
|
|||||||
+3
-2
@@ -106,8 +106,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
['./plugins/ios-push', { apsEnvironment: APS_ENVIRONMENT, teamId: APPLE_TEAM_ID }],
|
['./plugins/ios-push', { apsEnvironment: APS_ENVIRONMENT, teamId: APPLE_TEAM_ID }],
|
||||||
// Bundles the Involved sound calendar reminders play on iOS
|
// Bundles the Involved sound calendar reminders (src/lib/calendar-
|
||||||
// (src/lib/calendar-notifications.ts). Android has it in res/raw.
|
// notifications.ts) and new mail (plugins/ios-push) play on iOS.
|
||||||
|
// Android has it in res/raw.
|
||||||
[
|
[
|
||||||
'expo-notifications',
|
'expo-notifications',
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ final class NotificationService: UNNotificationServiceExtension {
|
|||||||
contentHandler(request.content)
|
contentHandler(request.content)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// The webmail's default new-mail sound. The app bundles the file
|
||||||
|
// (expo-notifications in app.config.js); iOS looks it up in the app's
|
||||||
|
// bundle, so the placeholder plays it too.
|
||||||
|
content.sound = UNNotificationSound(named: UNNotificationSoundName("involved_notification.wav"))
|
||||||
placeholder = content
|
placeholder = content
|
||||||
let push = request.content.userInfo["body"] as? [String: Any]
|
let push = request.content.userInfo["body"] as? [String: Any]
|
||||||
work = Task { [weak self] in
|
work = Task { [weak self] in
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
// New mail plays the Involved sound, the webmail's default
|
||||||
|
// (lib/notification-sound.ts). Native code, so these read the sources: on
|
||||||
|
// Android a channel of its own (a channel's sound is fixed once created), on
|
||||||
|
// iOS the notification service extension names the bundled file.
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, '../../..');
|
||||||
|
const read = (p: string) => fs.readFileSync(path.join(ROOT, p), 'utf8');
|
||||||
|
const KOTLIN = 'android/app/src/main/java/com/anonymous/bulwarkmobile';
|
||||||
|
|
||||||
|
describe('Involved sound for new mail', () => {
|
||||||
|
const service = read(`${KOTLIN}/BulwarkMessagingService.kt`);
|
||||||
|
const fcm = read(`${KOTLIN}/BulwarkFcmModule.kt`);
|
||||||
|
|
||||||
|
it('creates a new Android channel with the Involved sound and keeps the old id', () => {
|
||||||
|
expect(service).toMatch(/const val CHANNEL_ID = "bulwark_mail_involved"/);
|
||||||
|
expect(service).toMatch(/const val LEGACY_CHANNEL_ID = "bulwark_mail"/);
|
||||||
|
expect(service).toContain('android.resource://${context.packageName}/raw/involved_notification');
|
||||||
|
expect(service).toMatch(/setSound\(sound, attributes\)/);
|
||||||
|
expect(service).toContain('AudioAttributes.USAGE_NOTIFICATION');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps using the old channel when the user turned it off', () => {
|
||||||
|
const fn = service.slice(service.indexOf('fun channelId('), service.indexOf('fun soundUri('));
|
||||||
|
expect(fn).toContain('getNotificationChannel(LEGACY_CHANNEL_ID)');
|
||||||
|
expect(fn).toContain('IMPORTANCE_NONE');
|
||||||
|
// No new channel is created for such a user.
|
||||||
|
const ensure = service.slice(service.indexOf('fun ensureChannel('));
|
||||||
|
expect(ensure).toMatch(/if \(channelId\(context\) != CHANNEL_ID\) return/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('posts mail notifications to the channel channelId() picks', () => {
|
||||||
|
expect(fcm).not.toContain('BulwarkMessagingService.CHANNEL_ID');
|
||||||
|
expect(fcm.match(/NotificationCompat\.Builder\(ctx, BulwarkMessagingService\.channelId\(ctx\)\)/g)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ships the sound on both platforms', () => {
|
||||||
|
expect(fs.existsSync(path.join(ROOT, 'android/app/src/main/res/raw/involved_notification.mp3'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(ROOT, 'assets/sounds/involved_notification.wav'))).toBe(true);
|
||||||
|
expect(read('app.config.js')).toContain("sounds: ['./assets/sounds/involved_notification.wav']");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('names the bundled sound in the iOS notification extension, placeholder included', () => {
|
||||||
|
const nse = read('plugins/ios-push/NotificationService.swift');
|
||||||
|
const sound = nse.indexOf('content.sound = UNNotificationSound(named: UNNotificationSoundName("involved_notification.wav"))');
|
||||||
|
expect(sound).toBeGreaterThan(-1);
|
||||||
|
expect(sound).toBeLessThan(nse.indexOf('placeholder = content'));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user