Ajay Singh

Syncing Contacts with an Android Application

Syncing the phone book contacts in an android device with your application is being used by various android applications today. These applications include Whatsapp, Google+, Skype and various email applications. In this way, contacts syncing allows the user to access contacts from various social applications from the phone book itself.

Now I will explain the workings of the syncing process with an example android application. This android application runs in background as soon as launched and synchronizes itself with the device’s phone book contacts. Following is a screenshot of a contact synced with the app.

contact synced with the app

Clicking on MyData will take us to a desired screen on the application.

Contacts syncing requires three components to work in order which are:-

  1. Contacts Provider
  2. Authenticator service
  3. Sync service

Contacts Provider

Contacts provider is the standard access provider to the device’s data about people. All the contacts in the phone are managed by contacts provider through tables that can be queried to get data. There are basically three tables that store data for a person.

These tables are:-

contacts tables

Figure – 1: Contacts provider table structure (Source: http://developer.android.com/guide/topics/providers/contacts-provider.html)

  1. ContactsContract.Contacts is the table that contains the aggregated data from raw contacts rows that represents different people in the contacts list.
  2. ContactsContract.RawContacts table contains the persons accounts and type data summary.
  3. ContactsContract.Data table contains other details corresponding to each raw contact like email address, phone number etc.

I made use of the ContentProviderOperation to handle the table insertion operations on the table to add a raw contact and the data corresponding to my app and finally to apply the changes to the database as a batch operation.

Here is the sample code for class that handles this functionality:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class ContactsManager {
    private static String MIMETYPE = "vnd.android.cursor.item/com.example.ajay.contacts_4";

    public static void addContact(Context context,MyContact contact){
        ContentResolver resolver = context.getContentResolver();
        ArrayList<ContentProviderOperation> ops = 
                      new ArrayList<ContentProviderOperation>();

        ops.add(ContentProviderOperation
           .newInsert(addCallerIsSyncAdapterParameter(
            ContactsContract.RawContacts.CONTENT_URI, true))
                .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, 
                 Constants.ACCOUNT_NAME)
                .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, 
                 Constants.ACCOUNT_TYPE)
                .withValue(ContactsContract.RawContacts.AGGREGATION_MODE, 
                 ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT)
                .build());

        ops.add(ContentProviderOperation
           .newInsert(addCallerIsSyncAdapterParameter(
             ContactsContract.Data.CONTENT_URI, true))
                .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
                .withValue(ContactsContract.Data.MIMETYPE, 
                 ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
                .withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, 
                 contact.name)
                .build());

        ops.add(ContentProviderOperation
           .newInsert(addCallerIsSyncAdapterParameter(
             ContactsContract.Data.CONTENT_URI, true))
                .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID,0)
                .withValue(ContactsContract.Data.MIMETYPE, MIMETYPE)
                .withValue(ContactsContract.Data.DATA1, 12345)
                .withValue(ContactsContract.Data.DATA2, "user")
                .withValue(ContactsContract.Data.DATA3, "MyData")
                .build());

        try {
            resolver.applyBatch(ContactsContract.AUTHORITY, ops);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static Uri addCallerIsSyncAdapterParameter(Uri uri, 
                                     boolean isSyncOperation) {
        if (isSyncOperation) {
            return uri.buildUpon()
            .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER,
             "true").build();
        }
        return uri;
    }
}

 

Authenticator Service

The autheticator service is used to create an account for our app on the device. It plugs into the android accounts and authentication framework through which an account type is created on the device corresponding to our app. Accounts for standard social applications like Whatsapp, Skype, Facebook etc. can be found if you visit the accounts screen in settings on your device.

Here is the code for the authenticator class:-

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class Authenticator extends AbstractAccountAuthenticator {

    private final Context mContext;

    public Authenticator(Context context) {
        super(context);
        mContext = context;
    }

    @Override
    public Bundle editProperties(AccountAuthenticatorResponse response, 
           String accountType) {
        return null;
    }

    @Override
    public Bundle addAccount(AccountAuthenticatorResponse response, 
           String accountType, String authTokenType, String[] requiredFeatures, 
           Bundle options) throws NetworkErrorException {
        final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);

        final Bundle bundle = new Bundle();
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
        return bundle;
    }

    @Override
    public Bundle confirmCredentials(AccountAuthenticatorResponse response, 
           Account account, Bundle options) throws NetworkErrorException {
        return null;
    }

    @Override
    public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, 
          String authTokenType, Bundle options) throws NetworkErrorException {
        final Bundle result = new Bundle();
        result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
        result.putString(AccountManager.KEY_ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
        return result;
    }

    @Override
    public String getAuthTokenLabel(String authTokenType) {
        return null;
    }

    @Override
    public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, 
          String authTokenType, Bundle options) throws NetworkErrorException {
        return null;
    }

    @Override
    public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, 
          String[] features) throws NetworkErrorException {
        return null;
    }
}

 

Authenticator.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>

<account-authenticator 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="com.example.ajay.contacts_4"
    android:icon="@mipmap/ic_launcher"
    android:smallIcon="@mipmap/ic_launcher"
    android:label="@string/app_name" />

 

The authenticator service:-

1
2
3
4
5
6
7
8
9
<service android:name=".AuthenticationService" >
  <intent-filter>
       <action android:name="android.accounts.AccountAuthenticator" />
  </intent-filter>

  <meta-data
       android:name="android.accounts.AccountAuthenticator"
       android:resource="@xml/authenticator" />
</service>

 

Sync Service

The sync service is made up of two parts the sync adapter and the sync service itself. The sync adapter is used to synchronize data between the server and the local database. My app doesn’t fetch any data from server but still it is required to impement the sync adapter. The sync service is what binds the sync adapter to the android sync framework.

Here is the sample code for Sync adapter:-

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class SyncAdapter extends AbstractThreadedSyncAdapter {

    public SyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
        Log.I("Sync Adapter created.");
    }

    @Override
    public void onPerformSync(Account account, Bundle extras, 
          String authority, ContentProviderClient provider, 
          SyncResult syncResult) {
        Log.I("Sync Adapter called.");
    }
}

 

The sync service:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SyncService extends Service {

    private static final Object sSyncAdapterLock = new Object();
    private static SyncAdapter mSyncAdapter = null;

    @Override
    public void onCreate() {
        Log.I("Sync Service created.");
        synchronized (sSyncAdapterLock){
            if(mSyncAdapter == null){
                mSyncAdapter = new SyncAdapter(getApplicationContext(),
                                          true);
            }
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.I("Sync Service binded.");
        return mSyncAdapter.getSyncAdapterBinder();
    }
}

 

SyncAdapter.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>

<sync-adapter 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:contentAuthority="com.android.contacts"
    android:accountType="com.example.ajay.contacts_4"
    android:supportsUploading="false"
    android:userVisible="true" />

 

The sync service manifest:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<service android:name=".SyncService" >
   <intent-filter>
        <action android:name="android.content.SyncAdapter" />
   </intent-filter>

   <meta-data
        android:name="android.content.SyncAdapter"
        android:resource="@xml/syncadapter" />
   <meta-data
        android:name="android.provider.CONTACTS_STRUCTURE"
        android:resource="@xml/contacts" />
</service>

 

The authenticator service runs in the background when the app is launched to create an account for the app on the device. The sync service is called in a peroidic cycle to constantly sync the data.

Here is the main activity code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class MainActivity extends AppCompatActivity {

    private ArrayList<String> mNames = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

//      Retrieve names from phone's contact list and save in mNames
        getNames();

//      Apply changes to phone's contact list
        new Thread(new Runnable() {
            @Override
            public void run() {
                String name;
                for(int i=0;i<mNames.size();i++){
                    name = mNames.get(i);
                    ContactsManager.addContact(MainActivity.this, new MyContact(name));
                }
            }
        }).start();
    }

    private void getNames(){
        int hasPhone;
        Cursor c = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI,
                   null,null,null,null);
        if((c != null) && c.moveToFirst()){
            while(c.moveToNext()){
                hasPhone = Integer.parseInt(c.getString(
                c.getColumnIndexOrThrow(ContactsContract.Contacts.HAS_PHONE_NUMBER)));
                if(hasPhone == 1)
                    mNames.add(c.getString(
                    c.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME)));
            }
            c.close();
        }
    }
}

 

When you tap on the Whatsapp icon on the contacts screen, it will take you to the chat screen of that contact. Similarly in the phonebook contacts your app icon can be seen which is clickable to access the app screens that correspond to that contacts information on the app. The contact data can be sent through the intent extras to the activity that launches.

References:-
1. developer.android.com
2. http://chariotsolutions.com