Skip to content

Commit 5a8f4bb

Browse files
rajveermalviyagnprice
authored andcommitted
pigeon: Add helper binding for copySoundResourceToMediaStore
1 parent 2348fe0 commit 5a8f4bb

File tree

5 files changed

+179
-7
lines changed

5 files changed

+179
-7
lines changed

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,23 @@ interface AndroidNotificationHostApi {
522522
* 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)
523523
*/
524524
fun listStoredSoundsInNotificationsDirectory(): List<StoredNotificationSound>
525+
/**
526+
* Wraps `android.content.ContentResolver.insert` combined with
527+
* `android.content.ContentResolver.openOutputStream` and
528+
* `android.content.res.Resources.openRawResource`.
529+
*
530+
* Copies a raw resource audio file to `Notifications/Zulip/`
531+
* directory in device's shared media storage. Returns the URL
532+
* of the target file in media store.
533+
*
534+
* Requires minimum of Android 10 (API 29) or higher.
535+
*
536+
* See:
537+
* https://developer.android.com/reference/android/content/ContentResolver#insert(android.net.Uri,%20android.content.ContentValues)
538+
* https://developer.android.com/reference/android/content/ContentResolver#openOutputStream(android.net.Uri)
539+
* https://developer.android.com/reference/android/content/res/Resources#openRawResource(int)
540+
*/
541+
fun copySoundResourceToMediaStore(targetFileDisplayName: String, sourceResourceName: String): String
525542
/**
526543
* Corresponds to `android.app.NotificationManager.notify`,
527544
* combined with `androidx.core.app.NotificationCompat.Builder`.
@@ -649,6 +666,24 @@ interface AndroidNotificationHostApi {
649666
channel.setMessageHandler(null)
650667
}
651668
}
669+
run {
670+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.copySoundResourceToMediaStore$separatedMessageChannelSuffix", codec)
671+
if (api != null) {
672+
channel.setMessageHandler { message, reply ->
673+
val args = message as List<Any?>
674+
val targetFileDisplayNameArg = args[0] as String
675+
val sourceResourceNameArg = args[1] as String
676+
val wrapped: List<Any?> = try {
677+
listOf(api.copySoundResourceToMediaStore(targetFileDisplayNameArg, sourceResourceNameArg))
678+
} catch (exception: Throwable) {
679+
wrapError(exception)
680+
}
681+
reply.reply(wrapped)
682+
}
683+
} else {
684+
channel.setMessageHandler(null)
685+
}
686+
}
652687
run {
653688
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.notify$separatedMessageChannelSuffix", codec)
654689
if (api != null) {

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

Lines changed: 48 additions & 7 deletions
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,13 @@ 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+
val notificationSoundsDirectoryPath = "${Environment.DIRECTORY_NOTIFICATIONS}/Zulip/"
57+
58+
class ResolverFailedException(msg: String) : RuntimeException(msg)
59+
5260
override fun createNotificationChannel(channel: NotificationChannel) {
5361
val notificationChannel = NotificationChannelCompat
5462
.Builder(channel.id, channel.importance.toInt()).apply {
@@ -79,13 +87,6 @@ private class AndroidNotificationHost(val context: Context)
7987
throw UnsupportedOperationException()
8088
}
8189

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

126+
@SuppressLint(
127+
// For `getIdentifier`. TODO make a cleaner API.
128+
"DiscouragedApi")
129+
override fun copySoundResourceToMediaStore(
130+
targetFileDisplayName: String,
131+
sourceResourceName: String
132+
): String {
133+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
134+
throw UnsupportedOperationException()
135+
}
136+
137+
class ResolverFailedException(msg: String) : RuntimeException(msg)
138+
139+
val resolver = context.contentResolver
140+
val collection = AudioStore.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
141+
142+
// Based on: https://developer.android.com/training/data-storage/shared/media#add-item
143+
val url = resolver.insert(collection, ContentValues().apply {
144+
put(AudioStore.DISPLAY_NAME, targetFileDisplayName)
145+
put(AudioStore.RELATIVE_PATH, notificationSoundsDirectoryPath)
146+
put(AudioStore.IS_NOTIFICATION, 1)
147+
put(AudioStore.IS_PENDING, 1)
148+
}) ?: throw ResolverFailedException("resolver.insert failed")
149+
150+
(resolver.openOutputStream(url, "wt")
151+
?: throw ResolverFailedException("resolver.open… failed"))
152+
.use { outputStream ->
153+
val resourceId = context.resources.getIdentifier(
154+
sourceResourceName, "raw", context.packageName)
155+
context.resources.openRawResource(resourceId)
156+
.use { it.copyTo(outputStream) }
157+
}
158+
159+
resolver.update(
160+
url, ContentValues().apply { put(AudioStore.IS_PENDING, 0) },
161+
null, null)
162+
163+
return url.toString()
164+
}
165+
125166
@SuppressLint(
126167
// If permission is missing, `notify` will throw an exception.
127168
// 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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,47 @@ class AndroidNotificationHostApi {
579579
}
580580
}
581581

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

pigeon/notifications.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,22 @@ abstract class AndroidNotificationHostApi {
218218
/// 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)
219219
List<StoredNotificationSound> listStoredSoundsInNotificationsDirectory();
220220

221+
/// Wraps `android.content.ContentResolver.insert` combined with
222+
/// `android.content.ContentResolver.openOutputStream` and
223+
/// `android.content.res.Resources.openRawResource`.
224+
///
225+
/// Copies a raw resource audio file to `Notifications/Zulip/`
226+
/// directory in device's shared media storage. Returns the URL
227+
/// of the target file in media store.
228+
///
229+
/// Requires minimum of Android 10 (API 29) or higher.
230+
///
231+
/// See:
232+
/// https://developer.android.com/reference/android/content/ContentResolver#insert(android.net.Uri,%20android.content.ContentValues)
233+
/// https://developer.android.com/reference/android/content/ContentResolver#openOutputStream(android.net.Uri)
234+
/// https://developer.android.com/reference/android/content/res/Resources#openRawResource(int)
235+
String copySoundResourceToMediaStore({required String targetFileDisplayName, required String sourceResourceName});
236+
221237
/// Corresponds to `android.app.NotificationManager.notify`,
222238
/// combined with `androidx.core.app.NotificationCompat.Builder`.
223239
///

test/model/binding.dart

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

556+
/// A URL that the fake [copySoundResourceToMediaStore] would produce
557+
/// for a resource with the given name.
558+
String fakeStoredNotificationSoundUrl(String resourceName) {
559+
return 'content://media/external_primary/audio/media/$resourceName';
560+
}
561+
556562
final _storedNotificationSounds = <StoredNotificationSound>[];
557563

558564
/// Populates the media store with the provided entries.
@@ -565,6 +571,34 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi {
565571
return _storedNotificationSounds.toList(growable: false);
566572
}
567573

574+
/// Consume the log of calls made to [copySoundResourceToMediaStore].
575+
///
576+
/// This returns a list of the arguments to all calls made
577+
/// to [copySoundResourceToMediaStore] since the last call to this method.
578+
List<CopySoundResourceToMediaStoreCall> takeCopySoundResourceToMediaStoreCalls() {
579+
final result = _copySoundResourceToMediaStoreCalls;
580+
_copySoundResourceToMediaStoreCalls = [];
581+
return result;
582+
}
583+
List<CopySoundResourceToMediaStoreCall> _copySoundResourceToMediaStoreCalls = [];
584+
585+
@override
586+
Future<String> copySoundResourceToMediaStore({
587+
required String targetFileDisplayName,
588+
required String sourceResourceName,
589+
}) async {
590+
_copySoundResourceToMediaStoreCalls.add((
591+
targetFileDisplayName: targetFileDisplayName,
592+
sourceResourceName: sourceResourceName));
593+
594+
final url = fakeStoredNotificationSoundUrl(sourceResourceName);
595+
_storedNotificationSounds.add(StoredNotificationSound(
596+
fileName: targetFileDisplayName,
597+
isOwned: true,
598+
contentUrl: url));
599+
return url;
600+
}
601+
568602
/// Consume the log of calls made to [notify].
569603
///
570604
/// This returns a list of the arguments to all calls made
@@ -687,3 +721,8 @@ typedef AndroidNotificationHostApiNotifyCall = ({
687721
int? number,
688722
String? smallIconResourceName,
689723
});
724+
725+
typedef CopySoundResourceToMediaStoreCall = ({
726+
String targetFileDisplayName,
727+
String sourceResourceName,
728+
});

0 commit comments

Comments
 (0)