There are a couple of apps like Viber and Skype that export their contacts into the native contact database.
The native contact app then comes along, merges some contacts and for these merged contacts it displays multiple options like "Call", "SMS", "Call via skype" ,"IM via Skype", "Video call via skype".
How can I insert a contact into the native address book in such a way that the native address book app will display an option like "Call via My_Application" ?
I know how to insert a contact programatically, if the name is similar with an existing contact the native address book app even merges them, but there is no option to call via my app.
Thanks.
Ok, so I figured it out eventually.
You will need a SyncAdapter which in tern needs an AccountAuthenticator, more details on this can be found here: http://developer.android.com/training/sync-adapters/index.html
And here is the bear minimum distilled version with some stuff pointed out:
Create an AccountAuthenticator and just stub it out
public class Authenticator extends AbstractAccountAuthenticator {
public Authenticator(Context context) {
super(context);
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public Bundle confirmCredentials(
AccountAuthenticatorResponse response, Account account, Bundle options) {
return null;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
throw new UnsupportedOperationException();
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle loginOptions) throws NetworkErrorException {
throw new UnsupportedOperationException();
}
@Override
public String getAuthTokenLabel(String authTokenType) {
// null means we don't support multiple authToken types
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) {
throw new UnsupportedOperationException();
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle loginOptions) {
return null;
}
}
Create a SyncAdapter and just stub it out
public class SyncAdapter extends AbstractThreadedSyncAdapter {
public SyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
}
}
Make the AccountAuthenticator accessible through a service
public class AuthenticationService extends Service {
private Authenticator mAuthenticator;
@Override
public void onCreate() {
mAuthenticator = new Authenticator(this);
}
@Override
public IBinder onBind(Intent intent) {
return mAuthenticator.getIBinder();
}
}
Make the SyncAdapter accessible through a service
public class SyncService extends Service {
private static final Object sSyncAdapterLock = new Object();
private static SyncAdapter sSyncAdapter = null;
@Override
public void onCreate() {
synchronized (sSyncAdapterLock) {
if (sSyncAdapter == null) {
sSyncAdapter = new SyncAdapter(getApplicationContext(), true);
}
}
}
@Override
public IBinder onBind(Intent intent) {
return sSyncAdapter.getSyncAdapterBinder();
}
}
Declare the two services in the manifest
<service
android:name="com.test.customcontact.AuthenticationService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service
android:name="com.test.customcontact.SyncService"
android:exported="true">
<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>
As you can see we will also have 3 resources in res/xml :
@xml/contacts:
<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.test.customcontact"
android:icon="@drawable/ic_launcher"
android:smallIcon="@drawable/ic_launcher"
android:label="@string/app_name"
/>
<?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.test.customcontact"
android:supportsUploading="false"
android:userVisible="true"
/>
<?xml version="1.0" encoding="utf-8"?>
<ContactsSource
xmlns:android="http://schemas.android.com/apk/res/android">
<ContactsDataKind
android:mimeType="vnd.android.cursor.item/com.sample.call"
android:icon="@drawable/ic_launcher"
android:summaryColumn="data2"
android:detailColumn="data3"/>
</ContactsSource>
In these XMLs we will be interested in android:accountType which should be the app package and android:mimeType which will be the custom mime type we will use to store the contacts that will open our app Also note that
Now for the code:
Create a system account (will appear in settings/accounts) ACCOUNT_TYPE is the same as in the above XMLs
public static final String ACCOUNT_TYPE = "com.test.customcontact";
public static final String ACCOUNT_NAME = "sample";
private void addNewAccount() {
Account newAccount = new Account(ACCOUNT_NAME, ACCOUNT_TYPE);
AccountManager accountManager = (AccountManager) getSystemService(ACCOUNT_SERVICE);
accountManager.addAccountExplicitly(newAccount, null, null);
}
Now we can store a contact that will have a custom mime type and thus a custom option via which our app can be accessed
public void addContact(String name, String lastName) {
ContentResolver resolver = getContentResolver();
resolver.delete(ContactsContract.RawContacts.CONTENT_URI, ContactsContract.RawContacts.ACCOUNT_TYPE + " = ?", new String[] { ACCOUNT_TYPE });
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, ACCOUNT_NAME)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, ACCOUNT_TYPE)
.build());
//Uncomment below code if you want this contact to show up individually as well and not only if it gets matched with another contact
// ops.add(ContentProviderOperation.newInsert(ContactsContract.Settings.CONTENT_URI)
// .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, AccountGeneral.ACCOUNT_NAME)
// .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, AccountGeneral.ACCOUNT_TYPE)
// .withValue(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
// .build());
ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, name)
.withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, lastName)
.build());
ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE, MIMETYPE)
.withValue(ContactsContract.Data.DATA1, 12345)
.withValue(ContactsContract.Data.DATA2, "Call via my app")
.withValue(ContactsContract.Data.DATA3, "Call via my app")
.build());
try {
resolver.applyBatch(ContactsContract.AUTHORITY, ops);
}
catch (Exception e) {
e.printStackTrace();
}
}
The final step is to create an activity to handle the custom mimetype action - the connection is made via a special intent filter in the manifest
<activity
android:name="com.test.customcontact.ViewingActivity"
android:screenOrientation="portrait"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/com.sample.call" />
</intent-filter>
</activity>
Notice that <data android:mimeType is the same as <ContactsDataKind android:mimeType in the contacts.XML
Last but not least the manifest permissions required:
Add AndroidManifest.xml permissions
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.READ_PROFILE" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>