using System; using System.Collections.Generic; using System.Globalization; using Android.App; using Android.Content; using Android.Content.Res; using Android.Database; using Android.OS; using Android.Provider; using EasyPhone.AndroidHelpers; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using SeniorLauncher.Helpers; using Uri = Android.Net.Uri; using Phone = Android.Provider.ContactsContract.CommonDataKinds.Phone; using StructuredName = Android.Provider.ContactsContract.CommonDataKinds.StructuredName; namespace EasyPhone.DataLayer { /// /// Třída pro získání kontaktů ze zařízení (jméno, Id, telefonní číslo...) /// - jednotlivé metody jsou interně locked, využívají lokální cachování na vazvu Id -> číslo /// - cachování: viz parciální třída ContactsAdapter_StorageCache /// public partial class ContactsAdapter { public List ContactList = null; public bool NumbersFilled = false; Activity activity; object lockContactNames = new object(); object lockFillNumbers = new object(); object lockGetNumberByID = new object(); object lockLoadContactName = new object(); object lockGetFullPhoto = new object(); public ContactsAdapter(Activity activity) { this.activity = activity; } /// /// Naplní pole jmen kontaktů z telefonu /// - pouze jména, ne čísla, viz FillNumbers /// public void FillContactsFromPhone() { lock (lockContactNames) { FillContacts_Locked(); } } private void FillContacts_Locked() { // Upraveno podle: http://stackoverflow.com/questions/7803633/how-to-get-android-contact-list-data-on-my-seperate-listview-in-android-2-1 // - pouze lokální kontakty: http://stackoverflow.com/questions/12932111/how-to-prevent-gmail-contacts-in-contentresolver-query-in-android var uri = ContactsContract.Contacts.ContentUri; string[] projection = { ContactsContract.Contacts.InterfaceConsts.Id, ContactsContract.Contacts.InterfaceConsts.DisplayName, ContactsContract.Contacts.InterfaceConsts.PhotoId, ContactsContract.Contacts.InterfaceConsts.HasPhoneNumber }; string selection = ContactsContract.Contacts.InterfaceConsts.InVisibleGroup + "=1"; selection += " AND " + ContactsContract.Contacts.InterfaceConsts.HasPhoneNumber + "=1"; ICursor cursor = null; try { cursor = Game.Activity.ContentResolver.Query(uri, projection, selection, null, null); if (cursor == null) return; ContactList = new List(); if (cursor.MoveToFirst()) { do { // Načítá pouze viditelné kontakty s telefonním číslem ContactList.Add(new Contact { Id = cursor.GetLong(cursor.GetColumnIndex(projection[0])), DisplayName = cursor.GetString(cursor.GetColumnIndex(projection[1])), PhotoId = cursor.GetString(cursor.GetColumnIndex(projection[2])) }); } while (cursor.MoveToNext()); } } catch (Exception e) { ExceptionLogger.LogException(e, false, false); } finally { // Na konci uzavře cursor if (cursor != null) cursor.Close(); } // Setřídí seznam kontaktů podle jména... ContactList.Sort(); NumbersFilled = false; } /// /// Ke každému kontaktu v ContactList načte i jeho číslo (podle ID) /// - hodí se nám pro MessagesAdapter /// - při načítání jenom seznamu kontaktů je to zbytečné volat! (pomalé?) /// - cachuje si hodnoty, aby to příště bylo rychlejší :) /// public void FillNumbers() { lock (lockFillNumbers) { try { FillNumbers_Locked(); } catch (Exception e) { ExceptionLogger.LogException(e, false, false); } } } private void FillNumbers_Locked() { // Je potřeba aktualizovat tuhle cache, když přidáváme nějaké nové číslo / měníme ho! bool save = false; if (cachedNumbers.Count == 0) { // Načte hodnoty z Isolated Storage (je to rychlejší, než přes Cursory z Android API) // - načtené hodnoty rovnou uloží (cache se dále aktualizuje jen přes ActualizeCacheValue, AddCache, RemoveCache...) LoadNumbersFromStorage(); save = true; } for (int i = 0; i < ContactList.Count; i++) { var c = ContactList[i]; if (cachedNumbers.ContainsKey(c.Id)) c.Number = cachedNumbers[c.Id]; else { try { // Mohlo zde opět padat, že CursorWindows nebylo vytvořeno // - stává se, když se 2x znovu dotazuji na stejné číslo kontaktu? // tohle cachování by to mělo snad řešit... Kdyžtak opravit! // - v API Androidu 2.3 se musí načítat každé číslo kontaktu zvlášť, nejde to už v FillNumbers... // - tohle je ale navržené OK, pokud to náhodou načetlo "", příště už to načte správně c.Number = GetNumberByID(c.Id); } catch { } if (c.Number == null) c.Number = ""; if (c.Number != "") cachedNumbers.Add(c.Id, c.Number); } } if (save) SaveNumbersToStorage(); NumbersFilled = true; } /// /// Vrátí telefonní číslo kontaktu podle jeho ID (nastavuje jako parametr kontaktu v ContactsList) /// - voláno z FillNumbers /// public string GetNumberByID(long Id) { lock (lockGetNumberByID) { return GetNumberByID_Locked(Id); } } private string GetNumberByID_Locked(long Id) { // Načte od daného kontaktu 1 číslo (otevře a zavře si vlastní kurzor) var uri = Phone.ContentUri; String where = Phone.InterfaceConsts.ContactId + " = ?"; String[] selectionArgs = { Id.ToString(CultureInfo.InvariantCulture) }; ICursor cursor = null; string loadedPhone = ""; try { cursor = Game.Activity.ContentResolver.Query(uri, null, where, selectionArgs, null); if (cursor == null) return cachedNumbers.ContainsKey(Id) ? cachedNumbers[Id] : ""; if (cursor.MoveToFirst()) { // OK, kurzor je fajn, získáme z něj to číslo (a zaktualizujeme případně cache) String phone = cursor.GetString(cursor.GetColumnIndex(Phone.Number)); loadedPhone = phone ?? (cachedNumbers.ContainsKey(Id) ? cachedNumbers[Id] : ""); } } catch (Exception e) { ExceptionLogger.LogException(e, false, false); } finally { // Na konci uzavře cursor if (cursor != null) cursor.Close(); } // Kdybychom chtěli všechna čísla daného kontaktu (viz 1. odpověď): http://stackoverflow.com/questions/9644704/how-to-get-specific-contact-number-by-using-contact-id return loadedPhone; } /// /// Podle ID vrátí číslo kontaktu ze seznamu /// - pokud seznam zatím není načtený, načte ho /// public Contact ReturnContactByID(long contactID) { // Obecně FillContacts a FillNumbers není voláno pokaždé! // - např. u výpisu hovorů by se volalo na každý kontakt... // - ale můžeme si to vždy zavolat sami (např. při otevření obrazovky výpisu hovorů) if (ContactList == null) FillContactsFromPhone(); if (!NumbersFilled) FillNumbers(); lock (lockContactNames) { // Vrátí objekt kontaktu podle daného ID (ten stav null nastane jen tehdy, když tam neexistuje) if (ContactList == null) return new Contact() { Id = contactID, InContactList = false }; for (int i = 0; i < ContactList.Count; i++) if (ContactList[i].Id == contactID) return ContactList[i]; } return new Contact() { Id = contactID, InContactList = false }; } /// /// Vrátí ID kontaktu a "display name" pro dané telefonní číslo /// - číslo může být "podobné" (tj. zde se otestuje na mezery, chybějící předvolbu apod.) /// - využití: SMS zprávy, nebo nepřijaté hovory - načte k danému číslu jméno /// - voláno z MessagesAdapter a CallListAdapter.FillList - asynchronně! /// - ale asynchronnost je pořešena o úroveň výš (aby si nemohl zapisovat do jednoho pole z víc míst) /// - přeci jen to tu znovu lockujeme /// public void LoadContactNameBySimilarNumber(string contactNumber, out string displayName, out long contactID) { if (ContactList == null) FillContactsFromPhone(); if (!NumbersFilled) FillNumbers(); lock (lockLoadContactName) { try { LoadContactNameBySimilarNumber_Locked(contactNumber, out displayName, out contactID); } catch (Exception e) { ExceptionLogger.LogException(e, false, false); displayName = ""; contactID = 0; } } } private void LoadContactNameBySimilarNumber_Locked(string contactNumber, out string displayName, out long contactID) { // Vyhledá Id kontaktu + jeho jméno, podle "podobného čísla" (pro matchování v SMS) displayName = ""; contactID = -1; if (ContactList == null) return; for (int i = 0; i < ContactList.Count; i++) { var c = ContactList[i]; if (Operations.IsSimilarNumber(c.Number, contactNumber)) { displayName = c.DisplayName; contactID = c.Id; break; } } if (displayName == "") { displayName = contactNumber; // Pro ty, co nejsou v telefonním seznamu contactID = -1; } } /// /// Vytáhne si fotku kontaktu z contact URI (pokud existuje) /// - předává se rovnou odkaz na texturu, na ni se případně udělá dispose, pokud už tam něco je! /// - vrací null, pokud fotka neexistuje /// - neděje se zde cachování textury, je nutné tuhle metodu zavolat vždy z /// LoadContent a RefreshScreenGraphics (z dané obrazovky, kde je takový obrázek) /// public void GetFullPhotoOfContact(Contact contact, GraphicsDevice device, ContentManager content, ref Texture2D texture) { lock (lockGetFullPhoto) { GetFullPhotoOfContact_Locked(contact, device, content, ref texture); } } private void GetFullPhotoOfContact_Locked(Contact contact, GraphicsDevice device, ContentManager content, ref Texture2D texture) { if (device == null || content == null) { texture = null; return; } if (string.IsNullOrEmpty(contact.PhotoId)) { texture = null; return; } // Načte velkou fotku kontaktu if (((int) Build.VERSION.SdkInt) >= 14) { // Na Androidu 14+ použije tu verzi z PDA projektu try { Uri cUri = Uri.WithAppendedPath(ContactsContract.Contacts.ContentUri, Convert.ToString(contact.Id)); using (var inStream = ContactsContract.Contacts.OpenContactPhotoInputStream( Game.Activity.ContentResolver, cUri, true)) { Texture2D mgTexture = Texture2D.FromStream(device, inStream); texture = mgTexture; return; } } catch (Exception e) { // Zaloguje soft exception... ExceptionLogger.LogException(e, false, true); } texture = null; } else { // Starší Androidy: // - musíme trochu přes hack (Xamarin nemá potřebnou konstantu, API je už deprecated) // http://developer.android.com/reference/android/provider/ContactsContract.Contacts.Photo.html var contactUri = ContentUris.WithAppendedId(ContactsContract.Contacts.ContentUri, contact.Id); var contactPhotoUri = Uri.WithAppendedPath(contactUri, "display_photo"); AssetFileDescriptor fd = null; try { fd = Game.Activity.ContentResolver.OpenAssetFileDescriptor(contactPhotoUri, "r"); using (var inStream = fd.CreateInputStream()) { // Načte texturu ze streamu z daného Content URI, vytvoří MonoGame texturu... // - obrázek je velký 480x480 px, super :) // - není zde cachování textury, metoda se musí volat vždy z LoadContent a RefreshScreenGraphics Texture2D mgTexture = Texture2D.FromStream(device, inStream); texture = mgTexture; return; } } catch (Exception e) { // Zaloguje soft exception... ExceptionLogger.LogException(e, false, true); } finally { if (fd != null) fd.Close(); } texture = null; } } /// /// Přidá nový kontakt do telefonu, vrací přímo jako objekt kontaktu (aby se mohla zobrazit správná obrazovka) /// - automaticky zaktualizuje pole ContactsList, i cache Id/číslo /// public Contact AddNewContact(string number, string displayName) { try { // Via http://wptrafficanalyzer.in/blog/adding-contacts-programatically-using-contacts-provider-in-android-example/ var ops = new List(); int rawContactID = ops.Count; // Adding insert operation to operations list // to insert a new raw contact in the table ContactsContract.RawContacts ops.Add(ContentProviderOperation.NewInsert(ContactsContract.RawContacts.ContentUri) .WithValue(ContactsContract.RawContacts.InterfaceConsts.AccountType, null) .WithValue(ContactsContract.RawContacts.InterfaceConsts.AccountName, null) .Build()); // Adding insert operation to operations list // to insert display name in the table ContactsContract.Data ops.Add(ContentProviderOperation.NewInsert(ContactsContract.Data.ContentUri) .WithValueBackReference(ContactsContract.Data.InterfaceConsts.RawContactId, rawContactID) .WithValue(ContactsContract.Data.InterfaceConsts.Mimetype, StructuredName.ContentItemType) .WithValue(StructuredName.DisplayName, displayName) .Build()); // Adding insert operation to operations list // to insert Mobile Number in the table ContactsContract.Data ops.Add(ContentProviderOperation.NewInsert(ContactsContract.Data.ContentUri) .WithValueBackReference(ContactsContract.Data.InterfaceConsts.RawContactId, rawContactID) .WithValue(ContactsContract.Data.InterfaceConsts.Mimetype, Phone.ContentItemType) .WithValue(Phone.Number, number) .WithValue(Phone.InterfaceConsts.Type, 2) //2 = CommonDataKinds.Phone.TYPE_MOBILE .Build()); // Executing all the insert operations as a single database transaction var contentResolver = Game.Activity.ContentResolver; contentResolver.ApplyBatch(ContactsContract.Authority, ops); Operations.ShowToast(Engine.Lang.Get("new_contact_added") + ": " + displayName); } catch (Exception e) { Operations.ShowToast(Engine.Lang.Get("cannot_add_contact")); ExceptionLogger.LogException(e, false, false); } // Kontakt už je uložený v seznamu // - podle jména a čísla načteme aktuální ID FillContactsFromPhone(); FillNumbers(); //...vymazání bufferů obrazovek + přesměrování dál se děje v té metodě, co tohle volá (PressedGreenOKButton) Contact c = GetContactByNameAndNumber(displayName, number); if (c != null) AddCacheValue(c.Id, number); return c; } private Contact GetContactByNameAndNumber(string displayName, string number) { // Projde si načtený seznam kontaktů, vrátí objekt kontaktu podle jména / čísla lock (lockContactNames) { for (int i = 0; i < ContactList.Count; i++) { if (ContactList[i].DisplayName == displayName && ContactList[i].Number == number) { return ContactList[i]; } } } return null; } /// /// Nastaví danému kontaktu nové jméno a číslo, zaktualizuje ho v telefonu /// - obnoví i potřebné buffery (cache) /// public Contact UpdateContact(Contact contact, string number, string displayName) { // Zaktualizuje reálně kontakt v telefonu: // http://stackoverflow.com/questions/8490123/how-to-update-existing-contact?rq=1 try { var ops = new List(); int rawContactID = GetRawContactID(contact); if (rawContactID == -1) { Operations.ShowToast(Engine.Lang.Get("cannot_update_contact")); return contact; } // Jméno a číslo... var contentResolver = Game.Activity.ContentResolver; String where = ContactsContract.Data.InterfaceConsts.ContactId + " = ? AND " + ContactsContract.Data.InterfaceConsts.Mimetype + " = ?"; String[] nameParams = { Convert.ToString(contact.Id), StructuredName.ContentItemType }; String[] numberParams = { Convert.ToString(contact.Id), Phone.ContentItemType }; ops.Add(ContentProviderOperation.NewUpdate(ContactsContract.Data.ContentUri) .WithSelection(where, nameParams) .WithValue(StructuredName.DisplayName, displayName) .Build()); ops.Add(ContentProviderOperation.NewUpdate(ContactsContract.Data.ContentUri) .WithSelection(where,numberParams) .WithValue(Phone.Number, number) .Build()); // Executing all the insert operations as a single database transaction contentResolver.ApplyBatch(ContactsContract.Authority, ops); Operations.ShowToast(Engine.Lang.Get("new_contact_edited") + ": " + displayName); } catch (Exception e) { Operations.ShowToast(Engine.Lang.Get("cannot_update_contact")); ExceptionLogger.LogException(e, false, false); } // Zaktualizuje cache ActualizeCacheValue(contact.Id, number); contact.Number = number; contact.DisplayName = displayName; // A znovu načte aktuální data... FillContactsFromPhone(); return contact; } private int GetRawContactID(Contact contact) { // Načte "raw" ID kontaktu (to, podle kterého se přidávalo do seznamu) // - potřebujeme ho pro editaci (v UpdateContact) int outValue = -1; ICursor cursor = null; try { cursor = Game.Activity.ContentResolver.Query( ContactsContract.RawContacts.ContentUri, new[] { ContactsContract.RawContacts.InterfaceConsts.Id }, ContactsContract.RawContacts.InterfaceConsts.ContactId + "=?", new[] { Convert.ToString(contact.Id)}, null); if (cursor != null) if (cursor.MoveToFirst()) outValue = Convert.ToInt32(cursor.GetLong(0)); } catch (Exception e) { ExceptionLogger.LogException(e, false, false); } finally { if (cursor != null) cursor.Close(); } return outValue; } /// /// Smaže daný kontakt... (3. metoda z ContactDetailScreen) /// public void RemoveContact(Contact contact) { // Najde kontakt podle ID, smaže ho, uzavře zpátky cursor ICursor cursor = null; try { var cr = Game.Activity.ContentResolver; cursor = cr.Query(ContactsContract.Contacts.ContentUri, null, ContactsContract.Contacts.InterfaceConsts.Id + "=" + contact.Id, null, null); if (cursor == null) { Operations.ShowToast(Engine.Lang.Get("cannot_delete_contact")); return; } // Smaže pouze první, i kdyby jich tam bylo víc... if (cursor.MoveToNext()) { String lookupKey = cursor.GetString(cursor.GetColumnIndex(ContactsContract.Contacts.InterfaceConsts.LookupKey)); Uri uri = Uri.WithAppendedPath( ContactsContract.Contacts.ContentLookupUri, lookupKey); cr.Delete(uri, ContactsContract.Contacts.InterfaceConsts.Id + "=" + contact.Id, null); Operations.ShowToast(Engine.Lang.Get("contact_deleted") + ": " + contact.DisplayName); } } catch (Exception e) { Operations.ShowToast(Engine.Lang.Get("cannot_delete_contact")); ExceptionLogger.LogException(e, false, false); } finally { if (cursor != null) cursor.Close(); } // A znovu načte aktuální data... RemoveCacheValue(contact.Id); FillContactsFromPhone(); } } }