Showing posts with label android. Show all posts
Showing posts with label android. Show all posts

Sunday, February 16, 2014

Доступ к ресурсам Android приложения

Бывает так, что во время разработки, нужно заглянуть в приватные директории приложения. Как известно, доступ к ним закрыт, и при попытке, скажем, получить список файлов, имеем следующее:

adennis@foofive ~ $ adb shell
shell@android:/ $ ls -al
...
drwxrwx--x system   system            2012-09-24 23:38 data
...
shell@android:/ $ cd data/
shell@android:/data $ ls
opendir failed, Permission denied

Есть несколько решений данной проблемы.

root

Первое, что приходит на ум - получить root права. Недостатки очевидны. Ломать телефон из-за маленькой плюшки - варварство(да и телефон может быть не личный, а, например, корпоративный). Но имея root, получаем неограниченный доступ ко всем ресурсам системы.

run-as

В составе Android OS есть утилита run-as. По своей сути очень похожа на sudo - выполнение команд от имени приложения или говоря короче - делегирование прав:

adb shell
run-as com.your.application ls -l /data/data/com.your.application
run-as com.your.application rm /data/data/comp.your.application/databases/database.db
…
# or your can run it in a interractive mode 
run-as com.your.application
shell@android:/data/data/com.your.application$ ls -l
cache
databases
lib
…

Пара моментов которые нужно знать... Приложение должно быть собрано как debuggable и в нек. случаях нужно менять права на файлы/директории:

adb shell 'run-as com.your.application chmod 666 /data/data/com.your.application/databases/databases.db'

Из недостатков можно назвать лишь одно - нет возможности доступа к файлам приложений третьих лиц(установленных, например, с Play Market).

backup

Последний способ. Я его использовал до того, как познакомился с run-as. Android предоставляет возможность сделать копию приложения:

adb -s [device id] backup -f backup.dat -noapk com.your.application

В текущей директории будет создан файл backup.dat([device id] можно узнать командой 'adb devices'). Далее распаковываем *.dat файл:

dd if=backup.dat bs=1 skip=24 | openssl zlib -d | tar -xvf -

Приведенная команда у меня работает только под Linux, под Mac OS я получаю сообщение об ошибке, мол openssl собран без поддержки zlib. Немного пошаманив, нашел решение, которое не требует пересборки openssl:

dd if=backup.dat bs=1 skip=24 | python -c "import zlib,sys;sys.stdout.write(zlib.decompress(sys.stdin.read()))" | tar -xvf -

По сравнению с Linux вариантом, работет медленнее, но для меня не критично. После всех шагов, в текущей директории будет создана папка apps, содержащая все внутренности приложения.

До недавнего времени последний способ не имел недостатков. Т.е. была возможность получить доступ ко всем потрохам без каких-либо ограничений. Но начиная с версии Android 4.4 в AndroidMainifext.xml был добавлен флаг allowBackup:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.your.application">
...
    <application
...
        android:allowBackup="true"/>
...
</manifest>

Который гласит буквально следующее: если флаг установлен в значение false, то приложение никогда не будет иметь возможность сделать резервную копию или восстанновление из резервной копии, даже в случае резерного копирования всей системы(имеется ввиду средствами самой ОС). Поумолчанию флаг имеет значение true. Вот такие дела. Т.е. доспут к ресурсам своего приложения мы всегда будем иметь, а в остальном - как повезет.

Источники

Monday, July 8, 2013

SQLite в андроид приложении

Android предоставляет различные механизмы хранения данных приложения, от простых preference в виде ключ-значение до пользовательских баз данных в виде sqlite. Сегодня речь пойдет о втором способе - встраиваемой БД. Мотивацией к написанию данного топика послужило то, что информация о некоторых моментах весьма скудна, примеры приведенные в руководстве разработчика андроид приложений и в книгах, порой вызывают больше вопросов чем их было до момента чтения. Можно найти типовые примеры с небольшими вариациями, но пользы от этого мало. Я прекрасно понимаю, что цель упомянутых статей скорее не научить, а дать представление, но ознакомившись с базовыми техниками, хотелось бы найти источники демонстрирующие т.н. best practices или хотябы раскрывающие вопросы отличные от начального уровня. Интересовало, собственно, следующее:

  • Где/когда и как создавать(правильно) SQLiteOpenHelper?
  • Сколько экземпляров данного класа может создать в приложении? Один или несколько, скажем, по экземпляру в ContentProvider?
  • Если в каждом ContentProviderможет быть по экземпляру SQLiteOpenHelper, то как ими управлять(и нужно ли)? Я имею ввиду закрывать соединение с базой.
  • Как правильно организовать доступ к базе в многопоточном приложении? Например, что будет, если в одновременно попытаться сделать запись в базу из разных тредов?
Очевидно, что я не первый, кто задался похожими вопросами. Поэтому поиск ответов начал с интенсивного гугления. И таки кое-что удалось найти. Полный перевод приводить не буду, лишь сделаю выжимку из прочитанного. Итак по порядку.

Абстрактная фабрика для SQLiteDatabase

Самый простой способ организации доступа к БД приложения - создание наследника класса SQLiteDatabase, который реализует статический метод фабрику. Момент который нужно усчесть - это использование контекста приложения, а не активти либо сервиса:

public class DatabaseHelper extends SQLiteOpenHelper { 
 
  private static DatabaseHelper instance = null;
 
  private static final String DATABASE_NAME = "dbname.db";
  private static final int DATABASE_VERSION = 1;
 
  public synchronized static DatabaseHelper getInstance(Context ctx) {
      
    // Use the application context, which will ensure that you 
    // don't accidentally leak an Activity's context.    
    if (instance == null) {
        instance = new DatabaseHelper(ctx.getApplicationContext());
    }
    return instance;
  }
     
  private DatabaseHelper(Context ctx) {
      super(ctx, DATABASE_NAME, null, DATABASE_VERSION);
  } 
  ...
} 

В рантайме будет только один экземпляр SQLiteOpenHelper, которую можно получить из любого компонента, вызвав соответствующий метод фабрики. Использование контекста приложения, позволяет избежать такого рода ошибок:

E/Database(234): Leak found
E/Database(234): Caused by: java.lang.IllegalStateException:
                 SQLiteDatabase created and never closed

SQLiteDatabase обернутый в ContentProvider

Как говорят сторожилы: This is also a nice approach. Каждая реализация ContentProvider-а может содержать приватную ссылку на SQLiteDatabase. Более того, нет необходимости управлять соединением вручную(закрывать его). Вот такой ответ я нашел по этому поводу: A content provider is created when its hosting process is created, and remains around for as long as the process does, so there is no need to close the database -- it will get closed as part of the kernel cleaning up the process's resources when the process is killed. Резюмируя написанное, будем иметь следующий код:

public class SomeContentProvider extends ContentProvider {
  private HelperDatabase helper;
 
  @Override
  public boolean onCreate() {  
      helper = new HelperDatabase(getContext());
      return true;
  }
  ...
}
public class HelperDatabase extends SQLiteOpenHelper {
  private static final String DATABASE_NAME = "dbname.db";
  private static final int DATABASE_VERSION = 1;
 
  public HelperDatabase(Context ctx){
      super(ctx, DATABASE_NAME, null, DATABASE_VERSION);
  }
  ...
}

Многопоточность

Недавно на хабре был замечательный перевод статьи, раскрывающей некоторые внутренние детали работы SQLite-а. Советую почитать. Я лишь приведу краткую информацию по теме. Если попытаться осуществить одновременную запись из разных соединений(потоков) в базу, то одна из операций отвалит с ошибкой. Поток не будет заблокирован на ожидании(тут рассматривается второй случай доступа к базе данных, когда мы создаем наследника от ContentProvider). Более того, если выполнить неверный inser/update sql запрос, то Android не выбросит исключение, а лишь сделает соотв. запись в logcat. Сказанное, может показаться не существенным, но если у вас десяток потоков пишет в базу, то сообщение об ошибке может остаться не замеченным среди прочих. В общем, я к тому, что нужно быть внимательным и об этом помнить.

Резюмируя

  • Для создания экземпляров SQLiteOpenHelper можно использовать метод фабрику либо создать наследника ContentProvider.
  • Ограничений на количество экземпляров SQLiteOpenHelper нет, в том смысле, что это не обязательно должен быть сенглетон.
  • Управление SQLiteOpenHelper в рамках ContentProvider осуществляется самим провайдером. Удерживаемые ресурсы будут утилизированы месте с процессом удерживающим их.
  • SQLite имеет блокировку на уровне файла. Читать данные можно из различных потоков, писать может только один. При попытке одновременной записи из двух разных соединений, один из них вернет ошибку(поток не будет блокирован на ожидание)

Линки

  1. Механизм атомарного коммита в SQLite
  2. Настройки в Android-приложениях
  3. Android Preferences
  4. Correctly Managing Your SQLite Database
  5. Android Sqlite Locking

Monday, July 1, 2013

Форматирование даты/времени в Android приложении

Одним из аспектов локализации приложения является представление времени и даты в соответствии с настройками устройства. Различные страны/регионы используют разные форматы, как следствие, нужно учитывать ряд моментов.

Первая мысль, которая приходит в голову - это использование java.text.DateFormat(и как частный случай SimpleDateFormat):

Calendar cal = new GregorianCalendar(2013, 11, 20);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
String date = df.format(cal.getTime());
// date == "2013-12-20"

Данный подход хорошо работает, скажем, для вэб сервиса, но он совсем неприемлем для мобильных приложений. Т.к. с большой долей вероятности для конечного пользователя шаблон форматирования не является привычным. Например, не ясно когда часы(суточное время) должны быть представлены в 12-и или 24-х часовом формате.

Более правильный решение - это использовать android.text.format.DateFormat. Класс имеет ряд методов, возвращающих шаблон представления даты/времени в соответствии с системной локалью: getDateFormat(), getTimeFormat() и т.д.

Calendar cal = new GregorianCalendar(2013, 11, 20);
DateFormat df = android.text.format.DateFormat.getDateFormat(this); 
String date = df.format(cal.getTime());
// date == "12/20/2013"

Недостаток - это полное отсутствие гибкости. Скажем, что если я не хочу показывать год или наоборот - добавить день недели? Как можно догадаться, применять такое форматирование можно лишь в частном случае, когда нет жестких требований.

Мы подошли вплотную к правильному решению - DateUtils. Класс предоставляет семейство методов formatDateTime() и formatDateRange, принимающие флаги в качестве параметров, указывающие, какие поля нужно включить в шаблон. Преимущество в том, что форматирование осуществляется автоматически с учетом локали пользователя, избавляя нас от обработки всех тонкостей вручную:

Calendar cal = new GregorianCalendar(2013, 11, 20);
String date = DateUtils.formatDateTime(this, cal.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE);
// date == "December 20"
date = DateUtils.formatDateTime(this, cal.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_YEAR);
// date == "12/20/2013"
date = DateUtils.formatDateTime(this, cal.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME);
// date == "00:00, 12/20/2013"

DateUtils.formatDateRange() необходим для форматирования временного диапазона(например, "Jan 5 - Feb 12"). Может возникнуть логичный вопрос. Для чего это нужно, если можно выполнить конкатенацию двух дат с использованием formatDateTime()? Помимо того что он проще, в опр. условиях будет выполнена оптимизация представления даты за счет уменьшения количества отображаемых полей(например, если год/месяц не меняется в рамках диапазона):

Calendar cal1 = new GregorianCalendar(2013, 11, 20);
Calendar cal2 = new GregorianCalendar(2013, 11, 25);
Calendar cal3 = new GregorianCalendar(2014, 0, 5);
String date = DateUtils.formatDateRange(this, cal1.getTimeInMillis(), cal2.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE);
// date == "December 20 - 24"
date = DateUtils.formatDateRange(this, cal1.getTimeInMillis(), cal3.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE);
// date == "December 20, 2013 - January 4, 2014"

Единственная вещь в formatDateRange() на которую следует обратить внимание - округления даты. Возможно вы заметили в примере выше, что верхняя граница диапазона была округлена в меньшую сторону(до 24 декабря вместо 25-го). Это произошло из-за того, что было выполнено отсечение по суточной границе(оригинальный текст - that's because it cuts off at midnight). Если добавить миллисекунды, то диапазон будет представлен верно.

Чтобы ваше приложение правильно представляло дату/время и по прежнему имело возможность контролировать формат DateUtils - хорошая отправная точка. Зная и умело используя флаги, можно добиться определенной гибкости.

Замечания и доп. материалы

  • Примеры приведенные выше отображают дату/время с учетом текущих(моих) настроек локали. В вашем случае, отформатированные данные, могут выглядеть иначе.
  • Оригинальная статья

Thursday, December 27, 2012

ndk-stak

Сегодня расскажу про одну полезную утилиту. Тот кто хоть раз использовал NDK, наверняка сталкивался c чем-то вроде этого:

Транслировать адреса в номера строк я не умею :), и раньше для приведения такой информации к читабельному виду использовал addr2line, что не совсем удобно. Начиная с шестой ревизии в NDK была добавлена утилита ndk-stak, в значительной степени упрощающая наше бренное бытие. Есть два возможных способа использования:

В первом случае читаем logcat напрямую, во втором - "скармливаем" лог файл. После обработки получаем:

Monday, October 1, 2012

Android Media API

Вместе с выходом Android 4.1 был представлен новый Меdia API Поначалу данное событие мною воспринялось как многообещающее, но после попыток раскурить его, энтузиазма поубавилось. И так попорядку.

Видео

Автор раскрывает некоторые возможности, делится общими принципами того, как это хозяйство использовать. Т.к с предметной областью мне приходилось пересекаться, то возникло кипучее желание все испытать, покрутить, убедиться и т.д.

На первых парах API выглядит достаточно простым и стройным. Полистав слайды, кажется, что демо можно накидать за пару часов. Это почти правда. Как выяснилось потом, на слайдах нет самых интересных кусков кода. Поэтому к некоторым вещам приходилось идти империческим путем т.е. методом проб и ошибок. Загуглив, я лишь нашел javadoc и один юнит тест :). Негусто. Качнул исходники Android 4.1 и "грепнул" их - толку тоже мало. В общем самым полезным из всего найденного оказался юнит тест.

Абстрактный декодер

Напишим три класса: абстрактный декодер и два наследника. Один для видео другой для аудио. В наших примерах мы будем играть либо видео, либо аудио. Позже я объясню почему. А сейчас код:

Класс представляет собой тред, который "молотит" до момента пока в очереди есть декодированные данные(иначе говоря есть что отрисовывать/проигрывать). Важный момент - это инициализации входного/выходного буфера, и их заполнение/вычитка - методы dequeueInput()/dequeueOutput(). На начальном этапе трудности и вопросы возникает именно здесь.

Видео декодер

Класс выглядить достаточно просто. Нам нужно передать ссылку на канву и декодер отрисует все за нас. Порадовало то, что не нужно заморачиваться со всякого рода мелочами: формат пиксела и т.д. Собственно код: Второй параметр метода releaseOutputBuffer(..., true) "говорит", что мы рендерим картинку средствами декодера.

Аудио декодер

Кода на пару строк больше - создаем аудио трэк, а в качестве канвы передаем null(ибо для звука в ней нет необходимости).

Проблема номер один - синхронизация

Если честно, не нашел вменяемого способа, как синхронизировать аудио/видео в рамках данной задачи. Опробовав разные методы, пришел к очень примитивному, но в то же время простому - синхронизация по внешнему таймеру. Есть некий тред, который "посылает сигнал" декодеру, передавая текущую временную метку. Декодер получив уведомление, "проверяет" есть ли в очереди пакет PTS(presentation time stamp) которого равен либо меньше текущего значения метки. Если есть, то дергаем processDecodedData(). Повторюсь, способ убогий и не обеспечивает должной точности синхронизации, но он работает и для простенького демо сойдет. Пример синхронизации аудио(для видео абсолютно тоже самое за одним лишь отличием - вмето AudioTrackDecoder создать экземпляр VideoTrackDecoder с доп. параметром surface):

Проблема номер два - MediaExtractor

Как говорит дока - сие есть демультиплексор. Задача которого извлекать сырые данные. Если мы хотим работать по отдельности только с аудио, либо только с видео, то вопросов нет. А вот если одновременно, например, проиграть видео файл, то парочку есть. Перед чтением необходимо вызвать метод selectTrack, указать номер трэка в контейнере с которым мы собираемся работать. Допустим есть два трэда - один аудио декодер, другой видео декодер. Есть экземпляр медиа экстрактора расшаренный между этими тредами. При такой схеме у меня дико падала производительность, видео было куцее, звук прерывистый. Второй вариант - создать отдельный медиа экстрактор для каждого трэда. Показал гораздо лучшие результаты чем первый, но с точки зрения здравого смысла он не верный. Т.к. ресурс один, а соединений два(одно соединение для видео, другое - для аудио).

Выводы

Плюсы:
  • Pure java - не нужно париться с NDK
  • Относительно простой API
Минусы:
  • Нет примеров

Monday, September 17, 2012

Экспорт данных в галерю из Android приложения

Разрабатывая Android приложение, которое так или иначе связано с медиа контентом, может возникнуть задача этот контент экспортировать в стандартную галерею.

Способ номер раз - cтандартный. Используем предназначенное для этих целей API. Пример приводить не буду ибо он слишком тривиальный, чтобы разжевывать. Скажу лишь про недостатки:

  • Экспортировать можно только после того, как мы получили соотв. уведомление, что наш клиент законнекчен.
  • Экспортировать можно только графические файлы(во всяком случае видео закинуть в галерею у меня не получилось)
  • Нужно управлять процессом вручную. Т.е. сценарий выглядит следующим образом: коннектуведомлениеэкспортдисконнект. Понятно, что первое и последнее можно выполнять единожды, а не каждый раз во время экспорта.

Способ номер два - брутальный. Послать уведомление системе, что некая директори(а именно которая содержит экспортируемый файл) была примонтирована.

Данный способ я нашел на StackOverflow. Способ лишен всех недостатков способа один, но в тоже время имеет пару неприятных моментов:

  • Если ваше устройство в качестве основной памяти использует внешнюю SD карту, то с вероятностью 100% вы получите вот такое сообщение об ошибке:
  •  У меня есть подозрение(но не проверял), если директория содержит большое число файлов, то процесс экспорта может несколько затянуться во времени. И часто "бомбить" систему данными уведомлениями, думаю, не есть хорошо.

Способ номер три - мой. Почти тоже самое что и два, но с небольшими изменениями, которые позволяют избежать нам всех перечисленных недостатков:

Изменен лишь action и вмето директории мы передеам абсолютный путь к файлу, тем самым обновляем лишь один ресурс, а не пачку. Данный способ позволяет экспортировать как видео так и графические файлы. 

Saturday, August 25, 2012

Service vs. IntentService

Согласно документации Service - это компонент, который предназначен для выполнения длительных операций в фоновом режиме и при необходимости предоставляющий функциональность другим приложениям. Мы сейчас не будем рассматривать как написать свою реализацию, а поговорим немного о другом. Те кто не любит читать документацию, могут и не догадываться об одной важной детали. Service выполняется в главном потоке приложения(как и все компоненты), а это в свою очередь значит, что он косвенно влияет на производительность в целом. Т.е. если мы будем выполнять операции, которые требуют много процессорного времени(декодирование видео/аудио) или блокирующие операции(взаимодействие с сетью), то рано или поздно мы столкнемся с ANR(Application Not Responding). Та же документация предлагает два возможных варианта, решения вышеописанной проблемы: запуск отдельного потока или вместо базового класса Service использовать IntentService.

IntentService - компонент, который так же выполняет разного рода операции(запросы в виде intent-ов), но только по требованию и асинхронно. Сервис имеет внутреннюю очередь запросов. В отдельный момент времени обрабатывается только один запрос причем на все про все создается только одни рабочий поток(а не отдельный поток для каждого запроса). Как только все запросы были выполнены, сервис останавливается. Резюмируя, имеем следующие различия:

  • Serivece использует главный поток приложения в то время как IntentService - отдельный
  • IntentServie имеет внутреннюю очередь. В отдельный момент времени выполняется только один запрос. Если мы хотим реализовать псевдопараллельное выполнение запросов, то нужно отнаследоваться от Serivice и создавать отдельный поток для каждого запроса(что не есть хорошо :))
  • IntentService останавливается автоматически после выполнения всех задач(после очищения очереди), в то время как жизненным циклом Service-а нам нужно управлять "вручную", использовать метод stopSelf() для остановки
  • Метод onBind() в IntentService возвращает null, что значит, что по умолчанию  нет связанного компонента. Оно и понятно, принимая в расчет природу IntentService(асинхронность). Для обратной связи, как правило, используются совсем другие механизмы

Friday, April 27, 2012

Логирование в Android приложении и подготовка релиза

Логирование классная шутка и весьма полезная особенно на стадии разработки и тестирования. Но рано или поздно наступает время релиза и нам необходимо вычистить код от ненужных артефактов, например, таких как:

Если в проекте пару файлов, то все просто, но что делать если пару сотен или пару тысяч? Для начала заглянем в документацию по Log, которая говорит буквально следующее:

Verbose should never be compiled into an application except during development. Debug logs are compiled in but stripped at runtime. Error, warning and info logs are always kept.

Т.е. в Android релиз попадет логирование уровня Error, Warning и Info. Уже неплохо, но если я хочу убрать и это?

На сцену выходит ProGuard - библиотека по оптимизации java кода. Я не буду расписывать все достоинства, а лишь покажу базовую конфигурацию:

Собственно все. После сборки, методы Log.v(...) и Log.d(...) будут удалены из приложения. Следует добавить, Android проект уже содержит ProGuard конфигурацию, поэтому обычно нет необходимости создавать ее с нуля, нужно лишь добавить соответствующие правила.