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 12 Kontakte und Organizer
Pfeil 12.1 Kontakte
Pfeil 12.1.1 Emulator konfigurieren
Pfeil 12.1.2 Eine einfache Kontaktliste ausgeben
Pfeil 12.1.3 Weitere Kontaktdaten ausgeben
Pfeil 12.1.4 Geburtstage hinzufügen und aktualisieren
Pfeil 12.2 Kalender und Termine
Pfeil 12.2.1 Termine anlegen und auslesen
Pfeil 12.2.2 Alarme und Timer
Pfeil 12.2.3 Die Klasse »CalendarContract«
Pfeil 12.3 Zusammenfassung
 
Zum Seitenanfang

12    Kontakte und Organizer Zur vorigen ÜberschriftZur nächsten Überschrift

Mit Smartphone und Tablet haben Sie jederzeit Zugriff auf Ihre Termine und Kontakte. In diesem Kapitel zeige ich Ihnen, wie Sie diese wertvollen Datenquellen mit Ihren eigenen Apps »anzapfen«.

Die hohe Kunst der App-Entwicklung besteht in der kreativen Kombination von vorhandener Hard- und Software zu etwas Neuem. Denken Sie an Apps, die Sensordaten mit Audio- und Videosignalen kombinieren und mit zusätzlichen Informationen aus dem Netz anreichern (Stichwort Augmented Reality). Für Sie als Entwickler gilt deshalb: Je mehr »Datentöpfe« Ihnen zur Verfügung stehen, desto größer sind Ihre Kombinationsmöglichkeiten.

Verglichen mit der Auswertung und Visualisierung von Ortsinformationen mag der Zugriff auf das Adressbuch oder den Kalender zunächst unspektakulär, vielleicht sogar langweilig wirken. Aber wäre es nicht toll, wenn Ihr Handy Sie bei einem eingehenden Anruf über anstehende Termine mit dem Gesprächspartner informieren oder an dessen Geburtstag erinnern würde? Oder stellen Sie sich eine App vor, die nach dem Anklicken der Notiz »Max Mustermann anrufen« eine Liste seiner Rufnummern einblendet und anbietet, automatisch zu wählen.

 
Zum Seitenanfang

12.1    Kontakte Zur vorigen ÜberschriftZur nächsten Überschrift

Android verteilt Kontaktdaten auf eine ganze Reihe von Tabellen, die Ihnen über Content Provider zur Verfügung stehen. Wie Sie diese Puzzleteile zusammensetzen müssen, ist in Googles Dokumentation leider nur recht oberflächlich beschrieben. Deshalb möchte ich Ihnen in den folgenden Abschnitten einige wichtige Zugriffstechniken vorstellen.

 
Zum Seitenanfang

12.1.1    Emulator konfigurieren Zur vorigen ÜberschriftZur nächsten Überschrift

Über einen langen Zeitraum hat Google Emulator-Images in zwei Varianten zum Download angeboten: mit und ohne Google APIs. Die folgenden Beispiele setzen einen Emulator mit Google APIs voraus. Sofern Sie Ihren Emulator eingerichtet haben wie in Kapitel 1, »Android – eine offene, mobile Plattform«, beschrieben, ist diese Voraussetzung erfüllt. Auch auf echter Hardware sind die Google-Dienste üblicherweise installiert. Konten werden auf der Seite SettingsAccounts verwaltet. Um ein Konto hinzuzufügen, klicken Sie auf Add account. Die nun angezeigte Seite Add an account ist in Abbildung 12.1 zu sehen.

Seite zum Hinzufügen von Konten

Abbildung 12.1    Seite zum Hinzufügen von Konten

Klicken Sie auf Google. Daraufhin wird ein Assistent zum Hinzufügen eines Google-Kontos gestartet. Sie müssen, wie in Abbildung 12.2 zu sehen ist, auswählen, ob Sie ein neues Konto anlegen oder sich mit einem bestehenden anmelden möchten. Falls Sie ein vorhandenes Konto verwenden, geben Sie Ihren Benutzernamen und Ihr Passwort ein. Um ein neues anzulegen, klicken Sie auf Create account.

Nach dem Beenden des Assistenten gelangen Sie wieder zu der Hauptseite der Kontoeinstellungen. Ihr frisch hinzugefügtes Google-Konto wird in der Liste angezeigt (siehe Abbildung 12.3).

Anmeldung an einem Google-Konto

Abbildung 12.2    Anmeldung an einem Google-Konto

Das neu hinzugefügte Konto in den Systemeinstellungen

Abbildung 12.3    Das neu hinzugefügte Konto in den Systemeinstellungen

 
Zum Seitenanfang

12.1.2    Eine einfache Kontaktliste ausgeben Zur vorigen ÜberschriftZur nächsten Überschrift

Das Projekt KontakteDemo1 gibt in einem Textfeld eine Liste der im Adressbuch gespeicherten Kontakte aus. Es greift hierzu auf einen Content Provider zu, dessen Uniform Resource Identifier (URI) in der Konstante ContactsContract.Contacts.CONTENT_URI definiert ist. Die Klasse android.provider.ContactsContract fungiert als Schnittstelle oder Vertrag zwischen Apps und dem Datenbestand. Letzterer besteht aus drei Schichten, die sich in den Tabellen bzw. Klassen ContactsContract.Data, ContactsContract.RawContacts und ContactsContract.Contacts manifestieren. Data speichert beliebige persönliche Informationen wie Telefonnummer oder E-Mail-Adresse. RawContacts bündelt alle Informationen, die zu einer Person und einem Konto (zum Beispiel Twitter, Facebook oder Gmail) gehören. Contacts schließlich fasst einen RawContact oder mehrere zu einem Gesamtkontakt zusammen.

In Kapitel 10, »Datenbanken«, zeige ich, dass der Zugriff auf einen Content Provider über ein android.content.ContentResolver-Objekt erfolgt. Die Klasse KontakteDemo1Activity (Listing 12.1) ruft in der privaten Methode listContacts() dessen Methode query() auf und iteriert über den zurückgelieferten Cursor. Damit das funktioniert, muss die Berechtigung android.permission.READ_CONTACTS in der Manifestdatei eingetragen und zur Laufzeit der App angefordert werden. Dies geschieht wie gewohnt in der Methode onStart() durch Aufruf von checkSelfPermission() und requestPermissions(). onCreate() ist nur für das Laden und Anzeigen der Benutzeroberfläche zuständig.

package com.thomaskuenneth.androidbuch.kontaktedemo1

import android.Manifest.permission.*
import android.content.pm.PackageManager.*
import android.os.Bundle
import android.provider.ContactsContract
import android.provider.ContactsContract.CommonDataKinds.*
import android.provider.ContactsContract.Data
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import java.util.*
import java.util.regex.*
import java.text.*
import android.util.*

private const val REQUEST_READ_CONTACTS = 123
class KontakteDemo1Activity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}

override fun onStart() {
super.onStart()
if (checkSelfPermission(READ_CONTACTS)
!= PERMISSION_GRANTED) {
requestPermissions(arrayOf(READ_CONTACTS),
REQUEST_READ_CONTACTS)
} else {
listContacts()
}
}

override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>,
grantResults: IntArray) {
if (requestCode == REQUEST_READ_CONTACTS &&
grantResults.isNotEmpty() && grantResults[0] ==
PERMISSION_GRANTED) {
listContacts()
}
else
textview.text = getString(R.string.no_permission)
}

private fun listContacts() {
// IDs und Namen aller sichtbaren Kontakte ermitteln
val mainQueryProjection = arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME)
val mainQuerySelection =
"${ContactsContract.Contacts.IN_VISIBLE_GROUP} = ?"
val mainQuerySelectionArgs = arrayOf("1")
contentResolver.query(
ContactsContract.Contacts.CONTENT_URI,
mainQueryProjection,
mainQuerySelection,
mainQuerySelectionArgs, null)?.run {
// Trefferliste abarbeiten...
while (moveToNext()) {
val contactId = getString(0)
val displayName = getString(1)
textview.append("===> $displayName ($contactId)\n")
}
close()
}
}
}

Listing 12.1    Die Klasse »KontakteDemo1Activity«

Nachdem der Benutzer dem Zugriff auf Kontakte zugestimmt hat, werden die Namen und IDs aller Kontakte angezeigt. Wie das aussehen kann, sehen Sie in Abbildung 12.4.

Die App »KontakteDemo1«

Abbildung 12.4    Die App »KontakteDemo1«

Ist Ihnen aufgefallen, dass ich in der Methode listContacts() eine Auswahlbedingung definiert habe, die nur Einträge liefert, deren Tabellenspalte IN_VISIBLE_GROUP den Wert 1 enthält? Auf diese Weise erhalten Sie ausschließlich »richtige« Kontakte. Android merkt sich nämlich auch Absender von E-Mails. Diese würden ohne Verwendung der Bedingung ebenfalls geliefert, was in der Regel nicht gewünscht ist. Im Emulator fällt dieses Verhalten sehr wahrscheinlich nicht auf, wohl aber auf echter Hardware. IN_VISIBLE_GROUP wird in der Klasse ContactsContract.Contacts definiert. Viele weitere Konstanten sind dort nicht vorhanden. Interessante Daten wie Geburtsdatum, E-Mail-Adresse oder Telefonnummer müssen anderweitig ermittelt werden. Wie Sie hierzu vorgehen, zeige ich Ihnen anhand des Geburtsdatums.

 
Zum Seitenanfang

12.1.3    Weitere Kontaktdaten ausgeben Zur vorigen ÜberschriftZur nächsten Überschrift

Lassen Sie uns die Klasse KontakteDemo1Activity erweitern, indem wir unmittelbar unterhalb der Anweisung

textview.append("===> $displayName ($contactId)\n")

die folgende Zeile hinzufügen:

infosAuslesen(contactId)

Die Implementierung dieser neuen Methode sehen Sie in Listing 12.2.

private fun infosAuslesen(contactId: String) {
val dataQueryProjection = arrayOf( Event.TYPE, Event.START_DATE,
Event.LABEL)
val dataQuerySelection =
"${Data.CONTACT_ID} = ? AND ${Data.MIMETYPE} = ?"
val dataQuerySelectionArgs = arrayOf(contactId,
Event.CONTENT_ITEM_TYPE)
contentResolver.query(Data.CONTENT_URI, dataQueryProjection,
dataQuerySelection, dataQuerySelectionArgs,
null)?.run {
while (moveToNext()) {
val type = getInt(0)
val label = getString(2)
if (Event.TYPE_BIRTHDAY == type) {
val stringBirthday = getString(1)
textview.append("_____birthday: $stringBirthday\n")
} else {
val stringAnniversary = getString(1)
textview.append(
"_____event: $stringAnniversary (type=$type, label=$label)")
when {
Event.TYPE_ANNIVERSARY == type -> {
textview.append("_____TYPE_ANNIVERSARY\n")
}
Event.TYPE_CUSTOM == type -> {
textview.append("_____TYPE_CUSTOM\n")
}
else -> {
textview.append("_____TYPE_OTHER\n")
}
}
}
}
close()
}
}

Listing 12.2    Geburtstage und Jahrestage auslesen

Der Kern ist auch hier der Aufruf der Methode query(), wobei diesmal als URI ContactsContract.Data.CONTENT_URI übergeben wird. Diese Tabelle enthält beliebige einzelne Daten, unter anderem Jahres- und Geburtstage. Die Zuordnung zu einem Kontakt geschieht über die Spalte CONTACT_ID. Da wir eine solche ID als Parameter übergeben haben, können wir sehr einfach eine entsprechende Auswahlbedingung (dataQuerySelection) formulieren. Mit dem Ausdruck nach AND wird die Treffermenge auf einen bestimmten MIME-Type eingeschränkt:

val dataQuerySelection =
"${Data.CONTACT_ID} = ? AND ${Data.MIMETYPE} = ?"

Wir übergeben in der Variablen dataQuerySelectionArgs den Wert ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE. Er kennzeichnet Ereignisse. Die Abfrage liefert entsprechend der Definition der Variablen dataQueryProjection die drei Spalten Event.TYPE (Ereignistyp), Event.START_DATE (Startdatum des Ereignisses) und Event.LABEL (eine Beschreibung). Leider ist das Format des Startdatums nicht fest vorgegeben. Das folgende Quelltextfragment liefert meiner Erfahrung nach aber sehr oft das gewünschte Ergebnis. Die Methode getDateFromString1() parst den ihr übergebenen String als Datum im Format 19700829. Zwischen Jahr und Monat sowie zwischen Monat und Tag können beliebige Zeichen stehen.

private val FORMAT_YYYYMMDD = SimpleDateFormat("yyyyMMdd", Locale.US)
private val TAG = KontakteDemo1Activity::class.simpleName
...
fun getDateFromString1(string: String): Date {
val p = Pattern.compile("(\\d\\d\\d\\d).*(\\d\\d).*(\\d\\d)",
Pattern.DOTALL)
val m = p.matcher(string)
if (m.matches()) {
val date = "${m.group(1)}${m.group(2)}${m.group(3)}"
try {
return FORMAT_YYYYMMDD.parse(date) ?: Date()
} catch (tr: Throwable) {
Log.e(TAG, "getDateFromString1()", tr)
}
}
return Date()
}

Listing 12.3    Datum von »String« nach »Date« umwandeln

Bislang haben wir nur lesend auf Kontaktdaten zugegriffen. Wie das Ändern und Hinzufügen funktioniert, zeige ich Ihnen im folgenden Abschnitt.

[+]  Tipp

Versuchen Sie als kleine Übung, die Methode getDateFromString1() an den richtigen Stellen aufzurufen. Damit Sie das zurückgelieferte Date-Objekt in lesbarer Form ausgeben können, sollten Sie ein weiteres SimpleDateFormat-Objekt erzeugen und dessen Methode format() aufrufen.

 
Zum Seitenanfang

12.1.4    Geburtstage hinzufügen und aktualisieren Zur vorigen ÜberschriftZur nächsten Überschrift

Das Projekt KontakteDemo2 sucht nach einem Kontakt, dessen angezeigter Name »Testperson« lautet. Wird ein solcher Datensatz gefunden und hat dieser noch kein Geburtsdatum, setzt das Programm es auf das aktuelle Datum. War hingegen schon ein Geburtstag eingetragen, wird das Geburtsjahr um 1 herabgesetzt – der Kontakt wird also mit jedem Programmstart ein Jahr älter. Wenn Sie die App das erste Mal ausführen, wird die Meldung »Testperson nicht gefunden« ausgegeben. Legen Sie deshalb wie in Abbildung 12.5 dargestellt einen neuen Kontakt mit dem Namen »Testperson« an. Lassen Sie außer dem Vornamen alle Felder leer.

Einen Kontakt hinzufügen

Abbildung 12.5    Einen Kontakt hinzufügen

Ist die Testperson vorhanden, ermittelt das Programm die ID dieses Datensatzes. Wie Sie bereits wissen, wird diese für eine Suche in der Tabelle Data benötigt. Wurde dort schon ein Geburtstag eingetragen, so aktualisieren wir diesen, andernfalls wird ein neuer Datensatz hinzugefügt. Das können Sie in der Methode updateOrInsertBirthday() (Listing 12.4) einfach nachvollziehen. Ein Geburtstag wurde schon eingetragen, wenn es in der Tabelle Data eine Zeile gibt, deren Spalte CONTACT_ID der übergebenen Kontakt-ID entspricht, MIMETYPE den Wert Event.CONTENT_ITEM_TYPE und TYPE den Wert Event.TYPE_BIRTHDAY hat. In diesem Fall muss nur das aktuelle Geburtsdatum ausgelesen und das Jahr um 1 verringert werden. update() schreibt das geänderte Attribut zurück in die Tabelle.

private fun updateOrInsertBirthday(
contentResolver: ContentResolver,
contactId: String
) {
val dataQueryProjection = arrayOf(
CommonDataKinds.Event._ID,
CommonDataKinds.Event.START_DATE
)
val dataQuerySelection = """
${
Data.CONTACT_ID} = ? AND
${
Data.MIMETYPE} = ? AND
${
CommonDataKinds.Event.TYPE} = ?
""".cleanup()
val dataQuerySelectionArgs = arrayOf(
contactId,

CommonDataKinds.Event.CONTENT_ITEM_TYPE,
CommonDataKinds.Event.TYPE_BIRTHDAY.toString()
)

// Gibt es einen Geburtstag zu Kontakt #contactId?
contentResolver.query(
Data.CONTENT_URI, dataQueryProjection,
dataQuerySelection, dataQuerySelectionArgs, null
)?.run {
if (moveToNext()) {
// ja, Eintrag gefunden
val dataId = getString(0)
var date = getString(1)
output("Geburtstag (_id=$dataId): $date")
// Jahr um 1 verringern
try {
DATE_FORMAT.parse(date)?.let { d ->
val cal = Calendar.getInstance()
cal.time = d
cal.add(Calendar.YEAR, -1)
date = DATE_FORMAT.format(cal.time)
output("neues Geburtsdatum: $date")
}
// Tabelle aktualisieren
val updateWhere = """
${CommonDataKinds.Event._ID} = ? AND
${Data.MIMETYPE} = ? AND
${CommonDataKinds.Event.TYPE} = ?

""".cleanup()
val updateSelectionArgs = arrayOf(
dataId,
CommonDataKinds.Event.CONTENT_ITEM_TYPE,
CommonDataKinds.Event.TYPE_BIRTHDAY.toString()
)
val values = ContentValues()
values.put(
CommonDataKinds.Event.START_DATE,
date
)
val numRows = contentResolver.
update(
Data.CONTENT_URI, values,
updateWhere, updateSelectionArgs
)
output("update() war ${
if (numRows == 0) "
nicht " else ""}erfolgreich")
} catch (e: ParseException) {
output(e.toString())
}
} else {
output("keinen Geburtstag gefunden")
// Strings für die Suche nach RawContacts
val rawProjection = arrayOf(RawContacts._ID)
val rawSelection = "${RawContacts.CONTACT_ID} = ?"
val rawSelectionArgs = arrayOf(contactId)
// Werte für Tabellenzeile vorbereiten
val values = ContentValues()
values.put(
CommonDataKinds.Event.START_DATE,
DATE_FORMAT.format(Date())
)
values.put(
Data.MIMETYPE,
CommonDataKinds.Event.CONTENT_ITEM_TYPE
)
values.put(
CommonDataKinds.Event.TYPE,
CommonDataKinds.Event.TYPE_BIRTHDAY
)
// alle RawContacts befüllen
contentResolver.query(
RawContacts.CONTENT_URI,
rawProjection, rawSelection,
rawSelectionArgs, null
)?.run {
while (moveToNext()) {
val rawContactId = getString(0)
values.put(
CommonDataKinds.Event.RAW_CONTACT_ID,
rawContactId
)
val uri = contentResolver.insert(
Data.CONTENT_URI,
values
)
output(
"""
---> Hinzufügen des Geburtstags
für RawContacts-Id $rawContactId war
${if (uri == null) "
nicht" else ""} erfolgreich
"
"".cleanup()
)
}
close()
}
}
close()
}
}

Listing 12.4    Die Klasse »KontakteDemo2«

Sicher ist Ihnen aufgefallen, dass ich an mehreren Stellen die Funktion cleanup() aufrufe. Raw Strings behalten ja Einrückungen und Zeilenumbrüche. Um diese bequem zu eliminieren, habe ich eine sehr kurze Erweiterungsfunktion definiert. Sie entfernt Einrückungen und ersetzt Zeilenumbrüche durch ein Leerzeichen:

private fun String.cleanup(): String = 
trimIndent().replace("\n", " ")

Das Anlegen einer neuen Tabellenzeile folgt dem Schema, das ich in Kapitel 10, »Datenbanken«, ausführlich vorstelle. Die Methode insert() erhält ein ContentValues-Objekt, das mit Spalte-Wert-Paaren gefüllt wurde. Zu beachten ist allerdings, dass Sie im Gegensatz zu einem Update in der Tabelle RawContacts nach einer Zeile suchen müssen, die in der Spalte CONTACT_ID die übergebene Kontakt-ID enthält. Den Inhalt der Spalte RawContacts._ID müssen Sie mit der Anweisung

values.put(CommonDataKinds.Event.RAW_CONTACT_ID, rawContactId)

in das ContentValues-Objekt übernehmen, sonst bricht insert() zur Laufzeit mit einer Ausnahme ab. Beachten Sie, dass Sie für das Ändern oder Hinzufügen von Kontaktdaten die gefährliche Berechtigung android.permission.WRITE_CONTACTS in der Manifestdatei sowie zur Laufzeit Ihrer App anfordern müssen.

[»]  Hinweis

Die Beziehungen zwischen den Tabellen der Kontaktdatenbank sind recht komplex, deshalb sollten Sie Schreiboperationen sehr ausführlich im Emulator testen. Machen Sie vor Experimenten auf echter Hardware auf jeden Fall ein Backup Ihrer Kontakte. Eine kleine Unachtsamkeit bei der Entwicklung kann sonst zu ernsthaften Problemen führen.

 


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