El arsenal de Android – Sistema de archivos

Resumen

Descripción general

Cuanto más alto sea el nivel de la API, más acceso restringido tendrá Google a los archivos en el almacenamiento de Android. Aunque Storage Access Framework (SAF) está diseñado para proteger el espacio de almacenamiento del usuario de aplicaciones maliciosas, esto hace que sea aún más difícil para nosotros acceder a los archivos. Tomemos un ejemplo donde java.io.File fue obsoleto en Android 10.

Simple Storage facilita el acceso y la administración de archivos en todos los niveles de API. Si quieres saber más sobre los antecedentes de esta biblioteca, lee este artículo: Marco de fácil acceso al almacenamiento en Android con SimpleStorage

Agregar almacenamiento simple a su proyecto es bastante simple:

implementation "com.anggrayudi:storage:X.Y.Z"

Dónde está X.Y.Z es la versión de la biblioteca:

Todas las versiones se pueden encontrar aquí. Usar SNAPSHOT versión, debe agregar esta URL a la raíz de Gradle:

allprojects {
    repositories {
        google()
        mavenCentral()
        // add this line
        maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
    }
}

Compatibilidad Java

Simple Storage está integrado en Kotlin. Sigue esto documentación para usarlo en su proyecto Java.

Terminología

Otra terminología

Leer archivos

En almacenamiento simple, DocumentFile se utiliza para acceder a los archivos cuando a la aplicación se le ha otorgado acceso completo al almacenamiento, incluidos los permisos de URI para lectura y escritura. En lugar de MediaFile se utiliza para acceder a archivos multimedia desde MediaStore sin permisos de URI para el almacenamiento.

Puede leer archivos auxiliares en DocumentFileCompat Y MediaStoreCompat:

DocumentFileCompat

  • DocumentFileCompat.fromFullPath()
  • DocumentFileCompat.fromSimplePath()
  • DocumentFileCompat.fromFile()
  • DocumentFileCompat.fromPublicFolder()

Ejemplo

val fileFromExternalStorage = DocumentFileCompat.fromSimplePath(context, basePath = "Downloads/MyMovie.mp4")

val fileFromSdCard = DocumentFileCompat.fromSimplePath(context, storageId = "9016-4EF8", basePath = "Downloads/MyMovie.mp4")

MediaStoreCompat

  • MediaStoreCompat.fromMediaId()
  • MediaStoreCompat.fromFileName()
  • MediaStoreCompat.fromRelativePath()
  • MediaStoreCompat.fromFileNameContains()
  • MediaStoreCompat.fromMimeType()
  • MediaStoreCompat.fromMediaType()

Ejemplo

val myVideo = MediaStoreCompat.fromFileName(context, MediaType.DOWNLOADS, "MyMovie.mp4")

val imageList = MediaStoreCompat.fromMediaType(context, MediaType.IMAGE)

Administrar archivos

DocumentFile

Desde cuando java.io.File quedó en desuso en Android 10, por lo que debe usarlo DocumentFile para la gestión de archivos.

Simple Storage agrega funciones de extensión de Kotlin a DocumentFilepara que pueda manejar archivos como este:

  • DocumentFile.getStorageId()
  • DocumentFile.getStorageType()
  • DocumentFile.getBasePath()
  • DocumentFile.copyFileTo()
  • List<DocumentFile>.moveTo()
  • DocumentFile.search()
  • DocumentFile.deleteRecursively()
  • DocumentFile.getProperties()
  • DocumentFile.openOutputStream()y muchos otros…

MediaFile

Para los archivos multimedia, puede tener capacidades similares a DocumentFilees decir:

  • MediaFile.absolutePath
  • MediaFile.isPending
  • MediaFile.delete()
  • MediaFile.renameTo()
  • MediaFile.copyFileTo()
  • MediaFile.moveFileTo()
  • MediaFile.openInputStream()
  • MediaFile.openOutputStream()etc

Solicitar acceso al almacenamiento

Aunque el usuario ha otorgado permisos de lectura y escritura en tiempo de ejecución, es posible que su aplicación aún no tenga acceso total al almacenamiento, por lo que no puede buscar, mover ni copiar archivos. Puede verificar si tiene acceso al almacenamiento a través de SimpleStorage.hasStorageAccess().

Para habilitar el acceso completo al almacenamiento, debe abrir SAF y permitir que el usuario otorgue permisos de URI para acceso de lectura y escritura. Esta biblioteca proporciona una clase auxiliar llamada SimpleStorage para facilitar el proceso de solicitud:

class MainActivity : AppCompatActivity() {

    private val storage = SimpleStorage(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        setupSimpleStorage()
        btnRequestStorageAccess.setOnClickListener {
            storage.requestStorageAccess(REQUEST_CODE_STORAGE_ACCESS)
        }
    }

    private fun setupSimpleStorage() {
        storage.storageAccessCallback = object : StorageAccessCallback {
            override fun onRootPathNotSelected(
                requestCode: Int,
                rootPath: String,
                uri: Uri,
                selectedStorageType: StorageType,
                expectedStorageType: StorageType
            ) {
                MaterialDialog(this@MainActivity)
                    .message(text = "Please select $rootPath")
                    .negativeButton(android.R.string.cancel)
                    .positiveButton {
                        val initialRoot = if (expectedStorageType.isExpected(selectedStorageType)) selectedStorageType else expectedStorageType
                        storage.requestStorageAccess(REQUEST_CODE_STORAGE_ACCESS, initialRoot, expectedStorageType)
                    }.show()
            }

            override fun onCanceledByUser(requestCode: Int) {
                Toast.makeText(baseContext, "Canceled by user", Toast.LENGTH_SHORT).show()
            }

            override fun onStoragePermissionDenied(requestCode: Int) {
                /*
                Request runtime permissions for Manifest.permission.WRITE_EXTERNAL_STORAGE
                and Manifest.permission.READ_EXTERNAL_STORAGE
                */
            }

            override fun onRootPathPermissionGranted(requestCode: Int, root: DocumentFile) {
                Toast.makeText(baseContext, "Storage access has been granted for ${root.getStorageId(baseContext)}", Toast.LENGTH_SHORT).show()
            }
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        storage.onSaveInstanceState(outState)
        super.onSaveInstanceState(outState)
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        storage.onRestoreInstanceState(savedInstanceState)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        // Mandatory for Activity, but not for Fragment & ComponentActivity
        storage.onActivityResult(requestCode, resultCode, data)
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        // Mandatory for Activity, but not for Fragment & ComponentActivity
        storage.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }
}

Selector de carpetas

private fun requestStoragePermission() {
    /*
    Request runtime permissions for Manifest.permission.WRITE_EXTERNAL_STORAGE
    and Manifest.permission.READ_EXTERNAL_STORAGE
    */
}

private fun setupFolderPickerCallback() {
    storage.folderPickerCallback = object : FolderPickerCallback {
        override fun onStoragePermissionDenied(requestCode: Int) {
            requestStoragePermission()
        }

        override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) {
            if (storageType == StorageType.UNKNOWN) {
                requestStoragePermission()
                return
            }
            MaterialDialog(this@MainActivity)
                .message(
                    text = "You have no write access to this storage, thus selecting this folder is useless." +
                            "nWould you like to grant access to this folder?"
                )
                .negativeButton(android.R.string.cancel)
                .positiveButton {
                    storage.requestStorageAccess(REQUEST_CODE_STORAGE_ACCESS, storageType)
                }.show()
        }

        override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
            Toast.makeText(baseContext, folder.getAbsolutePath(baseContext), Toast.LENGTH_SHORT).show()
        }

        override fun onCanceledByUser(requestCode: Int) {
            Toast.makeText(baseContext, "Folder picker canceled by user", Toast.LENGTH_SHORT).show()
        }
    }
}

Selector de archivos

private fun setupFilePickerCallback() {
    storage.filePickerCallback = object : FilePickerCallback {
        override fun onCanceledByUser(requestCode: Int) {
            Toast.makeText(baseContext, "File picker canceled by user", Toast.LENGTH_SHORT).show()
        }

        override fun onStoragePermissionDenied(requestCode: Int, file: DocumentFile?) {
            requestStoragePermission()
        }

        override fun onFileSelected(requestCode: Int, file: DocumentFile) {
            Toast.makeText(baseContext, "File selected: ${file.name}", Toast.LENGTH_SHORT).show()
        }
    }
}

SimpleStorageHelper

Si encuentra que la implementación del selector de carpetas y archivos y la solicitud del disco completo es bastante complicada, puede usar SimpleStorageHelper para simplificar el proceso. Esta clase auxiliar contiene estilos predefinidos para administrar el acceso al almacenamiento.

class MainActivity : AppCompatActivity() {

    private val storageHelper = SimpleStorageHelper(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        storageHelper.onFolderSelected = { requestCode, folder ->
            // do stuff
        }
        storageHelper.onFileSelected = { requestCode, files ->
            // do stuff
        }

        btnRequestStorageAccess.setOnClickListener { storageHelper.requestStorageAccess() }
        btnOpenFolderPicker.setOnClickListener { storageHelper.openFolderPicker() }
        btnOpenFilePicker.setOnClickListener { storageHelper.openFilePicker() }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        storageHelper.onSaveInstanceState(outState)
        super.onSaveInstanceState(outState)
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        storageHelper.onRestoreInstanceState(savedInstanceState)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        // Mandatory for Activity, but not for Fragment & ComponentActivity
        storageHelper.storage.onActivityResult(requestCode, resultCode, data)
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        // Mandatory for Activity, but not for Fragment & ComponentActivity
        storageHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }
}

Más fácil, ¿verdad?

Mover y copiar: archivos y carpetas

Simple Storage lo ayuda a copiar/mover archivos y carpetas a través de:

  • DocumentFile.copyFileTo()
  • DocumentFile.moveFileTo()
  • DocumentFile.copyFolderTo()
  • DocumentFile.moveFolderTo()

Por ejemplo, puede mover una carpeta con unas pocas líneas de código:

val folder: DocumentFile = ...
val targetFolder: DocumentFile = ...

// Since moveFolderTo() is annotated with @WorkerThread, you must execute it in background thread
folder.moveFolderTo(applicationContext, targetFolder, skipEmptyFiles = false, callback = object : FolderCallback() {
    override fun onPrepare() {
        // Show notification or progress bar dialog with indeterminate state
    }

    override fun onCountingFiles() {
        // Inform user that the app is counting & calculating files
    }

    override fun onStart(folder: DocumentFile, totalFilesToCopy: Int, workerThread: Thread): Long {
        return 1000 // update progress every 1 second
    }

    override fun onParentConflict(destinationFolder: DocumentFile, action: FolderCallback.ParentFolderConflictAction, canMerge: Boolean) {
        handleParentFolderConflict(destinationFolder, action, canMerge)
    }

    override fun onContentConflict(
        destinationFolder: DocumentFile,
        conflictedFiles: MutableList<FolderCallback.FileConflict>,
        action: FolderCallback.FolderContentConflictAction
    ) {
        handleFolderContentConflict(action, conflictedFiles)
    }

    override fun onReport(report: Report) {
        Timber.d("onReport() -> ${report.progress.toInt()}% | Copied ${report.fileCount} files")
    }

    override fun onCompleted(result: Result) {
        Toast.makeText(baseContext, "Copied ${result.totalCopiedFiles} of ${result.totalFilesToCopy} files", Toast.LENGTH_SHORT).show()
    }

    override fun onFailed(errorCode: ErrorCode) {
        Toast.makeText(baseContext, "An error has occurred: $errorCode", Toast.LENGTH_SHORT).show()
    }
})

Lo mejor de esta biblioteca es que puede pedirles a los usuarios que elijan Fusionar, Reemplazar, Crear nuevo o Ignorar carpetas y archivos duplicados siempre que se encuentre un conflicto a través de onConflict(). Aquí están las capturas de pantalla del código de muestra cuando se trata de conflictos:

Leer MainActivity del código de ejemplo si desea imitar los cuadros de diálogo anteriores.

Preguntas más frecuentes

¿Teniendo problemas? Leer el Preguntas frecuentes.

Licencia

Copyright © 2020-2022 Anggrayudi Hardiannico A.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

.

Compruebe también

en vivo desde Droidcon, incluida la mayor actualización de Gemini en Android Studio y más lanzamientos del SDK de Android.

Acabamos de lanzar nuestro episodio de otoño de #TheAndroidShow en YouTube etcétera desarrollador.android.comy esta vez …

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *