Skip to content

Commit

Permalink
Add image in the shared storage instead of relying on a FileProvider (#…
Browse files Browse the repository at this point in the history
…142)

* Add image in the shared storage instead of relying on a FileProvider

* Add comment

* Apply Spotless

---------

Co-authored-by: yrezgui <[email protected]>
  • Loading branch information
yrezgui and yrezgui authored Aug 18, 2023
1 parent e461bf1 commit 2b2008e
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 43 deletions.
2 changes: 2 additions & 0 deletions compose/snippets/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ dependencies {
implementation(libs.accompanist.theme.adapter.material3)
implementation(libs.accompanist.theme.adapter.material)

implementation(libs.accompanist.permissions)

implementation(libs.coil.kt.compose)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
Expand Down
14 changes: 3 additions & 11 deletions compose/snippets/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
Expand Down Expand Up @@ -57,17 +60,6 @@
android:resource="@xml/my_app_widget_info" />
</receiver>
<!-- [END android_compose_glance_declare] -->


<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@

package com.example.compose.snippets.graphics

import android.R.attr.height
import android.R.attr.width
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.Intent.createChooser
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Picture
import android.graphics.drawable.PictureDrawable
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
Expand All @@ -38,6 +41,9 @@ import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
Expand All @@ -57,11 +63,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import androidx.core.content.FileProvider
import com.example.compose.snippets.R
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import java.io.File
import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine

/*
* Copyright 2022 The Android Open Source Project
Expand All @@ -78,27 +87,58 @@ import kotlinx.coroutines.launch
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Preview
@Composable
fun BitmapFromComposableSnippet() {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }

val picture = remember {
Picture()
}
Scaffold(floatingActionButton = {
FloatingActionButton(onClick = {
// TODO Move this logic to your ViewModel,
// Then trigger side effect with the result URI to share
coroutineScope.launch(Dispatchers.IO) {
val bitmap = createBitmapFromPicture(picture)
val uri = bitmap.saveToDisk(context)
shareBitmap(context, uri)

val writeStorageAccessState = rememberMultiplePermissionsState(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// No permissions are needed on Android 10+ to add files in the shared storage
emptyList()
} else {
listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
)

Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
floatingActionButton = {
FloatingActionButton(onClick = {
// TODO Move this logic to your ViewModel,
// Then trigger side effect with the result URI to share
if (writeStorageAccessState.allPermissionsGranted) {
coroutineScope.launch(Dispatchers.IO) {
val bitmap = createBitmapFromPicture(picture)
val uri = bitmap.saveToDisk(context)
shareBitmap(context, uri)
}
} else if (writeStorageAccessState.shouldShowRationale) {
coroutineScope.launch {
val result = snackbarHostState.showSnackbar(
message = "The storage permission is needed to save the image",
actionLabel = "Grant Access"
)

if (result == SnackbarResult.ActionPerformed) {
writeStorageAccessState.launchMultiplePermissionRequest()
}
}
} else {
writeStorageAccessState.launchMultiplePermissionRequest()
}
}) {
Icon(Icons.Default.Share, "share")
}
}) {
Icon(Icons.Default.Share, "share")
}
}) { padding ->
) { padding ->
// [START android_compose_draw_into_bitmap]
Column(
modifier = Modifier
Expand Down Expand Up @@ -163,31 +203,49 @@ private fun ScreenContentToCapture() {
}
}

suspend fun createBitmapFromPicture(picture: Picture): Bitmap {
fun createBitmapFromPicture(picture: Picture): Bitmap {
val pictureDrawable = PictureDrawable(picture)
val bitmap =
Bitmap.createBitmap(
pictureDrawable.intrinsicWidth,
pictureDrawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val bitmap = Bitmap.createBitmap(
pictureDrawable.intrinsicWidth,
pictureDrawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)

val canvas = Canvas(bitmap)
canvas.drawColor(android.graphics.Color.WHITE)
canvas.drawPicture(pictureDrawable.picture)
return bitmap
}

private fun Bitmap.saveToDisk(context: Context): Uri {
private suspend fun Bitmap.saveToDisk(context: Context): Uri {
val file = File(
context.getExternalFilesDir("external_files"),
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"screenshot-${System.currentTimeMillis()}.png"
)

file.writeBitmap(this, Bitmap.CompressFormat.PNG, 100)
return FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".provider",
file
)

return scanFilePath(context, file.path) ?: throw Exception("File could not be saved")
}

/**
* We call [MediaScannerConnection] to index the newly created image inside MediaStore to be visible
* for other apps, as well as returning its [MediaStore] Uri
*/
private suspend fun scanFilePath(context: Context, filePath: String): Uri? {
return suspendCancellableCoroutine { continuation ->
MediaScannerConnection.scanFile(
context,
arrayOf(filePath),
arrayOf("image/png")
) { _, scannedUri ->
if (scannedUri == null) {
continuation.cancel(Exception("File $filePath could not be scanned"))
} else {
continuation.resume(scannedUri)
}
}
}
}

private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
Expand Down
4 changes: 0 additions & 4 deletions compose/snippets/src/main/res/xml/provider_paths.xml

This file was deleted.

0 comments on commit 2b2008e

Please sign in to comment.