12 Kontakte und Organizer 

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.
12.1 Kontakte 

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.
12.1.1 Emulator konfigurieren 

Ü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 Settings • Accounts 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.
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).
Abbildung 12.2 Anmeldung an einem Google-Konto
Abbildung 12.3 Das neu hinzugefügte Konto in den Systemeinstellungen
12.1.2 Eine einfache Kontaktliste ausgeben 

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.
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.
12.1.3 Weitere Kontaktdaten ausgeben 

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.
12.1.4 Geburtstage hinzufügen und aktualisieren 

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.
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.