Rheinwerk Computing < openbook >

 
Inhaltsverzeichnis
Vorwort
Teil I Grundlagen
1 Android – eine offene, mobile Plattform
2 Hallo Android!
3 Von der Idee zur Veröffentlichung
Teil II Elementare Anwendungsbausteine
4 Wichtige Grundbausteine von Apps
5 Benutzeroberflächen
6 Multitasking
Teil III Gerätefunktionen nutzen
7 Telefonieren und surfen
8 Sensoren, GPS und Bluetooth
Teil IV Dateien und Datenbanken
9 Dateien lesen, schreiben und drucken
10 Datenbanken
Teil V Multimedia und Produktivität
11 Multimedia
12 Kontakte und Organizer
A Einführung in Kotlin
B Jetpack Compose
C Häufig benötigte Codebausteine
D Literaturverzeichnis
E Die Begleitmaterialien
Stichwortverzeichnis

Ihre Meinung?
Spacer
<< zurück
Android 11 von Thomas Künneth
Das Praxisbuch für App-Entwickler
Buch: Android 11

Android 11
Pfeil 9 Dateien lesen, schreiben und drucken
Pfeil 9.1 Grundlegende Dateioperationen
Pfeil 9.1.1 Dateien lesen und schreiben
Pfeil 9.1.2 Mit Verzeichnissen arbeiten
Pfeil 9.2 Externe Speichermedien
Pfeil 9.2.1 Mit externem Speicher arbeiten
Pfeil 9.2.2 Storage Manager
Pfeil 9.3 Drucken
Pfeil 9.3.1 Druckgrundlagen
Pfeil 9.3.2 Eigene Dokumenttypen drucken
Pfeil 9.4 Zusammenfassung
 
Zum Seitenanfang

9.2    Externe Speichermedien Zur vorigen ÜberschriftZur nächsten Überschrift

Android unterstützt zusätzlich zum internen Speicher externe Medien, auf denen Apps Daten ablegen können. Dabei kann es sich um austauschbare Medien (beispielsweise SD-Karten) oder um fest eingebauten Speicher handeln. Das mag paradox klingen: ein externer, fest eingebauter Speicher? Google rät in der Entwicklerdokumentation, sich nicht vom Begriff extern verwirren zu lassen. Letztlich ist damit gemeinsamer Speicher oder Speicher für Medien (zum Beispiel Fotos, Videos, Audiodaten) gemeint. Gemeinsam bedeutet übrigens nicht (mehr), dass Apps stets auf alle Daten, also auch die von fremden Apps, zugreifen können. Mit Android 10 hat Google den medienweiten Zugriff zum Schutz der Privatsphäre drastisch eingeschränkt.

 
Zum Seitenanfang

9.2.1    Mit externem Speicher arbeiten Zur vorigen ÜberschriftZur nächsten Überschrift

Wie Sie mit externen Medien umgehen, zeige ich Ihnen anhand des Projekts ExternalStorageDemo. Beispielsweise sollten Sie vor Lese- oder Schreibzugriffen mit android.os.Environment die Verfügbarkeit prüfen (Listing 9.4). Denn Medien müssen nicht permanent vorhanden (eingelegt) sein. Die Methode getExternalStorageState() gibt den Wert MEDIA_MOUNTED zurück, wenn lesender und schreibender Zugriff möglich ist. Bei MEDIA_MOUNTED_READ_ONLY kann nur lesend zugegriffen werden.

isExternalStorageRemovable() liefert true, wenn der Anwender das primäre externe Medium physikalisch entnehmen kann. false signalisiert, dass es fest eingebaut wurde. isExternalStorageEmulated() liefert true, wenn Android das externe Medium emuliert. In diesem Fall ist es einfach eine eigene Partition oder ein eigener Bereich des internen Speichers. Dann müssen Sie sich nicht die Mühe machen, Daten dort abzulegen. Denn die Vorteile des einfach mit anderen Apps teilen sind ja mit Android 10 weggefallen. Was es damit auf sich hat, sehen wir uns nun an.

private fun showExternalStorageState() {
tv.append(
getString(
if (Environment.isExternalStorageRemovable())
R.string.removable else R.string.not_removable
)
)
// Status abfragen
val state: String = Environment.getExternalStorageState()
val canRead: Boolean
val canWrite: Boolean
when (state) {
Environment.MEDIA_MOUNTED -> {
canRead = true
canWrite = true
}
Environment.MEDIA_MOUNTED_READ_ONLY -> {
canRead = true
canWrite = false
}
else -> {
canRead = false
canWrite = false
}
}
tv.append(getString(if (canRead) R.string.can_read
else R.string.cannot_read))
tv.append(getString(if (canWrite) R.string.can_write
else R.string.cannot_write))
tv.append("Wird emuliert: ${Environment.isExternalStorageEmulated()}\n")
}

Listing 9.4    Status eines externen Mediums abfragen

Environment.getExternalStorageDirectory() liefert den Zugriffspfad auf das Basisverzeichnis des primären externen Mediums. Mit der Variante getExternalStoragePublicDirectory() können Sie öffentliche Verzeichnisse für Medienarten (Fotos, Klingeltöne, ...) ermitteln. Beide Methoden gelten seit Android 10 aber als veraltet und sollten nicht mehr verwendet werden. Sie liefern weiterhin gültige Pfade, allerdings sind diese nicht mehr ohne Weiteres durch Apps verwendbar. Google hat mit API-Level 29 temporär einen sogenannten Legacy Mode eingeführt, um Entwicklern Zeit für den Umstieg auf andere APIs zu geben. Haben Apps diesen in ihrem Manifest aktiviert, sehen die Apps auch weiterhin fremde Daten.

Der Legacy Mode kann mit isExternalStorageLegacy() abgefragt werden. Er wirkt aber nur bei Apps, die als Ziel-Plattform API-Level 29 angegeben haben. Sie sollten ihn deshalb nicht verwenden, zumal es die von mir angesprochenen anderen APIs seit der ersten Android-Version gibt. Die Methode getExternalFilesDir() aus der Klasse Context erhält als Parameter eine Zeichenkette, die die Art der abzulegenden Dateien repräsentiert (zum Beispiel Environment.DIRECTORY_PICTURES). Solche Verzeichnisse werden der App zugerechnet. Deshalb sind für einen Zugriff keine Berechtigungen erforderlich. Dort abgelegte Daten werden bei der Deinstallation der App gelöscht.

Damit Sie das ausprobieren können, enthält ExternalStorageDemo die Methode saveBitmap() (Listing 9.5). Sie erzeugt mit createBitmap() ein Objekt des Typs Bitmap, das aus zwei sich kreuzenden Linien und dem Text »Hallo Android!« besteht. Die 100 Pixel breite und 100 Pixel hohe Grafik wird durch den Aufruf von compress() als .png-Grafik in einen OutputStream geschrieben. Der zweite Parameter gibt die gewünschte Qualität an. 0 bedeutet »auf kleine Dateigröße hin komprimieren«. 100 entspricht maximaler Qualität. Bei verlustfreien Formaten wie PNG spielt der Wert keine Rolle.

private fun saveBitmap(stream: OutputStream) {
val w = 100
val h = 100
val bm = Bitmap.createBitmap(w, h, Bitmap.Config.RGB_565)
val c = Canvas(bm)
val paint = Paint()
paint.textAlign = Align.CENTER
paint.color = Color.WHITE
c.drawRect(0f, 0f, w - 1f, h - 1f, paint)
paint.color = Color.BLUE
c.drawLine(0f, 0f, w - 1f, h - 1f, paint)
c.drawLine(0f, h - 1f, w - 1f, 0f, paint)
paint.color = Color.BLACK
c.drawText("Hallo Android!", w / 2f, h / 2f, paint)
bm.compress(CompressFormat.PNG, 100, stream)
}

Listing 9.5    Eine Grafik erzeugen und speichern

Zeichenoperationen erfolgen übrigens nicht direkt in die Bitmap, sondern auf einem Objekt des Typs android.graphics.Canvas. Es enthält unter anderem Methoden zum Zeichnen von Rechtecken (drawRect()), Linien (drawLine()) und Text (drawText()). Wichtige Eigenschaften von zu zeichnenden Elementen werden über Objekte des Typs android.graphics.Paint gesteuert. Zum Beispiel setzt textAlign die Ausrichtung eines auszugebenden Textes. paint.color = Color.BLACK sorgt dafür, dass der Text in Schwarz erscheint. Nach einer Zuweisung an color werden die nachfolgenden Malfunktionen mit der eben eingestellten Farbe ausgeführt.

Nach diesem kurzen Ausflug in die Welt des Zeichnens wenden wir uns wieder Speichermedien zu. Sicher möchten Sie wissen, wie saveBitmap() verwendet wird. FileOutputStream-Objekte können durch Übergabe einer File-Instanz an den Konstruktor erzeugt werden. getExternalFilesDir() liefert uns hierfür das Elternverzeichnis, den eigentlichen Dateinamen habe ich fest verdrahtet (grafik.png).

val dir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val file = File(dir, "grafik.png")
try {
FileOutputStream(file).use { fos -> saveBitmap(fos) }
tv.append("\n${file.absolutePath}")
} catch (e: IOException) {
Log.e(TAG, "FileOutputStream()", e)
}

Listing 9.6    Aufruf der Methode »saveBitmap()«

Vielleicht fragen Sie sich, warum ich gelegentlich vom primären externen Medium gesprochen habe. Je nach Gerät und Konfiguration kann es durchaus mehrere gemeinsam genutzte bzw. externe Medien geben. Die Methode getExternalFilesDirs() der Klasse android.content.Context liefert ein File-Feld (Achtung, einzelne Elemente können null sein), das die entsprechenden Pfade enthält. getExternalStorageState() und isExternalStorageRemovable() der Klasse Environment können File-Referenzen übergeben werden, um den Status zu prüfen oder um zu erfragen, ob das Medium entfernt werden kann. Element 0 des File-Arrays entspricht übrigens dem Rückgabewert von getExternalFilesDir().

In Googles Dokumentation ist zu lesen, dass getExternalFilesDirs() nur solche Verzeichnisse liefert, die als stabiler Bestandteil des Geräts angesehen werden können, zum Beispiel SD-Karten, die durch eine Abdeckung geschützt sind, nicht aber über USB On The Go angebundene Flash-Laufwerke. Um auch solche Ressourcen sauber anzusprechen, verwenden Sie den Storage Manager. Wie, das zeige ich Ihnen im folgenden Abschnitt.

 
Zum Seitenanfang

9.2.2    Storage Manager Zur vorigen ÜberschriftZur nächsten Überschrift

Die Klasse StorageManagerDemoActivity in Listing 9.7 gehört zu dem Projekt StorageManagerDemo (siehe Abbildung 9.5). In onCreate() wird mit getSystemService(StorageManager::class.java) die Referenz auf ein Objekt des Typs StorageManager ermittelt. storageVolumes liefert eine Liste von StorageVolumes, auf die der Benutzer aktuell zugreifen kann. Neben dem primären externen Medium kann es sich hierbei auch um SD-Karten und USB-Laufwerke handeln. getDescription() liefert eine Beschreibung des Volumes, state den aktuellen Status. Das primäre externe Medium liefert bei isPrimary den Wert true. isRemovable kennzeichnet Speicher, der entfernt werden kann. isEmulated schließlich gibt an, ob das externe Medium nur emuliert wird. Dies betrifft beispielsweise Smartphones und Tablets ohne Slot für SD-Karten.

Die App »StorageManagerDemo«

Abbildung 9.5    Die App »StorageManagerDemo«

Um Zugriff auf ein Volume zu erhalten, rufen Sie bis einschließlich Android 9 createAccessIntent() auf. Die Methode liefert ein Objekt des Typs android.content.Intent, das Sie an startActivityForResult() weiterreichen. Als Parameter übergeben Sie createAccessIntent() das gewünschte Verzeichnis oder null für das gesamte Volume. Letzteres ist aber nur möglich, wenn es sich nicht um das primäre externe Medium handelt. Mit API-Level 29 wurde createAccessIntent() für veraltet erklärt. Für neuere Android-Versionen sollten Sie deshalb die Methode createOpenDocumentTreeIntent() aufrufen. Auch sie liefert ein Intent, das Sie an startActivityForResult() übergeben. Der Anwender muss sich in beiden Fällen entscheiden, ob er der App den gewünschten Zugriff erlauben möchte.

package com.thomaskuenneth.androidbuch.storagemanagerdemo

import android.app.Activity
import android.content.Intent
import android.os.*
import android.os.storage.StorageManager
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import kotlinx.android.synthetic.main.activity_main.*

class StorageManagerDemoActivity : AppCompatActivity() {
private val requestCode = 123

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tv.text = ""
getSystemService(StorageManager::class.java).let {
for (volume in it.storageVolumes) {
if (tv.text.isNotEmpty()) tv.append("\n")
tv.append("${volume.getDescription(this)}\n")
tv.append(" --> state: ${volume.state}\n")
tv.append(" --> isPrimary: ${volume.isPrimary}\n")
tv.append(" --> isRemovable: ${volume.isRemovable}\n")
tv.append(" --> isEmulated: ${volume.isEmulated}\n")
if (volume.isPrimary) {
val intent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
volume.createOpenDocumentTreeIntent()
else
volume.createAccessIntent(Environment.DIRECTORY_DOWNLOADS)
startActivityForResult(intent, requestCode)
}
}
}
}

override fun onActivityResult(requestCode: Int,
resultCode: Int,
data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == this.requestCode &&
resultCode == Activity.RESULT_OK &&
data != null) {
val dir = DocumentFile.fromTreeUri(this,
data.data!!)
tv.append("\n${dir?.uri.toString()}\n")
for (file in dir?.listFiles() ?: emptyArray()) {
tv.append(" --> ${file.name}\n")
}
}
}
}

Listing 9.7    Die Klasse »StorageManagerDemoActivity«

Nach dem Bestätigen oder Ablehnen des Zugriffswunsches wird die Methode onActivityResult() mit RESULT_OK oder RESULT_CANCELED aufgerufen. Bei Letzterem hat das ebenfalls übergebene Intent den Wert null. Darf die App hingegen auf das Verzeichnis (und damit implizit auch auf alle Unterverzeichnisse) zugreifen, liefert data eine Uri-Instanz, die der statischen Methode fromTreeUri() der Klasse androidx.documentfile.provider.DocumentFile übergeben wird. Sie gehört zu Jetpack. In der Datei build.gradle des Moduls app muss deshalb eine implementation-Abhängigkeit zu androidx.documentfile:documentfile:... eingetragen werden. DocumentFile definiert Methoden, die sich am klassischen File-Objekt orientieren. Eine Liste der enthaltenen Elemente kann beispielsweise mit listFiles() ermittelt werden.

[+]  Tipp

Um nicht jedes Mal den Anwender beim Zugriff auf ein Verzeichnis um Erlaubnis fragen zu müssen, können Sie bei Bedarf die Methode takePersistableUriPermission() aufrufen.

Wie erfährt eine App eigentlich, dass der Nutzer eine SD-Karte eingelegt oder ein Flash-Laufwerk angeschlossen hat? Hierzu müssen Sie nur in der Manifestdatei einen Broadcast Receiver registrieren, der auf android.intent.action.MEDIA_MOUNTED reagiert. Listing 9.8 zeigt einen Auszug des Manifests von StorageManagerDemo.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thomaskuenneth.androidbuch.storagemanagerdemo">
<application
...
<activity
...
</activity>
<receiver
android:name=".MediaMountedReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name=
"android.intent.action.MEDIA_MOUNTED" />
<data android:scheme="file" />
</intent-filter>
</receiver>
</application>
</manifest>

Listing 9.8    Auszug der Manifestdatei von »StorageManagerDemo«

Wichtig ist, neben der Aktion (<action .. />) auch ein <data ../>-Tag mit dem Attribut android:scheme="file" hinzuzufügen. Sonst wird das Ereignis nicht an die App gemeldet. Die Implementierung meiner Klasse MediaMountedReceiver ist trivial: Es wird nur ein Toast mit dem Namen des gemounteten Volumes angezeigt.

package com.thomaskuenneth.androidbuch.storagemanagerdemo

import android.content.*
import android.os.storage.StorageVolume
import android.widget.Toast

class MediaMountedReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (Intent.ACTION_MEDIA_MOUNTED == intent?.action) {
intent.getParcelableExtra<StorageVolume>(
StorageVolume.EXTRA_STORAGE_VOLUME
)?.let { volume ->
Toast.makeText(
context, volume.getDescription(context),
Toast.LENGTH_LONG
).show()
}
}
}
}

Listing 9.9    Die Klasse »MediaMountedReceiver«

Mit getParcelableExtra(StorageVolume.EXTRA_STORAGE_VOLUME) können Sie abfragen, welches Volume sich geändert hat. Zugriff erhalten Sie dann auf die weiter oben beschriebene Weise. Um den Broadcast Receiver zu testen, können Sie in den Systemeinstellungen eines virtuellen Geräts (Settings • Storage) die Speicherkarte zunächst entfernen und danach wieder mounten.

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
Zur Rheinwerk-Konferenz für Kotlin
 Buchempfehlungen
Zum Rheinwerk-Shop: Kotlin

Kotlin


Zum Rheinwerk-Shop: Praxisbuch Usability und UX

Praxisbuch Usability und UX


Zum Rheinwerk-Shop: Flutter und Dart

Flutter und Dart


Zum Rheinwerk-Shop: App-Design

App-Design


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2023
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 
[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern