@@ -2,6 +2,7 @@ package com.zulip.flutter
2
2
3
3
import android.annotation.SuppressLint
4
4
import android.content.ContentUris
5
+ import android.content.ContentValues
5
6
import android.content.Context
6
7
import android.content.Intent
7
8
import android.net.Uri
@@ -49,6 +50,11 @@ fun toPigeonPerson(person: androidx.core.app.Person): Person {
49
50
50
51
private class AndroidNotificationHost (val context : Context )
51
52
: 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
+
52
58
override fun createNotificationChannel (channel : NotificationChannel ) {
53
59
val notificationChannel = NotificationChannelCompat
54
60
.Builder (channel.id, channel.importance.toInt()).apply {
@@ -79,11 +85,6 @@ private class AndroidNotificationHost(val context: Context)
79
85
throw UnsupportedOperationException ()
80
86
}
81
87
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
88
// Query and cursor-loop based on:
88
89
// https://developer.android.com/training/data-storage/shared/media#query-collection
89
90
val collection = AudioStore .getContentUri(MediaStore .VOLUME_EXTERNAL_PRIMARY )
@@ -120,6 +121,46 @@ private class AndroidNotificationHost(val context: Context)
120
121
return sounds
121
122
}
122
123
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
+
123
164
@SuppressLint(
124
165
// If permission is missing, `notify` will throw an exception.
125
166
// Which hopefully will propagate to Dart, and then it's up to Dart code to handle it.
0 commit comments