Xamarin.Forms读取并展示Android和iOS通讯录 - TerminalMACS客户端

时间:2023-11-09 21:07:20

Xamarin.Forms读取并展示Android和iOS通讯录 - TerminalMACS客户端

本文同步更新地址:

阅读导航:

  • 一、功能说明
  • 二、代码实现
  • 三、源码获取
  • 四、参考资料
  • 五、后面计划

一、功能说明

完整思维导图:https://github.com/dotnet9/TerminalMACS/blob/master/docs/TerminalMACS.xmind

Xamarin.Forms读取并展示Android和iOS通讯录 - TerminalMACS客户端

本文介绍图中右侧画红圈处的功能,即使用Xamarin.Forms获取和展示Android和iOS的通讯录信息,下面是最终效果,由于使用的是真实手机,所以联系人姓名及电话号码打码显示。

Xamarin.Forms读取并展示Android和iOS通讯录 - TerminalMACS客户端

并简单的进行了搜索功能处理,之所以说简单,是因为通讯录列表是全部读取出来了,搜索是直接从此列表进行过滤的。

下图来自:https://www.xamboy.com/2019/10/10/getting-phone-contacts-in-xamarin-forms/, 本功能是参考此文所写,所以直接引用文中的图片。

Xamarin.Forms读取并展示Android和iOS通讯录 - TerminalMACS客户端

二、代码实现

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文件添加通讯录权限使用说明

Xamarin.Forms读取并展示Android和iOS通讯录 - TerminalMACS客户端

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.CurrentActivityAcr.UserDialogs 包。

7、Android工程添加权限处理判断类

Permission.Util

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>

三、源码获取

已编译的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地址等。