mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +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)
|
||||
// Android 7 has no channels: the notification names its sound.
|
||||
.setSound(BulwarkMessagingService.soundUri(ctx))
|
||||
.setLargeIcon(largeIcon)
|
||||
.setContentTitle(title)
|
||||
// body is the subject with the "(No subject)" fallback; an empty
|
||||
@@ -284,7 +286,7 @@ class BulwarkFcmModule(reactContext: ReactApplicationContext)
|
||||
intent,
|
||||
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)
|
||||
.setContentTitle(groupTitle)
|
||||
.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.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.AudioAttributes
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
@@ -49,7 +51,10 @@ class BulwarkMessagingService : FirebaseMessagingService() {
|
||||
}
|
||||
|
||||
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 START_WAKE_LOCK_MS = 10_000L
|
||||
|
||||
@@ -103,10 +108,39 @@ class BulwarkMessagingService : FirebaseMessagingService() {
|
||||
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) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
if (channelId(context) != CHANNEL_ID) 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(
|
||||
CHANNEL_ID,
|
||||
"Mail notifications",
|
||||
@@ -114,6 +148,7 @@ class BulwarkMessagingService : FirebaseMessagingService() {
|
||||
).apply {
|
||||
description = "Incoming email alerts"
|
||||
enableVibration(true)
|
||||
setSound(sound, attributes)
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The calendar reminder channel names its sound by resource name from JS
|
||||
(src/lib/calendar-notifications.ts), which the release resource shrinker
|
||||
can't see. -->
|
||||
<!-- The calendar reminder channel (src/lib/calendar-notifications.ts) and the
|
||||
mail channel (BulwarkMessagingService.kt) name their sound by resource
|
||||
name, which the release resource shrinker can't see. -->
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:keep="@raw/involved_notification" />
|
||||
|
||||
+3
-2
@@ -106,8 +106,9 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
['./plugins/ios-push', { apsEnvironment: APS_ENVIRONMENT, teamId: APPLE_TEAM_ID }],
|
||||
// Bundles the Involved sound calendar reminders play on iOS
|
||||
// (src/lib/calendar-notifications.ts). Android has it in res/raw.
|
||||
// Bundles the Involved sound calendar reminders (src/lib/calendar-
|
||||
// notifications.ts) and new mail (plugins/ios-push) play on iOS.
|
||||
// Android has it in res/raw.
|
||||
[
|
||||
'expo-notifications',
|
||||
{
|
||||
|
||||
@@ -30,6 +30,10 @@ final class NotificationService: UNNotificationServiceExtension {
|
||||
contentHandler(request.content)
|
||||
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
|
||||
let push = request.content.userInfo["body"] as? [String: Any]
|
||||
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