The ContactsContract and CallLog Providers

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.

Prerequisites

Understanding this chapter requires that you have read these chapters in addition to the core chapters:

Introducing You to Your Contacts

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.

Organizational Structure

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.

A Look Back at Android 1.6

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.

Pick a Peck of Pickled People

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.

Picking a Contact

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);
  }
(from ConfigChange/Fragments/app/src/main/java/com/commonsware/android/rotation/frag/RotationFragment.java)

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.

Viewing a Contact

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));
  }
(from ConfigChange/Fragments/app/src/main/java/com/commonsware/android/rotation/frag/RotationFragment.java)

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

Spin Through Your Contacts

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.

Contact Permissions

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>
(from Contacts/Spinners/app/src/main/AndroidManifest.xml)

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.

Pre-Joined Data

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 UI

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>
(from Contacts/Spinners/app/src/main/res/layout/main.xml)

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);
  }
(from Contacts/Spinners/app/src/main/java/com/commonsware/android/contacts/spinners/ContactSpinners.java)

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 &amp; Numbers</item>
    <item>Contact Names &amp; Email Addresses</item>
  </string-array>
</resources>
(from Contacts/Spinners/app/src/main/res/values/arrays.xml)

Reacting to the Spinner

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
  }
(from Contacts/Spinners/app/src/main/java/com/commonsware/android/contacts/spinners/ContactSpinners.java)

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.

Loading the Data

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);
  }
(from Contacts/Spinners/app/src/main/java/com/commonsware/android/contacts/spinners/ContactSpinners.java)

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;
(from Contacts/Spinners/app/src/main/java/com/commonsware/android/contacts/spinners/ContactSpinners.java)

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
  };
(from Contacts/Spinners/app/src/main/java/com/commonsware/android/contacts/spinners/ContactSpinners.java)

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.

Showing the Results

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);
  }
(from Contacts/Spinners/app/src/main/java/com/commonsware/android/contacts/spinners/ContactSpinners.java)

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
  };
(from Contacts/Spinners/app/src/main/java/com/commonsware/android/contacts/spinners/ContactSpinners.java)

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));
      }
    }
  }
(from Contacts/Spinners/app/src/main/java/com/commonsware/android/contacts/spinners/ContactSpinners.java)

Makin’ Contacts

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>
(from Contacts/Inserter/app/src/main/res/layout/main.xml)

The trivial UI also sports a button to add the contact:

The ContactInserter sample application
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);
    }
  };
}
(from Contacts/Inserter/app/src/main/java/com/commonsware/android/inserter/ContactsInserter.java)

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:

The add-or-edit-a-contact 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:

The edit-contact form, showing the data from the ContactInserter activity
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.

Looking at the CallLog

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

Pondering Permissions

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.

Contents of CallLog.Calls

The sample app requests the READ_CALL_LOG permission, so it can query the CallLog:

  <uses-permission android:name="android.permission.READ_CALL_LOG" />
(from Contacts/CallLog/app/src/main/AndroidManifest.xml)

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);
  }
(from Contacts/CallLog/app/src/main/java/com/commonsware/android/calllog/consumer/CallLogConsumerActivity.java)

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 };
(from Contacts/CallLog/app/src/main/java/com/commonsware/android/calllog/consumer/CallLogConsumerActivity.java)

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.

Showing the CallLog

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);
  }
(from Contacts/CallLog/app/src/main/java/com/commonsware/android/calllog/consumer/CallLogConsumerActivity.java)

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();
    }
  }
(from Contacts/CallLog/app/src/main/java/com/commonsware/android/calllog/consumer/CallLogConsumerActivity.java)

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>
(from Contacts/CallLog/app/src/main/res/layout/row.xml)

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);
    }
  }
(from Contacts/CallLog/app/src/main/java/com/commonsware/android/calllog/consumer/CallLogConsumerActivity.java)

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.