Matthew Pans
Matthew Pans

Reputation: 839

MAUI: Issue with reading phonebook contacts

I follow this blog for reading phone book contacts from my Android and iOS devices and it was working fine on my Xamarin forms project.

Now I am migrating it to the MAUI and facing 2 problems: 1 is on Android ContactsService codes and the other one is on how to read contacts data. I have done a lot of logic and functionalities with this implementation, so changing it is a big headache for me.

Android ContactsService

I am getting 4 errors on this service.

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;
    static string[] PermissionsContact = {
        Manifest.Permission.ReadContacts
    };

    public event EventHandler<ContactEventArgs> OnContactLoaded;

    async void RequestContactsPermissions()
    {
        if (ActivityCompat.ShouldShowRequestPermissionRationale(CrossCurrentActivity.Current.Activity, Manifest.Permission.ReadContacts)
            || ActivityCompat.ShouldShowRequestPermissionRationale(CrossCurrentActivity.Current.Activity, Manifest.Permission.WriteContacts))
        {

            // Provide an additional rationale to the user if the permission was not granted
            // and the user would benefit from additional context for the use of the permission.
            // For example, if the request has been denied previously.

            await UserDialogs.Instance.AlertAsync("Contacts Permission", "This action requires contacts permission", "Ok");
        }
        else
        {
            // Contact permissions have not been granted yet. Request them directly.
            ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, PermissionsContact, RequestContacts);
        }
    }
    public static void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
    {
        if (requestCode == ContactsService.RequestContacts)
        {
            // We have requested multiple permissions for contacts, so all of them need to be
            // checked.
            if (PermissionUtil.VerifyPermissions(grantResults))
            {
                // All required permissions have been granted, display contacts fragment.
                contactPermissionTcs.TrySetResult(true);
            }
            else
            {
                contactPermissionTcs.TrySetResult(false);
            }

        }
    }

    public async Task<bool> RequestPermissionAsync()
    {
        contactPermissionTcs = new TaskCompletionSource<bool>();
        // Verify that all required contact 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)
        {
            // Contacts permissions have not been granted.
            RequestContactsPermissions();
        }
        else
        {
            // Contact permissions have been granted. 
            contactPermissionTcs.TrySetResult(true);
        }

        return await contactPermissionTcs.Task;
    }

    public async Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? cancelToken = null)
    {
        stopLoad = 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
            stopLoad = 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;
    }
    
    async Task<IList<Contact>> LoadContactsAsync()
    {
        IList<Contact> contacts = new List<Contact>();
        //var hasPermission = await RequestPermissionAsync();
        //if (hasPermission)
        //{
        var uri = ContactsContract.Contacts.ContentUri;
        var ctx = Application.Context;
        await Task.Run(() =>
        {
            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;
    }

    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;
    }


    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();
    }

    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));
    }

}

Errors:

enter image description here

Severity Code Description Project File Line Suppression State Error CS0234 The type or namespace name 'Content' does not exist in the namespace 'Android.Support.V4' (are you missing an assembly reference?)

Severity Code Description Project File Line Suppression State Error CS0104 'Application' is an ambiguous reference between 'Android.App.Application' and 'Microsoft.Maui.Controls.Application' ListPM Caller (net7.0-android)

Severity Code Description Project File Line Suppression State Error CS0019 Operator '>' cannot be applied to operands of type 'method group' and 'int' ListPM Caller (net7.0-android)

How to read data

In my Xamarin forms app I am reading the contact data like below and passing it as an argument to App.xaml.cs.

//Android
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
    IContactsService contactsService = new ContactsService();
    LoadApplication(new App(contactsService));
}

//iOS
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate, IUNUserNotificationCenterDelegate, IMessagingDelegate
{
    IContactsService contactsService = new ContactsService();
    LoadApplication(new App(contactsService));
}

//App.xaml.cs
public App(IContactsService contactsService)
{
    InitializeComponent();
    Utility.myContacts = contactsService;
}

//Utility.cs
public class Utility
{
    public static IContactsService myContacts;
}

//ContactsPage.xaml.cs
ContactsViewModel cvm;
public ContactsPage()
{
    InitializeComponent();
    cvm = new ContactsViewModel(Utility.myContacts);
    BindingContext = cvm;
}

//ContactsViewModel.cs
public ContactsViewModel(IContactsService contactService)
{
    //Other operations and showing it on UI
}

But in MAUI how can I pass the contact details from android and ios to main project?

I have uploaded a demo here to reproduce this issue.

Update

I have added the MyContactsService class like below, but it has a lot of errors due to the new logic.

namespace MyApp.Service
{
    public partial class MyContactsService
    {
        const string ThumbnailPrefix = "thumb";

        bool requestStop = false;

        public event EventHandler<ContactEventArgs> OnContactLoaded;

        bool _isLoading = false;
        public bool IsLoading => _isLoading;

        public async Task<IList<MyContact>> RetrieveContactsAsync(CancellationToken? cancelToken = null)
        {
            requestStop = false;

            if (!cancelToken.HasValue)
                cancelToken = CancellationToken.None;

            // We create a TaskCompletionSource of decimal
            var taskCompletionSource = new TaskCompletionSource<IList<MyContact>>();

            // 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;

        }

        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;

        }

        public partial async Task<List<MyContact>> GetContactsAsync()
        {
            IList<MyContact> contacts = new List<MyContact>();
            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 MyContact()
                        {
                            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;
        }
    }
}

Upvotes: 0

Views: 490

Answers (1)

Jessie Zhang -MSFT
Jessie Zhang -MSFT

Reputation: 13879

Previously I am passing contactsService object as an argument to App.xaml.cs from MainActivity and AppDelegate. But in MAUI no calls to App.xaml.cs.

We don't need to pass the returned value from the local platform to the shared platform, we can get the returned contacts list by Invoke platform code directly.

I think there are a couple of problems with the way you implement it on Maui.

Based on the code you shared, I achieved this function with Invoke platform code.

You can refer to the following code:

1.To distinguish class Contact of system, I created a class MyContact.cs

namespace MauiGetContactsApp.Models
{
    public class MyContact
    {
        public string DisplayName { get; set; }
        public string Image { get; set; }
        public string[] Emails { get; set; }
        public string[] PhoneNumbers { get; set; }
    }
}

2.create a partial class MyContactsService.cs and add a partial method GetContacts().Then we can get the contacts list by the returned value of method GetContacts():

namespace MauiGetContactsApp.Services
{
    public partial class MyContactsService
    {
        public partial List<MyContact> GetContacts();

    }
}

3.On platform Android,implement partial method GetContacts(). I copied the read contacts methods from your demo. And I removed the request permission on android platform. We just need to request permission on shared platform.

//namespace MauiGetContactsApp.Platforms.Android
namespace MauiGetContactsApp.Services
{
    public partial class MyContactsService
    {
        const string ThumbnailPrefix = "thumb";

        List<MyContact> contactList;


        public partial List<MyContact> GetContacts()
        {
           // contactList = new List<MyContact>();

            FillContacts();


            return contactList;
        }

        void FillContacts()
        {
            var uri = ContactsContract.Contacts.ContentUri;

            string[] projection = {
                ContactsContract.Contacts.InterfaceConsts.Id,
                ContactsContract.Contacts.InterfaceConsts.DisplayName,
                ContactsContract.Contacts.InterfaceConsts.PhotoId,
            };


            var ctx = Android.App.Application.Context;    

            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");

            contactList = new List<MyContact>();

            if (cursor.MoveToFirst())
            {
                do
                {  
                    var contact = CreateContact(cursor, ctx);
                    if (!string.IsNullOrWhiteSpace(contact.DisplayName))
                    {
                        contactList.Add(contact);
                    }
                } while (cursor.MoveToNext());
            }
        }


        MyContact 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 MyContact
            {
                DisplayName = GetString(cursor, ContactsContract.Contacts.InterfaceConsts.DisplayName),
                Emails = emails,
                Image = path,
                PhoneNumbers = numbers,
            };

            return contact;
        }


        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();
        }

        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));
        }
    }
}

4.On shared platform, I added a button to trigger the function of reading contacts. And before reading contacts,we need to check and request the ContactsRead permission.

using MauiGetContactsApp.Models;
using Microsoft.Maui.ApplicationModel.Communication;
using MyContactsService = MauiGetContactsApp.Services.MyContactsService;

namespace MauiGetContactsApp
{
    public partial class MainPage : ContentPage
    {
        List<MyContact> contacts;

        public MainPage()
        {
            InitializeComponent();

            contacts= new List<MyContact>();
        }

        private async void Btn_Clicked(object sender, EventArgs e)
        {
          await  RequestContactPermission();
        }


        async Task RequestContactPermission()
        {
            var status = PermissionStatus.Unknown;
            {
                status = await Permissions.CheckStatusAsync<Permissions.ContactsRead>();

                if (status == PermissionStatus.Granted)
                {
                    // get cotacts List
                    getContacts();
                    return;
                }

                if (Permissions.ShouldShowRationale<Permissions.ContactsRead>())
                {
                    await Shell.Current.DisplayAlert("Needs permissions", "BECAUSE!!!", "OK");
                }

                status = await Permissions.RequestAsync<Permissions.ContactsRead>();

            }


            if (status != PermissionStatus.Granted)
                await Shell.Current.DisplayAlert("Permission required",
                    "Read Contacts permission is required for calling. " +
                    "We just want to do a test.", "OK");

            else if (status == PermissionStatus.Granted)
            {
                getContacts();

            }
        }

        public async Task<PermissionStatus> CheckAndRequestPhonePermission()
        {
            PermissionStatus status = await Permissions.CheckStatusAsync<Permissions.ContactsRead>();

            if (status == PermissionStatus.Granted)
                return status;

            if (status == PermissionStatus.Denied && DeviceInfo.Platform == DevicePlatform.iOS)
            {
                // Prompt the user to turn on in settings
                // On iOS once a permission has been denied it may not be requested again from the application
                return status;
            }

            if (Permissions.ShouldShowRationale<Permissions.ContactsRead>())
            {
                // Prompt the user with additional information as to why the permission is needed

                await Shell.Current.DisplayAlert("Need permission", "We need the Phone permission", "Ok");
            }

            status = await Permissions.RequestAsync<Permissions.ContactsRead>();

            if (status == PermissionStatus.Granted)
            {
                getContacts();

            }
            else
            {
                await Shell.Current.DisplayAlert("Permission required",
                      "Read Contacts permission is required for read contacts " +
                      "We just want to test", "OK");

            }

            return status;
        }


        public void getContacts() {

            var service = new MyContactsService();
            contacts = service.GetContacts();


            foreach (var contact in contacts)
            {
                System.Diagnostics.Debug.Write("------> " + contact.DisplayName ) ;

                foreach (var phoneNumber in contact.PhoneNumbers) {
                    System.Diagnostics.Debug.Write("name = " + phoneNumber );
                }

                foreach (var email in contact.Emails) {
                    System.Diagnostics.Debug.Write("Email = " + email);
                }
            }
        }
    }
}

Note:

1.Remember to add ContactsRead permission on AndroidManifest.xaml

  <uses-permission android:name="android.permission.READ_CONTACTS" />

2.On iOS platform,the steps are the same. You just need to copy the code that reads contacts on the iOS platform to the method GetContacts(); implemented on iOS.

3.Remember to keep the namespace for MyContactsService.cs on individual platform(Android and iOS) to be consistent with the namespace of MyContactsService.cs on shared platform.

4.The whole file struct of my demo is:

enter image description here

Update:

On iOS, since we have checked and requested permission on shared platform, so we don't need to add the code requesting permission on iOS platform.

Please refer to the following code:

namespace MauiGetContactsApp.Services
{
    public partial  class MyContactsService
    {
        const string ThumbnailPrefix = "thumb";

        List<MyContact> contacts = new List<MyContact>();

        public partial List<MyContact> GetContacts()
        {
            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 MyContact()
                    {
                        DisplayName = 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.DisplayName))
                    {
                        contacts.Add(contact);
                    }


                }));
            }

            return contacts;
        }
     }
}

Remember to add the following code to file Info.plist

    <key>NSContactsUsageDescription</key>
    <string>We need this permission...</string>

Upvotes: 1

Related Questions