One of the more popular stores of data on your average Android device is the contact list. Ever since Android 2.0, Android tracks contacts across multiple different “accounts”, or sources of contacts. Some may come from your Google account, while others might come from Exchange or other services.
This chapter will walk you through some of the basics for accessing
the contacts on the device. Along the way, we will revisit and expand
upon our knowledge of using a ContentProvider
.
First, we will review the contacts APIs, past and present. We will then demonstrate how you can connect to the contacts engine to let users pick and view contacts… all without your application needing to know much of how contacts work. We will then show how you can query the contacts provider to obtain contacts and some of their details, like email addresses and phone numbers. We wrap by showing how you can invoke a built-in activity to let the user add a new contact, possibly including some data supplied by your application.
In addition, we will take a peek at the CallLog
provider, which, as the
name suggests, gives you access to a log of calls made on the device.
Understanding this chapter requires that you have read these chapters in addition to the core chapters:
Android makes contacts available to you via a complex
ContentProvider
framework, so you can access many facets of a
contact’s data — not just their name, but addresses, phone
numbers, groups, etc. Working with the contacts ContentProvider
set
is simple… only if you have an established pattern to work with.
Otherwise, it may prove somewhat daunting.
The contacts ContentProvider
framework can be found as the set of
ContactsContract
classes and interfaces in the android.provider
package. Unfortunately, there is a dizzying array of inner classes to
ContactsContract
.
Contacts can be broken down into two types: raw and aggregate. Raw
contacts come from a sync provider or are hand-entered by a user.
Aggregate contacts represent the sum of information about an
individual culled from various raw contacts. For example, if your
Exchange sync provider has a contact with an email address of
jdoe@foo.com
, and your Facebook sync provider has a contact with an
email address of jdoe@foo.com
, Android may recognize that those two
raw contacts represent the same person and therefore combine those in
the aggregate contact for the user. The classes relating to raw
contacts usually have Raw
somewhere in their name, and these
normally would be used only by custom sync providers.
The ContactsContract.Contacts
and ContactsContract.Data
classes
represent the “entry points” for the ContentProvider
, allowing you
to query and obtain information on a wide range of different pieces
of information. What is retrievable from these can be found in the
various ContactsContract.CommonDataKinds
series of classes. We will
see examples of these operations later in this chapter.
Prior to Android 2.0, Android had no contact synchronization built
in. As a result, all contacts were in one large pool, whether they
were hand-entered by users or were added via third-party
applications. The API used for this is the Contacts
ContentProvider
.
The Contacts
ContentProvider
still works, as
it is merely deprecated in Android 2.0.1, not removed. In practice,
it has one big limitation: it will only report contacts added directly
to the device (as opposed to ones synchronized from Microsoft Exchange,
Facebook, or other sources). As a result, modern Android apps should
not be using Contacts
in general — use ContactsContract
.
Back in the chapter on resource sets and configurations, we saw a series of examples of handling configuration changes. Those samples allowed the user to pick a contact and view a contact. There, we focused on the configuration change aspect. Here, let’s examine the actual pick and view logic a bit more closely.
When the user picks a contact, we call startActivityForResult()
with an ACTION_PICK
Intent
:
public void pickContact(View v) {
Intent i=
new Intent(Intent.ACTION_PICK,
ContactsContract.Contacts.CONTENT_URI);
startActivityForResult(i, PICK_REQUEST);
}
The Intent
has ContactsContract.Contacts.CONTENT_URI
as its Uri
.
Here, ContactsContract.Contacts.CONTENT_URI
is defined by the Android
SDK and points to the contacts “table” inside the ContactsContract
“database”, as it were. Whether there is really a database or a table involved
is up to the implementation of ContactsContract
, of course.
When we call startActivityForResult()
, Android needs to find an
activity to fulfill this request. However, at the outset, all
it has is an action string and a Uri
. There could be all sorts
of activities on the device that advertise that they can pick
from a collection identified by a Uri
starting with the content
scheme.
To help refine the request, Android asks the ContactsContract
ContentProvider
what the MIME type is for this Uri
. Then, Android
knows an action string, a MIME type, and a Uri
. It so happens that
the contacts apps that ship on Android have an activity that has an
<intent-filter>
that indicates that it can handle ACTION_PICK
of the relevant MIME type from a content
Uri
. And so that is the
activity that the user sees.
The Uri
that we get back in onActivityResult()
not only points
to the contact that the user picked, but also gives us
temporary read access to that contact’s personally identifying
information. In effect, it is as if the normal READ_CONTACTS
permission requirement was suspended, for this one Uri
, for
our app alone. Once our process terminates, we may no longer
have the ability to get at details about that contact via its
Uri
, as this read access is temporary.
As it turns out, the sample app does not take advantage of the temporary read access. Instead, when the user clicks the “View” button, the app just brings up an activity to go view that contact:
public void viewContact(View v) {
startActivity(new Intent(Intent.ACTION_VIEW, contact));
}
Once again, Android has an action string (ACTION_VIEW
) and a Uri
(the one that we got in response to the ACTION_PICK
request). And,
once again, Android asks ContactsContract
for the MIME type of the
data associated with this Uri
, so that the MIME type can help identify
the right activity to handle this request. The contacts app that comes
on the device should have an activity that complies, and so we can view
the contact.
In truth, the only reason why we as developers can count on these
activities existing is because of Google Play Services and the Play Store.
The Compatibility Definition Document (CDD) that manufacturers must
comply with to get Google’s proprietary Android apps requires that
the device ship with apps that fulfill all of the <intent-filter>
elements supported by the apps in the Android Open Source Project (AOSP).
Hence, for devices that legitimately have the Play Store on them,
there should always be an app that offers activities to allow users to
pick and view contacts. However, on devices that do not legitimately
have the Play Store, those activities might not exist. Manufacturers
who avoid Google’s proprietary apps should still aim to comply with
the CDD as much as possible, if they want third-party apps like yours
to work successfully on those devices. However, there is no contractual
requirement that they do, and so, as the saying goes, your mileage may vary
(YMMV).
The preceding example allows you to work with contacts, yet not
actually have any contact data other than a transient Uri
. All else
being equal, it is best to use the contacts system this way, as it
means you do not need any extra permissions that might raise privacy
issues.
Of course, all else is rarely equal.
Your alternative, therefore, is to execute queries against the
contacts ContentProvider
to get actual contact detail data back,
such as names, phone numbers, and email addresses. The
Contacts/Spinners
sample application will demonstrate this
technique.
Since contacts are privileged data, you need certain permissions to
work with them. Specifically, you need the READ_CONTACTS
permission
to query and examine the ContactsContract
content and
WRITE_CONTACTS
to add, modify, or remove contacts from the system.
This only holds true if your code will have access to personally-identifying
information, which is why the Pick
sample above — which just has an
opaque Uri
— does not need any permission.
For example, here is the manifest for the Contacts/Spinners
sample
application:
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.commonsware.android.contacts.spinners"
xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.READ_CONTACTS" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.Apptheme">
<activity
android:name=".ContactSpinners"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
And, since this app has a targetSdkVersion
of 26, we also need to deal
with runtime permissions, as READ_CONTACTS
has a protectionLevel
of
dangerous
, so we need to ask the user for permission at runtime. To that
end, we use the same AbstractPermissionActivity
seen elsewhere in the book
as the base class for our ContactSpinners
activity, so we can delegate
all of the runtime permission logic to AbstractPermissionActivity
.
While the database underlying the ContactsContract
content provider
is private, one can imagine that it has several tables: one for
people, one for their phone numbers, one for their email addresses,
etc. These are tied together by typical database relations, most
likely 1:N, so the phone number and email address tables would have a
foreign key pointing back to the table containing information about
people.
To simplify accessing all of this through the content provider interface, Android pre-joins queries against some of the tables. For example, you can query for phone numbers and get the contact name and other data along with the number — you do not have to do this join operation yourself.
The ContactSpinners
activity has a RecyclerView
along with a Spinner
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Spinner
android:id="@+id/spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawSelectorOnTop="true" />
<android.support.v7.widget.RecyclerView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
In onReady()
of the activity, we load up the Spinner
, plus configure the RecyclerView
:
@Override
public void onReady() {
setContentView(R.layout.main);
Spinner spin=findViewById(R.id.spinner);
spin.setOnItemSelectedListener(this);
ArrayAdapter<String> aa=new ArrayAdapter<String>(this,
android.R.layout.simple_spinner_item,
getResources().getStringArray(R.array.options));
aa.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item);
spin.setAdapter(aa);
RecyclerView rv=findViewById(android.R.id.list);
rv.setLayoutManager(new LinearLayoutManager(this));
rv.addItemDecoration(new DividerItemDecoration(this,
DividerItemDecoration.VERTICAL));
adapter=new RVCursorAdapter(getLayoutInflater());
rv.setAdapter(adapter);
}
In particular, we populate the Spinner
based on a <string-array>
resource from the res/values/arrays.xml
file:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="options">
<item>Contact Names</item>
<item>Contact Names & Numbers</item>
<item>Contact Names & Email Addresses</item>
</string-array>
</resources>
We set up the activity to be the OnItemSelectedListener
for the
Spinner
, which means that we have to implement onItemSelected()
and onNothingSelected()
:
@Override
public void onItemSelected(AdapterView<?> parent,
View v, int position, long id) {
getSupportLoaderManager().initLoader(position, null, this);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
// ignore
}
When the user selects something in the Spinner
— and for the default
selection — we will use the Loader
framework and use a CursorLoader
to query the ContactsContract
ContentProvider
. In this case, though,
we want three different Cursor
values, one for each option in the
Spinner
. That will mean that we need three different CursorLoader
objects. To identify which loader we are going to initialize, we pass in
the position
of the Spinner
to initLoader()
, so the 0/1/2 value
that we get as the position
forms our loader ID.
In onCreateLoader()
of our LoaderCallbacks
, we need to return
a CursorLoader
for whichever loaderId
was passed in. What varies
is the Uri
that we want to query and the “projection” of “columns”
that we want to get back. So, onCreateLoader()
uses a switch
statement to decide what Uri
and projection to use, then creates
a CursorLoader
based upon that:
@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) {
String[] projection;
Uri uri;
switch (loaderId) {
case LOADER_NAMES:
projection=PROJECTION_NAMES;
uri=ContactsContract.Contacts.CONTENT_URI;
break;
case LOADER_NAMES_NUMBERS:
projection=PROJECTION_NUMBERS;
uri=ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
break;
default:
projection=PROJECTION_EMAILS;
uri=ContactsContract.CommonDataKinds.Email.CONTENT_URI;
break;
}
return new CursorLoader(this, uri, projection, null, null,
ContactsContract.Contacts.DISPLAY_NAME);
}
The two case
values are just constants tied to the positions from
the Spinner
, defined as static data members:
private static final int LOADER_NAMES=0;
private static final int LOADER_NAMES_NUMBERS=1;
Similarly, the three projections are defined as static data members:
private static final String[] PROJECTION_NAMES=new String[]{
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
};
private static final String[] PROJECTION_NUMBERS=new String[]{
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER
};
private static final String[] PROJECTION_EMAILS=new String[]{
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Email.DATA
};
For the “names” Spinner
entry, we are going to retrieve the ID and
display name of the contact, using the standard ContactsContract.Contacts.CONTENT_URI
Uri
value.
For the “names and phone numbers” Spinner
entry, we still want the
display name of the contact, but we also want phone numbers. Fortunately,
as mentioned earlier, ContactsContract
denormalizes its data in
response to queries, so we can get the display name of the contact
even when we are querying the “table” of phone numbers, via
ContactsContract.CommonDataKinds.Phone.CONTENT_URI
. The same basic
process holds true for the “names and emails” entry, where we query
ContactsContract.CommonDataKinds.Email.CONTENT_URI
. Note that
we will get somewhat redundant information back — if a contact has
two phone numbers, we get two rows in our Cursor
, both for the
same contact, and one per phone number.
We can sort by DISPLAY_NAME
for all three cases, courtesy of the
aforementioned denormalization of the data.
We also have to implement onLoadFinished()
, to take in the Cursor
that is the result of the query against ContactsContract
and put
the results in the RecyclerView
. Once again, the rendering will differ
a bit based upon whether we are showing just names or names along
with other data (e.g., phone numbers). So, we have another switch
statement, where we determine what columns we want, what layout ID
to use, and what roster of widgets in that layout map to those
columns:
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
String[] columns;
switch (loader.getId()) {
case LOADER_NAMES:
columns=COLUMNS_NAMES;
break;
case LOADER_NAMES_NUMBERS:
columns=COLUMNS_NUMBERS;
break;
default:
columns=COLUMNS_EMAILS;
break;
}
adapter.changeCursor(c, columns);
}
The lists of columns and views are defined as static data members and map positionally (i.e., the first view is for the first column):
private static final String[] COLUMNS_NAMES=new String[]{
ContactsContract.Contacts.DISPLAY_NAME
};
private static final String[] COLUMNS_NUMBERS=new String[]{
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER
};
private static final String[] COLUMNS_EMAILS=new String[]{
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Email.DATA
};
Then, we update a RecyclerView.Adapter
, named RVCursorAdapter
, with
the fresh Cursor
and column list. That adapter, along with a
RecyclerView.ViewHolder
implementation named RowHolder
, populate a
framework-supplied simple_list_item_2
widget with the 1 or 2 columns
that we want from the Cursor
:
private static class RVCursorAdapter extends RecyclerView.Adapter<RowHolder> {
private Cursor cursor;
private final LayoutInflater inflater;
private String[] columns;
private RVCursorAdapter(LayoutInflater inflater) {
this.inflater=inflater;
}
@NonNull
@Override
public RowHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
View row=
inflater.inflate(android.R.layout.simple_list_item_2, parent, false);
return new RowHolder(row);
}
@Override
public void onBindViewHolder(@NonNull RowHolder holder,
int position) {
cursor.moveToPosition(position);
holder.bind(cursor, columns);
}
@Override
public int getItemCount() {
return cursor==null ? 0 : cursor.getCount();
}
private void changeCursor(Cursor cursor, String[] columns) {
if (this.cursor!=null) {
this.cursor.close();
}
this.cursor=cursor;
this.columns=columns;
notifyDataSetChanged();
}
private void clearCursor() {
cursor=null;
notifyDataSetChanged();
}
}
private static class RowHolder extends RecyclerView.ViewHolder {
private final TextView text1;
private final TextView text2;
RowHolder(View itemView) {
super(itemView);
text1=itemView.findViewById(android.R.id.text1);
text2=itemView.findViewById(android.R.id.text2);
}
public void bind(Cursor cursor, String[] columns) {
int index=cursor.getColumnIndex(columns[0]);
text1.setText(cursor.getString(index));
if (columns.length==2) {
index=cursor.getColumnIndex(columns[1]);
text2.setText(cursor.getString(index));
}
}
}
Let’s now take a peek at the reverse direction: adding contacts to the system. This was never particularly easy and now is… well, different.
First, we need to distinguish between sync providers and other apps. Sync providers are the guts underpinning the accounts system in Android, bridging some existing source of contact data to the Android device. Hence, you can have sync providers for Exchange, Facebook, and so forth. These will need to create raw contacts for newly-added contacts to their backing stores that are being sync’d to the device for the first time. Creating sync providers is outside of the scope of this book for now.
It is possible for other applications to create contacts. These, by
definition, will be phone-only contacts, lacking any associated
account, no different than if the user added the contact directly.
The recommended approach to doing this is to collect the data you
want, then spawn an activity to let the user add the contact —
this avoids your application needing the WRITE_CONTACTS
permission
and all the privacy/data integrity issues that creates.
To that end, take a look at the
Contacts/Inserter
sample project.
It defines a simple activity with a two-field UI, with one field
apiece for the person’s first name and phone number:
<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:stretchColumns="1"
>
<TableRow>
<TextView
android:text="First name:"
/>
<EditText android:id="@+id/name"
/>
</TableRow>
<TableRow>
<TextView
android:text="Phone:"
/>
<EditText android:id="@+id/phone"
android:inputType="phone"
/>
</TableRow>
<Button android:id="@+id/insert" android:text="Insert!" />
</TableLayout>
The trivial UI also sports a button to add the contact:
Figure 748: The ContactInserter sample application
When the user clicks the button, the activity gets the data and
creates an Intent
to be used to launch the add-a-contact activity.
This uses the ACTION_INSERT_OR_EDIT
action and a couple of extras
from the ContactsContract.Intents.Insert
class:
package com.commonsware.android.inserter;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Intents.Insert;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class ContactsInserter extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button btn=(Button)findViewById(R.id.insert);
btn.setOnClickListener(onInsert);
}
View.OnClickListener onInsert=new View.OnClickListener() {
public void onClick(View v) {
EditText fld=(EditText)findViewById(R.id.name);
String name=fld.getText().toString();
fld=(EditText)findViewById(R.id.phone);
String phone=fld.getText().toString();
Intent i=new Intent(Intent.ACTION_INSERT_OR_EDIT);
i.setType(Contacts.CONTENT_ITEM_TYPE);
i.putExtra(Insert.NAME, name);
i.putExtra(Insert.PHONE, phone);
startActivity(i);
}
};
}
We also need to set the MIME type on the Intent
via setType()
, to
be CONTENT_ITEM_TYPE
, so Android knows what sort of data we want to
actually insert. Then, we call startActivity()
on the resulting
Intent
. That brings up an add-or-edit activity:
Figure 749: The add-or-edit-a-contact activity
… where if the user chooses “Create new contact”, they are taken to the ordinary add-a-contact activity, with our data pre-filled in:
Figure 750: The edit-contact form, showing the data from the ContactInserter activity
Note that the user could choose an existing contact, rather than
creating a new contact. If they choose an existing contact, the first
name of that contact will be overwritten with the data supplied by
the ContactsInserter
activity, and a new phone number will be added
from those Intent
extras.
A closely-related ContentProvider
to ContactsContract
is CallLog
.
As the name suggests, it contains a log of calls for this device,
including things like the date/time of the call, the call duration, and
the other party on the call (e.g., a phone number).
If you wish to give the user another look at their calls, independent
from the UI available on the device (e.g., Dialer app), you might
wish to query the CallLog
, as we do in the
Contacts/CallLog
sample application
In the beginning, there was no READ_CALL_LOG
permission. To read the CallLog
provider, you needed to hold READ_CONTACTS
.
The reason for the READ_CONTACTS
permission is that the CallLog
denormalizes the data, copying into its own table contact data about
the other party, so that the CallLog
can remain independent
of ContactsContract
.
In API Level 16, though, they added the READ_CALL_LOG
permission. If your
minSdkVersion
is 16 or higher, you can just request READ_CALL_LOG
. If
your minSdkVersion
is lower than that, though, you will want to request
both permissions, to make sure that you are covered. You might consider
using the android:maxSdkVersion
attribute on the READ_CONTACTS
<uses-permission>
element, as you will not need it on newer devices, unless
you are also working with ContactContract
.
Both READ_CALL_LOG
and READ_CONTACTS
are dangerous
permissions, and
therefore if your targetSdkVersion
is 23 or higher, you need to request
those permissions at runtime. Of course, you only need to request the permissions
that you need, and so if your minSdkVersion
is 16 or higher and you are only
using READ_CALL_LOG
, you only need to request READ_CALL_LOG
at runtime.
Our sample app uses the same AbstractPermissionActivity
as did the first
sample app in this chapter, though this time our CallLogConsumerActivity
will request READ_CALL_LOG
instead of READ_CONTACTS
.
The sample app requests the READ_CALL_LOG
permission, so it can
query the CallLog
:
<uses-permission android:name="android.permission.READ_CALL_LOG" />
To request this permission when our app launches, this project uses the same
AbstractPermissionActivity
seen elsewhere in the book and profiled in
the chapter on permissions. That logic gives
us control in an onReady()
method in our CallLogConsumerActivity
when we
can start setting up the UI and requesting the call log data.
In onReady()
— among other bits of work that we will explore shortly –
we call initLoader()
, to query the CallLog
via a CursorLoader
. The activity itself implements the
LoaderManager.LoaderCallbacks
interface needed by initLoader()
, and so
the activity has the three required LoaderCallbacks
methods:
@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) {
return(new CursorLoader(this, CallLog.Calls.CONTENT_URI,
PROJECTION, null, null, CallLog.Calls.DATE
+ " DESC"));
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
adapter.changeCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.changeCursor(null);
}
Here, we retrieve the data from the CallLog.Calls
“table” via its
CONTENT_URI
, asking for the “columns” indicated by the PROJECTION
:
private static final String[] PROJECTION=new String[] {
CallLog.Calls.NUMBER, CallLog.Calls.DATE };
We sort the data descending by date. It would be nice if the documentation
for CallLog
included some indication that this approach was endorsed
and supported. Based on the CallLog
implementation, it should be stable.
onLoadFinished()
and onLoaderReset()
each call a changeCursor()
method on
our adapter
. That is an instance of RVCursorAdapter
, set up in onReady()
:
@Override
public void onReady() {
adapter=new RVCursorAdapter(getLayoutInflater());
RecyclerView rv=getRecyclerView();
setLayoutManager(new LinearLayoutManager(this));
rv.addItemDecoration(new DividerItemDecoration(this,
DividerItemDecoration.VERTICAL));
rv.setAdapter(adapter);
getSupportLoaderManager().initLoader(0, null, this);
}
RVCursorAdapter
wraps around the Cursor
of calendar data:
private static class RVCursorAdapter extends RecyclerView.Adapter<RowHolder> {
private Cursor cursor;
private final LayoutInflater inflater;
private RVCursorAdapter(LayoutInflater inflater) {
this.inflater=inflater;
}
@NonNull
@Override
public RowHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
View row=inflater.inflate(R.layout.row, parent, false);
return new RowHolder(row);
}
@Override
public void onBindViewHolder(@NonNull RowHolder holder,
int position) {
cursor.moveToPosition(position);
holder.bind(cursor);
}
@Override
public int getItemCount() {
return cursor==null ? 0 : cursor.getCount();
}
private void changeCursor(Cursor cursor) {
if (this.cursor!=null) this.cursor.close();
this.cursor=cursor;
notifyDataSetChanged();
}
}
changeCursor()
closes the old Cursor
(if there was one), holds onto the new
one, and tells the RecyclerView
to reload its contents, as our data has changed.
Our row layout (res/layout/row.xml
) has two
TextView
widgets for the two pieces of data that we want to
display:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:textSize="28sp" />
<TextView
android:id="@+id/number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textSize="28sp" />
</RelativeLayout>
Each row in the list is managed by a RowHolder
:
private static class RowHolder extends RecyclerView.ViewHolder {
private final TextView date;
private final TextView number;
RowHolder(View itemView) {
super(itemView);
date=itemView.findViewById(R.id.date);
number=itemView.findViewById(R.id.number);
}
public void bind(Cursor cursor) {
number.setText(cursor.getString(0));
long time=cursor.getLong(1);
String formattedTime=DateUtils.formatDateTime(date.getContext(), time,
DateUtils.FORMAT_ABBREV_RELATIVE);
date.setText(formattedTime);
}
}
The DATE
columns is returned to use as milliseconds since the
Unix epoch, so we use DateUtils.formatDateTime()
to convert that into something
that is human-readable and matches the user’s chosen locale settings.
The results is a list of calls by date and phone number.