Skip to content

Commit 60896f8

Browse files
pigeon: Add a helper binding for listStoredSoundsInNotificationsDirectory
1 parent f86498e commit 60896f8

File tree

5 files changed

+256
-0
lines changed

5 files changed

+256
-0
lines changed

android/app/src/main/kotlin/com/zulip/flutter/Notifications.g.kt

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,47 @@ data class StatusBarNotification (
345345
)
346346
}
347347
}
348+
349+
/**
350+
* Represents details about a notification sound stored in the
351+
* shared media store.
352+
*
353+
* Returned as a list entry by
354+
* [AndroidNotificationHostApi.listStoredSoundsInNotificationsDirectory].
355+
*
356+
* Generated class from Pigeon that represents data sent in messages.
357+
*/
358+
data class StoredNotificationsSound (
359+
/** The display name of the sound file. */
360+
val fileName: String,
361+
/**
362+
* Specifies whether this file was created by the app.
363+
*
364+
* It is true if the `MediaStore.Audio.Media.OWNER_PACKAGE_NAME` key in the
365+
* metadata matches the app's package name.
366+
*/
367+
val isOwner: Boolean,
368+
/** A `content://…` URL pointing to the sound file. */
369+
val contentUrl: String
370+
371+
) {
372+
companion object {
373+
@Suppress("LocalVariableName")
374+
fun fromList(__pigeon_list: List<Any?>): StoredNotificationsSound {
375+
val fileName = __pigeon_list[0] as String
376+
val isOwner = __pigeon_list[1] as Boolean
377+
val contentUrl = __pigeon_list[2] as String
378+
return StoredNotificationsSound(fileName, isOwner, contentUrl)
379+
}
380+
}
381+
fun toList(): List<Any?> {
382+
return listOf(
383+
fileName,
384+
isOwner,
385+
contentUrl,
386+
)
387+
}
388+
}
348389
private object NotificationsPigeonCodec : StandardMessageCodec() {
349390
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
350391
return when (type) {
@@ -393,6 +434,11 @@ private object NotificationsPigeonCodec : StandardMessageCodec() {
393434
StatusBarNotification.fromList(it)
394435
}
395436
}
437+
138.toByte() -> {
438+
return (readValue(buffer) as? List<Any?>)?.let {
439+
StoredNotificationsSound.fromList(it)
440+
}
441+
}
396442
else -> super.readValueOfType(type, buffer)
397443
}
398444
}
@@ -434,6 +480,10 @@ private object NotificationsPigeonCodec : StandardMessageCodec() {
434480
stream.write(137)
435481
writeValue(stream, value.toList())
436482
}
483+
is StoredNotificationsSound -> {
484+
stream.write(138)
485+
writeValue(stream, value.toList())
486+
}
437487
else -> super.writeValue(stream, value)
438488
}
439489
}
@@ -459,6 +509,17 @@ interface AndroidNotificationHostApi {
459509
* See: https://developer.android.com/reference/kotlin/androidx/core/app/NotificationManagerCompat#deleteNotificationChannel(java.lang.String)
460510
*/
461511
fun deleteNotificationChannel(channelId: String)
512+
/**
513+
* Corresponds to `android.content.ContentResolver.query`.
514+
*
515+
* Returns the list of notification sounds present under
516+
* `Notifications/Zulip/` directory in device's shared media storage.
517+
*
518+
* Requires minimum of Android 10 (API 29) or higher.
519+
*
520+
* See: https://developer.android.com/reference/android/content/ContentResolver#query(android.net.Uri,%20java.lang.String[],%20java.lang.String,%20java.lang.String[],%20java.lang.String)
521+
*/
522+
fun listStoredSoundsInNotificationsDirectory(): List<StoredNotificationsSound>
462523
/**
463524
* Corresponds to `android.app.NotificationManager.notify`,
464525
* combined with `androidx.core.app.NotificationCompat.Builder`.
@@ -571,6 +632,21 @@ interface AndroidNotificationHostApi {
571632
channel.setMessageHandler(null)
572633
}
573634
}
635+
run {
636+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.listStoredSoundsInNotificationsDirectory$separatedMessageChannelSuffix", codec)
637+
if (api != null) {
638+
channel.setMessageHandler { _, reply ->
639+
val wrapped: List<Any?> = try {
640+
listOf(api.listStoredSoundsInNotificationsDirectory())
641+
} catch (exception: Throwable) {
642+
wrapError(exception)
643+
}
644+
reply.reply(wrapped)
645+
}
646+
} else {
647+
channel.setMessageHandler(null)
648+
}
649+
}
574650
run {
575651
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.notify$separatedMessageChannelSuffix", codec)
576652
if (api != null) {

android/app/src/main/kotlin/com/zulip/flutter/ZulipPlugin.kt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package com.zulip.flutter
22

33
import android.annotation.SuppressLint
4+
import android.content.ContentUris
45
import android.content.Context
56
import android.content.Intent
67
import android.net.Uri
8+
import android.os.Build
79
import android.os.Bundle
10+
import android.os.Environment
11+
import android.provider.MediaStore
12+
import android.provider.MediaStore.Audio.Media as AudioStore
813
import android.util.Log
914
import androidx.annotation.Keep
1015
import androidx.core.app.NotificationChannelCompat
@@ -69,6 +74,52 @@ private class AndroidNotificationHost(val context: Context)
6974
NotificationManagerCompat.from(context).deleteNotificationChannel(channelId)
7075
}
7176

77+
override fun listStoredSoundsInNotificationsDirectory(): List<StoredNotificationsSound> {
78+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
79+
throw UnsupportedOperationException()
80+
}
81+
82+
// The directory we store our notification sounds into,
83+
// expressed as a relative path suitable for:
84+
// https://developer.android.com/reference/kotlin/android/provider/MediaStore.MediaColumns#RELATIVE_PATH:kotlin.String
85+
val notificationSoundsDirectoryPath = "${Environment.DIRECTORY_NOTIFICATIONS}/Zulip/"
86+
87+
// Query and cursor-loop based on:
88+
// https://developer.android.com/training/data-storage/shared/media#query-collection
89+
val collection = AudioStore.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
90+
val projection = arrayOf(AudioStore._ID, AudioStore.DISPLAY_NAME, AudioStore.OWNER_PACKAGE_NAME)
91+
val selection = "${AudioStore.RELATIVE_PATH}=?"
92+
val selectionArgs = arrayOf(notificationSoundsDirectoryPath)
93+
val sortOrder = "${AudioStore._ID} ASC"
94+
95+
val sounds = mutableListOf<StoredNotificationsSound>()
96+
val query = context.contentResolver.query(
97+
collection,
98+
projection,
99+
selection,
100+
selectionArgs,
101+
sortOrder,
102+
)
103+
query?.use { cursor ->
104+
val idColumn = cursor.getColumnIndexOrThrow(AudioStore._ID)
105+
val nameColumn = cursor.getColumnIndexOrThrow(AudioStore.DISPLAY_NAME)
106+
val ownerColumn = cursor.getColumnIndexOrThrow(AudioStore.OWNER_PACKAGE_NAME)
107+
while (cursor.moveToNext()) {
108+
val id = cursor.getLong(idColumn)
109+
val fileName = cursor.getString(nameColumn)
110+
val ownerPackageName = cursor.getString(ownerColumn)
111+
112+
val contentUrl = ContentUris.withAppendedId(collection, id)
113+
sounds.add(StoredNotificationsSound(
114+
fileName = fileName,
115+
isOwner = context.packageName == ownerPackageName,
116+
contentUrl = contentUrl.toString()
117+
))
118+
}
119+
}
120+
return sounds
121+
}
122+
72123
@SuppressLint(
73124
// If permission is missing, `notify` will throw an exception.
74125
// Which hopefully will propagate to Dart, and then it's up to Dart code to handle it.

lib/host/android_notifications.g.dart

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,48 @@ class StatusBarNotification {
338338
}
339339
}
340340

341+
/// Represents details about a notification sound stored in the
342+
/// shared media store.
343+
///
344+
/// Returned as a list entry by
345+
/// [AndroidNotificationHostApi.listStoredSoundsInNotificationsDirectory].
346+
class StoredNotificationsSound {
347+
StoredNotificationsSound({
348+
required this.fileName,
349+
required this.isOwner,
350+
required this.contentUrl,
351+
});
352+
353+
/// The display name of the sound file.
354+
String fileName;
355+
356+
/// Specifies whether this file was created by the app.
357+
///
358+
/// It is true if the `MediaStore.Audio.Media.OWNER_PACKAGE_NAME` key in the
359+
/// metadata matches the app's package name.
360+
bool isOwner;
361+
362+
/// A `content://…` URL pointing to the sound file.
363+
String contentUrl;
364+
365+
Object encode() {
366+
return <Object?>[
367+
fileName,
368+
isOwner,
369+
contentUrl,
370+
];
371+
}
372+
373+
static StoredNotificationsSound decode(Object result) {
374+
result as List<Object?>;
375+
return StoredNotificationsSound(
376+
fileName: result[0]! as String,
377+
isOwner: result[1]! as bool,
378+
contentUrl: result[2]! as String,
379+
);
380+
}
381+
}
382+
341383

342384
class _PigeonCodec extends StandardMessageCodec {
343385
const _PigeonCodec();
@@ -370,6 +412,9 @@ class _PigeonCodec extends StandardMessageCodec {
370412
} else if (value is StatusBarNotification) {
371413
buffer.putUint8(137);
372414
writeValue(buffer, value.encode());
415+
} else if (value is StoredNotificationsSound) {
416+
buffer.putUint8(138);
417+
writeValue(buffer, value.encode());
373418
} else {
374419
super.writeValue(buffer, value);
375420
}
@@ -396,6 +441,8 @@ class _PigeonCodec extends StandardMessageCodec {
396441
return Notification.decode(readValue(buffer)!);
397442
case 137:
398443
return StatusBarNotification.decode(readValue(buffer)!);
444+
case 138:
445+
return StoredNotificationsSound.decode(readValue(buffer)!);
399446
default:
400447
return super.readValueOfType(type, buffer);
401448
}
@@ -495,6 +542,41 @@ class AndroidNotificationHostApi {
495542
}
496543
}
497544

545+
/// Corresponds to `android.content.ContentResolver.query`.
546+
///
547+
/// Returns the list of notification sounds present under
548+
/// `Notifications/Zulip/` directory in device's shared media storage.
549+
///
550+
/// Requires minimum of Android 10 (API 29) or higher.
551+
///
552+
/// See: https://developer.android.com/reference/android/content/ContentResolver#query(android.net.Uri,%20java.lang.String[],%20java.lang.String,%20java.lang.String[],%20java.lang.String)
553+
Future<List<StoredNotificationsSound?>> listStoredSoundsInNotificationsDirectory() async {
554+
final String __pigeon_channelName = 'dev.flutter.pigeon.zulip.AndroidNotificationHostApi.listStoredSoundsInNotificationsDirectory$__pigeon_messageChannelSuffix';
555+
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
556+
__pigeon_channelName,
557+
pigeonChannelCodec,
558+
binaryMessenger: __pigeon_binaryMessenger,
559+
);
560+
final List<Object?>? __pigeon_replyList =
561+
await __pigeon_channel.send(null) as List<Object?>?;
562+
if (__pigeon_replyList == null) {
563+
throw _createConnectionError(__pigeon_channelName);
564+
} else if (__pigeon_replyList.length > 1) {
565+
throw PlatformException(
566+
code: __pigeon_replyList[0]! as String,
567+
message: __pigeon_replyList[1] as String?,
568+
details: __pigeon_replyList[2],
569+
);
570+
} else if (__pigeon_replyList[0] == null) {
571+
throw PlatformException(
572+
code: 'null-error',
573+
message: 'Host platform returned null value for non-null return value.',
574+
);
575+
} else {
576+
return (__pigeon_replyList[0] as List<Object?>?)!.cast<StoredNotificationsSound?>();
577+
}
578+
}
579+
498580
/// Corresponds to `android.app.NotificationManager.notify`,
499581
/// combined with `androidx.core.app.NotificationCompat.Builder`.
500582
///

pigeon/notifications.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,31 @@ class StatusBarNotification {
164164
// Various other properties too; add them if needed.
165165
}
166166

167+
/// Represents details about a notification sound stored in the
168+
/// shared media store.
169+
///
170+
/// Returned as a list entry by
171+
/// [AndroidNotificationHostApi.listStoredSoundsInNotificationsDirectory].
172+
class StoredNotificationsSound {
173+
StoredNotificationsSound({
174+
required this.fileName,
175+
required this.isOwner,
176+
required this.contentUrl,
177+
});
178+
179+
/// The display name of the sound file.
180+
final String fileName;
181+
182+
/// Specifies whether this file was created by the app.
183+
///
184+
/// It is true if the `MediaStore.Audio.Media.OWNER_PACKAGE_NAME` key in the
185+
/// metadata matches the app's package name.
186+
final bool isOwner;
187+
188+
/// A `content://…` URL pointing to the sound file.
189+
final String contentUrl;
190+
}
191+
167192
@HostApi()
168193
abstract class AndroidNotificationHostApi {
169194
/// Corresponds to `androidx.core.app.NotificationManagerCompat.createNotificationChannel`.
@@ -181,6 +206,16 @@ abstract class AndroidNotificationHostApi {
181206
/// See: https://developer.android.com/reference/kotlin/androidx/core/app/NotificationManagerCompat#deleteNotificationChannel(java.lang.String)
182207
void deleteNotificationChannel(String channelId);
183208

209+
/// Corresponds to `android.content.ContentResolver.query`.
210+
///
211+
/// Returns the list of notification sounds present under
212+
/// `Notifications/Zulip/` directory in device's shared media storage.
213+
///
214+
/// Requires minimum of Android 10 (API 29) or higher.
215+
///
216+
/// See: https://developer.android.com/reference/android/content/ContentResolver#query(android.net.Uri,%20java.lang.String[],%20java.lang.String,%20java.lang.String[],%20java.lang.String)
217+
List<StoredNotificationsSound> listStoredSoundsInNotificationsDirectory();
218+
184219
/// Corresponds to `android.app.NotificationManager.notify`,
185220
/// combined with `androidx.core.app.NotificationCompat.Builder`.
186221
///

test/model/binding.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,18 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi {
549549
_activeChannels.remove(channelId);
550550
}
551551

552+
final _storedNotificationSounds = <StoredNotificationsSound>[];
553+
554+
/// Populates the media store with the provided entries.
555+
void setupStoredNotificationSounds(List<StoredNotificationsSound> sounds) {
556+
_storedNotificationSounds.addAll(sounds);
557+
}
558+
559+
@override
560+
Future<List<StoredNotificationsSound?>> listStoredSoundsInNotificationsDirectory() async {
561+
return _storedNotificationSounds.toList(growable: false);
562+
}
563+
552564
/// Consume the log of calls made to [notify].
553565
///
554566
/// This returns a list of the arguments to all calls made

0 commit comments

Comments
 (0)