Android Arsenal – Dateisystem

Inhalt

Überblick

Je höher das API-Level, desto stärker beschränkt Google den Zugriff auf Dateien im Android-Speicher. Obwohl das Storage Access Framework (SAF) darauf ausgelegt ist, den Benutzerspeicher vor bösartigen Anwendungen zu schützen, macht es uns den Zugriff auf Dateien noch schwerer. Nehmen Sie das Beispiel wo java.io.File ist in Android 10 veraltet.

Die einfache Speicherung erleichtert Ihnen den Zugriff auf und die Verwaltung von Dateien auf allen Ebenen der API. Wenn Sie mehr über den Hintergrund dieser Bibliothek erfahren möchten, lesen Sie diesen Artikel: Framework für einfachen Speicherzugriff in Android mit SimpleStorage

Das Hinzufügen von Simple Storage zu Ihrem Projekt ist ziemlich einfach:

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

Wo X.Y.Z ist eine Version der Bibliothek:

Alle Versionen sind zu finden Hier. Benutzen SNAPSHOT Version müssen Sie diese URL zu Root-Gradle hinzufügen:

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

Java-Kompatibilität

Simple Storage wurde in Kotlin erstellt. Folge dies Dokumentation um es in Ihrem Java-Projekt zu verwenden.

Terminologie

Andere Terminologie

Dateien lesen

In einfacher Lagerung, DocumentFile Wird verwendet, um auf Dateien zuzugreifen, wenn Ihrer Anwendung vollständiger Speicherzugriff gewährt wurde, einschließlich Lese- und Schreibberechtigungs-URIs. Während MediaFile wird verwendet, um auf Mediendateien zuzugreifen MediaStore keine Speicherberechtigungs-URIs.

Sie können die Hilfsfunktionsdatei einlesen DocumentFileCompat ich MediaStoreCompat:

DocumentFileCompat

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

Beispiel

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()

Beispiel

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

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

Dokumentenverwaltung

DocumentFile

Denn java.io.File ist in Android 10 veraltet, also müssen Sie es verwenden DocumentFile um Dateien zu verwalten.

Simple Storage fügt Kotlina-Erweiterungsfunktionen hinzu DocumentFileSo können Sie Dateien wie folgt verwalten:

  • DocumentFile.getStorageId()
  • DocumentFile.getStorageType()
  • DocumentFile.getBasePath()
  • DocumentFile.copyFileTo()
  • List<DocumentFile>.moveTo()
  • DocumentFile.search()
  • DocumentFile.deleteRecursively()
  • DocumentFile.getProperties()
  • DocumentFile.openOutputStream()und vieles mehr…

MediaFile

Sie können ähnliche Optionen für Mediendateien haben DocumentFileoder:

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

Fordern Sie Zugang zum Lager an

Obwohl der Benutzer während der Laufzeit Lese- und Schreibberechtigungen erteilt hat, hat Ihre Anwendung möglicherweise noch keinen vollständigen Zugriff auf das Repository, sodass Sie keine Dateien suchen, verschieben und kopieren können. Ob Sie Zugriff auf das Lager haben, können Sie über prüfen SimpleStorage.hasStorageAccess().

Um vollen Zugriff auf das Repository zu gewähren, müssen Sie das SAF öffnen und dem Benutzer erlauben, URIs Lese- und Schreibzugriffsberechtigungen zu erteilen. Diese Bibliothek bietet Ihnen eine Hilfsklasse namens SimpleStorage um den Bewerbungsprozess zu erleichtern:

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)
    }
}

Ordnerauswahl

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()
        }
    }
}

Dateiauswahl

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

Wenn Sie die Implementierung der Ordner- und Dateiauswahl und der vollständigen Festplattenanforderungen kompliziert genug finden, können Sie verwenden SimpleStorageHelper um den Prozess zu vereinfachen. Diese Hilfsklasse enthält Standardstile für die Verwaltung des Warehouse-Zugriffs.

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)
    }
}

Einfacher, oder?

Verschieben und kopieren: Dateien und Ordner

Simple Storage hilft Ihnen beim Kopieren/Verschieben von Dateien und Ordnern über:

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

Sie können beispielsweise einen Ordner mit mehreren Codezeilen verschieben:

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()
    }
})

Das Beste an dieser Bibliothek ist, dass Sie Benutzer bitten können, „Zusammenführen“, „Ersetzen“, „Neu erstellen“ oder „Doppelte Ordner und Dateien überspringen“ zu wählen, wenn ein Konflikt über gefunden wird onConflict(). Hier sind Screenshots des Codebeispiels zum Umgang mit Konflikten:

Lesen MainActivity aus dem Beispielcode, wenn Sie die obigen Dialoge imitieren möchten.

FAQ

Probleme? Weiter lesen Häufig gestellte Fragen.

Weitere Beispiele für die Verwendung von SimpleStorage

In diesen Open-Source-Projekten kommt SimpleStorage zum Einsatz. Sehen Sie sich an, wie diese Repositories es verwenden:

Lizenz

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.