воскресенье, 16 марта 2014 г.

Руководство. Как использовать iOS NSURLConnection на примере

Перевод http://codewithchris.com/tutorial-how-to-use-ios-nsurlconnection-by-example/

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

Есть несколько способов, которыми можно получить данные в вашем iPhone приложении и много сетевых библиотек, которые можно интегрировать в проект, но по прежнему самой актуальной является класс NSURLConnection, т.к. вероятнее всего многие сторонние библиотеки используют в качестве основы именно его.

1. Пример асинхронного запроса


Чтобы загрузить данные, мы должны сделать три шага:

1. Наш класс должен соответствовать протоколу NSURLConnectionDelegate и содержать переменную для хранения ответа сервера
2. Реализовать методы протокола NSURLConnectionDelegate
3. Создать экземпляр NSURLRequest и NSURLConnection для отправки запроса

Идея в том, что в шаге 3 вы отправляете запрос и собираетесь получать уведомления о некоторых событиях, которые могут произойти при получении ответа. Обработка событий происходит в методах, объявленных и реализованных в шаге 2. На шаге 1 вы сообщаете, что хотите обрабатывать события NSURLConnection.

Шаг 1. 


В классе где вы собираетесь использовать NSURLConnection укажите в заголовочном файле что будет использоваться протокол NSURLConnectionDelegate. Также объявите переменную для хранения ответа сервера.
 @interface ViewController : UIViewController<NSURLConnectionDelegate>  
 {  
   NSMutableData *_responseData;  
 }  

Шаг 2

Далее пишем методы протокола NSURLConnection. В комментариях к коду, вы можете увидеть, для чего используется каждый метод протокола:

 #pragma mark NSURLConnection Delegate Methods  
 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {  
   // Ответ был получен, здесь мы инициализируем переменную которую вы создали
   // и в которую мы позже добавим данные в методе didReceiveData
   // Кроме того этот метод вызывается каждый раз при редиректе для переинициализации   
   // переменных  
   _responseData = [[NSMutableData alloc] init];  
 }  
 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {  
   // Добавить новые данные в переменную которую вы объявили  
   [_responseData appendData:data];  
 }  
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection  
          willCacheResponse:(NSCachedURLResponse*)cachedResponse {  
   // Возвращает nil чтобы показать что нет необходимости кэшировать ответ  
   return nil;  
 }  
 - (void)connectionDidFinishLoading:(NSURLConnection *)connection {  
   // Запрос выполнени и данные были получены
   // Здесь можно обработать вашу объявленную переменную
 }  
 - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {  
   // По каким-то причинам запрос не выполнен  
   // Нужно проверить переменную error с кодом ошибки
 }  

Шаг 3

Теперь вы готовы выполнить асинхронный запрос. Создайте экземпляр NSURLRequest, установите ему URL. Затем создайте экземпляр NSURLConnection и вызовите метод initWithRequest, используя в качестве параметра NSURLRequest:


 // Создать запрос.  
 NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://google.com"]];  
 // Создать соединение и послать запрос  
 NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];  

Шаг 4

Что должно произойти когда ваш запрос отправлен в шаге 3? Первым будет вызван connection:didReceiveResponse показывая, что пришел ответ от сервера. Далее несколько раз вызывается connection:didReceiveData, здесь добавляются последние данные в переменную. И наконец, срабатывает connectionDidFinishLoading, где можно обработать переменную.

Если вы хотите сделать запрос синхронным, вы можете использовать метод
sendSynchronousRequest NSURLConnection

2. Пример синхронного запроса

Синхронный запрос выглядит следующим образом:
 // Отправка синхронного запроса  
 NSURLRequest * urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://google.com"]];  
 NSURLResponse * response = nil;  
 NSError * error = nil;  
 NSData * data = [NSURLConnection sendSynchronousRequest:urlRequest  
                      returningResponse:&response  
                            error:&error];  
 if (error == nil)  
 {  
   // Обрабатываем здесь данные  
 }  

По прежнему необходима NSURLRequest. Переменные NSURLResponse и NSError   передаются в метод sendSynchronousReqeust и когда он возвращает данные, они будут содержать поток данных и ошибки (если таковые имеются). Если вам нужно проверить на такие вещи как код ответа, вы можете сделать это при помощи переменной "response" из строки 6.

Запрос из строки 5 выполняется синхронно, результаты присваиваются переменной NSData. Перед тем как обрабатывать ответ, необходимо проверить error на отсутствие ошибок (эквивалентность nil).

Синхронный или асинхронный?


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

3. Отправка данных POST


Для отправки данных методом POST, всё что вам нужно сделать, это использовать NSURLRequest как мы делали раньше, но на этот раз нужно установить свойство HTTPMethod в значение POST и установить отправляемые поля. Затем NSURLConnection устанавливается как обычно с разницей лишь в том, что используется экземпляр
NSMutableURLRequest, т.к. его свойства устанавливаются после инициализации.


 // Создаем запрос  
 NSMutableURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://google.com"]];  
 // Устанавливаем метод в POST  
 request.HTTPMethod = @"POST";  
 // Устанавливаем поля заголовка  
 [request setValue:@"application/xml; charset=utf-8" forHTTPHeaderField:@"Content-Type"];  
 // Преобразоване ваших данный и установка свойства HTTPBody  вашего запроса   
 NSString *stringData = @"some data";  
 NSData *requestBodyData = [stringData dataUsingEncoding:NSUTF8StringEncoding];  
 request.HTTPBody = requestBodyData;  
 // Создание url соединения и отправка запроса  
 NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];  


4. Пример установки таймаута соединения

 Если вы хотите установить таймаут ответа от сервера, вы должны установить свойство timeoutInterval  объекта NSURLRequest. Если вы получили сообщение об ошибке в XCode, убедитесь что используете экземпляр NSMutableURLRequest, т.к. NSURLRequest не может быть изменен


5. Политика кэширования

Если вы не определили политику кэширования в вашем NSURLRequest, он будет пытаться определить их по своему состоянию. Например, с HTTP он будет смотреть на заголовки ответов и пытаться следовать их указаниям о кэшировании. Другие примеры и информация о кэшировании в NSURLConnection  можно найти в посте caching problems with NSURLConnection.


Видеоурок 1 [ENG]:

среда, 12 марта 2014 г.

Little Fluffy Location Library

Задался поиском энергосберегающей технологии фонового определения координат из сервиса и нашел небольшую библиотечку.
Вебсайт автора http://www.littlefluffytoys.mobi/

Энергосберегающее определение местоположения для Android, использующая при возможности Google Play Location.

Описание

Поместите эту небольшую библиотеку в ваше Android приложение, поправьте манифест, инициализируйте её в методе onCreate ... и пользуйтесь!


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

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

Базируется на принципах и использует адаптированный код из android-protips-location by Reto Meier, из его поста A Deep Dive Into Location.

Требуется Android 2.1 и выше. Лучше всего работает с 2.2 и выше.

Теперь, по возможности, работает с закрытым исходным кодом и авторскими правами Google Play Services (GMS) объединенного провайдера местонахождения. Если это не так (например, на Kindle Fire, Nokia X) используется Location API Android Open Source Project.

Библиотека работает с помощью пассивного Location Listener (возможно только с Android 2.2 и выше, именно поэтому она лучше всего работает с ним), который слушает обновления местоположения по запросу других приложений на вашем телефоне.

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

Особенности:

  • обновление местоположения примерно каждые 15 минут (если новые обновления данных о местоположении найдены)
  • если нет обновления данных о местоположении в течении часа, обновление происходит принудительно
  • по необходимости, при каждом обновлении данных о местоположении выполняется вещание
  • конфигурируемые частота регулярных трансляций (по умолчанию 15 минут) и принудительного обновления (по умолчанию - 60 минут)
  • принудительная функция обновления
  • возможность получить последнее известное положение
  • выбор наиболее точного провайдера местоположения из возможных
  • абстрагирует сложности Google Play Services по сравнению с Android Open Source
  • необязательный debug output

С чего начать

Для использования библиотеки, добавьте в ваш проект файл littlefluffylocationlibrary.jar как external JAR. В Eclipse, можно просто создать в проекте папку libs и положить файл туда, обновить проект, правым кликом на файле .jar выбрать в меню "Add to build path".
Далее, отредактируйте ваш файл manifest, добавив в него следующее:


 ...  
 <manifest {...}>  
 ...  
  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />  
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />  
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />  
  <uses-feature android:name="android.hardware.location" android:required="true" />  
  <uses-feature android:name="android.hardware.location.gps" android:required="false" />  
  ...  
  <application {...}>  
   ...  
   <service android:name="com.littlefluffytoys.littlefluffylocationlibrary.LocationBroadcastService" />  
   <receiver android:name="com.littlefluffytoys.littlefluffylocationlibrary.StartupBroadcastReceiver" android:exported="true">  
    <intent-filter>  
     <action android:name="android.intent.action.BOOT_COMPLETED" />   
    </intent-filter>   
   </receiver>  
   <receiver android:name="com.littlefluffytoys.littlefluffylocationlibrary.PassiveLocationChangedReceiver" android:exported="true" />  
   ...  
   <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version"/>  
  </application>  
  ...  
 </manifest>  

Внутри метода OnCreate() поместите вызов
 LocationLibrary.initialiseLibrary (getBaseContext (), "com.your.package.name");   


заменив имя пакета на ваш. Здесь также нужно обрабатывать исключение UnsupportedOperationException который указывает, что устройство не имеет провайдеров местоположения.

Теперь мы готовы использовать библиотеку!


Приложение транслирует действие com.your.package.name.littlefluffylocationlibrary.LOCATION_CHANGED с сериализованным объектом типа LocationInfo от com.littlefluffytoys.littlefluffylocationlibrary.LocationInfo которые содержат параметры местоположения. Т.е. вы должны настроить broadcast receiver, который будет слушать это действие. Такая настройка выходит за рамки этой документации, но вы можете скачать пример приложения и посмотреть там как это сделать.

Дополнительная информация

Режим отладки


Отладка по умолчанию отключена, чтобы ее включить, добавьте при инициализации:
 LocationLibrary.showDebugOutput();  

LocationLibraryConstants

Здесь содержится несколько полезных констант и функций:
 public static final long DEFAULT_ALARM_FREQUENCY = AlarmManager.INTERVAL_FIFTEEN_MINUTES;  
 public static final int DEFAULT_MAXIMUM_LOCATION_AGE = (int) AlarmManager.INTERVAL_HOUR;  
 public static final String LOCATION_BROADCAST_EXTRA_LOCATIONINFO = "com.littlefluffytoys.littlefluffylocationlibrary.LocationInfo";  
 public static String getLocationChangedPeriodicBroadcastAction()...  
 public static String getLocationChangedTickerBroadcastAction()...  


Перегруженные методы initialiseLibrary

Вы можете использовать любой из перегруженных методов initialiseLibrary, если вам требуется тонкая настройка параметров. Например, во время тестирования, вы можете установить alarmFrequency 60000 (1 минута в миллисекундах ) и locationMaximumAge в 120000 ( 2 минуты в миллисекундах).

Следующие параметры доступны через различные методы:

alarmFrequency: Как часто транслировать обновление местоположения в миллисекундах, если оно было получено. Для энергосбережения батареи, это должно быть одним из доступных неточных интервалов повторяемости, распознаваемых AlarmManager.setInexactRepeating(int, long, long, PendingIntent). Вам не запрещается использовать любое другого значение, но в этом случае AlarmManager  Android, использует setRepeating вместо setInexactRepeating, и это приводит к ухудшению работы от батареи. По умолчанию используется AlarmManager.INTERVAL_FIFTEEN_MINUTES.

locationMaximumAge: Максимальный таймаут обновления местоположения. Если при срабатывании события местоположение старше этого возраста, будет произведено обновление местоположения. По умолчанию используется AlarmManager.INTERVAL_HOUR.

broadcastEveryLocationUpdate: Если параметр TRUE, в дополнение к трансляции периодических обновлений, передается информация о всех обновлениях местоположения, как он будет найден, используя интент com.your.package.name.littlefluffylocationlibrary.LOCATION_CHANGED_TICK. По умолчанию FALSE.

broadcastPrefix: Настоятельно рекомендуется обеспечить уникальное значение для этого параметра, например по имени вашего пакета. В более ранней версии Little Fluffy Location Library, broadcast, чтобы показать изменение местоположения всегда был тем же - в частности, com.littlefluffytoys.littlefluffylocationlibrary.LOCATION_CHANGED (и LOCATION_CHANGED_TICK , если broadcastEveryLocationUpdate TRUE ).  
 Это означает, что если несколько приложений с использованием библиотеки были установлены на устройстве, каждое приложение будет принимать broadcast, посланный любому из приложений. На момент написания статьи, очень популярное приложение Groupon использует библиотеку и отправляет эту постоянную трансляцию, так что это важный вопрос. Таким образом, для того, чтобы ваше приложение реагировало только на broadcast из вашего приложения, а не дублировало передачи из других приложений, пожалуйста, сделайте уникальное значение для этого параметра. Однако, если вы должны поддерживать обратную совместимость с предыдущей версией этой библиотеки , и вы по какой-то причине не в состоянии переработать свое приложение , просто используйте com.littlefluffytoys для этого параметра.

Доступ к последнему местоположению

Чтобы получить последнюю информацию о местоположении, не дожидаясь трансляции, просто создайте объект LocationInfo: 
 
 LocationInfo latestInfo = new LocationInfo(getBaseContext());  

Если у вас уже есть объект LocationInfo, просто вызовите метод refresh().

Форсирование получения местоположения

Для принудительного обновления местоположения, вызовите LocationLibrary.forceLocationUpdate(), и в ближайшее время вы получите трансляцию, содержащую последнее местоположение.

суббота, 11 января 2014 г.

IP вебкамера WiFi из Android смартфона с разбитым тачскрином

В один прекрасный момент мой лабрадор основательно пожевал Android смартфон Мегафон Логин (он же Huawei U8180 Ideos X1) сына. Хотя у него и развитая общая тормознутость, он обладает неплохой 3.2 МП камерой сзади и поэтому было решено сделать из него беспроводную WiFi вебкамеру.

Проблема оставалась одна, точнее три - полностью неработающий тачскрин, а также выключенный wifi и отсутствие программы Google Play (выпилил, чтоб ребенок ничего лишнего не поставил).






0. Подключаем телефон к компьютеру и запускаем ADB
Нам очень крупно повезло, т.к. режим разработчика на нем был подключен. Иначе пришлось бы посвятить еще одну статью перепрошивке смартфона с этим предустановленным режимом, а может быть ничего бы и не получилось.
Но режим оказался включен, и можно было подключить смартфон к ПК, установить драйвер ADB и законнектиться через ADB шелл. Хорошая статья по подключению и работе через ADB есть на 4pda.

1. Разблокируем экран
Результат нашей бурной деятельности невозможно было увидеть, т.к. экран был заблокирован, а без тачскрина что-либо сделать было проблематично. Я попытался разблокировать смартфон командами ADB, но с сожалению ничего не получалось. Поэтому в эклипсе была написана программа:

Создаем новый пустой проект в Eclipse, в AndroidManifest прописываем строчку:

 <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>  

В основную активити добавляем три строчки:
 KeyguardManager keyguardManager = (KeyguardManager)getSystemService( Activity.KEYGUARD_SERVICE );  
 KeyguardLock lock = keyguardManager.newKeyguardLock(KEYGUARD_SERVICE);  
 lock.disableKeyguard();  

Запускаем и вуаля, смартфон разблокирован!

2. Осваиваем управление через ADB
Еще полчаса ушло на освоение работой смартфона кейкодами.

Если мы хотим написать текст, пишем в консоли:
input text [текст]


Если мы хотим послать событие, пишем в консоли:
input keyevent [код события]
Их перечень:
 0 --> "KEYCODE_UNKNOWN"   
 1 --> "KEYCODE_MENU"   
 2 --> "KEYCODE_SOFT_RIGHT"   
 3 --> "KEYCODE_HOME"   
 4 --> "KEYCODE_BACK"   
 5 --> "KEYCODE_CALL"   
 6 --> "KEYCODE_ENDCALL"   
 7 --> "KEYCODE_0"   
 8 --> "KEYCODE_1"   
 9 --> "KEYCODE_2"   
 10 --> "KEYCODE_3"   
 11 --> "KEYCODE_4"   
 12 --> "KEYCODE_5"   
 13 --> "KEYCODE_6"   
 14 --> "KEYCODE_7"   
 15 --> "KEYCODE_8"   
 16 --> "KEYCODE_9"   
 17 --> "KEYCODE_STAR"   
 18 --> "KEYCODE_POUND"   
 19 --> "KEYCODE_DPAD_UP"   
 20 --> "KEYCODE_DPAD_DOWN"   
 21 --> "KEYCODE_DPAD_LEFT"   
 22 --> "KEYCODE_DPAD_RIGHT"   
 23 --> "KEYCODE_DPAD_CENTER"   
 24 --> "KEYCODE_VOLUME_UP"   
 25 --> "KEYCODE_VOLUME_DOWN"   
 26 --> "KEYCODE_POWER"   
 27 --> "KEYCODE_CAMERA"   
 28 --> "KEYCODE_CLEAR"   
 29 --> "KEYCODE_A"   
 30 --> "KEYCODE_B"   
 31 --> "KEYCODE_C"   
 32 --> "KEYCODE_D"   
 33 --> "KEYCODE_E"   
 34 --> "KEYCODE_F"   
 35 --> "KEYCODE_G"   
 36 --> "KEYCODE_H"   
 37 --> "KEYCODE_I"   
 38 --> "KEYCODE_J"   
 39 --> "KEYCODE_K"   
 40 --> "KEYCODE_L"   
 41 --> "KEYCODE_M"   
 42 --> "KEYCODE_N"   
 43 --> "KEYCODE_O"   
 44 --> "KEYCODE_P"   
 45 --> "KEYCODE_Q"   
 46 --> "KEYCODE_R"   
 47 --> "KEYCODE_S"   
 48 --> "KEYCODE_T"   
 49 --> "KEYCODE_U"   
 50 --> "KEYCODE_V"   
 51 --> "KEYCODE_W"   
 52 --> "KEYCODE_X"   
 53 --> "KEYCODE_Y"   
 54 --> "KEYCODE_Z"   
 55 --> "KEYCODE_COMMA"   
 56 --> "KEYCODE_PERIOD"   
 57 --> "KEYCODE_ALT_LEFT"   
 58 --> "KEYCODE_ALT_RIGHT"   
 59 --> "KEYCODE_SHIFT_LEFT"   
 60 --> "KEYCODE_SHIFT_RIGHT"   
 61 --> "KEYCODE_TAB"   
 62 --> "KEYCODE_SPACE"   
 63 --> "KEYCODE_SYM"   
 64 --> "KEYCODE_EXPLORER"   
 65 --> "KEYCODE_ENVELOPE"   
 66 --> "KEYCODE_ENTER"   
 67 --> "KEYCODE_DEL"   
 68 --> "KEYCODE_GRAVE"   
 69 --> "KEYCODE_MINUS"   
 70 --> "KEYCODE_EQUALS"   
 71 --> "KEYCODE_LEFT_BRACKET"   
 72 --> "KEYCODE_RIGHT_BRACKET"   
 73 --> "KEYCODE_BACKSLASH"   
 74 --> "KEYCODE_SEMICOLON"   
 75 --> "KEYCODE_APOSTROPHE"   
 76 --> "KEYCODE_SLASH"   
 77 --> "KEYCODE_AT"   
 78 --> "KEYCODE_NUM"   
 79 --> "KEYCODE_HEADSETHOOK"   
 80 --> "KEYCODE_FOCUS"   
 81 --> "KEYCODE_PLUS"   
 82 --> "KEYCODE_MENU"   
 83 --> "KEYCODE_NOTIFICATION"   
 84 --> "KEYCODE_SEARCH"   
 85 --> "TAG_LAST_KEYCODE"  

Самые полезные команды это - 66 - enter, 82 - menu, 19,20,21,22 - стрелки (помогают перемещаться по настройкам).
Освоил список, когда настраивал WiFi для подключения на работе.

3.Устанавливаем необходимое
Теперь необходимо установить программу IP Webcam. Она на Google Play. У нее очень аскетичный интерфейс в меню, который позволяет прыгать по нему командами ADB, многие программы этого не дают делать.

Через ADB программа устанавливается так:
adb install *.apk

Чтобы запустить приложение, придется провести реверс-инжиниринг и узнать в AndroidManifest.xml как называется package и первая (LAUNCHER) активити этого приложения.
Есть набор утилит APK MultiTool для разбора дистрибутива программы.
Лаунчер этого приложения, как ни странно, называется Configuration:
     <activity android:name="Configuration" android:label="@string/app_name" android:launchMode="singleTop">  
       <intent-filter>  
         <action android:name="android.intent.action.MAIN" />  
         <category android:name="android.intent.category.LAUNCHER" />  
       </intent-filter>  
     </activity>  


Команды на запуск определенного приложения неплохо описаны здесь:
http://stackoverflow.com/questions/4567904/how-to-start-an-application-using-android-adb-tools

Запускаем командой
am start -n com.package.name/com.package.name.ActivityName
в нашем случае
am start -n com.pas.webcam/com.pas.webcam.Configuration 

а дальше уже по отработанной технологии настраиваем его как нам надо.
Наверное всё


понедельник, 30 декабря 2013 г.

Wi-Fi камера Multicam Defender WF-10HD

С недавнего времени организовал себе в квартире видеонаблюдение, с помощью двух USB вебкамер и ноутбука под Windows 8, т.е. тем, что было под рукой. В качестве программного обеспечения поставил Contacam - замечательная бесплатная программа для онлайн трансляций, может потом сделаю её нормальный обзор. Да только не в этом суть.

Показалось мне, что слишком много проводов получается и недавно купил еще сдуру WiFi камеру Defender WF-10HD.
Задним умом стало понятно, что для онлайн трансляций она совершенно не подходит, т.к. отсутствует на ней в явном виде сервис вещания. Вещать можно только на USTREAM, причем описывается это в рекламе как клёвая фича, а на самом деле выходит - безальтернативный режим.

Чтобы посмотреть, что творится в зоне наблюдения этой камеры, предлагается:

- сконнектиться к активированной на камере точке доступа Android или iOS устройством
- запустить программу Unieye
- управлять камерой через программу
минусы - радиус действия метров 10, нельзя смотреть через браузер, на устройстве пропадает интернет (вы ведь висите на другой точке доступа)

или

- открыть вещание в USTREAM облако
- зайти браузером или через приложение в USTREAM и смотреть трансляцию
минусы - вещаешь всему миру, если нужна приватность - $10/месяц, режется качество

Выводы 
Если нужна IP-вебкамера, Defender WF-10HD абсолютно не подходит. Если нужна видеоняня для двух комнат и есть лишний планшет - эта камера нормальный вариант, хотя качество ночной съемки никакое.
Камера плохо подходит в качестве авторегистратора, т.к. у нее небольшой угол обзора (по сравнению с профильными устройствами), отвратительная ночная съемка и отсутствует автозарядка в комплекте.
Можно использовать как бюджетный вариант action-камеры, т.к. крепления позволяют установить ее на велосипед, прицепить за карман

Известные факты

SD Карточка 32Gb class 10 не поддерживается

Открыты порты
23/tcp open telnet Busybox telnetd
80/tcp open http Cherokee httpd 1.2.101b130624_

Через telnet пускает по логину default, без пароля
login: "default" password: ""



Полезные ресурсы
Обзор на iXBT - http://www.ixbt.com/divideo/defender-wf-10hd.shtml
Обзор на Habrahabr - http://habrahabr.ru/company/ulmart/blog/201320/
Обзор брата-близнеца - http://ria.ru/columns/20130902/960120749.html 
Форум на iXBT - http://forum.ixbt.com/topic.cgi?id=29:35055
Программное обеспечение под Windows - http://download.aiptek.de/Air2u/MobileEyes%20HD/Mobile-Eyes-HD_unieye_v1.0.zip
Программное обеспечение под Android - https://play.google.com/store/apps/details?id=com.Unieye.smartphone
Программное обеспечение под iOS - https://itunes.apple.com/ru/app/unieye/id590211436?mt=8
Статья на Habrahabr -Приручаем Ustream.tv для HD видеотрансляции на собственном сайте без показа рекламы

Аналоги
http://www.aliexpress.com/item/2013-EASY-EYE-anti-lost-security-wifi-camera-portable-multi-function-WiFi-camcorder-internet-live-video/1377609106.html
http://e-joy.en.alibaba.com/product/1206589579-218696525/New_Design_The_World_Smallest_P2P_HD_720P_multi_function_wifi_camera_E9000.html
http://vobis.ru/product/highscreen-black-box-connect/
http://www.aiptek.eu/index.php/en/products/air2u/mobile-cameras/mobileeyes-hd
http://www.icarmultimedia.com.cn/product/721516263-214783362/WIFI_Camera_AMB_A7cs13_Mobile_remote_control_Night_Vision_HD_Car_Dvr.html
http://www.ebay.com/itm/Unieye-Waterproof-Anti-Dust-Wireless-Video-Camera-720P-30FPS-Handfree-Mini-DV-GI-/350869344314?pt=US_Surveillance_Digital_Video_Recorders_Cards&hash=item51b171503a



понедельник, 10 декабря 2012 г.

Android. Лицензирование собственного приложения

Итак, в Google Play у вас есть крутое приложение, которое купит у вас целая куча народа. И вы хотите встроить проверку лицензии на него. Кстати, если вы разместили в маркете бесплатное приложение, платным его сделать уже не получится.

1. Настраиваем консоль разработчика


Наше лицензируемое приложение уже должно лежать в Google Developer Console. С новой политикой Google каждое приложение имеет свой уникальный ключ. Щелкаем по приложению, выбираем в левом меню "Службы и API" и копируем RSA ключ:


Необходимо подготовить аккаунт к тестированию, для этого заходим через левое меню в Настройки - Сведения об аккаунте, добавляем свой e-mail в Аккаунты GMail разработчиков (соответственно этот аккаунт должен у нас быть забит в настройках Вашего устройства). Выбирая в ComboBox "Информация о лицензии" требуемый ответ, можно протестировать реакцию приложения. А вот и картинка этого окна:



2. Настраиваем Eclipse


В Eclipse заходим в Window - Android SDK Manager, секция Extras и скачиваем Google Play Licensing Library (на картинке - в красной рамке):

Теперь подключаем библиотеку к нашему проекту. Для этого жмем правой кнопкой в Package Explorer, далее Import, далее Android - Existing Android Code Into Workspace, находим нашу библиотеку в директории <путь до android sdk>/google-market_licensing. Импортируем.

3. Создаем проект Eclipse


В нашем проекте заходим в свойства проекта, секция Android, нижняя вкладка Library. Подключаем библиотеку к проекту:





Вроде всё готово.

В нашем приложении в Android Manifest.xml в корень manifest добавляем всего одну строчку:
    



Заодно исправим главный layout в res/layout/. Вот его полный код:

В главной активити приложения добавляем импорты:

import com.google.android.vending.licensing.AESObfuscator;
import com.google.android.vending.licensing.LicenseChecker;
import com.google.android.vending.licensing.LicenseCheckerCallback;
import com.google.android.vending.licensing.ServerManagedPolicy;

И следующие члены класса:

    // Поставьте сюда свой RSA ключ из Developer Console
    private static final String BASE64_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFveIcGxnHJQfSUs6S5eoWwYr87LfNBupk9G0rN1QZ3YiVYzp/LeJ7fNkDjC1f8RHMeJF3ZA9VjLH5d4TUmQ3M4e6/vVrNFga+BXEbAmhsv6aQ1fNzt5tBWFYUdGlhHTcfsTiFPDh17ejlfm7XlhxWuYNLuxJtzXpwdiqTqiTZed0mFut1Z1khL+34SXL4qDzegbkSdxrka/zyLnuS5dSyacszmyST7x+/NjWgg/9zlu+FRETXl+XYO2STb6RuVVgLaQIDAQAB";

    // Забейте сюда какие-нибудь числа. Свои, иначе ничего не заработает!
    private static final byte[] SALT = new byte[] { -46, 65, 30, -19, -103, -5, 74, -64, 51, 88, -95, -45, 77, -117, -36, -113, -11, 32, -64, 89 };   
        
        // компоненты на лэйауте
    private TextView mStatusText;
    private Button mCheckLicenseButton;
        
        // чекер и коллбэк для проверки лицензии
    private LicenseCheckerCallback mLicenseCheckerCallback;
    private LicenseChecker mChecker; 
 
    // Хэндлер для процесса UI.
    private Handler mHandler;
 
Метод onCreate с комментариями:

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
        setContentView(R.layout.main); 
        // получаем компоненты layouta, по кнопке запускаем повторную проверку лицензии 
        mStatusText = (TextView) findViewById(R.id.status_text);
        mCheckLicenseButton = (Button) findViewById(R.id.check_license_button);
        mCheckLicenseButton.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                doCheck();
            }
        });

        mHandler = new Handler();

        // Попробуйте использовать здесь другие данные для ID устройства. ANDROID_ID будет взламываться в первую очередь.
        String deviceId = Secure.getString(getContentResolver(), Secure.ANDROID_ID);

        // Коллбэк функция по окончании проверки лицензии
        mLicenseCheckerCallback = new MyLicenseCheckerCallback();
        // конструктор LicenseChecker с правами.
        mChecker = new LicenseChecker(
            this, new ServerManagedPolicy(this,
                new AESObfuscator(SALT, getPackageName(), deviceId)),
            BASE64_PUBLIC_KEY); 
  
        // запускаем проверку  
        doCheck();
    }
  
    // в методе doCheck() готовим лэйаут к отсылке запроса  
    private void doCheck() {
        mCheckLicenseButton.setEnabled(false);
        setProgressBarIndeterminateVisibility(true);
        mStatusText.setText(R.string.checking_license);
        mChecker.checkAccess(mLicenseCheckerCallback);
    } 

Описываем класс MyLicenseChecker, который служит для обработки ответов от сервера:

 
    private class MyLicenseCheckerCallback implements LicenseCheckerCallback {

     @Override
  public void allow(int reason) {
            if (isFinishing()) {
                // Don't update UI if Activity is finishing.
                return;
            }
            // Should allow user access.
            displayResult("Allow");
        }

  @Override
  public void dontAllow(int reason) {
            if (isFinishing()) {
                // Don't update UI if Activity is finishing.
                return;
            }
            displayResult("DONT ALLOW");
            // Should not allow access. In most cases, the app should assume
            // the user has access unless it encounters this. If it does,
            // the app should inform the user of their unlicensed ways
            // and then either shut down the app or limit the user to a
            // restricted set of features.
            // In this example, we show a dialog that takes the user to Market.
            showDialog(0);
        }
  
  @Override
        public void applicationError(int errorCode) {
            if (isFinishing()) {
                // Don't update UI if Activity is finishing.
                return;
            }
            // This is a polite way of saying the developer made a mistake
            // while setting up or calling the license checker library.
            // Please examine the error code and fix the error.
            String result = String.format("ERROR", errorCode);
            displayResult(result);
        }

    } 

А также несколько функций чтобы просто показать диалог:

    protected Dialog onCreateDialog(int id) {
        // We have only one dialog.
        return new AlertDialog.Builder(this)
            .setTitle("UNLICENSED")
            .setMessage("UNLICENSED DIALOG BODY")
            .setPositiveButton("BUY BUTTON", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(
                        "http://market.android.com/details?id=" + getPackageName()));
                    startActivity(marketIntent);
                }
            })
            .setNegativeButton("QUIT BUTTON", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    finish();
                }
            })
            .create();
    }    
    
    private void displayResult(final String result) {
        mHandler.post(new Runnable() {
            public void run() {
                mStatusText.setText(result);
                setProgressBarIndeterminateVisibility(false);
                mCheckLicenseButton.setEnabled(true);
            }
        });
    }  


Собственно и всё. Запуская приложение и изменяя требуемый ответ через Developer Console(Информация о лицензии), получаем требуемые данные
P.S.: Если в консоли видим, что типа Using cached license response, меняйте SALT.

Полный код активити:

package com.osmsoft.licensingexample;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings.Secure;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.TextView;

import com.android.vending.licensing.AESObfuscator;
import com.android.vending.licensing.LicenseChecker;
import com.android.vending.licensing.LicenseCheckerCallback;
import com.android.vending.licensing.ServerManagedPolicy;

public class MainActivity extends Activity {
    private static final String BASE64_PUBLIC_KEY = "REPLACE THIS WITH YOUR PUBLIC KEY";

    // Generate your own 20 random bytes, and put them here.
    private static final byte[] SALT = new byte[] {
        -46, 65, 30, -128, -103, -57, 74, -64, 51, 88, -95, -45, 77, -117, -36, -113, -11, 32, -64,
        89
    };

    private TextView mStatusText;
    private Button mCheckLicenseButton;

    private LicenseCheckerCallback mLicenseCheckerCallback;
    private LicenseChecker mChecker;
    // A handler on the UI thread.
    private Handler mHandler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
        setContentView(R.layout.main);

        mStatusText = (TextView) findViewById(R.id.status_text);
        mCheckLicenseButton = (Button) findViewById(R.id.check_license_button);
        mCheckLicenseButton.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                doCheck();
            }
        });

        mHandler = new Handler();

        // Try to use more data here. ANDROID_ID is a single point of attack.
        String deviceId = Secure.getString(getContentResolver(), Secure.ANDROID_ID);

        // Library calls this when it's done.
        mLicenseCheckerCallback = new MyLicenseCheckerCallback();
        // Construct the LicenseChecker with a policy.
        mChecker = new LicenseChecker(
            this, new ServerManagedPolicy(this,
                new AESObfuscator(SALT, getPackageName(), deviceId)),
            BASE64_PUBLIC_KEY);
        doCheck();
    }

    protected Dialog onCreateDialog(int id) {
        // We have only one dialog.
        return new AlertDialog.Builder(this)
            .setTitle("UNLICENSED")
            .setMessage("UNLICENSED DIALOG BODY")
            .setPositiveButton("BUY BUTTON", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(
                        "http://market.android.com/details?id=" + getPackageName()));
                    startActivity(marketIntent);
                }
            })
            .setNegativeButton("QUIT BUTTON", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    finish();
                }
            })
            .create();
    }   

    private void doCheck() {
        mCheckLicenseButton.setEnabled(false);
        setProgressBarIndeterminateVisibility(true);
        mStatusText.setText(R.string.checking_license);
        mChecker.checkAccess(mLicenseCheckerCallback);
    }

    private void displayResult(final String result) {
        mHandler.post(new Runnable() {
            public void run() {
                mStatusText.setText(result);
                setProgressBarIndeterminateVisibility(false);
                mCheckLicenseButton.setEnabled(true);
            }
        });
    }

    private class MyLicenseCheckerCallback implements LicenseCheckerCallback {

     @Override
  public void allow(int reason) {
            if (isFinishing()) {
                // Don't update UI if Activity is finishing.
                return;
            }
            // Should allow user access.
            displayResult("Allow");
        }

  @Override
  public void dontAllow(int reason) {
            if (isFinishing()) {
                // Don't update UI if Activity is finishing.
                return;
            }
            displayResult("DONT ALLOW");
            // Should not allow access. In most cases, the app should assume
            // the user has access unless it encounters this. If it does,
            // the app should inform the user of their unlicensed ways
            // and then either shut down the app or limit the user to a
            // restricted set of features.
            // In this example, we show a dialog that takes the user to Market.
            showDialog(0);
        }
  
  @Override
        public void applicationError(int errorCode) {
            if (isFinishing()) {
                // Don't update UI if Activity is finishing.
                return;
            }
            // This is a polite way of saying the developer made a mistake
            // while setting up or calling the license checker library.
            // Please examine the error code and fix the error.
            String result = String.format("ERROR", errorCode);
            displayResult(result);
        }

    }  

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mChecker.onDestroy();
    }

}

Android Camera API. Используем камеру в Android приложениях

1. Камера Android


Большинство устройств на Android имеют камеру. А некоторые даже две - переднюю и заднюю. Использовать камеру можно с помощью существующих приложений. В этом случае нужно запустить существующее приложение и получить после этого данные в нашем приложении. А можно напрямую интегрировать камеру в приложение с помощью API.

2. Создание фото установленными приложениями

Создаем новый Android проект com.osmsoft.cameraexample с Activity ImagePickActivity.
Изменяем класс ImagePickActivity следующим образом:


package com.osmsoft.cameraexample;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;

public class ImagePickActivity extends Activity {
  private static final int REQUEST_CODE = 1;
  private Bitmap bitmap;
  private ImageView imageView;

  
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    imageView = (ImageView) findViewById(R.id.result);
  }

  public void pickImage(View View) {
    Intent intent = new Intent();
    intent.setType("image/*");
    intent.setAction(Intent.ACTION_GET_CONTENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    startActivityForResult(intent, REQUEST_CODE);
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK)
      try {
        // We need to recyle unused bitmaps
        if (bitmap != null) {
          bitmap.recycle();
        }
        InputStream stream = getContentResolver().openInputStream(data.getData());
        bitmap = BitmapFactory.decodeStream(stream);
        stream.close();
        imageView.setImageBitmap(bitmap);
      } catch (FileNotFoundException e) {
        e.printStackTrace();
      } catch (IOException e) {
        e.printStackTrace();
      }
    super.onActivityResult(requestCode, resultCode, data);
  }
} 

3. Создание фото используя встроенный API

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

Создаем новый Android проект com.osmsoft.cameraapiexample с Activity MakePhotoActivity.

Добавим
в AndroidManifest.xml разрешения android.permission.CAMERA для доступа к вашей камере и android.permission.WRITE_EXTERNAL_STORAGE, чтобы иметь возможность писать на карту памяти SD.


    
    
    

    
        
            
                

                
            
        
    

  

Изменяем в директории res/layout главный лэйаут нашего приложения:



      


Создаем класс PhotoHandler, который требуется для сохранения фото на SD

package com.osmsoft.cameraapi.example;

import java.io.File;
import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;

import android.content.Context;
import android.hardware.Camera;
import android.hardware.Camera.PictureCallback;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;

public class PhotoHandler implements PictureCallback {

  private final Context context;

  public PhotoHandler(Context context) {
    this.context = context;
  }

  @Override
  public void onPictureTaken(byte[] data, Camera camera) {

    File pictureFileDir = getDir();

    if (!pictureFileDir.exists() && !pictureFileDir.mkdirs()) {

      Log.d(Constants.DEBUG_TAG, "Can't create directory to save image.");
      Toast.makeText(context, "Can't create directory to save image.",
          Toast.LENGTH_LONG).show();
      return;

    }

    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyymmddhhmmss");
    String date = dateFormat.format(new Date());
    String photoFile = "Picture_" + date + ".jpg";

    String filename = pictureFileDir.getPath() + File.separator + photoFile;

    File pictureFile = new File(filename);

    try {
      FileOutputStream fos = new FileOutputStream(pictureFile);
      fos.write(data);
      fos.close();
      Toast.makeText(context, "New Image saved:" + photoFile,
          Toast.LENGTH_LONG).show();
    } catch (Exception error) {
      Log.d(Constants.DEBUG_TAG, "File" + filename + "not saved: "
          + error.getMessage());
      Toast.makeText(context, "Image could not be saved.",
          Toast.LENGTH_LONG).show();
    }
  }

  private File getDir() {
    File sdDir = Environment
      .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
    return new File(sdDir, "CameraAPIDemo");
  }
} 

Изменяем главное Activity:

package com.osmsoft.cameraapi.example;

import android.app.Activity;
import android.content.pm.PackageManager;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.osmsoft.camearaapi.example.R;

public class MakePhotoActivity extends Activity {
  private final static String DEBUG_TAG = "MakePhotoActivity";
  private Camera camera;
  private int cameraId = 0;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    // do we have a camera?
    if (!getPackageManager()
        .hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
      Toast.makeText(this, "No camera on this device", Toast.LENGTH_LONG)
          .show();
    } else {
      cameraId = findFrontFacingCamera();
      camera = Camera.open(cameraId);
      if (cameraId < 0) {
        Toast.makeText(this, "No front facing camera found.",
            Toast.LENGTH_LONG).show();
      }
    }
  }

  public void onClick(View view) {
    camera.takePicture(null, null,
        new PhotoHandler(getApplicationContext()));
  }

  private int findFrontFacingCamera() {
    int cameraId = -1;
    // Search for the front facing camera
    int numberOfCameras = Camera.getNumberOfCameras();
    for (int i = 0; i < numberOfCameras; i++) {
      CameraInfo info = new CameraInfo();
      Camera.getCameraInfo(i, info);
      if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
        Log.d(DEBUG_TAG, "Camera found");
        cameraId = i;
        break;
      }
    }
    return cameraId;
  }

  @Override
  protected void onPause() {
    if (camera != null) {
      camera.release();
      camera = null;
    }
    super.onPause();
  }

} 

среда, 5 декабря 2012 г.

Android Google Maps Api v2. Урок 4. Строим маршрут на карте

Итак, продолжаем нашу серию спонтанных уроков, вызванных необходимостью переводить своё приложение на новый API
Сегодня мы разберемся, как получить маршрут и отобразить его на карте. Для этого обратимся к API Google Directions.

Получать маршрут мы будем грамотно, используя AsyncTask. Мы должны обратиться к Google Service Directions, передать координаты начальной и конечной точек и распарсить JSON ответ. Например вот такой: http://maps.google.com/maps/api/directions/json?origin=55.772851,37.586806&destination=55.415003,37.899904&sensor=false

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

Итак, пишем интерфейс для AsyncTask, сохраняем его в файл OnRouteCalcCompleted.java:

package com.example.mapexample;

import java.util.ArrayList;

import com.google.android.gms.maps.model.LatLng;

public interface OnRouteCalcCompleted{
    void onRouteCalcBegin();
    void onRouteCompleted( ArrayList route );
}


И саму AsyncTask RouteHandler.java:


package com.example.mapexample;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.google.android.gms.maps.model.LatLng;

import android.os.AsyncTask;
import android.util.Log;


/**
 * Класс возвращает полилинию дороги, если указан mapView, отрисовывается на нём
 * @author awacs
 *
 */
public class RouteHandler extends AsyncTask {
   
    private final static String TAG = "RouteHandler";
    

    /**
     * Для точного интерполирования марштура
     */
    public static final int FINE_ROUTE = 1;
   
    /**
     * Для грубого интерполирования маршрута
     */
    public static final int COARSE_ROUTE = 2;    

 
    private final HttpClient client = new DefaultHttpClient();
    private String content;
    private boolean error = false;
    private String error_msg = "";
   
    private ArrayList polyline;
   
    private int accuracyRoute = 1;
   
    private long distance;
   
    private OnRouteCalcCompleted listener;
   
    public RouteHandler( OnRouteCalcCompleted l ){
        this.listener = l;
    }
   
    public double getDistance(){
        Log.d(TAG, "distance = " + distance + "m");
        return distance/1000;
    }
   
    public void calculateRoute( double latStart, double lonStart, double latEnd, double lonEnd, int accuracy ){
       
        this.accuracyRoute = accuracy;
       
        StringBuilder origin = new StringBuilder();
        origin.append( Double.toString(latStart));
        origin.append(",");
        origin.append( Double.toString(lonStart));       

        StringBuilder destination = new StringBuilder();
        destination.append( Double.toString(latEnd));
        destination.append(",");
        destination.append( Double.toString(lonEnd));       
       
        List nameValuePairs = new ArrayList();
        nameValuePairs.add(new BasicNameValuePair("origin", origin.toString()));
        nameValuePairs.add(new BasicNameValuePair("destination", destination.toString() ));
        nameValuePairs.add(new BasicNameValuePair("sensor", "false"));
        String paramString = URLEncodedUtils.format(nameValuePairs, "utf-8");
        execute( "http://maps.google.com/maps/api/directions/json" + "?" + paramString );

 
    }
   

    public boolean isError(){
        return error;
    }
   
    public String getErrorMsg(){
        return error_msg;
    }
   
    @Override
    protected String doInBackground(String... urls) {
       
        Log.d(TAG, "RouteHandler::doInBackground");

        try {
            Log.v(TAG, urls[0]);
            HttpPost httppost = new HttpPost(urls[0]);
            ResponseHandler responseHandler = new BasicResponseHandler();
            content = client.execute( httppost, responseHandler );
        } catch (ClientProtocolException e) {
            Log.d(TAG, "GetRouteHandler::ClientProtocolException");
            e.printStackTrace();
            error = true;
            cancel(true);
         } catch (IOException e) {
            Log.d(TAG, "GetRouteHandler::IOException");
            e.printStackTrace();
            error = true;
            cancel(true);
         }
        return content;
    }
   
    protected void onPostExecute(String content) {
        if (error) {
            error_msg = "Offline";
        } else {
            try {
                JSONObject response = new JSONObject(content);
                String status = response.getString("status");
                Log.v(TAG, content);
                if( status.equalsIgnoreCase("OK") ){
                    polyline = new ArrayList();
               
                    JSONArray routesArray = response.getJSONArray("routes");
                    JSONObject route = routesArray.getJSONObject(0);
                    // массив с информацией об отрезке маршрута
                    JSONArray legs = route.getJSONArray("legs");
                    JSONObject leg = legs.getJSONObject(0);   
                   
                    JSONObject distanceObj = leg.getJSONObject("distance");
                    distance = distanceObj.getLong("value");
                   
                    JSONObject durationObj = leg.getJSONObject("duration");
                   
                    // содержит куб выделения информационного окна для маршрута.
                    JSONObject bounds = route.getJSONObject("bounds");
                    JSONObject bounds_southwest = bounds.getJSONObject("southwest");
                    JSONObject bounds_northeast = bounds.getJSONObject("northeast");
                   
                    double maxLat = bounds_northeast.getDouble("lat");
                    double maxLon = bounds_northeast.getDouble("lng");
                    double minLat = bounds_southwest.getDouble("lat");
                    double minLon = bounds_southwest.getDouble("lng");
           

                    JSONArray steps = leg.getJSONArray("steps");
                    for( int i=0; i
                        JSONObject step = steps.getJSONObject(i);
                        JSONObject start_location = step.getJSONObject("start_location");
                        JSONObject end_location = step.getJSONObject("end_location");

                        double latitudeStart = start_location.getDouble("lat");
                        double longitudeStart = start_location.getDouble("lng");
                        double latitudeEnd = end_location.getDouble("lat");
                        double longitudeEnd = end_location.getDouble("lng");
                        LatLng startGeoPoint = new LatLng(latitudeStart,longitudeStart);
                        LatLng endGeoPoint = new LatLng(latitudeEnd,longitudeEnd);
                        JSONObject polylineObject = step.getJSONObject("polyline");
                        if( accuracyRoute == FINE_ROUTE ){
                            List points = decodePoly(polylineObject.getString("points"));
                            Log.d(TAG, " " + points.size());
                            polyline.addAll(points);
                        } else {
                            polyline.add(startGeoPoint);
                            polyline.add(endGeoPoint);
                        }
                    }

                } else if( status.equalsIgnoreCase("NOT_FOUND")){
                    // по крайней мере для одной заданной точки (исходной точки, пункта назначения или путевой точки) геокодирование невозможно.
                } else if( status.equalsIgnoreCase("ZERO_RESULTS")){
                    // между исходной точкой и пунктом назначения не найдено ни одного маршрута.
                } else if( status.equalsIgnoreCase("MAX_WAYPOINTS_EXCEEDED")){
                    // в запросе задано слишком много waypoints. Максимальное количество waypoints равно 8 плюс исходная точка и пункт назначения. ( (Пользователи Google Maps Premier могут выполнять запросы с количеством путевых точек до 23.)
                } else if( status.equalsIgnoreCase("INVALID_REQUEST")){
                    // запрос недопустим
                }else if( status.equalsIgnoreCase("OVER_QUERY_LIMIT")){
                    // служба получила слишком много запросов от вашего приложения в разрешенный период времени.
                }else if( status.equalsIgnoreCase("REQUEST_DENIED")){
                    // служба Directions отклонила запрос вашего приложения.
                }else if( status.equalsIgnoreCase("UNKNOWN_ERROR")){
                    // обработка запроса маршрута невозможна из-за ошибки сервера. При повторной попытке запрос может быть успешно выполнен
                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
       
        listener.onRouteCompleted( polyline );
       
    } // end postExecute
   
    /**
     * Декодирует полилинию из переданной гуглом строки
     * @param encoded
     * @return
     */
    private List decodePoly(String encoded) {

        List poly = new ArrayList();
        int index = 0, len = encoded.length();
        int lat = 0, lng = 0;

        while (index < len) {
            int b, shift = 0, result = 0;
            do {
                b = encoded.charAt(index++) - 63;
                result |= (b & 0x1f) << shift;
                shift += 5;
            } while (b >= 0x20);
            int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
            lat += dlat;

            shift = 0;
            result = 0;
            do {
                b = encoded.charAt(index++) - 63;
                result |= (b & 0x1f) << shift;
                shift += 5;
            } while (b >= 0x20);
            int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
            lng += dlng;

            LatLng p = new LatLng( lat/1E5, lng/1E5);
            poly.add(p);
        }

        return poly;
    } // end decodePoly      
   
} // end class


Для использования этого таска в нашей активити, нужно имплементить OnRouteCalcCompleted, соответвенно в Activity добавятся методы:
    @Override
    public void onRouteCalcBegin() {
        // Тут можно добавить например вызов прогрессбара, "Ждите, строим маршрут...."
    }

а отобразить готовую полилинию можно в добавленном onRouteCompleted:


    @Override
    public void onRouteCompleted(ArrayList route) {
        mMap.addPolyline((new PolylineOptions().color(Color.BLUE).width(5)).addAll(route));
        // а заодно и удалить прогрессбар :)
    }


Да, теперь добавляем например в инициализацию mMap из урока 3 код для вызова AsyncTask и компилируем готовое приложение.

        private RouteHandler routeHandler;

    private void setUpMapIfNeeded() {
        if (mMap == null) {
            mMap = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.mapView)).getMap();
        }
        routeHandler = new RouteHandler( this );
        routeHandler.calculateRoute(55.772935, 37.594272, 55.88459, 37.4263165, RouteHandler.FINE_ROUTE);       
    }


Код полной активити:

package com.example.mapexample;

import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.OnMapLongClickListener;
import com.google.android.gms.maps.LocationSource;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.PolylineOptions;

import java.sql.Time;
import java.util.ArrayList;

import android.graphics.Color;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;


public class MainActivity extends FragmentActivity implements LocationSource, LocationListener, OnRouteCalcCompleted{
   
    private final static String TAG = "MainActivity";

    private OnLocationChangedListener mListener;
    private LocationManager lManager;
   
    private GoogleMap mMap;
   
    private static double mLatitude;
    private static double mLongitude;   
   
    private RouteHandler routeHandler;
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mMap = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.mapView)).getMap();
        //mMap.setTrafficEnabled(true);
        mMap.setMyLocationEnabled(true);
        mMap.setOnMapLongClickListener(this);
        lManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        lManager.requestLocationUpdates( lManager.getBestProvider(new Criteria(), true), 1, 1000, this);       
        setUpMapIfNeeded();
    }
   
    @Override
    protected void onResume() {
        super.onResume();
        setUpMapIfNeeded();
        if( lManager != null ){
            lManager.requestLocationUpdates(lManager.getBestProvider(new Criteria(), true), 1, 1000, this);
        }       
    }
   
    private void setUpMapIfNeeded() {
        if (mMap == null) {
            mMap = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.mapView)).getMap();
        }
        routeHandler = new RouteHandler( this );
        routeHandler.calculateRoute(55.772935, 37.594272, 55.88459, 37.4263165, AppSettings.FINE_ROUTE);       
    }


     protected void onPause() {
         if( lManager != null ){
             lManager.removeUpdates(this);
         }
         super.onPause();
     }   
   



    @Override
    public void onLocationChanged(Location location) {
        if( mListener != null ){
            mListener.onLocationChanged( location );
        }
    }

    @Override
    public void activate(OnLocationChangedListener listener) {
        mListener = listener;
       
    }

    @Override
    public void deactivate() {
        mListener = null;
    }

    @Override
    public void onProviderDisabled(String provider) {
        // TODO Auto-generated method stub
       
    }

    @Override
    public void onProviderEnabled(String provider) {
        // TODO Auto-generated method stub
       
    }

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
        // TODO Auto-generated method stub
       
    }

    @Override
    public void onRouteCalcBegin() {
        // TODO Auto-generated method stub
       
    }

    @Override
    public void onRouteCompleted(ArrayList route) {
        mMap.addPolyline((new PolylineOptions().color(Color.BLUE).width(5)).addAll(route));
    }

}