4.4 Berechtigungen
Android-Apps werden immer in einer Sandbox ausgeführt. Innerhalb dieses geschützten Bereichs können sie sich frei entfalten. Möchte eine App aber andere Systemkomponenten nutzen oder auf fremde Anwendungen, deren Bausteine und Daten zugreifen, muss sie hierfür eine entsprechende Berechtigung besitzen.
4.4.1 Normale und gefährliche Berechtigungen
Android unterscheidet zwischen normalen und gefährlichen Berechtigungen. Erstere finden Verwendung, wenn eine App zwar auf Daten oder Ressourcen außerhalb der eigenen Sandbox zugreift, dies aber keinen nennenswerten Einfluss auf die Privatsphäre des Benutzers oder die Integrität anderer Apps hat. Beispiele für normale Berechtigungen sind etwa Zugriff auf das Internet, Setzen der Zeitzone, Setzen eines Wallpapers sowie Benachrichtigung über den Abschluss des Boot-Vorgangs (Abschnitt 4.2.3, »Broadcast Receiver«).
Fordert eine App eine normale Berechtigung an, gewährt das System diese automatisch. Gefährliche Berechtigungen hingegen müssen vom Anwender ausdrücklich gewährt oder verweigert werden. Vorher ist die Nutzung der entsprechenden Funktion nicht möglich. Beispiele für gefährliche Berechtigungen sind der lesende oder schreibende Zugriff auf Kontakt- und Kalenderdaten, die Nutzung von Kamera oder Mikrofon, das Versenden und Empfangen von SMS sowie in vielen Fällen das Laden und Speichern von Dateien.
Berechtigungen deklarieren
Apps müssen in der Manifestdatei sowohl normale als auch gefährliche Berechtigungen anfordern. Wie ein entsprechender Eintrag aussieht, zeigt Listing 4.25. Es gehört zu dem Beispielprojekt PermissionDemo. Jede Berechtigung erhält ihr eigenes <uses-permission ... />-Tag. Achten Sie bitte darauf, sie an der richtigen Stelle einzufügen, als Kinder von <manifest ... />.
[»] Hinweis
Auch Activities und Services können Berechtigungen definieren. Diese haben aber eine andere Bedeutung und funktionieren anders. Hinweise hierzu finden Sie in Abschnitt 6.2, »Services«.
Bis einschließlich Android 5 wurden Berechtigungen während der Installation abgefragt. Der Benutzer musste entscheiden, ob er sie in Gänze akzeptieren wollte, und falls nicht, so wurde die App nicht installiert. Ein selektives Zustimmen oder Ablehnen war ebenso wenig möglich wie das nachträgliche Entziehen oder Gewähren einer Berechtigung.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thomaskuenneth.androidbuch.permissiondemo">
<uses-permission
android:name="android.permission.READ_PHONE_NUMBERS" />
<application
...
<activity android:name=".MainActivity">
..
</activity>
</application>
</manifest>
Dies hat sich 2015 mit der Einführung von Marshmallow glücklicherweise geändert. Die sogenannten Runtime Permissions geben Anwendern viel Kontrolle darüber, was eine App darf. Ein entsprechendes Programmiermodell sorgt dafür, dass Berechtigungsabfragen erst zur Laufzeit erfolgen. Apps müssen hierfür als targetSdkVersion in der Datei build.gradle des Moduls app mindestens 23 eintragen. Andernfalls werden die Rechte weiterhin während der Installation abgefragt. Bitte denken Sie aber daran, dass Google bei Uploads in den Play Store die targetSdkVersion prüft. Apps mit zu niedrigem API-Level können Sie weder neu einstellen noch aktualisieren. Weitere Infos hierzu finden Sie in Abschnitt 3.3.2, »Apps in Google Play einstellen«. Das nachträgliche Entziehen oder Gewähren von Rechten über die Einstellungen funktioniert ab Android 6 übrigens unabhängig von der targetSdkVersion. Ein sorgfältiges Behandeln von Rückgabewerten und Ausnahmen wäre also auch bei niedrigerer targetSdkVersion Pflicht.
Berechtigungen prüfen und anfordern
Runtime Permissions bedeuten für Entwickler etwas zusätzliche Arbeit. Bitte sehen Sie sich hierzu Listing 4.26 an. Es demonstriert den Umgang mit ihnen, indem es in der Methode getLine1Number() die Telefonnummer eines Geräts ermittelt. Sie wird nur aufgerufen, wenn die App die nötige Berechtigung hat. Trotzdem ist die Behandlung der sonst geworfenen SecurityException nötig, weil sonst die in Android Studio eingebaute statische Codeanalyse (zu Recht) meckert.
In onCreate() wird die Benutzeroberfläche wie üblich mit setContentView() geladen und angezeigt sowie für eine Schaltfläche ein OnClickListener registriert. Dieser ruft die private Methode requestPermission() auf. Spannendes geschieht in onStart(): Als Erstes wird mit checkSelfPermission() geprüft, ob die App aktuell Telefonnummern auslesen darf. Ist dies der Fall, ruft mein Beispiel die Methode outputLine1Number() auf, in der die primäre Telefonnummer ermittelt und ausgegeben wird. Hat die App hingegen keine Berechtigung, diese Nummer auszulesen, fordert sie diese durch Aufrufen meiner privaten Methode requestPermission() an. Unter Umständen tut sie das aber nicht sofort, denn Google sieht vor, dass eine App den Benutzer über den Grund, weshalb sie ein bestimmtes Recht haben möchte, informieren soll, wenn er ihr die Berechtigung schon einmal verweigert hat.
package com.thomaskuenneth.androidbuch.permissiondemo
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.telephony.TelephonyManager
import android.util.Log
import android.view.View
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
private const val REQUEST_READ_PHONE_NUMBER = 123
private val TAG = PermissionDemoActivity::class.simpleName
class PermissionDemoActivity : AppCompatActivity() {
private lateinit var tv: TextView
private lateinit var bt: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tv = findViewById(R.id.tv)
bt = findViewById(R.id.bt)
bt.setOnClickListener { requestPermission() }
}
override fun onStart() {
super.onStart()
bt.visibility = View.GONE
if (checkSelfPermission(Manifest.permission.READ_PHONE_NUMBERS)
!= PackageManager.PERMISSION_GRANTED) {
if (shouldShowRequestPermissionRationale(
Manifest.permission.READ_PHONE_NUMBERS)) {
tv.setText(R.string.explain1)
bt.visibility = View.VISIBLE
} else {
requestPermission()
}
} else {
outputLine1Number()
}
}
override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>,
grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions,
grantResults)
if (requestCode == REQUEST_READ_PHONE_NUMBER) {
bt.visibility = View.GONE
if (grantResults.isNotEmpty() && grantResults[0]
== PackageManager.PERMISSION_GRANTED) {
outputLine1Number()
} else {
tv.setText(R.string.explain2)
}
}
}
private fun requestPermission() {
requestPermissions(arrayOf(
Manifest.permission.READ_PHONE_NUMBERS),
REQUEST_READ_PHONE_NUMBER)
}
private fun outputLine1Number() {
tv.text = getString(R.string.template,
getLine1Number())
}
private fun getLine1Number(): String {
var result = "???"
getSystemService(TelephonyManager::class.java)?.run {
try {
result = line1Number
} catch (ex: SecurityException) {
Log.e(TAG, "getLine1Number()", ex)
}
}
return result
}
}
Hierfür ist die Methode shouldShowRequestPermissionRationale() zuständig. Liefert sie true, blendet PermissionDemo die Schaltfläche Verstanden ein und zeigt den Hinweis »Bitte gewähren Sie der App die Berechtigung« an. Abbildung 4.13 zeigt die App, nachdem die Schaltfläche Verstanden angeklickt wurde (im Hintergrund sind Button und Text zu sehen). Nach dem ersten Ablehnen einer Berechtigungsanfrage wird hingegen nur »Die Berechtigung wurde verweigert« ausgegeben.
Wie sich eine App verhält, wenn der Anwender eine Berechtigung verweigert, ist davon abhängig, wie zentral das Recht für ihr Funktionieren ist. Berührt es nur eine Funktion, müssen Sie nur diese temporär stilllegen. Ist hingegen ohne die Berechtigung der Betrieb der App nicht möglich, sollte die Info an den Benutzer wie in PermissionDemo entsprechend deutlich ausfallen.
Auf Rechtevergaben reagieren
Eine Methode meines Beispiels habe ich bislang noch nicht besprochen, nämlich onRequestPermissionsResult(). Sie wird von Android aufgerufen, nachdem meine private Methode requestPermission() folgende Anweisung ausgeführt hat:
requestPermissions(arrayOf(
Manifest.permission.READ_PHONE_NUMBERS),
REQUEST_READ_PHONE_NUMBER)
Hierbei handelt es sich um das Pendant zum <uses-permission />-Tag in der Manifestdatei. requestPermissions() muss vor dem Auslesen der Telefonnummer aufgerufen werden, wenn checkSelfPermission() ergeben hat, dass die App die Berechtigung aktuell nicht hat. Bis einschließlich Android 10 bot der Berechtigungsdialog übrigens ein Verweigern und nicht mehr fragen. Android 11 sieht das wiederholte Anfordern von denselben Berechtigungen als schlechten Stil an und verwehrt diese nach dem zweiten Versuch.
Sicher ist Ihnen aufgefallen, dass der Abfragedialog vage von »Telefonanrufe tätigen und verwalten« schreibt, obwohl die App doch ganz genau eine Berechtigung, android.permission.READ_PHONE_NUMBERS, anfordert. Android fasst Berechtigungen zu sogenannten Berechtigungsgruppen zusammen. Möchte eine App eine bestimmte Berechtigung erhalten, zeigt das System die Meldung, die zu derjenigen Gruppe passt, der eine Berechtigung zugeordnet ist. Das bedeutet, dass kein Meldungsdialog mehr erscheint, wenn eine andere Berechtigung angefordert wird, die zu derselben Gruppe gehört. Bitte denken Sie auch daran, dass normale Berechtigungen grundsätzlich ohne Benutzerinteraktion gewährt werden.
4.4.2 Tipps und Tricks zu Berechtigungen
Die Methode shouldShowRequestPermissionRationale() steuert also, ob eine App die Nutzung einer Berechtigung erklären sollte. Hat der Anwender ein Recht endgültig entzogen, so liefert sie false. Wenn eine Geräterichtlinie einer App verbietet, eine Berechtigung zu erhalten, wird ebenfalls false zurückgegeben.
Für Tests ist es sehr praktisch, Berechtigungen über die Kommandozeile steuern zu können. Das folgende Kommando entzieht PermissionDemo die Berechtigung, die Telefonnummer auszulesen. Sie können das Kommando zum Beispiel im Werkzeugfenster-Terminal eingeben.
adb shell pm revoke com.thomaskuenneth.androidbuch.permissiondemo android.permission.READ_PHONE_NUMBERS
Das Schlüsselwort grant gewährt die Berechtigung.
Anwender erhalten über die in Abbildung 4.14 dargestellte Einstellungsseite App-Info Zugriff auf Berechtigungen.
Falls Sie diese Seite aus einer Activity Ihrer App heraus anzeigen möchten, sind nur wenige Zeilen Code nötig:
val intent = android.content.Intent()
intent.action = android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
val uri = android.net.Uri.fromParts("package", packageName, null)
intent.data = uri
startActivity(intent)
Ein Klick auf Berechtigungen öffnet die Seite mit den Berechtigungen, die von einer App angefordert werden. In Abbildung 4.15 ist diese Seite zu sehen.
Berechtigungen und der Lebenszyklus von Activities
Vielleicht fragen Sie sich, ob es »den besten Zeitpunkt« für das Prüfen und Anfordern von Berechtigungen gibt. Wie Sie wissen, sollten Sie Berechtigungen nur dann anfordern, wenn sie für das Ausführen einer bestimmten Aktion erforderlich sind. Aber was bedeutet das? Activities durchlaufen einen komplexen Lebenszyklus einschließlich einer ganzen Reihe von Callbacks, also Methoden, die Sie bei Bedarf überschreiben können.
onCreate() wird von praktisch jeder Activity überschrieben. Theoretisch können Sie also dort Ihre Berechtigungsprüfungen machen. Allerdings wird diese Methode nicht immer aufgerufen, sondern nur, wenn die Activity noch nie gestartet oder nach einem früheren Lauf zerstört wurde. Deshalb bietet sich onStart() als Alternative an. Hierbei kann es aber zu einem unerwarteten Effekt kommen. Wenn Sie mit requestPermissions() den systemweiten Berechtigungsdialog öffnen und der Benutzer während dessen Anzeige das Gerät dreht, also einen Orientierungswechsel vornimmt, wird wieder onStart() aufgerufen. Und damit wieder der Berechtigungsdialog. Android 7 und alle folgenden Versionen ignorieren den zweiten, unnötigen requestPermissions()-Aufruf, aber unter Android 6 erscheint der Dialog tatsächlich mehrmals. Mir ist keine Empfehlung von Google bekannt, wie man damit am besten umgehen sollte. Auf der sicheren Seite sind Sie, wenn Sie in Ihrer App speichern, dass gerade die Antwort auf eine Berechtigungsanfrage aussteht. Hierfür bieten sich die Shared Preferences an, die ich Ihnen in Abschnitt 5.2, »Vorgefertigte Bausteine für Oberflächen«, vorstelle. Da das Problem aber eine recht alte Version betrifft und auch eher selten auftreten dürfte, können Sie es wahrscheinlich auch ignorieren.