效果展示
方案概述
如何在联系人中添加自己 app 的按钮
- 首先你需要一个 Android 系统的账户,这个在系统设置中可以找到,你会发现你的手机已经有很多其他 app 的账户存在了,比如微信,支付宝,twitter 等。
- 然后在这个账户下添加你的联系人信息,这里你可以声明你的联系人结构,告诉系统它有一个按钮,对应的 action 是
xxx
,然后你写一个可以接收 action
为 xxx
的 Activity
,就能够在点击按钮后跳转到这个 Activity
了。
简单来说就是上面这两个步骤,具体实现可以看下面的代码实现。
如何将自己 app 的按钮添加到通讯录已存在的联系人中
在使用 telegram 时,我们会发现自己的一些通讯录联系人被添加上了 telegram 的按钮。效果如图:
实际上这并不是 telegram 将自己的按钮塞到了通讯录中,而是 Android 系统判断,telegram 添加进来的这个人,手机号以及姓名,都和通讯录中已有的一个人相同,因此自动将两个联系人合并显示了。 本质上还是两个联系人,你也可以选择让这两个联系人分开独立显示,只需要在联系人详情页取消他们的关联即可。另外,如果你卸载了 telegram,你会发现那个按钮也会消失,因为那个有按钮的联系人随着 telegram 的卸载被删除了。
代码实现
嫌麻烦的同学可以直接看源码:
https://github.com/yueban/CustomContact
1. 添加系统级账户
添加账户认证服务 AuthenticatorService
,继承于 Service
。
1 2 3 4 5 6 7 8 9 10 11 12
| <service android:name=".account.AuthenticatorService" android:exported="true"> <intent-filter> <action android:name="android.accounts.AccountAuthenticator" /> </intent-filter> <meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/auth" /> </service>
|
声明你要创建的账户信息 xml/auth
,注意这里的 android:accountType
,之后创建账户,删除账户,以及在账户下添加联系人,都需要与这里的 accountType
一致
1 2 3 4 5 6
| <?xml version="1.0" encoding="utf-8"?> <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" android:accountType="@string/account_type" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:smallIcon="@mipmap/ic_launcher" />
|
在 AuthenticatorService
的 onBind
方法中,返回一个账户认证对象 Authenticator
,Authenticator
继承于 AbstractAccountAuthenticator
。
1 2 3 4 5 6 7
| override fun onBind(intent: Intent): IBinder? { return if (AccountManager.ACTION_AUTHENTICATOR_INTENT == intent.action) { Authenticator(this).iBinder } else { null } }
|
添加必要的权限(不需要在 app 中动态申请)
1 2
| <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
然后就可以创建你自己 app 的系统级账户了。注意,这里的 account_type
必须和 xml/auth
中声明的一致。
1 2
| val account = Account("account_name", "account_type") AccountManager.get(context).addAccountExplicitly(account, "", null)
|
进入系统设置 → 账户,应该可以看到一个名为 account_name
的账户,这就是我们刚刚创建的。
这部分源码对应的 commit 为:
https://github.com/yueban/CustomContact/commit/4ce5e365cd4670a0517e162e79234014808ce370
2. 向这个系统级账户中添加联系人信息
添加联系人同步服务 ContactsSyncAdapterService
,继承于 Service
。
1 2 3 4 5 6 7 8 9 10 11 12 13
| <service android:name=".contact.ContactsSyncAdapterService" android:exported="true"> <intent-filter> <action android:name="android.content.SyncAdapter" /> </intent-filter>
<meta-data android:name="android.content.SyncAdapter" android:resource="@xml/sync_contacts" /> </service>
|
声明同步服务的类型 xml/sync_contacts
,我们使用联系人同步,因此是 com.android.contacts
。注意,这里的 account_type
必须和 xml/auth
中声明的一致。
1 2 3
| <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" android:accountType="@string/account_type" android:contentAuthority="com.android.contacts" />
|
在 ContactsSyncAdapterService
的 onBind
方法中,返回一个联系人同步对象 SyncAdapterImpl
,SyncAdapterImpl
继承于 AbstractThreadedSyncAdapter
。
1 2 3
| override fun onBind(intent: Intent?): IBinder { return SyncAdapterImpl(this).syncAdapterBinder }
|
声明必要的权限,其中 联系人读写权限需要在 app 中动态申请。
1 2 3 4 5
| <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" /> <uses-permission android:name="android.permission.READ_CONTACTS" />
|
然后,我们就可以向第 1 步创建的账户中,插入联系人信息了。注意,这里的 account_name
, account_type
必须与第 1 步 addAccountExplicitly
时使用的一致。 (也就是 account_type
必须与 xml/auth
中声明的一致)
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 53 54 55 56 57 58 59 60
| val ops = ArrayList<ContentProviderOperation>()
ops.add( ContentProviderOperation.newInsert( addCallerIsSyncAdapterParameter( ContactsContract.RawContacts.CONTENT_URI ) ).withValue( ContactsContract.RawContacts.ACCOUNT_NAME, "account_name" ).withValue( ContactsContract.RawContacts.ACCOUNT_TYPE, "account_type" ).build() )
ops.add( ContentProviderOperation.newInsert( addCallerIsSyncAdapterParameter( ContactsContract.Settings.CONTENT_URI ) ).withValue( ContactsContract.RawContacts.ACCOUNT_NAME, "account_name" ).withValue( ContactsContract.RawContacts.ACCOUNT_TYPE, "account_type" ).withValue(ContactsContract.Settings.UNGROUPED_VISIBLE, 1).build() )
ops.add( ContentProviderOperation.newInsert( addCallerIsSyncAdapterParameter( 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.DISPLAY_NAME, "Bob" ).build() )
ops.add( ContentProviderOperation.newInsert( addCallerIsSyncAdapterParameter( ContactsContract.Data.CONTENT_URI ) ).withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0).withValue( ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE ).withValue( ContactsContract.CommonDataKinds.Phone.NUMBER, "010-1234" ).build() )
context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
打开系统通讯录 app,应该可以看到一个名为 Bob
,电话为 010-1234
的联系人,进入这个联系人的详情页,滑到最底部,应该可以看到这个联系人来自我们自己创建的账户。
这部分源码对应的 commit 为:
https://github.com/yueban/CustomContact/commit/3177c9fd9aef9c2f424e7138f23776198230ba4d
3. 联系人中添加自己 app 的按钮
首先我们需要声明自己账户下联系人独有的数据类型。在联系人同步服务 ContactsSyncAdapterService
中声明自己账户下联系人的数据结构。
1 2 3 4 5 6 7 8 9
| <service android:name=".contact.ContactsSyncAdapterService" android:exported="true"> ... <meta-data android:name="android.provider.CONTACTS_STRUCTURE" android:resource="@xml/contact_structure" /> ... </service>
|
声明自己账户下联系人的数据结构 xml/contact_structure
。这里我们声明了两个自己联系人独有的字段,其中 mimeType
为字段的唯一标识,而 detailColumn
则是这个字段在联系人详情页的显示的内容。(data2
是 Android
系统中预定好的自定义字段名,往后看就会知道它是如何工作的)
这里的两个 mimeType
分别为:
vnd.android.cursor.item/vnd.com.yueban.customcontact.message
vnd.android.cursor.item/vnd.com.yueban.customcontact.call
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?xml version="1.0" encoding="utf-8"?> <ContactsSource xmlns:android="http://schemas.android.com/apk/res/android"> <ContactsDataKind android:detailColumn="data2" android:detailSocialSummary="true" android:icon="@mipmap/ic_launcher" android:mimeType="vnd.android.cursor.item/vnd.com.yueban.customcontact.message" android:summaryColumn="data1" /> <ContactsDataKind android:detailColumn="data2" android:detailSocialSummary="true" android:icon="@mipmap/ic_launcher" android:mimeType="vnd.android.cursor.item/vnd.com.yueban.customcontact.call" android:summaryColumn="data1" /> </ContactsSource>
|
然后,我们就可以在插入联系人时,设置我们刚刚添加的两个字段了。注意,这里的 ContactsContract.Data.MIMETYPE
必须和我们在 xml/contact_structure
中定义的 mimeType
相同。 而我们为 ContactsContract.Data.DATA2
设置的值,会被 detailColumn
引用到,最终会显示在联系人详情页中。
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
| val ops = ArrayList<ContentProviderOperation>()
ops.add( ContentProviderOperation.newInsert( addCallerIsSyncAdapterParameter( ContactsContract.Data.CONTENT_URI ) ).withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0).withValue( ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.yueban.customcontact.message" ).withValue(ContactsContract.Data.DATA1, phoneNumber).withValue( ContactsContract.Data.DATA2, "message: 010-1234" ).build() )
ops.add( ContentProviderOperation.newInsert( addCallerIsSyncAdapterParameter(ContactsContract.Data.CONTENT_URI) ) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) .withValue( ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.yueban.customcontact.call" ) .withValue(ContactsContract.Data.DATA1, phoneNumber) .withValue( ContactsContract.Data.DATA2, "call: 010-1234" ) .build() )
|
我们再次打开 Bob
这个人的详情页,就会看到多了两个按钮,其中一个为 message: 010-1234
,另一个为 message: 010-1234
。
最后,我们添加一个 DetailActivity
用于接收这两个按钮点击的跳转事件。通过声明字段对应的 mimeType
,即可接收到按钮点击后跳转的事件。
1 2 3 4 5 6 7 8 9 10 11
| <activity android:name=".DetailActivity"> <intent-filter tools:ignore="AppLinkUrlError"> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/vnd.com.yueban.customcontact.message" /> <data android:mimeType="vnd.android.cursor.item/vnd.com.yueban.customcontact.call" /> </intent-filter> </activity>
|
在 DetailActivity
中处理 Intent
,判断是从哪个按钮跳转过来的。
1 2 3 4 5 6 7 8
| when (intent.type) { CustomContactManager.CONTACT_MIME_TYPE_MESSAGE -> { text_view.text = "message from contact" } CustomContactManager.CONTACT_MIME_TYPE_CALL -> { text_view.text = "call from contact" } }
|
这里的 intent.type
即对应我们插入联系人时设置的 ``ContactsContract.Data.MIMETYPE,也就是
xml/contact_structure中字段定义的
mimeType`。
最后再运行一下 app,点击 Bob
的 message: 010-1234
按钮,即可跳转到 DetailActivity
。
这部分源码对应的 commit 为:
https://github.com/yueban/CustomContact/commit/1db65e07670c1144b735b073ef66cd03668ea5e6
参考
- Create a custom account type
- Android account, sync adapter, and contacts contract database to link to your app
- SampleContacts
- Set a custom contact item in android address book
- Telegram
- How does Android contact linking work?