diff --git a/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/features/saf/SafFolderCreator.kt b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/features/saf/SafFolderCreator.kt new file mode 100644 index 0000000..794c058 --- /dev/null +++ b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/features/saf/SafFolderCreator.kt @@ -0,0 +1,72 @@ +package com.vaultdrop.mobile.features.saf + +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.DocumentsContract +import dagger.hilt.android.qualifiers.ApplicationContext +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Crée un dossier physique via DocumentsContract (SAF) — facteur commun de + * `ensureVaultDropFolder` (onboarding) et de la feature « Créer un dossier ». + * + * Le parent peut être un URI *tree* (racine importée) ou *document* (dossier + * déjà créé/parcouru) : les deux sont acceptés par `createDocument`. + */ +@Singleton +class SafFolderCreator @Inject constructor( + @ApplicationContext private val context: Context, +) { + + private val resolver: ContentResolver get() = context.contentResolver + + /** + * Crée un sous-dossier nommé `name` sous `parentUri`. Retourne son URI SAF, + * ou null si le parent est absente ou si la création échoue. + */ + suspend fun createFolder(parentUri: String?, name: String): Uri? { + val parentDocumentUri = toDocumentUri(parentUri) ?: return null + val created = runCatching { + DocumentsContract.createDocument( + resolver, + parentDocumentUri, + DocumentsContract.Document.MIME_TYPE_DIR, + name, + ) + }.getOrElse { e -> + Timber.w(e, "cannot create folder '$name' in $parentUri") + null + } ?: return null + + // Best-effort : prolonge l'accès au-delà de l'intent original. + runCatching { + resolver.takePersistableUriPermission( + created, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION, + ) + }.onFailure { Timber.w(it, "persistable uri permission absent for new folder") } + + return created + } + + /** + * `DocumentsContract.createDocument` attend un URI *document*, pas un URI + * *tree*. Convertit un tree URI en document URI (équivalent au dossier + * racine de l'arbre) — les deux autorités sont identiques. + */ + private fun toDocumentUri(uri: String?): Uri? { + val raw = uri?.let(Uri::parse) ?: return null + return if (DocumentsContract.isTreeUri(raw)) { + DocumentsContract.buildDocumentUriUsingTree( + raw, + DocumentsContract.getTreeDocumentId(raw), + ) + } else { + raw + } + } +} \ No newline at end of file diff --git a/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/components/FolderNameDialog.kt b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/components/FolderNameDialog.kt new file mode 100644 index 0000000..3914dda --- /dev/null +++ b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/components/FolderNameDialog.kt @@ -0,0 +1,50 @@ +package com.vaultdrop.mobile.ui.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.vaultdrop.mobile.R + +/** Dialog de nommage d'un nouveau dossier — partagé accueil / dossier. */ +@Composable +fun FolderNameDialog( + onDismiss: () -> Unit, + onConfirm: (String) -> Unit, +) { + var name by remember { mutableStateOf("") } + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.new_folder_name_title)) }, + text = { + OutlinedTextField( + value = name, + onValueChange = { name = it }, + placeholder = { Text(stringResource(R.string.new_folder_name_hint)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + }, + confirmButton = { + TextButton( + onClick = { onConfirm(name.trim()) }, + enabled = name.isNotBlank(), + ) { + Text(stringResource(R.string.create_folder)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.pdf_builder_cancel)) + } + }, + ) +} \ No newline at end of file