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