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 7 Telefonieren und surfen
Pfeil 7.1 Telefonieren
Pfeil 7.1.1 Anrufe tätigen und SMS versenden
Pfeil 7.1.2 Auf eingehende Anrufe reagieren
Pfeil 7.2 Telefon- und Netzstatus
Pfeil 7.2.1 Systemeinstellungen auslesen
Pfeil 7.2.2 Netzwerkinformationen anzeigen
Pfeil 7.2.3 Carrier Services
Pfeil 7.3 Das Call Log
Pfeil 7.3.1 Entgangene Anrufe ermitteln
Pfeil 7.3.2 Änderungen vornehmen und erkennen
Pfeil 7.4 Webseiten mit WebView anzeigen
Pfeil 7.4.1 Einen einfachen Webbrowser programmieren
Pfeil 7.4.2 JavaScript nutzen
Pfeil 7.5 Webservices nutzen
Pfeil 7.5.1 Auf Webinhalte zugreifen
Pfeil 7.5.2 Senden von Daten
Pfeil 7.6 Zusammenfassung
 
Zum Seitenanfang

7.3    Das Call Log Zur vorigen ÜberschriftZur nächsten Überschrift

Das Call Log speichert Informationen über getätigte und empfangene Anrufe und ist über den Menübefehl Anrufliste (im Emulator heißt er Call History) der App Telefon erreichbar. Der Zugriff auf die Daten des Call Logs ist für die verschiedensten Anwendungsfälle interessant. Denken Sie an Statistik-Apps, die das Anrufverhalten in Gestalt einer Tag Cloud visualisieren könnten. Oder stellen Sie sich die Integration in einen persönlichen Assistenten vor, der den Anwender daran erinnert, nach einem verpassten Anruf einen wichtigen Kunden zurückzurufen.

 
Zum Seitenanfang

7.3.1    Entgangene Anrufe ermitteln Zur vorigen ÜberschriftZur nächsten Überschrift

Wie Sie auf die Anrufhistorie zugreifen, zeige ich Ihnen anhand des Projekts CallLogDemo. Die in Abbildung 7.4 dargestellte App zeigt eine Liste der entgangenen Anrufe an. Sie können einzelne Einträge antippen, um sie als »zur Kenntnis genommen« zu markieren. Kommt während der Laufzeit des Programms ein verpasster Anruf hinzu, wird die Liste automatisch aktualisiert.

Da das Programm etwas länger ist, zeige ich Ihnen den Code nicht am Stück, sondern in hoffentlich leicht verdaulichen Häppchen.

Das Auslesen der Anrufhistorie findet in der Methode getMissedCalls() statt. Sie baut eine Verbindung zu einer Datenquelle auf und ermittelt alle Einträge, die der Bedingung CallLog.Calls.TYPE = CallLog.Calls.MISSED_TYPE genügen. Sie finden diesen Ausdruck allerdings nicht direkt im nachfolgenden Quelltext. Es ist nämlich bewährte Praxis, die der Variablen selection zugewiesene Anfrage mit Fragezeichen als Platzhalter zu versehen. Die zu substituierenden Werte stehen in selectionArgs. Das hat den Vorteil, nach unterschiedlichen Werten suchen zu können, ohne die eigentliche Abfrage verändern zu müssen. Beide Variablen werden der Abfragemethode query() übergeben.

In projection legen wir fest, welche Spalten einer Datenbanktabelle wir erhalten möchten. In meinem Beispiel sind dies die Nummer des Anrufers, Datum und Uhrzeit des Anrufes, ein Statusflag, das anzeigt, ob der Eintrag neu ist, sowie eine eindeutige Kennung. Letztere brauchen wir später, um einen Eintrag zu aktualisieren.

Die App »CallLogDemo«

Abbildung 7.4    Die App »CallLogDemo«

@Throws(SecurityException::class)
private fun getMissedCalls(): Cursor? {
val projection = arrayOf(CallLog.Calls.NUMBER, CallLog.Calls.DATE,
CallLog.Calls.NEW, CallLog.Calls._ID)
val selection = CallLog.Calls.TYPE + " = ?"
val selectionArgs = arrayOf(CallLog.Calls.MISSED_TYPE.toString())
return contentResolver.query(CallLog.Calls.CONTENT_URI,
projection, selection,
selectionArgs, null)
}

Listing 7.8    Die Methode »getMissedCalls()« der Klasse »CallLogDemoActivity«

getMissedCalls() benötigt die Referenz auf einen ContentResolver, wie sie beispielsweise die Methode getContentResolver() (in Kotlin einfach contentResolver) liefert. Sie ist in allen von android.content.Context abgeleiteten Klassen vorhanden. Content Resolver bilden die Zugriffsschichten auf beliebige Datenquellen, die Content Provider genannt werden. Android bringt neben der Anrufhistorie weitere Content Provider mit, die unter anderem Zugriff auf Termine und Kontakte gewähren.

Content Resolver stellen Methoden für die klassischen CRUD-Operationen, also Anlegen, Lesen, Ändern und Löschen, zur Verfügung. Die Vorgehensweise erinnert stark an klassische Datenbanksysteme. Die Datenquellen, also die Content Provider, müssen aber keineswegs zwingend SQL-Datenbanken sein. Beispielsweise könnten Webservices über einen Content Provider verfügbar gemacht werden. Wie Sie eigene Content Provider erstellen, zeige ich Ihnen in Kapitel 10, »Datenbanken«.

Damit getMissedCalls() funktioniert, muss die App in der Manifestdatei die Berechtigung android.permission.READ_CALL_LOG anfordern, und der Anwender muss sie erteilt haben. Die gegebenenfalls nötige Fehlerbehandlung findet an anderer Stelle statt, deshalb wirft die Methode eine SecurityException. Lassen Sie uns nun einen Blick darauf werfen, wie die Liste der entgangenen Anrufe angezeigt wird. Sehen Sie sich hierzu die Methode onCreate() der Hauptaktivität CallLogDemoActivity an.

Die Klasse leitet von androidx.appcompat.app.AppCompatActivity ab. Mit listview.adapter = ... wird ein ListAdapter gesetzt, der eine Datenquelle benötigt. Ich verwende hierfür einen SimpleCursorAdapter. Die von der Activity angezeigte ListView erhält ihre Einträge somit aus einem Cursor, der zunächst mit null vorbelegt ist. android.R.layout.simple_list_item_1 wird vom System bereitgestellt und eignet sich prima für einzeilige Textelemente. Die Zuordnung von Spalten eines Cursors zu Views des Elementlayouts geschieht mit den beiden folgenden Array-Parametern. Um die Rufnummer anzuzeigen, mappen wir CallLog.Calls.NUMBER auf android.R.id.text1. Wie aber können wir den Text »(neu)« nach der Rufnummer ausgeben, wenn Android sich vollständig um die Aufbereitung der Listeneinträge kümmert?

Ich verwende hierfür das Interface SimpleCursorAdapter.ViewBinder und setze ein passendes Objekt mit cursorAdapter.viewBinder = ... . Die Idee ist, Cursorspalten an Views zu binden. Android ruft die Methode setViewValue() (der Name ist durch die Verwendung eines Lambda-Ausdrucks nicht direkt zu sehen) für jede Spalte auf. Der Rückgabewert true signalisiert dem System, dass ein neuer Wert gesetzt wurde. Auf diese Weise lassen sich Spalten bequem kombinieren. Übrigens liefert die Cursormethode getColumnIndex() den Index einer Tabellenspalte. Es ist bewährte Praxis, sie aufzurufen, auch wenn die Reihenfolge der Spalten »eigentlich« bekannt ist (sie ergibt sich aus dem Array projection in getMissedCalls()).

Im Anschluss daran registriert onCreate() einen OnItemClickListener. Seine Methode onItemClick() wird aufgerufen, wenn Sie ein Listenelement antippen. In diesem Fall ermittle ich mit getItem() einen Cursor, der den zugehörigen Eintrag in der Anrufhistorie repräsentiert. Der Ausdruck c.getLong(c.getColumnIndex(CallLog.Calls._ID)) liefert dessen eindeutige Kennung. Sie wird benötigt, um in der privaten Methode updateCallLogData() den Status »neu« zu ändern. Mehr dazu gleich.

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
cursorAdapter = SimpleCursorAdapter(this,
android.R.layout.simple_list_item_1,
null, arrayOf(CallLog.Calls.NUMBER),
intArrayOf(android.R.id.text1), 0)
cursorAdapter.viewBinder = SimpleCursorAdapter.ViewBinder {
view: View, cursor: Cursor, columnIndex: Int ->
if (columnIndex ==
cursor.getColumnIndex(CallLog.Calls.NUMBER)) {
var number = cursor.getString(columnIndex)
val isNew = cursor.getInt(cursor.getColumnIndex(
CallLog.Calls.NEW))
if (isNew != 0) {
number += " (neu)"
}
(view as TextView).text = number
true
} else {
false
}
}
listview.adapter = cursorAdapter
listview.onItemClickListener = OnItemClickListener {
_: AdapterView<*>?, _: View?, position: Int, _: Long ->
val c = cursorAdapter.getItem(position) as Cursor
val callLogId = c.getLong(c.getColumnIndex(
CallLog.Calls._ID))
updateCallLogData(callLogId)
}
updateAdapter()
}

Listing 7.9    Die Methode »onCreate()« der Klasse »CallLogDemoActivity«

Woher bekommt der SimpleCursorAdapter eigentlich seine Daten? Beim Aufruf des Konstruktors hatte ich ja null übergeben. Die im Folgenden vorgestellte Methode updateAdapter() wird unter anderem in onCreate() aufgerufen. Sofern der Benutzer den Zugriff auf die Anrufhistorie gestattet hat (das wird wie üblich mit checkSelfPermission() geprüft), übergibt sie den von getMissedCalls() gelieferten Cursor an die Methode changeCursor() der abstrakten Klasse CursorAdapter, von der SimpleCursorAdapter ableitet. Dies sorgt dafür, dass die Liste ihre Elemente (neu) einliest und ein eventuell vorher gesetzter Cursor automatisch geschlossen wird.

private fun updateAdapter() {
if (checkSelfPermission(Manifest.permission.READ_CALL_LOG)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.READ_CALL_LOG),
REQUEST_READ_CALL_LOG)
} else {
// hier kommt später Code zum Registrieren
// eines ContentObservers
thread {
val c = getMissedCalls()
runOnUiThread { cursorAdapter.changeCursor(c) }
}
}
if (checkSelfPermission(Manifest.permission.WRITE_CALL_LOG)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(
Manifest.permission.WRITE_CALL_LOG),
REQUEST_WRITE_CALL_LOG)
}
}

Listing 7.10    Die Methode »updateAdapter()« der Klasse »CallLogDemoActivity«

Vielleicht fragen Sie sich, warum an dieser Stelle ein neuer Thread gestartet wird. Die Verarbeitung von Datenbankabfragen kann je nach Komplexität einige Zeit in Anspruch nehmen. Solche »Langläufer« dürfen aber, wie ich in Kapitel 6, »Multitasking«, ausführlich erläutere, nicht auf dem Mainthread ausgeführt werden. Das Setzen des neuen Cursors hingegen muss auf diesem geschehen – deshalb die Verwendung von runOnUiThread(). Bitte achten Sie darauf, mit

import kotlin.concurrent.thread

die Funktion thread zu importieren.

 
Zum Seitenanfang

7.3.2    Änderungen vornehmen und erkennen Zur vorigen ÜberschriftZur nächsten Überschrift

Die Daten des Call Logs können durch Apps nicht nur gelesen, sondern auch verändert werden. Sie können sich dies beispielsweise zunutze machen, um den Status Calls.NEW auf false zu setzen. Damit werden Anrufe nicht mehr als »neu« angezeigt. Dies funktioniert folgendermaßen:

private fun updateCallLogData(id: Long) {
if (checkSelfPermission(Manifest.permission.WRITE_CALL_LOG)
== PackageManager.PERMISSION_GRANTED) {
val values = ContentValues()
values.put(CallLog.Calls.NEW, 0)
val where = CallLog.Calls._ID + " = ?"
val selectionArgs = arrayOf(id.toString())
contentResolver.update(CallLog.Calls.CONTENT_URI,
values, where, selectionArgs)
}
}

Listing 7.11    Die Methode »updateCallLogData()« der Klasse »CallLogDemoActivity«

Mit einem Objekt des Typs android.content.ContentValues legen Sie fest, welche Spalte einer Tabelle (zum Beispiel CallLog.Calls.NEW) einen neuen Wert erhalten soll und wie dieser lautet. Welche Zeilen aktualisiert werden, ergibt sich – analog zur Abfrage mit query() – aus einer Bedingung. In meinem Beispiel lautet sie CallLog.Calls._ID = ?. Das Fragezeichen als Platzhalter kennen Sie schon: Zur Laufzeit wird es durch den Methodenparameter id ersetzt. Die Methode updateCallLogData() wird von einem OnItemClickListener aufgerufen, den wir in onCreate() gesetzt haben.

Da durch die Update-Anweisung Daten verändert werden, müssen Sie im Manifest die Berechtigung android.permission.WRITE_CALL_LOG anfordern und sicherstellen, dass sie bei Ausführung des gerade eben gezeigten Codes auch vorliegt. Das Anfordern erfolgt in der Ihnen bereits bekannten Methode updateAdapter().

Benachrichtigung bei Änderungen

Ich habe Ihnen bisher gezeigt, wie Sie entgangene Anrufe ermitteln können. Was aber geschieht, wenn sich Änderungen im Call Log ergeben, zum Beispiel weil Sie einen Anruf verpasst oder selbst ein Telefonat getätigt haben? Sowohl Apps im Vordergrund als auch Widgets sollten dann ihre Anzeigen entsprechend aktualisieren. Ein regelmäßiger Aufruf der Methode getMissedCalls() (Polling) würde unnötig Rechenzeit und damit Energie verbrauchen, aber Android bietet hierfür eine elegante Lösung: ContentResolver bieten die Möglichkeit, sich bei Änderungen in einer Datenbank informieren zu lassen. Das funktioniert so:

private val handler = Handler(Looper.getMainLooper())
private val contentObserver = object : ContentObserver(handler) {
override fun onChange(selfChange: Boolean) {
updateAdapter()
}
}
private var contentObserverWasRegistered = false
...
private fun updateAdapter() {
...
if (!contentObserverWasRegistered) {
contentResolver.registerContentObserver(
CallLog.Calls.CONTENT_URI,
false, contentObserver)
contentObserverWasRegistered = true
}
..

Listing 7.12    Auszug aus der Methode »updateAdapter()« der Klasse »CallLogDemoActivity«

Zuerst wird ein Objekt des Typs ContentObserver erzeugt. Seine Methode onChange() wird immer dann aufgerufen, wenn sich Änderungen an einem Datenbestand ergeben. In welchem Thread die Abarbeitung erfolgt, ergibt sich aus dem Handler, den wir dem Konstruktor übergeben haben. In meinem Beispiel ist dies der Mainthread, weil contentObserver eine Instanzvariable der Activity CallLogDemoActivity ist. Da wir in der Methode updateAdapter() für die eigentliche Datenbankabfrage einen neuen Thread starten, hat das keine Auswirkungen auf die Reaktionszeit der Benutzeroberfläche. registerContentObserver() registriert den Content Observer. An dieser Stelle wird auch festgelegt, was überwacht werden soll. Übrigens ist contentObserver eine Instanzvariable, weil wir beim Zerstören der Aktivität die Überwachung beenden möchten.

override fun onDestroy() {
super.onDestroy()
if (contentObserverWasRegistered) {
contentResolver.unregisterContentObserver(contentObserver)
}
}

Listing 7.13    Die Methode »onDestroy()« der Klasse »CallLogDemoActivity«

Die beiden Berechtigungen READ_CALL_LOG und WRITE_CALL_LOG ermöglichen Apps den Zugriff auf sehr sensible Daten. Bitte denken Sie daran, dass sie nur dann in Apps angefordert werden dürfen, wenn das für Hauptfunktionen der App unerlässlich ist. Außerdem ist eine entsprechende Freigabe von Google erforderlich, sofern die App über den Play Store vertrieben werden soll.

Sie haben in diesem Abschnitt nicht nur die Anrufhistorie von Android kennengelernt, sondern auch viel über die Nutzung von Datenbanken erfahren. Nun wenden wir uns einem weiteren wichtigen Einsatzgebiet von Smartphones und Tablets zu, dem Anzeigen von Webseiten.

 


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