This article updates the address synchronously:
Read navigation:
- 1, Function description
- 2, Code implementation
- 3, Source code acquisition
- 4, References
- 5, Later plans
1, Function description
Complete Mind Map: https://github.com/dotnet9/TerminalMACS/blob/master/docs/TerminalMACS.xmind
This paper introduces the function of the red circle on the right side of the figure, that is to use Xamarin.Forms to obtain and display the address book information of Android and iOS. The following is the final effect. Because the real mobile phone is used, the contact name and phone number are displayed by typing.
And simple search function processing, the reason is simple, because the address list is all read out, search is directly filtered from this list.
The figure below comes from: https://www.xamboy.com/2019/10/10/getting-phone-contacts-in-xamarin-forms/, This function is written with reference to this article, so it directly refers to the pictures in the article.
2, Code implementation
1. Shared library project create contact entity class: Contacts.cs
namespace TerminalMACS.Clients.App.Models { /// <summary> //Mail list /// </summary> public class Contact { /// <summary> ///Get or set name /// </summary> public string Name { get; set; } /// <summary> ///Get or set the Avatar /// </summary> public string Image { get; set; } /// <summary> ///Get or set email address /// </summary> public string[] Emails { get; set; } /// <summary> ///Get or set mobile number /// </summary> public string[] PhoneNumbers { get; set; } } }
2. Share library create address book service interface: IContactsService.cs
Include:
- An address book acquisition request interface: RetrieveContactsAsync
- One notification event for reading a communication result: 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> ///Address book event parameters /// </summary> public class ContactEventArgs:EventArgs { public Contact Contact { get; } public ContactEventArgs(Contact contact) { Contact = contact; } } /// <summary> ///Address book service interface, which android and iOS terminal specific address book acquisition services need to inherit /// </summary> public interface IContactsService { /// <summary> ///Read a data notification /// </summary> event EventHandler<ContactEventArgs> OnContactLoaded; /// <summary> ///Loading or not /// </summary> bool IsLoading { get; } /// <summary> ///Try to get all contacts /// </summary> /// <param name="token"></param> /// <returns></returns> Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? token = null); } }
3. Add address book service in iOS project to realize IContactsService interface:
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> ///Address book access 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 address book 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 created a decimal TaskCompletionSource var taskCompletionSource = new TaskCompletionSource<IList<Contact>>(); // Register lambda in cancelationtoken cancelToken.Value.Register(() => { // We received a cancellation message to cancel TaskCompletionSource.Task requestStop = true; taskCompletionSource.TrySetCanceled(); }); _isLoading = true; var task = LoadContactsAsync(); // Wait for the first of the two tasks to complete var completedTask = await Task.WhenAny(task, taskCompletionSource.Task); _isLoading = false; return await completedTask; } /// <summary> ///Load address book asynchronously, specific 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; } } }
4. Add address book permission instructions to Info.plist file in iOS project
5. Add read address book permission configuration in Android project: Android manifest.xml
<uses-permission android:name="android.permission.READ_CONTACTS"/>
The complete permission configuration is as follows
<?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. Add address book service in Android project to realize IContactServer interface: 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> ///Address book access service /// </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; //Permission request status code public const int RequestContacts = 1239; /// <summary> ///Get the request permission required for the address book /// </summary> static string[] PermissionsContact = { Manifest.Permission.ReadContacts }; public event EventHandler<ContactEventArgs> OnContactLoaded; /// <summary> ///Request address book permission asynchronously /// </summary> async void RequestContactsPermissions() { //Check whether you can pop up to apply for permission to read and write address book if (ActivityCompat.ShouldShowRequestPermissionRationale(CrossCurrentActivity.Current.Activity, Manifest.Permission.ReadContacts) || ActivityCompat.ShouldShowRequestPermissionRationale(CrossCurrentActivity.Current.Activity, Manifest.Permission.WriteContacts)) { // If a license is not granted, provide the user with other reasons the user will benefit from the additional context of the use right. // For example, if the request was previously denied. await UserDialogs.Instance.AlertAsync("Address book permissions", "Address book permission is required for this operation", "Determine"); } else { // Address book permission has not been granted. Request these permissions directly. ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, PermissionsContact, RequestContacts); } } /// <summary> ///Results after receiving the user response request permission Operation /// </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) { // We have requested more than one address book permission, so we need to check all related permissions if (PermissionUtil.VerifyPermissions(grantResults)) { // All required permissions have been granted to display the contact snippet. contactPermissionTcs.TrySetResult(true); } else { contactPermissionTcs.TrySetResult(false); } } } /// <summary> ///Asynchronous request permission /// </summary> /// <returns></returns> public async Task<bool> RequestPermissionAsync() { contactPermissionTcs = new TaskCompletionSource<bool>(); // Verify that all required address book permissions have been granted. 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) { // Address book permission has not been granted. RequestContactsPermissions(); } else { // Address book permission granted. contactPermissionTcs.TrySetResult(true); } return await contactPermissionTcs.Task; } /// <summary> ///Request address book asynchronously. This method is called by the interface /// </summary> /// <param name="cancelToken"></param> /// <returns></returns> public async Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? cancelToken = null) { stopLoad = false; if (!cancelToken.HasValue) cancelToken = CancellationToken.None; // We created a decimal TaskCompletionSource var taskCompletionSource = new TaskCompletionSource<IList<Contact>>(); // Register lambda in cancelationtoken cancelToken.Value.Register(() => { // We received a cancellation message to cancel TaskCompletionSource.Task stopLoad = true; taskCompletionSource.TrySetCanceled(); }); _isLoading = true; var task = LoadContactsAsync(); // Wait for the first of the two tasks to complete var completedTask = await Task.WhenAny(task, taskCompletionSource.Task); _isLoading = false; return await completedTask; } /// <summary> ///Load address book asynchronously, specific 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) { return contacts; } var uri = ContactsContract.Contacts.ContentUri; var ctx = Application.Context; await Task.Run(() => { // Only address book Id, DisplayName and PhotoThumbnailUri are requested temporarily, which can be extended 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)) { // Read and take out one item, that is, display the notice interface OnContactLoaded?.Invoke(this, new ContactEventArgs(contact)); contacts.Add(contact); } if (stopLoad) break; } } }); return contacts; } /// <summary> ///Read an address book /// </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> ///Read contact phone number /// </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> ///Read contact email address /// </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)); } } }
Need to add Plugin.CurrentActivity and Acr.UserDialogs Bag.
7. Add permission processing judgment class to Android project
Permission.Util
using Android.Content.PM; namespace TerminalMACS.Clients.App.Droid { public static class PermissionUtil { /** * Check that all given permissions have been granted by verifying that the value of each entry in the given array is Permission.Granted. * * See Activity#onRequestPermissionsResult (int, String[], int[]) */ public static bool VerifyPermissions(Permission[] grantResults) { // At least one result must be checked if (grantResults.Length < 1) return false; // Verify that each required permission has been granted, otherwise false is returned foreach (Permission result in grantResults) { if (result != Permission.Granted) { return false; } } return true; } } }
MainActivity.OnRequestPermissionResult is the result processing function of permission application. In this function, ContactsService.OnRequestPermissionsResult is called to notify the mail list service permissions.
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); // Pass the address book service instance to the shared library, which uses the read address book interface 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); // Address book service processing permission request results ContactsService.OnRequestPermissionsResult(requestCode, permissions, grantResults); base.OnRequestPermissionsResult(requestCode, permissions, grantResults); } } }
8. Create the address book ViewModel and use the address book service
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> ///Address book ViewModel /// </summary> public class ContactViewModel : BaseViewModel { /// <summary> ///Address book service interface /// </summary> IContactsService _contactService; /// <summary> //Title /// </summary> public new string Title => "Mail list"; private string _SearchText; /// <summary> ///Search keywords /// </summary> public string SearchText { get { return _SearchText; } set { SetProperty(ref _SearchText, value); } } /// <summary> ///Address book search command /// </summary> public ICommand RaiseSearchCommand { get; } /// <summary> ///Address book list /// </summary> public ObservableCollection<Contact> Contacts { get; set; } private List<Contact> _FilteredContacts; /// <summary> ///Address book filter list /// </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> ///Filter address book /// </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 enables 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> ///Receive an event notification and read an address book message /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void OnContactLoaded(object sender, ContactEventArgs e) { Contacts.Add(e.Contact); RaiseSearchHandle(); } /// <summary> ///Asynchronous read terminal address book /// </summary> /// <returns></returns> async Task LoadContacts() { try { await _contactService.RetrieveContactsAsync(); } catch (TaskCanceledException) { Console.WriteLine("Task cancelled"); } } } }
9. Add address book page to display address book data
<?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>
3, Source code acquisition
-
1. Complete source code: https://github.com/dotnet9/TerminalMACS
-
2.Android client can successfully obtain the address book data and query it;
Compiled Android clients: https://terminalmacs.com/terminalmacs-clients-app-android
- 3. The iOS read address book function code has also been added, but because I don't have an iOS test environment, I haven't verified it. Conditional friends can test the iOS read address book function. If the code doesn't work, please refer to the article referenced in this article to check the iOS code.
4, References
Getting phone contacts in Xamarin Forms: https://www.xamboy.com/2019/10/10/getting-phone-contacts-in-xamarin-forms/
Refer to the source code link at the end of the article.
5, Later plans
Obtain basic information of Xamarin.Forms client, such as IMEI, IMSI, native number, Mac address, etc.