среда, 14 марта 2012 г.

Идентификаторы Android-контакта: ContactID и LookupKey. Нюансы и баги.

Согласно документации, контакт в Android характеризуется двумя идентификаторами: contactId и lookupKey. Предположим, мы создаем приложение, работающее с определенным контактом. Пример - мое приложение Animated Widget Contact Launcher, которое позволяет создавать для контактов виджеты быстрого доступа. Какой идентификатор контакта нужно хранить в настройках такого виджета - contactId или lookupKey? Или оба? Как правильно создавать ссылку на контакт? Практика показала, что вопрос не тривиален.

Идентификаторы контакта

Итак, у контакта есть два идентификатора. ContactID представляет из себя обычное число, типа long: 2, 40, 3222 и т.д. LookupKey - это кодированная строка типа "1157icbbec86124b1b50", "30410abc...gmail.com" и т.д.

Если вам известен хотя бы один идентификатор, то вы можете получить URI контакта и, через URI, запросить любую информацию о контакте. Вот как это делается:
//Вариант 1: известен contactID
Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI,
            Long.parseLong(contactId));
//Вариант 2: известен lookupKey
Uri uri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey)
//Вариант 3: известны оба идентификатора
Uri uri = getLookupUri(contactId, lookupKey) 
//вариант 3 не работает под 2.1 из-за бага андроида

//Пример: находим имя контакта
Cursor c = getContentResolver().query(uri, new String[]{Contacts.DISPLAY_NAME}, null, null, null);
try {
    c.moveToFirst();
    String displayName = c.getString(0);
} finally {
    c.close();
}
О том, как правильно использовать Contact URI (и Contact API в целом) можно почитать, например, здесь и здесь.

Первые два варианта подходят, когда вам известен один из идентификаторов. Третий вариант - когда известны оба (в 2.1 он не работает). Вопрос - зачем нам вариант три, когда первых двух вроде бы достаточно? Вариант 3 нужен потому, что идентификаторы контакта могут изменяться с течением времени.

Непостоянство contactId и lookupKey

ContactId может измениться при агрегации контактов. Скажем, есть у вас в контактах пользователь Вася, contactId = 1. Вы установили на свой телефон skype. В скайпе Вася у вас тоже есть, на телефоне появляется контакт Вася(2) с contactId = 100. Андроид автоматически объединяет эти контакты в общий, агрегированный контакт Вася(3) с contactId = 200. Если после агрегации вы попробуете найти контакт Вася(1) с contactId = 1, то вы его не найдете. Контакт потерялся.

Чтобы избежать такой потери контактов, разработчики Android и ввели lookupkey. Если вы вместе с contactId=1 сохранили второй идентификатор контакта lookupkey="abc", то используя вариант поиска номер 3, вы без проблем найдете агрегированный контакт Васи:
Uri uri = getLookupUri(1, "abc");
Здесь есть тонкий момент. Идентификатор lookupKey может изменяться. Как правило он изменяется после редактирования свойств контакта. Так что в SQL-запросах к Contact API никогда нельзя включать явные выражения типа " and (LOOKUP_KEY='abc')" - будет работать, но до поры до времени. Lookup Key нужно передавать в getLookupUri, получать Uri, а дальше работать c Uri и contactId, не используя более lookupKey.

Если lookupKey изменился - можно ли будет по нему найти контакт? Практика показывает, что можно - устаревшие lookupKey работают корректно.

Как ссылаться на контакт?

Мы подходим к вопросу - если требуется сохранить ссылку на контакт, какой идентификатор сохранять? Одного contactId однозначно не хватает. Можно ли обойтись одним lookupKey?

В документации написано буквально следующее:
If performance is a concern for your application, you might want to store both the lookup and the long ID of a contact and construct a lookup URI out of both IDs...

When both IDs are present in the URI, the system will try to use the long ID first. That is a very quick query. If the contact is not found, or if the one that is found has the wrong lookup key, the content provider will parse the lookup key and track down the constituent raw contacts. If your app bulk-processes contacts, you should maintain both IDs. If your app works with a single contact per user action, you probably don't need to bother with storing the long ID
.

Другими словами, для однозначной идентификации контакта достаточно хранить один лишь lookupKey. На самом деле, это не так.

Неоднозначность LookupKey

Мое приложение Animated Contact Widget предназначено для быстрого доступа к контактам. Естественно, ему приходится хранить ссылки на контакты. Вплоть до текущей версии в информации о контакте хранился единственный идентификатор - lookupKey. В 99% случаев все работало без проблем. Но время от времени приходили единичные письма от пользователей, которые сообщали о странном баге - выбираешь один контакт, а виджет создается для другого...

В чем дело я не мог понять очень долго. На stackoverflow упоминался аналогичный баг, но дельного ответа на него никто не дал.

Наконец нашелся пользователь, который не поленился и помог мне отыскать причину проблемы (за что ему огромнейшее спасибо). Отладочные логи показали, что на девайсе пользователя примерно треть контактов имеет ОДИНАКОВЫЕ lookupKey. Что же удивляться, что контакт выбирается не тот... Вот фрагмент лог файла:
k=1957i5 c=152
k=1957i5 c=153
k=1957i46d39cd0743de699 c=154
k=1957i327657105acc3975 c=155
k=1957i5 c=394
k=1957i300808f8df4189b1 c=156
k=1957i660d8a742c21a4a6 c=411
k=1957i3793876d5c49591e c=157
k=1957i5 c=158
k=1957i250bebb5bd1009fd c=170
k=1957i39112193df972581 c=171
k=1957i5 c=431
k=1957i2c74309cd7539f2b c=169

Здесь c - contactId, k - lookupKey.

Трудно судить, насколько часто такая проблема проявляется. Сообщений о баге я получил с десяток. Плюс, наверное, было десятка два комментариев на маркете. Из полмиллиона закачек - это мизер. Тем не менее, сообщения об этой ошибке приходили стабильно, от пользователей с разными устройствами. Проблема точно проявлялась на некоторых HTC-девайсах (Desire, Wildfire S).

Мы попробовали выявить общий знаменатель между "бракованными" контактами. Не получилось. Есть подозрение, что шалит синхронизация HTC Sense - Outlook, но не факт, что дело в ней...

Начиная с версии 1.5.5, Animated Widget использует два идентификатора контакта для ссылки на контакт. Баг пофиксился.

Еще бывает Factory Reset

Factory Reset - сброс к заводским настройкам. Сброс приводит к тому, что у контактов меняются оба идентификатора - и contactId и lookupKey. После заводского сброса контакт по ранее сохраненным идентификаторам найти не получится. Нужно искать его по косвенной информации - телефону, ФИО и т.д. Если она сохранена.

Выводы

Вывод прост: если вам нужно сохранить информацию о контакте так, чтобы в дальнейшем этот контакт можно было гарантированно найти, сохраняйте оба идентификатора контакта - lookupKey и contactId. Одного единственного lookupKey не достаточно, несмотря на то, что написано в SDK. Ну а если контакт нужно суметь найти даже после заводского сброса, необходимо хранить так же дополнительную информацию о контакте: телефоны, ФИО и т.д.

2 комментария:

  1. Здравствуйте. Если getLookupUri(...) гарантированно возвращает Uri контакта по двум параметрам, может быть стоит именно его и сохранять для дальнейшей работы?

    ОтветитьУдалить
  2. Спасибо, ваша статья прояснила многое

    ОтветитьУдалить