Skip to content

Commit b8c2dde

Browse files
committed
fix(accesskey): use MediaStore for saving recovery key bitmap
Replace direct FileOutputStream write to external storage with MediaStore ContentResolver API, fixing EACCES on Android 10+. Remove WRITE_EXTERNAL_STORAGE permission and requestLegacyExternalStorage flag. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent a28aa4c commit b8c2dde

7 files changed

Lines changed: 53 additions & 52 deletions

File tree

apps/flipcash/app/src/main/AndroidManifest.xml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
1010
<uses-permission android:name="android.permission.CAMERA" />
1111

12-
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
13-
1412
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
1513
<!-- needed for network connectivity detection in Api24NetworkObserver -->
1614
<uses-permission
@@ -39,7 +37,6 @@
3937
android:label="@string/app_name"
4038
android:largeHeap="true"
4139
android:enableOnBackInvokedCallback="true"
42-
android:requestLegacyExternalStorage="true"
4340
android:roundIcon="@mipmap/ic_launcher_round"
4441
android:theme="@style/Theme.Code"
4542
tools:targetApi="tiramisu">
Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,45 @@
11
package com.flipcash.app.core.internal.extensions
22

3+
import android.content.ContentResolver
4+
import android.content.ContentValues
35
import android.graphics.Bitmap
6+
import android.os.Environment
7+
import android.provider.MediaStore
48
import com.getcode.utils.ErrorUtils
59
import com.getcode.utils.timedTrace
6-
import java.io.File
7-
import java.io.FileOutputStream
810

9-
fun Bitmap.save(destination: File, name: () -> String): Boolean {
11+
fun Bitmap.save(contentResolver: ContentResolver, name: () -> String): Boolean {
1012
val filename = name()
11-
if (!destination.exists()) {
12-
if (!destination.mkdirs()) {
13-
return false
14-
}
15-
}
16-
val dest = File(destination, filename)
1713

1814
return timedTrace("saving bitmap") {
1915
try {
20-
FileOutputStream(dest).use { out ->
16+
val contentValues = ContentValues().apply {
17+
put(MediaStore.Images.Media.DISPLAY_NAME, filename)
18+
put(MediaStore.Images.Media.MIME_TYPE, "image/png")
19+
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
20+
put(MediaStore.Images.Media.IS_PENDING, 1)
21+
}
22+
23+
val uri = contentResolver.insert(
24+
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
25+
contentValues
26+
) ?: return@timedTrace false
27+
28+
contentResolver.openOutputStream(uri)?.use { out ->
2129
compress(Bitmap.CompressFormat.PNG, 90, out)
30+
} ?: run {
31+
contentResolver.delete(uri, null, null)
32+
return@timedTrace false
2233
}
34+
35+
contentValues.clear()
36+
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
37+
contentResolver.update(uri, contentValues, null, null)
38+
39+
return@timedTrace true
2340
} catch (e: Exception) {
2441
ErrorUtils.handleError(e)
2542
return@timedTrace false
2643
}
27-
return@timedTrace true
2844
}
2945
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.flipcash.app.core.storage
2+
3+
import android.content.Context
4+
import android.graphics.Bitmap
5+
import com.flipcash.app.core.internal.extensions.save
6+
import dagger.hilt.android.qualifiers.ApplicationContext
7+
import javax.inject.Inject
8+
9+
class MediaSaver @Inject constructor(
10+
@ApplicationContext private val context: Context
11+
) {
12+
fun saveBitmap(bitmap: Bitmap, filename: String): Boolean {
13+
return bitmap.save(context.contentResolver) { filename }
14+
}
15+
}

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/storage/MediaScanner.kt

Lines changed: 0 additions & 15 deletions
This file was deleted.

apps/flipcash/features/backupkey/src/main/kotlin/com/flipcash/app/backupkey/internal/BackupKeyScreenViewModel.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.flipcash.app.backupkey.internal
22

33
import com.flipcash.app.accesskey.BaseAccessKeyViewModel
4-
import com.flipcash.app.core.storage.MediaScanner
4+
import com.flipcash.app.core.storage.MediaSaver
55
import com.flipcash.services.user.UserManager
66
import com.getcode.libs.qr.QRCodeGenerator
77
import com.getcode.opencode.managers.MnemonicManager
@@ -17,13 +17,13 @@ import kotlin.time.Duration.Companion.seconds
1717
internal class BackupKeyScreenViewModel @Inject constructor(
1818
resources: ResourceHelper,
1919
mnemonicManager: MnemonicManager,
20-
mediaScanner: MediaScanner,
20+
mediaSaver: MediaSaver,
2121
userManager: UserManager,
2222
qrCodeGenerator: QRCodeGenerator,
2323
) : BaseAccessKeyViewModel(
2424
resources,
2525
mnemonicManager,
26-
mediaScanner,
26+
mediaSaver,
2727
userManager,
2828
qrCodeGenerator
2929
) {

apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/accesskey/LoginAccessKeyViewModel.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import com.flipcash.app.analytics.Action
55
import com.flipcash.app.analytics.Button
66
import com.flipcash.app.analytics.FlipcashAnalyticsService
77
import com.flipcash.app.auth.AuthManager
8-
import com.flipcash.app.core.storage.MediaScanner
8+
import com.flipcash.app.core.storage.MediaSaver
99
import com.flipcash.app.userflags.UserFlagsCoordinator
1010
import com.flipcash.services.user.UserManager
1111
import com.getcode.libs.qr.QRCodeGenerator
@@ -22,12 +22,12 @@ class LoginAccessKeyViewModel @Inject constructor(
2222
resources: ResourceHelper,
2323
mnemonicManager: MnemonicManager,
2424
qrCodeGenerator: QRCodeGenerator,
25-
mediaScanner: MediaScanner,
25+
mediaSaver: MediaSaver,
2626
userManager: UserManager,
2727
private val userFlags: UserFlagsCoordinator,
2828
private val authManager: AuthManager,
2929
private val analytics: FlipcashAnalyticsService,
30-
): BaseAccessKeyViewModel(resources, mnemonicManager, mediaScanner, userManager, qrCodeGenerator) {
30+
): BaseAccessKeyViewModel(resources, mnemonicManager, mediaSaver, userManager, qrCodeGenerator) {
3131

3232
suspend fun onWroteDownInstead(): Result<Boolean> {
3333
trackButton(Button.WroteAccessKey)

apps/flipcash/shared/accesskey/src/main/kotlin/com/flipcash/app/accesskey/BaseAccessKeyViewModel.kt

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@ import android.graphics.Path
88
import android.graphics.RectF
99
import android.graphics.Shader
1010
import android.graphics.Typeface
11-
import android.os.Environment
1211
import androidx.core.graphics.applyCanvas
1312
import androidx.core.graphics.createBitmap
1413
import androidx.core.graphics.drawable.toBitmap
1514
import androidx.lifecycle.viewModelScope
16-
import com.flipcash.app.core.internal.extensions.save
17-
import com.flipcash.app.core.storage.MediaScanner
15+
import com.flipcash.app.core.storage.MediaSaver
1816
import com.flipcash.app.theme.internal.Flipcash2ColorSpec
1917
import com.flipcash.services.user.UserManager
2018
import com.flipcash.shared.accesskey.R
@@ -63,7 +61,7 @@ data class AccessKeyUiModel(
6361
abstract class BaseAccessKeyViewModel(
6462
private val resources: ResourceHelper,
6563
private val mnemonicManager: MnemonicManager,
66-
private val mediaScanner: MediaScanner,
64+
private val mediaSaver: MediaSaver,
6765
userManager: UserManager,
6866
private val qrCodeGenerator: QRCodeGenerator
6967
) : ViewModel() {
@@ -129,22 +127,12 @@ abstract class BaseAccessKeyViewModel(
129127
uiFlow.update { it.copy(exportState = LoadingSuccessState(loading = true)) }
130128
val bitmap = uiFlow.value.accessKeyBitmap
131129
?: return Result.failure(IllegalStateException("No access key?"))
132-
val destination =
133-
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
134130

135131
return withContext(Dispatchers.IO) {
136132
runCatching {
137-
val result = bitmap.save(
138-
destination = destination,
139-
name = {
140-
val date: DateFormat = SimpleDateFormat("yyy-MM-dd-h-mm", Locale.CANADA)
141-
"Flipcash-Recovery-${date.format(Date())}.png"
142-
}
143-
)
144-
if (result) {
145-
mediaScanner.scan(destination)
146-
}
147-
result
133+
val date: DateFormat = SimpleDateFormat("yyy-MM-dd-h-mm", Locale.CANADA)
134+
val filename = "Flipcash-Recovery-${date.format(Date())}.png"
135+
mediaSaver.saveBitmap(bitmap, filename)
148136
}
149137
}.onFailure {
150138
getAccessKeySaveError()

0 commit comments

Comments
 (0)