Skip to content

Commit 6b800a7

Browse files
pigeon: Add a helper binding for listStoredSoundsInNotificationsDirectory
1 parent 5e63b1d commit 6b800a7

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
@@ -315,6 +315,39 @@ data class StatusBarNotification (
315315
)
316316
}
317317
}
318+
319+
/**
320+
* Represents a row in the media database when queried via
321+
* `android.content.ContentResolver.query`.
322+
*
323+
* Returned as a list entry by
324+
* [AndroidNotificationHostApi.listStoredSoundsInNotificationsDirectory].
325+
*
326+
* Generated class from Pigeon that represents data sent in messages.
327+
*/
328+
data class StoredNotificationsSound (
329+
val fileName: String,
330+
val isOwner: Boolean,
331+
val uri: String
332+
333+
) {
334+
companion object {
335+
@Suppress("LocalVariableName")
336+
fun fromList(__pigeon_list: List<Any?>): StoredNotificationsSound {
337+
val fileName = __pigeon_list[0] as String
338+
val isOwner = __pigeon_list[1] as Boolean
339+
val uri = __pigeon_list[2] as String
340+
return StoredNotificationsSound(fileName, isOwner, uri)
341+
}
342+
}
343+
fun toList(): List<Any?> {
344+
return listOf(
345+
fileName,
346+
isOwner,
347+
uri,
348+
)
349+
}
350+
}
318351
private object NotificationsPigeonCodec : StandardMessageCodec() {
319352
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
320353
return when (type) {
@@ -358,6 +391,11 @@ private object NotificationsPigeonCodec : StandardMessageCodec() {
358391
StatusBarNotification.fromList(it)
359392
}
360393
}
394+
137.toByte() -> {
395+
return (readValue(buffer) as? List<Any?>)?.let {
396+
StoredNotificationsSound.fromList(it)
397+
}
398+
}
361399
else -> super.readValueOfType(type, buffer)
362400
}
363401
}
@@ -395,6 +433,10 @@ private object NotificationsPigeonCodec : StandardMessageCodec() {
395433
stream.write(136)
396434
writeValue(stream, value.toList())
397435
}
436+
is StoredNotificationsSound -> {
437+
stream.write(137)
438+
writeValue(stream, value.toList())
439+
}
398440
else -> super.writeValue(stream, value)
399441
}
400442
}
@@ -420,6 +462,17 @@ interface AndroidNotificationHostApi {
420462
* See: https://developer.android.com/reference/kotlin/androidx/core/app/NotificationManagerCompat#deleteNotificationChannel(java.lang.String)
421463
*/
422464
fun deleteNotificationChannel(channelId: String)
465+
/**
466+
* Corresponds to `android.content.ContentResolver.query`.
467+
*
468+
* Returns the list of notification sounds present under
469+
* `Notifications/Zulip/` directory in device's shared media storage.
470+
*
471+
* Requires minimum of Android 10 (API 29) or higher.
472+
*
473+
* 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)
474+
*/
475+
fun listStoredSoundsInNotificationsDirectory(): List<StoredNotificationsSound>
423476
/**
424477
* Corresponds to `android.app.NotificationManager.notify`,
425478
* combined with `androidx.core.app.NotificationCompat.Builder`.
@@ -532,6 +585,21 @@ interface AndroidNotificationHostApi {
532585
channel.setMessageHandler(null)
533586
}
534587
}
588+
run {
589+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.listStoredSoundsInNotificationsDirectory$separatedMessageChannelSuffix", codec)
590+
if (api != null) {
591+
channel.setMessageHandler { _, reply ->
592+
val wrapped: List<Any?> = try {
593+
listOf(api.listStoredSoundsInNotificationsDirectory())
594+
} catch (exception: Throwable) {
595+
wrapError(exception)
596+
}
597+
reply.reply(wrapped)
598+
}
599+
} else {
600+
channel.setMessageHandler(null)
601+
}
602+
}
535603
run {
536604
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.notify$separatedMessageChannelSuffix", codec)
537605
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,9 +1,14 @@
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
7+
import android.os.Build
68
import android.os.Bundle
9+
import android.os.Environment
10+
import android.provider.MediaStore
11+
import android.provider.MediaStore.Audio.Media as AudioStore
712
import android.util.Log
813
import androidx.annotation.Keep
914
import androidx.core.app.NotificationChannelCompat
@@ -68,6 +73,52 @@ private class AndroidNotificationHost(val context: Context)
6873
NotificationManagerCompat.from(context).deleteNotificationChannel(channelId)
6974
}
7075

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

306+
/// Represents a row in the media database when queried via
307+
/// `android.content.ContentResolver.query`.
308+
///
309+
/// Returned as a list entry by
310+
/// [AndroidNotificationHostApi.listStoredSoundsInNotificationsDirectory].
311+
class StoredNotificationsSound {
312+
StoredNotificationsSound({
313+
required this.fileName,
314+
required this.isOwner,
315+
required this.uri,
316+
});
317+
318+
String fileName;
319+
320+
bool isOwner;
321+
322+
String uri;
323+
324+
Object encode() {
325+
return <Object?>[
326+
fileName,
327+
isOwner,
328+
uri,
329+
];
330+
}
331+
332+
static StoredNotificationsSound decode(Object result) {
333+
result as List<Object?>;
334+
return StoredNotificationsSound(
335+
fileName: result[0]! as String,
336+
isOwner: result[1]! as bool,
337+
uri: result[2]! as String,
338+
);
339+
}
340+
}
341+
306342

307343
class _PigeonCodec extends StandardMessageCodec {
308344
const _PigeonCodec();
@@ -332,6 +368,9 @@ class _PigeonCodec extends StandardMessageCodec {
332368
} else if (value is StatusBarNotification) {
333369
buffer.putUint8(136);
334370
writeValue(buffer, value.encode());
371+
} else if (value is StoredNotificationsSound) {
372+
buffer.putUint8(137);
373+
writeValue(buffer, value.encode());
335374
} else {
336375
super.writeValue(buffer, value);
337376
}
@@ -356,6 +395,8 @@ class _PigeonCodec extends StandardMessageCodec {
356395
return Notification.decode(readValue(buffer)!);
357396
case 136:
358397
return StatusBarNotification.decode(readValue(buffer)!);
398+
case 137:
399+
return StoredNotificationsSound.decode(readValue(buffer)!);
359400
default:
360401
return super.readValueOfType(type, buffer);
361402
}
@@ -455,6 +496,41 @@ class AndroidNotificationHostApi {
455496
}
456497
}
457498

499+
/// Corresponds to `android.content.ContentResolver.query`.
500+
///
501+
/// Returns the list of notification sounds present under
502+
/// `Notifications/Zulip/` directory in device's shared media storage.
503+
///
504+
/// Requires minimum of Android 10 (API 29) or higher.
505+
///
506+
/// 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)
507+
Future<List<StoredNotificationsSound?>> listStoredSoundsInNotificationsDirectory() async {
508+
final String __pigeon_channelName = 'dev.flutter.pigeon.zulip.AndroidNotificationHostApi.listStoredSoundsInNotificationsDirectory$__pigeon_messageChannelSuffix';
509+
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
510+
__pigeon_channelName,
511+
pigeonChannelCodec,
512+
binaryMessenger: __pigeon_binaryMessenger,
513+
);
514+
final List<Object?>? __pigeon_replyList =
515+
await __pigeon_channel.send(null) as List<Object?>?;
516+
if (__pigeon_replyList == null) {
517+
throw _createConnectionError(__pigeon_channelName);
518+
} else if (__pigeon_replyList.length > 1) {
519+
throw PlatformException(
520+
code: __pigeon_replyList[0]! as String,
521+
message: __pigeon_replyList[1] as String?,
522+
details: __pigeon_replyList[2],
523+
);
524+
} else if (__pigeon_replyList[0] == null) {
525+
throw PlatformException(
526+
code: 'null-error',
527+
message: 'Host platform returned null value for non-null return value.',
528+
);
529+
} else {
530+
return (__pigeon_replyList[0] as List<Object?>?)!.cast<StoredNotificationsSound?>();
531+
}
532+
}
533+
458534
/// Corresponds to `android.app.NotificationManager.notify`,
459535
/// combined with `androidx.core.app.NotificationCompat.Builder`.
460536
///

pigeon/notifications.dart

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

156+
/// Represents a row in the media database when queried via
157+
/// `android.content.ContentResolver.query`.
158+
///
159+
/// Returned as a list entry by
160+
/// [AndroidNotificationHostApi.listStoredSoundsInNotificationsDirectory].
161+
class StoredNotificationsSound {
162+
StoredNotificationsSound({
163+
required this.fileName,
164+
required this.isOwner,
165+
required this.uri,
166+
});
167+
168+
final String fileName;
169+
final bool isOwner;
170+
final String uri;
171+
}
172+
156173
@HostApi()
157174
abstract class AndroidNotificationHostApi {
158175
/// Corresponds to `androidx.core.app.NotificationManagerCompat.createNotificationChannel`.
@@ -170,6 +187,16 @@ abstract class AndroidNotificationHostApi {
170187
/// See: https://developer.android.com/reference/kotlin/androidx/core/app/NotificationManagerCompat#deleteNotificationChannel(java.lang.String)
171188
void deleteNotificationChannel(String channelId);
172189

190+
/// Corresponds to `android.content.ContentResolver.query`.
191+
///
192+
/// Returns the list of notification sounds present under
193+
/// `Notifications/Zulip/` directory in device's shared media storage.
194+
///
195+
/// Requires minimum of Android 10 (API 29) or higher.
196+
///
197+
/// 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)
198+
List<StoredNotificationsSound> listStoredSoundsInNotificationsDirectory();
199+
173200
/// Corresponds to `android.app.NotificationManager.notify`,
174201
/// combined with `androidx.core.app.NotificationCompat.Builder`.
175202
///

test/model/binding.dart

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

591+
final _storedNotificationSounds = <StoredNotificationsSound>[];
592+
593+
void setupStoredNotificationSounds(List<StoredNotificationsSound> sounds) {
594+
_storedNotificationSounds.addAll(sounds);
595+
}
596+
597+
@override
598+
Future<List<StoredNotificationsSound?>> listStoredSoundsInNotificationsDirectory() async {
599+
return _storedNotificationSounds;
600+
}
601+
591602
/// Consume the log of calls made to [notify].
592603
///
593604
/// This returns a list of the arguments to all calls made

0 commit comments

Comments
 (0)