суббота, 29 марта 2014 г.

Работаем со списком контактов, #1

Мне уже не раз писали, и спрашивали «как же работать с контактами на Android?». Я отправлял людей читать АПИ Андроида. Недавно получив ещё один подобный вопрос (Спасибо «helper999999» :), я всё-таки решил написать статью на эту тему и ответить на данный вопрос. Возможно, позже появятся статьи о работе с СМС и звонками.

Upd (20.04.14). Проверил код на Delphi XE6 и добавил информацию о необходимых изменениях.


Прим.1. В конце статьи, как обычно есть исходный код приложения.

Прим.2. Я намеренно писал весь код под API 10 (Android 2.3.3), чтобы код работал и на минимально поддерживаемых версиях Android (Определяем, поддерживается ли наше устройство для разработки приложений)

Прим.3. Будет продолжение данной статьи.

Кстати очень интересно, зачем может понадобиться такой функционал в вашем приложении? Ответы пишите в комменты.


Начнём с теории. (Кратко)

Прим. Некоторый материал взят и переведён из официальной документации по Android API.

Список контактов в Android’е хранится в Базе Данных, которая имеет 3 основные таблицы, это «Contacts»; «Raw_Contacts»; «Data». Также есть ещё немало вспомогательных таблиц, но нам они пока не понадобятся.

Примерная схема таблиц (Автор: Habrahabr -> User -> BreatheInMyVoid):


Структура:

Для работы с приложением «Контакты» в Android API есть класс «ContactsContract» (http://developer.android.com/reference/android/provider/ContactsContract.html). Также есть класс «Contacts», но не используйте его, т.к. с API 5 он был помечен как «не рекомендуемый».

Обязательно нужно дать приложению разрешение на чтение контактов «Uses Permissions: READ_CONTACTS=true»

Чтобы использовать класс «ContactsContract», необходимо воспользоваться контент-провайдером (Content Provider).
Контент-провайдер – это способ получения доступа к информации из другого приложения.
Посмотреть какие Контент-провайдеры уже встроены в Android можно в документации.

Для получения доступа к различным данным используется класс «ContentResolver» и метод «query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)». Т.е. по сути, этот метод выполняет SQL запросы к контент-провайдеру.

Для того чтобы получить доступ к таблице «Contacts» необходимо указать идентификатор URI: «TJContactsContract_Contacts.JavaClass.CONTENT_URI» (на Java: «ContactsContract.Contacts. CONTENT_URI»)
По аналогии вы можете получить доступ к двум оставшимся таблицам.


Переходим к практике.

Создаём мобильное приложение.
На форму кладём TTabControl, создаём две вкладки. На первой вкладке будем по нажатию на кнопку выводить список контактов (имена). На второй вкладке будем выводить все номера телефонов принадлежащие одному, выбранному контакту. Выводить имена и телефоны будем в два ListBox’а.

Выглядит это так:


Структура проекта:

Сразу добавляем в Uses необходимые для работы модули:
uses
  FMX.Helpers.Android, Androidapi.JNI.GraphicsContentViewText,
  Androidapi.JNI.Provider, Androidapi.JNI.JavaTypes;

Если вы прочитали теорию, то уже знаете с чего начать. Нам необходимо получить доступ к таблице «Contacts» и извлечь идентификаторы (_ID) и имена контактов (DISPLAY_NAME), которые отобразим в ListBox1.

Для этого пишем такой код:
procedure TForm1.Button1Click(Sender: TObject);
var
  cursor: JCursor;
  ListBoxItem : TListBoxItem;
begin

  cursor := SharedActivity.getContentResolver.query(
   TJContactsContract_Contacts.JavaClass.CONTENT_URI,
    nil,
     nil,
      nil,
       nil);

  ListBox1.Clear;
  ListBox1.BeginUpdate;

  if(cursor.getCount > 0) then
  begin

    Label1.Text := 'Всего: ' + IntToStr(cursor.getCount);

    while (cursor.moveToNext) do
    begin

      ListBoxItem := TListBoxItem.Create(ListBox1);

      ListBoxItem.ItemData.Text := JStringToString(cursor.getString(
        cursor.getColumnIndex(StringToJString('DISPLAY_NAME'))));

      ListBoxItem.Tag := cursor.getInt(cursor.getColumnIndex(StringToJString('_ID')));
      ListBox1.AddObject(ListBoxItem);

    end;
  end;

  cursor.close;

  ListBox1.EndUpdate;
end;

Запустив приложение и нажав на кнопку, получим список. Полистав список, вы заметите, что в нём указаны все контакты, т.е. даже те контакты, у которых есть только email. А также все контакты выводятся не отсортированными.


Вообще контакты могут быть как минимум 3-х типов: Контакты на устройстве, на сим-карте и в аккаунте гугла. О том, как определять тип контакта мы поговорим в отдельном дополнении к статье.

А пока давайте выясним, как же можно вывести все контакты, имеющие хотя бы один телефонный номер и желательно отсортировать их по алфавиту. Для того чтобы сделать это, необходимо добавить в наш запрос («SharedActivity.getContentResolver.query») несколько условий.

Вы, скорее всего, заметили, что метод «query» принимает 5 параметров (пока у нас указан только первый). Давайте укажем условие выборки «WHERE» определённых записей.

В таблице «Contacts» есть столбец «HAS_PHONE_NUMBER», в нём хранится значение «0» или «1». Если указана единица, то это означает, что у контакта есть хотя бы один номер телефона. Если указан ноль, то номеров телефона нет.

Как же нам добавить это условие в наш запрос, всё просто, за условие выборки отвечает третий параметр запроса.
За условие сортировки отвечает последний (пятый) параметр. Сортировать будем по столбцу «DISPLAY_NAME» (имя контакта).

В итоге у нас получится такой код запроса:
  cursor := SharedActivity.getContentResolver.query(
   TJContactsContract_Contacts.JavaClass.CONTENT_URI,
    nil,
     StringToJString('HAS_PHONE_NUMBER = 1'),
      nil,
       StringToJString('DISPLAY_NAME ASC'))

И результатом будет список контактов, содержащих хотя бы один номер телефона.

Ну вот, теперь мы умеем выводить отсортированный список контактов.
Далее необходимо вывести все номера для отдельно взятого контакта.
Для этого:
  1. Идентификатор каждого выведенного контакта я сохранил в свойство «Tag»
  2. Создал событие «OnItemClick»
  3. Переместился на вторую вкладку в TTabControl
  4. Написал код для обработчика.
Самый простой способ извлечь номера и их типы (домашний, мобильный, факс и т.д.), создать запрос на извлечение данных, указав идентификатор URI: «TJCommonDataKinds_Phone.JavaClass.CONTENT_URI» и условие выборки по столбцу («CONTACT_ID» = Идентификатор выбранного контакта). Сам номер хранится в поле «DATA1»(NUMBER), а тип в «DATA2»(integer) (Список возможных значений: ContactsContract.CommonDataKinds.Phone).

Далее пишем код:
procedure TForm1.ListBox1ItemClick(const Sender: TCustomListBox;
  const Item: TListBoxItem);
var
  id: integer;
  FIO: string;
  cursor: JCursor;
  ListBoxItem : TListBoxItem;
begin

  TabControl1.ActiveTab := TabItem2;

  id := Item.Tag;
  FIO := Item.Text;

  Label2.Text := FIO;

  cursor := SharedActivity.getContentResolver.query(
   TJCommonDataKinds_Phone.JavaClass.CONTENT_URI,
    nil,
     StringToJString('CONTACT_ID = ' + IntToStr(id)),
      nil,
       nil);

  ListBox2.Clear;
  ListBox2.BeginUpdate;

  while (cursor.moveToNext) do
  begin

    ListBoxItem := TListBoxItem.Create(ListBox2);

    ListBoxItem.ItemData.Text := JStringToString(cursor.getString(
      cursor.getColumnIndex(TJCommonDataKinds_Phone.JavaClass.NUMBER)));

    ListBoxItem.ItemData.Detail := IntToStr(
    cursor.getInt(cursor.getColumnIndex(StringToJString('DATA2'))));

    ListBox2.AddObject(ListBoxItem);

  end;

  cursor.close;

  ListBox2.EndUpdate;

end;


UPDATE (20.04.14):
Чтобы код заработал в Delphi XE6, необходимо:
в "uses" заменить модуль "Androidapi.JNI.JavaTypes" на "Androidapi.Helpers".

Архив обновлён (Добавил комментарий для Delphi XE6)!

Как в итоге это выглядит:


Итог: Теперь вы умеете извлекать, сортировать список контактов (имён и телефонов).

Я решил не писать одну огромную статью, а разделить данную тему на несколько статей. В следующей статье я покажу, как определить тип контакта и соответственно выводить контакты только определённого типа (сим-карты, гугл аккаунта или из памяти телефона).

О найденных ошибках пишите в комменты, либо мне на почту.

p.s. Оказалось, что подробной информации о работе с контактами (тем более в Delphi) в интернете почти нет, поэтому моя статья вроде как уникальна (строго не судите, старался написать понятно, но чтобы было не много буков :)

Удачной разработки!

Исходный код: Скачать с Google Drive

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

  1. у метода insert в качестве праметров передается URI и ContentValues,
    заполняя ContentValues я столкнулся с тем, что не все идентификаторы есть в обертке провайдеров
    например пытаюсь добавить календарь:

    ContentValues := TJContentValues.Create;
    Calrndar_URI := TJCalendarContract_Calendars.JavaClass.CONTENT_URI;
    ContentValues.put(TJCalendarContract_Calendars.JavaClass.ACCOUNT_NAME, StringToJString('dashboard@site.ru'));
    ContentValues.put(TJCalendarContract_Calendars.JavaClass.ACCOUNT_TYPE, TJCalendarContract_Calendars.JavaClass.ACCOUNT_TYPE_LOCAL);
    ContentValues.put(TJCalendarContract_Calendars.JavaClass.NAME, StringToJString('dashboard@site.ru'));
    ContentValues.put(JCalendarContract_Calendars.JavaClass.VISIBLE, 1);
    Calrndar_URI.buildUpon().appendQueryParameter(TJCalendarContract_Calendars.JavaClass.CALLER_IS_SYNCADAPTER, StringToJString('TRUE'));
    Calrndar_URI.buildUpon().appendQueryParameter(TJCalendarContract_Calendars.JavaClass.ACCOUNT_NAME, StringToJString('ACCOUNT_NAME'));
    Calrndar_URI.buildUpon().appendQueryParameter(TJCalendarContract_Calendars.JavaClass.ACCOUNT_TYPE, TJCalendarContract_Calendars.JavaClass.ACCOUNT_TYPE_LOCAL);
    SharedActivity.getContentResolver.insert(Calrndar_URI, ContentValues);

    и из всего что нужно, доступен только "TJCalendarContract_Calendars.JavaClass.NAME"
    p.s. как преобразовать Integer в JInteger?

    ОтветитьУдалить
    Ответы
    1. По умолчанию, все обёртки написаны для API 10, хотите больше возможностей или просто нового функционала, дописывайте обёртку самостоятельно. Проще говоря, по умолчанию реализованы только необходимые классы, для работы FireMonkey.
      Необходимый вам функционал появился в API 14, поэтому я вообще удивлен, что этот класс (CalendarContract.Calendars) частично уже написан.
      «JInteger» - нет такого, Integer так и остаётся Integer.

      Удалить
    2. Кстати говоря, готовые обёртки, сгенерированные автоматически, можно взять тут: https://github.com/FMXExpress/android-object-pascal-wrapper/

      Удалить