Вступление
Я программистом не являюсь уже давно, я админ. Но порой надо быстро сделать утилитку анализа логов, какую-нибудь автоматизацию — делаю, если не нахожу ничего похожего в инете за день поиска.
На андроиде моё возмущение сразу вызвали два факта — отсутствие мелодий у групп (ну почему, почему никто кроме Сони не додумался до этого очевидно необходимого функционала?) и невозможность сбакапить мелодии вместе с контактами. В Symbian, которая тогда была эталоном систем для смартов, последняя функция была.
Поиск ничего не дал по второму пункту и почти ничего — по первому. Это были суровые времена перехода с андроид 1.5 на андроид 1.6 и где-то на горизонте маячил Android 2.0.
Ну нет и нет — напишу, не боги горшки обжигают. Начал с более простого, с бакапа установленных на контак мелодий.
В итоге родилась программка, с простейшей функцией — бакапить соответствия имя-контакта = установленный-звонок и потом восстанавливать эти данные (её можно найти в маркете по названию Ringtone Keeper), а вот на каких граблях я постоял в процессе — я тут и опишу.
Андроид 1.6
Писать я решил сразу под 1.6, потому как сам гугль настойчиво советовал с 1.5 не заморачиваться. Ну нет так нет. С документацией в сети голяк, с сайта на сайт кочует один и тот же пример с одними и теми же комментариями. У самого гугла на developer.android.com примеров нет и очень аскетичные описания полей и функций. Основной ресурс — stackoverflow.com/questions/, там в ответах на вопросы удаётся углядеть логику. Прочёл пару книжек для чайников и поехали. Программа рождается, грабли лезут.
Ну например: все данные по звуку лежат в андроиде в MediaStore. Но лежат они там в виде индексной таблицы и вытащить из неё имена файлов (а мне нужны именно имена файлов, после переустановки системы с высокой вероятностью те же самый файлы будут иметь в MediaStore другие индексы) довольно нетривиально.
Примерно та же песня с контактами. Но с ними хоть примеры были, а всю войну со звуком пришлось нащупывать методом проб и ошибок.
Ещё оригинальнее сделана установка звонка контакту. Опять же надо произвести преобразование имя файла — индекс в MediaStore. Но этого мало! Если присвоить контакту этот индекс на мелодию, то мелодия у контакта удалиться и всё. Файл, который есть в MediaStore, надо тем не менее в нём зарегистрировать (метод withAppendedId) и только полученный при регистрации Uri можно пихать в контакт.
Вот зачем так сложно, спрашивается? Или я чего-то не нашел?
Андроид 2.0
Программа работала, но тут пришло время обновляться на андроид 2.0. А у гугла всё как обычно. Он изменил SDK по работе с контактами, а заодно, чтоб два раза не вставать — сохранил старое SDK, но перевёл его в режим read-only. То есть режим чтения данных контакта работает и старый.
Два примера кода:
SDK 1.6
String[] selectCols = { People._ID, People.NAME, People.CUSTOM_RINGTONE };
if ((ContactName == null) || (ContactName.length() < 1)) { return DATA_NOT_FOUND; }
// Ищем контакт по имени
ContentResolver localContentResolver = this.getContentResolver();
Cursor cur = localContentResolver.query(
Uri.withAppendedPath(People.CONTENT_FILTER_URI,
Uri.encode(ContactName)),
selectCols,
null,
null,
null);
if (cur.moveToFirst()) {
int cID = cur.getInt(cur.getColumnIndex(People._ID));
return cID;
}
return DATA_NOT_FOUND;
SDK 2.0
tring[] selectCols = { ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.CUSTOM_RINGTONE };
if ((ContactName == null) || (ContactName.length() < 1)) { return DATA_NOT_FOUND; }
// Ищем контакт по имени
ContentResolver localContentResolver = this.getContentResolver();
Cursor cur = localContentResolver.query(
Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, Uri.encode(ContactName)),
selectCols,
null,
null,
null);
if (cur.moveToFirst()) {
int indID = cur.getColumnIndex(ContactsContract.Contacts._ID);
return cur.getInt(indID);
}
return DATA_NOT_FOUND;
}
То есть можно просто поменять People на ContactsContract.Contacts и .NAME на .DISPLAY_NAME. Можно даже не менять — на чтение старый способ работает прекрасно!
А вот с записью беда.
Старый SDK:
Uri contactUri = ContentUris.withAppendedId(People.CONTENT_URI, cID);
ContentValues value = new ContentValues();
value.put(People.Phones.TYPE, People.TYPE_MOBILE);
value.put(People.NUMBER, "+7 (777) 777-77-77");
ContentResolver localContentResolver = this.getContentResolver();
int updRow =
localContentResolver.update(contactUri, value, null, null);
localContentResolver.notifyChange(People.CONTENT_URI, null);
return ((updRow > 0));
Этот код собирается, запускается, работает, ни единой ошибки — только пишет он либо в /dev/null, либо в старую базу, из которой никто уже не читает.
Тут замена People на ContactsContract.Contacts обязательна, без этого никак.
Хотя казалось бы, сделай синонимы и пусть старый код по старым правилам пишет в реальную базу. Ну или сделай ошибку для People, если платформа выше 1.6. Не сделали ни того, ни другого. Спрашивается — зачем такие сложности?
Маркет и люди
На этом примерно этапе я решил выложить программу в маркет (среди моих знакомых она шла на ура, я подумал, пусть будет). И при массовом использовании полезли следующие грабли.
Поиск контакта возвращает его имя в формате, заданном системой. Если у вас при бакапе стоял формат отображения контакта «Фамилия, Имя», то контакт так и запишется «Иванов Вася» (как его вернёт DISPLAY_NAME). Но после переустановки системы формат стоит «Имя, Фамилия». Пользователь нажимает Восстановить, программа честно запускает поиск контакта «Иванов Вася» — и не находит его, потому что система считает, что «Иванов Вася» и «Вася Иванов» — это разные люди. Как можно объяснить это системе, я не придумал и просто вынес парсинг контактов и их поиск к себе в программу. Скорость упала драматически, звонки на 200 контактов устанавливаются секунд 20-30. Понятно, что зависшая на 30 секунд программа — это баг. Нужна табличка с вывеской «Работаю…» и индикатором активности. Слава гуглу, андроид её предлагает! Тут я узнал много нового про параллельность в андроиде, но во всём этом хоть какая-то логика прослеживается, так что промолчу. Это не грабли, это мясник, он так видит (с).
Дальше пользователи стали просить сделать сохранение-восстановление звонка по умолчанию и звука смс. И снова грабли — простейшее очевиднейшее решение
Settings.System.DEFAULT_RINGTONE_URI при установленном внешнем (с sdcard) рингтоне возвращает результат вида "/system/audio/ringtone". При этом и MediaStore, и RingtoneManager наотрез отказываются догадаться, что это за файл. Пришлось привычно лезть через антифасад, использовать RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_RINGTONE) и потом конвертировать его возврат в имя файла.
И тот же баян при установке звонка или смс — пока его не добавишь в MediaStore, на android 2 и 3 он не устанавливается. В android 4 этот баг наконец-то похоже пофиксили, потому что там всё работает и без ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, ringID).
Ещё одним частым вопросом пользователей была возможность выбора места, куда класть бакап и имена файла бакапа (сейчас это жёстко задано программой). И тут я обнаружил (фанфары, Win3.11 нервно переворачивается в гробу), что у андроида НЕТ стандартного диалога выбора файла/каталога. Нужен — садись сам и пиши. На этом месте я сказал «хватит извращений» и стал всем отвечать, что в связи с отсутствием наличия писать такую штуку самому мне лень и пусть они используют любой файл-менеджер для переноса бакапов куда им угодно.
Но почему его нет, почему?
Вопросы
В итоге — программа написана, работает, но остались вопросы — зачем всё так сложно? В гугле андроид API пишет несколько разных конкурирующих команд? Почему не учтён опыт предшествующих систем, почему очевидные функции делаются автогеном через анус или отсутствую вообще?
Кто это всё придумал и зачем?
Или не всё так плохо, а я просто не нашёл ответов?
Автор: Smithson