Xamarin.Forms读取并展示Android和iOS通讯录 - TerminalMACS客户端
程序员文章站
2022-04-27 18:41:52
Xamarin.Forms读取并展示Android和iOS通讯录 TerminalMACS客户端 本文同步更新地址: https://dotnet9.com/11520.html https://terminalmacs.com/861.html 阅读导航: 一、功能说明 二、代码实现 三、源码获取 ......
xamarin.forms读取并展示android和ios通讯录 - terminalmacs客户端
本文同步更新地址:
阅读导航:
- 一、功能说明
- 二、代码实现
- 三、源码获取
- 四、参考资料
- 五、后面计划
一、功能说明
完整思维导图: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
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>
三、源码获取
-
2.android客户端可成功取得通讯录数据,并可查询;
已编译的android客户端:
- 3.ios读取通讯录功能代码也已添加,但由于本人没有ios测试环境,所以未验证,有条件的朋友可以测试下ios的通讯录读取功能,如果代码不起作用,可参考本文参考的文章检查ios代码。
四、参考资料
getting phone contacts in xamarin forms:
参考文章末尾有源代码链接。
五、后面计划
xamarin.forms客户端基本信息获取,比如imei、imsi、本机号码、mac地址等。