Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.