Formation Android

TP 4: Images, Permissions, Stockage

Coil

imageView.load("https://goo.gl/gEgYUd")

UserActivity

setContent {
    var bitmap: Bitmap? by remember { mutableStateOf(null) }
    var uri: Uri? by remember { mutableStateOf(null) }
    Column {
        AsyncImage(
            modifier = Modifier.fillMaxHeight(.2f),
            model = bitmap ?: uri,
            contentDescription = null
        )
        Button(
            onClick = {},
            content = { Text("Take picture") }
        )
        Button(
            onClick = {},
            content = { Text("Pick photo") }
        )
    }
}

Caméra: ActivityResult

val takePicture = rememberLauncherForActivityResult(TakePicturePreview()) {
    bitmap = it
}

Uploader l’image capturée

@Multipart
@POST("sync/v9/update_avatar")
suspend fun updateAvatar(@Part avatar: MultipartBody.Part): Response<User>
private fun Bitmap.toRequestBody(): MultipartBody.Part {
    val tmpFile = File.createTempFile("avatar", "jpg")
    tmpFile.outputStream().use { // *use*: open et close automatiquement
        this.compress(Bitmap.CompressFormat.JPEG, 100, it) // *this* est le bitmap ici
    }
    return MultipartBody.Part.createFormData(
        name = "avatar",
        filename = "avatar.jpg",
        body = tmpFile.readBytes().toRequestBody()
    )
}
imageView.load(user.avatar) {
    error(R.drawable.ic_launcher_background) // image par défaut en cas d'erreur
}

Stockage: Accéder et uploader

On va maintenant permettre à l’utilisateur d’uploader une image enregistrée sur son téléphone

Pour simplifier on utilisera PhotoPicker

private fun Uri.toRequestBody(): MultipartBody.Part {
    val fileInputStream = contentResolver.openInputStream(this)!!
    val fileBody = fileInputStream.readBytes().toRequestBody()
    return MultipartBody.Part.createFormData(
        name = "avatar",
        filename = "avatar.jpg",
        body = fileBody
    )
}

Permissions

Jusqu’ici, vous avez probablement utilisé un Android en version 10 ou plus récente: la gestion de l’accès aux fichiers est simplifiée tant qu’on utilise les dossiers partagés (Images, Videos, etc) Mais pour gérer les versions plus anciennes, il faut demander la permission READ_EXTERNAL_STORAGE avant d’accéder au fichiers:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

Amélioration de la caméra

Actuellement, la qualité d’image récupérée de l’appareil photo est très faible (car passée en Bitmap dans la RAM), pour changer cela il faut utiliser le contrat TakePicture qui écrit dans un fichier passé au launcher par une Uri

// propriété: une URI dans le dossier partagé "Images"
private val captureUri by lazy {
    contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, ContentValues())
}

// launcher
val takePicture = rememberLauncherForActivityResult(TakePicture()) { success ->
    if (success) uri = capturedUri
}

// utilisation
takePicture.launch(captureUri)

Édition infos utilisateurs

@PATCH("sync/v9/sync")
suspend fun update(@Body userUpdate: UserUpdate): Response<Unit>

Gérer le refus

private fun pickPhotoWithPermission() {
    val storagePermission = Manifest.permission.READ_EXTERNAL_STORAGE
    val permissionStatus = checkSelfPermission(storagePermission)
    val isAlreadyAccepted = permissionStatus == PackageManager.PERMISSION_GRANTED
    val isExplanationNeeded = shouldShowRequestPermissionRationale(storagePermission)
    when {
        isAlreadyAccepted -> // lancer l'action souhaitée
        isExplanationNeeded -> // afficher une explication
        else -> // lancer la demande de permission et afficher une explication en cas de refus
    }
}

private fun showMessage(message: String) {
    Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show()
}

Pour faire encore mieux, vous pouvez aussi afficher un message avec AlertDialog en Compose et continuer le flow en fonction de la réponse de l’utilisateur