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 DocumentFile
para 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 DocumentFile
es 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.
.