1+ package com.darkrockstudios.app.securecamera.share
2+
3+ import android.content.ContentProvider
4+ import android.content.ContentValues
5+ import android.database.Cursor
6+ import android.database.MatrixCursor
7+ import android.graphics.Bitmap.CompressFormat
8+ import android.graphics.BitmapFactory
9+ import android.net.Uri
10+ import android.os.Handler
11+ import android.os.Looper
12+ import android.os.ParcelFileDescriptor
13+ import android.os.ProxyFileDescriptorCallback
14+ import android.os.storage.StorageManager
15+ import android.provider.OpenableColumns
16+ import com.darkrockstudios.app.securecamera.camera.PhotoDef
17+ import com.darkrockstudios.app.securecamera.camera.SecureImageManager
18+ import com.darkrockstudios.app.securecamera.preferences.AppPreferencesManager
19+ import kotlinx.coroutines.flow.first
20+ import kotlinx.coroutines.runBlocking
21+ import org.koin.core.component.KoinComponent
22+ import org.koin.core.component.inject
23+ import java.io.ByteArrayOutputStream
24+ import kotlin.uuid.ExperimentalUuidApi
25+ import kotlin.uuid.Uuid
26+
27+
28+ /* *
29+ * A ContentProvider that decrypts and streams images on-demand without writing decrypted data to disk.
30+ * This provider handles URIs in the format:
31+ * content://com.darkrockstudios.app.securecamera.decryptingprovider/photos/[photo_name]
32+ */
33+ class DecryptingImageProvider : ContentProvider (), KoinComponent {
34+
35+ private val imageManager: SecureImageManager by inject()
36+ private val preferencesManager: AppPreferencesManager by inject()
37+
38+ @OptIn(ExperimentalUuidApi ::class )
39+ private val uuid = Uuid .random()
40+
41+ override fun onCreate (): Boolean {
42+ return true
43+ }
44+
45+ override fun getStreamTypes (uri : Uri , mimeTypeFilter : String ): Array <String > {
46+ return arrayOf(MIME_TYPE )
47+ }
48+
49+ override fun openFile (uri : Uri , mode : String ): ParcelFileDescriptor ? {
50+ if (mode != " r" ) return null
51+
52+ val segments = uri.pathSegments
53+ 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() }
57+
58+ val ropc = ReadOnlyPhotoCallback (photoDef, sanitizeMetadata, imageManager)
59+ val storage = context!! .getSystemService(StorageManager ::class .java)
60+ return storage.openProxyFileDescriptor(
61+ ParcelFileDescriptor .MODE_READ_ONLY ,
62+ ropc,
63+ Handler (Looper .getMainLooper())
64+ )
65+ }
66+
67+ /* *
68+ * Handles the query method to provide additional metadata about the file
69+ * This allows us to set a sanitized filename when the file is shared
70+ */
71+ @OptIn(ExperimentalStdlibApi ::class )
72+ override fun query (
73+ uri : Uri ,
74+ projection : Array <out String >? ,
75+ selection : String? ,
76+ selectionArgs : Array <out String >? ,
77+ sortOrder : String?
78+ ): Cursor ? {
79+ val segments = uri.pathSegments
80+ if (segments.size < 2 ) return null
81+
82+ val photoName = segments.last()
83+ val photoDef = imageManager.getPhotoByName(photoName) ? : return null
84+
85+ val sanitizeName = runBlocking { preferencesManager.sanitizeFileName.first() }
86+ val sanitizeMetadata = runBlocking { preferencesManager.sanitizeMetadata.first() }
87+
88+ val size = runBlocking {
89+ if (sanitizeMetadata)
90+ stripMetadataInMemory(imageManager.decryptJpg(photoDef)).size
91+ else
92+ imageManager.decryptJpg(photoDef).size
93+ }
94+
95+ val columnNames = projection ? : arrayOf(OpenableColumns .DISPLAY_NAME , OpenableColumns .SIZE )
96+ val row = arrayOfNulls<Any >(columnNames.size)
97+ for (i in columnNames.indices) {
98+ when (columnNames[i]) {
99+ OpenableColumns .DISPLAY_NAME -> {
100+ row[i] = getFileName(photoDef, sanitizeName)
101+ }
102+
103+ OpenableColumns .SIZE -> {
104+ row[i] = size
105+ }
106+ }
107+ }
108+
109+ val cursor = MatrixCursor (columnNames, 1 )
110+ cursor.addRow(row)
111+ return cursor
112+ }
113+
114+ override fun getType (uri : Uri ): String = MIME_TYPE
115+
116+ override fun insert (uri : Uri , values : ContentValues ? ): Uri ? = error(" insert Unsupported" )
117+ override fun delete (uri : Uri , selection : String? , selectionArgs : Array <out String >? ): Int =
118+ error(" delete Unsupported" )
119+
120+ override fun update (uri : Uri , values : ContentValues ? , selection : String? , selectionArgs : Array <out String >? ): Int =
121+ error(" update Unsupported" )
122+
123+ @OptIn(ExperimentalUuidApi ::class )
124+ private fun getFileName (photoDef : PhotoDef , sanitizeName : Boolean ): String {
125+ return if (sanitizeName) {
126+ " image_" + uuid.toHexString() + " .jpg"
127+
128+ } else {
129+ photoDef.photoName
130+ }
131+ }
132+
133+ companion object {
134+ private const val MIME_TYPE = " image/jpeg"
135+ }
136+ }
137+
138+ private class ReadOnlyPhotoCallback (
139+ private val photoDef : PhotoDef ,
140+ private val sanitizeMetadata : Boolean ,
141+ private val imageManager : SecureImageManager ,
142+ ) : ProxyFileDescriptorCallback() {
143+
144+ private val decryptedBytes: ByteArray by lazy {
145+ if (sanitizeMetadata) {
146+ val bytes = runBlocking { imageManager.decryptJpg(photoDef) }
147+ stripMetadataInMemory(bytes)
148+ } else {
149+ runBlocking { imageManager.decryptJpg(photoDef) }
150+ }
151+ }
152+
153+ override fun onGetSize (): Long = decryptedBytes.size.toLong()
154+
155+ override fun onRead (offset : Long , size : Int , data : ByteArray ): Int {
156+ if (offset >= decryptedBytes.size) return 0
157+ val actually = minOf(size, decryptedBytes.size - offset.toInt(), data.size)
158+ System .arraycopy(decryptedBytes, offset.toInt(), data, 0 , actually)
159+ return actually
160+ }
161+
162+ override fun onRelease () {
163+ decryptedBytes.fill(0 )
164+ }
165+ }
166+
167+ private fun stripMetadataInMemory (imageData : ByteArray ): ByteArray {
168+ val bitmap = BitmapFactory .decodeByteArray(imageData, 0 , imageData.size)
169+ return if (bitmap != null ) {
170+ ByteArrayOutputStream ().use { outputStream ->
171+ bitmap.compress(CompressFormat .JPEG , 90 , outputStream)
172+ outputStream.toByteArray()
173+ }
174+ } else {
175+ error(" Failed to strip metadata in memory" )
176+ }
177+ }
0 commit comments