Skip to content

Commit c4ee342

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

File tree

5 files changed

+233
-0
lines changed

5 files changed

+233
-0
lines changed

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,39 @@ data class StatusBarNotification (
345345
)
346346
}
347347
}
348+
349+
/**
350+
* Represents a row in the media database when queried via
351+
* `android.content.ContentResolver.query`.
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+
val fileName: String,
360+
val isOwner: Boolean,
361+
val uri: String
362+
363+
) {
364+
companion object {
365+
@Suppress("LocalVariableName")
366+
fun fromList(__pigeon_list: List<Any?>): StoredNotificationsSound {
367+
val fileName = __pigeon_list[0] as String
368+
val isOwner = __pigeon_list[1] as Boolean
369+
val uri = __pigeon_list[2] as String
370+
return StoredNotificationsSound(fileName, isOwner, uri)
371+
}
372+
}
373+
fun toList(): List<Any?> {
374+
return listOf(
375+
fileName,
376+
isOwner,
377+
uri,
378+
)
379+
}
380+
}
348381
private object NotificationsPigeonCodec : StandardMessageCodec() {
349382
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
350383
return when (type) {
@@ -393,6 +426,11 @@ private object NotificationsPigeonCodec : StandardMessageCodec() {
393426
StatusBarNotification.fromList(it)
394427
}
395428
}
429+
138.toByte() -> {
430+
return (readValue(buffer) as? List<Any?>)?.let {
431+
StoredNotificationsSound.fromList(it)
432+
}
433+
}
396434
else -> super.readValueOfType(type, buffer)
397435
}
398436
}
@@ -434,6 +472,10 @@ private object NotificationsPigeonCodec : StandardMessageCodec() {
434472
stream.write(137)
435473
writeValue(stream, value.toList())
436474
}
475+
is StoredNotificationsSound -> {
476+
stream.write(138)
477+
writeValue(stream, value.toList())
478+
}
437479
else -> super.writeValue(stream, value)
438480
}
439481
}
@@ -459,6 +501,17 @@ interface AndroidNotificationHostApi {
459501
* See: https://developer.android.com/reference/kotlin/androidx/core/app/NotificationManagerCompat#deleteNotificationChannel(java.lang.String)
460502
*/
461503
fun deleteNotificationChannel(channelId: String)
504+
/**
505+
* Corresponds to `android.content.ContentResolver.query`.
506+
*
507+
* Returns the list of notification sounds present under
508+
* `Notifications/Zulip/` directory in device's shared media storage.
509+
*
510+
* Requires minimum of Android 10 (API 29) or higher.
511+
*
512+
* 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)
513+
*/
514+
fun listStoredSoundsInNotificationsDirectory(): List<StoredNotificationsSound>
462515
/**
463516
* Corresponds to `android.app.NotificationManager.notify`,
464517
* combined with `androidx.core.app.NotificationCompat.Builder`.
@@ -571,6 +624,21 @@ interface AndroidNotificationHostApi {
571624
channel.setMessageHandler(null)
572625
}
573626
}
627+
run {
628+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.listStoredSoundsInNotificationsDirectory$separatedMessageChannelSuffix", codec)
629+
if (api != null) {
630+
channel.setMessageHandler { _, reply ->
631+
val wrapped: List<Any?> = try {
632+
listOf(api.listStoredSoundsInNotificationsDirectory())
633+
} catch (exception: Throwable) {
634+
wrapError(exception)
635+
}
636+
reply.reply(wrapped)
637+
}
638+
} else {
639+
channel.setMessageHandler(null)
640+
}
641+
}
574642
run {
575643
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.notify$separatedMessageChannelSuffix", codec)
576644
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 uri = ContentUris.withAppendedId(collection, id).toString()
113+
sounds.add(StoredNotificationsSound(
114+
fileName = fileName,
115+
isOwner = context.packageName == ownerPackageName,
116+
uri = uri
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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,42 @@ class StatusBarNotification {
338338
}
339339
}
340340

341+
/// Represents a row in the media database when queried via
342+
/// `android.content.ContentResolver.query`.
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.uri,
351+
});
352+
353+
String fileName;
354+
355+
bool isOwner;
356+
357+
String uri;
358+
359+
Object encode() {
360+
return <Object?>[
361+
fileName,
362+
isOwner,
363+
uri,
364+
];
365+
}
366+
367+
static StoredNotificationsSound decode(Object result) {
368+
result as List<Object?>;
369+
return StoredNotificationsSound(
370+
fileName: result[0]! as String,
371+
isOwner: result[1]! as bool,
372+
uri: result[2]! as String,
373+
);
374+
}
375+
}
376+
341377

342378
class _PigeonCodec extends StandardMessageCodec {
343379
const _PigeonCodec();
@@ -370,6 +406,9 @@ class _PigeonCodec extends StandardMessageCodec {
370406
} else if (value is StatusBarNotification) {
371407
buffer.putUint8(137);
372408
writeValue(buffer, value.encode());
409+
} else if (value is StoredNotificationsSound) {
410+
buffer.putUint8(138);
411+
writeValue(buffer, value.encode());
373412
} else {
374413
super.writeValue(buffer, value);
375414
}
@@ -396,6 +435,8 @@ class _PigeonCodec extends StandardMessageCodec {
396435
return Notification.decode(readValue(buffer)!);
397436
case 137:
398437
return StatusBarNotification.decode(readValue(buffer)!);
438+
case 138:
439+
return StoredNotificationsSound.decode(readValue(buffer)!);
399440
default:
400441
return super.readValueOfType(type, buffer);
401442
}
@@ -495,6 +536,41 @@ class AndroidNotificationHostApi {
495536
}
496537
}
497538

539+
/// Corresponds to `android.content.ContentResolver.query`.
540+
///
541+
/// Returns the list of notification sounds present under
542+
/// `Notifications/Zulip/` directory in device's shared media storage.
543+
///
544+
/// Requires minimum of Android 10 (API 29) or higher.
545+
///
546+
/// 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)
547+
Future<List<StoredNotificationsSound?>> listStoredSoundsInNotificationsDirectory() async {
548+
final String __pigeon_channelName = 'dev.flutter.pigeon.zulip.AndroidNotificationHostApi.listStoredSoundsInNotificationsDirectory$__pigeon_messageChannelSuffix';
549+
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
550+
__pigeon_channelName,
551+
pigeonChannelCodec,
552+
binaryMessenger: __pigeon_binaryMessenger,
553+
);
554+
final List<Object?>? __pigeon_replyList =
555+
await __pigeon_channel.send(null) as List<Object?>?;
556+
if (__pigeon_replyList == null) {
557+
throw _createConnectionError(__pigeon_channelName);
558+
} else if (__pigeon_replyList.length > 1) {
559+
throw PlatformException(
560+
code: __pigeon_replyList[0]! as String,
561+
message: __pigeon_replyList[1] as String?,
562+
details: __pigeon_replyList[2],
563+
);
564+
} else if (__pigeon_replyList[0] == null) {
565+
throw PlatformException(
566+
code: 'null-error',
567+
message: 'Host platform returned null value for non-null return value.',
568+
);
569+
} else {
570+
return (__pigeon_replyList[0] as List<Object?>?)!.cast<StoredNotificationsSound?>();
571+
}
572+
}
573+
498574
/// Corresponds to `android.app.NotificationManager.notify`,
499575
/// combined with `androidx.core.app.NotificationCompat.Builder`.
500576
///

pigeon/notifications.dart

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

167+
/// Represents a row in the media database when queried via
168+
/// `android.content.ContentResolver.query`.
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.uri,
177+
});
178+
179+
final String fileName;
180+
final bool isOwner;
181+
final String uri;
182+
}
183+
167184
@HostApi()
168185
abstract class AndroidNotificationHostApi {
169186
/// Corresponds to `androidx.core.app.NotificationManagerCompat.createNotificationChannel`.
@@ -181,6 +198,16 @@ abstract class AndroidNotificationHostApi {
181198
/// See: https://developer.android.com/reference/kotlin/androidx/core/app/NotificationManagerCompat#deleteNotificationChannel(java.lang.String)
182199
void deleteNotificationChannel(String channelId);
183200

201+
/// Corresponds to `android.content.ContentResolver.query`.
202+
///
203+
/// Returns the list of notification sounds present under
204+
/// `Notifications/Zulip/` directory in device's shared media storage.
205+
///
206+
/// Requires minimum of Android 10 (API 29) or higher.
207+
///
208+
/// 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)
209+
List<StoredNotificationsSound> listStoredSoundsInNotificationsDirectory();
210+
184211
/// Corresponds to `android.app.NotificationManager.notify`,
185212
/// combined with `androidx.core.app.NotificationCompat.Builder`.
186213
///

test/model/binding.dart

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

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

0 commit comments

Comments
 (0)