diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts index c2e5d9d7..4a65136d 100644 --- a/compose/snippets/build.gradle.kts +++ b/compose/snippets/build.gradle.kts @@ -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) diff --git a/compose/snippets/src/main/AndroidManifest.xml b/compose/snippets/src/main/AndroidManifest.xml index bdd0ad82..d8b06316 100644 --- a/compose/snippets/src/main/AndroidManifest.xml +++ b/compose/snippets/src/main/AndroidManifest.xml @@ -18,6 +18,9 @@ + + - - - - - \ No newline at end of file diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/AdvancedGraphicsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/AdvancedGraphicsSnippets.kt index 789ac0d8..6ac0152d 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/AdvancedGraphicsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/AdvancedGraphicsSnippets.kt @@ -16,8 +16,7 @@ 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 @@ -25,7 +24,11 @@ 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 @@ -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 @@ -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 @@ -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 @@ -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) { diff --git a/compose/snippets/src/main/res/xml/provider_paths.xml b/compose/snippets/src/main/res/xml/provider_paths.xml deleted file mode 100644 index 0f243bbc..00000000 --- a/compose/snippets/src/main/res/xml/provider_paths.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file