Skip to content

Commit 2348fe0

Browse files
rajveermalviyagnprice
authored andcommitted
pigeon: Add a helper binding for listStoredSoundsInNotificationsDirectory
1 parent c8a681f commit 2348fe0

File tree

5 files changed

+264
-0
lines changed

5 files changed

+264
-0
lines changed

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

Lines changed: 78 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 StoredNotificationSound (
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 isOwned: 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?>): StoredNotificationSound {
375+
val fileName = __pigeon_list[0] as String
376+
val isOwned = __pigeon_list[1] as Boolean
377+
val contentUrl = __pigeon_list[2] as String
378+
return StoredNotificationSound(fileName, isOwned, contentUrl)
379+
}
380+
}
381+
fun toList(): List<Any?> {
382+
return listOf(
383+
fileName,
384+
isOwned,
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+
StoredNotificationSound.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 StoredNotificationSound -> {
484+
stream.write(138)
485+
writeValue(stream, value.toList())
486+
}
437487
else -> super.writeValue(stream, value)
438488
}
439489
}
@@ -459,6 +509,19 @@ 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+
* The list of notification sound files present under `Notifications/Zulip/`
514+
* in the device's shared media storage,
515+
* found with `android.content.ContentResolver.query`.
516+
*
517+
* This is a complex ad-hoc method.
518+
* For detailed behavior, see its implementation.
519+
*
520+
* Requires minimum of Android 10 (API 29) or higher.
521+
*
522+
* 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)
523+
*/
524+
fun listStoredSoundsInNotificationsDirectory(): List<StoredNotificationSound>
462525
/**
463526
* Corresponds to `android.app.NotificationManager.notify`,
464527
* combined with `androidx.core.app.NotificationCompat.Builder`.
@@ -571,6 +634,21 @@ interface AndroidNotificationHostApi {
571634
channel.setMessageHandler(null)
572635
}
573636
}
637+
run {
638+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.listStoredSoundsInNotificationsDirectory$separatedMessageChannelSuffix", codec)
639+
if (api != null) {
640+
channel.setMessageHandler { _, reply ->
641+
val wrapped: List<Any?> = try {
642+
listOf(api.listStoredSoundsInNotificationsDirectory())
643+
} catch (exception: Throwable) {
644+
wrapError(exception)
645+
}
646+
reply.reply(wrapped)
647+
}
648+
} else {
649+
channel.setMessageHandler(null)
650+
}
651+
}
574652
run {
575653
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.notify$separatedMessageChannelSuffix", codec)
576654
if (api != null) {

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

Lines changed: 53 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,54 @@ private class AndroidNotificationHost(val context: Context)
6974
NotificationManagerCompat.from(context).deleteNotificationChannel(channelId)
7075
}
7176

77+
override fun listStoredSoundsInNotificationsDirectory(): List<StoredNotificationSound> {
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+
class ResolverFailedException(msg: String) : RuntimeException(msg)
88+
89+
// Query and cursor-loop based on:
90+
// https://developer.android.com/training/data-storage/shared/media#query-collection
91+
val collection = AudioStore.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
92+
val projection = arrayOf(AudioStore._ID, AudioStore.DISPLAY_NAME, AudioStore.OWNER_PACKAGE_NAME)
93+
val selection = "${AudioStore.RELATIVE_PATH}=?"
94+
val selectionArgs = arrayOf(notificationSoundsDirectoryPath)
95+
val sortOrder = "${AudioStore._ID} ASC"
96+
97+
val sounds = mutableListOf<StoredNotificationSound>()
98+
val cursor = context.contentResolver.query(
99+
collection,
100+
projection,
101+
selection,
102+
selectionArgs,
103+
sortOrder,
104+
) ?: throw ResolverFailedException("resolver.query failed")
105+
cursor.use {
106+
val idColumn = cursor.getColumnIndexOrThrow(AudioStore._ID)
107+
val nameColumn = cursor.getColumnIndexOrThrow(AudioStore.DISPLAY_NAME)
108+
val ownerColumn = cursor.getColumnIndexOrThrow(AudioStore.OWNER_PACKAGE_NAME)
109+
while (cursor.moveToNext()) {
110+
val id = cursor.getLong(idColumn)
111+
val fileName = cursor.getString(nameColumn)
112+
val ownerPackageName = cursor.getString(ownerColumn)
113+
114+
val contentUrl = ContentUris.withAppendedId(collection, id)
115+
sounds.add(StoredNotificationSound(
116+
fileName = fileName,
117+
isOwned = context.packageName == ownerPackageName,
118+
contentUrl = contentUrl.toString()
119+
))
120+
}
121+
}
122+
return sounds
123+
}
124+
72125
@SuppressLint(
73126
// If permission is missing, `notify` will throw an exception.
74127
// 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: 84 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 StoredNotificationSound {
347+
StoredNotificationSound({
348+
required this.fileName,
349+
required this.isOwned,
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 isOwned;
361+
362+
/// A `content://…` URL pointing to the sound file.
363+
String contentUrl;
364+
365+
Object encode() {
366+
return <Object?>[
367+
fileName,
368+
isOwned,
369+
contentUrl,
370+
];
371+
}
372+
373+
static StoredNotificationSound decode(Object result) {
374+
result as List<Object?>;
375+
return StoredNotificationSound(
376+
fileName: result[0]! as String,
377+
isOwned: 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 StoredNotificationSound) {
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 StoredNotificationSound.decode(readValue(buffer)!);
399446
default:
400447
return super.readValueOfType(type, buffer);
401448
}
@@ -495,6 +542,43 @@ class AndroidNotificationHostApi {
495542
}
496543
}
497544

545+
/// The list of notification sound files present under `Notifications/Zulip/`
546+
/// in the device's shared media storage,
547+
/// found with `android.content.ContentResolver.query`.
548+
///
549+
/// This is a complex ad-hoc method.
550+
/// For detailed behavior, see its implementation.
551+
///
552+
/// Requires minimum of Android 10 (API 29) or higher.
553+
///
554+
/// 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)
555+
Future<List<StoredNotificationSound?>> listStoredSoundsInNotificationsDirectory() async {
556+
final String __pigeon_channelName = 'dev.flutter.pigeon.zulip.AndroidNotificationHostApi.listStoredSoundsInNotificationsDirectory$__pigeon_messageChannelSuffix';
557+
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
558+
__pigeon_channelName,
559+
pigeonChannelCodec,
560+
binaryMessenger: __pigeon_binaryMessenger,
561+
);
562+
final List<Object?>? __pigeon_replyList =
563+
await __pigeon_channel.send(null) as List<Object?>?;
564+
if (__pigeon_replyList == null) {
565+
throw _createConnectionError(__pigeon_channelName);
566+
} else if (__pigeon_replyList.length > 1) {
567+
throw PlatformException(
568+
code: __pigeon_replyList[0]! as String,
569+
message: __pigeon_replyList[1] as String?,
570+
details: __pigeon_replyList[2],
571+
);
572+
} else if (__pigeon_replyList[0] == null) {
573+
throw PlatformException(
574+
code: 'null-error',
575+
message: 'Host platform returned null value for non-null return value.',
576+
);
577+
} else {
578+
return (__pigeon_replyList[0] as List<Object?>?)!.cast<StoredNotificationSound?>();
579+
}
580+
}
581+
498582
/// Corresponds to `android.app.NotificationManager.notify`,
499583
/// combined with `androidx.core.app.NotificationCompat.Builder`.
500584
///

pigeon/notifications.dart

Lines changed: 37 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 StoredNotificationSound {
173+
StoredNotificationSound({
174+
required this.fileName,
175+
required this.isOwned,
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 isOwned;
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,18 @@ 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+
/// The list of notification sound files present under `Notifications/Zulip/`
210+
/// in the device's shared media storage,
211+
/// found with `android.content.ContentResolver.query`.
212+
///
213+
/// This is a complex ad-hoc method.
214+
/// For detailed behavior, see its implementation.
215+
///
216+
/// Requires minimum of Android 10 (API 29) or higher.
217+
///
218+
/// 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)
219+
List<StoredNotificationSound> listStoredSoundsInNotificationsDirectory();
220+
184221
/// Corresponds to `android.app.NotificationManager.notify`,
185222
/// combined with `androidx.core.app.NotificationCompat.Builder`.
186223
///

test/model/binding.dart

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

556+
final _storedNotificationSounds = <StoredNotificationSound>[];
557+
558+
/// Populates the media store with the provided entries.
559+
void setupStoredNotificationSounds(List<StoredNotificationSound> sounds) {
560+
_storedNotificationSounds.addAll(sounds);
561+
}
562+
563+
@override
564+
Future<List<StoredNotificationSound?>> listStoredSoundsInNotificationsDirectory() async {
565+
return _storedNotificationSounds.toList(growable: false);
566+
}
567+
556568
/// Consume the log of calls made to [notify].
557569
///
558570
/// This returns a list of the arguments to all calls made

0 commit comments

Comments
 (0)