Skip to content

Commit a41f4fe

Browse files
pigeon: Add helper binding for copySoundResourceToMediaStore
1 parent c4ee342 commit a41f4fe

File tree

5 files changed

+177
-5
lines changed

5 files changed

+177
-5
lines changed

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

+35
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,23 @@ interface AndroidNotificationHostApi {
512512
* 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)
513513
*/
514514
fun listStoredSoundsInNotificationsDirectory(): List<StoredNotificationsSound>
515+
/**
516+
* Wraps `android.content.ContentResolver.insert` combined with
517+
* `android.content.ContentResolver.openOutputStream` and
518+
* `android.content.res.Resources.openRawResource`.
519+
*
520+
* Copies a raw resource audio file to `Notifications/Zulip/`
521+
* directory in device's shared media storage. Returns the uri
522+
* of the target file in media store.
523+
*
524+
* Requires minimum of Android 10 (API 29) or higher.
525+
*
526+
* See:
527+
* https://developer.android.com/reference/android/content/ContentResolver#insert(android.net.Uri,%20android.content.ContentValues)
528+
* https://developer.android.com/reference/android/content/ContentResolver#openOutputStream(android.net.Uri)
529+
* https://developer.android.com/reference/android/content/res/Resources#openRawResource(int)
530+
*/
531+
fun copySoundResourceToMediaStore(targetFileDisplayName: String, sourceResourceName: String): String
515532
/**
516533
* Corresponds to `android.app.NotificationManager.notify`,
517534
* combined with `androidx.core.app.NotificationCompat.Builder`.
@@ -639,6 +656,24 @@ interface AndroidNotificationHostApi {
639656
channel.setMessageHandler(null)
640657
}
641658
}
659+
run {
660+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.copySoundResourceToMediaStore$separatedMessageChannelSuffix", codec)
661+
if (api != null) {
662+
channel.setMessageHandler { message, reply ->
663+
val args = message as List<Any?>
664+
val targetFileDisplayNameArg = args[0] as String
665+
val sourceResourceNameArg = args[1] as String
666+
val wrapped: List<Any?> = try {
667+
listOf(api.copySoundResourceToMediaStore(targetFileDisplayNameArg, sourceResourceNameArg))
668+
} catch (exception: Throwable) {
669+
wrapError(exception)
670+
}
671+
reply.reply(wrapped)
672+
}
673+
} else {
674+
channel.setMessageHandler(null)
675+
}
676+
}
642677
run {
643678
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.notify$separatedMessageChannelSuffix", codec)
644679
if (api != null) {

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

+46-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.zulip.flutter
22

33
import android.annotation.SuppressLint
44
import android.content.ContentUris
5+
import android.content.ContentValues
56
import android.content.Context
67
import android.content.Intent
78
import android.net.Uri
@@ -49,6 +50,11 @@ fun toPigeonPerson(person: androidx.core.app.Person): Person {
4950

5051
private class AndroidNotificationHost(val context: Context)
5152
: AndroidNotificationHostApi {
53+
// The directory we store our notification sounds into,
54+
// expressed as a relative path suitable for:
55+
// https://developer.android.com/reference/kotlin/android/provider/MediaStore.MediaColumns#RELATIVE_PATH:kotlin.String
56+
private val notificationSoundsDirectoryPath = "${Environment.DIRECTORY_NOTIFICATIONS}/Zulip/"
57+
5258
override fun createNotificationChannel(channel: NotificationChannel) {
5359
val notificationChannel = NotificationChannelCompat
5460
.Builder(channel.id, channel.importance.toInt()).apply {
@@ -79,11 +85,6 @@ private class AndroidNotificationHost(val context: Context)
7985
throw UnsupportedOperationException()
8086
}
8187

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-
8788
// Query and cursor-loop based on:
8889
// https://developer.android.com/training/data-storage/shared/media#query-collection
8990
val collection = AudioStore.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
@@ -120,6 +121,46 @@ private class AndroidNotificationHost(val context: Context)
120121
return sounds
121122
}
122123

124+
@SuppressLint(
125+
// For `getIdentifier`. TODO make a cleaner API.
126+
"DiscouragedApi")
127+
override fun copySoundResourceToMediaStore(
128+
targetFileDisplayName: String,
129+
sourceResourceName: String
130+
): String {
131+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
132+
throw UnsupportedOperationException()
133+
}
134+
135+
class ResolverFailedException(msg: String) : RuntimeException(msg)
136+
137+
val resolver = context.contentResolver
138+
val collection = AudioStore.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
139+
140+
// Based on: https://developer.android.com/training/data-storage/shared/media#add-item
141+
val url = resolver.insert(collection, ContentValues().apply {
142+
put(AudioStore.DISPLAY_NAME, targetFileDisplayName)
143+
put(AudioStore.RELATIVE_PATH, notificationSoundsDirectoryPath)
144+
put(AudioStore.IS_NOTIFICATION, 1)
145+
put(AudioStore.IS_PENDING, 1)
146+
}) ?: throw ResolverFailedException("resolver.insert failed")
147+
148+
(resolver.openOutputStream(url, "wt")
149+
?: throw ResolverFailedException("resolver.open… failed"))
150+
.use { outputStream ->
151+
val resourceId = context.resources.getIdentifier(
152+
sourceResourceName, "raw", context.packageName)
153+
context.resources.openRawResource(resourceId)
154+
.use { it.copyTo(outputStream) }
155+
}
156+
157+
resolver.update(
158+
url, ContentValues().apply { put(AudioStore.IS_PENDING, 0) },
159+
null, null)
160+
161+
return url.toString()
162+
}
163+
123164
@SuppressLint(
124165
// If permission is missing, `notify` will throw an exception.
125166
// Which hopefully will propagate to Dart, and then it's up to Dart code to handle it.

lib/host/android_notifications.g.dart

+41
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,47 @@ class AndroidNotificationHostApi {
571571
}
572572
}
573573

574+
/// Wraps `android.content.ContentResolver.insert` combined with
575+
/// `android.content.ContentResolver.openOutputStream` and
576+
/// `android.content.res.Resources.openRawResource`.
577+
///
578+
/// Copies a raw resource audio file to `Notifications/Zulip/`
579+
/// directory in device's shared media storage. Returns the uri
580+
/// of the target file in media store.
581+
///
582+
/// Requires minimum of Android 10 (API 29) or higher.
583+
///
584+
/// See:
585+
/// https://developer.android.com/reference/android/content/ContentResolver#insert(android.net.Uri,%20android.content.ContentValues)
586+
/// https://developer.android.com/reference/android/content/ContentResolver#openOutputStream(android.net.Uri)
587+
/// https://developer.android.com/reference/android/content/res/Resources#openRawResource(int)
588+
Future<String> copySoundResourceToMediaStore({required String targetFileDisplayName, required String sourceResourceName}) async {
589+
final String __pigeon_channelName = 'dev.flutter.pigeon.zulip.AndroidNotificationHostApi.copySoundResourceToMediaStore$__pigeon_messageChannelSuffix';
590+
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
591+
__pigeon_channelName,
592+
pigeonChannelCodec,
593+
binaryMessenger: __pigeon_binaryMessenger,
594+
);
595+
final List<Object?>? __pigeon_replyList =
596+
await __pigeon_channel.send(<Object?>[targetFileDisplayName, sourceResourceName]) as List<Object?>?;
597+
if (__pigeon_replyList == null) {
598+
throw _createConnectionError(__pigeon_channelName);
599+
} else if (__pigeon_replyList.length > 1) {
600+
throw PlatformException(
601+
code: __pigeon_replyList[0]! as String,
602+
message: __pigeon_replyList[1] as String?,
603+
details: __pigeon_replyList[2],
604+
);
605+
} else if (__pigeon_replyList[0] == null) {
606+
throw PlatformException(
607+
code: 'null-error',
608+
message: 'Host platform returned null value for non-null return value.',
609+
);
610+
} else {
611+
return (__pigeon_replyList[0] as String?)!;
612+
}
613+
}
614+
574615
/// Corresponds to `android.app.NotificationManager.notify`,
575616
/// combined with `androidx.core.app.NotificationCompat.Builder`.
576617
///

pigeon/notifications.dart

+16
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,22 @@ abstract class AndroidNotificationHostApi {
208208
/// 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)
209209
List<StoredNotificationsSound> listStoredSoundsInNotificationsDirectory();
210210

211+
/// Wraps `android.content.ContentResolver.insert` combined with
212+
/// `android.content.ContentResolver.openOutputStream` and
213+
/// `android.content.res.Resources.openRawResource`.
214+
///
215+
/// Copies a raw resource audio file to `Notifications/Zulip/`
216+
/// directory in device's shared media storage. Returns the uri
217+
/// of the target file in media store.
218+
///
219+
/// Requires minimum of Android 10 (API 29) or higher.
220+
///
221+
/// See:
222+
/// https://developer.android.com/reference/android/content/ContentResolver#insert(android.net.Uri,%20android.content.ContentValues)
223+
/// https://developer.android.com/reference/android/content/ContentResolver#openOutputStream(android.net.Uri)
224+
/// https://developer.android.com/reference/android/content/res/Resources#openRawResource(int)
225+
String copySoundResourceToMediaStore({required String targetFileDisplayName, required String sourceResourceName});
226+
211227
/// Corresponds to `android.app.NotificationManager.notify`,
212228
/// combined with `androidx.core.app.NotificationCompat.Builder`.
213229
///

test/model/binding.dart

+39
Original file line numberDiff line numberDiff line change
@@ -549,8 +549,14 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi {
549549
_activeChannels.remove(channelId);
550550
}
551551

552+
/// Generates a fake uri for a notification sound present in media store.
553+
String fakeStoredNotificationSoundUri(String resourceName) {
554+
return 'content://media/external_primary/audio/media/$resourceName';
555+
}
556+
552557
final _storedNotificationSounds = <StoredNotificationsSound>[];
553558

559+
/// Populates the media store with the provided entries.
554560
void setupStoredNotificationSounds(List<StoredNotificationsSound> sounds) {
555561
_storedNotificationSounds.addAll(sounds);
556562
}
@@ -560,6 +566,34 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi {
560566
return _storedNotificationSounds.toList(growable: false);
561567
}
562568

569+
/// Consume the log of calls made to [copySoundResourceToMediaStore].
570+
///
571+
/// This returns a list of the arguments to all calls made
572+
/// to [copySoundResourceToMediaStore] since the last call to this method.
573+
List<CopySoundResourceToMediaStoreCall> takeCopySoundResourceToMediaStoreCalls() {
574+
final result = _copySoundResourceToMediaStoreCalls;
575+
_copySoundResourceToMediaStoreCalls = [];
576+
return result;
577+
}
578+
List<CopySoundResourceToMediaStoreCall> _copySoundResourceToMediaStoreCalls = [];
579+
580+
@override
581+
Future<String> copySoundResourceToMediaStore({
582+
required String targetFileDisplayName,
583+
required String sourceResourceName,
584+
}) async {
585+
_copySoundResourceToMediaStoreCalls.add((
586+
targetFileDisplayName: targetFileDisplayName,
587+
sourceResourceName: sourceResourceName));
588+
589+
final uri = fakeStoredNotificationSoundUri(sourceResourceName);
590+
_storedNotificationSounds.add(StoredNotificationsSound(
591+
fileName: targetFileDisplayName,
592+
isOwner: true,
593+
uri: uri));
594+
return uri;
595+
}
596+
563597
/// Consume the log of calls made to [notify].
564598
///
565599
/// This returns a list of the arguments to all calls made
@@ -682,3 +716,8 @@ typedef AndroidNotificationHostApiNotifyCall = ({
682716
int? number,
683717
String? smallIconResourceName,
684718
});
719+
720+
typedef CopySoundResourceToMediaStoreCall = ({
721+
String targetFileDisplayName,
722+
String sourceResourceName,
723+
});

0 commit comments

Comments
 (0)