Xamarin.Forms在国内的开发者相对较少,除了官方的文档之外,网上很少有的,5分钟搞明白Xamarin.Forms读取Android和iOS通讯录及源码放送这篇文章是Xamarin.Forms读取通讯录的实战代码。
一、功能说明
完整思维导图:https://github.com/dotnet9/TerminalMACS/blob/master/docs/TerminalMACS.xmind
本文介绍图中右侧画红圈处的功能,即使用Xamarin.Forms获取和展示Android和iOS的通讯录信息,下面是最终效果,由于使用的是真实手机,所以联系人姓名及电话号码打码显示。
并简单的进行了搜索功能处理,之所以说简单,是因为通讯录列表是全部读取出来了,搜索是直接从此列表进行过滤的。
下图来自:https://www.xamboy.com/2019/10/10/getting-phone-contacts-in-xamarin-forms/,本功能是参考此文所写,所以直接引用文中的图片。
二、代码实现
1、共享库工程创建联系人实体类:Contacts.cs
namespace TerminalMACS.Clients.App.Models { /// <summary> /// 通讯录 /// </summary> public class Contact { /// <summary> /// 获取或者设置名称 /// </summary> public string Name { get; set; } /// <summary> /// 获取或者设置 头像 /// </summary> public string Image { get; set; } /// <summary> /// 获取或者设置 邮箱地址 /// </summary> public string[] Emails { get; set; } /// <summary> /// 获取或者设置 手机号码 /// </summary> public string[] PhoneNumbers { get; set; } } }
2、共享库创建通讯录服务接口:IContactsService.cs
包括:
- 一个通讯录获取请求接口:RetrieveContactsAsync
- 一个读取一条通讯结果通知事件:OnContactLoaded
using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using TerminalMACS.Clients.App.Models; namespace TerminalMACS.Clients.App.Services { /// <summary> /// 通讯录事件参数 /// </summary> public class ContactEventArgs:EventArgs { public Contact Contact { get; } public ContactEventArgs(Contact contact) { Contact = contact; } } /// <summary> /// 通讯录服务接口,android和iOS终端具体的通讯录获取服务需要继承此接口 /// </summary> public interface IContactsService { /// <summary> /// 读取一条数据通知 /// </summary> event EventHandler<ContactEventArgs> OnContactLoaded; /// <summary> /// 是否正在加载 /// </summary> bool IsLoading { get; } /// <summary> /// 尝试获取所有通讯录 /// </summary> /// <param name="token"></param> /// <returns></returns> Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? token = null); } }
3、iOS工程中添加通讯录服务,实现IContactsService接口
using Contacts; using Foundation; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using TerminalMACS.Clients.App.Models; using TerminalMACS.Clients.App.Services; namespace TerminalMACS.Clients.App.iOS.Services { /// <summary> /// 通讯录获取服务 /// </summary> public class ContactsService : NSObject, IContactsService { const string ThumbnailPrefix = "thumb"; bool requestStop = false; public event EventHandler<ContactEventArgs> OnContactLoaded; bool _isLoading = false; public bool IsLoading => _isLoading; /// <summary> /// 异步请求权限 /// </summary> /// <returns></returns> public async Task<bool> RequestPermissionAsync() { var status = CNContactStore.GetAuthorizationStatus(CNEntityType.Contacts); Tuple<bool, NSError> authotization = new Tuple<bool, NSError>(status == CNAuthorizationStatus.Authorized, null); if (status == CNAuthorizationStatus.NotDetermined) { using (var store = new CNContactStore()) { authotization = await store.RequestAccessAsync(CNEntityType.Contacts); } } return authotization.Item1; } /// <summary> /// 异步请求通讯录,此方法由界面真正调用 /// </summary> /// <param name="cancelToken"></param> /// <returns></returns> public async Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? cancelToken = null) { requestStop = false; if (!cancelToken.HasValue) cancelToken = CancellationToken.None; // 我们创建了一个十进制的TaskCompletionSource var taskCompletionSource = new TaskCompletionSource<IList<Contact>>(); // 在cancellationToken中注册lambda cancelToken.Value.Register(() => { // 我们收到一条取消消息,取消TaskCompletionSource.Task requestStop = true; taskCompletionSource.TrySetCanceled(); }); _isLoading = true; var task = LoadContactsAsync(); // 等待两个任务中的第一个任务完成 var completedTask = await Task.WhenAny(task, taskCompletionSource.Task); _isLoading = false; return await completedTask; } /// <summary> /// 异步加载通讯录,具体的通讯录读取方法 /// </summary> /// <returns></returns> async Task<IList<Contact>> LoadContactsAsync() { IList<Contact> contacts = new List<Contact>(); var hasPermission = await RequestPermissionAsync(); if (hasPermission) { NSError error = null; var keysToFetch = new[] { CNContactKey.PhoneNumbers, CNContactKey.GivenName, CNContactKey.FamilyName, CNContactKey.EmailAddresses, CNContactKey.ImageDataAvailable, CNContactKey.ThumbnailImageData }; var request = new CNContactFetchRequest(keysToFetch: keysToFetch); request.SortOrder = CNContactSortOrder.GivenName; using (var store = new CNContactStore()) { var result = store.EnumerateContacts(request, out error, new CNContactStoreListContactsHandler((CNContact c, ref bool stop) => { string path = null; if (c.ImageDataAvailable) { path = path = Path.Combine(Path.GetTempPath(), $"{ThumbnailPrefix}-{Guid.NewGuid()}"); if (!File.Exists(path)) { var imageData = c.ThumbnailImageData; imageData?.Save(path, true); } } var contact = new Contact() { Name = string.IsNullOrEmpty(c.FamilyName) ? c.GivenName : $"{c.GivenName} {c.FamilyName}", Image = path, PhoneNumbers = c.PhoneNumbers?.Select(p => p?.Value?.StringValue).ToArray(), Emails = c.EmailAddresses?.Select(p => p?.Value?.ToString()).ToArray(), }; if (!string.IsNullOrWhiteSpace(contact.Name)) { OnContactLoaded?.Invoke(this, new ContactEventArgs(contact)); contacts.Add(contact); } stop = requestStop; })); } } return contacts; } } }
4、在iOS工程中的Info.plist文件添加通讯录权限使用说明
5、在Android工程中添加读取通讯录权限配置:AndroidManifest.xml
<uses-permission android:name="android.permission.READ_CONTACTS"/>
完整权限配置如下
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.terminalmacs.clients.app"> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" /> <application android:label="TerminalMACS.Clients.App.Android"></application> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.READ_CONTACTS"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> </manifest>
6、在Android工程中添加通讯录服务,实现IContactServer接口:ContactsService.cs
using Acr.UserDialogs; using Android; using Android.App; using Android.Content; using Android.Content.PM; using Android.Database; using Android.Provider; using Android.Runtime; using Android.Support.V4.App; using Plugin.CurrentActivity; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using TerminalMACS.Clients.App.Models; using TerminalMACS.Clients.App.Services; namespace TerminalMACS.Clients.App.Droid.Services { /// <summary> /// 通讯录获取服务 /// </summary> public class ContactsService : IContactsService { const string ThumbnailPrefix = "thumb"; bool stopLoad = false; static TaskCompletionSource<bool> contactPermissionTcs; public string TAG { get { return "MainActivity"; } } bool _isLoading = false; public bool IsLoading => _isLoading; //权限请求状态码 public const int RequestContacts = 1239; /// <summary> /// 获取通讯录需要的请求权限 /// </summary> static string[] PermissionsContact = { Manifest.Permission.ReadContacts }; public event EventHandler<ContactEventArgs> OnContactLoaded; /// <summary> /// 异步请求通讯录权限 /// </summary> async void RequestContactsPermissions() { //检查是否可以弹出申请读、写通讯录权限 if (ActivityCompat.ShouldShowRequestPermissionRationale(CrossCurrentActivity.Current.Activity, Manifest.Permission.ReadContacts) || ActivityCompat.ShouldShowRequestPermissionRationale(CrossCurrentActivity.Current.Activity, Manifest.Permission.WriteContacts)) { // 如果未授予许可,请向用户提供其他理由用户将从使用权限的附加上下文中受益。 // 例如,如果请求先前被拒绝。 await UserDialogs.Instance.AlertAsync("通讯录权限", "此操作需要“通讯录”权限", "确定"); } else { // 尚未授予通讯录权限。直接请求这些权限。 ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, PermissionsContact, RequestContacts); } } /// <summary> /// 收到用户响应请求权限操作后的结果 /// </summary> /// <param name="requestCode"></param> /// <param name="permissions"></param> /// <param name="grantResults"></param> public static void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults) { if (requestCode == RequestContacts) { // 我们请求了多个通讯录权限,因此需要检查相关的所有权限 if (PermissionUtil.VerifyPermissions(grantResults)) { // 已授予所有必需的权限,显示联系人片段。 contactPermissionTcs.TrySetResult(true); } else { contactPermissionTcs.TrySetResult(false); } } } /// <summary> /// 异步请求权限 /// </summary> /// <returns></returns> public async Task<bool> RequestPermissionAsync() { contactPermissionTcs = new TaskCompletionSource<bool>(); // 验证是否已授予所有必需的通讯录权限。 if (Android.Support.V4.Content.ContextCompat.CheckSelfPermission(CrossCurrentActivity.Current.Activity, Manifest.Permission.ReadContacts) != (int)Permission.Granted || Android.Support.V4.Content.ContextCompat.CheckSelfPermission(CrossCurrentActivity.Current.Activity, Manifest.Permission.WriteContacts) != (int)Permission.Granted) { // 尚未授予通讯录权限。 RequestContactsPermissions(); } else { // 已授予通讯录权限。 contactPermissionTcs.TrySetResult(true); } return await contactPermissionTcs.Task; } /// <summary> /// 异步请求通讯录,此方法由界面真正调用 /// </summary> /// <param name="cancelToken"></param> /// <returns></returns> public async Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? cancelToken = null) { stopLoad = false; if (!cancelToken.HasValue) cancelToken = CancellationToken.None; // 我们创建了一个十进制的TaskCompletionSource var taskCompletionSource = new TaskCompletionSource<IList<Contact>>(); // 在cancellationToken中注册lambda cancelToken.Value.Register(() => { // 我们收到一条取消消息,取消TaskCompletionSource.Task stopLoad = true; taskCompletionSource.TrySetCanceled(); }); _isLoading = true; var task = LoadContactsAsync(); // 等待两个任务中的第一个任务完成 var completedTask = await Task.WhenAny(task, taskCompletionSource.Task); _isLoading = false; return await completedTask; } /// <summary> /// 异步加载通讯录,具体的通讯录读取方法 /// </summary> /// <returns></returns> async Task<IList<Contact>> LoadContactsAsync() { IList<Contact> contacts = new List<Contact>(); var hasPermission = await RequestPermissionAsync(); if (!hasPermission) { return contacts; } var uri = ContactsContract.Contacts.ContentUri; var ctx = Application.Context; await Task.Run(() => { // 暂时只请求通讯录Id、DisplayName、PhotoThumbnailUri,可以扩展 var cursor = ctx.ApplicationContext.ContentResolver.Query(uri, new string[] { ContactsContract.Contacts.InterfaceConsts.Id, ContactsContract.Contacts.InterfaceConsts.DisplayName, ContactsContract.Contacts.InterfaceConsts.PhotoThumbnailUri }, null, null, $"{ContactsContract.Contacts.InterfaceConsts.DisplayName} ASC"); if (cursor.Count > 0) { while (cursor.MoveToNext()) { var contact = CreateContact(cursor, ctx); if (!string.IsNullOrWhiteSpace(contact.Name)) { // 读取出一条,即通知界面展示 OnContactLoaded?.Invoke(this, new ContactEventArgs(contact)); contacts.Add(contact); } if (stopLoad) break; } } }); return contacts; } /// <summary> /// 读取一条通讯录数据 /// </summary> /// <param name="cursor"></param> /// <param name="ctx"></param> /// <returns></returns> Contact CreateContact(ICursor cursor, Context ctx) { var contactId = GetString(cursor, ContactsContract.Contacts.InterfaceConsts.Id); var numbers = GetNumbers(ctx, contactId); var emails = GetEmails(ctx, contactId); var uri = GetString(cursor, ContactsContract.Contacts.InterfaceConsts.PhotoThumbnailUri); string path = null; if (!string.IsNullOrEmpty(uri)) { try { using (var stream = Android.App.Application.Context.ContentResolver.OpenInputStream(Android.Net.Uri.Parse(uri))) { path = Path.Combine(Path.GetTempPath(), $"{ThumbnailPrefix}-{Guid.NewGuid()}"); using (var fstream = new FileStream(path, FileMode.Create)) { stream.CopyTo(fstream); fstream.Close(); } stream.Close(); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } } var contact = new Contact { Name = GetString(cursor, ContactsContract.Contacts.InterfaceConsts.DisplayName), Emails = emails, Image = path, PhoneNumbers = numbers, }; return contact; } /// <summary> /// 读取联系人电话号码 /// </summary> /// <param name="ctx"></param> /// <param name="contactId"></param> /// <returns></returns> string[] GetNumbers(Context ctx, string contactId) { var key = ContactsContract.CommonDataKinds.Phone.Number; var cursor = ctx.ApplicationContext.ContentResolver.Query( ContactsContract.CommonDataKinds.Phone.ContentUri, null, ContactsContract.CommonDataKinds.Phone.InterfaceConsts.ContactId + " = ?", new[] { contactId }, null ); return ReadCursorItems(cursor, key)?.ToArray(); } /// <summary> /// 读取联系人邮箱地址 /// </summary> /// <param name="ctx"></param> /// <param name="contactId"></param> /// <returns></returns> string[] GetEmails(Context ctx, string contactId) { var key = ContactsContract.CommonDataKinds.Email.InterfaceConsts.Data; var cursor = ctx.ApplicationContext.ContentResolver.Query( ContactsContract.CommonDataKinds.Email.ContentUri, null, ContactsContract.CommonDataKinds.Email.InterfaceConsts.ContactId + " = ?", new[] { contactId }, null); return ReadCursorItems(cursor, key)?.ToArray(); } IEnumerable<string> ReadCursorItems(ICursor cursor, string key) { while (cursor.MoveToNext()) { var value = GetString(cursor, key); yield return value; } cursor.Close(); } string GetString(ICursor cursor, string key) { return cursor.GetString(cursor.GetColumnIndex(key)); } } }
需要添加 Plugin.CurrentActivity 和 Acr.UserDialogs 包。
7、Android工程添加权限处理判断类
Permission.Util.cs
using Android.Content.PM; namespace TerminalMACS.Clients.App.Droid { public static class PermissionUtil { /** * 通过验证给定数组中的每个条目的值是否为Permission.Granted,检查是否已授予所有给定权限。 * * See Activity#onRequestPermissionsResult (int, String[], int[]) */ public static bool VerifyPermissions(Permission[] grantResults) { // 必须至少检查一个结果. if (grantResults.Length < 1) return false; // 验证是否已授予每个必需的权限,否则返回false. foreach (Permission result in grantResults) { if (result != Permission.Granted) { return false; } } return true; } } }
MainActivity.OnRequestPermissionResult是权限申请结果处理函数,在此函数中调用ContactsService.OnRequestPermissionsResult通知通讯录服务权限处理结果。
MainActivity.cs
using Acr.UserDialogs; using Android.App; using Android.Content.PM; using Android.OS; using Android.Runtime; using TerminalMACS.Clients.App.Droid.Services; using TerminalMACS.Clients.App.Services; namespace TerminalMACS.Clients.App.Droid { [Activity(Label = "TerminalMACS.Clients.App", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity { IContactsService contactsService = new ContactsService(); protected override void OnCreate(Bundle savedInstanceState) { TabLayoutResource = Resource.Layout.Tabbar; ToolbarResource = Resource.Layout.Toolbar; base.OnCreate(savedInstanceState); Xamarin.Essentials.Platform.Init(this, savedInstanceState); global::Xamarin.Forms.Forms.Init(this, savedInstanceState); UserDialogs.Init(() => this); // 将通讯录服务实例传递给共享库,由共享库使用读取通讯录接口 LoadApplication(new App(contactsService)); } public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults) { Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); // 通讯录服务处理权限请求结果 ContactsService.OnRequestPermissionsResult(requestCode, permissions, grantResults); base.OnRequestPermissionsResult(requestCode, permissions, grantResults); } } }
8、创建通讯录ViewModel,并使用通讯录服务
using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using System.Windows.Input; using TerminalMACS.Clients.App.Models; using TerminalMACS.Clients.App.Services; using Xamarin.Forms; namespace TerminalMACS.Clients.App.ViewModels { /// <summary> /// 通讯录ViewModel /// </summary> public class ContactViewModel : BaseViewModel { /// <summary> /// 通讯录服务接口 /// </summary> IContactsService _contactService; /// <summary> /// 标题 /// </summary> public new string Title => "通讯录"; private string _SearchText; /// <summary> /// 搜索关键字 /// </summary> public string SearchText { get { return _SearchText; } set { SetProperty(ref _SearchText, value); } } /// <summary> /// 通讯录搜索命令 /// </summary> public ICommand RaiseSearchCommand { get; } /// <summary> /// 通讯录列表 /// </summary> public ObservableCollection<Contact> Contacts { get; set; } private List<Contact> _FilteredContacts; /// <summary> /// 通讯录过滤列表 /// </summary> public List<Contact> FilteredContacts { get { return _FilteredContacts; } set { SetProperty(ref _FilteredContacts, value); } } public ContactViewModel(IContactsService contactService) { _contactService = contactService; Contacts = new ObservableCollection<Contact>(); Xamarin.Forms.BindingBase.EnableCollectionSynchronization(Contacts, null, ObservableCollectionCallback); _contactService.OnContactLoaded += OnContactLoaded; LoadContacts(); RaiseSearchCommand = new Command(RaiseSearchHandle); } /// <summary> /// 过滤通讯录 /// </summary> void RaiseSearchHandle() { if (string.IsNullOrEmpty(SearchText)) { FilteredContacts = Contacts.ToList(); return; } Func<Contact, bool> checkContact = (s) => { if (!string.IsNullOrWhiteSpace(s.Name) && s.Name.ToLower().Contains(SearchText.ToLower())) { return true; } else if (s.PhoneNumbers.Length > 0 && s.PhoneNumbers.ToList().Exists(cu => cu.ToString().Contains(SearchText))) { return true; } return false; }; FilteredContacts = Contacts.ToList().Where(checkContact).ToList(); } /// <summary> /// BindingBase.EnableCollectionSynchronization 为集合启用跨线程更新 /// </summary> /// <param name="collection"></param> /// <param name="context"></param> /// <param name="accessMethod"></param> /// <param name="writeAccess"></param> void ObservableCollectionCallback(IEnumerable collection, object context, Action accessMethod, bool writeAccess) { // `lock` ensures that only one thread access the collection at a time lock (collection) { accessMethod?.Invoke(); } } /// <summary> /// 收到事件通知,读取一条通讯录信息 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void OnContactLoaded(object sender, ContactEventArgs e) { Contacts.Add(e.Contact); RaiseSearchHandle(); } /// <summary> /// 异步读取终端通讯录 /// </summary> /// <returns></returns> async Task LoadContacts() { try { await _contactService.RetrieveContactsAsync(); } catch (TaskCanceledException) { Console.WriteLine("任务已经取消"); } } } }
9、添加通讯录页面展示通讯录数据
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core" mc:Ignorable="d" Title="{Binding Title}" x:Class="TerminalMACS.Clients.App.Views.ContactPage" ios:Page.UseSafeArea="true"> <ContentPage.Content> <StackLayout> <SearchBar x:Name="filterText" HeightRequest="40" Text="{Binding SearchText}" SearchCommand="{Binding RaiseSearchCommand}"/> <ListView ItemsSource="{Binding FilteredContacts}" HasUnevenRows="True"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <StackLayout Padding="10" Orientation="Horizontal"> <Image Source="{Binding Image}" VerticalOptions="Center" x:Name="image" Aspect="AspectFit" HeightRequest="60"/> <StackLayout VerticalOptions="Center"> <Label Text="{Binding Name}" FontAttributes="Bold"/> <Label Text="{Binding PhoneNumbers[0]}"/> <Label Text="{Binding Emails[0]}"/> </StackLayout> </StackLayout> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> </StackLayout> </ContentPage.Content> </ContentPage>
三、源码获取
- 1.完整源码:https://github.com/dotnet9/TerminalMACS
- 2.Android客户端可成功取得通讯录数据,并可查询;
已编译的Android客户端:https://terminalmacs.com/terminalmacs-clients-app-android
- 3.iOS读取通讯录功能代码也已添加,但由于本人没有iOS测试环境,所以未验证,有条件的朋友可以测试下iOS的通讯录读取功能,如果代码不起作用,可参考本文参考的文章检查iOS代码。
四、参考资料
Getting phone contacts in Xamarin Forms:https://www.xamboy.com/2019/10/10/getting-phone-contacts-in-xamarin-forms/
参考文章末尾有源代码链接。
五、后面计划
Xamarin.Forms客户端基本信息获取,比如IMEI、IMSI、本机号码、Mac地址等。
Xamarin.Forms读取Android和iOS通讯录,原文标题:Xamarin.Forms读取并展示Android和iOS通讯录 – TerminalMACS客户端,点击这里阅读原文。