Xamarin.Forms读取手机信息,3分钟搞定联系人信息和基本信息,作为TerminalMACS的一个子进程模块,目前完成第一版:读取展示手机基本信息、联系人信息、应用程序本地化。
1. Xamarin.Forms读取手机信息 – 功能简介
1.1. 读取手机基本信息
主要使用Xamarin.Essentials库获取设备基本信息,Xam.Plugin.DeviceInfo插件获取App Id,其实该插件也能获取设备基本信息。
1.2. 读取手机联系人信息
Android和iOS工程具体实现联系人读取服务,使用到DependencyService获取服务功能。
1.3. 应用本地化
使用资源文件实现本地化,目前只做了中、英文。
2. 详细功能说明
2.1. 读取手机基本信息
Xamarin.Essentials库用于获取手机基本信息,比如手机厂商、型号、名称、类型、版本等;Xam.Plugin.DeviceInfo插件获取App Id,用于唯一标识不同手机,获取信息见下图:
代码结构如下图:
ClientInfoViewModel.cs
using Plugin.DeviceInfo; using System; using Xamarin.Essentials; namespace TerminalMACS.Clients.App.ViewModels { /// <summary> /// Client base information page ViewModel /// </summary> public class ClientInfoViewModel : BaseViewModel { /// <summary> /// Gets or sets the id of the application. /// </summary> public string AppId { get; set; } = CrossDeviceInfo.Current.GenerateAppId(); /// <summary> /// Gets or sets the model of the device. /// </summary> public string Model { get; private set; } = DeviceInfo.Model; /// <summary> /// Gets or sets the manufacturer of the device. /// </summary> public string Manufacturer { get; private set; } = DeviceInfo.Manufacturer; /// <summary> /// Gets or sets the name of the device. /// </summary> public string Name { get; private set; } = DeviceInfo.Name; /// <summary> /// Gets or sets the version of the operating system. /// </summary> public string VersionString { get; private set; } = DeviceInfo.VersionString; /// <summary> /// Gets or sets the version of the operating system. /// </summary> public Version Version { get; private set; } = DeviceInfo.Version; /// <summary> /// Gets or sets the platform or operating system of the device. /// </summary> public DevicePlatform Platform { get; private set; } = DeviceInfo.Platform; /// <summary> /// Gets or sets the idiom of the device. /// </summary> public DeviceIdiom Idiom { get; private set; } = DeviceInfo.Idiom; /// <summary> /// Gets or sets the type of device the application is running on. /// </summary> public DeviceType DeviceType { get; private set; } = DeviceInfo.DeviceType; } }
ClientInfoPage.xaml
<?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:resources="clr-namespace:TerminalMACS.Clients.App.Resx" xmlns:vm="clr-namespace:TerminalMACS.Clients.App.ViewModels" mc:Ignorable="d" x:Class="TerminalMACS.Clients.App.Views.ClientInfoPage" Title="{x:Static resources:AppResource.Title_ClientInfoPage}"> <ContentPage.BindingContext> <vm:ClientInfoViewModel/> </ContentPage.BindingContext> <ContentPage.Content> <StackLayout> <Label Text="{x:Static resources:AppResource.AppId}"/> <Label Text="{Binding AppId}" FontAttributes="Bold" Margin="10,0,0,10"/> <Label Text="{x:Static resources:AppResource.DeviceModel}"/> <Label Text="{Binding Model}" FontAttributes="Bold" Margin="10,0,0,10"/> <Label Text="{x:Static resources:AppResource.DeviceManufacturer}"/> <Label Text="{Binding Manufacturer}" FontAttributes="Bold" Margin="10,0,0,10"/> <Label Text="{x:Static resources:AppResource.DeviceName}"/> <Label Text="{Binding Name}" FontAttributes="Bold" Margin="10,0,0,10"/> <Label Text="{x:Static resources:AppResource.DeviceVersionString}"/> <Label Text="{Binding VersionString}" FontAttributes="Bold" Margin="10,0,0,10"/> <Label Text="{x:Static resources:AppResource.DevicePlatform}"/> <Label Text="{Binding Platform}" FontAttributes="Bold" Margin="10,0,0,10"/> <Label Text="{x:Static resources:AppResource.DeviceIdiom}"/> <Label Text="{Binding Idiom}" FontAttributes="Bold" Margin="10,0,0,10"/> <Label Text="{x:Static resources:AppResource.DeviceType}"/> <Label Text="{Binding DeviceType}" FontAttributes="Bold" Margin="10,0,0,10"/> </StackLayout> </ContentPage.Content> </ContentPage>
2.2. 读取手机联系人信息
Android和iOS工程具体实现联系人读取服务,使用到DependencyService获取服务功能,功能截图如下:
2.2.1. TerminalMACS.Clients.App
代码结构如下图:
2.2.1.1. 联系人实体类:Contacts.cs
目前只获取联系人名称、图片、电子邮件(可能多个)、电话号码(可能多个),更多可以扩展。
namespace TerminalMACS.Clients.App.Models { /// <summary> /// Contact information entity. /// </summary> public class Contact { /// <summary> /// Gets or sets the name /// </summary> public string Name { get; set; } /// <summary> /// Gets or sets the image /// </summary> public string Image { get; set; } /// <summary> /// Gets or sets the emails /// </summary> public string[] Emails { get; set; } /// <summary> /// Gets or sets the phone numbers /// </summary> public string[] PhoneNumbers { get; set; } } }
2.2.1.2. 联系人服务接口:IContactsService.cs
包括:
- 一个联系人获取请求接口:RetrieveContactsAsync
- 一个读取一条联系人结果通知事件:OnContactLoaded
该接口由具体平台(Android和iOS)实现。
using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using TerminalMACS.Clients.App.Models; namespace TerminalMACS.Clients.App.Services { /// <summary> /// Read a contact record notification event parameter. /// </summary> public class ContactEventArgs:EventArgs { public Contact Contact { get; } public ContactEventArgs(Contact contact) { Contact = contact; } } /// <summary> /// Contact service interface, which is required for Android and iOS terminal specific /// contact acquisition service needs to implement this interface. /// </summary> public interface IContactsService { /// <summary> /// Read a contact record and notify the shared library through this event. /// </summary> event EventHandler<ContactEventArgs> OnContactLoaded; /// <summary> /// Loading or not /// </summary> bool IsLoading { get; } /// <summary> /// Try to get all contact information /// </summary> /// <param name="token"></param> /// <returns></returns> Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? token = null); } }
2.2.1.3. 联系人VM:ContactViewModel.cs
VM提供下面两个功能:
- 全部联系人加载。
- 联系人关键字查询。
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.Resx; using TerminalMACS.Clients.App.Services; using Xamarin.Forms; namespace TerminalMACS.Clients.App.ViewModels { /// <summary> /// Contact page ViewModel /// </summary> public class ContactViewModel : BaseViewModel { /// <summary> /// Contact service interface /// </summary> IContactsService _contactService; private string _SearchText; /// <summary> /// Gets or sets the search text of the contact list. /// </summary> public string SearchText { get { return _SearchText; } set { SetProperty(ref _SearchText, value); } } /// <summary> /// The search contact command. /// </summary> public ICommand RaiseSearchCommand { get; } /// <summary> /// The contact list. /// </summary> public ObservableCollection<Contact> Contacts { get; set; } private List<Contact> _FilteredContacts; /// <summary> /// Contact filter list. /// </summary> public List<Contact> FilteredContacts { get { return _FilteredContacts; } set { SetProperty(ref _FilteredContacts, value); } } public ContactViewModel() { _contactService = DependencyService.Get<IContactsService>(); Contacts = new ObservableCollection<Contact>(); Xamarin.Forms.BindingBase.EnableCollectionSynchronization(Contacts, null, ObservableCollectionCallback); _contactService.OnContactLoaded += OnContactLoaded; LoadContacts(); RaiseSearchCommand = new Command(RaiseSearchHandle); } /// <summary> /// Filter contact list /// </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 /// Enable cross thread updates for collections /// </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> /// Received a event notification that a contact information was successfully read. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void OnContactLoaded(object sender, ContactEventArgs e) { Contacts.Add(e.Contact); RaiseSearchHandle(); } /// <summary> /// Read contact information asynchronously /// </summary> /// <returns></returns> async Task LoadContacts() { try { await _contactService.RetrieveContactsAsync(); } catch (TaskCanceledException) { Console.WriteLine(AppResource.TaskCancelled); } } } }
2.2.1.4. 联系人展示页面:ContactPage.xaml
简单的布局,一个StackLayout布局容器竖直排列,一个SearchBar提供关键字搜索功能。
<?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:resources="clr-namespace:TerminalMACS.Clients.App.Resx" xmlns:vm="clr-namespace:TerminalMACS.Clients.App.ViewModels" xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core" mc:Ignorable="d" Title="{x:Static resources:AppResource.Title_ContactPage}" x:Class="TerminalMACS.Clients.App.Views.ContactPage" ios:Page.UseSafeArea="true"> <ContentPage.BindingContext> <vm:ContactViewModel/> </ContentPage.BindingContext> <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.2.2. Android
代码结构如下图:
- AndroidManifest.xml:写入读、写联系人权限请求。
- ContactsService.cs:具体的联系人权限请求、数据读取操作。
- MainActivity.cs:接收权限请求结果
- MainApplicaion.cs:此类未添加任务关键代码,但必不可少,否则无法正确弹出权限请求窗口。
- PermissionUtil.cs:权限请求结果判断
2.2.2.1. AndroidManifest.xml添加权限
只添加下面这一行即可:
<uses-permission android:name="android.permission.READ_CONTACTS" />
2.2.2.2. ContactsService.cs
Android联系人获取实现服务,实现IContactsService。注意命名空间上的特性代码,必须添加上这个特性后,在前面的联系人VM中才能使用DependencyService.Get()获取此服务实例,默认服务是单例的:
[assembly: Xamarin.Forms.Dependency(typeof(TerminalMACS.Clients.App.iOS.Services.ContactsService))]
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; [assembly: Xamarin.Forms.Dependency(typeof(TerminalMACS.Clients.App.iOS.Services.ContactsService))] namespace TerminalMACS.Clients.App.iOS.Services { /// <summary> /// Contact service. /// </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> /// Asynchronous request permission /// </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> /// Request contact asynchronously. This method is called by the interface. /// </summary> /// <param name="cancelToken"></param> /// <returns></returns> public async Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? cancelToken = null) { requestStop = false; if (!cancelToken.HasValue) cancelToken = CancellationToken.None; // We create a TaskCompletionSource of decimal var taskCompletionSource = new TaskCompletionSource<IList<Contact>>(); // Registering a lambda into the cancellationToken cancelToken.Value.Register(() => { // We received a cancellation message, cancel the TaskCompletionSource.Task requestStop = true; taskCompletionSource.TrySetCanceled(); }); _isLoading = true; var task = LoadContactsAsync(); // Wait for the first task to finish among the two var completedTask = await Task.WhenAny(task, taskCompletionSource.Task); _isLoading = false; return await completedTask; } /// <summary> /// Load contacts asynchronously, fact reading method of address book. /// </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; } } }
2.2.2.3. MainActivity.cs
代码简单,只在OnRequestPermissionsResult方法中接收权限请求结果:
ContactsService.OnRequestPermissionsResult(requestCode, permissions, grantResults);
2.2.3. iOS
代码结构如下图:
- ContactsService.cs:具体的联系人权限请求、数据读取操作。
- Info.plist:权限请求时描述文件
2.2.3.1. ContactsService.cs
iOS具体的联系人读取服务,实现IContactsService接口,原理同Android联系人服务类似,本人无调试环境,iOS此功能未测试。
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; [assembly: Xamarin.Forms.Dependency(typeof(TerminalMACS.Clients.App.iOS.Services.ContactsService))] namespace TerminalMACS.Clients.App.iOS.Services { /// <summary> /// Contact service. /// </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> /// Asynchronous request permission /// </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> /// Request contact asynchronously. This method is called by the interface. /// </summary> /// <param name="cancelToken"></param> /// <returns></returns> public async Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? cancelToken = null) { requestStop = false; if (!cancelToken.HasValue) cancelToken = CancellationToken.None; // We create a TaskCompletionSource of decimal var taskCompletionSource = new TaskCompletionSource<IList<Contact>>(); // Registering a lambda into the cancellationToken cancelToken.Value.Register(() => { // We received a cancellation message, cancel the TaskCompletionSource.Task requestStop = true; taskCompletionSource.TrySetCanceled(); }); _isLoading = true; var task = LoadContactsAsync(); // Wait for the first task to finish among the two var completedTask = await Task.WhenAny(task, taskCompletionSource.Task); _isLoading = false; return await completedTask; } /// <summary> /// Load contacts asynchronously, fact reading method of address book. /// </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; } } }
2.2.3.2. Info.plist
联系人权限请求说明
2.3. 应用本地化
使用资源文件实现本地化,目前只做了中、英文。
资源文件如下:
指定默认区域性
要使资源文件可正常使用,应用程序必须指定 NeutralResourcesLanguage。 在共享项目中,应自定义 AssemblyInfo.cs 文件以指定默认区域性 。 以下代码演示如何在 AssemblyInfo.cs 文件中将 NeutralResourcesLanguage 设置为 zh-CN (摘自官方文档:https://docs.microsoft.com/zh-cn/samples/xamarin/xamarin-forms-samples/usingresxlocalization/,后经测试,注释下面这段代码也能正常本地化):
[assembly: NeutralResourcesLanguage("zh-Hans")]
XAML中使用
引入资源文件命名空间
xmlns:resources="clr-namespace:TerminalMACS.Clients.App.Resx"
具体使用如
<Label Text="{x:Static resources:AppResource.ClientName_AboutPage}" FontAttributes="Bold"/>
3. 关于TerminalMACS及本客户端
3.1. TermainMACS – Xamarin.Forms读取手机信息
多终端资源管理与检测系统,包含多个子进程模块,目前只开发了Xamarin.Forms客户端,下一步开发服务端,使用 .NET 5 Web API开发,基于Abp vNext搭建。
3.2. Xamarin.Forms客户端 – Xamarin.Forms读取手机信息
作为TerminalMACS系统的一个子进程模块,目前只开发了手机基本信息获取、联系人信息获取、本地化功能,后续开发服务端时,会配合添加通信功能,比如连接服务端验证、主动推送已获取资源等。
3.3. 关于项目开源 – Xamarin.Forms读取手机信息
- 开源项目地址:https://github.com/dotnet9/TerminalMACS
原文链接:https://terminalmacs.com/890.html
【江湖人士】(jhrs.com) 投稿作者:玩币老铁,不代表江湖人士立场,如若转载,请注明出处:https://jhrs.com/2020/35534.html
扫码加入电报群,让你获得国外网赚一手信息。