Pull to refresh

Программирование в Android — зачем такие сложности?

Programming *Google API *
Sandbox
Вступление

Я программистом не являюсь уже давно, я админ. Но порой надо быстро сделать утилитку анализа логов, какую-нибудь автоматизацию — делаю, если не нахожу ничего похожего в инете за день поиска.

На андроиде моё возмущение сразу вызвали два факта — отсутствие мелодий у групп (ну почему, почему никто кроме Сони не додумался до этого очевидно необходимого функционала?) и невозможность сбакапить мелодии вместе с контактами. В 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 пишет несколько разных конкурирующих команд? Почему не учтён опыт предшествующих систем, почему очевидные функции делаются автогеном через анус или отсутствую вообще?

Кто это всё придумал и зачем?
Или не всё так плохо, а я просто не нашёл ответов?
Tags:
Hubs:
Total votes 69: ↑41 and ↓28 +13
Views 27K
Comments Comments 49