tag:blogger.com,1999:blog-33336193573910102122024-03-16T14:09:01.383+07:00Блог Виктора ДеревянкоО жизни, о программировании.
Все публикуемые исходные коды можно взять
<a href="http://code.google.com/p/dvsrc/">здесь</a>Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.comBlogger87125tag:blogger.com,1999:blog-3333619357391010212.post-14000396221393795102019-04-27T17:36:00.001+07:002019-04-27T17:36:31.419+07:00Boost Test Adapter не показывает в Visual Studio тесты для QT проекта - проверьте положение QTDIR в файле .vcxproj.users.<div dir="ltr" style="text-align: left;" trbidi="on">
Встретился с неприятной проблемой. В QT-проекте использую Boost.Tests. На одном из компьютеров Boost Test Adapter в VS2017 наотрез отказывался показывать тесты. Оказалось, что виноват был файл ProjectName.vcxproj.user. Выглядел он так:
<pre brush="xml">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LocalDebuggerEnvironment>PATH=$(QTDIR)bin%3b$(PATH)</LocalDebuggerEnvironment>
<QTDIR>C:libsQt5.12.0msvc2017</QTDIR>
</PropertyGroup>
</pre>
Т.е. QTDIR объявлена после того, как используется в LocalDebuggerEnvironment.
Все компилировалось и работало, а юнит-тесты видны не были. Переставил QTDIR в начало:
<pre brush="xml">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<QTDIR>C:libsQt5.12.0msvc2017</QTDIR>
<LocalDebuggerEnvironment>PATH=$(QTDIR)bin%3b$(PATH)</LocalDebuggerEnvironment>
</PropertyGroup>
</pre>
и наконец-то увидел тесты в VS2017.
<br /></div>
Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com0tag:blogger.com,1999:blog-3333619357391010212.post-58299912661267532452019-04-21T15:01:00.001+07:002019-04-27T17:36:48.667+07:00Запуск Firebird 2.5 и Firebird 1.5 на одном сервере<div dir="ltr" style="text-align: left;" trbidi="on">
Порядок запуска двух сервисов Firebird на одном сервере подробно рассмотрен в руководстве <a href="http://www.ibase.ru/inst_manual/">Установка InterBase и Firebird вручную</a>. Но есть одна тонкость - как изменить имя сервиса Firebird 1.5.
<a name='more'></a>
В руководстве этот вопрос упоминается:
<blockquote>Теперь, если речь идет об установке двух Firebird 1.5, то сложным моментом является идентичность имен сервисов и ключей в реестре, которые прописывают instreg и instsvc. Информацию instreg надо убрать (instreg remove), и придется самостоятельно создавать альтернативный сервис в базе сервиса.
Сделать это можно используя примеры программ управления сервисами <a href="http://www.ibase.ru/files/articles/develop/ibguardinst.zip">из командной строки</a> и <a href="http://www.ibase.ru/files/articles/develop/instguard.zip">интерактивного</a>.
</blockquote>
Но можно обойтись и средствами командной строки. Ниже приведен пример регистрации сервисов для Firebird 2.5 x64 (порт 3050) и Firebird 1.5 x86 (порт 3051).
Firebrid 1.5:
<pre class="brush:java">
sc.exe create FirebirdGuardianDefaultInstance binpath="\"C:\Program Files (x86)\Firebird\Firebird_1_5\bin\fbguard.exe\" -s" start= auto displayname="Firebird Guardian - [15x86_3051]"
sc.exe create FirebirdServerDefaultInstance binpath="\"C:\Program Files (x86)\Firebird\Firebird_1_5\bin\fbserver.exe\" -s -p 3051" start= auto displayname="Firebird Server - [15x86_3051]"
</pre>
Firebird 2.5:
<pre class="brush:java">
sc.exe create FirebirdGuardianFirebird25x64_3050 binpath="\"C:\Program Files\Firebird\Firebird_2_5\bin\fbguard.exe\" -s Firebird25x64_3050 -p 3050" start= auto displayname="Firebird Guardian - [25x64_3050]"
sc.exe create FirebirdServerFirebird25x64_3050 binpath="\"C:\Program Files\Firebird\Firebird_2_5\bin\fbserver.exe\" -s Firebird25x64_3050" start= auto displayname="Firebird Server - [25x64_3050]"
</pre>
Результат показан на скриншоте ниже.
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEicOSYU0azeFzmDUvTSjC26y25q7ltHF-3YL_8tnlpdGv6n903l27XOu3XMzSDrFymzbR4JHc-4G4PZ5Doluxu0_0oKW6uueH6NLzt-ps8C6qBm5ci2OpSlcj0fmYh50707icH7m4Ad8T2S/s1600/fb25_15.png" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEicOSYU0azeFzmDUvTSjC26y25q7ltHF-3YL_8tnlpdGv6n903l27XOu3XMzSDrFymzbR4JHc-4G4PZ5Doluxu0_0oKW6uueH6NLzt-ps8C6qBm5ci2OpSlcj0fmYh50707icH7m4Ad8T2S/s320/fb25_15.png" width="320" height="100" data-original-width="820" data-original-height="256" /></a></div>
<r /></div>
Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com0tag:blogger.com,1999:blog-3333619357391010212.post-77149117852782447142017-12-10T15:05:00.000+07:002017-12-10T15:05:58.249+07:00Удалить файл из git<div dir="ltr" style="text-align: left;" trbidi="on">
Бывает, из истории git-репозитория необходимо удалить какой-нибудь файл. Чаще всего это либо файл с паролем, либо файл большого размера.
<p/>
Возможность такая есть и описана в книге <a href="https://git-scm.com/book/ru/v2/Инструменты-Git-Исправление-истории">Pro Git</a>. Требуется использовать filter-branch, что на практике довольно не просто.
<p/>
К счастью, существует альтернатива - <a href="https://rtyley.github.io/bfg-repo-cleaner/">утилита BFG</a>.
<p/>
Пользоваться довольно просто:
<pre>
; Клонируем репозиторий
$ git clone --mirror git://example.com/my-repo.git
; Удаляем файл
$ java -jar bfg.jar --delete-files FILE_NAME_TO_REMOVE my-repo.git
; Заходим в репозиторий и проводим в нем чистку
$ cd my-repo.git
$ git reflog expire --expire=now --all && git gc --prune=now --aggressive
; Заливаем измененеия
$ git push
</pre>
В случае, если возникает ошибка
<pre>
Protected commits
-----------------
These are your protected commits, and so their contents will NOT be altered:
* commit ******* (protected by 'HEAD')
</pre>
помогает опция --no-blob-protection.
</div>
Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com0tag:blogger.com,1999:blog-3333619357391010212.post-10965821404580026782015-10-17T20:00:00.000+07:002015-10-20T13:17:30.815+07:00Подключение Nexus 5 к PC после обновления до Android 6<div dir="ltr" style="text-align: left;" trbidi="on">
Мой Nexus 5 недавно обновился до Android 6. Неожиданно обнаружил, что при подключении Nexus к компьютеру проводник перестал отображать список файлов на девайсе.. Проверил настройки. В настройках Developers Options -> Select USB Configuration выбран режим MTP (Media Transfer Protocol). Ну и что за дела?
<p/>
Ответ нашелся <a href="https://productforums.google.com/forum/#!topic/nexus/6lMKqYYXpXw;context-place=topicsearchin/nexus/category$3Aconnecting-to-networks-and-devices">на форуме Nexus</a>. Оказывается, при подключении Nexus к компьютеру на девайсе отображалось уведомление <b>USB for charging. Touch for more options</b>. Открыл его, вывались меню <b>Use USB for</b>. Выбрал <b>Transfer files (MTP)</b> и все заработало.
<p/>
Виновата, конечно, моя невнимательность. Ведь все на экране написано :) И все же: почему в настройках указан один режим, а по факту девайс подключился в другом? Баг это или "фича"?
<p/><b>Update</b> Похоже, все же "фича". Опцию <b>USB for charging. Touch for more options</b> приходиться менять каждый раз при подключении девайса к компьютеру. И, кстати, появляется она на экране залоканного девайса ни сразу, а с небольшой задержкой.
<br /></div>Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com1tag:blogger.com,1999:blog-3333619357391010212.post-89933486174663300822015-10-14T11:12:00.002+07:002015-10-14T11:12:17.944+07:00Ссылка на рисунки и таблицы в Microsoft Word - показывать только номер<div dir="ltr" style="text-align: left;" trbidi="on">
Недавно вышел новый Microsoft Office 2016 и в нем по-прежнему существует проблема создания перекрестных ссылок для рисунков и таблиц - интерфейс не дает возможность создать ссылку в виде номера. Максимум чего можно добиться - выбрать при создании ссылки "Постоянная часть и номер" и получить ссылку в виде "Рисунок 1". Возникает вопрос, как выкинуть слово "Рисунок" и получить просто "1"?
<p/>
В интернете полно статей на эту тему. Обычно предлагают два варианта: <a href="http://blogs.technet.com/b/tasush/archive/2011/06/15/123-word-2010.aspx">пометить "Рисунок" как скрытый текст или воспользоваться закладками</a>.
Однако существует <a href="http://answers.microsoft.com/en-us/office/forum/office_2007-word/how-to-display-figure-number-only-in-cross/02ae3cd0-6c3c-401d-990c-5220d7f0be1e?auth=1">еще один способ</a>, на мой взгляд, более быстрый и удобный.
<p>Щелкаем по вставленной перекрестной ссылке правой клавишей мыши и выбираем команду <b>Коды/значения полей</b>. На месте перекрестной ссылки в документе появится ее код, например вот такой <code>
{REF _Ref432583060 \h}
</code>. Дописываем к коду поля вот такой постфикс
<p/>
<code><b>
\# "0"
</b></code>
<p/>Т.е. превращаем код ссылки в
<code>
{REF _Ref432583060 \h \# "0"}
</code>
<p/>
Еще раз даем команду <b>Коды/значения полей</b> - перекрестная ссылка на рисунок отображается в виде "1". То что нужно.
<br /></div>Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com0tag:blogger.com,1999:blog-3333619357391010212.post-39851734811021972982014-04-02T12:15:00.001+08:002014-04-02T18:35:19.343+08:00Отключение junit.framework.Assert в релизе Android-приложения<div dir="ltr" style="text-align: left;" trbidi="on">
Как я уже <a href="http://derevyanko.blogspot.ru/2014/01/android.html">писал</a>, стандартные директивы assert по умолчанию отключены на Android-девайсах. Их необходимо <a href="http://stackoverflow.com/questions/2364910/can-i-use-assert-on-android-devices">включать</a> (например командой <code>adb shell setprop debug.assert 1</code>, после чего assert будет работать до перезагрузки девайса).
</p>
Альтернатива - <a href="http://developer.android.com/reference/junit/framework/Assert.html">junit.framework.Assert</a>. И по функционалу побогаче, и работает на девайсах сразу, без дополнительных включений. При <a href="https://wiki.eclipse.org/JDT_Core/Null_Analysis/Options">Null-анализе</a> компилятор eclipse учитывает junit.framework.Assert точно так же, как обычный assert. Вроде бы, одни плюсы. Но: в релизе ассерты следует отключить. И вот тут требуются дополнительные телодвижения.
<a name='more'></a>
</p>
<b>Далее предполагается, что используется Eclipse + ADT plugin.</b>
</p>
Все что нужно сделать - это заставить ProGuard удалить все вызовы <code>junit.framework.Assert.*</code> из кода. Для это существует директива ProGuard <code><a href="http://proguard.sourceforge.net/manual/usage.html#optimizationoptions">-assumenosideeffects</a></code>. В файл <code>proguard-project.txt</code> в корне проекта добавляем строчку:
<pre>
-assumenosideeffects class junit.framework.Assert {
<methods>
}
</pre>
Все? Не все. Для того, чтобы ProGuard смог исключать вызовы методов в коде, необходимо включить <a href="http://proguard.sourceforge.net/manual/optimizations.html#/manual/optimizations.html">оптимизацию</a>, которая, <a href="http://tools.android.com/recent/proguardimprovements">отключена по-умолчанию</a>. В файлах <code>ant.properties</code> (сборка из командной строки через Ant) и <code>project.properties</code> (сборка из Eclipse через export) смотрим строчку
<pre>
proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
</pre>
и меняем ее на
<pre>
proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard-project.txt
</pre>
Загляните в директорию <code>${sdk.dir}/tools/proguard/</code>, посмотрите, чем отличаются файлы <code>proguard-android.txt</code> и <code>proguard-android-optimize.txt</code>. В первом оптимизация отключена, во втором - включена. В файле <code>proguard-android-optimize.txt</code> кратко написано, почему оптимизация по умолчанию отключена и в каких случаях ее включение может приводить к проблемам.
</p>
На <a href="http://stackoverflow.com/questions/16999679/what-optimisation-is-safe-that-still-allows-assumenosideeffects-removals">StackOverflow</a> предлагают так же явно прописывать параметры оптимизации через команду <code>-optimizations</code>. Поэкспериментировав я <s>пришел к выводу, что это лишнее</s> обнаружил, что подобное ограничение в оптимизации действительно может быть необходимо. Код, работающий с Android Facebook SDK падал в моем приложении до тех пор, пока я не отредактировал <code>proguard-project.txt</code> следующим образом
<pre>
-optimizations code/removal/simple,code/removal/advanced
-assumenosideeffects class junit.framework.Assert {
<methods>
}
</pre>
<br /></div>Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com1tag:blogger.com,1999:blog-3333619357391010212.post-53904176074654498312014-02-02T10:53:00.001+08:002014-09-24T11:23:34.919+08:00REST под Android. Часть 3: библиотеки Square<div dir="ltr" style="text-align: left;" trbidi="on">
В процессе работы над <a href="http://derevyanko.blogspot.ru/2014/01/rest-android-1.html">первой</a> и <a href="http://derevyanko.blogspot.ru/2014/01/rest-android-2-api.html">второй</a> частью статьи, я неоднократно натыкался на упоминание разнообразных <a href="http://square.github.io">библиотек компании Square</a>, которые народ активно использует при реализации REST-клиентов. Приглядимся к этим библиотекам поближе.
<a name='more'></a>
<p/>
<h2>OkHttp: продвинутый HTTP-клиент</h2>
Как известно, под Android имеется два стандартных HTTP-клиента: Apache HTTP Client и HttpURLConnection. <a href="http://android-developers.blogspot.ru/2011/09/androids-http-clients.html">Первый оптимален на старых версиях Android (Eclair and Froyo), второй - на более поздних версиях</a>.
<p/>
Библиотека <a href="http://square.github.io/okhttp/">OkHttp</a> - это <a href="http://corner.squareup.com/2013/05/announcing-okhttp.html">альтернативный HTTP-клиент</a>, основанный на исходных кодах HttpURLConnection, и реализующий множество дополнительных полезных функций. В частности, в OkHttp:
<ul>
<li>добавлена поддержка протоколов <a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-09">HTTP 2 (draft)</a>, <a href="http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1">SPDY 3 (draft)</a>;</li>
<li>реализовано автоматическое восстановление соединения, при возникновении распространенных сетевых проблем (например, проблем c прокси-сервером и TLS рукопожатием);</li>
<li>реализован пул соединений, обеспечивающий повторное использование HTTP и SPDY соединений, за счет чего увеличивается пропускная способность и снижается время ожидания.</li>
</ul>
Важнейший плюс OkHttp - библиотека <a href="http://android-developers.blogspot.ru/2011/09/androids-http-clients.html">устраняет проблемы реализации HttpURLConnection на старых версиях Android</a>. Например, прозрачная поддержка GZip реализована в HttpURLConnection только в Gingerbread (Android 2.3), кэш ответов - в Ice Cream Sandwich (Android 4.0). OkHttp предоставляет весь этот функционал, начиная с Android 2.2.
<p/>
<h2>Retrofit: безопасный REST-клиент</h2>
В общем случае, при выполнении запроса к REST-серверу, требуется выполнить ряд операций:
<ul>
<li>сформировать URL;</li>
<li>задать HTTP-заголовки;</li>
<li>выбрать тип HTTP-запроса;</li>
<li>сформировать тело HTTP-запроса, т.е. преобразовать Java объект в JSON;</li>
<li>выполнить запрос, воспользовавшись HTTP-клиентом;</li>
<li>распарсить результаты запроса - преобразовать полученный JSON в Java объект.</li>
</ul>
Куча кода на каждый запрос? Библиотека <a href="http://square.github.io/retrofit/">Retrofit</a> позволяет описать все перечисленные операции с помощью аннотаций - компактно и без ошибок. Рассмотрим пример. Пусть у нас есть запрос из <a href="https://github.com/dvpublic/SampleBooksSetRestAPI/blob/master/apiary.apib">BooksSet REST API</a>:
<pre>
GET /books/1234
</pre>
Этот запрос возвращает JSON вида
<pre>
{
"id": "1236",
"title": "Yellow mist",
"author": "Alexander Volkov",
"released": "1970"
}
</pre>
С помощью Retrofit мы можем описать этот запрос следующим образом:
<pre class="brush:java">
import retrofit.http.GET;
import retrofit.http.Path;
import retrofit.http.Query;
public class Book {
public int id;
public String title;
public String author;
public int released;
}
public interface IBookSetRestAPI {
@GET("/v1/books/{book_id}")
Book getBook(@Path("book_id") int book_id);
}
</pre>
Выполнить запрос к серверу можно так:
<pre class="brush:java">
RestAdapter restAdapter = new RestAdapter.Builder()
.setServer("http://samplebookssetrestapi.apiary.io")
.build();
IBookSetRestAPI rest_api = restAdapter.create(IBookSetRestAPI.class);
Book book = rest_api.getBook(136);
</pre>
<p/>
Каждый запрос к REST-серверу описывается отдельной функцией в интерфейсе <code>IBookSetRestAPI</code>. Тип HTTP запроса задается с помощью аннотаций <code>@GET, @POST, @PUT, @DELETE, @HEAD, @PATCH</code>, параметры URI-шаблона задаются через параметры функции и описываются аннотацией <code>@Path</code>.
<p/>
В случае, когда в URL присутствуют переменные, они задаются через аннотацию <code>@Query</code>, например запросы:
<pre>
GET /v1/books?limit=10&offset=20
GET /v1/books?limit=10
</pre>
описываются следующим образом:
<pre class="brush:java">
public interface IBookSetRestAPI {
@GET("/v1/books/{book_id}")
Book getBook(@Path("book_id") int book_id);
@GET("/v1/books")
ListBooks getBooks(@Query("limit") int limit);
@GET("/v1/books")
ListBooks getBooks(@Query("limit") int limit, @Query("offset") int offset);
}
</pre>
<p/>
Разумеется, есть и <a href="http://square.github.io/retrofit/">другие аннотации</a> - для задания заголовков HTTP-запросов, для формирования <a href="http://ru.wikipedia.org/wiki/Multipart/form-data">составных запросов</a> и т.д.
<p/>
Несмотря на то, что аннотации Retrofit по структуре похожи на аннотации <a href="http://www.techferry.com/articles/RESTful-web-services-JAX-RS-annotations.html">JAX-RS</a>, эти наборы аннотаций не совместимы. <a href="https://plus.google.com/communities/109244258569782858265/stream/63e22a6d-b165-489a-92ab-d35f942beb5b">Причина</a>: аннотации JAX-RS ориентированы на серверную часть, а аннотации Retrofit - на клиентскую.
<p/><h3>Парсинг результатов</h3>
Функция <code>Book getBook(int book_id)</code> возвращает объект, распарсивание JSON производится автоматически. По умолчанию, для распарсивания используется GSON, т.е. в проект необходимо добавить <a href="https://code.google.com/p/google-gson/downloads/list">gson-2.2.4.jar</a>. Библиотека поддерживает кастомизацию конвертации объектов, причем конвертеры XML и Protobuf <a href="https://github.com/square/retrofit/tree/master/retrofit-converters">доступны</a> вместе с библиотекой.
<p/>
Можно получать результаты в "сыром", нераспарсенном виде. Достаточно определить интерфейс IBookSetRestAPI следующим способом:
<pre class="brush:java">
import retrofit.client.Response;
import retrofit.http.GET;
import retrofit.http.Path;
import retrofit.http.Query;
public interface IBookSetRestAPI {
@GET("/v1/books/{book_id}")
Response getBook(@Path("book_id") int book_id);
@GET("/v1/books")
Response getBooks(@Query("limit") int limit);
@GET("/v1/books")
Response getBooks(@Query("limit") int limit, @Query("offset") int offset);
}
</pre>
Объект <code>Response</code> дает прямой доступ к содержимому ответа сервера - можно парсить его вручную.
<p/><h3>OkHttp и Retrofit</h3>
Retrofit выбирает HTTP-клиента следующим образом:
<ul>
<li>Если OkHttp задействована в проекте, то используется OkHttpClient;</li>
<li>В противном случае: если приложение работает под Android 2.2 или ниже, используется HttpClient, иначе - HttpURLConnection.
</ul>
<p/><h3>GZip и Retrofit</h3>
OkHttp использует Gzip автоматически при условии, что заголовок Accept-Encoding не задан явно. Таким образом, если Accept-Encoding не задан, то:
<ul>
<li>OkHttp автоматически добавляет "Accept-Encoding: gzip" и отправляет запрос серверу;</li>
<li>далее, проверяет ответ от сервера: если в заголовках ответа есть "Content-Encoding: gzip", значит данные запакованы;</li>
<li>запакованные данные OkHttp автоматически распаковывает. В результате, клиент получает Response с распакованным содержимым; если же используется конвертер, то в функцию fromBody конвертера опять же приходят уже распакованные данные.</li>
</ul>
Если же по каким-либо причинам transparent gzip необходимо отключить, то следует <b>явно</b> задать заголовок Accept-Encoding. Например:
<pre class="brush:java">
@Headers("Accept-Encoding: identity")
@GET("/v1/books")
ListBooks getBooks();
@Headers("Accept-Encoding: gzip")
@GET("/v1/books")
ListBooks getBooks2();
</pre>
Запрос getBooks будет работать без gzip. Запрос getBooks2 будет поддерживать gzip, однако на выходе пользователь получит запакованные данные (как в Response, так и в функции fromBody конвертера), которые необходимо будет распаковывать вручную.
<p/><h3>Синхронное и асинхронное выполнение запросов</h3>
Retrofit поддерживает оба варианта. Асинхронное выполнение запросов требует подключения библиотеки <a href="https://github.com/Netflix/RxJava/wiki">RxJava</a>.
<p/><h3>Зависимости Retrofit</h3>
Все зависимости опциональны:
<ul>
<li><a href="http://square.github.io/okhttp/">OkHttp</a> - если требуется OkHttpClient;</li>
<li><a href="https://code.google.com/p/google-gson/downloads/list">GSon</a> - если планируется использовать стандартный GSon конвертер;</li>
<li><a href="https://github.com/Netflix/RxJava/wiki">RxJava</a> - если требуется асинхронное выполнение запросов.</li>
</ul>
<p/><h3>Примеры использования Retrofit</h3>
<a href="http://kdubblabs.com/java/retrofit-by-square/">Неплохая коллекция примеров.</a>
<p/><h2>MimeCraft: формирование HTTP-запросов</h2>
Еще одна библиотека, <a href="https://github.com/square/mimecraft">MimeCraft</a>, предназначена для формирования тела <a href="http://stackoverflow.com/questions/16958448/what-is-http-multipart-request">составных HTTP-запросов</a> и данных веб-формы. Все это умеет делать Retrofit, однако синтаксис немного другой. Пример:
<ul>
<li>Retrofit:
<pre class="brush:java">
@FormUrlEncoded
@POST("/user/edit")
User updateUser(@Field("first_name") String first, @Field("last_name") String last);
...
User u = rest_api.updateUser("First", "Last");
</pre>
</li>
<li>MimeCraft:
<pre class="brush:java">
FormEncoding fe = new FormEncoding.Builder()
.add("first_name", "First")
.add("last_name", "Last")
.build();
fe.writeBodyTo(outstream);
</pre>
</li>
</ul>
<p/><h2>Выводы</h2>
Библиотека Retrofit идеально подходит для целей реализации паттернов Virgil Dobjanschi. Причем функционал Retrofit и RoboSpice не пересекается: RoboSpice реализует каркас шаблона A, Retrofit - берет на себя всю низкоуровневую работу по формированию запроса к серверу и обработке результатов. Другими словами, Retrofit предоставляет весь тот функционал, который в RoboSpice требуется реализовать в SpiceRequest, так что библиотеки хорошо дополняют друг друга.
<p/>
OkHttp - удобная замена для стандартных HTTP-клиентов, с более эффективной реализацией, с поддержкой SPDY и без проблем работающая на старых версиях Android (>= 2.2).
<p/>
MimeCraft можно использовать для формирования тела составных HTTP-запросов и запросов форм вручную - Retrofit позволяет сделать все то же самое через аннотации.
<p/>
<strike>Следующая часть статьи будет посвящена реализации тестового проекта на базе RoboSpice + Retrofit + OkHttp.</strike>
<p/>
<ul>
<li><a href="http://derevyanko.blogspot.ru/2014/01/rest-android-1.html">REST под Android. Часть 1: паттерны Virgil Dobjanschi</a></li>
<li><a href="http://derevyanko.blogspot.ru/2014/01/rest-android-2-api.html">REST под Android. Часть 2: разрабатываем API</a></li>
</ul>
<br /></div>Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com5tag:blogger.com,1999:blog-3333619357391010212.post-8217757063030960182014-01-24T11:05:00.000+08:002014-02-05T09:30:28.554+08:00REST под Android. Часть 2: разрабатываем API<div dir="ltr" style="text-align: left;" trbidi="on">
<a href="http://derevyanko.blogspot.ru/2014/01/rest-android-1.html">Первая часть статьи</a> была посвящена вопросу реализации клиентского REST-приложения под Android. Корректная реализация требует использования одного из паттернов, предложенных Virgil Dobjanschi. Существует ряд подходящих библиотек, способных облегчить эту задачу. Наиболее интересной из них мне показалась <a href="https://github.com/octo-online/robospice">RoboSpice</a>.
<p/>
Прежде чем переходить к разработке тестового проекта на RoboSpice, необходимо определиться с API для тестового проекта. Разумеется, можно взять одно из готовых API - благо их сейчас сотни. Однако гораздо интереснее написать свое, тем более, что это нетривиальный процесс. Рассмотрим, как пишутся REST API, какие инструменты для этого существуют, и попробуем реализовать простейшее тестовое API.
<a name='more'></a>
<p/>
<h2>Принципы разработки REST API</h2>
<h3><a href="http://apigee.com">Apigee</a></h3>
Если вы собираетесь разрабатывать REST API самостоятельно, то первым делом загляните на сайт <a href="http://apigee.com">apigee.com</a>. Здесь собрана масса полезной информации на эту тему. В первую очередь - книги:
<ul>
<li><a href="http://info.apigee.com/Portals/62317/docs/web%20api.pdf">Web API Design. Crafting Interfaces that Developers Love (2011, pdf)</a> by Brian Mulloy (<a href="https://blog.apigee.com/detail/slides_for_restful_api_design_second_edition_webinar/">вебинар</a>). Кратко и четко расписаны основные принципы разработки API: как именовать ресурсы и действия, паджинация, как возвращать ошибки, поддерживать несколько форматов представления ресурсов, как реализовывать поиск и т.д и т.п. На хабре есть хорошая <a href="http://habrahabr.ru/post/181988/">выжимка на русском</a>.</li>
<li><a href="http://chrismaloney.org/notes/API%20Webinar%20Series_/api-facade-pattern-ebook-2012-06.pdf">API Facade Pattern: A Simple Interface to a Complex System (2012, pdf) </a> by Brian Mulloy (<a href="https://blog.apigee.com/detail/api_facade_pattern_webinar_shorts_full_series">вебинар</a>);</li>
<li><a href="http://shop.oreilly.com/product/0636920021223.do">APIs: A Strategy Guide. Creating Channels with Application Programming Interfaces (2013) </a> by Daniel Jacobson, Greg Brail, Dan Woods;</li>
<li><a href="http://info.apigee.com/Portals/62317/docs/oauth_big_picture.pdf">OAuth: The Big Picture (2011, pdf)</a> by Greg Brail & Sam Ramji, (<a href="http://apigee.com/about/api-best-practices/oauth-big-picture-0">вебинар</a>);</li>
<li>и <a href="http://apigee.com/about/api-best-practices">другие полезные книжки.</a></li>
</ul>
<p/>Там же на отдельной странице <a href="https://apigee.com/providers">собраны</a> примеры готовых REST API (github, twitter и т.д.). Специальная <a href="https://apigee.com/console">консоль</a> позволяет поработать с выбранным API прямо из браузера.
<h3>Другие ресурсы</h3>
<ul>
<li>Книга <a href="http://shop.oreilly.com/product/0636920028468.do">RESTful Web APIs (2013) by Leonard Richardson, Mike Amundsen, Sam Ruby.</a></li>
<li>REST+JSON API Design - Best Practices for Developers: <a href="http://www.slideshare.net/stormpath/rest-jsonapis">slides</a>, <a href="https://www.youtube.com/watch?v=hdSrT4yjS1g">video</a></li>
<li><a href="http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api">Best Practices for Designing a Pragmatic RESTful API</a> by Vinay Sahni</li>
<li><a href="http://www.nielskrijger.com/2013/08/rest-and-json-api-guidelines-and-best.html">REST and JSON API guidelines and best practices</a> by Niels Krijger - удобная шпаргалка, краткая компиляция из других источников.</li>
<li><a href="http://shop.oreilly.com/product/9781565925090.do">HTTP: The Definitive Guide</a> by David Gourley, Brian Totty, Marjorie Sayer, Anshu Aggarwal, Sailu Reddy. В книге детально рассмотрен HTTP протокол, на котором в большинстве случаев реализуются REST API. Так же HTTP посвящена отдельная глава "HTTP for APIs" в книге "RESTful Web APIs".</li>
</ul>
<p/>
<h2>Тестовый пример: набор книг</h2>
Рассмотрим простую задачу - нужно разработать REST API для <b>набора книг</b>. Ресурс, для простоты, будет только один - книга (book). Что нам требуется от API:
<ul>
<li>[1] получить полный список книг (с пагинацией).</li>
<li>[2] получить информацию о книге с заданной степенью детализации;</li>
<li>[3] добавить книгу в набор;</li>
<li>[4] удалить книгу из набора;</li>
<li>[5] редактировать информацию о книге, например, уточнить ФИО автора;</li>
<li>[6] провести поиск книг по автору или названию (с пагинацией);</li>
</ul>
<p/>
<h2>Первая попытка описания API: список URL в текстовом файле</h2>
В первом приближении, наше API можно записать в виде простого списка HTTP-запросов, которые клиент может посылать на сервер. Учитывая рекомендации из книги "Web API Design. Crafting Interfaces that Developers Love", получаем такие запросы:
<pre>
//[1] получить полный список книг
GET /v1/books
GET /v1/books?limit=10&offset=30 //поиск с пагинацией; получаем максимум 10 результатов, начиная с 30-ой позиции
//[2] получить информацию о книге
GET /v1/books/[book_id]
GET /v1/books/[book_id]?fields=title,author,description //явно указываем, какие поля нам нужны
//[3] добавить книгу в набор
POST /v1/books
//[4] удалить книгу из набора
DELETE /v1/books/[book_id]
//[5] редактировать информацию о книге
PATCH /v1/books/[book_id] //заменяем только выбранные поля переданными данными
//[6] провести поиск книг по автору, по названию
GET /v1/books?[book_id]?author=John //поиск по автору; по умолчанию выводится первые 10 результатов
GET /v1/books?[book_id]?author=John&limit=25&offset=50 //поиск по автору с пагинацией
GET /v1/books?[book_id]?title=hero&?fields=title,author //поиск по названию, явно указываем, какие поля нам нужны
</pre>
Для простоты считаем, что наши клиенты поддерживают все необходимые HTTP-методы. На практике, некоторые клиенты могут не поддрживать PUT, DELETE и, тем более, PATCH, так что могут потребоваться альтернативные варианты API, например, вместо <code>PUT /v1/books/[book_id]</code> - <code>POST /v1/books/[book_id].patch</code>. Тег <code>v1</code> задает версию нашего API.
<p/>
Договоримся, что для формат представления ресурсов можно задавать следующим образом:
<pre>
GET /v1/books //json по умолчанию
GET /v1/books.xml //получить список книг в xml
GET /v1/books/[book_id].proto //получить информацию о книге в формате protobuffer
</pre>
<p/>
Теперь зафиксируем способ обработки ошибок. Будем считать, что в случае успеха сервер возвращает код <code>200 - OK</code>, в случае ошибки один из следующих кодов: <code>400 - Bad Request, 500 - Internal Server Error</code>. Для наших целей вполне достаточно, хотя можно задействовать и <a href="http://en.wikipedia.org/wiki/Http_error_codes">другие коды</a>.
<p/>
В случае ошибки сервер обязан вернуть сообщение с расшифровкой ошибки в формате:
<pre code="brush:json">
{"code" : 401, "message": "book wasn't found"}
</pre>
<p/>
Осталось описать формат представления ресурса и списков ресурсов.
<ul>
<li>[1] получить полный список книг
<pre>
GET /books
Response 200 OK
{"books": [{"book":{ "id":"1234", "title": "Война и мир", "author": "Л.Н. Толстой", "released": "1868"}}
, {"book":{ "id":"1235", "title":"Господин Ау", "author", "Э. Успенский", "released" : "1980"}}]
"_metadata": [{"totalCount":250,"limit":10,"offset":0}]
"_links" : [
{"rel": "next", "href": "/v1/books?offset=30&limit=10},
{"rel": "prev", "href": "/v1/books?offset=10},
{"rel": "first","href": "/v1/books?offset=0},
{"rel": "last", "href": "/v1/books?offset=240}
]}
</pre></li>
<li>[2] получить информацию о книге
<pre>
GET /books/1234
Response 200 OK
{"book":{ "id":"1234", "title": "Война и мир", "author": "Л.Н. Толстой", "released": "1868" }}
</pre></li>
<li>[3] добавить книгу в набор
<pre>
POST /v1/books
title="Желтый туман"&author="Александр Волков"
Response 200 OK
{"book":{ "id":"1236", "title": "Желтый туман", "author": "Александр Волков", "released" : ""}}
</pre></li>
<li>[4] удалить книгу из набора
<pre>
DELETE /v1/books/[book_id]
Response 200 OK
</pre></li>
<li>[5] редактировать информацию о книге
<pre>
PATCH /v1/books/1236
released="1970"
Response 200 OK
{"book":{ "id":"1236", "title": "Желтый туман", "author": "Александр Волков", "released": "1970"}}
</pre></li>
<li>[6] провести поиск книг по автору, по названию - аналогично [1]</li>
</ul>
<h2>Инструменты для описания API</h2>
Вариант описания API в виде текстового файла вполне жизнеспособен, но далеко не идеален. Основная проблема в том, что в этом случае реализация и документация API неминуемо разойдутся. Решений несколько:
<ul>
<li>[1] Описать API, воспользовавшись одним из специальных языков. Далее, на основе описания, автоматически генерировать документацию и код реализации, проверять корректность и полноту реализацию API и т.д.</li>
<li>[2] Описать API в коде сервера, генерировать актуальную документацию на основе кода.</li>
<li>[3] Воспользоваться подходом <a href="http://en.wikipedia.org/wiki/HATEOAS">Hypermedia as the Engine of Application State (HATEOAS)</a> и вообще обойтись без описания API. Такой подход подразумевает, что клиент изначально знает лишь адрес сервера, и узнает все ссылки для доступа к конкретным ресурсам в процессе навигации. Для его реализации можно использовать обобщенные языки описания API типа <a href="http://stateless.co/hal_specification.html">HAL</a>, <a href="https://github.com/kevinswiber/siren">Siren</a>. В данной статье подход HATEOAS рассматриваться не будет.
</ul>
Подходящий инструментарий для [1] и [2]:
<ul>
<li><a href="http://apiary.io">apiary.io</a>. Бесплатный сервис, который позволяет разрабатывать REST API на языке <a href="http://apiblueprint.org/">API Blueprint Language</a>. На основе такого API можно:
<ul><li>генерировать документацию;</li>
<li>проверить реализацию на соответствие API; проверка выполняется следующим образом: сервер Apiari используется как отладочный прокси-сервер, который пропускает через себя весь трафик к серверу и от сервера, сравнивает запросы и результаты с API и сообщает о найденных ошибках.</li></ul>
Приятные "фишки" сервиса:
<ul>
<li>сервис может работать как сервер-заглушка (Server Mock), так что с API можно будет попробовать работать сразу, не реализуя реальную серверную часть;</li>
<li>сервис поддерживает отладку запросов (в разделе "Documentation" возле каждого запроса есть кнопочка Try it).</li>
<li>сервис плотно интегрирован с GitHub.</li>
</ul>
</li>
<li><a href="http://www.apihub.com/">http://www.apihub.com/</a>. Альтернативный вариант, использующий для описания REST API язык <a href="http://raml.org/">RESTful API Modeling Language (RAML)</a>. Сервис платный, с возможностью бесплатно разработать одно API.</li>
<li><a href="http://enunciate.codehaus.org">http://enunciate.codehaus.org</a> - Java + <a href="https://jersey.java.net/documentation/latest/jaxrs-resources.html">JAX-RS аннотации</a> - для тех, кто <a href="http://habrahabr.ru/post/140181/">разрабатывает сервер на основе JAX-RS</a>.</li>
<li>Самописные <a href="http://habrahabr.ru/company/dnevnik_ru/blog/174555/">велосипеды</a>;</li>
<li><a href="http://mestachs.wordpress.com/2012/08/06/rest-api-documentation/">и т.д.</a></li>
</ul>
<h2>Вторая попытка описания API: Apiary.io</h2>
Воспользуемся сервисом apiari.io, и перепишем наше API, используя <a href="https://github.com/apiaryio/api-blueprint/blob/master/API%20Blueprint%20Specification.md">документацию по API Blueprint Language</a> и образцы готовых API (<a href="http://blog.apiary.io/2014/01/09/Inspirational-Resources/">1</a>, <a href="https://github.com/apiaryio/api-blueprint/tree/master/examples">2</a>).
<p/>Результат можно <a href="https://github.com/dvpublic/SampleBooksSetRestAPI/blob/master/apiary.apib">посмотреть на GitHub</a> и на <a href="https://app.apiary.io/samplebookssetrestapi/settings">Apiary</a>.
<p/>А вот результаты работы Mock-сервера Apiary:
<ul>
<li><a href="http://samplebookssetrestapi.apiary.io/v1/books/1236">http://samplebookssetrestapi.apiary.io/v1/books/1236</a></li>
<li><a href="http://samplebookssetrestapi.apiary.io/v1/books">http://samplebookssetrestapi.apiary.io/v1/books</a></li>
</ul>
<p/>
В целом, работа с API Blueprint Language не вызвала каких-то особых проблем. Смутил только один момент, с которым так и не удалось разобраться. Я создал ресурс Book:
<pre>
## Book [/v1/books/{id}{?fields}]
</pre>
и действия к нему
<pre>
### Retrieve a Single Book [GET]
...
### Edit a Book [PATCH]
...
### Delete a Book [DELETE]
...
</pre>
Параметр <code>fields</code> может использоваться только с GET, а в PATCN и DELETE его быть не может. Набор параметров я задаю отдельно для каждого действия. Тем не менее, в документации для Edit и Delete параметр <code>fields</code> присутствует в URL, что несколько сбивает с толку. Причем ниже, в списке возможных параметров, он отсутствует.
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiCjHyYb5yF25VjofwML9-46xF35mie1Zk_Q1yaGqInhpZ3tZlI6oY4TNR6n7JAQvdkfJLY4PKJyN5NOuLN3K6e1v7FLa9TcWcrstb4uqC0swftCDDlENzz6BNYD9Spy6jAuWXa_JueLgbw/s1600/apiary2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiCjHyYb5yF25VjofwML9-46xF35mie1Zk_Q1yaGqInhpZ3tZlI6oY4TNR6n7JAQvdkfJLY4PKJyN5NOuLN3K6e1v7FLa9TcWcrstb4uqC0swftCDDlENzz6BNYD9Spy6jAuWXa_JueLgbw/s320/apiary2.png" /></a></div>
Несколько неудобно, хотя не слишком критично.
<p/>
<h2>Выводы</h2>
<ul>
<li>Прежде чем разрабатывать собственное REST API, имеет смысл ознакомиться как минимум с <a href="http://info.apigee.com/Portals/62317/docs/web%20api.pdf">Web API Design. Crafting Interfaces that Developers Love (pdf)</a>, чтобы не наделать грубых ошибок.</li>
<li>Сервис <a href="http://apiary.io">apiary.io</a> - весьма функционален. Разрабатывать REST API на нем гораздо удобнее, чем в просто фиксировать список URL в текстовом файле. Возможность проверки реализации на соответствие спецификации и Mock-сервер стоят того, чтобы потратить время на изучение API Blueprint Language.</li>
</ul>
Цель достигнута - <a href="https://github.com/dvpublic/SampleBooksSetRestAPI/blob/master/apiary.apib">BooksSet REST API</a> для тестового проекта создано. Переходим к разработке проекта. Об этом - в следующий раз.
<p/>
<ul>
<li><a href="http://derevyanko.blogspot.ru/2014/01/rest-android-1.html">REST под Android. Часть 1: паттерны Virgil Dobjanschi</a></li>
<li><a href="http://derevyanko.blogspot.ru/2014/02/rest-android-3-square.html">REST под Android. Часть 3: библиотеки Square</a></li>
</ul>
<p/>
<a href="http://ru.wikipedia.org/wiki/Диаграмма_связей">Mindmap</a>, созданный в процессе работы над этой статьей, можно скачать <a href="https://www.dropbox.com/s/cfj74zkgazgwy8s/201401_rest2.mm">здесь</a>, в формате <a href="http://freemind.sourceforge.net/">Freemind 1.0</a> (портативная версия: <a href="http://sourceforge.net/projects/freemind/files/freemind/1.0.0/freemind-bin-1.0.0.zip/download">freemind-bin-1.0.0.zip</a>).
<p/>
<b>Update</b> Mock-сервер Apiary не поддерживает компрессию gzip. Может когда-нибудь сделают, особенно если за эту функцию <a href="http://support.apiary.io/forums/120125-general/suggestions/3619633-support-for-gzip-encoding-in-mock-server">проголосует</a> достаточное количество пользователей.
<br /></div>Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com0tag:blogger.com,1999:blog-3333619357391010212.post-58880856175292447672014-01-17T18:10:00.004+08:002014-02-02T10:56:11.165+08:00REST под Android. Часть 1: паттерны Virgil Dobjanschi<div dir="ltr" style="text-align: left;" trbidi="on">
В одном из проектов появилась необходимость написать REST-приложение под Android. Каким путем пойти, какие Android-особенности учесть, на какие библиотеки опереться в работе? Пришлось провести исследование. Результатами хочу поделиться.
<a name='more'></a>
<p/>
<h2>Архитектура REST</h2>
Не буду останавливаться на принципах REST-архитектуры. Приведу лишь список книг и статей, где эти принципы изложены.
<ul>
<li><a href="http://www.infoq.com/articles/rest-introduction">A Brief Introduction to REST (2007)</a> - замечательная статья <a href="http://www.infoq.com/author/Stefan-Tilkov">Стефана Тилкова (Stefan Tilkov) на InfoQ</a>, в которой кратко и точно описана REST-архитектура. Если вы не знакомы с REST, начните с нее.</li>
<li><a href="http://www.infoq.com/minibooks/emag-03-2010-rest">InfoQ - compilation of REST resources</a> - сборник статей, вышедших на InfoQ, посвященных REST.</li>
<li>Книга <a href="http://shop.oreilly.com/product/9780596529260.do">"RESTful Web Services" (2007) by Leonard Richardson, Sam Ruby</a>. Первая книга по REST, с описанием основных принципов и преимуществ по сравнению с Web Services.</li>
<li>Книга <a href="http://shop.oreilly.com/product/0636920028468.do">"RESTful Web APIs" (2013) by Leonard Richardson, Mike Amundsen, Sam Ruby.</a> Новая книга, тех же авторов. Здесь акцент сделан не на основы REST, а на принципы разработки правильных REST API. Возможно скоро <a href="http://habrahabr.ru/company/piter/blog/200352/">появится перевод</a> это книги на русский.</li>
<li>Книга <a href="http://shop.oreilly.com/product/9780596801694.do">"RESTful Web Services Cookbook" (2010) by Subbu Allamaraju</a>. В интернете пишут, что в этой книге очень хорошо описана концепция REST.</li>
<li>Книга <a href="http://shop.oreilly.com/product/9780596805838.do">"REST in Practice" (2010) by Jim Webber, Savas Parastatidis, Ian Robinson</a></li>
<li>Диссертация Роя Филдинга (Roy Fielding), <a href="http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm">глава 5</a> (2000). Именно здесь было впервые введено понятие REST-архитектуры. Принципы REST изложены в общем виде, читать тяжеловато.
<li><a href="http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven">REST APIs must be hypertext-driven (2008)</a>. Статья Роя Филдинга, посвященная типичным ошибкам при реализации REST-архитектуры.</li>
<li><a href="http://www.infoq.com/articles/rest-anti-patterns">REST Anti-Patterns (2008)</a>. Статья Стефана Тилкова, посвященная типичным ошибкам при реализации REST-архитектуры.</
li>
</ul>
<p/>
<h2>REST-клиент под Android. Лекция Virgil Dobjanschi на Google I/O 2010</h2>
Реализация REST-клиента под Android обладает рядом особенностей. "По полочкам" все разложил, в свое время, Virgil Dobjanschi на Google I/O 2010, см.
"Developing Android REST client applications" (<a href="http://www.google.com/events/io/2010/sessions/developing-RESTful-android-apps.html">видео</a>, <a href="http://dl.google.com/googleio/2010/android-developing-RESTful-android-apps.pdf">слайды</a>, <a href="https://docs.google.com/file/d/0B2dn_3573C3RdlVpU2JBWXdSb3c/edit">текст
лекции</a>).
<p/>
Лекция очень интересная и информативная. В двух словах: простейший подход, который приходит в голову - запустить из Activity отдельный поток, посылать из него на сервер REST-запросы, сохранять результаты в память (а не в базу данных), - абсолютно неверен. Взамен Dobjanschi предложил (на выбор) три корректных варианта реализации REST-клиента.
<p/>
<ul>
<li><b>Pattern А.</b> Использовать Service API: Activity -> Service -> Content Provider. В данном варианте Activity работает с API Android Servcie. При необходимости послать REST-запрос Activity создает Service, Service асинхронно посылает запросы к REST-серверу и сохраняет результаты в Content Provider (sqlite). Activity получает уведомление о готовности данных и считывает результаты из Content Provider (sqlite).
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh27ZrdkEk50DoRsMUrXztCAETqa_6isNCEhArzPg7JIkyUlxVYsAiXOp3HEh0uDSQVKcuZOWD_nTro7QnkgTJm-v9SpHt3jhDNfre2TP2D5BhNld7LT03vOA2-RQLJfv2rpkCMe8dTd_EH/s1600/patternA.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh27ZrdkEk50DoRsMUrXztCAETqa_6isNCEhArzPg7JIkyUlxVYsAiXOp3HEh0uDSQVKcuZOWD_nTro7QnkgTJm-v9SpHt3jhDNfre2TP2D5BhNld7LT03vOA2-RQLJfv2rpkCMe8dTd_EH/s200/patternA.png" /></a></div>
</li>
<li><b>Pattern B.</b> Использовать ContentProvider API: Activity -> Content Provider -> Service. В этом случае Activity работает с API Content Provider, который выступает фасадом для сервиса. Данный подход основан на схожести Content Provider API и REST API: GET REST эквивалентен select-запросу к базе данных, POST REST эквивалентен insert, PUT REST ~ update, DELETE REST ~ delete. Результаты Activity так же загружает из sqlite.
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEifjI9e8Hb_7dC5Gelu3-7X0ecyE1Ax1OsQFDAOm_XUpnxZKFetoAh87jNidJhTU7lqE9WTBlHIdy7RsnP7bUzaWAga0u6JNVbqRIammLTnxjlFIU5GDjhBQQBaHfZkYG9nuHYZYWGiunHR/s1600/patternB.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEifjI9e8Hb_7dC5Gelu3-7X0ecyE1Ax1OsQFDAOm_XUpnxZKFetoAh87jNidJhTU7lqE9WTBlHIdy7RsnP7bUzaWAga0u6JNVbqRIammLTnxjlFIU5GDjhBQQBaHfZkYG9nuHYZYWGiunHR/s200/patternB.png" /></a></div>
</li>
<li><b>Pattern C.</b> Использовать Content Provider API + SyncAdapter: Activity -> Content Provider -> Sync Adapter. Вариация подхода "B", в котором вместо сервиса используется <a href="http://udinic.wordpress.com/2013/07/24/write-your-own-android-sync-adapter/">собственный Sync Adapter</a>. Activity дает команду Content Provider, который переадресовывает ее в Sync Adapter. Sync Adapter вызывается из Sync Manager, но не сразу, а в "удобный" для системы момент. Т.о. возможны задержки в исполнении команд.
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWDEzpdGzsYpre4updmf2AB38Zjd31Nu7O35ZaOII8_qsj4cTA4OAzcsYS7uesxJ3eJb7IGphNX2aWFHDkQ08b4DnplrHMorWMehI7sFq85aZfbjdKvtvbi38IXCJvjpxZkR-jQq0asI30/s1600/patternC.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWDEzpdGzsYpre4updmf2AB38Zjd31Nu7O35ZaOII8_qsj4cTA4OAzcsYS7uesxJ3eJb7IGphNX2aWFHDkQ08b4DnplrHMorWMehI7sFq85aZfbjdKvtvbi38IXCJvjpxZkR-jQq0asI30/s200/patternC.png" /></a></div>
</li>
</ul>
Основные проблемы связаны с тем, что время жизни Activity в Android совершенно непредсказуемо. В любой момент ваша Activity может быть приостановлена или даже удалена из памяти. Соответственно, какие-либо длительные операции в Activity делать нельзя, т.к. Activity запросто может "отвалиться" до завершения этих операций. В "лучшем" случае, будут потеряны результаты работы. В худшем - произойдет рассинхронизация данных на сервере и клиенте. Отсюда - необходимость все длительные операции запускать в сервисе. Сервис гораздо более живуч, чем Activity, его система без веских причин останавливать не будет.
<p/>
Сервис и активити - два разных компонента, данные между ними приходится передавать специальным образом, через сериализацию. Virgil Dobjanschi проводит аналогию с маршаллингом - передать данные через маршалинг можно, но объем передаваемых данных лучше минимизировать. Оптимальным представляется подход, при котором сервис сохраняет данные в базу данных, а активити их из базы считывает. В целом, тонкостей реализации - масса, Virgil Dobjanschi на них тщательно обращает внимание в своем выступлении. Некоторые важные моменты:
<ul>
<li>Данные, полученные от REST-сервера, всегда сохраняются в sqlite. Напрямую в Activity они никогда не передаются. Вместо этого в Activity передается уведомление о том, что данные загружены в sqlite и их можно оттуда загрузить (вариант - Activity получает уведомление об обновлении данных в Content Provider через Content Observer).</li>
<li>При выполнении операций insert, delete, update данные в sqlite обновляются дважды: первый раз до отправки REST-запроса, второй раз - после получения результата. Первая операций выставляет информационные флаги, сигнализирующие о типе операции, проводимой над данными, и о статусе операции.</li>
<li>REST-методы следует всегда выполнять в отдельном потоке.</li>
<li>Следует использовать Apache HTTP client, а не Java URL connection.</li>
<li>Форматы данных в порядке предпочтения: какой-либо бинарный формат (например, <a href="http://ru.wikipedia.org/wiki/AMF_(формат_обмена_данными)">AMF3</a>), затем JSON, затем XML.</li>
<li>Желательно включать gzip. GZip на Android реализован "нативно", библиотека быстрая. В некоторых случаях можно получить коэффициент сжатия 5:1 и даже 10:1, в зависимости от количества получаемых данных. Использование GZip ускоряет загрузку данных и экономит батарею.</li>
<li>Если используете Sqlite - используйте транзакции.</li>
<li>Если программе требуется скачать 10-20 картинок, не стоит запускать 10-20 параллельных закачек. Запускайте 1-3, а остальные ставьте в очередь. </li>
<li>Activity регистрирует <a href="http://developer.android.com/reference/android/os/ResultReceiver.html">binder callback</a> (т.е. <a href="http://habrahabr.ru/post/167679/">ResultReceiver</a>), для получения ответа от сервиса. Этот callback нужно обязательно удалить при вызове onPause у Activity, иначе можно налететь на ANR.</li>
<li>Длительные операции всегда следует запускать из сервиса. Сервис обязательно следует останавливать после того, как требуемые операции выполнены.</li>
<li>Не стоит позволять вашей базе данных расти бесконечно. При превышении лимита в 1 Мб работа приложения существенно замедлится (<a href="http://ponystyle.com/blog/2010/03/26/dealing-with-asset-compression-in-android-apps/">не актуально в Android версии 2.3 и выше.</a>)</li>
<li>Необходимо минимизировать сетевой трафик.</li>
<li>Следует разбивать данные на страницы (конечно, если REST Api предоставляют такую возможность).</li>
<li>Для некритичной по времени синхронизации данных между клиентом и сервером рекомендуется использовать SyncAdapter.</li>
</ul>
<br/>
<h2>Варианты реализации REST-клиентов на Android</h2>
Предложенные паттерны можно реализовать вручную, воспользовавшись библиотеками <a href="http://loopj.com/android-async-http/">Android Asynchronous HTTP Client API</a> или <a href="http://habrahabr.ru/post/188860/">Volley (Google)</a> (см. примеры в статье <a href="http://adndevblog.typepad.com/cloud_and_mobile/2013/09/experimenting-a-couple-of-android-rest-apis.html">Experimenting a couple of Android REST APIs</a>). Можно взять за основу готовые варианты реализации паттернов, например:
<ul>
<li><a href="http://www.codeproject.com/Articles/429997/Sample-Implementation-of-Virgil-Dobjanschis-Rest-p">Sample Implementation of Virgil Dobjanschi's Rest pattern</a> - статья на CodeProject, <b>pattern A</b></li>
<li><a href="https://github.com/aug-mn/restful-android">Restfull-android на Github</a> by Haberman and Armstrong of the Minnesota Android User's Group. <b>Pattern A</b></li>
<li>Пример реализации второго варианта - <b>Pattern B</b>, - есть в книге <a href="http://shop.oreilly.com/product/0636920023005.do">"Programming Android, 2nd Edition" by Zigurd Mednieks, Laird Dornin, G. Blake Meike and Masumi Nakamura,</a> в главе 13 "A Content Provider as a Facade for a RESTful Web Service"</li>
</ul>
<p/>
Однако, проще и удобнее будет воспользоваться одной из подходящих библиотек:
<ul>
<li><a href="https://github.com/octo-online/robospice">RoboSpice</a> - мощная, модульная, хорошо документированная библиотека, регулярно обновляется. <b>Pattern A</b> + много полезных доп. функций. Детальное объяснение, как устроена библиотека: <a href="https://github.com/octo-online/robospice/wiki/A-User's-Perspective-on-RoboSpice">A User's Perspective on RoboSpice</a>.</li>
<li><a href="http://www.datadroidlib.com/">Datadroid</a>, реализует <b><a href="http://www.datadroidlib.com/presentation">pattern A</a></b>, о чем явно написано в документации. Текущая версия <a href="http://www.datadroidlib.com/2013/01/datadroid-version-2-1-2-is-available">2.1.2</a></li>
<li><a href="https://github.com/PCreations/RESTDroid">RESTDroid</a>, <b>pattern A</b></li>
<li><a href="http://robotoworks.com/mechanoid/doc/ops/index.html">Mechanoid Ops</a>. Mechanoid Library - это набор из четырех кодогенераторов (база данных sqlite, код для работы с длительными асинхронными запросами в отрыве от UI - <b>Pattern A</b>, работа с JSON, работа с Shared Preference). Текущая версия - 0.2.2 ALPHA</li>
<li><a href="https://github.com/novoda/RESTProvider">RESTProvider</a> (не совсем ясно, развивается ли библиотека: последнее обновление - 2 года назад).</li>
<li><a href="http://square.github.io/retrofit/">Retrofit</a>. HTTP-запросы описываются через аннотации, синхронные и асинхронные вызовы REST-методов, данные могут передаваться в виде JSON, XML, Protobuf. Текущая версия 1.3, обновляется регулярно. С этой библиотекой на пару умеет работать RoboSpice.</li>
<li><a href="https://github.com/fedepaol/PostmanLib--Rings-Twice--Android">PostmanLib</a>. </li>
</ul>
<h2>Задачи, которые требуется решить при реализации REST-клиента</h2>
Перечислим, какие основные задачи придется решать при реализации REST-клиента на Android согласно паттернам Virgil Dobjanschi.
<ul>
<li>Управление сервисом: запуск, остановка.</li>
<li>Передача результатов из сервиса в активити.</li>
<li>Кэшировать результатов в sqlite.</li>
<li>Фиксирование статуса данных sqlite перед и после выполнения REST-запроса.</li>
<li>Запись информации о проводимых REST-операциях в sqlite.</li>
<li>Парсинг полученных данных.</li>
<li>Конструирование REST-запроса на основе URI и набора параметров.</li>
<li>Выполнение сетевых запросов к REST-серверу.</li>
<li>Чистка базы данных от устаревших данных.</li>
<li>В случае неудачи REST-запроса, пытаться повторить запрос (например, экспоненциально увеличивая время между запросами).</li>
<li>Возможность отложенного запуска REST-запроса через SyncAdapter.</li>
</ul>
<p/>
Теперь посмотрим, что предлагают библиотеки. Я просмотрел исходные коды трех наиболее популярных библиотек - RoboSpice, Datadroid и RESTDroid, - и попробовал составить сравнительную таблицу их возможностей. Вот что получилось.
<table border="1" width="100%">
<tr><th></th> <th>RoboSpice 1.4.8</th> <th>Datadroid 2.1.2</th> <th>RESTDroid 0.8.2</th></tr>
<tr><td>Тип паттерна</td>
<td>A</td>
<td>A</td>
<td>A</td>
</tr>
<tr><td>Кэширование данных</td>
<td>Кэш внешний (требуется реализовать абстрактный класс CacheManager.java). При запуске запроса можно указать, кэшировать его или нет, если кэшировать - то какое время считать результаты валидными. Среди расширений библиотеки есть <a href="https://github.com/octo-online/robospice/wiki/ORMLite-module">ORMLite module</a>, предназначенный для записи и считывания POJO в/из sqlite.</td>
<td>Кэш встроенный (в RequestManager), данные хранятся в памяти в виде экземпляра LruCache. Для каждого типа запросов кэширование (вкл/выкл) настраивается отдельно. Не совсем ясно, можно ли задействовать вместо LruCache-кэша базу данных, т.к. LruCache не отключаемый.</td>
<td>Кэш встроенный (CacheManager), результаты каждого запроса хранятся в отдельном файле. Валидность данных в кэше определяется временем создания файла. Использование кэша можно отключить, а вот запись в кэш, похоже, нельзя. Способ хранения данных можно менять путем реализации наследника <code>PersistableFactory</code></td>
</tr>
<tr><td>Как идентифицируются типы REST-запросов.</td>
<td>Идентификация на уровне экземпляров класса, <code> if (this == other) ... </code></td>
<td>int</td>
<td>UUID</td>
</tr>
<tr><td>Слой ServiceHelper</td>
<td><code>SpiceManager</code></td>
<td><code>RequestManager</code></td>
<td><code>WebService</code></td>
</tr>
<tr><td>Pre- и post- операции для REST-методов</td>
<td>Встроенной поддержки нет. Можно реализовать самостоятельно в реализации наследника <code>SpiceRequest</code></td>
<td>Встроенной поддержки нет. Можно реализовать самостоятельно в реализации наследника <code>Operation</code></td>
<td>Логику пре- и пост-запросов можно изменить, создав наследника класса <code>Processor</code> и перекрыв в нем методы <code>preRequestProcess, preGetRequest</code> и т.д.</td>
</tr>
<tr><td>Встроенные средства парсинга результатов</td>
<td>Функция <code>loadDataFromNetwork</code> в <code>SpiceRequest<T></code> предусматривает реализацию парсинга.</td>
<td>Для каждого типа запросов можно задать реализацию парсера, см. функцию <code>getOperationForType</code> в классе <code>PoCRequestService</code> в демонстрационном приложении.</td>
<td>Предусмотрен парсинг результатов через <code>parseToObject</code> в <code>Processor</code>, парсер задается через фабрику классов.</td>
</tr>
<tr><td>Передача данных из сервиса в Activity</td>
<td>Через RequestListener. Тип результата кастомизируется через generic-параметр. Listener передается в качестве параметра в spiceManager.execute, результаты приходят в распарсенном виде.</td>
<td>Через RequestListener. Результаты загружаюстя в Bundle. Listener передается в качестве параметра в RequestManager.execute. Результаты приходят в виде bundle.</td>
<td>Через RequestListeners можно получить код результата. В случае успеха, готовый распарсенный объект можно загрузить через метод <code>getResource</code> из <code>RESTRequest</code> </td>
</tr>
<tr><td>Реализация REST-запросов</td>
<td>Работа с сетью кастомизируется наследниками SpiceRequest (функция loadDataFromNetwork). В библиотеку входит реализация SpiceRequest по умолчанию, на основе: <a href="http://docs.oracle.com/javase/6/docs/api/java/net/URL.html">java.net.URL</a> для текстовых данных, <a href="http://developer.android.com/reference/java/net/HttpURLConnection.html">HttpURLConnection</a> для бинарных данных</td>
<td>Для сетевой работы используется <a href="http://square.github.io/okhttp/">OkHTTP from Square</a> или <a href="http://developer.android.com/reference/java/net/HttpURLConnection.html">HttpURLConnection</a>. Вначале идет попытка инициализировать okHttpClient через java.lang.reflect, в случае исключения используется стандартный HttpURLConnection.</td>
<td>Для сетевой работы используется <a href="http://developer.android.com/reference/org/apache/http/client/HttpClient.html">DefaultHttpClient</a> (на Gingerbread и выше <a href="http://android-developers.blogspot.ru/2011/09/androids-http-clients.html">предпочтительнее использовать HttpURLConnection, а не DefaultHttpClient</a>)</td>
</tr>
<tr><td>Автоматический повтор запроса в случае неудачи</td>
<td>Есть. Алгоритм повтора настраивается через RetryPolicy. По умолчанию используется DefaultRetryPolicy, реализующий "exponential back off"-алгоритм.</td>
<td>Нет. В случае ошибки при выполнении запроса "наверх" отправляется исключение с информацией о типе проблемы, см. <code>RequestService</code></td>
<td>Автоматический повтор через заданные интервалы времени, по умолчанию, 1 минута.</td>
</tr>
<tr><td>Поддержка GZip</td>
<td>Реализуется путем кастомизации класса SpiceRequest, см. пример robospice-motivations.</td>
<td>Встроена, настраивается через <code>setGzipEnabled</code> в <code>NetworkConnection</code></td>
<td>нет(?), см. HttpRequestHandler.java</td>
</tr>
<tr><td>Конструктор REST-запросов</td>
<td>Все стандартные реализации SpiceRequest<T> принимают в качестве параметра конструктора уже готовую URL, которую необходимо конструировать самостоятельно.</td>
<td>Класс Request позволяет задать параметры запроса. Класс NetworkConnectionImpl реализует сборку URL для REST-запроса.</td>
<td>Класс RESTRequest принимает уже готовую URL, которую необходимо конструировать самостоятельно.</td>
</tr>
<tr><td>Уведомления в UI thread о ходе выполнения операции</td>
<td>Встроена, см. <code><small>SpiceNotificationService</small></code></td>
<td>нет?</td>
<td>нет?</td>
</tr>
<tr><td>Многопоточность при отправке REST-запросов</td>
<td>Да. Размер пула потоков настраивается путем перекрытия функции <code>getThreadCount</code> в <code>SpiceManager</code>, по умолчанию 3.</td>
<td>Да. Размер пула потоков настраивается путем перекрытия функции <code><small>getMaximumNumberOfThreads</small></code> в <code>RequestService</code>, по умолчанию 1.</td>
<td>Да. Размер пула потоков настраивается константой в классе WebService, по умолчанию 10.</td>
</tr>
<tr><td>Встроенная поддержка SyncAdapter</td>
<td>Нет.</td>
<td>Нет.</td>
<td>Нет.</td>
</tr>
<tr><td>Минимальная версия Android SDK</td> <td>8 (Froyo / 2.2.x)</td> <td>8 (Froyo / 2.2.x)</td> <td>8 (Froyo / 2.2.x)</td></tr>
<tr><td>Примеры приложений</td>
<td>На GitHub отдельный репозиторий <a href="https://github.com/octo-online/RoboSpice-samples">RoboSpice-samples</a> с примерами.</td>
<td>Демонстрационное приложение <a href="https://github.com/foxykeep/DataDroid/tree/master/DataDroidPoC">DataDroidPoC</a> распространяется вместе с исходниками.</td>
<td>Демонстрационного примера нет. Страница документации <a href="http://pcreations.fr/restdroid-guides">RESTDroid guide</a> в настоящий момент не доступна.</td>
</tr>
<tr><td>Наличие unit-тестов</td>
<td>В описании библиотеки на git сказано про 160 тестов.</td>
<td>нет</td>
<td>нет</td>
</tr>
</table>
<p/>
<h2>Выводы</h2>
<ul>
<li>RoboSpice - крайне гибкая библиотека. Хорошо подходит в качестве фундамента для реализации "собственного велосипеда". Кастомизация на основе generic и policy, общие решения, минимум ограничений для разработчика.</li>
<li>Datadroid и RESTDroid - неплохие попытки реализовать готовые REST-библиотеки в стиле "бери и пользуйся". Каждая - со своими преимуществами и ограничениями.</li>
</ul>
<p/>
<ul>
<li><a href="http://derevyanko.blogspot.ru/2014/01/rest-android-2-api.html">REST под Android. Часть 2: разрабатываем API</a></li>
<li><a href="http://derevyanko.blogspot.ru/2014/02/rest-android-3-square.html">REST под Android. Часть 3: библиотеки Square</a></li>
</ul>
<p/>
<a href="http://ru.wikipedia.org/wiki/Диаграмма_связей">Mindmap</a>, созданный в процессе работы над этой статьей, можно скачать <a href="https://www.dropbox.com/s/w54m5xp31pgsbsy/201401_rest1.mm">здесь</a>, в формате <a href="http://freemind.sourceforge.net/">Freemind 1.0</a> (портативная версия: <a href="http://sourceforge.net/projects/freemind/files/freemind/1.0.0/freemind-bin-1.0.0.zip/download">freemind-bin-1.0.0.zip</a>).
<br /></div>Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com3tag:blogger.com,1999:blog-3333619357391010212.post-10736748711287806212014-01-11T20:19:00.000+08:002014-01-12T08:58:40.063+08:00Сдача налоговой отчетности ИП (УСН 6%) за 2013 год<div dir="ltr" style="text-align: left;" trbidi="on">
Год закончился, до 30 апреля нужно подать декларацию по ИП. Как всегда, в этом году в отчетности есть <a href="http://forum.klerk.ru/showthread.php?t=512431">кое-какие изменения</a>:
<ul><li>ОКАТО заменили на <a href="http://ru.wikipedia.org/wiki/Общероссийский_классификатор_территорий_муниципальных_образований">ОКТМО</a>,</li><li>отменили необходимость подачи "сведений о среднесписочной численности работников" для ИП-шников, у которых в течении года не было работников.</li></ul>
В остальном, похоже, все как в <a href="http://derevyanko.blogspot.com/2011/12/android.html">прошлые годы</a>.
<br/>P.s. Не забудьте <a href="http://www.gnivc.ru/software/free_software/software_ul_fl/taxpayer_ul/1370/">скачать свежую версию программы Налогоплательщик ЮЛ</a>.
<br /></div>
Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com0tag:blogger.com,1999:blog-3333619357391010212.post-59183816764079578762014-01-03T11:12:00.001+08:002014-04-02T12:16:48.604+08:00Контрактное программирование под Android<div dir="ltr" style="text-align: left;" trbidi="on">
О пользе и преимуществах методики контрактного программирования (Design by Contracts, DBC) написано много. Вот несколько хороших статей:
<ul>
<li><a href="http://www.rsdn.ru/article/design/Code_Contracts.xml">Теляков С.В. Проектирование по контракту. RSDN Magazine #1-2010</a></li>
<li><a href="http://en.wikibooks.org/wiki/Computer_programming/Design_by_Contract">Computer Programming/Design by Contract</a></li></li>
<li><a href="http://www.win.tue.nl/~wstomv/edu/2ip30/references/design-by-contract/index.html">Todd Plessel. Design By Contract: A Missing Link In The Quest For Quality Software</a></li>
<li><a href="http://www.eiffel.com/developers/design_by_contract_in_detail.html">Building bug-free O-O software: An Introduction to Design by Contract</a></li>
</ul>
Основная идея методики: улучшить надежность программного обеспечения за счет определения контрактов между взаимодействующими программными компонентами. Для метода задается контракт, который описывает:
<ul>
<li>пред-условия, которым должен соответствовать вызывающий код;</li>
<li>пост-условия, которые гарантируются методом.</li>
</ul>
Некоторые преимущества, которые обеспечивает DBC:
<ul>
<li>Повышение надежности программы за счет систематической и гарантированной проверки входных данных.</li>
<li>Исключение лишних проверок в коде. Корректность данных проверяется в методе, а не в вызывающем коде.</li>
<li>Автоматическое документирование кода, создание спецификации API программного модуля.</li>
<li>Косвенно: повышение эффективности статического анализа кода.</li>
</ul>
Вариантов реализации DBC для Java достаточно много. Обзорные статьи на эту тему:
<ul>
<li><a href="http://marxsoftware.blogspot.ru/2011/02/contracts-for-java-another-entrant-in.html">Contracts for Java: Another Entrant in Java Programming by Contract</a></li>
<li><a href="http://www.javapractices.com/topic/TopicAction.do?Id=194">Design by Contract</a></li>
<li><a href="http://java-arg-val.sourceforge.net/introduction.html">Java Argument Validation</a></li>
</ul>
Однако, Java - это Java, а Android - это Android. Как всегда, под Android есть своя специфика. В данной статье я хочу рассмотреть практические подходы для реализации технологии DBC при программировании на Android.
<a name='more'></a>
<p/>
<h2>Assert, Exception и Guava</h2>
Язык Java не поддерживает концепцию Design by Contracts напрямую и, вероятно, <a href="http://bugs.sun.com/view_bug.do?bug_id=4449383">не будет поддерживать в обозримом будущем</a>. Все что доступно разработчикам - <a href="http://docs.oracle.com/javase/6/docs/technotes/guides/language/assert.html#usage-conditions">Programming With Assertions</a>. Т.е. assert-ы и исключения. Пример:
<pre class="brush:java">
public BigInteger modInverse(BigInteger m) {
//проверка пред-условий. В случае ошибки - exception
if (m == null) {
throw new ArithmeticException("Value is null");
}
if (m.signum <= 0) {
throw new ArithmeticException("Modulus not positive: " + m);
}
...
//проверка пост-условий
assert this.multiply(result).mod(m).equals(ONE) : this;
return result;
}
</pre>
Пред-условия предлагается проверять всегда и в случае ошибки генерировать exception. Пост-условия - проверять через assert, т.е. только в отладочном коде. При необходимости, включать проверку assert в релизе.
<p/>
Несмотря на то, что такой подход вполне рабочий, он не лишен ряда недостатков:
<ul>
<li>код метода загромождается;</li>
<li>условия контракта "размазаны" в теле метода;</li>
<li>поздняя проверка контракта (во время выполнения, а не в процессе программирования/компиляции);</li>
<li>"неотключаемость" проверок и т.д.</li>
</ul>
Есть так же проблема, специфичная для Android: стандартные assert по умолчанию отключены на Android-девайсах и эмуляторе. Их нужно не забывать <a href="http://stackoverflow.com/questions/2364910/can-i-use-assert-on-android-devices">включать</a>, например командой <code>adb shell setprop debug.assert 1</code>(assert будет работать до перезагрузки девайса). Вместо стандартных assert можно использовать <a href="http://developer.android.com/reference/junit/framework/Assert.html">JUnit Assert</a> и <a href="http://derevyanko.blogspot.ru/2014/04/junitframeworkassert-android.html">исключать их из релиза с помощью ProGuard</a>. <b>Update:</b> для проверки состояния стандартных контролов Android хорошо подойдет библиотека <a href="http://square.github.io/fest-android/">FEST Android</a>.
<p/>Частично указанные выше проблемы можно решить с помощью набора библиотек <a href="http://code.google.com/p/guava-libraries/">Guava</a>. В состав Guava входит библиотека <a href="http://code.google.com/p/guava-libraries/wiki/PreconditionsExplained">Preconditions</a>, предоставляющая набор функций для явной и компактной записи предусловий. Пример выше можно переписать с помощью Guava.Preconditions следующим образом:
<pre class="brush:java">
public BigInteger modInverse(BigInteger m) {
//проверка пред-условий. В случае ошибки - exception
com.google.common.base.Preconditions.checkNotNull(m, "Value is null");
com.google.common.base.Preconditions.checkArgument(m.signum <= 0, "Modulus not positive: %d", m);
...
//проверка пост-условий
assert this.multiply(result).mod(m).equals(ONE) : this;
return result;
}
</pre>
<h2>Null анализ. JSR 305, JSR 308 и статический анализ кода</h2>
В Java 5 был добавлен механизм аннотаций - появилась возможность снабжать код Java метаданными. Аннотации находят <a href="http://habrahabr.ru/post/139736/">много полезных применений</a>, в том числе, <a href="http://androidannotations.org/">Android-специфичных</a>.
<p/>Одно из типичных применений аннотаций - помощь в null-анализе. С помощью аннотаций @Nullable и @NotNull разработчик явно указывает, какие параметры могут принимать значения null, какие не могут, какие методы могут возвращать null, какие нет. По сути, это частный случай "контрактов" концепции Design by Contracts. Подобные "контракты" позволяют отловить распространенные ошибки, связанные с null, в частности:
<ul>
<li>Null Pointer Access/Dereference: попытка обращения к объекту, в то время как он равен null; null pointer exception в результате.</li>
<li>Redundant Check For Null: лишняя проверка на null там, где точно известно, что значение не может быть равно null.</li>
</ul>
Типичный пример:
<pre class="brush:java">
public @NonNull String test(@Nullable String b) {
return b; //Ошибка! функция не должна возвращать null, тогда как b может быть равна null
}
public void makeTest() {
String result = test(null);
if (result != null) { ... } //!Ошибка: test не может возвращать null, проверка на null не нужна
}
</pre>
В приведенном коде две ошибки. Первая: согласно аннотации @NonNull, функция test не может возвращать null, но в данной она null может вернуть. Вторая ошибка: поскольку test не может возвращать null, то проверка результатов работы функции на null - лишняя. Статические анализаторы кода легко ловят подобные ошибки на стадии компиляции кода.
<p/>
Первоначально появилось великое <a href="http://stackoverflow.com/questions/4963300/which-notnull-java-annotation-should-i-use">множество вариантов Null-аннотаций</a>. Естественно, это оказалось не удобно. Поэтому была предпринята попытка выработать стандартный набор "аннотаций, способных помочь статическим анализаторам в поиске багов" - см. <a href="http://jcp.org/en/jsr/detail?id=305">JSR-305: Annotations for Software Defect Detection</a>. Помимо null-аннотаций, набор включал и другие полезные аннотации - для работы с регулярными выражениями, многопоточностью, аннотации для борьбы с утечкой ресурсов и т.д. (<a href="http://www.java2s.com/Code/Jar/j/Downloadjsr305jar.htm">полный список</a>). В настоящий момент, этот JSR-документ не развивается и помечен как <i>Dormant</i> (бездействующий, пассивный). Тем не менее, предложенный в JSR-305 аннотаций утвердился в качестве стандарта де-факто и, по крайней мере в части null-аннотаций, его поддерживают многие инструменты статического анализа (проблемы с именованием все равно остались: например, в IntelliJ используется директива @NotNull вместо @NonNull из JSR-305).
<p/>Замечу, что помимо JSR-305, есть <a href="http://beanvalidation.org/1.0/spec/">JSR-303: Bean Validation</a>, описывающий набор аннотаций <code>javax.validation.constraints.*</code>. Он <i>уже</i> реализован в Java EE 6. Однако, эти аннотации предназначены для проверки кода <i>во время исполнения, а не для статического анализа</i>.
<p/>В Java 8 планируют существенно расширить возможности аннотаций, см. <a href="http://jcp.org/en/jsr/detail?id=308">JSR-308: Annotations on Java Types</a>. Например, можно будет написать <code>ArrayList<@Nonnull String></code>. По идее, возможности nullness-анализа на стадии компиляции должны существенно возрасти.
<p/>
Конечно, расстановка аннотаций в коде требует дополнительной работы со стороны программиста. Некоторые разработчики считают, что такая работа может не оправдать себя - аннотаций не достаточно, чтобы выловить многие null-ошибки, см.
<a href="http://www.klocwork.com/blog/static-analysis/jsr-305-a-silver-bullet-or-not-a-bullet-at-all/">JSR 305: a silver bullet or not a bullet at all?</a> Тем не менее, несомненно, что аннотации позволяют более точно описать намерения программиста и помочь статическим анализаторам. А количество дополнительной работы можно и нужно минимизировать. Разработчики библиотеки <a href="http://code.google.com/p/google-guice/wiki/UseNullable">Google Guice</a> предлагают следующий подход:
<ul>
<li>запретить null по умолчанию;</li>
<li>все nullable-значения явно помечать @Nullable.</li>
</ul>
При таком подходе аннотацию явного использования @NotNull не требуется, достаточно использовать только @Nullable.
<p/>
Стандарт JSR-305 не описывает аннотацию "non null by default". Однако, такая аннотация есть в Eclipse: с помощью <a href="http://help.eclipse.org/juno/index.jsp?topic=%2Forg.eclipse.jdt.doc.isv%2Freference%2Fapi%2Forg%2Feclipse%2Fjdt%2Fannotation%2FNonNullByDefault.html">org.eclipse.jdt.annotation.NonNullByDefault</a> можно <a href="http://stackoverflow.com/questions/8405336/how-do-i-use-or-edit-package-info-java">пометить пакет</a>, после чего весь код в пакете будет считаться not-null по умолчанию.
<p/>
Разработчики IntelliJ пошли несколько иным путм. IDE <a href="http://blog.jetbrains.com/idea/2010/12/auto-infer-nullablenotnull-annotations/">автоматически определяет параметры, которые используются без проверки на null и далее в процессе анализа считает, что это @NotNull-параметры</a>.
<h2>Инструменты для анализ кода</h2>
Теперь поговорим о средствах анализа кода. Статических анализаторов кода для Android <a href="http://derevyanko.blogspot.ru/2012/01/android.html">достаточно много</a>, но не все из них содержат средства nullness-анализа. Вот некоторые, которые точно содержат:
<ul>
<li>Компилятор <a href="www.eclipse.org">Eclipse</a>, (раздел документации <a href="http://help.eclipse.org/juno/index.jsp?topic=%2Forg.eclipse.jdt.doc.user%2Ftasks%2Ftask-using_null_annotations.htm">Using null annotations</a>);</li>
<li><a href="http://findbugs.sourceforge.net/">FindBugs</a>, (<a href="http://findbugs.sourceforge.net/manual/annotations.html">поддерживаемые аннотации JSR-305</a>);</li>
<li><a href="http://code.google.com/p/checker-framework/">Checker Framework</a>, (<a href="http://types.cs.washington.edu/checker-framework/current/checkers-manual.html#introduction">документация - заявлена поддержка JSR-305</a>);</li>
<li>Компилятор <a href="http://www.jetbrains.com/idea/features/android.html">IntelliJ</a> / <a href="http://developer.android.com/sdk/installing/studio.html">Android Studio</a>, (раздел документации <a href="http://www.jetbrains.com/idea/documentation/howto.html">Nullable How-To</a>);</li>
<li><a href="http://www.juliasoft.com/">Julia</a>, коммерческий продукт;</li>
<li><a href="http://www.klocwork.com/">Klocwork</a>, коммерческий продукт.</li>
</ul>
Протестируем работу бесплатного инструментария на тестовом примере.
<h3>Тестовый пример</h3>
<pre class="brush:java">
public static class TestClass {
@NonNull String _NonNull /*= "test1"*/; //Error 1: _NonNull is not initialized in the code below
@Nullable String _Nullable;
@Nullable private String test_nullable(@NonNull String paramNonNull, @Nullable String paramNullable) {
Log.i(LOG_TAG, _NonNull.toString()); //null pointer dereference because of Error 1
Log.i(LOG_TAG, _Nullable.toString()); //Error 2: null pointer dereference
Log.i(LOG_TAG, paramNonNull.toString());
Log.i(LOG_TAG, paramNullable.toString()); //Error 3: null pointer dereference
_NonNull = null; //Error 4: store of null value into nonnull-variable
double d = java.lang.Math.random();
if (d > 0.99f) {
return null;
}
if (d > 0.9f ) {
_Nullable = "test2";
_NonNull = "test3";
}
if (_Nullable != null) { //no error 1: _Nullable can be null here
String m = _Nullable.toString(); //no error 2: because _Nullable is not null here
Log.i(LOG_TAG, m);
Log.i(LOG_TAG, _Nullable.toString()); //no error 3: because _Nullable is not null here
}
if (_NonNull != null) { //Error 5: redundant nullcheck
Log.i(LOG_TAG, _NonNull.toString());
}
if (_NonNull.length() > 2) { //Error 6: possible null pointer dereference
return _NonNull;
} else {
return _Nullable;
}
}
@NonNull private String test_nonnull(@NonNull String paramNonNull, @Nullable String paramNullable) {
return paramNullable; //Error 7: paramNullable can be null, but function must returns not-null
}
public void makeTests() {
String t1 = test_nullable(null, "test4"); //Error 8: first param must be not null
if (t1 != null) { //no error 4: t1 can be null
Log.i(LOG_TAG, t1.toString()); //no error 5: t1 is not null here
}
Log.i(LOG_TAG, t1.toString()); //Error 9: possible null pointer dereference
String t2 = test_nonnull("test5", null);
if (t2 != null) { //Error 10: redundant nullcheck
Log.i(LOG_TAG, t2.toString());
}
Log.i(LOG_TAG, t2.toString()); //no error 6: t2 is not null here
}
}
</pre>
Вполне допускаю, что данный пример охватывает не все возможные типы ошибок. Но типичные ошибки в нем точно присутствуют.
<h3>Результаты тестирования</h3>
<table border="1">
<tr>
<td>Issue</td><td>Eclipse 4.1 (Indigo)</td><td>Eclipse 4.2 (Juno)</td><td>Eclipse 4.3 (Kepler)</td><td>FindBugs 2.0.3</td><td>Checker Framework 1.7.1</td><td>Android Studio 0.4</td>
</tr>
<tr>
<td>Error 1</td> <td>FAILED</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>FAILED</td>
</tr>
<tr>
<td>Error 2</td> <td>FAILED</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td>
</tr>
<tr>
<td>Error 3</td> <td>FAILED</td> <td>ok</td> <td>ok</td> <td>FAILED</td> <td>ok</td> <td>ok</td>
</tr>
<tr>
<td>Error 4</td> <td>FAILED</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td>
</tr>
<tr>
<td>Error 5</td> <td>FAILED</td> <td>FAILED</td> <td>ok</td> <td>FAILED</td> <td>ok</td> <td>ok</td>
</tr>
<tr>
<td>Error 6</td> <td>FAILED</td> <td>FAILED</td> <td>FAILED</td> <td>ok</td> <td>FAILED</td> <td>FAILED</td>
</tr>
<tr>
<td>Error 7</td> <td>FAILED</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td>
</tr>
<tr>
<td>Error 8</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>FAILED</td> <td>ok</td> <td>ok</td>
</tr>
<tr>
<td>Error 9</td> <td>FAILED</td> <td>ok</td> <td>ok</td> <td>FAILED</td> <td>ok</td> <td>ok</td>
</tr>
<tr>
<td>Error 10</td> <td>FAILED</td> <td>ok</td> <td>ok</td> <td>FAILED</td> <td>FAILED</td> <td>ok</td>
</tr>
<tr>
<td>No Error 1</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td>
</tr>
<tr>
<td>No Error 2</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td>
</tr>
<tr>
<td>No Error 3</td> <td>ok</td> <td>FAILED</td> <td>FAILED</td> <td>ok</td> <td>FAILED</td> <td>ok</td>
</tr>
<tr>
<td>No Error 4</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td>
</tr>
<tr>
<td>No Error 5</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td>
</tr>
<tr>
<td>No Error 6</td> <td>FAILED</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td> <td>ok</td>
</tr>
</table>
<p/>
Компилятор IntelliJ / Android Studio как всегда на высоте. Последняя версия Eclipse 4.3 (Kepler) не отстает, в ней nullness-анализ сильно допилили. Правда, есть пара особенностей:
<ul>
<li>По умолчанию null-analysis с учетом аннотаций отключен, его нужно включить в настройках проекта: <code>Java Compiler/Errors Warnings/Null analysis/Enable annotation-based null analysis</code>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmX_L3_dBDM7katO58Jxs8dnZyedKST-NIbQiYxsrpVIAzXY5l7WydsBSx2To7vzwsZhdH4oUNnf6P6p4phonn305GS677Js-MmbZ6rlqPgswJOioZLVfXC-FU0KFBsVduflLy_8psT7KO/s1600/eclipse.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmX_L3_dBDM7katO58Jxs8dnZyedKST-NIbQiYxsrpVIAzXY5l7WydsBSx2To7vzwsZhdH4oUNnf6P6p4phonn305GS677Js-MmbZ6rlqPgswJOioZLVfXC-FU0KFBsVduflLy_8psT7KO/s200/eclipse.png" /></a></div>
</li>
<li>Из-за ложных срабатываний (например, см. "no error 3" в тестовом примере) приходится выставлять уровень предупреждений для всех типов ошибок в Warning.</li>
</ul>
FindBugs работает отлично и находит некоторые ошибки, которые пропускают другие анализаторы. Текущая версия Checker Framework довольно сильно глючит - зависает в процессе анализа на проектах, использующих сторонние библиотеки, не работает под 64-битным eclipse... Но если не зависнет, то может принести много пользы на практике.
<p/>
<h2>Тяжелая артиллерия: библиотеки, реализующие Design By Contracts в Java</h2>
Обратимся теперь к библиотекам Java. Варианты реализации подобных библиотек не плохо рассмотрены в диссертации создателя популярной библиотеки Modern Jass
<a href="http://modernjass.sourceforge.net/docs/mastersthesis-presentation_johannes_rieken.pdf">слайды (pdf)</a>, <a href="http://modernjass.sourceforge.net/docs/mastersthesis-johannes_rieken.pdf">диссертация (pdf)</a>. В целом, реализации DBC для Java отличаются прежде всего способом описания контрактов (аннотации, скриптовый язык в комментариях, отдельные методы и даже классы) и способом проверки соблюдения контракта (пре-процессор, подмена байткода, собственный компилятор, интерпретатор, аспектно-ориентированное программирование и т.д.). Вот, далеко не полный, список библиотек, реализующих концепцию Design by Contracts в Java:
<ul>
<li><a href="http://modernjass.sourceforge.net/">Modern Jass.</a> Контракты задаются аннотациями. Плагин для Eclipse есть только под Mac. Последнее обновление: 2007 год.
<pre class = "brush:java">
public class Bar {
@Pre("args.length % 2 == 0")
public static void main(String[] args){
System.out.println("Hello, " + args.length);
}
}
</pre>
</li>
<li><a href="http://polyglotprogramming.com/contract4j">Contract4J.</a> Контракты задаются аннотациями Java и записываются на языке <a href="http://en.wikipedia.org/wiki/AspectJ">AspectJ</a>. Последнее обновление: 2010 год.
<pre class ="brush:java">
@Contract
public class SearchEngine {
...
@Pre
@Post("$return != null && $return.isValid()")
public PhoneNumber search (String first, String last,
Address streetAddress) {
PhoneNumber result =
doSearch (first, last, streetAddress);
return result;
}
...
}
</pre>
</li>
<li><a href="http://jcontractor.sourceforge.net/">jContractor.</a> Контракты записываются в дополнительных методах, имеющих определенные названия. Например, для проверки предусловий и постусловий метода "push" создаются дополнительные методы push_Precondition и push_Precondition, а для проверки инварианта класса метод _Invariant. Последнее обновление библиотеки: 2003 год.
<pre class="brush:java">
class Stack {
public void push (Object o) { ... }
protected boolean push_Precondition (Object o) {
return o != null;
}
protected boolean push_Postcondition (Object o, Void RESULT) {
return implementation.contains(o) && (size() == OLD.size() + 1);
}
}
</pre>
</li>
<li><a href="http://c4j-team.github.io/C4J/ease-of-use.html">C4J.</a> Контракт для класса задается <b>отдельным</b> классом. В процессе работы C4J instrumentor <a href="http://c4j-team.github.io/C4J/under-the-hood.html">автоматически изменяет код классов, связанных контрактом</a> - если для метода класса задано предусловие, то в начало метода добавляется вызов предусловия; если задано постусловие, то в конце метода производится его вызов и т.д. Библиотека гибко настраивается - проверку контрактов можно отключить целиком или для отдельных методов, в случае нарушения контракта можно генерировать исключения или ограничиться записью в лог файл и т.д. Последнее обновление: 2013 год.
<pre class="brush:java">
@ContractReference(TimeOfDayContract.class)
public class TimeOfDay {
...
public void setHour(int hour) {
this.hour = hour;
}
}
public class TimeOfDayContract extends TimeOfDay {
...
@Override public void setHour(int hour) {
if (preCondition()) {
assert hour >= HOUR_MIN : "hour >= HOUR_MIN"; // precondition clause 1
assert hour <= HOUR_MAX : "hour <= HOUR_MAX"; // precondition clause 2
}
if (postCondition()) {
...
}
}
</pre>
</li>
<li><a href="http://code.google.com/p/cofoja/">Cofoja (contracts for java)</a>. Библиотека, разрабатываемая сотрудниками Google в рамках <a href="http://habrahabr.ru/post/190362/">ныне отмененного</a> правила использования <a href="http://googleblog.blogspot.com/2006/05/googles-20-percent-time-in-action.html">20% свободного времени</a> на разработку сторонних проектов. Контракты задаются через аннотации. Плагина для Eclipse нет, но прикрутить библиотеку к Eclipse можно: <a href="http://webcourse.cs.technion.ac.il/236700/Spring2013/ho/WCFiles/Contracts%20for%20Java.pdf">Contracts for Java (Cofoja) V1.0 in Eclipse (PDF)</a> (у меня к 64-битному Eclipse 4.3 cofoja не прикрутилась, а к 32-битному - без проблем).
<pre class="brush:java">
@Requires("n >= 0")
@Ensures({
"!result || old (n) % 2 == 1",
/* For testing purposes: check that old () has the right value. */
"old (n) == n"
})
public static boolean odd(int n) {
if (n == 0) {
return false;
} else {
return even(n - 1);
}
</pre>
</li>
<li><a href="http://www.eecs.ucf.edu/~leavens/JML//index.shtml">Java Modeling Language (JML).</a> Контракты описываются на специальном языке в комментариях к методам.
<pre class="brush:java">
//@ requires (* x is positive *);
/*@ ensures (* \result is an approximation to
@ the square root of x *)
@ && \result >= 0;
@*/
public static double sqrt(double x) {
return Math.sqrt(x);
}
</pre>
</li>
</ul>
<p/>
А теперь, плохие новости. К сожалению, практически все эти библиотеки не могут быть использованы под Android из-за <a href="http://stackoverflow.com/questions/230193/what-can-you-not-do-on-the-dalvik-vm-androids-vm-that-you-can-in-sun-vm">ограничений Dalvik VM</a>. А именно, отпадают все библиотеки, использующие Java Bytecode Instrumentation - Modern Jass, Contract4J, jContractor, C4J, cofoja... В Android технология Bytecode Instrumentation <a href="http://stackoverflow.com/questions/19522058/how-to-specify-javaagent-for-android">реализована через Instrumentation classes</a>, на что DBC-библиотеки попросту не рассчитаны. Печально, но мне не удалось найти ни одной библиотеки для реализации DBC в Android-приложении.
<h2>Выводы.</h2>
Библиотек, позволяющих использовать методологию Design By Contracts в Android приложениях, найти не получилось. Так что полноценной удобной поддержки DBC в Android, похоже, обеспечить пока не удастся. Тем не менее, кое-какие DBC-инструменты для Android-разработчиков все же доступны. Перечислю их еще раз:
<ul>
<li>Библиотека <a href="http://code.google.com/p/guava-libraries/wiki/PreconditionsExplained">Preconditions</a> из Guava позволяет явно и компактно задавать проверку предусловий.</li>
<li>Для проверки постусловий методов можно использовать assert'ы. Поскольку стандартные assert'ы java на девайсах и эмуляторе по умолчанию отключены, их следует не забывать <a href="http://stackoverflow.com/questions/2364910/can-i-use-assert-on-android-devices">включать</a>. Или же использовать <a href="http://developer.android.com/reference/junit/framework/Assert.html">JUnit Assert</a> + <a href="http://square.github.io/fest-android/">FEST Android</a> и <a href="http://stackoverflow.com/questions/5043681/better-way-to-do-debug-only-assert-code">исключать их из релиза с помощью ProGuard</a>.
<li>Аннотации и @NonNull, @Nullable из JSR-305 позволяют явно указать код, в котором не могут или, наоборот, могут присутствовать null-значения. С этими аннотациями умеют работать многие бесплатные и платные статические анализаторы кода, в частности, <a href="http://findbugs.sourceforge.net/">FindBugs</a> и <a href="http://code.google.com/p/checker-framework/">Checker Framework</a>.</li>
<li>Если вы используете Eclipse, то имеет смысл последовать совету разработчиков библиотеки <a href="http://code.google.com/p/google-guice/wiki/UseNullable">Google Guice</a> и считать, что по умолчанию никакие параметры и переменные не могут принимать null-значение, а методы - не могут null возвращать. Достигается это за счет использования аннотации
<a href="http://help.eclipse.org/juno/index.jsp?topic=%2Forg.eclipse.jdt.doc.isv%2Freference%2Fapi%2Forg%2Feclipse%2Fjdt%2Fannotation%2FNonNullByDefault.html">org.eclipse.jdt.annotation.NonNullByDefault</a>, с помощью которой следует <a href="http://stackoverflow.com/questions/8405336/how-do-i-use-or-edit-package-info-java">пометить</a> все пакеты в вашем проекте. В результате, необходимость проставлять @NotNull отпадет, потребуется лишь помечать @Nullable те места в коде, где могут присутствовать null-значения.
</li>
<li>Следует использовать наиболее свежую версию Eclipse 4.3 (Keppler), т.к. в ней существенно доработаны средства null analyse. По умолчанию они выключены, их следует включить в настройках компилятора Java.</li>
</ul>
<br />
<b>Update:</b>В контексте null-анализа стоит так же упомянуть библиотеку <a href="http://code.google.com/p/guava-libraries/wiki/UsingAndAvoidingNullExplained#Optional">Guava.Optional</a>, с помощью которой можно явно помечать объекты, которые могут принимать null-значения. Полезнейшая штука.
<h2>Mindmap для этой статьи</h2>
<a href="http://ru.wikipedia.org/wiki/Диаграмма_связей">Mindmap</a>, созданный в процессе работы над этой статьей, можно взять <a href="https://www.dropbox.com/s/7feah1rqyfn7ovu/201401_mindmap_design_by_contract.mm">здесь</a>, в формате <a href="http://freemind.sourceforge.net/">Freemind 1.0</a> (портативная версия: <a href="http://sourceforge.net/projects/freemind/files/freemind/1.0.0/freemind-bin-1.0.0.zip/download">freemind-bin-1.0.0.zip</a>).
<br /></div>Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com0tag:blogger.com,1999:blog-3333619357391010212.post-46858580624136571692013-08-29T18:49:00.002+08:002013-08-29T18:50:03.892+08:00Релиз Named Folders 4.x для FAR 3 (Beta) <div dir="ltr" style="text-align: left;" trbidi="on">
Портировал плагин Named Folders под FAR 3.
<ul>
<li><a href="http://code.google.com/p/namedfolders/downloads/detail?name=NamedFolders_x86_b284_beta1.7z&can=2&q=">Named Folders 4.0 build 284 для FAR 3 x86"</a></li>
<li><a href="http://code.google.com/p/namedfolders/downloads/detail?name=NamedFolders_x64_b284_beta1.7z&can=2&q=">Named Folders 4.0 build 284 для FAR 3 x64</a></li>
</ul>
Версионность у плагина получилась такой:
<ul>
<li>2.x - FAR 1.X</li>
<li>3.x - FAR 2.X</li>
<li>4.x - FAR 3.x</li>
</ul>
<br/>
Изменений в код пришлось внести много - API сильно поменялось. Ключевое изменение новой версии - все шоткаты теперь хранятся в sqlite, а не в реестре. В остальном функциональность осталась практически прежней.
<br/><br/>
Сообщения о багах приветствуются.
<br /></div>
Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com3tag:blogger.com,1999:blog-3333619357391010212.post-7562041257287254752012-12-20T10:31:00.002+08:002012-12-20T10:39:44.077+08:00Автоинкремент версии в QT / С++ приложениях<div dir="ltr" style="text-align: left;" trbidi="on">
Задачу инкремента номера версии при сборке релиза QT-шного проекта, естественно, хочется автоматизировать. Но есть несколько тонкостей.
<a name='more'></a>
В своих проектах я использую два варианта именования версий:
<ul>
<li>Классический: 1.0.0.BUILD_NUMBER</li>
<li>Дата релиза: 2012.12.20.BUILD_NUMBER</li>
</ul>
В коммерческих приложениях, где релизы достаточно редки, удобнее первый формат. В корпоративных приложениях, где "релизы" могут собираться каждый день и файл "что нового" никто не пишет - предпочтителен второй формат.
</br></br>
Номер билда может генерироваться двумя способами:
<ul>
<li>Последовательный инкремент: 1,2,3,4...</li>
<li>Произвольная последовательность чисел: N1, N2, N3.. важно, чтобы N1 < N2 < N3. Сам по себе номер билда не важен, главное чтобы он отличался у разных релизов.</li>
</ul>
</br></br>
Для удобства, написал крохотную утилиту <a href="http://code.google.com/p/dvsrc/source/browse/trunk/CSharp/FlexBuildIncrementer">FlexBuildIncrementer</a>, которая позволяет задавать/инкрементировать номер версии во всех перечисленных вариантах.
</br></br>
Вызывается утилита следующим образом:
<pre>
FlexBuildIncrementer <path> [mode]
</pre>
Здесь path - путь к файлу с номером версии. Если файла нет, он будет автоматически создан по этому пути; если есть - номер версии будет изменен согласно заданным правилам. Файл содержит одну единственную строку вида:
<pre>
#define APPLICATION_VERSION "1.0.0.0" //[inc] inc, sec70, date, date_sec70
</pre>
Правила инкремента версии задаются параметром [mode]. Поддерживаются четыре варианта:
<ul>
<li>inc: 1.0.0.1 -> 1.0.0.2 </li>
<li>sec70: 1.0.0.1 -> 1.0.0.COUNT_SECONDS_SINCE_1970</li>
<li>date: 1.0.0.1 -> CURRENT_YEAR.CURRENT_MONTH.CURRENT_DAY.2</li>
<li>date_sec70: 1.0.0.1 -> CURRENT_YEAR.CURRENT_MONTH.CURRENT_DAY.COUNT_SECONDS_SINCE_1970</li>
</ul>
Пример результатов работы утилиты:
<pre>
FlexBuildIncrementer.exe version
#define APPLICATION_VERSION "1.0.0.1" //[inc] inc, sec70, date, date_sec70
FlexBuildIncrementer.exe version
#define APPLICATION_VERSION "1.0.0.2" //[inc] inc, sec70, date, date_sec70
FlexBuildIncrementer.exe version inc
#define APPLICATION_VERSION "1.0.0.3" //[inc] inc, sec70, date, date_sec70
FlexBuildIncrementer.exe version date
#define APPLICATION_VERSION "2012.12.20.4" //[inc] inc, sec70, date, date_sec70
FlexBuildIncrementer.exe version sec70
#define APPLICATION_VERSION "2012.12.20.1355969884" //[inc] inc, sec70, date, date_sec70
FlexBuildIncrementer.exe version sec70
#define APPLICATION_VERSION "1.0.0.1355969831" //[inc] inc, sec70, date, date_sec70
FlexBuildIncrementer.exe version date_sec70
#define APPLICATION_VERSION "2012.12.20.1355969899" //[inc] inc, sec70, date, date_sec70
</pre>
Можно задать режим генерации версии прямо в файле version и НЕ передавать второй параметр командной строки. Пример:
<pre>
#define APPLICATION_VERSION "1.0.0.1" //[sec70]
FlexBuildIncrementer.exe version
#define APPLICATION_VERSION "1.0.0.1355970013" //[sec70]
</pre>
Перед сборкой релиза я вызываю утилиту (через bat-файл) - и в проекте автоматически выставляется нужный номер версии. В принципе, можно автоматизировать вызов bat-файла при сборке релиза (см. топик на <a href="http://stackoverflow.com/questions/1417061/automatic-increment-of-build-number-in-qt-creator">stackoverflow</a>), но лично мне ручной режим удобнее.
</br></br>
<a href="http://code.google.com/p/dvsrc/source/browse/trunk/CSharp/FlexBuildIncrementer/">Исходные коды FlexBuildIncrementer</a>
</br>
Скомпилированная версия FlexBuildIncrementer: <a href="http://code.google.com/p/dvsrc/downloads/detail?name=20121220_FlexBuildIncrementer.exe.7z">FlexBuildIncrementer.7z</a>
</div>
Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com0tag:blogger.com,1999:blog-3333619357391010212.post-52562980729873119792012-11-23T13:33:00.000+08:002014-06-14T11:18:11.851+08:00Получение доходов с Google Play на расчетный счет ИП<div dir="ltr" style="text-align: left;" trbidi="on">
В октябре 2012 <a href="http://droidblog.merigotech.com/?p=70">вступили в силу новые правила получения платежей от продаж на Google Play</a> и, одновременно, <a href="http://droidblog.merigotech.com/?p=73">новая инструкция ЦБ РФ О порядке предоставления документов, связанных с проведением валютных операций</a> (Инструкция Банка России от 04.06.2012 № 138-И - <a href="http://www.cbr.ru/analytics/standart_acts/currency_regulations/138-i.pdf">PDF</a>). В результате, платежи с Google Checkout теперь можно получать напрямую на расчетный счет индивидуального предпринимателя. Это позволяет избавиться от <a href="http://aldro.ru/internet-i-biznes/29-nalogi-adsens.html">работы с AdSense</a> и связанных с ней <a href="http://forum.searchengines.ru/archive/index.php/t-646949.html">морем вопросов</a>.
</br></br>
Многие разработчики уже <a href="http://habrahabr.ru/post/151831/">опробовали</a> новую схему. Попробовал и я - на днях пришел первый платеж. Хочу поделиться подробностями организации работы с банком.
<a name='more'></a>
<h2>Расчетный счет</h2>
Для получения платежей требуется долларовый расчетный счет ИП. Процесс открытия валютного счета я уже <a href="http://derevyanko.blogspot.ru/2010/09/1.html">подробно описывал</a>. При открытии счета вам откроют два счета: текущий и транзитный.
</br></br>
В настройка Google Checkout в разделе Settings\Financials нужно будет указать информацию о банке
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEingg-aeUi8I7NxgD2Ep6RkGAoQQKT1InL7JoYCsuGZ39-7IPF3_Enr506KaE_agetKev212XnliprAB-hseaCziUPIZCUQcH26F4AYGTIFQPB6azz2qyFJqDIOVHqUSHjUFDuLAVsLG7iD/s1600/checkout.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="260" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEingg-aeUi8I7NxgD2Ep6RkGAoQQKT1InL7JoYCsuGZ39-7IPF3_Enr506KaE_agetKev212XnliprAB-hseaCziUPIZCUQcH26F4AYGTIFQPB6azz2qyFJqDIOVHqUSHjUFDuLAVsLG7iD/s320/checkout.jpg" /></a></div>
На примере <a href="http://www.sibenergocom.ru/contact">реквизитов Сибирского филиала Промсвязьбанка</a>:
<pre>
Сибирский филиал ОАО «Промсвязьбанк» г. Новосибирск
Рс/сч (20-значное число)
Кор/сч 30101810500000000816
БИК 045004816
Intermediary Bank (Банк – Посредник): Deutsche Bank Trust Company Americas
New York, NY, USA
SWIFT: BKTR US 33
Account with Institution (Банк Бенефициара): ОJSC "Promsvyazbank"
SIBIRSKY BRANCH,
SWIFT: PRMSRUMMNSB
</pre>
Здесь BIK = 045004816 (это БИК вашего банка). Account number = номер вашего <b>транзитного</b> счета. SWIFT = PRMSRUMMNSB. Bank name = ОJSC "Promsvyazbank". Account holder name = ваши фамилия и имя.
</br></br>
<h2>Документы в банк</h2>
Получение денег на валютный расчетный счет связано с обязательной процедурой валютного контроля. Принципиально вас ожидает то же самое, что при <a href="http://derevyanko.blogspot.ru/2010/09/blog-post.html">получении денег от иностранного заказчика</a>. При поступлении платежа требуется:
<ol>
<li>Представить в банк договор с Google</li>
<li>Создать справку о валютной операции и перевести деньги с транзитного счета на текущий валютный или сразу же с продать валюту с транзитного валютного счета на текущий рублевый.</li>
</ol>
Банк требует предоставить оригинал и перевод соглашения <a href="https://checkout.google.com/termsOfService?type=SELLER">Accept payments through Google - Terms of Service. Google Checkout - Terms of Service (https://checkout.google.com/termsOfService?type=SELLER)</a> (для просмотра требуется войти в свой аккаунт Android-разработчика). С оригиналом - проблем нет. А вот перевода на русский в сети мне найти не удалось. Пришлось переводить самому...
</br></br>
<s>
<a href="http://code.google.com/p/dvsrc/downloads/detail?name=20120919v1_%D0%9F%D0%B5%D1%80%D0%B5%D0%B2%D0%BE%D0%B4%20Accept%20payments%20through%20Google%20-%20Terms%20of%20Service.7z&can=2&q=">Выкладываю перевод</a> в свободный доступ. (Качество перевода, прямо говоря, не очень - уж слишком много там юридической терминологии... Буду признателен, если кто-нибудь укажет на официальный или более качественный перевод. Если кто-нибудь возьмется править перевод - могу предоставить доступ к оригинальному документу в google docs, - параграфы на английском и русском чередуются. Пишите мне на dvpublic0 на гмейле).
</s> (Ниже в комментариях дали <a href="https://dl.dropbox.com/u/884087/Google%20Checkout%20Terms%20of%20Service.zip">ссылку на профессиональный перевод</a> <b>Update:</b>Более актуальная версия перевода раздается здесь: <a href="http://habrahabr.ru/post/223847/">http://habrahabr.ru/post/223847</a> )
</br></br>
Судя по <a href="http://habrahabr.ru/post/151831/">комментариям на Хабре</a>, в некоторых банках могут требовать и другие документы, например, скришнот Payouts.
</br></br>
Если у вас есть все необходимые документы, то дальше проблем особых нет. Создаете справку о валютной операции и заявление о переводе/продаже валюты с транзитного счета. Здесь главное уложиться в 15 дней с момента поступления платежа. Штрафы за опоздание могут быть <a href="http://habrahabr.ru/blogs/Dura_Lex/114069/">ОЧЕНЬ</a> высокими. Лично мне Google перевел первый платеж чуть ли не в тот же день, как я вбил реквизиты банка в Google Checkout (а вовсе не в конце месяца). В итоге я чуть было этот платеж не пропустил.
<h2>Паспорт сделки</h2>
Если совокупная сумма платежей по договору превысит $50 000, то потребуется, как мне сказали в банке, "открыть паспорт сделки или подписать новый договор". Сразу открывать паспорт сделки не требуется. <b>Update:</b> В комментариях пишут, что в некоторых банках ПС требуют открывать сразу, так что уточняйте в своем банке. Дело тут в коде валютной операции. Если договор рассматривается как агентский, то код валютной операции нужно указывать 21500 и паспорт сделки обязательно открывать ДО первого платежа. В противном случае можно использовать код 20200 и паспорт сделки открывать уже после достижении лимита $50 000.
<br/><br/>
<BIG><b>Нерешенные вопросы (обсуждение - в комментариях)</b></BIG>
<ul>
<li>Какой код валютной операции использовать? (варианты: 20200, 21500)</li>
<li>Следует ли платить НДС с агентского вознаграждения в 30%, взымаемого Google?</li>
</ul>
<br/><br/>
<b>Update</b>: В валютном отделе промсвязьбанка мне сказали следующее:
<ul>
<li>При заключении агентского договора на каждую операцию потребуется оформлять в три раза больше документов - нужны будут подтверждающие документы на выплаченные агентские проценты + придется составлять какие-то дополнительные акты. Паспорт сделки для агентского договора необходимо оформлять сразу, до первого платежа.</li>
<li>В договоре с Google они не видят признаков агентского договора. Это договор офферты, но никак не агентский договор. Вот если бы я получал от Google 100% стоимости и выплачивал им обратно причитающиеся 30%, тогда другое дело. А так можно указывать работать с КВО 20200.</li>
</ul>
<br />
<b>Update</b>: Ниже в комментариях пишут, что в другом отделение промсвяьбанка рекомендуют использовать код 35030.
<br />
<b>Update</b>: Привожу ответ, полученный из отдела валютного контроля: "Нам разъяснений из ЦБ не поступало по поводу таких договоров, как у вас, поэтому в разных банках или даже разных филиалах нашего банка валютные контролеры могут интерпретировать договора по-своему, что в свою очередь никак не отражается на клиенте, поскольку ответственность за правильность проставления кода вида валютной операции целиком лежит на валютном контролере."
</p>
<b>Update, август 2013: полезная статья на хабре</b> <a href="http://habrahabr.ru/post/191280/">Google Play -- работаем легально! / Хабрахабр</a>
</p>
<b>Update, май 2014: еще одна полезная статья на хабре</b> <a href="http://habrahabr.ru/post/222537/">О правомерности работы физического лица с магазином App Store в РФ </a>
</p>
В тему: <a href="http://habrahabr.ru/post/225657/">Как легализовать доход от Google AdSense для ИП</a>
</p>
</div>Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com58tag:blogger.com,1999:blog-3333619357391010212.post-27825485569335250702012-11-13T16:53:00.001+08:002012-11-13T16:53:27.474+08:00Ошибка "unexpected at this time" при установке boost 1.52<div dir="ltr" style="text-align: left;" trbidi="on">
Взялся устанавливать свежий boost 1.52. При попытке собрать bjam и b2 командой
"...boost_1_52_0\tools\build\v2\engine\build.bat"
вылетела ошибка
<pre>
C:\>D:\libs\boost_1_52_0\tools\build\v2\engine\build.bat
\Git\bin";"C:\utils\PuTTY";C:\Program Files (x86)\MiKTeX 2.9\miktex\
bin\;C:\Program Files (x86)\Git\cmd;C:\Program Files\TortoiseSVN\bin" was unexpected at this time.
</pre>
Что за "was unexpected at this time"?
<br/><br/>Решение нашлось <a href="http://www.blinnov.com/en/2010/06/04/microsoft-was-unexpected-at-this-time/">здесь</a>. Проблема была в том, что некоторые пути, перечисленные в переменной среды %PATH%, содержали кавычки. При подстановке таких путей в "vcvars32.bat" местами возникало двоековычье :) и батник вылетал. Убрал кавычки во всех путях в %PATH% - boost нормально установился.
<br /></div>
Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com0tag:blogger.com,1999:blog-3333619357391010212.post-27339948663764916572012-11-13T10:26:00.000+08:002012-11-13T11:01:59.673+08:00Вызов функций Android из Unity3d<a href="http://unity3d.com/">Unity3D</a> - мощная система разработки 3D приложений, - поддерживает Android. <a href="http://docs.unity3d.com/Documentation/Manual/PluginsForAndroid.html">Документация</a>, к сожалению, очень скудная, да и сам процесс разработки - не сахар... Тем не менее, возможность создать Android-приложение есть.
<br/><br/>
В целом процесс разработки выглядит так. Вы создаете Android-проект, экспортируете его в jar-файл. Далее, помещаете этот jar файл в директорию проекта Unity: Assets\Plugins\Android\xxx.jar. В эту же директорию кладете AndroidManifest.xml, jar-файлы сторонних библиотек, которые задействованы в вашем проекте и ресурсы (папка res с той же структурой, что в обычном Android-проекте).
<br/><br/>
Следующий шаг - вызов функций, реализованных на стороне Android, из Unity. Вот на этом вопросе я и хочу остановиться поподробнее, т.к. здесь не все тривиально и есть подводные камни.
<a name='more'></a>
<h2>Тестовый проект</h2>
Для удобства работы я создал тестовый проект <a href="http://code.google.com/p/dvsrc/source/browse/trunk/Android/Unity2AndroidTest/">Unity2AndroidTest</a> (который, кстати, может служить в примером реализации Android-плагина для Unity3D). В него входят:
<ul>
<li>u2aTest - Android-проект - плагин для Unity; содержит тестовые активити MyActivity и класс MyClass. В MyActivity и MyClass реализован набор функций-заглушек, которые ничего не делают, только фиксируют в логе факт их вызова. Функций реализовано множество разных: с разным набором параметров, статические и нестатические.</li>
<li>unity - маленький проект на Unity, реализующий приложение с одной кнопкой. Нажимаешь на кнопку - происходит вызов функций u2aTest множеством различных способов.</li>
</ul>
<h2>Перекрытие Activity</h2>
Начнем с задачи перекрытия Activity. Unity внутри себя способна реализовать "главную" Activity. Но мы можем подменить ее собственной с нужным функционалом. Делается это так:
<pre class="brush:java">
//file: MyMainActivity.java
package com.example.u2aTest;
import android.os.Bundle;
import com.unity3d.player.UnityPlayerActivity;
public class MyMainActivity extends UnityPlayerActivity {
@Override protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
Kernel.logInfo("MyMainActivity.onCreate");
}
}
</pre>
<pre class="brush:xml">
//file: Assets\Plugins\Android\Android\AndroidManifest.xml
<application android:icon="@drawable/app_icon" android:label="@string/app_name">
<activity android:name=".MyMainActivity" android:label="@string/app_name" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</pre>
Здесь хочу обратить внимание на то, что у нас ДВА файла AndroidManifest.xml. Первый находится в Android-проекте - он нас совершенно не интересует. Второй - в Assets\Plugins\Android\Android. Вот во втором и следует объявлять активити, сервисы, permissions и т.д. - имеено он используется Unity при сборке приложения. Функция Kernel.logInfo - это обертка над функцией записи сообщения в лог, она сводится к простейшему коду: <code>Log.i(TAG_LOG, message);</code>
</br></br>
Итак, теперь у нас есть Activity. Если все сделано правильно, то при запуске Unity-проекта на девайсе мы увидим в логе строчку "MyMainActivity.onCreate" - наша активити запустилась. Теперь добавим в Activity две функции:
<pre class="brush:java">
//file: MyMainActivity.java
package com.example.u2aTest;
import android.os.Bundle;
import com.unity3d.player.UnityPlayerActivity;
public class MyMainActivity extends UnityPlayerActivity {
@Override protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
Kernel.logInfo("MyMainActivity.onCreate");
}
public void testVoid() {
Kernel.logInfo("MyMainActivity.testVoid");
}
public static void testVoidStatic() {
Kernel.logInfo("MyMainActivity.testVoidStatic");
}
}
</pre>
<h2>Обращение к функциям Current Activity</h2>
Как вызвать функции testVoid и testVoidStatic на стороне Unity? Для начала нам потребуются объекты <a href="http://docs.unity3d.com/Documentation/ScriptReference/AndroidJavaClass.html">AndroidJavaClass</a> и <a href="http://docs.unity3d.com/Documentation/ScriptReference/AndroidJavaObject.html">AndroidJavaObject</a>
<br/><br/>
<pre class="brush:csharp">
public class Caller {
private readonly AndroidJavaClass _ActivityClass;
private readonly AndroidJavaObject _ActivityObject;
private readonly AndroidJavaClass _MyActivityClass;
public Caller() {
_ActivityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
_MyActivityClass = new AndroidJavaClass("com.example.u2aTest.MyMainActivity");
_ActivityObject = _ActivityClass.GetStatic<AndroidJavaObject>("currentActivity");
// AndroidJavaObject my_activity_object = _MyActivityClass.GetStatic<AndroidJavaObject>("currentActivity"); //A0: не работает
}
}
</pre>
Мы создаем два объекта типа AndroidJavaClass:
<ul>
<li>_ActivityClass - для доступа к свойствам класса "com.unity3d.player.UnityPlayer"</li>
<li>_MyActivityClass - для доступа к свойствам класса "com.example.u2aTest.MyMainActivity"</li>
</ul>
Далее, через ActivityClass мы получаем доступ к полю currentActivity, которая указывает на экземпляр MyMainActivity, создаваемый при запуске приложения. Любопытно, что через MyActivityClass у нас доступа к currentActivity нет.
<br/><br/>
Теперь переходим к вызову функций testVoid и testVoidStatic:
<pre class="brush:csharp">
public class Caller {
//...................
public void MakeTestCalls() {
_ActivityObject.Call("testVoid"); // А1
//_ActivityClass.CallStatic("testVoidStatic"); //А2: не работает
_MyActivityClass.CallStatic("testVoidStatic"); //А3
//_ActivityObject.CallStatic("testVoid"); //А4: работает, но не на всех девайсах
}
}
</pre>
Через объект _ActivityObject мы спокойно можем вызывать функции MyMainActivity. Статические функции MyMainActivity доступны только через _MyActivityClass, через _ActivityClass доступа к ним нет. Вообщем все закономерно: доступ к статическим функциям через AndroidJavaClass, доступ к нестатическим функциям - через AndroidJavaObject. Вот только _ActivityObject (почему-то) позволяет вызывать и статические функции тоже, хотя работает это не на всех девайсах. По-моему - это косяк реализации, не должна такая функциональность работать. Тем не менее, на форумах такой код встречается.
<h2>Вызов функций с параметрами</h2>
Функции testVoid и testVoidStatic ничего не возвращают и не принимают параметры. Рассмотрим более интересные варианты:
<pre class="brush:java">
//file: MyMainActivity.java
package com.example.u2aTest;
import android.os.Bundle;
import com.unity3d.player.UnityPlayerActivity;
public class MyMainActivity extends UnityPlayerActivity {
public String testStringString(String paramValue) {
Kernel.logInfo("MyMainActivity.testStringString:" + paramValue);
return paramValue;
}
public int testInt(int intValue) {
Kernel.logInfo("MyMainActivity.testInt:" + intValue);
return intValue;
}
public static String testStringStringStatic(String paramValue) {
Kernel.logInfo("MyMainActivity.testStringStringStatic:" + paramValue);
return paramValue;
}
public static int testIntStatic(int intValue) {
Kernel.logInfo("MyMainActivity.testIntStatic:" + intValue);
return intValue;
}
}
</pre>
<pre class="brush:csharp">
public class Caller {
//...................
public void MakeTestCalls2() {
int iresult = _ActivityObject.Call<int>("testInt", 999);
String sresult = _ActivityObject.Call<String>("testStringString", "s2");
iresult = _MyActivityClass.CallStatic<int>("testIntStatic", 999);
sresult =_MyActivityClass.CallStatic<String>("testStringStringStatic", "s1");
}
}
</pre>
Все работает. Особых отличий тут нет. Важно только не забывать использовать generic-варианты функций Call и CallStatic, а так же соблюдать <a href="http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/types.html#wp9502">соответствие типов C# и Java</a>.
<h2>Подводные камни Thread</h2>
А что, если попробовать вызвать Android-функции из потока, созданного на стороне Unity? Пробуем:
<pre class="brush:csharp">
public class Caller {
//...................
public void MakeTestCallsFromThread() {
System.Threading.Thread t = new System.Threading.Thread(this.thread_proc);
t.Start();
t.Join();
}
private void thread_proc() {
_ActivityObject.Call("testVoid");
_MyActivityClass.CallStatic("testVoidStatic");
int iresult = _ActivityObject.Call<int>("testInt", 999);
String sresult = _ActivityObject.Call<String>("testStringString", "s2");
iresult = _MyActivityClass.CallStatic<int>("testIntStatic", 999);
sresult =_MyActivityClass.CallStatic<String>("testStringStringStatic", "s1");
}
}
</pre>
... и вот здесь нас ждем много интересных и неординарных ошибок. Вот некоторые из них:
<pre>
W/System.err(4850): java.lang.NoSuchMethodError: no static method with name='testVoid' signature='()V' in class Lcom/unity3d/player/UnityPlayer;
W/System.err(4850): at com.unity3d.player.UnityPlayer.nativeRender(Native Method)
W/System.err(4850): at com.unity3d.player.UnityPlayer.onDrawFrame(Unknown Source)
W/System.err(4850): at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1470)
W/System.err(4850): at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1224)
W/System.err(4850): java.lang.NoClassDefFoundError: com/unity3d/player/ReflectionHelper
W/System.err(4850): at dalvik.system.NativeStart.run(Native Method)
W/System.err(4850): Caused by: java.lang.ClassNotFoundException: com.unity3d.player.ReflectionHelper
W/System.err(4850): at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:61)
W/System.err(4850): at java.lang.ClassLoader.loadClass(ClassLoader.java:501)
W/System.err(4850): at java.lang.ClassLoader.loadClass(ClassLoader.java:461)
I/Unity(3092): (Filename: /Applications/buildAgent/work/14194e8ce88cdf47/Runtime/ExportGenerated/AndroidManaged/UnityEngineDebug.cpp Line: 43)
I/com.example.u2aTest(3092): MyMainActivity.testVoid
E/dalvikvm(3092): JNI ERROR (app bug): accessed stale local reference 0x1d200001 (index 0 in a table of size 0)
E/dalvikvm(3092): VM abortin </pre>
Последняя <a href="http://android-developers.blogspot.ru/2011/11/jni-local-reference-changes-in-ics.html">ошибка</a> приводит к полному краху приложения. Печально, но факт - из потока функции Android не вызвать.
</br></br>
<h2>Выводы</h2>
Вызов функций Android из Unity не составляет особых проблем, если проводить вызов из основного потока. Из дополнительных потоков функции Android НЕ ВЫЗЫВАЮТСЯ. Подозреваю, что это особенность реализации Unity или фишка JNI. Я, к сожалению, не знал о такой особенности и потратил кучу времени, чтобы выяснить в чем дело. Надеюсь, кому-нибудь эта статья сэкономит время. Удачи.
</br></br>
<a href="http://code.google.com/p/dvsrc/downloads/detail?name=20121113_Unity2AndroidTest.7z">Скачать исходные коды примера Android-плагина для Unity3D: 20121113_Unity2AndroidTest.7z</a>
Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com10tag:blogger.com,1999:blog-3333619357391010212.post-89260605491430014192012-08-12T21:08:00.001+08:002012-08-13T09:32:36.145+08:00Квест: как под Android выдрать кадр из видеофайла средствами FFMpeg и передать его на сторону Java.<div dir="ltr" style="text-align: left;" trbidi="on">
Программирование под Android очень часто напоминает настоящий квест. Шаг влево, шаг вправо - и ты попадаешь на минное поле, полное багов, особенностей реализации, девайсо-зависимых проблем и т.п. И так происходит <b>каждый</b> раз, как только начинаешь реализовывать что-нибудь более менее нетривиальное. Вот свежий пример. Потребовалось мне извлекать кадры из видеофайла и показывать их на экране. Казалось бы - что может быть проще?
</br></br>
<a name='more'></a>
<h2>Почему MediaMetadataRetriever не годится</h2>
В Android есть для этого необходимый класс - <a href="http://developer.android.com/reference/android/media/MediaMetadataRetriever.html">MediaMetadataRetriever</a>, в котором предусмотрена функция - <a href="http://developer.android.com/reference/android/media/MediaMetadataRetriever.html#getFrameAtTime(long, int)">getFrameAtTime</a>.
</br></br>
Первая проблема - MediaMetadataRetriever есть только начиная с API 10. Вторая проблема более серьезная - функция getFrameAtTime не работает как минимум под Android 2.X. Вместо того, чтобы возвращать требуемый фрейм, она <a href="http://stackoverflow.com/questions/10188938/how-to-get-the-frame-from-video-file-in-android">всегда возвращает <b>один и тот же фрейм</b></a>. Причем это не баг, это фича! В документации <a href="http://developer.android.com/reference/android/media/MediaMetadataRetriever.html#getFrameAtTime(long, int)">прямо написано</a>: <blockquote>This method finds a representative frame close to the given time position by considering the given option <b>if possible</b>, and returns it as a bitmap. Returns: A Bitmap containing a representative video frame, which can be null, if such a frame cannot be retrieved.</blockquote>
</br>
Итак, MediaMetadataRetriever нам не подходит. Что можно использовать? Выбор невелик - библиотеку <a href="http://ffmpeg.org/">FFMpeg</a>. Добро пожаловать в мир NDK...</br></br>
<h2>Компиляция FFMpeg под Android</h2>
Компиляция FFMpeg под Android - задача непростая. К счастью, все таки решаемая. <a href="http://dmitrydzz-hobby.blogspot.com/2012/04/ffmpeg-android.html">Вот здесь</a> подробно описано как можно скомпилировать FFMpeg (огромное спасибо автору). Компиляцию, правда, приходится проводить под Ubuntu, но после компиляции скомпилированные библиотеки можно спокойно использовать под Windows (вот <a href="http://code.google.com/p/dvsrc/downloads/detail?name=20120812ffmpeg-compiled.7z">мои результаты</a> компиляции FFMpeg под Android для arm6).
</br></br>
<h2>Извлечение кадра в битмапку средствами FFMpeg</h2>
Нативное API для извлечения кадра из видеофайла может быть, например, следующим.
<h3>На стороне Java</h3><pre class="brush:java">public class FFMpegWrapper {
static {
System.loadLibrary("sblib");
initialize();
}
private static native int initialize();
public static native long openFile(String fileName);
public static native int getFrameBufferSize(long handleFile, int format, int width, int height);
public static native int getFrame(long handleFile, long timeUS, int width, int height, java.nio.Buffer buffer);</pre></br>
<h3>На стороне JNI</h3>
<pre class="brush:cpp">#include <jni.h>
#include <android/log.h>
#include "libavutil/pixfmt.h"
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "cmdutils.h"
#define LOG_TAG "com.domain.tag"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
const int ERROR_OPEN_FILE = -1;
const int ERROR_FIND_VIDEOSTREAM = -2;
const int ERROR_FIND_VIDEODECODER = -3;
const int ERROR_OPEN_VIDEODECODER = -4;
const int ERROR_NO_STREAM_INFO = -5;
typedef long thandle_file;
// Handle для файла - полная информация об открытом файле
struct thandle {
AVFormatContext* ctx;
AVCodecContext* codecCtx;
int videoStream;
AVPacket* packet;
} tHandle;
//отправляем в JAVA исключение с кодом errorCode
void make_exception(JNIEnv *env, int errorCode) {
LOGI("exception %d", errorCode);
char buffer[32];
sprintf(buffer, "%d", errorCode);
(*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/Exception"), buffer);
}
//инициализация
jint Java_com_domain_applicationname_FFMpegWrapper_initialize(JNIEnv *env) {
av_register_all();
LOGI("initialize_passed");
return 0;
}
//определить размер кадра в формате Bitmap.Config.ARGB_8888
jint Java_com_domain_applicationname_FFMpegWrapper_getFrameBufferSize(JNIEnv *env, jobject thiz, thandle_file handleFile, jint width, jint height);
//открыть файл, получить handle
jint Java_com_domain_applicationname_FFMpegWrapper_openFile(JNIEnv *env, jobject thiz, jstring fileName) {
AVFormatContext* ctx = 0;
const char *filename = (*env)->GetStringUTFChars(env, fileName, 0);
if(av_open_input_file(&ctx, filename, NULL, 0, NULL) != 0) {
make_exception(env, ERROR_OPEN_FILE); //"Не удалось открыть видеофайл
}
// Retrieve stream information
if(av_find_stream_info(ctx)< 0) {
make_exception(env, ERROR_NO_STREAM_INFO); //"Не удалось открыть видеофайл; // Couldn't find stream information
}
int videoStream = -1;
for (int i = 0; i < ctx->nb_streams; ++i) {
if (ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStream = i;
break;
}
}
if (videoStream == -1) make_exception(env, ERROR_FIND_VIDEOSTREAM);
AVCodecContext* codecCtx = ctx->streams[videoStream]->codec;
AVCodec* codec = avcodec_find_decoder(codecCtx->codec_id);
if (codec == 0) {
make_exception(env, ERROR_FIND_VIDEODECODER);
}
if (avcodec_open2(codecCtx, codec, 0) != 0) {
make_exception(env, ERROR_OPEN_VIDEODECODER);
}
AVPacket* packet = (AVPacket*)malloc(sizeof(struct AVPacket));
av_init_packet(packet);
struct thandle* h = malloc(sizeof(struct thandle));
h->ctx = ctx;
h->codecCtx = codecCtx;
h->videoStream = videoStream;
h->packet = packet;
//release java string
(*env)->ReleaseStringUTFChars(env, fileName, filename);
return (unsigned long)h;
}
//выдрать из открытого файла фрейм в формате Bitmap.Config.ARGB_8888
//в заданных размерах
//и сохранить битмапку в переданный Java объект java.nio.ByteBuffer
//время timeUS задается в микросекундах.
jint Java_com_domain_applicationname_FFMpegWrapper_getFrame(JNIEnv *env, jobject thiz, thandle_file handleFile, jlong timeUS, jint width, jint height, jobject buffer) {
//Функция для выдирания заданного фрейма из открытого файла
AVFormatContext* ctx = ((struct thandle*)handleFile)->ctx;
AVCodecContext* codecCtx = ((struct thandle*)handleFile)->codecCtx;
AVPacket* packet = ((struct thandle*)handleFile)->packet;
int videoStream = ((struct thandle*)handleFile)->videoStream;
jshort* buff = (jshort*) (*env)->GetDirectBufferAddress(env, shortBuffer);
AVFrame* frame = avcodec_alloc_frame(); //YUV frame
avcodec_get_frame_defaults(frame);
//AV_TIME_BASE * time_in_seconds = avcodec_timestamp
//см. http://dranger.com/ffmpeg/tutorial07.html
int64_t pos = frameNumber * AV_TIME_BASE / 1000000;
int64_t seek_target= av_rescale_q(pos, AV_TIME_BASE_Q, ctx->streams[videoStream]->time_base);
//выполняем seek - попадаем на ближайший кейфрейм
//затем ищем нужный кадр.
int res = avformat_seek_file(ctx
, videoStream
, INT64_MIN
, seek_target//* AV_TIME_BASE
, INT64_MAX
, 0);
LOGI("seek: %d f=%ld pos=%lld st=%lld", res, frameNumber, (int64_t)pos, seek_target);
if (res >= 0) {
avcodec_flush_buffers(codecCtx);
LOGI("flushed");
}
av_init_packet(packet);
AVFrame* frameRGB = avcodec_alloc_frame();
avcodec_get_frame_defaults(frameRGB);
enum PixelFormat pixel_format = PIX_FMT_RGBA;
avpicture_fill((AVPicture*) frameRGB
, (uint8_t*)buff
, pixel_format
, width
, height
);
while (av_read_frame(ctx, packet) == 0) {
LOGI("pts=%lld st=%lld", packet->pts, seek_target);
if (packet->stream_index == videoStream) {
int gotPicture = 0;
int bytesDecompressed = avcodec_decode_video2(codecCtx, frame, &gotPicture, packet);
if (gotPicture && packet->pts >= seek_target) {
// конвертируем данные из формата YUV в RGB24
struct SwsContext* scaleCtx = sws_getContext(frame->width,
frame->height,
(enum PixelFormat)frame->format
, width //frame->width,
, height //frame->height,
, pixel_format
, SWS_BICUBIC
, 0, 0, 0);
int height = sws_scale(scaleCtx
, frame->data
, frame->linesize
, 0
, frame->height
, frameRGB->data
, frameRGB->linesize);
break;
}
av_free_packet(packet);
}
}
av_free(frameRGB);
av_free(frame);
return 0;
}
</pre></br><h2>Передача кадра из JNI в Java, используя allocateDirect</h2>
Итак, для получения кадра нам нужно передать в функцию getFrame буфер - объект типа <a href="http://developer.android.com/reference/java/nio/ByteBuffer.html">java.nio.ByteBuffer</a>. Как выделить память в буфере?
</br></br>
Первое, что приходит в голову - воспользоваться функцией <a href="http://developer.android.com/reference/java/nio/ByteBuffer.html#allocateDirect(int)">allocateDirect</a>. Собственно, она для этих целей и предназначена. Получаем:
<pre class="brush:java">
//На стороне JNI:
jshort* buff = (jshort*) (*env)->GetDirectBufferAddress(env, shortBuffer);
... //используем buff в функциях FFMpeg
//На стороне Java:
ByteBuffer my_buffer = ByteBuffer.allocateDirect(bufferSize).asShortBuffer();
FFMpegWrapper.getFrame(handle, timeUS, width, height, my_buffer);
Bitmap dest = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
dest.copyPixelsFromBuffer(buffer);
//Ура, мы получили в dest битмапку c извлеченным кадром :)</pre>
</br>Все просто.. но таким образом очень легко получить <b>Out Of Memory</b>. Причин, как минимум, две:<ul><li>Функция allocateDirect выделяет буфер в нативной памяти, тогда как сам Java-объект ByteBuffer занимает минимум места в управляемой памяти. Предположим, вы выделили с помощью allocateDirect память размером 10 Мб. Как только буфер стал ненужен, память нужно освободить. Но такой возможности у нас нет - предполагается, что сборщик мусора сам освободит эту память, когда придет время. Сборщик же не торопится - с его точки зрения ByteBuffer занимает не 10 Мб, а несколько байт, чего торопиться его удалять? Детально проблема описана вот <a href="http://stackoverflow.com/questions/1744533/jna-bytebuffer-not-getting-freed-and-causing-c-heap-to-run-out-of-memory">здесь</a>. На практике несколько последовательных allocateDirect по 10 Мб легко дают Out Of Memory.</li><li>Под Android 3.X функция allocateDirect работает некорректно - она <a href="http://code.google.com/p/android/issues/detail?id=16941">выделяет в 4 раза больше памяти, чем запрашиваешь.</a></li></ul>
<h2>Альтернативный вариант выделения памяти под буфер</h2>
Исправить ситуацию можно, выделяя и освобождая память самостоятельно, не используя ByteBuffer.allocateDirect. Например, как это описано <a href="http://stackoverflow.com/questions/5060307/bytebuffer-not-releasing-memory">здесь</a>. <h3>Функции на стороне JNI</h3>
<pre class="brush:cpp">
jobject Java_com_domain_applicationname_selector_FFMpegWrapper_allocNative(JNIEnv* env, jobject thiz, jlong size)
{
void* buffer = malloc(size);
jobject directBuffer = (*env)->NewDirectByteBuffer(env, buffer, size);
jobject globalRef = (*env)->NewGlobalRef(env, directBuffer);
return globalRef;
}
void Java_com_domain_applicationname_FFMpegWrapper_freeNative(JNIEnv* env, jobject thiz, jobject globalRef)
{
void *buffer = (*env)->GetDirectBufferAddress(env, globalRef);
free(buffer);
(*env)->DeleteGlobalRef(env, globalRef);
}
</pre><h3>На стороне Java</h3>
<pre class="brush:java">public class FFMpegWrapper {
.........
/** Выделить нативный буфер заданного размера*/
public static native ByteBuffer allocNative(long bufferSize);
/** Выделить нативный буфер заданного размера*/
public static native void freeNative(ByteBuffer buffer);
}
ByteBuffer my_buffer = FFMpegWrapper.allocNative(bufferSize).asShortBuffer();
FFMpegWrapper.getFrame(handle, timeUS, width, height, my_buffer);
Bitmap dest = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
dest.copyPixelsFromBuffer(buffer);
//Ура, мы получили в dest битмапку c извлеченным кадром :)
...
FFMpegWrapper.freeNative(my_buffer);</pre><h2>Выводы</h2>
А выводы очень просты :D Когда планируешь разработку приложения под Android (а тем более, пишешь ТЗ для заказчики с оценкой временных затрат), следует учитывать, что разработка под Android - это вот такой квест. И что простейшая задача, которая казалось бы решается за 15 минут, может вылиться в несколько недель тяжелой работы...
<br /></div>Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com0tag:blogger.com,1999:blog-3333619357391010212.post-68082061450459763592012-03-14T19:36:00.000+08:002012-03-14T19:43:35.690+08:00Идентификаторы Android-контакта: ContactID и LookupKey. Нюансы и баги.Согласно <a href="http://developer.android.com/resources/articles/contacts.html">документации</a>, контакт в Android характеризуется двумя идентификаторами: contactId и lookupKey. Предположим, мы создаем приложение, работающее с определенным контактом. Пример - мое приложение Animated Widget Contact Launcher, которое позволяет создавать для контактов виджеты быстрого доступа. Какой идентификатор контакта нужно хранить в настройках такого виджета - contactId или lookupKey? Или оба? Как правильно создавать ссылку на контакт? Практика показала, что вопрос не тривиален.
<a name='more'></a>
<br/><br/><h2>Идентификаторы контакта</h2>
Итак, у контакта есть два идентификатора. ContactID представляет из себя обычное число, типа long: 2, 40, 3222 и т.д. LookupKey - это кодированная строка типа "1157icbbec86124b1b50", "30410abc...gmail.com" и т.д.
<br/><br/>Если вам известен хотя бы один идентификатор, то вы можете получить URI контакта и, через URI, запросить любую информацию о контакте. Вот как это делается:
<pre class="brush:java">
//Вариант 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();
}
</pre>
О том, как правильно использовать Contact URI (и Contact API в целом) можно почитать, например, <a href="http://www.vtgroup.com/#ContactsContract">здесь</a> и <a href="http://www.higherpass.com/Android/Tutorials/Working-With-Android-Contacts/4/">здесь</a>.
<br/><br/>Первые два варианта подходят, когда вам известен один из идентификаторов. Третий вариант - когда известны оба (в 2.1 он <a href="http://stackoverflow.com/questions/3754217/android-manage-contacts-with-lookup-key">не работает</a>). Вопрос - зачем нам вариант три, когда первых двух вроде бы достаточно? Вариант 3 нужен потому, что идентификаторы контакта могут изменяться с течением времени.
<br/><br/><h2>Непостоянство contactId и lookupKey</h2>
ContactId может измениться при агрегации контактов. Скажем, есть у вас в контактах пользователь Вася, contactId = 1. Вы установили на свой телефон skype. В скайпе Вася у вас тоже есть, на телефоне появляется контакт Вася(2) с contactId = 100. Андроид автоматически объединяет эти контакты в общий, агрегированный контакт Вася(3) с contactId = 200. Если после агрегации вы попробуете найти контакт Вася(1) с contactId = 1, то вы его не найдете. Контакт потерялся.
<br/><br/>Чтобы избежать такой потери контактов, разработчики Android и ввели lookupkey. Если вы вместе с contactId=1 сохранили второй идентификатор контакта lookupkey="abc", то используя вариант поиска номер 3, вы без проблем найдете агрегированный контакт Васи:
<pre class="brush:java">
Uri uri = getLookupUri(1, "abc");
</pre>
Здесь есть тонкий момент. Идентификатор lookupKey может изменяться. Как правило он изменяется после редактирования свойств контакта. Так что в SQL-запросах к Contact API никогда нельзя включать явные выражения типа " and (LOOKUP_KEY='abc')" - будет работать, но до поры до времени. Lookup Key нужно передавать в getLookupUri, получать Uri, а дальше работать c Uri и contactId, не используя более lookupKey.
<br/><br/>Если lookupKey изменился - можно ли будет по нему найти контакт? Практика показывает, что можно - устаревшие lookupKey работают корректно.
<br/><br/><h2>Как ссылаться на контакт?</h2>
Мы подходим к вопросу - если требуется сохранить ссылку на контакт, какой идентификатор сохранять? Одного contactId однозначно не хватает. Можно ли обойтись одним lookupKey?
<br/><br/>В документации написано буквально следующее:
<br/><i>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...
<br/><br/>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</i>.
<br/><br/>Другими словами, для однозначной идентификации контакта достаточно хранить один лишь lookupKey. На самом деле, это не так.
<br/><br/><h2>Неоднозначность LookupKey</h2>
Мое приложение Animated Contact Widget предназначено для быстрого доступа к контактам. Естественно, ему приходится хранить ссылки на контакты. Вплоть до текущей версии в информации о контакте хранился единственный идентификатор - lookupKey. В 99% случаев все работало без проблем. Но время от времени приходили единичные письма от пользователей, которые сообщали о странном баге - выбираешь один контакт, а виджет создается для другого...
<br/><br/>В чем дело я не мог понять очень долго. На stackoverflow <a href="http://stackoverflow.com/questions/6698458/android-strange-thing-about-loading-contact-info/8305928#8305928">упоминался</a> аналогичный баг, но дельного ответа на него никто не дал.
<br/><br/>Наконец нашелся пользователь, который не поленился и помог мне отыскать причину проблемы (за что ему огромнейшее спасибо). Отладочные логи показали, что на девайсе пользователя примерно треть контактов имеет ОДИНАКОВЫЕ lookupKey. Что же удивляться, что контакт выбирается не тот... Вот фрагмент лог файла:
<code>
<br/>k=1957i5 c=152
<br/>k=1957i5 c=153
<br/>k=1957i46d39cd0743de699 c=154
<br/>k=1957i327657105acc3975 c=155
<br/>k=1957i5 c=394
<br/>k=1957i300808f8df4189b1 c=156
<br/>k=1957i660d8a742c21a4a6 c=411
<br/>k=1957i3793876d5c49591e c=157
<br/>k=1957i5 c=158
<br/>k=1957i250bebb5bd1009fd c=170
<br/>k=1957i39112193df972581 c=171
<br/>k=1957i5 c=431
<br/>k=1957i2c74309cd7539f2b c=169
</code>
<br/>Здесь c - contactId, k - lookupKey.
<br/><br/>Трудно судить, насколько часто такая проблема проявляется. Сообщений о баге я получил с десяток. Плюс, наверное, было десятка два комментариев на маркете. Из полмиллиона закачек - это мизер. Тем не менее, сообщения об этой ошибке приходили стабильно, от пользователей с разными устройствами. Проблема точно проявлялась на некоторых HTC-девайсах (Desire, Wildfire S).
<br/><br/>Мы попробовали выявить общий знаменатель между "бракованными" контактами. Не получилось. Есть подозрение, что шалит синхронизация HTC Sense - Outlook, но не факт, что дело в ней...
<br/><br/>Начиная с версии 1.5.5, Animated Widget использует два идентификатора контакта для ссылки на контакт. Баг пофиксился.
<br/><br/><h2>Еще бывает Factory Reset</h2>
Factory Reset - сброс к заводским настройкам. Сброс приводит к тому, что у контактов меняются оба идентификатора - и contactId и lookupKey. После заводского сброса контакт по ранее сохраненным идентификаторам найти не получится. Нужно искать его по косвенной информации - телефону, ФИО и т.д. Если она сохранена.
<br/><br/><h2>Выводы</h2>
Вывод прост: если вам нужно сохранить информацию о контакте так, чтобы в дальнейшем этот контакт можно было гарантированно найти, сохраняйте оба идентификатора контакта - lookupKey и contactId. Одного единственного lookupKey не достаточно, несмотря на то, что написано в SDK. Ну а если контакт нужно суметь найти даже после заводского сброса, необходимо хранить так же дополнительную информацию о контакте: телефоны, ФИО и т.д.Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com2tag:blogger.com,1999:blog-3333619357391010212.post-67478843109459479642012-02-01T15:04:00.000+08:002013-04-23T13:57:50.631+08:00Тестирование Android приложенийТестирование - тема очень многообразная. Разновидностей тестов масса, методик тестирования тоже, количество разнообразных инструментов просто огромно. Начинающему разобраться не просто. Но за что бороться есть, поскольку грамотная стратегия тестирования существенно улучшает качество приложения.
<br/><br/>Данная статья - это каталогизированный набор ссылок по теме тестирования Android приложений, с краткими аннотациями. Я создал его для того, чтобы самому было удобнее разбираться во всем этом материале и выбирать оптимальную стратегию тестирования собственных Android приложений. Надеюсь, этот набор ссылок пригодится и вам.
<a name='more'></a>
<br/><br/><h2>С чего начать</h2>
В июне 2011 вышла книга, посвященная тестированию Android-приложений: <a href="http://dtmilano.blogspot.com/2011/06/android-application-testing-guide.html">Diego Torres Milano. Android Application Testing Guide</a>. Книга добротная, написана хорошо. <a href="http://dtmilano.blogspot.com/">Блог автора</a> так же содержит массу полезной информации, например - вот презентацию <a href="http://assets.en.oreilly.com/1/event/80/Introduction%20to%20Android%20Testing%20Presentation.pdf">Introduction to Android Testing @ OSCON 2012 (PDF)</a>.
<br/><br/>Официальная документация по встроенным средствам тестирования <a href="http://developer.android.com/guide/developing/testing/index.html">Android Developers Guide. Testing</a> (<a href="http://anddev.ru/33/osnovyi-testirovaniya-android-prilozheniy.html">перевод</a>).
Из недостатков: Android testing API основано на стиле JUnit 3 и не поддерживает JUnit 4. Тесты запускаются и работают на эмуляторе/девайсе, т.е. запуск медленный.
<br/><br/><a href="http://habrahabr.ru/blogs/android_development/113584/">Тестирование Android приложений</a> - пример разработки небольшого Android-приложения с тестом на базе стандартного фреймворка android.test.
<br/><br/><a href="http://stackoverflow.com/questions/522312/best-practices-for-unit-testing-android-apps">Best practices for unit testing Android apps</a> - обсуждение инструментов тестирования на stackoverflow.
<br/><br/><a href="http://www.gubatron.com/blog/2010/05/02/how-to-do-unit-testing-on-android-with-eclipse/">How to do Unit Testing on Android with Eclipse</a> - видео, демонстрирующее создание тестового проекта в Eclipse.
<br/><br/><a href="http://habrahabr.ru/blogs/testing/123026/">Автоматизированное тестирование мобильных приложений</a> - обзор инструментов для тестирования интерфейса мобильных приложений.
<br/><br/><a href="http://automated-testing.info/news/anons-serii-kursov-avtomatizacija-mobilnyh-prilozhenij">Серия курсов - Автоматизация мобильных приложений</a> (в настоящее время курсов там мало, но анонс многообещающий).
<br /><br /><a href="http://habrahabr.ru/blogs/android_development/131446/">Шаблоны проектирования при разработке под Android. Часть 2 — MVP и Unit tests. Путь Джедая</a>,
<a href="http://habrahabr.ru/blogs/android_development/131652/">Шаблоны проектирования при разработке под Android. Часть 3 — Пользовательский интерфейс, тестирование, AndroidMock</a> - пара статей по теме тестирования на хабре. Содержание местами спорное, но комментарии, как всегда, интересные.
</br><br/><a href="http://www.slideshare.net/dtmilano/testing-on-android">Testing on android</a> - весьма интересная презентация с полезными ссылками.
</br><br/>Серия обзорных статей, посвященных тестированию мобильных приложений: <a href="http://blogs.globallogic.com/mobile-application-testing">Mobile Application Testing - Part I</a>, <a href="http://blogs.globallogic.com/mobile-application-testing-part-ii">part II</a>, <a href="http://blogs.globallogic.com/mobile-application-testing-part-iii">part III</a>.
<br/><br/><h2>Проблема скорости запуска тестов</h2>
При разработке под Android очень не удобно использовать короткие unit-тесты (<a href="https://sites.google.com/site/androiddevtesting/">раз статья</a>, <a href="http://daverog.wordpress.com/2009/12/14/why-android-isnt-ready-for-tdd-and-how-i-tried-anyway/">два статья</a>). Дело в том, что стандартные тесты работают только на эмуляторе или на устройстве (т.к. зависят от Android API). Запуск тестов, в этом случае, становится достаточно длительным.
<br/><br/>Для ускорения работы можно выделять Android-независимые тесты в отдельный Java проект и запускать их на JVM компьютера, но это, конечно же, не решение проблемы. Вот дискуссия на stackoverflow: <a href="http://stackoverflow.com/questions/522312/best-practices-for-unit-testing-android-apps">Best practices for unit testing Android apps</a>. Основные варианты: использовать библиотеки <a href="http://pivotal.github.com/robolectric/">Robolectric</a> и <a href="http://code.google.com/p/robotium/">Robotium</a> .
<br/><br/>Библиотека Robolectric действительно решает проблему скорости запуска теста. Тесты запускаются не на эмуляторе/устройстве, а прямо на JVM компьютера, что на порядок быстрее. Robolectric позволяет тестировать большую часть функциональности Android, включая layouts, GUI, сервисы, работу с сетью, виджеты. К тому же, Robolectric использует синтаксис junit4. В то же время следует отдавать себе отчет в том, что Roboelectric эмулирует Android API. Точность и полнота такой эмуляции, естественно, не 100%.
<br/><br/><a href="http://brainflush.wordpress.com/2010/01/10/introducing-calculon-a-java-dsl-for-android-activity-testing/">Introducing Calculon – a Java DSL for Android Activity testing</a> - библиотека для удобного тестирования Activity.
<br/><br/><h2>Mock-библиотеки под Android</h2>
Про то, что такое mock-объекты и как их использовать, есть статьи на хабре: <a href="http://habrahabr.ru/blogs/java/136466/">JMock и EasyMock: сравнение и howto в примерах и не только</a>, <a href="http://habrahabr.ru/blogs/java/72617/">Глоток МоКито</a>. Mock-библиотек, работающих под Android, существует несколько: <a href="http://easymock.org/">EasyMonkey</a>, <a href="http://code.google.com/p/powermock/">PowerMock</a>, <a href="http://code.google.com/p/android-mock/">Android Mock</a>, <a href="http://code.google.com/p/mockito/">Mockito</a>.
<br/><br/><h2>Автоматизация тестирования. Многократное воспроизведение записанного теста.</h2>
<a href="http://developer.android.com/guide/developing/tools/monkeyrunner_concepts.html">MonkeyRunner</a> - инструмент, входящий в состав Android SDK. С помощью MonkeyRunner можно писать программы на Питоне, способные установить Android-приложение, запустить его, послать ему последовательность нажатий клавиш, сделать и сохранить результирующий скриншот экрана.
<br/><br/><a href="http://code.google.com/p/robotium/">Robotium</a> - фреймворк, дающий возможность разрабатывать тесты "черного ящика" для Android приложений. Тесты пишутся на Java. Для тестирования создается стандартный тестовый проект, в который добавляется библиотека Robotium. Тестовый проект можно запускать как на эмуляторе, так и на девайсе. Robotium использует синтаксис JUnit3. Порядок установки и создания тестового проекта на Robotium подробно описан в <a href="http://code.google.com/p/robotium/wiki/RobotiumTutorials">Robotium Tutorials</a>. Полезная статья: <a href="http://automated-testing.info/knowledgebase/article/nastrojka-sredy-dlja-razrabotki-android-prilozhenij-i-avtomatizacii-na-robotiu">Настройка среды для разработки Android приложений и автоматизации на Robotium</a>.
<br/><br/><a href="http://www.gorillalogic.com/fonemonkey4android">FoneMonkey for Android</a> - бесплатный open source инструмент для тестирования rich interface, разработанный компанией Gorilla Logic (см. <a href="http://jaxenter.com/fonemonkey-for-android-q-a-39195.html">интервью с разработчиком FoneMonkey for Android</a>, а так же статью в <a href="http://drdobbs.com/open-source/231901614">DrDobbs</a>, посвященную FoneMonkey for IOS). Программа умеет записывать высокоуровневые action-based test automation скрипты на Java/Java Script, которые можно редактировать и (при необходимости) писать вручную.
<br/><br/><a href="http://sikuli.org/">Sikuli</a> - еще один бесплатный инструмент для автоматизации тестирования GUI. Особенность - скрипт, задающий последовательность действий, позволяет использовать скриншоты. Чтобы дать команду нажать кнопку, достаточно подставить в скрипт скриншот этой кнопки (используется специальная Sikuli IDE). <a href="http://www.youtube.com/v/FxDOlhysFcM&fs=1&rel=0&hd=1&iframe=true&width=640&height=505">Видео на youtube</a> наглядно демонстрирует процесс создания скрипта. А вот пример видео, где sikuli "играет" в <a href="http://sikuli.org/blog/2011/08/15/sikuli-plays-angry-birds-on-google-games/">Angry birds</a>. Вот <a href="http://i-miss-erin.blogspot.com/2010/01/automated-test-in-android-by-sikuli.html">пример</a> использования sikuli для тестирования Android GUI. Преимущества и недостатки использования Sikuli для тестирования мобильных приложений приведены в <a href="http://www.siliconindia.com/events/siliconindia_events/Softec_Conf_Pune/Vinod_Doshi.pdf">презентации</a>.
<br/><br/><a href="http://www.t-plan.com/robot/">T-PLAN ROBOT (VNCRobot)</a> - универсальный инструмент для тестирования "черных ящиков" (<a href="http://www.t-plan.com/tplan_robot.html">видео</a>, <a href="http://www.t-plan.com/resources/t-planproductinfo/T-Plan_Robot_Mobile_Product_Information.pdf">mobile testing brochure (pdf)</a>). Есть <a href="http://www.t-plan.com/robot/docs/versions.html">платная и бесплатная open-source версия</a>.
<br/><br/>На самом деле, инструментов, предназначенных для автоматизации тестирования приложений, очень много. Из платных отмечу: <a href="http://www.testplant.com/products/eggplant/">Eggplants</a>,<a href="http://www.bsquare.com/android.aspx">Test Quest</a>, <a href="http://www.zap-fix.com/">ZPX</a>. Есть еще <a href="http://www.jamosolutions.com/Pages/Platforms/Android.html">Jamo Solutions: M-eux test</a> и <a href="www.experitest.com/download">SeeTest</a> специально заточенные под мобильные приложения.
<br/><br/><h2>Облачные сервисы</h2>
При разработке Android-приложений важнейшая проблема - протестировать работу приложения на различных устройствах. Возможных вариантов устройств сотни, все себе не купишь. А платформо-железозависимые баги, к сожалению, совсем не редкость.
<br/><br/>Специальные сервисы предоставляют доступ к стендам, содержащим множество разнообразных устройств, и позволяют провести тестирование на всех этих устройствах разом. Таких сервисов как минимум три: <a href="https://www.perfectomobile.com/portal/cms/services/automated_testing.html">Perfecto Mobile</a>, <a href="http://www.deviceanywhere.com/">Device Anywhere</a> и <a href="http://testdroid.com/">TestDroid</a>. Вот <a href="http://mobiforge.com/testing/story/testing-physical-devices-made-easy">здесь</a> описан принцип работы Device Anywhere и приведены скриншоты. Услуга удобная, но достаточно дорогая.
<br/><br/>TestDroid, кстати, позволяет записывать тесты в формате Robotium.
<b>Update</b>: <a href="http://www.manymo.com/">Online emulator</a> для множества различных Android-девайсов. В настоящий момент - бесплатен.
<br/><br/><h2>Сервисы для бета-тестирования</h2>
<a href="http://www.utest.com/">uTest</a> - сообщество из 45 тыс профессиональных тестеров из 180 стран. Реальные пользователи протестируют работу вашего приложения. Платный.
<br/><br/><a href="http://thebetafamily.com/">The Beta Family</a> - бесплатный сервис для тестирования приложения. Заводите аккаунт, заливаете бета-версию приложения, рассылаете приглашение на тестирование, обрабатываете результаты тестирования. На главной странице сайта написано, что сервис предназначен для тестирования iPhone/iPad/iPod приложений. Но Android так же поддерживается, о чем прямо написано в <a href="http://thebetafamily.com/faq/for-beta-testers#which-mobile-platforms-do-you-support-for-beta-testing">FAQ</a>. Можно выбрать тип бета-тестеров: private или public. Если public, то ваше приложение смогут тестировать все желающие.
<br/><br/><a href="http://www.appaloosa-store.com/">Appaloosa</a> - сервис для приватного распространения приложения (например, среди доверенных бета-тестеров или среди работников вашей компании). Заводите аккаунт, даете доступ вашим знакомым бета-тестерам. После авторизации бета-тестеры получают возможность скачивать приложения, которые вы выложили на Appaloosa. Бесплатен на стадии бета тестирования, потом станет платным.
<br/><br/><a href="http://hockeykit.net/">Hockey Kit</a> - еще один подобный сервис для распространения бета-версий среди своих бета-тестеров.
<br/><br/><a href="https://zubhium.com/">Zubhium</a>. Предоставляет SDK, с помощью которого вы в свое приложение встраиваете код для автоматического сбора информации об ошибках. Выкладываете бету. Ее тестируют в настоящий момент на стадии беты и поэтому - бесплатен. Бета тестеров нужно приглашать своих. Полезная <a href="http://zubhium.posterous.com/android-app-beta-testing">презентация на тему Zibhum</a>.
<br/><br/><a href="https://www.push-link.com/">PushLink</a>. Еще один подобный сервис для приватного распространения приложений (например, бета версий). Добавляете в приложение немного кода для работы с PushLink. Собираете apk и отдаете бета-тестеру. Через некоторое время собираете новую версию apk и заливаете на PushLink. Пользователь автоматически получает уведомление о новой версии и скачивает ее с PushLink. Удобно - нет необходимости рассылать новые версии по email. Сервис бесплатен.
<br/><br/><a href="http://developer.motorola.com/testing/motoready/">MOTOREADY App Testing: MOTOROLA XOOM</a> - платный сервис Motorola для тестирования Android-приложений.
<br/><br/><a href="https://sites.google.com/site/approveproject/home">Approve</a> - Android-приложение, призванное упростить процесс распространения apk-файлов среди тестеров. Фактически, обертка над Gmail. Вы посылаете тестеру письмо и прикладываете APK-файл на тестирование. На стороне тестера Approve получает письмо и берет на себя установку приложения, сбор логов, отправку разработчику отчета об ошибках и т.п.
<br/><br/><h2>Monkey и т.д.</h2>
<a href="http://habrahabr.ru/blogs/android_development/131637/">Инструменты функционального тестирования — Monkey и MonkeyRunner</a> - обзор двух стандартных инструментов: Monkey (стресс-тестирование) и MonkeyRunner("прокликивающие" тесты, сценарии тестов пишутся на Python).
<br/><br/><a href="http://dtmilano.blogspot.com/2011/11/android-using-monkey-from-java.html">Android: Using monkey from Java</a> - статья в блоге Diego Torres Milano, посвященная стандартной библиотеке chimpchat, эквиваленту monkeyrunner.
<br/><br/><h2>Code coverage</h2>
Code coverage инструменты определяют <a href="http://en.wikipedia.org/wiki/Code_coverage">степень покрытия кода тестами</a>.
<br/><br/>Варианты для Android: <a href="http://emma.sourceforge.net/">EMMA</a>, <a href="http://stackoverflow.com/questions/7887870/how-to-get-code-coverage-in-android-using-maven-android-maven-plugin">Robotium</a>, <a href="http://mojo.codehaus.org/sonar-maven-plugin/">Sonar</a>, включающий Maven и <a href="cobertura.sourceforge.net/">Cobertura</a>. Так же функциональость code coverage есть в бесплатном <a href="http://derevyanko.blogspot.com/2012/01/android.html">статическом анализаторе</a> от Google - <a href="http://code.google.com/javadevtools/codepro/doc/features/codecoverage/code_coverage.html">CodePro AnalytiX</a>
<br/><br/><h2>Сбор информации от пользователей</h2>
<a href="http://code.google.com/p/acra/">Библиотека ACRA</a> - пишет креш-репорты в документ Google Docs.
<br/><br/><a href="http://www.bugsense.com/">BugSense</a> - real-time bug tracking service.
<br/><br/><a href="http://derevyanko.blogspot.com/2011/07/android.html">Системы сбора статистики</a> по работе приложения.
<br/><br/><a href="http://code.google.com/p/android-log-collector/">Log collector</a> - приложение для генерирования log-файла и отправки его по email. Удобно использовать в своих приложениях для реализации возможности отправки лога.
<br/><br/><h2>Другие инструменты</h2>
<a href="http://code.google.com/p/vogar/wiki/TestHistory">Vogar</a> - запуск большого количества тестов с записью истории выполнения тестов.
<br/><br/><a href="http://code.google.com/p/caliper/">Caliper</a> - open-source фреймворк для создания микробенчмарков, их запуска и просмотра результатов.
<br/><br/><a href="https://github.com/jsankey/android-junit-report">Android Junit Report</a> - замена стандартному InstrumentationTestRunner с возможностью генерации XML-отчетов.
<br/><br/><h2>Тестировщику на заметку</h2>
Интересная <a href="http://www.slideshare.net/qaclubkiev/mobile-testing-android-ios-blackberry">презентация</a>.
<br/><br/>Чек-лист для тестирования Android приложения: <a href="http://www.unifiedtestinginitiative.org/Android-UTC">http://www.unifiedtestinginitiative.org/Android-UTC</a>.
<br/><br/><a href="http://unifiedtestinginitiative.org/files/uti_best_practices_v1_final.pdf">Best Practice Guidelines for developing quality mobile applications</a>
<br/><br/><a href="http://www.mutualmobile.com/wp-content/uploads/2011/04/ADG1.1.pdf">Android design guidelines</a>
<br/><br/><a href="http://developer.motorola.com/testing/motoready/test-criteria/">MOTOREADY Test Criteria</a> и <a href="http://developer.motorola.com/testing/motoready/test-criteria/files/MOTOREADY_Public_Test_Cases_v3_5_4-Final.pdf/">MOTOREADY Public Test Cases (PDF)</a> - критерии тестирования приложений, предназначенных для устройств Motorolla. Без привязки к операционной системе.
<br/><br/><h2>Особенности окружающей среды</h2>
<a href="http://www.mobileappstesting.com/2011/09/06/how-to-test-the-3g-or-wi-fi-connection-speed-on-iphone-and-android-smartphones/">How to test the 3G or Wi-Fi Connection speed on Iphone and Android Smartphones?</a>
<br/><br/><a href="http://habrahabr.ru/blogs/android/136154/">Тестирование поведения приложения в условиях нехватки памяти</a>
<br/><br/><a href="http://wanem.sourceforge.net/">Wide Area Network Emulator</a> - позволяет разработчику эмулировать проблемы в сети - Network delay, Packet loss, Packet corruption, Disconnections, Packet re-ordering, Jitter и т.д.
<br/><br/><h2>Методологии</h2>
Test Driven Development, TDD: <a href="http://ru.wikipedia.org/wiki/Разработка_через_тестирование">Разработка через тестирование</a>.
<br/><br/>Behavior Driven Development, BDD:: <a href="http://behaviour-driven.org">http://behaviour-driven.org</a>, фреймверк для BDD: <a href="http://jbehave.org">JBehave</a>
<br/><br/>Fitness: <a href="www.fitnesse.org">www.fitnesse.org</a>, <a href="http://fit.c2.com">http://fit.c2.com</a> - совместная работа клиентов, программистов и тестировщиков. <a href="http://code.google.com/p/givwenzen/downloads/list">GivWenZen</a> - запись сценариев тестов простым английским языком.
<br/></br><h2>Update - еще полезные ссылки</h2>
<a href="http://habrahabr.ru/company/intel/blog/152122/">Автоматизация тестирования Android приложений (сентябрь 2012)</a> - статья на хабре, посвященная бесплатным средствам реализации автоматического тестирования.
<br/>
<a href="http://www.enterra.ru/blog/tools_for_qa/">Инструменты тестирования приложений для мобильных устройств: обзор вариантов и возможностей</a> - хорошая ссылка из комментариев.Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com3tag:blogger.com,1999:blog-3333619357391010212.post-88275496063406671052012-01-29T12:39:00.000+08:002012-01-29T12:40:11.459+08:00Список языков с переводом названий (ISO 639-1).Занимаюсь локализацией своего Android приложения <a href="https://market.android.com/details?id=com.mobilityflow.awidget&hl=en">Animated Widget Contact</a>. Потребовалось найти список языков, поддерживаемых Android, с переводом названий. Т.е. в виде Ru-Russian-Русский, da-Danish-Dansk и т.д. Как ни странно, готовый список найти не удалось. Поэтому составил такой список самостоятельно, взяв за основу следующие источники: <a href="http://codex.wordpress.org/WordPress_in_Your_Language#Korean_-_.ED.95.9C.EA.B5.AD.EC.96.B4_-_.28ko_KR.29">1</a>, <a href="http://api.drupal.ru/api/function/_locale_get_predefined_list/6">2</a>, <a href="http://unicode.org/cldr/version/1.3.html">3</a>, <a href="http://www.omegawiki.org/Meta:Main_Page/rus">4</a>. В список включил только языки имеющие двухбуквенный код по стандарту ISO 639-1, т.к. именно они актуальны для Android приложений. Вот что в итоге получилось.
<a name='more'></a>
<br/><table border="1">
<tr><th width="10%">ISO 639-1</th><th width="50%">Название на английском</th><th>Перевод</th></tr>
<tr><td>aa</td><td>Afar</td> <TD>Qafar (Afaraf)</TD> </tr>
<tr><td>ab</td><td>Abkhazian</td> <TD>аҧсуа бызшәа</TD> </tr>
<tr><td>ae</td><td>Avestan</td> <TD></TD> </tr>
<tr><td>af</td><td>Afrikaans</td> <TD>Afrikaans</TD></tr>
<tr><td>ak</td><td>Akan</td> <TD></TD> </tr>
<tr><td>am</td><td>Amharic</td> <TD>አማርኛ</TD> </tr>
<tr><td>an</td><td>Aragonese</td> <TD>Aragonés</TD> </tr>
<tr><td>ar</td><td>Arabic</td> <TD>عربي</TD> </tr>
<tr><td>as</td><td>Assamese</td> <TD>অসমীয়া</TD> </tr>
<tr><td>av</td><td>Avaric</td> <TD>Авар</TD> </tr>
<tr><td>ay</td><td>Aymara</td> <TD>Aymar aru</TD> </tr>
<tr><td>az</td><td>Azerbaijani</td> <TD>azərbaycan</TD> </tr>
<tr><td>ba</td><td>Bashkir</td> <TD>Башҡортса</TD> </tr>
<tr><td>be</td><td>Belarusian</td> <TD>Беларуская - Biełaruskaja</TD> </tr>
<tr><td>bg</td><td>Bulgarian</td> <TD>Български</TD> </tr>
<tr><td>bh</td><td>Bihari languages</td> <TD>भोजपुरी</TD> </tr>
<tr><td>bi</td><td>Bislama</td> <TD>Bislama</TD> </tr>
<tr><td>bm</td><td>Bambara</td> <TD>Bamanankan</TD> </tr>
<tr><td>bn</td><td>Bengali</td> <TD>বাংলা</TD> </tr>
<tr><td>bo</td><td>Tibetan</td> <TD>བོད་ཡིག</TD> </tr>
<tr><td>br</td><td>Breton</td> <TD>Brezhoneg</TD> </tr>
<tr><td>bs</td><td>Bosnian</td> <TD>Bosanski</TD> </tr>
<tr><td>ca</td><td>Catalan; Valencian</td> <TD>Català</TD> </tr>
<tr><td>ce</td><td>Chechen</td> <TD>Нохчийн</TD> </tr>
<tr><td>ch</td><td>Chamorro</td> <TD>Chamoru</TD> </tr>
<tr><td>co</td><td>Corsican</td> <TD>Corsu</TD> </tr>
<tr><td>cr</td><td>Cree</td> <TD></TD> </tr>
<tr><td>cs</td><td>Czech</td> <TD>Čeština</TD> </tr>
<tr><td>cu</td><td>Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic</td> <TD>Словѣ́ньскъ</TD> </tr>
<tr><td>cv</td><td>Chuvash</td> <TD>Чӑвашла</TD> </tr>
<tr><td>cy</td><td>Welsh</td> <TD>Cymraeg</TD> </tr>
<tr><td>da</td><td>Danish</td> <TD>Dansk</TD> </tr>
<tr><td>de</td><td>German</td> <TD>Deutsch</TD> </tr>
<tr><td>dv</td><td>Divehi; Dhivehi; Maldivian</td> <TD>ދިވެހިބަސ</TD> </tr>
<tr><td>dz</td><td>Dzongkha</td> <TD>རྫོང་ཁ</TD> </tr>
<tr><td>ee</td><td>Ewe</td> <TD>Eʋegbe</TD> </tr>
<tr><td>el</td><td>Greek, Modern (1453-)</td> <TD>Ελληνικά</TD> </tr>
<tr><td>en</td><td>English</td> <TD></TD> </tr>
<tr><td>eo</td><td>Esperanto</td> <TD>Esperanto </TD> </tr>
<tr><td>es</td><td>Spanish; Castilian</td> <TD>Español</TD> </tr>
<tr><td>et</td><td>Estonian</td> <TD>Eesti</TD> </tr>
<tr><td>eu</td><td>Basque</td> <TD>Euskera</TD> </tr>
<tr><td>fa</td><td>Persian</td> <TD>پارسی </TD> </tr>
<tr><td>ff</td><td>Fulah</td> <TD>Fulfulde</TD> </tr>
<tr><td>fi</td><td>Finnish</td> <TD>Suomi</TD> </tr>
<tr><td>fj</td><td>Fijian</td> <TD>Na Vosa Vakaviti</TD> </tr>
<tr><td>fo</td><td>Faroese</td> <TD>føroyskt</TD> </tr>
<tr><td>fr</td><td>French</td> <TD>Français</TD> </tr>
<tr><td>fy</td><td>Western Frisian</td> <TD>Frysk</TD> </tr>
<tr><td>ga</td><td>Irish</td> <TD>Gaeilge</TD> </tr>
<tr><td>gd</td><td>Gaelic; Scottish Gaelic</td> <TD>Gàidhlig</TD> </tr>
<tr><td>gl</td><td>Galician</td> <TD>Galego</TD> </tr>
<tr><td>gn</td><td>Guarani</td> <TD>Avañe'ẽ</TD> </tr>
<tr><td>gu</td><td>Gujarati</td> <TD>ગુજરાતી</TD> </tr>
<tr><td>gv</td><td>Manx</td> <TD>Gaelg</TD> </tr>
<tr><td>ha</td><td>Hausa</td> <TD>هَوُسَ</TD> </tr>
<tr><td>he</td><td>Hebrew</td> <TD>עברית</TD> </tr>
<tr><td>hi</td><td>Hindi</td> <TD>हिन्दी</TD> </tr>
<tr><td>ho</td><td>Hiri Motu</td> <TD></TD> </tr>
<tr><td>hr</td><td>Croatian</td> <TD>Hrvatski</TD> </tr>
<tr><td>ht</td><td>Haitian; Haitian Creole</td> <TD>Kreyòl ayisyen</TD> </tr>
<tr><td>hu</td><td>Hungarian</td> <TD>Magyar</TD> </tr>
<tr><td>hy</td><td>Armenian</td> <TD>Հայերեն</TD> </tr>
<tr><td>hz</td><td>Herero</td> <TD></TD> </tr>
<tr><td>ia</td><td>Interlingua (International Auxiliary Language Association)</td> <TD>Interlingua</TD> </tr>
<tr><td>id</td><td>Indonesian</td> <TD>Bahasa Indonesia</TD> </tr>
<tr><td>ie</td><td>Interlingue; Occidental</td> <TD>Interlingue</TD> </tr>
<tr><td>ig</td><td>Igbo</td> <TD>Igbo</TD> </tr>
<tr><td>ii</td><td>Sichuan Yi; Nuosu</td> <TD>ꆇꉙ</TD> </tr>
<tr><td>ik</td><td>Inupiaq</td> <TD>Iñupiak</TD> </tr>
<tr><td>io</td><td>Ido</td> <TD>Ido</TD> </tr>
<tr><td>is</td><td>Icelandic</td> <TD>Icelandic</TD> </tr>
<tr><td>it</td><td>Italian</td> <TD>Italian</TD> </tr>
<tr><td>iu</td><td>Inuktitut</td> <TD>ᐃᓄᒃᑎᑐᑦ/inuktitut</TD> </tr>
<tr><td>ja</td><td>Japanese</td> <TD>日本語</TD> </tr>
<tr><td>jv</td><td>Javanese</td> <TD>Basa Jawa</TD> </tr>
<tr><td>ka</td><td>Georgian</td> <TD>ქართული</TD> </tr>
<tr><td>kg</td><td>Kongo</td> <TD></TD> </tr>
<tr><td>ki</td><td>Kikuyu; Gikuyu</td> <TD></TD> </tr>
<tr><td>kj</td><td>Kuanyama; Kwanyama</td> <TD></TD> </tr>
<tr><td>kk</td><td>Kazakh</td> <TD>Қазақ</TD> </tr>
<tr><td>kl</td><td>Kalaallisut; Greenlandic</td> <TD>Kalaallisut</TD> </tr>
<tr><td>km</td><td>Central Khmer</td> <TD>ខ្មែរ</TD> </tr>
<tr><td>kn</td><td>Kannada</td> <TD>ಕನ್ನಡ</TD> </tr>
<tr><td>ko</td><td>Korean</td> <TD>한국어</TD> </tr>
<tr><td>kr</td><td>Kanuri</td> <TD></TD> </tr>
<tr><td>ks</td><td>Kashmiri</td> <TD>कश्मीरी - (كشميري)</TD> </tr>
<tr><td>ku</td><td>Kurdish</td> <TD> وۆردپرێس بەکوردی</TD> </tr>
<tr><td>kv</td><td>Komi</td> <TD>Коми</TD> </tr>
<tr><td>kw</td><td>Cornish</td> <TD>kernewek</TD> </tr>
<tr><td>ky</td><td>Kirghiz; Kyrgyz</td> <TD>Кыргыз</TD> </tr>
<tr><td>la</td><td>Latin</td> <TD>Latina</TD> </tr>
<tr><td>lb</td><td>Luxembourgish; Letzeburgesch</td> <TD></TD> </tr>
<tr><td>lg</td><td>Ganda</td> <TD>Luganda</TD> </tr>
<tr><td>li</td><td>Limburgan; Limburger; Limburgish</td> <TD>Limburgs</TD> </tr>
<tr><td>ln</td><td>Lingala</td> <TD>Lingála</TD> </tr>
<tr><td>lo</td><td>Lao</td> <TD>ລາວ</TD> </tr>
<tr><td>lt</td><td>Lithuanian</td> <TD>Lietuvių</TD> </tr>
<tr><td>lu</td><td>Luba-Katanga</td> <TD></TD> </tr>
<tr><td>lv</td><td>Latvian</td> <TD>Latviešu</TD> </tr>
<tr><td>mg</td><td>Malagasy</td> <TD>Malagasy</TD> </tr>
<tr><td>mh</td><td>Marshallese</td> <TD></TD> </tr>
<tr><td>mi</td><td>Maori</td> <TD>Māori</TD> </tr>
<tr><td>mk</td><td>Macedonian</td> <TD>Македонски</TD> </tr>
<tr><td>ml</td><td>Malayalam</td> <TD>മലയാളം</TD> </tr>
<tr><td>mn</td><td>Mongolian</td> <TD>Монгол хэл</TD> </tr>
<tr><td>mr</td><td>Marathi</td> <TD>मराठी</TD> </tr>
<tr><td>ms</td><td>Malay</td> <TD>Bahasa Melayu</TD> </tr>
<tr><td>mt</td><td>Maltese</td> <TD>Malt</TD> </tr>
<tr><td>my</td><td>Burmese</td> <TD></TD> </tr>
<tr><td>na</td><td>Nauru</td> <TD>Dorerin Naoero</TD> </tr>
<tr><td>nb</td><td>Bokmal, Norwegian; Norwegian Bokmal</td> <TD>Bokmål</TD> </tr>
<tr><td>nd</td><td>Ndebele, North; North Ndebele</td> <TD></TD> </tr>
<tr><td>ne</td><td>Nepali</td> <TD>नेपाली</TD> </tr>
<tr><td>ng</td><td>Ndonga</td> <TD></TD> </tr>
<tr><td>nl</td><td>Dutch; Flemish</td> <TD>Nederlands</TD> </tr>
<tr><td>nn</td><td>Norwegian Nynorsk; Nynorsk, Norwegian</td> <TD>Nynorsk</TD> </tr>
<tr><td>no</td><td>Norwegian</td> <TD>Norsk (bokmål)</TD> </tr>
<tr><td>nr</td><td>Ndebele, South; South Ndebele</td> <TD></TD> </tr>
<tr><td>nv</td><td>Navajo; Navaho</td> <TD>Diné bizaad</TD> </tr>
<tr><td>ny</td><td>Chichewa; Chewa; Nyanja</td> <TD>Chi-Chewa</TD> </tr>
<tr><td>oc</td><td>Occitan</td> <TD>Occitan</TD> </tr>
<tr><td>oj</td><td>Ojibwa</td> <TD></TD> </tr>
<tr><td>om</td><td>Oromo</td> <TD>Oromoo</TD> </tr>
<tr><td>or</td><td>Oriya</td> <TD>ଓଡ଼ିଆ</TD> </tr>
<tr><td>os</td><td>Ossetian; Ossetic</td> <TD>Ирон</TD> </tr>
<tr><td>pa</td><td>Panjabi; Punjabi</td> <TD>ਪੰਜਾਬੀ</TD> </tr>
<tr><td>pi</td><td>Pali</td> <TD>पािऴ</TD> </tr>
<tr><td>pl</td><td>Polish</td> <TD>Polski</TD> </tr>
<tr><td>ps</td><td>Pushto; Pashto</td> <TD>پښتو</TD> </tr>
<tr><td>pt</td><td>Portuguese</td> <TD>Português</TD> </tr>
<tr><td>qu</td><td>Quechua</td> <TD>Runa Simi</TD> </tr>
<tr><td>rm</td><td>Romansh</td> <TD>Rumantsch</TD> </tr>
<tr><td>rn</td><td>Rundi</td> <TD></TD> </tr>
<tr><td>ro</td><td>Romanian; Moldavian; Moldovan</td> <TD>Română</TD> </tr>
<tr><td>ru</td><td>Russian</td> <TD>Русский</TD> </tr>
<tr><td>rw</td><td>Kinyarwanda</td> <TD></TD> </tr>
<tr><td>sa</td><td>Sanskrit</td> <TD>संस्कृत</TD> </tr>
<tr><td>sc</td><td>Sardinian</td> <TD>Sardu</TD> </tr>
<tr><td>sd</td><td>Sindhi</td> <TD>سنڌي</TD> </tr>
<tr><td>se</td><td>Northern Sami</td> <TD>Sámegiella</TD> </tr>
<tr><td>sg</td><td>Sango</td> <TD>Sängö</TD> </tr>
<tr><td>si</td><td>Sinhala; Sinhalese</td> <TD>සිංහල</TD> </tr>
<tr><td>sk</td><td>Slovak</td> <TD>Slovenčina</TD> </tr>
<tr><td>sl</td><td>Slovenian</td> <TD>Slovenščina</TD> </tr>
<tr><td>sm</td><td>Samoan</td> <TD>Gagana Samoa</TD> </tr>
<tr><td>sn</td><td>Shona</td> <TD>chiShona</TD> </tr>
<tr><td>so</td><td>Somali</td> <TD>Soomaali</TD> </tr>
<tr><td>sq</td><td>Albanian</td> <TD>Shqip</TD> </tr>
<tr><td>sr</td><td>Serbian</td> <TD>Српски</TD> </tr>
<tr><td>ss</td><td>Swati</td> <TD>SiSwati</TD> </tr>
<tr><td>st</td><td>Sotho, Southern</td> <TD>Sesotho</TD> </tr>
<tr><td>su</td><td>Sundanese</td> <TD>Basa Sunda</TD> </tr>
<tr><td>sv</td><td>Swedish</td> <TD>Svenska</TD> </tr>
<tr><td>sw</td><td>Swahili</td> <TD>Kiswahili</TD> </tr>
<tr><td>ta</td><td>Tamil</td> <TD>தமிழ்</TD> </tr>
<tr><td>te</td><td>Telugu</td> <TD>తెలుగు</TD> </tr>
<tr><td>tg</td><td>Tajik</td> <TD>Tajik</TD> </tr>
<tr><td>th</td><td>Thai</td> <TD>ภาษาไทย</TD> </tr>
<tr><td>ti</td><td>Tigrinya</td> <TD>ትግርኛ</TD> </tr>
<tr><td>tk</td><td>Turkmen</td> <TD>Türkmençe</TD> </tr>
<tr><td>tl</td><td>Tagalog</td> <TD>Tagalog</TD> </tr>
<tr><td>tn</td><td>Tswana</td> <TD>Setswana</TD> </tr>
<tr><td>to</td><td>Tonga (Tonga Islands)</td> <TD>lea faka-Tonga</TD> </tr>
<tr><td>tr</td><td>Turkish</td> <TD>Türkçe</TD> </tr>
<tr><td>ts</td><td>Tsonga</td> <TD>Xitsonga</TD> </tr>
<tr><td>tt</td><td>Tatar</td> <TD>Tatarça</TD> </tr>
<tr><td>tw</td><td>Twi</td> <TD></TD> </tr>
<tr><td>ty</td><td>Tahitian</td> <TD>Reo Mā`ohi</TD> </tr>
<tr><td>ug</td><td>Uighur; Uyghur</td> <TD>ئۇيغۇرچە</TD> </tr>
<tr><td>uk</td><td>Ukrainian</td> <TD>Українська</TD> </tr>
<tr><td>ur</td><td>Urdu</td> <TD>اردو</TD> </tr>
<tr><td>uz</td><td>Uzbek</td> <TD>O‘zbekcha</TD> </tr>
<tr><td>ve</td><td>Venda</td> <TD>Tshivenda</TD> </tr>
<tr><td>vi</td><td>Vietnamese</td> <TD>Tiếng Việt</TD> </tr>
<tr><td>vo</td><td>Volapuk</td> <TD>Volapük</TD> </tr>
<tr><td>wa</td><td>Walloon</td> <TD>Walon</TD> </tr>
<tr><td>wo</td><td>Wolof</td> <TD>Wolof</TD> </tr>
<tr><td>xh</td><td>Xhosa</td> <TD>isiXhosa</TD> </tr>
<tr><td>yi</td><td>Yiddish</td> <TD>ייִדיש</TD> </tr>
<tr><td>yo</td><td>Yoruba</td> <TD>Yorùbá</TD> </tr>
<tr><td>za</td><td>Zhuang; Chuang</td> <TD>Vahcuengh</TD> </tr>
<tr><td>zh</td><td>Chinese</td> <TD>中文 (zh_CN); 香港 (zh_HK); 台灣 (zh_TW)</TD> </tr>
<tr><td>zu</td><td>Zulu</td> <TD>isiZulu</TD> </tr>
</table>Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com0tag:blogger.com,1999:blog-3333619357391010212.post-60819405986948559422012-01-16T11:54:00.001+08:002014-01-12T09:00:49.934+08:00Сдача налоговой отчетности ИП за 2011 год<div dir="ltr" style="text-align: left;" trbidi="on">
ИП на УСН без работников сдает за 2011 год в налоговую <a href="http://www.nalogservice.ru/faq/document34366.htm">следующую отчетность:</a><br />
<ul><li>Сведения о среднесписочной численности работников за предшествующий календарный год (<b>не позднее 20 января</b>)</li>
<li>Книга учета доходов и расходов организаций и индивидуальных предпринимателей, применяющих УСН (не позднее 30 апреля) </li>
<li>Налоговая декларация по налогу, уплачиваемому в связи с применением УСН (не позднее 30 апреля)</li>
</ul>Рассмотрим, как эту отчетность можно подготовить.<br />
<a name='more'></a><br />
<h4>Софт</h4>Скачиваем <a href="http://www.gnivc.ru/software/free_software/software_ul_fl/">свежую версию программы Налогоплательщик ЮЛ</a> (в настоящий момент, это версия <a href="http://www.gnivc.ru/software/free_software/software_ul_fl/taxpayer_ul/722/">4.28</a> + <a href="http://www.gnivc.ru/software/free_software/software_ul_fl/taxpayer_ul/731/">обновления</a>. Устанавливаем ее. <br />
<br />
Рекомендую устанавливать Налогоплательщик ЮЛ на виртуальной машине. Эта программа нужна один раз в год, смысла держать ее у себя на компьютере нет никакого. Программа поддерживает экспорт/импорт личных данных (<code>Сервис\Сохранение информации</code>, <code>Сервис\Восстановление информации</code>), так что: установил программу, загрузил в нее личные данные из бакапа, подготовил отчетность, выгрузил обновленные личные данные в бакап, снес программу. Виртуалка тут подходит идеально.<br />
<br />
Документы потребуется распечатать. На виртуальной машине проще всего печатать в PDF. Для этого нужно на виртуалке дополнительно установить <a href="http://sourceforge.net/projects/pdfcreator/">PDFCreator</a>.<br />
<br />
<h4>Подготовка</h4>Загрузили данные из бакапа или ввели данные о своем ИП впервые. Теперь надо выбрать отчетный период - год 2011.<br />
<br />
<h4>Сведения о среднесписочной численности работников</h4>Добраться до этого документа в Налогоплательщик ЮЛ можно следующим образом.<br />
<ul><li>Документы\Налоговая отчетность</li>
<li>Добавить</li>
<li>Выбираем первый документ - КНД 1110018, Сведения о среднесписочной численности работников за предшествующий календарный год.</li>
</ul><br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVIJYzc5G6H4PHz8bKv14bWwOWGfQIPn3-UInSt-dzO5cfzxgHYMrxy0acMwsj3qRxO0UZKfAxhRUh0mN5C8HQ2gIb-Jno0rIIFde899ax6bam1P8QouZ5RcXkApw-1gnce9crStEYUoFO/s1600/doc1.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="236" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVIJYzc5G6H4PHz8bKv14bWwOWGfQIPn3-UInSt-dzO5cfzxgHYMrxy0acMwsj3qRxO0UZKfAxhRUh0mN5C8HQ2gIb-Jno0rIIFde899ax6bam1P8QouZ5RcXkApw-1gnce9crStEYUoFO/s320/doc1.jpg" /></a></div><br />
Выбираем, заполняем. Заполнить нужно два поля: дату (1 января 2012) и количество работников (если ИП без работников, ставим 0).<br />
<br />
Подписываем, распечатываем в двух экземплярах, экспортируем в XML файл, сохраняем его на флешку.<br />
<br />
<h4>Декларация</h4>В Налогоплательщик ЮЛ:<br />
<ul><li>Документы\Налоговая отчетность</li>
<li>Добавить</li>
<li>Выбираем документ - КНД 1152017, Декларация по налогу, уплачиваемому в связи с применением упрощенной системы налогообложения.</li>
</ul><br />
Заполняем декларацию, распечатываем в двух экземплярах, экспортируем в XML файл, сохраняем его на флешку (налоговая требует XML). Ответы на вопросы по заполнению декларации лучше всего искать на <a href="http://www.klerk.ru/">klerk.ru</a>, народ на форуме там грамотный. Пример заполнения приведен <a href="http://www.rnk.ru/journal/archives/2010/4/nalogovoe_administrirovanie/nalogooblozhenie_malogo_biznesa/po_itogam129892.phtml">здесь.</a><br />
<br />
<h4>Книга доходов и расходов</h4>Книги доходов и расходов (КДР) в Налогоплательщик ЮЛ нет. Поэтому берем <a href="http://www.nalogservice.ru/faq/document28480.htm">бланк</a> и заполняем ее в Excel. <br />
<br />
Порядок заполнения КДР рассмотрен <a href="http://www.delo-press.ru/articles.php?n=7279">здесь</a>. 5 лист нужен только тем, у кого в качестве объекта налогообложения выбраны "доходы, уменьшенные на величину расходов". 4 лист нужен тем, кто уплачивает <a href="http://ru.wikipedia.org/wiki/Единый_налог_на_вменённый_доход">единый налог</a>. Остальным достаточно заполнить первые три листа.<br />
<br />
Книгу доходов и расходов распечатываем в двух экземплярах. Согласно законодательству, каждый экземпляр должен быть прошит и опечатан. Протыкаем в КДР три дырки, на расстоянии ~ 1 см от левого края. Сшиваем ниткой, концы нитки выводим на обратную сторону последнего листа КДР. Заклеиваем концы нитки бумажкой, на которой пишем: "Прошито, пронумеровано, скреплено печатью 3 (три) листа". Подпись, ФИО, дата. Естественно, если печати нет, то "скреплено печатью" писать не нужно. Вот <a href="http://www.ippnou.ru/images/article/0701/2922_6.gif">пример опечатанного документа</a>.<br />
<br />
<h4>Идем в налоговую</h4>Берем с собой:<br />
<ul><li>Два экземпляра сведений о среднесписочной численности + <b>электронная версия в виде XML на флешке</b>.</li>
<li>Два экземпляра декларации + электронная версия в виде XML файла на флешке.</li>
<li>Два прошитых и опечатанных экземпляра КДР.</li>
</ul>Один экземпляр каждого документа отдаем в налоговую, один заверяем в налоговой и оставляем себе.
<br /></div>Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com3tag:blogger.com,1999:blog-3333619357391010212.post-29014428238844083212012-01-06T14:55:00.008+08:002014-03-27T08:35:19.477+08:00Статические анализаторы кода для Android-приложения.Одним из путей улучшения качества кода приложения является <a href="http://habrahabr.ru/blogs/code_review/135234/#habracut">регулярное</a> применение статических анализаторов кода. Статический анализ кода позволяет находить ошибки в коде, отслеживать несоблюдение стандарта кодирования, находить "плохой" код, который может привести к проблемам в будущем, а так же неэффективный код, негативно сказывающийся на производительности в настоящем, строить метрики кода, находить полностью или частично одинаковые фрагменты кода и так далее. <br />
<br />
Для разработчиков, ведущих кодирование в одиночку, статические анализаторы вещь вообще неоценимая. Ведь им некому показать код своего приложения. Так что "электронный помощник", способный грамотно выполнить "code review", может им здорово пригодится.<br />
<br />
Вопрос - какие статические анализаторы кода доступны разработчику Android приложений? Для Java статических анализаторов существует множество (см. список <a href="http://en.wikipedia.org/wiki/Lint_(software)">lint</a> приложений для Java в википедии), но не все они умеют работать с Android-приложениями и, тем более, учитывать их специфику.<br />
<br />
Мне удалось отыскать следующие статические анализаторы кода, которые умеют работать с Android-приложениями:<br />
<ul><li><a href="http://tools.android.com/tips/lint">Lint</a>. "Родной" анализатор кода для Android, входит в состав Android SDK начиная с r16.</li>
<li><a href="http://findbugs.sourceforge.net/">FindBugs</a> (бесплатный). </li>
<li><a href="http://checkstyle.sourceforge.net/">Checkstyle</a> (бесплатный). </li>
<li><a href="https://developers.google.com/java-dev-tools/codepro/doc/">CodePro Analytix</a> (бесплатный). </li>
<li><a href="http://pmd.sourceforge.net/">PMD</a> (бесплатный)</li>
<li>Motodev App Validator (бесплатный). Входит в состав среды разработки <a href="http://developer.motorola.com/docstools/motodevstudio/">MOTODEV Studio for Android</a>. Есть <a href="http://developer.motorola.com/testing/app-validator/">online</a> вариант.</li>
<li><a href="http://www.klocwork.com/products/solo/">Klocwork Solo</a> (платный). Доступна триальная версия</li>
<li><a href="http://www.parasoft.com/jsp/products/jtest.jsp">JTest</a> от Parasoft (платный). </li>
<li><a href="http://juliasoft.com/">Julia</a> (платный). <s>Доступен бесплатный <a href="http://julia.scienze.univr.it/">online</a> вариант.</s></li>
</ul>Далее представлены результаты сравнения всех этих анализаторов - какой анализатор что умеет, для чего предназначен, насколько удобно пользоваться, какие ошибки находит, насколько он полезен на практике. <br />
<br />
Чтобы было интереснее сравнивать, я "натравил" каждый из этих анализаторов на одно и то же тестовое приложение. В качестве такового я взял код одной из ранних версий моего приложения <a href="https://market.android.com/details?id=com.mobilityflow.awidget&hl=ru">Animated Widget Contact Launcher</a>. Версию я выбрал годичной давности, багов и проблем в ней было вагон (с FindBugs на тот момент я был не знаком). Так что "что поискать" в ней точно было.<br />
<a name='more'></a><br />
<br />
<h3>Lint</h3><a href="http://tools.android.com/tips/lint">Lint</a> - анализатор кода, который поставляется вместе с Android SDK. Он появился совсем недавно, в версии r16. <br />
<br />
Lint нацелен на поиск проблем, связанных с ресурсами. Неиспользуемые ресурсы, ненужные или наоборот, отсутствующие, аттрибуты, неоптимальные конструкции в layout и т.д. Список проблем, которые ищет Lint, приведен <a href="http://tools.android.com/tips/lint-checks">здесь</a>. <br />
<br />
Работать с Lint очень удобно. Вызывать и настраивать его можно прямо из Eclipse. Для найденных ошибок показывается объяснение - в чем состоит ошибка и как ее исправить. Ошибки делятся на 3 уровня (error, warning, information). <br />
<br />
При проверке тестового приложения был выдан 101 warning. Все по существу - ложных срабатываний не было, за исключением пары сообщений об неиспользуемости ресурсов (и это при отсутствии библиотек; если же библиотеки используются, то разработчики прямо предупреждают о возможности ложных сообщений, так что будьте осторожны при чистке кода).<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhxRdfQnJ_JUfYMPm88nv-MepcVdb0wBgUvpCVjb_qHxqQbgjtK3RkXqDty-O_W4Ayl50hz3aldNEiOdErf8Q3MpqXg0Mqhq4h9JcY_vI4vwrj0ka8wUA8weGrH5t3Nb2pG6N2TUdhotWzB/s1600/lint2.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="252" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhxRdfQnJ_JUfYMPm88nv-MepcVdb0wBgUvpCVjb_qHxqQbgjtK3RkXqDty-O_W4Ayl50hz3aldNEiOdErf8Q3MpqXg0Mqhq4h9JcY_vI4vwrj0ka8wUA8weGrH5t3Nb2pG6N2TUdhotWzB/s320/lint2.png" /></a></div><br />
<h3>FindBugs</h3>FindBugs я пользуюсь уже почти год и очень доволен. Находит реальные ошибки в коде. Вот полный <a href="http://findbugs.sourceforge.net/bugDescriptions.html">список проблем</a>, которые он умеет находить.<br />
<br />
В декабре 2011, после длительного перерыва, FindBugs обновился с версии 1.3.9 до 2.0. Субъективно новая версия стала точнее, удобнее и быстрее предыдущей. Настраивается она теперь прямо из Eclipse (важно: в свойствах проекта, а не в свойствах workspace). Плагин для eclipse устанавливается с помощью location URL: <code>http://findbugs.cs.umd.edu/eclipse/</code> (в Eclipse выбрать <code>Help\Install new software</code>, ввести location URL в поле Work with).<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjP4GGwXFJYNnGrF2FjQlDGEEcO_hIyDaehUa6t3dGS8BMEBX_Y_qkb5XObiFwH98oTPz7pmorGGJtQWnBnJWxvlTgLSG3LEv6DQZ48PRjfRSn_Mua601CuX_4SOP6UkTk1rzvDtsxeuXLP/s1600/findbugs_settings.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="218" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjP4GGwXFJYNnGrF2FjQlDGEEcO_hIyDaehUa6t3dGS8BMEBX_Y_qkb5XObiFwH98oTPz7pmorGGJtQWnBnJWxvlTgLSG3LEv6DQZ48PRjfRSn_Mua601CuX_4SOP6UkTk1rzvDtsxeuXLP/s320/findbugs_settings.jpg" /></a></div><br />
FindBugs нашел в тестовом приложении 12 ошибок при дефолтном уровне чувствительности. При максимальной чувствительности ошибок нашлось 137. При этом ложных срабатываний было всего 10 (анализатору не понравился способ именования классов в автоматические генерируемом файле ресурсов; этот файл нужно просто исключить из обработки в настройках FindBugs). Подавляющее большинство остальных ошибок я исправил в более поздних версиях.<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFgLj41xg1DoP2DD50r5CIM5bAFvkbWgUXDdAjyr3rziMv69wYYj5h067mSR0b2j-UJp2IaCT-hx_L9fxz5gcvLMC0fmtwShGVVn8wN4qNWpRKrAy1f_6Ns1Binto2peWJLRWFXbyqcliO/s1600/findbugs_sample.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="227" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFgLj41xg1DoP2DD50r5CIM5bAFvkbWgUXDdAjyr3rziMv69wYYj5h067mSR0b2j-UJp2IaCT-hx_L9fxz5gcvLMC0fmtwShGVVn8wN4qNWpRKrAy1f_6Ns1Binto2peWJLRWFXbyqcliO/s320/findbugs_sample.jpg" /></a></div><br />
Приведу примеры ошибок, которые нашел FindBugs:<br />
<ul><li><code>Should be a static inner class</code> (вложенный класс не сделан статичным, хотя доступ к внешнему классу ему не нужен).</li>
<li><code>Should this field be static?</code> (константа не объявлена как static)</li>
<li><code>Switch statement found where one case falls through to the next case</code> (пропущен break)</li>
<li><code>Comparison of String parameter using == or != in</code> (строки в Java небходимо сравнивать через equal)</li>
</ul><br />
Специфичных для Android правил в FindBugs пока нет. Но, возможно, со временем они появятся - разработчики <a href="http://groups.google.com/group/openintents/browse_thread/thread/b3a520a90254ce7f?pli=1">готовы идти навстречу пожеланиям пользователей.</a><br />
<br />
<h3>Checkstyle</h3><a href="http://checkstyle.sourceforge.net/">Checkstyle</a> нацелен на проверку соблюдения стандарта кодирования. Проверка соблюдения правил именования, правил расстановки скобочек, правил оформления кода и т.д. - вот цель этого анализатора. Полный список правил можно посмотреть в <a href="http://checkstyle.sourceforge.net/config_coding.html">документации</a>. Плагин для eclipse <a href-"http://eclipse-cs.sourceforge.net/downloads.html">устанавливается</a> с помощью location URL: <code>http://eclipse-cs.sf.net/update/</code><br />
<br />
После анализа тестового приложения Checkstyle выдал 8741(!) ворнинг. Я внимательно просмотрел все типы ворнингов.. и не нашел для себя ничего полезного. На мой взгляд - это совершенно не тот инструмент, который необходим индивидуальным разработчикам и небольшим командам. С другой стороны, его активно используют и <a href="http://sevntu-checkstyle.github.com/sevntu.checkstyle/">дорабатывают</a>, так что видимо область применения у него все же есть.<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtPxkoXq_paRxbyDsUvqgvK-JAsurudixiSXF7cVU5Ewssp5TyQOArj551sYZbkQz-RAJwOUPxScPFLlqD3G4iXYq2dvl-q2U75u1aGiZRXfp8iicuSNRl-f0PLA60uubsHZeD4WbWLCCK/s1600/checkstyle_settings.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="320" width="290" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtPxkoXq_paRxbyDsUvqgvK-JAsurudixiSXF7cVU5Ewssp5TyQOArj551sYZbkQz-RAJwOUPxScPFLlqD3G4iXYq2dvl-q2U75u1aGiZRXfp8iicuSNRl-f0PLA60uubsHZeD4WbWLCCK/s320/checkstyle_settings.png" /></a></div><br />
<h3>CodePro Analytix</h3><a href="http://code.google.com/intl/ru-RU/javadevtools/codepro/doc/index.html">CodePro Analytix</a> - разработка компании Instantiations. Google купил эту компанию и <a href="http://googlewebtoolkit.blogspot.com/2010/09/google-relaunches-instantiations.html">сделал эти продукт бесплатным</a>, за что ему большое спасибо.<br />
<br />
Заявленные <a href="http://code.google.com/intl/ru-RU/javadevtools/codepro/doc/index.html">возможности CodePro Analytix</a> впечатляют. <a href="http://code.google.com/intl/ru-RU/javadevtools/codepro/doc/features/audit/audit.html">Аудит кода</a> на основе нескольких различных наборов правил ("The Elements of Java Style", "Effective Java", "Potential Errors and Refactoring", "Security" и т.д.). Измерение кода с помощью различных метрик. Поиск "дублей" в коде - результатов копипаста. И т.д.<br />
<br />
Провел аудит кода тестового приложения используя все возможные наборы правил. Получил: 152 предупреждения с уровнем серьезности High, 1246 - medium, 1062 - low. Для сравнения - анализ с помощью единственного набора правил "Code Pro Core" выдал 64 medium и 4 low предупреждения.<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjVSiumczKf8FS4eQU58PBCAC01NWhoPf_wG62iz-cl7kKMjwLf5BqXm7AAuwRNCz_K4m2ZFoFIAlAHUqzBQg47rnCwxu7XtMtVvaroqqyBJGQ2YOnxkovYP4FpJa_ZDLpJgaRPz88Kr5sY/s1600/codepro_audit.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="268" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjVSiumczKf8FS4eQU58PBCAC01NWhoPf_wG62iz-cl7kKMjwLf5BqXm7AAuwRNCz_K4m2ZFoFIAlAHUqzBQg47rnCwxu7XtMtVvaroqqyBJGQ2YOnxkovYP4FpJa_ZDLpJgaRPz88Kr5sY/s320/codepro_audit.png" /></a></div><br />
Что-же нашел Code Pro? Частично, результаты поиска пересеклись с результатами FindBugs и Lint. Но только частично. Большую часть ошибок составили минорные проблемы в коде, которые не были обнаружены другими анализаторами. Например:<br />
<ul><li><code>String literal can be replaced by a character literal</code>. В StringBuilder вместо append("\n") использовать append('\n').</li>
<li><code>Badly located array declarators</code>. Массив объявлен как "String abc[]" вместо "String[] abc".</li>
<li><code>Constant on right side of comparison</code>. Код "if (a == 5) { .. }" следует переписать так "if (5 == a") { .. }"</li>
<li><code>Switch statements should include all possible enumeration constants</code>. В перечислимом типе 3 возможных значения, а в switch задействовано только 2 из них.</li>
<li><code>Do not divide by powers of 2: use ">> 1" rather than "/ 2"</code></li>
<li><code>Invalid string literal: "Yes"</code>. Строки следует хранить в ресурсах, а не кодировать их жестко в код.</li>
<li><code>Use charAt() rather than startsWith() when the constant is a single character string</code>. Код skinName.startsWith("/") можно переписать skinName.charAt(0) == '/'</li>
<li><code>Define the initial capacity of StringBuilder instances</code></li>
</ul>Если не полениться и настроить Code Pro под себя, то инструмент оказывается весьма полезным. Возможно, некоторые рекомендации похожи на "ловлю блох".. но почему бы не привыкнуть писать более оптимальный код, если это возможно?<br />
<br />
Кстати, юзабилити у Code Pro на высоте. Находясь в списке ошибок можно:<br />
<ul><li>автоматически исправить ошибку (естестенно, автоматический рефакторинг кода поддерживается не для всех типов правил);</li>
<li>проигнорировать ошибку - она перестанет появляться в списке ошибок;</li>
<li>отключить правило, по которому была найдена ошибка;</li>
<li>настроить (!) параметры правила,</li>
</ul>и так далее.<br />
<br />
Кроме аудита, очень понравилось, как Code Pro ищет дубли. Незаменимая функция. <br />
<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj0_pBNiUbwcxEnTG0ZfXrDMplUNJOkcWTRrN5SHZlhqVpw_mRJu1AMwqVRDQTciKJtgZCiZ08886bD8Sxc-ypbdoYGuWNxGjFJJ1Be4XIVIglG4veiZ8ywgkndS1LrLuHovOf9lcNbtikc/s1600/codepro_compare.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="211" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj0_pBNiUbwcxEnTG0ZfXrDMplUNJOkcWTRrN5SHZlhqVpw_mRJu1AMwqVRDQTciKJtgZCiZ08886bD8Sxc-ypbdoYGuWNxGjFJJ1Be4XIVIglG4veiZ8ywgkndS1LrLuHovOf9lcNbtikc/s320/codepro_compare.png" /></a></div><br />
<h3>PMD</h3><a href="http://pmd.sourceforge.net/">PMD</a> - еще один анализатор кода, типа Code Pro. Основное назначение - поиск неоптимального кода, проблем с производительностью, нарушений стиля кодирования, дублей в коде и т.д. Плагин PMD устанавливается с помощью location URL: <code> http://sourceforge.net/projects/pmd/files/pmd-eclipse/update-site/</code><br />
<br />
Примечательно, что в PMD реализовано <a href="http://pmd.sourceforge.net/rules/android.html">несколько правил, специфичных для Android</a><br />
<br />
Для тестового приложения PMD выдал около 2000 предупреждений. Примеры:<br />
<ul><li><code>Substitute calls to size() == 0 (or size() != 0) with calls to isEmpty()</code></li>
<li><code>Private field ''{0}'' could be made final; it is only initialized in the declaration or constructor.</code></li>
<li><code>UnnecessaryCaseChange: Using equalsIgnoreCase() is cleaner than using toUpperCase/toLowerCase().equals()</code></li>
<li><code>UnusedFormalParameter: Avoid unused {0} parameters such as ''{1}''. Avoid passing parameters to methods or constructors and then not using those parameters.</code></li>
<li><code>InefficientEmptyStringCheck: String.trim().length()==0 is an inefficient way to validate an empty String.</code></li>
<li><code>UseIndexOfChar: String.indexOf(char) is faster than String.indexOf(String).</code></li>
<li><code>BooleanInversion: Use bitwise inversion to invert boolean values</code></li>
</ul>Несмотря на то, что PMD генерирует множество лишних предупреждений, следует отметить, что и полезных подсказок он выдает множество. Так же как и Code Pro, PMD следует тщательно настраивать.<br />
<br />
В PMD, так же как в Code Pro, реализована функция поиска повторяющихся участков кода. Визуализация найденных фрагментов, правда, похуже. Зато результаты поиска - другие, так что программы дополняют друг друга. <br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhTUKIo0c7cNwhajcqNyL1fbtzoX0Qppg2i8m-MUrqxI4jZWvRtjvZ8hS2ZVf6UUnM-42l3xfHSKQaNxLYtZ6R-gwLOHk7aqtEKV6cV_Cs1TfBvfxZXHlxvySFKgH1roitzPSSOBz_PjHZy/s1600/pmd.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="227" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhTUKIo0c7cNwhajcqNyL1fbtzoX0Qppg2i8m-MUrqxI4jZWvRtjvZ8hS2ZVf6UUnM-42l3xfHSKQaNxLYtZ6R-gwLOHk7aqtEKV6cV_Cs1TfBvfxZXHlxvySFKgH1roitzPSSOBz_PjHZy/s320/pmd.jpg" /></a></div><br />
<h3>Motodev App Validator</h3>Motodev App Validator входит в состав среды разработки <a href="http://developer.motorola.com/docstools/motodevstudio/">MOTODEV Studio for Android</a>. Прочитать про него можно в презентации <a href="http://assets.en.oreilly.com/1/event/68/Static%20Analysis%20For%20Improved%20Application%20Performance%20And%20Quality%20Presentation.pdf">Static Analysis For Improved Application Performance And Quality Presentation (PDF)</a><br />
<br />
Те, кто не использует MOTODEV Studio, могут попробовать <a href="http://developer.motorola.com/testing/app-validator/">online вариант Motodev App Validator</a> (требуется зарегистрироваться на сайте). <br />
<br />
Online вариант Motodev App Validator принимает на вход apk-файл приложения. К сожалению, 400 килобайтную apk-шку тестового приложения online вариант не заглотил - выдал ошибку. Мелкие приложение в 30-40 кб анализирует, а с "крупным" не работает. Пришлось <a href="http://developer.motorola.com/docstools/motodevstudio/download/">качать Motodev Studio</a><br />
<br />
Скачал, запустил. Для тестового приложения Application Validator выдал 101 предупреждение. Из них:<br />
<ul><li>75 - <code>"Drawable "XXX.png" does not exist in the "drawable-ldpi" folder. Add the appropriate "XXX.png" drawable to the "drawable-ldpi" folder."</code> - ничего нового.<br />
</li>
<li>13 - <code>"Default layout file YYY.xml does not have an xlarge-specific version. This app may not have the look and feel expected on devices with extra large screens. Define an xlarge layout for YYY.xml or add <supports-screens android:xlargeScreens="false"> to the manifest file."</code> - так же ничего нового.</li>
<li>4 - <code>"The import android.util.DisplayMetrics is never used ZZZ.java"</code> - ошибка, на которую умеет указывать сам Eclipse.</li>
<li>1 - <code>"The value of the local variable AAA is not used"</code> - эту ошибку умеют находить многие анализаторы.</li>
<li>3 - <code>"The static field A.B should be accessed in a static way ClassA.B"</code> - и эту тоже.</li>
</ul>Интересным оказалось лишь одно сообщение - <code>"The application declares the permission android.permission.CALL_PHONE which implies the unsupported feature android.hardware.telephony..."</code>. Честно говоря, результаты работы не впечатлили.<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgBfuTKXnNCoe4gjQpV0lRrXKJuMwLiGdc2VS8KmgY44AvhjPxT-b31nEzK7XFk-Wxyo77tA5k_4K9p7m-2f1InDLPgySHq6NW2lUz14bb3pzT645kez3dgom_kmK1DmepT92L-uha-3Wko/s1600/motodev_settings.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="319" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgBfuTKXnNCoe4gjQpV0lRrXKJuMwLiGdc2VS8KmgY44AvhjPxT-b31nEzK7XFk-Wxyo77tA5k_4K9p7m-2f1InDLPgySHq6NW2lUz14bb3pzT645kez3dgom_kmK1DmepT92L-uha-3Wko/s320/motodev_settings.png" /></a></div><br />
Впрочем, судя по списку правил, Application Validator умеет находить и более интересные вещи: незакрытые курсоры, недочеты Android Manifest, отсутствие перевода строки в локализованных ресурсах, ненужные или наоборот, пропущенные permissions и т.д.<br />
<br />
<h3>Klocwork Solo for Java</h3><a href="http://www.klocwork.com/products/solo/">Klocwork Solo</a> - платный статический анализатор кода. По заверениям разработчиков, способен находить более 200 типов проблем, связанных с уязвимостью и надежностью кода. Анализатор умеет находить проблемы, специфичные для Android-приложений. Интегрируется с EClipse. Триальная 30-дневная версия доступна после регистрации на сайте. У триальной версии ограничение - не более 300 файлов в проекте.<br />
<br />
Полный список правил, которые проверяет Kockwork Solo, можно посмотреть в <a href="http://download.klocwork.com/docs/issuehelp/index.html">документации</a>. На настоящий момент правил, специфичных для Android, всего девять.<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQ7MrNswJ3F0Wmam03p4daXFn-YgCo-Q6FtcfgeQ6YNyZ0de_Wz0bYKLWHYU4F_GuxksxrsjqiFtxpZuWkXtVA5Bh2irWE23UIliQzXpAoPEQzUZZSz3qYpQRRm2-UrzztaCQCTNa5P38x/s1600/klocwork_settings.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="315" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQ7MrNswJ3F0Wmam03p4daXFn-YgCo-Q6FtcfgeQ6YNyZ0de_Wz0bYKLWHYU4F_GuxksxrsjqiFtxpZuWkXtVA5Bh2irWE23UIliQzXpAoPEQzUZZSz3qYpQRRm2-UrzztaCQCTNa5P38x/s320/klocwork_settings.png" /></a></div><br />
Для тестового приложения Kockwork Solo выдал 240 предупреждений. Неожиданно, очень интересных. Примеры:<br />
<ul><li><code>NPE.RET : Null pointer dereference of 'xy' where null is returned from a method</code>. В приложении имеется такой код: <code>Point xy = m_SA.getShortcutPosition(); Rect r = new Rect(0, 0, xy.x, xy.y);</code>. Функция getShortcutPosition() в отдельных случаях может возвращать null. Но в вызывающей функции проверки на null нет. Это типичная ошибка.</li>
<li><code>NPE.COND : Null pointer dereference of 'xxx' where null comes from condition</code>. Объект xxx используется без проверки на null, а между тем, он запросто может быть равен null.</li>
<li><code>RTC.CALL : Type cast from 'android.view.View' to 'android.widget.ImageButton' is redundant because method 'setTag' is defined in 'android.view.View'</code>. В приложении код такой: <code>((ImageButton)findViewById(R.id.hs_image)).setTag(combination)</code>. Т.е. анализатор обнаружил ненужное приведение типа.</li>
<li><code>REDUN.FINAL : Redundant 'final' modifier</code> - приватный метод объявлен как final. Приватные методы всегда final, так что объявление излишне.</li>
<li><code>JD.SYNC.IN : Field 'VarName' synchronized inconsistently.</code> - доступ к переменной VarName синхронизирован частично. Это ошибка - доступ должен либо всегда синхронизироваться, либо никогда.</li>
<li><code>ANDROID.NPE : Null pointer dereference of 'window' in an Android application</code>. Код в приложении следующий: <code>Window window = activity.getWindow(); window.getDecorView().getWindowVisibleDisplayFrame(rect)</code> Т.е. не выполняется проверка window на null</li>
</ul>Так что Kockwork Solo находит реальные ошибки, причем иные, чем FindBugs (списки дефектов, которые они обнаруживают, пересекаются незначительно). Лично меня очень заинтересовали ошибки типа NPE.RET и Android.NPE. Такие ошибки, судя по всему, не находит ни один другой анализатор.<br />
<br />
Юзабилити у продукта не понравилось. В списке ошибок есть фильтрация, есть возможность указать статус ошибки. Но если указать статус "Ignored", то ошибка пропадает и не ясно, как ее вернуть. Если отфильтровать список, то часть ошибок из списка пропадает и опять же не ясно, как сбросить фильтр. Не интуитивно все как-то. В CodePro сделано удобнее.<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiyDBx2e-DWkhmYwbZWTSb3vv0bzutGowVzfkfPL0cGGWjI7jf7lLi60w13e-vWFR0L07-QIJfKZc0bYMg8-ST4uO9g5OXPTEUT2UlIeBQKvi7xnOoxO5enf5Ku9GJIqvbGQVOBASlf0FX7/s1600/klocwok_screen.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="232" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiyDBx2e-DWkhmYwbZWTSb3vv0bzutGowVzfkfPL0cGGWjI7jf7lLi60w13e-vWFR0L07-QIJfKZc0bYMg8-ST4uO9g5OXPTEUT2UlIeBQKvi7xnOoxO5enf5Ku9GJIqvbGQVOBASlf0FX7/s320/klocwok_screen.jpg" /></a></div><br />
<br />
<h3>JTest от Parasoft</h3>Триальную версию <a href="http://www.parasoft.com/jsp/products/jtest.jsp">Jtest</a> просто так скачать не дают. Так что этот анализатор я в работе проверить не смог. Судя по описанию - функциональность очень интересная. Например, JTest, так же как и Code Pro, умеет самостоятельно рефакторить код.<br />
<br />
<h3>Julia</h3>Julia - коммерческий анализатор кода Java / Android приложений. "Семантический инструмент, основанный на <a href="http://www.juliasoft.com/public/Biblioteca/cade11.pdf">математической теории</a>". <br />
<br />
Как указано на сайте разработчика, протестировать работу анализатора можно <a href="http://julia.scienze.univr.it/">online</a>, загрузив в него jar-файл. У android приложения нет jar файла, есть apk (бинарный формат - другой). APK-файл анализатор принял.. но ни один из доступных способов анализа (Nullness, Termination, Check) ничего не выдал. Пишет - "there are no warnings". <br />
<br />
Загрузил первый попавшийся JAR - работает. Судя по всему, там неплохой анализ на "отсутствие проверки на null". Жаль, что apk-шки не грузятся.<br />
<br />
<b>Update</b>: Написал разработчикам. Они мне подсказали, что можно экспортировать Android приложение в JAR-файл прямо из Eclipse. Экспортировал, загрузил в Julia.<br />
<br />
Nullness analysis и Termination analysis результатов не выдали. Дело в том, что в демонстрационной online-версии для данных типов анализа существует ограничение: общее количество методов не должно превышать 10000 и 8500 соответственно. В тестовом приложении количество методов оказалось большим, так что процедура анализа принудительно останавливалась до завершения работы.<br />
<br />
Третий тип анализа - "Checks", - выдал 286 предупреждений. По большей части, вот таких:<br />
<ul><li><code>[Classcast] A you sure that this cast from classA into classB is always legal?</code></li>
<li><code>[BadNames] Method XXX has a bad name</code></li>
<li><code>[Deadcode] Methos XXX is not reachable</code></li>
<li><code>[BadEq] Inefficient comparison with the empty string. Use isEmpty() instead</code></li>
<li><code>[Approximation] Unsafe comparison beween non-integral numbers.</code></li>
<li><code>[StaticFieldAccess] Modification of static field from a non-static context</code> (это что-то новенькое; другие анализаторы на такие ошибки не ругались).</li>
<li><code>[Unused class] Class YYY is not used.</code></li>
<li><code>[Field access] Field XXX is never reach in reachable code.</code></li>
<li><code>[Useless call] Useless call toString()</code></li>
<li><code>[BadEq] Suspicious use of == rather then equals() to compare two java.lang.String</code></li>
</ul>В принципе, кое что интересно есть. Но: пользоваться очень неудобно (результаты выдаются в виде странички на флеш), поскольку сервис online, приходится "отдавать" исходники приложения в виде jar, что не всегда приемлемо.<br />
<br />
<b>Update2</b>. Попросил разработчиков Julia провести анализ тестового приложения и прислать мне результаты. Они любезно согласились.<br />
<br />
Termination analysis - это анализ кода на наличие бесконечных циклов и рекурсий в коде. Он выдал одно предупреждение. Анализатору не понравился код <br />
<code><br />
private void expungeStaleEntries() {<br />
Reference<? extends V> sv;<br />
while ((sv = queue.poll()) != null) {<br />
hash.remove(reverseLookup.remove(sv));<br />
}<br />
}<br />
</code><br />
из реализации SoftHashMap, взятой <a href="http://www.javaspecialists.eu/archive/Issue098.html">отсюда</a>. Опасения анализатора понятны. Тем не менее, код правильный.<br />
<br />
Nullness analysis проверяет в коде отсутствие необходимых и наличие излишних проверок на null. Таких предупреждений было выдано 187. Примеры ошибок:<br />
<ul><li><code>X1.java:57: is the return value of getY1 non-null?</code></li>
<li><code>X2.java:57: is the 0th actual parameter of parseInt non-null?</code></li>
<li><code>X3.java:69,70,72,78,105,109,178: is the return value of getY2 non-null?</code></li>
<li><code>X4.java:227,261,309: is the value of field mX1 non-null?</code></li>
<li><code>X5.java:84: is this nullness check useless?</code></li>
<li><code>X6.java:318,361: is the receiver of the call to iterator non-null?</code></li>
<li><code>X7.java:34: is the formal parameter srcRect non-null?</code></li>
</ul>Отмечу, что Kockwork Solo выдал всего 43 предупреждения, связанных с проверкой на null. Подробнее результаты, полученные Julia и Kockwork Solo, сравниваются ниже.<br />
<br />
<h3>Напоследок. Сравнительные тесты</h3>Ну как же без тестов... Многие анализаторы обнаруживают одни и те же типы проблемы в коде. Сравним, кто что находит.<br />
<br />
<h4>Неверное сравнение строк</h4>В Java строки надо сравнивать через equal. Между тем, по старой сишной привычке, можно сравнить строки через != и == и получить ошибку. Этот баг настолько распространен, что его ищут чуть ли не все анализаторы.<br />
<br />
В тестовом приложении оказалось семь таких багов. Вот они:<br />
<code><br />
1. widget_name == ""<br />
2. title != adapter.getItem(i).Title<br />
3. srcStr == "S"<br />
4. m_K != ""<br />
5. contact_name != m_B.getB().GetR(this)<br />
6. contact_name == ""<br />
7. name == "" ? "" : name + ": "<br />
</code><br />
<br />
Вот как справились с поиском этого бага анализаторы кода:<br />
<table border="1"><tr> <th></th> <th>1</th> <th>2</th> <th>3</th> <th>4</th> <th>5</th> <th>6</th> <th>7</th> </tr>
<tr> <th>Kockwork Solo</th> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> </tr>
<tr> <th>CodePro</th> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> </tr>
<tr> <th>FindBugs</th> <td>1</td> <td>0</td> <td>1</td> <td>0</td> <td>1</td> <td>1</td> <td>0</td> </tr>
<tr> <th>PMD</th> <td>1</td> <td>0</td> <td>1</td> <td>1</td> <td>0</td> <td>1</td> <td>1</td> </tr>
<tr> <th>Checkstyle</th> <td>1</td> <td>0</td> <td>1</td> <td>1</td> <td>0</td> <td>1</td> <td>1</td> </tr>
<tr> <th>Julia</th> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> </tr>
</table><br />
<h4>Проблемы со switch</h4>Для switch характерны следующие проблемы:<br />
<ul><li>1. Отсутствие default</li>
<li>2. Пропущен break</li>
<li>3. Switch выполняется по значению перечислимого типа. В "case" используются не все значения, входящие в перечислимый тип.</li>
<li>4. Switch слишком маленький - например, в нем одно или два значения. Можно обойтись if/else.</li>
<li>5. В двух case используется одинаковый код.</li>
</ul>Далеко не всегда эти проблемы являются реальными ошибками. Тем не менее, анализаторы кода как правило считают своим долгом о них сообщить.<br />
<br />
Разные анализаторы работают с разными типами проблем. Итак, вот как отработали анализаторы на тестовом приложении.<br />
<br />
<table border="1"><tr><th></th> <th>1</th> <th>2</th> <th>3</th> <th>4</th> <th>5</th></tr>
<tr><th>Kockwork Solo</th> <th></th> <th></th> <th></th> <th></th> <th></th></tr>
<tr><th>CodePro</th> <th>32</th> <th>4</th> <th>51</th> <th></th> <th></th></tr>
<tr><th>FindBugs</th> <th>18</th> <th>1</th> <th></th> <th></th> <th>0</th></tr>
<tr><th>PMD</th> <th>32</th> <th>8</th> <th></th> <th>9</th> <th></th></tr>
<tr><th>Checkstyle</th> <th>32</th> <th></th> <th></th> <th></th></tr>
<tr><th>Julia</th> <th></th> <th></th> <th></th> <th></th> <th></th></tr>
</table><br />
<h4>NP-анализ</h4>Отсутствие проверки на null и, как результат, "NUll pointer exception" - одна из наиболее частых ошибок при разработке под Android. Другая сторона медали - излишние проверки. <br />
<br />
К слову сказать, FindBugs успешно отлавливает такую ошибку: вначале у объекта вызывается функция, а затем объект проверяется на null. <br />
<br />
Klocwork и Julia "заточены" под Android. И эта "заточенность" проявляется, прежде всего, в возможности NP-анализа. Как я писал вышел, Klocwork для тестового приложения выдал 43 NP-предупреждения, Julia - 187. Сравним, кто что нашел. <br />
<br />
<h5>NP анализ. Файл 1</h5>Проблема 1.1. <code>String action = intent.getAction(); if (action.equals("abc")) {..};</code> Переменная action может быть null. Проблему обнаружили обе программы.<br />
Проблема 1.2. <code>intent.getExtras().getInt(...);</code> Результат getExtras() не проверяется на null. Проблему обнаружили обе программы. <br />
Проблема 1.3. <code>Uri uri = intent.getData(); Integer.parseInt(uri.getQueryParameter("widget_id"))</code>. Оба анализатора сообщили об использовании uri без проверки uri на null. Julia дополнительно сообщила о том, что результаты работы uri.getQueryParameter на null не проверяются и в parseInt может быть передан null.<br />
Проблема 1.4. <code>WidgetContent wc = WidgetFabric.getWidgetContent(); wc.getFlagValue();</code>. Оба анализатора сообщили, что wc может быть null. В реализации getWidgetContent() действительно есть ветка кода, которая возвращает null.<br />
Проблема 1.5. <code>((Singleton)context.getApplicationContext()).getABC().Register();</code> Об этой проблеме сообщила только Julia. Функция getABC() может вернуть null и вызывать Register() нельзя. На самом деле, объект ABC создается в <a href="http://derevyanko.blogspot.com/2010/12/android_26.html">синглетоне, реализованном поверх класса Application.</a> Инициализируется он в момент запуска приложения и null быть не может. Однако, инициализация ABC проводится не в конструкторе синглетона, а в функции onCreate. Анализатор не знает про тонкости создания синглетона и предупреждает об ошибке, которая вряд ли возможна.<br />
<br />
<h5>NP анализ. Файл 2</h5>Проблема 2.1. <code>CheckBoxPreference pref = (CheckBoxPreference)getPreferenceScreen().findPreference("pref_name");</code>. findPreference может возвращать null.<br />
Проблема 2.2. <code>List<skininfo> list_skins = ((Singleton)this.getApplicationContext()).getSkinManager().getListSkins("skin_name");</code> Проблема аналогична 1.5. Функция getSkinManager может возвращать null, но только теоретически.<br />
Проблема 2.3. <code>int len_skins = list_skins.size();</code> Переменная list_skins действительно может быть null.<br />
Проблема 2.4. <code>for (int j = 0; j < len_skins; ++j) { array_skins[j] = list_skins.get(j).SkinTag; }</code> Результаты get не проверяются на null. Такая проверка необходима, если list_skins может содержать null. В приложении такого быть не может, так что проверка не нужна.<br />
Проблема 2.5. <code>String skin_name = (String) p.getEntries()[p.findIndexOfValue(skin_tag)];</code> Результат работы getEntries() не проверяется на null.<br />
Проблема 2.6. <code>CharSequence[] titles = res.getTextArray(idTitles); if (values[0].equals(svalue)) {...};</code> Значение values[0] не проверяется на null.<br />
Julia сообщила обо всех этих проблемах. Klocwork - только о 2.3.<br />
<br />
<h5>NP анализ. Файл 3</h5>Проблема 3.1. <code>m_AsyncTask = new AppInfoReceiverTask(mUtils.getList());</code> Эту ошибка нашла только Julia - переменная mUtils может быть null. На самом деле, mUtils инициализируется в onCreate (речь идет о наследнике Activity) и null быть не может.<br />
Проблема 3.2. <code>protected void onActivityResult(int requestCode, int resultCode, Intent data) { data.getExtras().getString(...);</code> Эту проблему нашли оба анализатора: data.getExtra может вернуть null.<br />
Проблема 3.3. <code>(LauncherContent)WidgetFabric.getWidgetContent()</code> Эту проблему нашел Klocwork. Julia ее пропустила.<br />
Проблема 3.4. <code>SpinnerItem selected_item = (SpinnerItem)spinner.getSelectedItem(); String s = selected_item.Title;</code> Функция getSelectedItem может вернуть null согласно документации. Проблему нашла только Julia<br />
Проблема 3.5. <code>ListView list_view = (ListView) dialog.findViewById(R.id.listview); list_view.setAdapter(new SelectImageAdapter(list_images) );</code> Теоретически, findViewById может вернуть null. На проблему указала только Julia.<br />
Проблема 3.6. <code>private final void remove_shortcut(AppInfo appInfo) { m_List.remove(appInfo); }</code> В функцию может быть передан null, appInfo на null не проверяется. Проблему нашла только Julia.<br />
Проблема 3.7. <code>for (SkinManager.SkinInfo skin : skins) { ... }</code> Переменная skins может содержать null. Проблему нашли оба анализатора.<br />
<br />
Резюме. Julia находит почти все NP-ошибки, которые обнаруживает Klocwork, плюс еще ряд дополнительных. Одновременно, она генерирует довольно много ложных сообщений, которые на практике придется просто игнорировать. Самая большая проблема на мой взгляд - она не учитывает, что в Activity переменные инициализируются в onCreate, а не в конструкторе.<br />
<br />
<b>Update</b>: качество NP-анализа может быть значительно улучшено за счет использования аннотаций @Nullable и @NotNull, см. <a href="http://derevyanko.blogspot.ru/2014/01/android.html">Контрактное программирование под Android</a>.
<br />
<h3>Итоги</h3>В итоге получается следующая картина. В настоящее время существует восемь доступных статических анализаторов кода для Android приложений: Lint, FindBugs, Checkstyle, CodePro, PMD, Motodev App Validator, Klocwork Solo, Julia. Основная специализация у них следующая:<br />
<ul><li>Lint и Motodev App Validator - поиск проблем в ресурсах Android приложения;</li>
<li>FindBugs, Klocwork Solo и Julia - поиск ошибок в коде приложения;</li>
<li>Checkstyle - проверка соблюдения правил оформления кода и стандарта кодирования;</li>
<li>CodePro и PMD - поиск неоптимального кода, проблемного кода, поиск дублей.</li>
</ul>FindBugs и Lint прекрасно работают "с нуля" - настраивать их практически не требуется. CodePro и PMD требуют тонкой настройки. Но если отключить в них ненужные проверки, они приносят реальную пользу. Motodev App Validator и Checkstyle мне показались не слишком полезными - на любителя. Klocwork Solo - находит ряд ошибок, которые не находят другие анализаторы. Он платный, но и триальную версию можно вполне успешно использовать для небольших приложений. Julia находит ряд NP-ошибок, которые не находит Klocwork, но при этом генерирует еще и множество ложных предупреждений (ошибка возможна, но только теоретически). К сожалению, воспользоваться бесплатной версией практически не возможно.
<br/>
<b>Update:</b> Еще один весьма интересный статический анализатор кода - <a href="http://code.google.com/p/checker-framework">Checker Framework</a>. Заслуживает отдельного обзора..Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com2tag:blogger.com,1999:blog-3333619357391010212.post-56656566736289681532011-12-17T13:27:00.000+08:002012-11-30T09:00:24.067+08:00Диалог выбора на Android или контекстное меню с картинками.Задача выбора элемента из списка возникает в Android-приложениях регулярно. В принципе, для выбора элемента можно воспользоваться <a href="http://developer.android.com/guide/topics/ui/menus.html">стандартным контекстным меню</a>. Но у контекстного меню есть ряд ограничений:<br />
<ul><li>В контекстном меню функции вызова (<code>onCreateContextMenu</code>) и обработки результатов(<code>onContextItemSelected</code>) реализуются в Activity.</li>
<li>Нельзя выбрать несколько элементов.</li>
<li>В контекстном меню показывается только название элемента. Картинки не отображаются.</li>
</ul>Наиболее напрягает необходимость реализации функций в Activity. Типичная ситуация - вам нужна вспомогательная функция, которая предлагает пользователю что-нибудь выбрать из списка и затем выполняет действие над выбранным элементом. Такая функция может вызываться из <b>множества</b> разных Activity. И что, мне в каждой Activity реализовывать функции <code>onCreateContextMenu</code> и <code>onContextItemSelected</code>? Так не пойдет. <br />
<br />
Лучше сделать диалог. А поскольку подобные диалоги нужны сплошь и рядом, нужно сделать более-менее универсальный диалог, который подойдет для выбора элементов в большинстве случаев. О реализации такого диалога и пойдет речь.<br />
<a name='more'></a><br />
<h3>Постановка задачи</h3>Итак, задача - создать гибкий диалог выбора элементов из списка. Диалог, который идеально подходит для случаев, когда есть фиксированный список произвольных элементов (не обязательно строк) и нужно дать возможность пользователю выбрать один или несколько элементов. Диалог должен "уметь" показывать название, картинку и описание элемента, причем показ картинки и описания должен быть опциональным (просто потом, что картинка и описание требуются далеко не всегда). <br />
<br />
В сложных случаях подобного диалога, естественно, не хватит. Например, при выборе списка контактов потребуется возможность фильтрации списка, фоновая подгрузка картинок, возможность вызова меню и т.д. Но в простейших случаях его будет за глаза. <br />
<br />
Еще одним требованием к диалогу является возможность передать в него функцию обработчик полученного результата. Таким образом, появится возможность реализовывать функцию обработчик в анонимном классе прямо в точке вызова диалога. <br />
<br />
Ну и последнее требование - простота использования. <br />
<br />
<h3>Реализация UniDialog</h3>Исходные коды получившегося диалога (я его назвал UniDialog) можно <a href="http://code.google.com/p/dvsrc/source/browse/trunk/Android/DemoSelectDialogs/">посмотреть здесь</a>. Реализация включает три файла: <a href="http://code.google.com/p/dvsrc/source/browse/trunk/Android/DemoSelectDialogs/src/com/kbitubit/android/demo/UniDialogMultySelector.java">UniDialogMultySelector</a> - диалог для выбора нескольких элементов, <a href="http://code.google.com/p/dvsrc/source/browse/trunk/Android/DemoSelectDialogs/src/com/kbitubit/android/demo/UniDialogSingleSelector.java">UniDialogSingleSelector</a> - диалог для выбора одного элемента, <a href="http://code.google.com/p/dvsrc/source/browse/trunk/Android/DemoSelectDialogs/src/com/kbitubit/android/demo/UniDialogUtils.java">UniDialogUtils</a> - вспомогательные классы. Кроме того, для работы диалог требует файлы ресурсов: <a href="http://code.google.com/p/dvsrc/source/browse/trunk/Android/DemoSelectDialogs/res/layout/unidialog_selector_row.xml">unidialog_selector_row.xml</a>, <a href="http://code.google.com/p/dvsrc/source/browse/trunk/Android/DemoSelectDialogs/res/layout/unidialog_selector.xml">unidialog_selector.xml</a>, <a href="http://code.google.com/p/dvsrc/source/browse/trunk/Android/DemoSelectDialogs/res/values/colors.xml">colors.xml</a> и <a href="http://code.google.com/p/dvsrc/source/browse/trunk/Android/DemoSelectDialogs/res/drawable/unidialog_listview_background.xml">unidialog_listview_background.xml</a>. <br />
<br />
Далее, опуская детали реализации, рассмотрим несколько примеров вызова диалога.<br />
<h3>Пример 1. Список строк</h3><pre class="java">/** Список строк - элементов списка */
private static String[] items = {"Item 1"
, "Item 2"
, "Item 3"
, "Item 4"};
...
//подготавливаем данные для диалога
DialogConfig dc = new DialogConfig("Main title"
, false //картинки не поддерживаются
, false //описания элементов отсутствуют
);
ArrayList<String> list = new ArrayList<String>();
for (String s: items) {
list.add(s);
}
//вызываем диалог
UniDialogSingleSelector.showDialog(this
, dc //конфигурация диалога
, list //список элементов диалога
, new UniDialogSingleSelector.SingleItemReceiver<String>() {
@Override public void onSelectItem(String selectedItem) {
if (selectedItem == null) {
//пользователь отменил выбор
} else {
//пользователь выбрал selectedItem
}
}
});
</pre>В примере выводится список строк, из которого пользователь должен выбрать одну строку. Код делится на две части: подготовка данных для диалога и вызов диалога. <br />
<br />
Подготовка данных включает создание объекта DialogConfig, задающего конфигурацию диалога: название и два флага, указывающих требуется ли показывать картинки и описания элементов. В нашем случае элементы - это обычные строки, у них нет картинок и описаний, поэтому флаги выставлены в false.<br />
<br />
Диалог вызывается статической функцией <code>UniDialogSingleSelector.showDialog</code>, в которую передается исходная Activity, конфигурация диалога, список элементов и обработчик результатов. Обработчик задается в виде анонимного класса, реализующего функцию onSelectItem. Если пользователь отменил выбор, то в onSelectItem будет передат null. Если пользователь выбрал элемент - то в onSelectItem будет передан выбранный элемент. <br />
<br />
Функция UniDialogSingleSelector.showDialog является параметризированной. Параметром является тип элементов списка.<br />
<br />
<h3>Пример 2. Список элементов с картинками и описанием</h3>Более сложный пример. Введем класс для хранения элементов списка:<br />
<pre class="java">class CustomDataItem implements UniDialogUtils.ItemImageProvider, ItemDescriptionProvider {
public final String Title;
private final Bitmap Image;
public final String Description;
public CustomDataItem(String title, Bitmap image, String description) {
this.Title = title;
this.DrawableId = drawableId;
this.Description = description;
}
@Override public Bitmap getImage() {
return this.Image;
}
@Override public String toString() {
return this.Title;
}
@Override public String getDescription() {
return this.Description;
}
}</pre>Класс реализует два вспомогательных интерфейса - <code>ItemImageProvider</code> и <code>ItemDescriptionProvider</code>. Через эти интерфейсы диалога получает от элементов списка картинки и описания.<br />
<pre class="java">UniDialogUtils.DialogConfig dc = new UniDialogUtils.DialogConfig(items[2]
, true //CustomDataItem supports images
, true //CustomDataItem supports desctiptions
);
//предполагаем, что массивы ITEMS, IMAGES и DESCRIPTIONS
//содержат необходимые названия, картинки и описания
ArrayList<CustomDataItem> list = new ArrayList<CustomDataItem>();
for (int i = 0; i < ITEMS.length; ++i) {
list.add(new CustomDataItem(ITEMS[i], IMAGES[i], DESCRIPTIONS[i]));
}
UniDialogMultySelector.showDialog(this
, dc
, list
, new UniDialogMultySelector.MultyItemsReceiver<CustomDataItem>() {
@Override public void OnSelectItems(List<CustomDataItem> selectedItems) {
if (selectedItems == null) {
//выбор отменен
} else {
//пользователь выбрал элементы
}
}
}
, true //все элементы выделены по умолчанию
);
</pre>Здесь мы используем диалог мульти-выбора - <code>UniDialogMultySelector</code>. Он отличается от <code>UniDialogSingleSelector</code> наличием дополнительного (последнего) параметра в showDialog, задающего набор выбранных по умолчанию элементов. Кроме того, используется другой класс для обработки результатов - MultyItemsReceiver. Этот класс реализует функцию OnSelectItems, принимающую список выбранных элементов или null, если выбор отменен.<br />
<br />
Вместо CustomDataItem можно использовать "стандартный" класс UniDialogUtils.SimplestDataItem, который дополнительно позволяет сохранять для элементов теги. А можно использовать сторонние классы, применяемые в других местах приложения - иногда это оказывается очень удобно. Единственное ограничение - если требуется показывать картинки и/или описания, то классы должны реализовывать один-два дополнительных интерфейса. <br />
<br />
В showDialog последним параметром вместо true/false можно явно передать список элементов, которые должны быть по умолчанию выделены.<br />
<br />
На картинке показан вид получившегося диалога.<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitml-edQqTz_VjzVVXzDn7FenEDfoqjMWumhj_cLgFfVmaJCNIALu0VLCVmSDfSWvFF2EaotkXCbUYOEUxrltzB-sVnX9p3X5xF_DJj0EXR4veXzWAPyJ3aF7K5EMK7-nG8m_uD8a8OG8D/s1600/dialog_screen.png" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="320" width="192" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitml-edQqTz_VjzVVXzDn7FenEDfoqjMWumhj_cLgFfVmaJCNIALu0VLCVmSDfSWvFF2EaotkXCbUYOEUxrltzB-sVnX9p3X5xF_DJj0EXR4veXzWAPyJ3aF7K5EMK7-nG8m_uD8a8OG8D/s320/dialog_screen.png" /></a></div>Редактируя файлы ресурсов, можно подогнать вид диалога под "стандартный" для приложения вид.<br />
<br />
<h3>Заключение</h3>Выше я привел два примера использования диалога. В демонстрационном приложении можно посмотреть и другие примеры. <br />
<br />
Я активно использую данный диалог в своих приложения - удобно. Надеюсь, пригодится кому-нибудь еще. Пожелания и предложения, а так же замечания об ошибках - приветствуются :)<br />
<br />
<a href="http://code.google.com/p/dvsrc/downloads/detail?name=20111217DemoSelectDialog.7z&can=2&q=">Скачать</a> демонстрационный проект и apk. <br />
<br />
<a href="http://code.google.com/p/dvsrc/source/browse/trunk/Android/DemoSelectDialogs/">Просмотреть</a> исходные коды приложения DemoSelectDialog.Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com2tag:blogger.com,1999:blog-3333619357391010212.post-65790777433631532992011-10-28T11:44:00.000+08:002012-02-22T15:23:35.255+08:00Три недели в Као-Лаке.Отдых в Таиланде в октябре 2011.Несколько дней назад вернулся из Таиланда. Отдыхали всей семьей в курортном городе <a href="http://www.ukrest.ru/ru/?m=1023">Као-Лак</a> (Khaolak, 80 км от Пхукета) в течении трех недель. Отдохнули хорошо, хотя и не без проблем.<br />
<br />
Путешествовали мы самостоятельно, без турагентства. На Као-Лак летели через Бангкок. Из Бангкока мы успели уехать до начала наводнения, так что обошлось без экстрима. <br />
<br />
Ребенок у нас еще совсем маленький - менее двух лет. Так что отдых наш был несколько специфичным: пляж, отель, море, прогулки вблизи отеля, никаких экскурсий. Тем не менее, о чем рассказать - есть.<br />
<a name='more'></a><br />
<br />
<h4>Подготовка и первоначальные расходы</h4>Из Красноярска в Бангкок есть прямые регулярные рейсы авиакомпании <a href="http://www.s7.ru/">S7</a>. Билеты покупали за 4 мес. до вылета. Стоимость билета туда-обратно ~23 т.р. на взрослого и 2110 руб на ребенка до 2 лет.<br />
<br />
Из Бангкока на Као-Лак и обратно летели рейсами <a href="http://airasia.com/">AirAsia</a>. Билеты туда и обратно обошлись ~10 т.р.<br />
<br />
Отели бронировали через <a href="http://www.agoda.ru">agoda.ru</a>. Отель в Као-Лаке - <a href="http://www.agoda.ru/asia/thailand/khao_lak_phang_nga/khaolak_laguna_resort.html">Khaolak Laguna Resort</a> (вилла с завтраком, ~44 т.р). Отель в Бангкоке - <a href="http://www.agoda.ru/asia/thailand/bangkok/pratunam_park_hotel.html">Pratunam Park Hotel</a> (~3600 руб). <br />
<br />
Медицинскую страховку оформили в Ингосстрахе - 768 руб при страховой сумме в $15000.<br />
<br />
В процессе подготовки много полезной информации о Таиланде почерпнули <a href="http://www.rino4ka.ru/">здесь</a>.<br />
<br />
<h4>Путешествие с ребенком. Особенности</h4>Мы путешествовали с ребенком первый раз. Естественно, была куча вопросов: как везти коляску, как везти детское питание, сколько одежды брать и т.д.<br />
<br />
По факту, с коляской проблем не возникло вообще. Довозишь ребенка на коляске до входа в самолет. Возле самолета коляску у тебя забирают. Получаешь ее потом вместе с багажом. При регистрации билета нужно сообщить о коляске и получить для нее багажную квитанцию. Интересно, что когда мы летели в Бангкок, у нас коляску не забрали - я пронес ее в самолет как ручную кладь и ехала она на полке над сиденьем.<br />
<br />
Кстати, несмотря на то, что ребенок дома в коляске давно не ездит, в путешествии коляска очень пригодилась. Не представляю, как можно было бы обойтись без нее. Перемещаться приходилось много, ребенка на руках - не натаскаешься. Дождь, ты тащишь кучу чемоданов, а ребенок спит себе в коляске и нет проблем... У нас была коляска <a href="http://krasbaby.ru/item1046.html">Baby Care NEW YORK</a>. Легкая, компактно складывается, отлично амортизирует (ребенка не трясет даже на неровной дороге). Корзина внизу, накидка на ноги и (особенно!) дождевик были незаменимы. Рекомендую.<br />
<br />
Детского питания мы брали не слишком много, благо, ребенок уже давно лопает обычную еду. Часть питания мы сложили в чемоданы, часть - в ручную кладь. В ручную кладь питание брать нужно обязательно, т.к. всегда есть риск, что багаж потеряется, а оставить ребенка без еды никак нельзя. Когда мы летели в Бангкок, ручную кладь не взвешивали. А вот когда летели обратно - взвешивали, так что мне пришлось объяснять, что сумка такая тяжелея именно из-за детского питания. Стеклянные банки с питанием, сок в тетрапаках, маленькие бутылочки с водой - с ребенком пропускают все.<br />
<br />
Еще одна проблема - сколько брать с собой одежды? Мы приехали в Као-Лак в октябре, до начала сезона, и нам довелось увидеть, что такое тропические ливни. Влажность - чудовищная. Белье сохнет неделю. Так что либо брать для ребенка достаточное количество одежды, либо докупать одежду на месте. У влажности есть еще один неприятный эффект - когда собираешься ехать домой, вся одежда влажная.. и гораздо более тяжелая, чем обычно. <br />
<br />
Имеет смысл взять ребенку теплую кофточку. Во многих магазинах, ресторанах, в McDonalds работают кондиционеры - там достаточно прохладно, ребенок без кофточки может простудиться. Не повредит и рубашка с длинным рукавом, чтобы можно было закрыть ребенку руки от солнца. Впрочем, мы такую рубашку без проблем купили на месте - легкую, южную, у нас в Сибири таких не продают.<br />
<br />
Во всех ресторанах есть детские стульчики. В отелях предлагают (бесплатно) детские кроватки. Правда кроватки а-ля манеж, совершенно не удобные.<br />
<br />
Особенность Тайланда - феноменальный интерес к европейским детям. Куда бы ты не шел - куча народа хочет подойти к ребенку, заговорить с ним, притронуться к нему. Мы научили ребенка махать ручкой таким надоедливым типам, это нас и спасало. Помашет - таец заулыбается, покудахчет, помашет в ответ и пойдет дальше.<br />
<br />
<h4>Khao Lak</h4>Местечко Као-Лак - это, по большому счету, одна единственная улица, расположенная между горой и морем. Весь берег моря занят отелями. На горе - лес. На улице - куча ресторанов, магазинов (с одеждой, сувенирами и пляжными принадлежностями) оптик, салонов массажа, туристических агентств (предлагающих разнообразные экскурсии) и великих портных, готовых вам за небольшие деньги сшить костюм от Армани. <br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOEitP3M1WW8RMhH0elWI0XM6N7kgx5ZVy1rbN2Gfgz3QSvychA0tziB_kVm1Dd096kpAqHv4WfQtqcFQhL45tHpLrjnjGEH13demVDd90zN3AKqMMNM_9vWs_GlwFGu8STVyqCjXwS4Hx/s1600/kl1.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="240" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOEitP3M1WW8RMhH0elWI0XM6N7kgx5ZVy1rbN2Gfgz3QSvychA0tziB_kVm1Dd096kpAqHv4WfQtqcFQhL45tHpLrjnjGEH13demVDd90zN3AKqMMNM_9vWs_GlwFGu8STVyqCjXwS4Hx/s320/kl1.jpg" /></a></div>
<br />
Есть один большой супермаркет Nang Thong, плюс пара мелких. Имеется Макдональдс. Три аптеки на противоположной от Макдональдса стороне. <br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhJSO5PllDRkrNtyY8UjqnW-6oy2B40VKYLsxwV_s2RRf9qsYVvwHSTXSWTK2gO0zifcJEFLVicSvqQVw-r53f-uYX_t-EFCR4CfKjlQp6p0PE6Vzd-0MdlFOIoBW3tOZlnBRKhQ5VhlvuM/s1600/kl4.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="320" width="240" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhJSO5PllDRkrNtyY8UjqnW-6oy2B40VKYLsxwV_s2RRf9qsYVvwHSTXSWTK2gO0zifcJEFLVicSvqQVw-r53f-uYX_t-EFCR4CfKjlQp6p0PE6Vzd-0MdlFOIoBW3tOZlnBRKhQ5VhlvuM/s320/kl4.jpg" /></a></div>
<br />
Фрукты продаются на рынке, который работает три раза в неделю - понедельник, среда, суббота. Рынок расположен километрах в 5 от отеля, туда надо ехать на минибасе или такси. В принципе и пешком дойти не сложно, но на коляске туда не проехать - тротуары кончаются возле Nang Thong, дальше только шоссе, дорожки для пешеходов не предусмотрено. Стоимость минибаса до рынка 100 бат, такси - 150 бат (можно и за 100-120 договориться). <br />
<br />
Спиртное можно покупать в Nang Thong - дешевле чем у нас в России раза в 2. Бутылка 0.7 л London Dry Gin стоит ~550 бат, капитанского джина - 350 бат. Стоимость банки местного пива 0.33 на уровне 35-40 бат.<br />
<br />
<h4>Погода</h4>Мы отдыхали в Тайланде со 2 по 22 октября. А сезон там начинается в ноябре..<br />
<br />
В день приезда в Као-Лаке лили ливни. Льет час, перерыв на 10-15 минут и опять льет. Мы поначалу расстроились - ничего себе отдыхать приехали... И не только мы. Напротив немцы заселились - глаза у них натурально квадратные были.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLkStZeDquYqF6ubv6Zl0CHr4s_pBBi6nvM8JQhHb9V90actJkWzKmK4R0XBl86ru68qABGecgvS-IWiVoz5YFV1qkbeB6ZsCuoWpaNSDTxGM-W_Gg9U47TwTd5gFQQBrSqFq3dvACEoKI/s1600/kl6.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="240" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLkStZeDquYqF6ubv6Zl0CHr4s_pBBi6nvM8JQhHb9V90actJkWzKmK4R0XBl86ru68qABGecgvS-IWiVoz5YFV1qkbeB6ZsCuoWpaNSDTxGM-W_Gg9U47TwTd5gFQQBrSqFq3dvACEoKI/s320/kl6.jpg" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi16I_peivfeOevDV4XIhW34T-FqQhBXobYOmWlNUs7VtsSTI98vVsjLTtokJYqxn-D0ztoE1hqcfXfN2SNEcztD4ygFMY1OGMCvtTPid8jNrSvsmCFpHrDmhs-P0ZUteJpMgrtw6OvH52J/s1600/k7.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="240" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi16I_peivfeOevDV4XIhW34T-FqQhBXobYOmWlNUs7VtsSTI98vVsjLTtokJYqxn-D0ztoE1hqcfXfN2SNEcztD4ygFMY1OGMCvtTPid8jNrSvsmCFpHrDmhs-P0ZUteJpMgrtw6OvH52J/s320/k7.jpg" /></a></div>
<br />
Впрочем, погода очень быстро наладилась. На следующей день дождь стал идти реже, а через пару дней появилось солнце. И стало так нещадно жарить, что мы с тоской вспоминали непогоду и очень радовались редким дождичкам. <br />
<br />
Первоначально пасмурная погода помогла нам адаптироваться к местному солнцу и не сгореть. Если бы мы приехали сразу в солнечную погоду - сгорели бы обязательно. С 11:00 до 15:00 носу из отеля не высунуть, такое пекло..<br />
<br />
<h4>Отель Khaolak Laguna Resort</h4>От отеля осталось двойственное впечатление: есть как плюсы, так и минусы.<br />
<br />
В отеле, в основном, живут немцы. Русских за все время нашего пребывания была лишь одна пара. Попадались так же шведы, японцы.. <br />
<br />
Отель расположен достаточно удачно. Если встать лицом к отелю, то слева дорога поднимается в гору, справа, метрах в 200 от отеля, начинаются магазины. Отели слева уже располагаются на горе и менее удобны. <br />
<br />
Прямо напротив Khaolak Laguna Resort расположено несколько ресторанов - Gold Elephant, Sea & Sand 2, Everyday, - в любом из них можно вкусно поесть за сумму в два раза меньшую, чем в отеле (500-700 бат против 1000-1300 бат). Нам особенно понравился Sea & Sand 2, в котором есть не только тайская еда, но и европейские блюда. <br />
<br />
В самом Khaolak Laguna Resort куча лестниц. Номера расположены у моря, ресепшен и ресторан - на пригорке, так что на коляске на ресепшен и в ресторан не заехать. В остальном перемещение по отелю на коляске проблем не вызывает - предусмотрены съезды (персонал перемещается по территории отеля на мотоциклах).<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi35ypWMLAG1ZzeQpnnKqwxX6uUljtFm3PsU_KATAezyuDYFeaewb0eAiexa8KS38NyNEdv5hMCGkeOOogkNrpA2ucIjx9y_dHHv5R1eYszM1oyQfNSQZasxTlTQ_KuKV20105cSlBR7Whg/s1600/kl5.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="240" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi35ypWMLAG1ZzeQpnnKqwxX6uUljtFm3PsU_KATAezyuDYFeaewb0eAiexa8KS38NyNEdv5hMCGkeOOogkNrpA2ucIjx9y_dHHv5R1eYszM1oyQfNSQZasxTlTQ_KuKV20105cSlBR7Whg/s320/kl5.jpg" /></a></div>
<br />
Ресторан в отеле хороший. Завтраки вкусные, более менее разнообразные. Персонал доброжелательный, повар веселый, здоровается с приезжающими на их языке (по русски не плохо говорит "Доброе утро" и "Спасибо"). <br />
<br />
Море было чистое (но не прозрачное), вода очень теплая. Большую часть времени на море были волны, иногда довольно большие - накупались всласть, гораздо интереснее, чем с спокойное море. Никаких медуз, ежей и т.д. - не встречали. <br />
<br />
Пляж отличный, песок мелкий. Народа на пляже нет - загорают все на лежаках, в отеле. Лежаки выше всяких похвал. На лежаках лежат матрасы. Приходишь - тебе тут же приносят махровую простынь на матрас, очень удобно. <br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgf_11sLCTCiJtPeaKDvMg2lgYEG-pCB1ekZb_vlSojJxi-W6hpKCghuA5T4rzFgZVt5QBbSRTpbneYEbbVfW_L_sM_tsvkGH-T0uJgP6cmjT0tudx1JHCsM4UiT2MRJSnJUBwPu7RbJfgf/s1600/kl2.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="240" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgf_11sLCTCiJtPeaKDvMg2lgYEG-pCB1ekZb_vlSojJxi-W6hpKCghuA5T4rzFgZVt5QBbSRTpbneYEbbVfW_L_sM_tsvkGH-T0uJgP6cmjT0tudx1JHCsM4UiT2MRJSnJUBwPu7RbJfgf/s320/kl2.jpg" /></a></div>
<br />
Бассейны в отеле отличные. Два взрослых бассейна, два детских - все с пресной водой. Во взрослых бассейнах есть джакузи. Бассейны чистые, вода проточная.<br />
<br />
Сам отель представляет из себя парк флоры и фауны. Вокруг домиков рассажено множество разнообразных фикусуов, кротонов, пальм и т.д. По всему отелю - бассейны с рыбками. По территории бассейна протекает река, в которой так же плавает множество рыб (по виду - здоровенные такие караси.. но кухни в домиках не предусмотрено). Есть несколько прудов с лягушками. Мелких ящериц вокруг - не счесть. Встречаются и здоровенные ящерицы, сантиметров 60 длинной, но их рассмотреть сложно, близко они не подпускают - убегают. В отеле "проживает" множество птичек. Народ рыбок и птичек активно кормит - в основном, вкусным белым хлебом, который дают на завтраке. <br />
<br />
Теперь о плохом. Самая большая проблема, на мой взгляд, это неудачный дизайн домиков, который портит все впечатление от отеля.<br />
<br />
Виллы натыканы очень близко друг к другу. Три стены в комнате - это окна и двери, так что вечером, когда включаешь свет - сидишь как в телевизоре. Надо закрывать шторы. Шторы римские, окон - штук 10. Не комфортно совершенно.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikJKhW_h7zNlqi_wPCxDdA-0IvCv9GVmHOkU74skxUgNIhc2Ongc-CgKyEiq4dihiRFftVSYG1Vzhb6z6p2wbKI18-kv8U-U4M9UWaBcBkY9K5Z8-FIrFH0QNkxGp_8j6g-jfbPt8Ulp9L/s1600/kl3.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="240" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikJKhW_h7zNlqi_wPCxDdA-0IvCv9GVmHOkU74skxUgNIhc2Ongc-CgKyEiq4dihiRFftVSYG1Vzhb6z6p2wbKI18-kv8U-U4M9UWaBcBkY9K5Z8-FIrFH0QNkxGp_8j6g-jfbPt8Ulp9L/s320/kl3.jpg" /></a></div>
<br />
Виллы тесные. Комната примерно 4 x 6 метров, в ней стоит вся мебель - кровать, диван, два кресла, столик с тумбочками и холодильником, телевизор на столике, маленький столик, пара прикроватных тумбочек. В принципе, с теснотой можно было бы смириться, если бы ни кондиционер.<br />
<br />
Кондиционер у них расположен прямо над кроватью. Воздух идет над кроватью, утыкается в стену и далее идет прямиком в кровать. Включил кондиционер - спать не возможно, сквозняк. У кондиционера три режима вентилятора. Даже если выставить минимальный уровень, сквозняк слишком сильный, чтобы можно было оставить кондишен включенным на ночь. Мы купили в супермаркете лист пластика, сделали из него короб, закрыли кондиционер и направили воздух прочь от кровати. Стало гораздо лучше. Но комната слишком тесная, потоки воздуха над кроватью все равно гуляют. Для взрослого может и ничего, для ребенка - недопустимо. <br />
<br />
В итоге, пришлось спать с выключенным кондиционером. Открывали окна на сквозняк, периодически ночью включали кондиционер, когда совсем туго становилось... Мало того, что жарко, так еще и москиты одолели. Не знаю - климат у них такой, или пруды с рыбками и лягушками дают о себе знать, - но москитов там дофига. Купленный аналог фумитокса не помог. Пришлось покупать в аптеке крем OFF от москитов, мазать себя и ребенка и спать намазанными. Так и жили.<br />
<br />
Забавно, что в более дешевых номерах (не виллах) кондиционеры расположены не над кроватями. Точнее, в половине номеров - не над кроватями, в другой половине - над кроватями. Тот, кто проектировал номера, явно не задумывался об этой проблеме.<br />
<br />
Еще пара мелких недостатков. В туалете нет вентиляции. Тусклое освещение. Крайне неудобная входная дверь. <br />
<br />
В комнате есть чайник и фен. Ежедневно выдают бесплатно две бутылки питьевой воды. Сейфа нет, но это не проблема - бесплатный сейф есть на ресепшене.<br />
<br />
Когда я резервировал отель, то выбирал отель с бесплатным Wi-Fi. В Khaolak Laguna Resort WI-FI в номерах практически не ловится (очень редко, в шкафу, в дождливую погоду, можно ненадолго подсоединиться). WI-FI работает на ресепшене, в ресторане, около бассейна и на море. Когда я пришел на ресепшен с жалобой на неработающий WI-FI, мне предложили подключить бесплатную кабельную сеть. Но для смартфонов кабельная сеть не актуальна.<br />
<br />
<h4>Страховой случай</h4>Ребенок у нас таки простыл от кондишена и сквозняков. Как-то ночью, примерно через две недели после начала путешествия, у ребенка поднялась температура 39. Мы были вынуждены позвонить в Москву, в Ингосстрах. Натолкнулись неожиданно на проблемы с билайном - звонок обошелся крайне дорого, утекло ~1500 руб. Плюс дозвониться не могли - какое-то время не было соединения. <br />
<br />
В остальном все прошло гладко. В 4 утра позвонили. В 7:30 приехал местный доктор, который улыбался так же широко, как доктор Ливси. Измерил ребенку температуру дистанционного, послушал ребенка, выяснил какие лекарства мы давали. Объяснил нам, что у ребенка кашель, бронхит и инфекция, что бывает очень часто у таких маленьких детей в таком климате. Выдал лекарства - антибиотик, жаропонижающее и два лекарства от кашля. Взял копию паспорта ребенка и листок страхового полиса. Выставил счет на 6 тыс. бат, я его подписал. Сказал, что температура должна упасть в течении 3 дней, если не упадет, снова звонить в Москву и вызывать его повторно.<br />
<br />
Лекарства помогли, ребенок быстро выздоровел. Кашель окончательно прошел примерно через неделю. <br />
<br />
<h4>Перелет на Пхукет и обратно на Air Asia</h4>Последний раз я летал AirAsia 5 лет назад. С тех пор они внедрили ряд усовершенствований.<br />
<br />
Во-первых, теперь при покупке билета явно указываешь, <a href="http://www.airasia.com/my/en/flightinfo/supersizesave.page">сколько багажа с собой будет</a>. Довольно удобно, можно брать до 30 кг на человека. <br />
<br />
При покупке билета желательно указать правильный вес чемоданов. Если будет перевес, будешь доплачивать. Причем у них на сайте явно указано, что в аэропорту цены гораздо выше, чем при оплате багажа заранее. <br />
<br />
Мы на это налетели. Когда мы летели в Пхукет, у нас был перевес 15 кг. Я доплатил ~500 бат - приемлемо. Когда мы летели обратно, у нас был перевес 13 кг. С нас запросили ~5000 бат. Естественно, мы отказались платить. Вместо этого - перепаковали чемодан, кое-что выкинули, сделали себе вторую сумку в ручную кладь. Улетели так, ничего не доплачивая. В AirAsia есть требования к ручной клади - ограничения на килограммы (7 кг) и на размер (56cm X 36cm X 23cm). Но по факту ее никто не взвешивает, и народ тащит внутрь довольно здоровые сумки. <br />
<br />
Во-вторых, теперь можно регистрироваться на рейс самостоятельно - через Web Check In. По сути, сам себе распечатываешь посадочный талон. Если регистрироваться в аэропорту, то будешь платить дополнительные деньги. <br />
<br />
<h4>Банког, Pratunum Park Hotel</h4>Последние два дня нашего путешествия мы провели в Банкоге. Это было 21-22 октября, наводнение в Банкоге еще не началось. По улицам были разложены баррикады из мешков с песком - вот и все, что свидетельствовало о чрезвычайной ситуации. Рынки и магазины работали на полную катушку.<br />
<br />
Мы остановились в отеле Pratunum Park Hotel. Отель понравился. Персонал приветливый, кондиционер не над кроватью:), питьевую воду дают, в комнате есть сейф. На мой взгляд - нормальный трехзвездочный отель, само то, чтобы перекантоваться пару дней. Бесплатный Wi-Fi уверенно работает в номерах и на ресепшене. Сеть закрытая, нужно брать у администратора пароли.<br />
<br />
Добираться до отеля просто. В аэропорту спускаешься на нулевой этаж, садишься на City Line. Доезжаешь до станции Ratchaprarop. Если смотреть со станции, отель находится за близстоящим небоскребом - примерно в 400 м от станции. Задача - обойти небоскреб.<br />
<br />
Спускаемся со станции, встаем лицом к дороге. Идем направо, доходим до первого широкого (подворотни не считаются) поворота направо - метров 50-100. Поворачиваем. Идем до первого поворота налево - тоже метров 100 (справа увидите несколько индийских ресторанов). Поворачиваем. За спиной небоскреб, впереди небоскреб, вокруг - рынок... Проходим метров 50, сворачиваем в первый поворот направо. Именно на этой улице стоит отель, справа. До него метров 100-150.<br />
<br />
Мы поначалу немножко поплутали там по улицам. Спустились со станции на улицу - а там не город, а рынок. Тротуары метр шириной, и на этом метре еще три лотка стоит со всякой всячиной. С коляской и чемоданами передвигаться очень не просто... Бангкок - это трущебы с небоскребами. <br />
<br />
<h4>Шоппинг в Бангкоке</h4>Мы сходили в местный "всемирный торговый центр" <a href="http://ptnpark.com/ptn_map_big.gif">(Central World Plazza)</a>, расположенный в 15 минутах хотьбы от отеля. Там два торговых центра, расположенных напротив друг друга. Нам больше всего понравился Setan. Там можно купить все что угодно. Забавно, цены на сувениры в таком огромном торговом центре ничуть не выше, чем в магазинчиках в Као-Лаке. <br />
<br />
<h4>Duty Free в аэропорте</h4>Богатый Duty Free, есть что выбрать. Спиртное стоит так же как в Као-Лаке. Хороший выбор парфюма. Вот только курс доллара там безумно невыгодный. Если вы планируете что-нибудь покупать в duty free, лучше обменять доллары на баты в обменнике и тратить баты. <br />
<br />
<h4>Общие впечатления</h4>Очень понравились море и пустынный пляж в Као-Лак. Собственно, за этим мы туда и ехали. Огорчила проблема с кондиционером в достаточно недешевом отеле. Не ожидали. О Бангкоке сложилось впечатление как о большом рынке. В целом, Тайланд показался гораздо менее цивилизованным, чем та же Малайзия.
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj1odKt-DkpQznHcOgu99h-OPuU27gEx64i16K70FnphZknGTCvv62b0ZjEVnRSXVChg1vqatJoS0nGnX6tkK39k1I1QmJDAaNymAWruaxJR7JDdCxKcE5iDkn5JOGpMIg2QAEtFPrenuaW/s1600/k8.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="240" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj1odKt-DkpQznHcOgu99h-OPuU27gEx64i16K70FnphZknGTCvv62b0ZjEVnRSXVChg1vqatJoS0nGnX6tkK39k1I1QmJDAaNymAWruaxJR7JDdCxKcE5iDkn5JOGpMIg2QAEtFPrenuaW/s320/k8.jpg" /></a></div>Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com5tag:blogger.com,1999:blog-3333619357391010212.post-76151096314422131632011-09-15T22:01:00.001+08:002011-11-09T18:15:51.485+08:00Книга Version Control by Example в свободном доступе.Eric Sink <a href="http://www.ericsink.com/vcbe/index.html">раздает</a> на своем сайте свою новую книжку, посвященную системам контроля верстки: Version Control by Example (SVN, GIT, Mercurial, Veracity). Раздает бесплатно, причем можно даже заказать в <b>бумажном</b> виде. <br />
<br />
Книжка интересная, так что заказал. Бумажный вариант читать все-таки на порядок удобнее, чем PDF.<br />
<br />
<b>Update 9.11.2011</b> Пришла книжка. Почитаем...Виктор Деревянкоhttp://www.blogger.com/profile/05900550318230375812noreply@blogger.com1