@@ -15,7 +15,9 @@ import android.os.storage.StorageManager
1515import android.provider.OpenableColumns
1616import com.darkrockstudios.app.securecamera.camera.PhotoDef
1717import com.darkrockstudios.app.securecamera.camera.SecureImageRepository
18+ import com.darkrockstudios.app.securecamera.camera.VideoDef
1819import com.darkrockstudios.app.securecamera.preferences.AppSettingsDataSource
20+ import com.darkrockstudios.app.securecamera.security.streaming.StreamingDecryptor
1921import kotlinx.coroutines.flow.first
2022import kotlinx.coroutines.runBlocking
2123import org.koin.core.component.KoinComponent
@@ -26,11 +28,12 @@ import kotlin.uuid.Uuid
2628
2729
2830/* *
29- * A ContentProvider that decrypts and streams images on-demand without writing decrypted data to disk.
31+ * A ContentProvider that decrypts and streams photos and videos on-demand without writing decrypted data to disk.
3032 * This provider handles URIs in the format:
31- * content://com.darkrockstudios.app.securecamera.decryptingprovider/photos/[photo_name]
33+ * - content://com.darkrockstudios.app.securecamera.decryptingprovider/photos/[photo_name]
34+ * - content://com.darkrockstudios.app.securecamera.decryptingprovider/videos/[video_name]
3235 */
33- class DecryptingImageProvider : ContentProvider (), KoinComponent {
36+ class DecryptingMediaProvider : ContentProvider (), KoinComponent {
3437
3538 private val imageManager: SecureImageRepository by inject()
3639 private val preferencesManager: AppSettingsDataSource by inject()
@@ -42,26 +45,55 @@ class DecryptingImageProvider : ContentProvider(), KoinComponent {
4245 return true
4346 }
4447
48+ companion object {
49+ const val PATH_PHOTOS = " photos"
50+ const val PATH_VIDEOS = " videos"
51+ }
52+
4553 override fun getStreamTypes (uri : Uri , mimeTypeFilter : String ): Array <String > {
46- return arrayOf(MIME_TYPE )
54+ val segments = uri.pathSegments
55+ if (segments.size < 2 ) return emptyArray()
56+
57+ return when (segments[0 ]) {
58+ PATH_PHOTOS -> arrayOf(" image/jpeg" )
59+ PATH_VIDEOS -> arrayOf(" video/mp4" )
60+ else -> emptyArray()
61+ }
4762 }
4863
4964 override fun openFile (uri : Uri , mode : String ): ParcelFileDescriptor ? {
5065 if (mode != " r" ) return null
5166
5267 val segments = uri.pathSegments
5368 if (segments.size < 2 ) return null
54- val photoName = segments.last()
55- val photoDef = imageManager.getPhotoByName(photoName) ? : return null
56- val sanitizeMetadata = runBlocking { preferencesManager.sanitizeMetadata.first() }
5769
58- val ropc = ReadOnlyPhotoCallback (photoDef, sanitizeMetadata, imageManager)
70+ val mediaType = segments[0 ] // "photos" or "videos"
71+ val mediaName = segments.last()
72+
5973 val storage = context!! .getSystemService(StorageManager ::class .java)
60- return storage.openProxyFileDescriptor(
61- ParcelFileDescriptor .MODE_READ_ONLY ,
62- ropc,
63- Handler (Looper .getMainLooper())
64- )
74+
75+ return when (mediaType) {
76+ PATH_PHOTOS -> {
77+ val photoDef = imageManager.getPhotoByName(mediaName) ? : return null
78+ val sanitizeMetadata = runBlocking { preferencesManager.sanitizeMetadata.first() }
79+ val ropc = ReadOnlyPhotoCallback (photoDef, sanitizeMetadata, imageManager)
80+ storage.openProxyFileDescriptor(
81+ ParcelFileDescriptor .MODE_READ_ONLY ,
82+ ropc,
83+ Handler (Looper .getMainLooper())
84+ )
85+ }
86+ PATH_VIDEOS -> {
87+ val videoDef = imageManager.getVideoByName(mediaName) ? : return null
88+ val rovc = ReadOnlyVideoCallback (videoDef, imageManager)
89+ storage.openProxyFileDescriptor(
90+ ParcelFileDescriptor .MODE_READ_ONLY ,
91+ rovc,
92+ Handler (Looper .getMainLooper())
93+ )
94+ }
95+ else -> null
96+ }
6597 }
6698
6799 /* *
@@ -79,25 +111,43 @@ class DecryptingImageProvider : ContentProvider(), KoinComponent {
79111 val segments = uri.pathSegments
80112 if (segments.size < 2 ) return null
81113
82- val photoName = segments.last()
83- val photoDef = imageManager.getPhotoByName(photoName) ? : return null
114+ val mediaType = segments[ 0 ]
115+ val mediaName = segments.last()
84116
85117 val sanitizeName = runBlocking { preferencesManager.sanitizeFileName.first() }
86- val sanitizeMetadata = runBlocking { preferencesManager.sanitizeMetadata.first() }
87118
88- val size = runBlocking {
89- if (sanitizeMetadata)
90- stripMetadataInMemory(imageManager.decryptJpg(photoDef)).size
91- else
92- imageManager.decryptJpg(photoDef).size
119+ val (displayName, size) = when (mediaType) {
120+ PATH_PHOTOS -> {
121+ val photoDef = imageManager.getPhotoByName(mediaName) ? : return null
122+ val sanitizeMetadata = runBlocking { preferencesManager.sanitizeMetadata.first() }
123+ val photoSize = runBlocking {
124+ if (sanitizeMetadata)
125+ stripMetadataInMemory(imageManager.decryptJpg(photoDef)).size
126+ else
127+ imageManager.decryptJpg(photoDef).size
128+ }
129+ Pair (getPhotoFileName(photoDef, sanitizeName), photoSize)
130+ }
131+ PATH_VIDEOS -> {
132+ val videoDef = imageManager.getVideoByName(mediaName) ? : return null
133+ val videoSize = runBlocking {
134+ val scheme = imageManager.getStreamingEncryptionScheme()!!
135+ val decryptor = scheme.createStreamingDecryptor(videoDef.videoFile)
136+ val size = decryptor.totalSize
137+ decryptor.close()
138+ size
139+ }
140+ Pair (getVideoFileName(videoDef, sanitizeName), videoSize)
141+ }
142+ else -> return null
93143 }
94144
95145 val columnNames = projection ? : arrayOf(OpenableColumns .DISPLAY_NAME , OpenableColumns .SIZE )
96146 val row = arrayOfNulls<Any >(columnNames.size)
97147 for (i in columnNames.indices) {
98148 when (columnNames[i]) {
99149 OpenableColumns .DISPLAY_NAME -> {
100- row[i] = getFileName(photoDef, sanitizeName)
150+ row[i] = displayName
101151 }
102152
103153 OpenableColumns .SIZE -> {
@@ -111,7 +161,16 @@ class DecryptingImageProvider : ContentProvider(), KoinComponent {
111161 return cursor
112162 }
113163
114- override fun getType (uri : Uri ): String = MIME_TYPE
164+ override fun getType (uri : Uri ): String {
165+ val segments = uri.pathSegments
166+ if (segments.size < 2 ) return " application/octet-stream"
167+
168+ return when (segments[0 ]) {
169+ PATH_PHOTOS -> " image/jpeg"
170+ PATH_VIDEOS -> " video/mp4"
171+ else -> " application/octet-stream"
172+ }
173+ }
115174
116175 override fun insert (uri : Uri , values : ContentValues ? ): Uri ? = error(" insert Unsupported" )
117176 override fun delete (uri : Uri , selection : String? , selectionArgs : Array <out String >? ): Int =
@@ -121,17 +180,21 @@ class DecryptingImageProvider : ContentProvider(), KoinComponent {
121180 error(" update Unsupported" )
122181
123182 @OptIn(ExperimentalUuidApi ::class )
124- private fun getFileName (photoDef : PhotoDef , sanitizeName : Boolean ): String {
183+ private fun getPhotoFileName (photoDef : PhotoDef , sanitizeName : Boolean ): String {
125184 return if (sanitizeName) {
126185 " image_" + uuid.toHexString() + " .jpg"
127-
128186 } else {
129187 photoDef.photoName
130188 }
131189 }
132190
133- companion object {
134- private const val MIME_TYPE = " image/jpeg"
191+ @OptIn(ExperimentalUuidApi ::class )
192+ private fun getVideoFileName (videoDef : VideoDef , sanitizeName : Boolean ): String {
193+ return if (sanitizeName) {
194+ " video_" + uuid.toHexString() + " .mp4"
195+ } else {
196+ videoDef.videoName
197+ }
135198 }
136199}
137200
@@ -164,6 +227,30 @@ private class ReadOnlyPhotoCallback(
164227 }
165228}
166229
230+ private class ReadOnlyVideoCallback (
231+ private val videoDef : VideoDef ,
232+ private val imageManager : SecureImageRepository ,
233+ ) : ProxyFileDescriptorCallback() {
234+
235+ private val decryptor: StreamingDecryptor = runBlocking {
236+ val scheme = imageManager.getStreamingEncryptionScheme()!!
237+ scheme.createStreamingDecryptor(videoDef.videoFile)
238+ }
239+
240+ override fun onGetSize (): Long = decryptor.totalSize
241+
242+ override fun onRead (offset : Long , size : Int , data : ByteArray ): Int {
243+ if (offset >= decryptor.totalSize) return 0
244+ return runBlocking {
245+ decryptor.read(offset, data, 0 , size)
246+ }
247+ }
248+
249+ override fun onRelease () {
250+ decryptor.close()
251+ }
252+ }
253+
167254private fun stripMetadataInMemory (imageData : ByteArray ): ByteArray {
168255 val bitmap = BitmapFactory .decodeByteArray(imageData, 0 , imageData.size)
169256 return if (bitmap != null ) {
0 commit comments