As a sub-process module of TerminalMACS, the first edition is currently completed: reading basic mobile information, contact information, application localization.
- Functional introduction
- Detailed Functional Description
- About TerminalMACS
1. Functional introduction
1.1. Read basic mobile information
Use the Xamarin.Essentials library to get basic device information, and the Xam.Plugin.DeviceInfo plug-in to get App Id. In fact, the plug-in can also get basic device information.
1.2. Read mobile contact information
The Android and iOS projects specifically implement contact reading services, using DependencyService to get service capabilities.
1.3. Application localization
Only Chinese and English have been localized using resource files.
2. Detailed functional description
2.1. Read basic mobile information
The Xamarin.Essentials library is used to get basic information about the mobile phone, such as the manufacturer, model, name, type, version, etc. The Xam.Plugin.DeviceInfo plug-in gets App Id to uniquely identify different mobile phones. See the following image for information:
The code structure is as follows:
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. Read mobile contact information
The Android and iOS projects specifically implement the Contact Read service, using DependencyService to get service capabilities, with the following screenshots:
2.2.1. TerminalMACS.Clients.App
The code structure is as follows:
2.2.1.1. Contact Entity Class: Contacts.cs
Currently only contact names, pictures, e-mail (possibly multiple), phone numbers (possibly multiple), and more can be extended.
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. Contact Service Interface: IContactsService.cs
Include:
- A Contact Gets Request Interface: RetrieveContactsAsync
- A read contact result notification event: OnContactLoaded
The interface is implemented by a specific platform (Android and 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. Contact VM: ContactViewModel.cs
VM provides the following two functions:
- Load all contacts.
- Contact keyword query.
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 Contact display page: ContactPage.xaml
Simple layouts, a StackLayout layout container arranged vertically, and a SearchBar providing keyword search capabilities.
<?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
The code structure is as follows:
- AndroidManifest.xml: Write read and write contact rights requests.
- ContactsService.cs: Specific contact rights requests, data read operations.
- MainActivity.cs: Receive permission request results
- MainApplicaion.cs: This class does not add task-critical code, but it is essential or the permission request window will not pop up correctly.
- PermissionUtil.cs: Permission Request Result Judgment
2.2.2.1. Add permissions to AndroidManifest.xml
Just add the following line:
<uses-permission android:name="android.permission.READ_CONTACTS" />
2.2.2.2. ContactsService.cs
Android contacts get implementation services to implement IContactsService.Note the attribute code on the namespace, which must be added before you can use DependencyService.Get() to obtain this service instance in the previous contact VM. The default service is singleton:
[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
The code is simple and only receives permission request results in the OnRequestPermissionsResult method:
// The contact service processes the result of the permission request. ContactsService.OnRequestPermissionsResult(requestCode, permissions, grantResults);
2.2.3. iOS
The code structure is as follows:
- ContactsService.cs: Specific contact rights requests, data read operations.
- Info.plist: Describe file when permission is requested
2.2.3.1. ContactsService.cs
IOS specific contact reading service, which implements the IContactsService interface, works like Android Contact Service, I have no debugging environment, iOS this function is not tested.
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
Contact Rights Request Description
2.3. Application localization
Only Chinese and English have been localized using resource files.
The resource files are as follows:
Specify default culture
For the resource file to work properly, the application must specify NeutralResourcesLanguage.In shared projects, the AssemblyInfo.cs file should be customized to specify the default culture.The following code demonstrates how to set NeutralResourcesLanguage to zh-CN in the AssemblyInfo.cs file (extracted from official documents: https://docs.microsoft.com/zh-cn/samples/xamarin/xamarin-forms-samples/usingresxlocalization/, after testing, this code can also be localized properly):
[assembly: NeutralResourcesLanguage("zh-CN")]
Use in XAML
Introducing a resource file namespace
xmlns:resources="clr-namespace:TerminalMACS.Clients.App.Resx"
Specific uses such as
<Label Text="{x:Static resources:AppResource.ClientName_AboutPage}" FontAttributes="Bold"/>
3. About TerminalMACS and this client
3.1. TermainMACS
The multi-terminal resource management and detection system, which contains several sub-process modules, has only developed Xamarin.Forms client at present. The next step is to develop the server, which is based on Abp vNext using.NET 5 Web API.
3.2. Xamarin.Forms Client
As a sub-process module of TerminalMACS system, only the basic information acquisition, contact information acquisition and localization functions of mobile phone have been developed at present. When developing service side in the future, they will cooperate with adding communication functions, such as connecting service side validation, actively pushing acquired resources, etc.
3.3. About open source projects
-
- Open source project address: https://github.com/dotnet9/TerminalMACS
-
- Official website: https://terminalmacs.com
-
- Cooperative website: https://dotnet9.com