Sony SmartWatch – достаточно интересный девайс своего времени, разработку под который почему-то обошли стороной на хабре. Ну раз так – давайте исправлять! В качестве примера мы разработаем простое приложение для управлением любым аудио-плеером.
Статья предназначена для тех, кто уже хотя бы минимально знает, с какой стороны держать инструменты для разработки под Android, а так же тех, кто видел те самые часы или читал про них обзоры, и, соответственно, представляет их функционал. Разрабатывать будем сразу под первую и вторую версии SmartWatch.
Установка необходимых библиотек
Запускаем Android SDK Manager и идём в меню Tools -> Manage Add-on Sites
На вкладке User Defined Sites добавляем адрес с SDK под часы:
http://dl-developer.sonymobile.com/sdk_manager/Sony-Add-on-SDK.xml
На самом деле, данный SDK поддерживает не только часы, но и некоторые другие хитрые устройства от Sony, такие как например Smart Headset… Но нам пока интересны только часы.
И теперь выбираем новые, появившиеся в списке пакеты и устанавливаем их:
Кроме собственно необходимых библиотек, после установки обязательно загляните в папку [директория Android SDK]/sdk/add-ons/addon-sony_add-on_sdk_2_1-sony-16/samples. Там есть примеры использованию абсолютно всех возможностей часиков, мы поговорим только об избранных.
Эмулятор часов
В принципе, разрабатывать под реальные часы гораздо проще и удобнее, но тем не менее, вместе с SDK идёт и эмулятор. Для его использования пойдём в AVD Manager и создадим одно из появившихся в списке новых устройство от Sony, например, Xperia T. Главное, что бы в качестве параметра Target был выбран Sony Add-on SDK.
Теперь, если запустить такое устройство на эмуляцию, то в списке приложений на эмулируемом устройстве можно найти Accessory emulator
Который эмулирует необходимые нам часики (и не только, как уже упоминалось выше).
План проекта
Ну а теперь, что именно мы будем разрабатывать? Как мне кажется, делать всякие hello word скучно, так что напишем приложение для управления плеером! Любым плеером на телефоне. Вот это подходящий масштаб действий. ;)
- Приложение будет управляться жестами и кликами. Жест справа-налево и обратно – это следующий/предыдущий трек, вверх/вниз – громче/тише. Клик в центре – поставить на паузу/продолжить воспроизведение.
- Кроме самого экрана приложения реализуем виджет (для часов), который по клику будет вызывать основное окно программы.
- Сделаем заготовку для экрана настроек приложения – ну просто про запас.
- Поддерживать оно должно обе версии SmartWatch (первую и вторую, как подсказываем Кэп).
Подключаем библиотеки к проекту в IntelliJ IDEA
Поскольку я использую IntelliJ IDEA, то и пример приводить на ней. Для начала – создадим проект, в качестве версии SDK выбираем вариант от Sony.
Кроме того, для работы мы подключим к проекту пару модулей из той самой папки samples– в частности SmartExtensions/SmartExtensionAPI и SmartExtensions/SmartExtensionUtils. Вторую, теоретически, можно не подключать, и написать всё её содержимое с нуля, но мы, адепты тёмной стороны силы, ценим эффективность и удобство, а желание писать с нуля то, что уже существует нам чуждо. Инструкции по самому подключению я убрал под спойлер, благо там всё просто.
Находим папку SmartExtensionAPI:
Дальше ОК и Next->Next->Next до победного конца, как в старые добрые времена.
После чего подключаем к основному проекту добавленный модуль.
Аналогичным образом подключаем и SmartExtensionUtils.
Настраиваем базовые классы и параметры
Начнём с манифеста.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.smartwatch_habra_demo">
<uses-sdk android:minSdkVersion="9"
android:targetSdkVersion="16"/>
<uses-permission android:name="com.sonyericsson.extras.liveware.aef.EXTENSION_PERMISSION" />
<application android:label="Демо-приложения для часов для хабра" android:icon="@drawable/icon">
<activity
android:name="DemoConfigActivity"
android:label="Экран с настройками" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<service android:name="DemoReceiverService" />
<receiver
android:name="DemoExtensionReceiver"
android:permission="com.sonyericsson.extras.liveware.aef.HOSTAPP_PERMISSION" >
<intent-filter>
<!-- Generic extension intents. -->
<action android:name="com.sonyericsson.extras.liveware.aef.registration.EXTENSION_REGISTER_REQUEST" />
<action android:name="com.sonyericsson.extras.liveware.aef.registration.ACCESSORY_CONNECTION" />
<action android:name="android.intent.action.LOCALE_CHANGED" />
<!-- Notification intents -->
<action android:name="com.sonyericsson.extras.liveware.aef.notification.VIEW_EVENT_DETAIL" />
<action android:name="com.sonyericsson.extras.liveware.aef.notification.REFRESH_REQUEST" />
<!-- Widget intents -->
<action android:name="com.sonyericsson.extras.aef.widget.START_REFRESH_IMAGE_REQUEST" />
<action android:name="com.sonyericsson.extras.aef.widget.STOP_REFRESH_IMAGE_REQUEST" />
<action android:name="com.sonyericsson.extras.aef.widget.ONTOUCH" />
<action android:name="com.sonyericsson.extras.liveware.extension.util.widget.scheduled.refresh" />
<!-- Control intents -->
<action android:name="com.sonyericsson.extras.aef.control.START" />
<action android:name="com.sonyericsson.extras.aef.control.STOP" />
<action android:name="com.sonyericsson.extras.aef.control.PAUSE" />
<action android:name="com.sonyericsson.extras.aef.control.RESUME" />
<action android:name="com.sonyericsson.extras.aef.control.ERROR" />
<action android:name="com.sonyericsson.extras.aef.control.KEY_EVENT" />
<action android:name="com.sonyericsson.extras.aef.control.TOUCH_EVENT" />
<action android:name="com.sonyericsson.extras.aef.control.SWIPE_EVENT" />
<action android:name="com.sonyericsson.extras.aef.control.OBJECT_CLICK_EVENT" />
<action android:name="com.sonyericsson.extras.aef.control.MENU_ITEM_SELECTED" />
</intent-filter>
</receiver>
</application>
</manifest>
Суть происходящего такова: мы создаём в приложении класс, который будет принимать события от часов, передавать их в сервис обработки, который и будет производить некие осмысленные действия. Единственная activity нам нужна для окна настроек, если же таковое нам не нужно – можно было бы выкинуть её совсем.
Класс-receiver совсем простой:
public class DemoExtensionReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
intent.setClass(context, DemoReceiverService.class);
context.startService(intent);
}
}
Ну а теперь перейдём к самому сервису:
public class DemoReceiverService extends ExtensionService {
public static final String EXTENSION_KEY = "com.smartwatch_habra_demo"; //todo не смог найти в документации подробностей о применимости, так что просто копипастим из примеров по шаблону "пакет.приложения"
public DemoReceiverService() {
super(EXTENSION_KEY);
}
@Override
protected RegistrationInformation getRegistrationInformation() {
return new DemoRegistrationInformation(this);
}
@Override
protected boolean keepRunningWhenConnected() {//нам не нужно постоянно держать сервис работающим
return false;
}
@Override
public WidgetExtension createWidgetExtension(String hostAppPackageName) { //возвращаем объект виджета
return new DemoWidget(this,hostAppPackageName);
}
@Override
public ControlExtension createControlExtension(String hostAppPackageName) {//возвращаем объект основной программы
boolean IsSmartWatch2= DeviceInfoHelper.isSmartWatch2ApiAndScreenDetected(
this, hostAppPackageName);
if (IsSmartWatch2){
return new DemoControl2(this,hostAppPackageName);
}else{
return new DemoControl(this,hostAppPackageName);
}
}
}
Достаточно лаконично, правда? Ключевые моменты поясняются комментариями, вопросов вроде не должно возникнуть. ControlExtension нам нужен для обработки и рисования основного приложения на часах, WidgetExtension – для тех же целей, но уже для виджета.
А вот RegistrationInformation – это информация для регистрации нашего расширения в программе управления часами так сказать.
public class DemoRegistrationInformation extends RegistrationInformation {
public static final int WIDGET_WIDTH_SMARTWATCH = 128;
public static final int WIDGET_HEIGHT_SMARTWATCH = 110;
public static final int CONTROL_WIDTH_SMARTWATCH = 128;
public static final int CONTROL_HEIGHT_SMARTWATCH = 128;
public static final int CONTROL_WIDTH_SMARTWATCH_2 = 220;
public static final int CONTROL_HEIGHT_SMARTWATCH_2 = 176;
Context mContext;
protected DemoRegistrationInformation(Context context) {
if (context == null) {
throw new IllegalArgumentException("context == null");
}
mContext = context;
}
@Override
public ContentValues getExtensionRegistrationConfiguration() {
String iconHostapp = ExtensionUtils.getUriString(mContext, R.drawable.icon);
ContentValues values = new ContentValues();
values.put(Registration.ExtensionColumns.CONFIGURATION_ACTIVITY,DemoConfigActivity.class.getName()); //активити, которое будет отображаться в меню "настройки расширения". Если оно нам не нужно - убираем параметр совсем.
values.put(Registration.ExtensionColumns.CONFIGURATION_TEXT,"Настройки демо-расширения");//а это текст, отображащийся в качестве пункта меню программы управления часами. Если оно нам не нужно - убираем параметр совсем.
values.put(Registration.ExtensionColumns.NAME, "Хабра-демо-расширение");//имя, отображаемое в списке приложений внутри программы управления часами
values.put(Registration.ExtensionColumns.EXTENSION_KEY,DemoReceiverService.EXTENSION_KEY); //уникальный ключ расширения
values.put(Registration.ExtensionColumns.HOST_APP_ICON_URI, iconHostapp); //иконка в списке приложений в телефоне
values.put(Registration.ExtensionColumns.EXTENSION_ICON_URI, iconHostapp); //иконка в списке приложений на самих часах, в идеале 48x48
values.put(Registration.ExtensionColumns.NOTIFICATION_API_VERSION,getRequiredNotificationApiVersion());//нужная версия механизма уведомлений
values.put(Registration.ExtensionColumns.PACKAGE_NAME, mContext.getPackageName());
return values;
}
@Override
public int getRequiredNotificationApiVersion() { //нам не нужно управление нотификациями
return 0;
}
@Override
public int getRequiredSensorApiVersion() { //нам не нужна инфа с сенсоров вроде акселерометра
return 0;
}
//---------------------------------------------
//всё что нужно для поддержки виджета
//---------------------------------------------
@Override
public boolean isWidgetSizeSupported(final int width, final int height) {
return (width == WIDGET_WIDTH_SMARTWATCH && height == WIDGET_HEIGHT_SMARTWATCH);
}
@Override
public int getRequiredWidgetApiVersion() { //для поддержки первых часов
return 1;
}
//---------------------------------------------
//всё что нужно для поддержки контроллера
//---------------------------------------------
@Override
public int getRequiredControlApiVersion() { //для поддержки первых часов
return 1;
}
@Override
public int getTargetControlApiVersion() { //для поддержки второй версии часов
return 2;
}
@Override
public boolean isDisplaySizeSupported(int width, int height) {
return (width == CONTROL_WIDTH_SMARTWATCH_2 && height == CONTROL_HEIGHT_SMARTWATCH_2)
|| (width == CONTROL_WIDTH_SMARTWATCH && height == CONTROL_HEIGHT_SMARTWATCH);
}
}
Здесь стоит остановиться поподробнее. Дело в том, что скачанное нами API от Sony – универсальное для целой пачки устройств от Sony, и никто не мешает нам написать приложение (расширение), которое может запуститься на всех этих устройствах разом. Или только на избранных из них.
Раз такое дело, нам надо сообщить, какие размеры экранов и версии API для сенсоров, виджетов и т.п. нам нужно поддержать. Нам нужно указать:
- Поддержка разных сенсоров (акселерометров и т.п.) – getRequiredSensorApiVersion. Нам оно не надо совсем, так что версия API = 0.
- Нотификации (Notification) — всплывающие сообщения-уведомления; нам они тоже не нужны. Так что в getRequiredNotificationApiVersion снова 0.
- “Контроллер” – это то самое “обычное окно программы на часах”. Для него нам нужно определить версию. Кроме того, нам придётся указать поддерживаемые размеры экранов первых и вторых часов, и только их, никакие иные устройства нам не нужны. Поэтому передаём:
- getRequiredControlApiVersion – версию 1 (для поддержки первой версии часов). Если бы передали 2 – поддерживались бы только Smartwatch 2, на первых бы не запустилось.
- getTargetControlApiVersion – целевая версия API, здесь 2 для опять же поддержки Smartwatch 2
- isDisplaySizeSupported – получаем размеры экрана устройства и определяем, хотим ли мы запускаться на нём или нет.
- “Виджет” (Widget) – это изображение в списке виджетов. Аналогично, нужно указать требуемую версию и размеры экрана. Важный момент: вторая версия часов виджеты не поддерживает. Увы.
Плюс пачка параметров в getExtensionRegistrationConfiguration, но там всё понятно из комментариев.
Основное окно программы
Здесь важно осознать следующим момент. На часы в первой версии часов мы можем отправлять только изображения. Картинки. Всё. Ничего больше. Иным способом рисовать мы не можем. Во второй версии появились расширенные контроллеры, но мы-то изначально пишем для поддержки обоих версий, так что только изображения.
Если же вы хотите использовать для рендера возможности Layout, например, отрендерить компоненты – без проблем, но координаты кликов и прочее взаимодействие придётся обрабатывать вручную. Безрадостная перспектива… Но тем не менее. Вот так будет выглядеть наша картинка:
А вот так — код, который будет за всё ответит:
public class DemoControl extends ControlExtension {
static final Rect buttonStopPlaySmartWatch = new Rect(43, 42, 85, 88);
public DemoControl(Context context, String hostAppPackageName) {
super(context, hostAppPackageName);
}
@Override
public void onTouch(final ControlTouchEvent event) {//реакция на клики
if (event.getAction() == Control.Intents.CLICK_TYPE_SHORT) {
if (buttonStopPlaySmartWatch.contains(event.getX(), event.getY())){
MusicBackgroundControlWrapper.TogglePausePlay(mContext);
}
}
}
@Override
public void onSwipe(int direction) {//реакция на жесты
if (direction== Control.Intents.SWIPE_DIRECTION_UP){
MusicBackgroundControlWrapper.VolumeUp(mContext);
}
if (direction==Control.Intents.SWIPE_DIRECTION_DOWN){
MusicBackgroundControlWrapper.VolumeDown(mContext);
}
if (direction==Control.Intents.SWIPE_DIRECTION_LEFT){
MusicBackgroundControlWrapper.Next(mContext);
}
if (direction==Control.Intents.SWIPE_DIRECTION_RIGHT){
MusicBackgroundControlWrapper.Prev(mContext);
}
}
@Override
public void onResume() {//рисуем изображение
Bitmap mPicture = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.control_picture);
showBitmap(mPicture);
}
}
Назначение событий onSwipe и onTouch говорят сами за себя, onResume вызывается каждый раз, как оно программы будет видно, например, часы вышли из спячки или была выбрана иконка приложения. В принципе, этого достаточно для большинства взаимодействий с приложением.
MusicBackgroundControlWrapper – это небольшой самописный класс, предназначенный для управления плеером с использованием эмуляции нажатий мультимедийных клавиш. Нормально работает не со всеми плеерами и телефонами, но там где работает – работает на ура. Если знаете лучший способ (с поддержкой Android 2.3 и выше!) – поделитесь пожалуйста в комментариях.
public class MusicBackgroundControlWrapper {
public static void KeyPressDownAndUp(int key,Context context){
long eventtime = SystemClock.uptimeMillis() - 1;
Intent downIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null);
KeyEvent downEvent = new KeyEvent(eventtime, eventtime,
KeyEvent.ACTION_DOWN, key, 0);
downIntent.putExtra(Intent.EXTRA_KEY_EVENT, downEvent);
context.sendOrderedBroadcast(downIntent, null);
eventtime++;
Intent upIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null);
KeyEvent upEvent = new KeyEvent(eventtime, eventtime,
KeyEvent.ACTION_UP, key, 0);
upIntent.putExtra(Intent.EXTRA_KEY_EVENT, upEvent);
context.sendOrderedBroadcast(upIntent, null);
}
public static void VolumeUp(Context context){
AudioManager audioManager =(AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
int max=audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int current=audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
if (current<max){
current++;
}
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
current,0);
}
public static void VolumeDown(Context context){
AudioManager audioManager =(AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
int current=audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
if (current>0){
current--;
}
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
current,0);
}
public static void TogglePausePlay(Context context){
KeyPressDownAndUp(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,context);
}
public static void Next(Context context){
KeyPressDownAndUp(KeyEvent.KEYCODE_MEDIA_NEXT, context);
}
public static void Prev(Context context){
KeyPressDownAndUp(KeyEvent.KEYCODE_MEDIA_PREVIOUS, context);
}
}
Для поддержки второй версии часов мы унаследуем DemoControl2 от DemoControl, с парой изменений – в onResume() будем передавать другое изображение, а в onTouch – проверять иные координаты.
public class DemoControl2 extends DemoControl {
static final Rect buttonStopPlaySmartWatch2 = new Rect(59, 52, 167, 122);
public DemoControl2(Context context, String hostAppPackageName) {
super(context, hostAppPackageName);
}
@Override
public void onTouch(final ControlTouchEvent event) {//реакция на клики
if (event.getAction() == Control.Intents.CLICK_TYPE_SHORT) {
if (buttonStopPlaySmartWatch2.contains(event.getX(), event.getY())){
MusicBackgroundControlWrapper.TogglePausePlay(mContext);
}
}
}
@Override
public void onResume() {//рисуем изображение
Bitmap mPicture = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.control_picture2);
showBitmap(mPicture);
}
}
Виджет
Итак, виджет. Каноничный виджет имеет разрешение 92x92 пикселя для первой версии часов и не поддерживаются в принципе для второй. Можно растянуть его и на бОльшие разрешения (вплоть до 128x110), но он тогда будет выбиваться из стилистики и закрывать стандартные элементы управления и индикации.
Нам от него понадобится только одно действие – по клику запускать наше основное приложение на часах. Класс, отвечающий за это тоже очень простой, все подробности в комментариях.
public class DemoWidget extends WidgetExtension {
public DemoWidget(Context context, String hostAppPackageName) {
super(context, hostAppPackageName);
}
@Override
public void onStartRefresh() { //Когда виджет становится видимым и/или обновляется.
showBitmap(new DemoWidgetImage(mContext).getBitmap());
}
@Override
public void onStopRefresh() { //Когда виджет перестаёт быть видимым. Нам ничего не нужно делать, мы и так не обновляем его и не анимируем.
}
@Override
public void onTouch(final int type, final int x, final int y) {
if (!SmartWatchConst.ACTIVE_WIDGET_TOUCH_AREA.contains(x, y)) { //если кликнули вне иконки приложения - ничего не делаем
return;
}
//по клику (быстрому или долгому) запускаем основное окно программы
if (type == Widget.Intents.EVENT_TYPE_SHORT_TAP || type==Widget.Intents.EVENT_TYPE_LONG_TAP) {
Intent intent = new Intent(Control.Intents.CONTROL_START_REQUEST_INTENT);
intent.putExtra(Control.Intents.EXTRA_AEA_PACKAGE_NAME, mContext.getPackageName());
intent.setPackage(mHostAppPackageName);
mContext.sendBroadcast(intent, Registration.HOSTAPP_PERMISSION);
}
}
}
Хотя есть там и интересный момент. В комплекте с API, среди утилит есть класс специально для виджетов, самостоятельно рендерящий Layout в картинку. Грех такой возможностью не воспользоваться, хотя бы и в целях обучения. Рендерить будем через класс DemoWidgetImage.
public class DemoWidgetImage extends SmartWatchWidgetImage {
public DemoWidgetImage(Context context) {
super(context);
setInnerLayoutResourceId(R.layout.music_widget_image);
}
@Override
protected void applyInnerLayout(LinearLayout innerLayout) {
//даже если ничего не делаем с содержимым - переопределить обязаны. Угу.
}
}
Окно настроек
Ну тут нужно совсем минимум. Поскольку в классе DemoRegistrationInformation мы уже прописали имя активити, то тут нам сейчас остаётся только заполнить её ну хоть чем-то. Даже комментировать не буду. Просто код.
public class DemoConfigActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.config);
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="Демонстрационный текст"
android:id="@+id/textView" android:layout_gravity="center_horizontal"/>
</LinearLayout>
Как опубликовать приложение в Google Play
Что бы ваше приложение находилось утилитой управления часами в магазине приложений – нужно добавить в текст описания программы на Google Play:
- Для поддержки SmartWatch — “LiveWare extension for SmartWatch”
- Для поддержки SmartWatch 2 – “Smart Connect extension for SmartWatch 2”
- Если нужны оба – добавляем соответственно обе строки.
Что характерно, установить приложение сможет и человек, у которого самих часов нет. Установить, не запустить и влепить минимальную оценку, да. Привыкайте, это мир Google Play! Но нам ведь не важна оценка, нам важно, что мир становится чуточку лучше, верно…?
Что ещё можно доделать в приложении-примере
- Окно настроек (сделать, например инвертирование жестов).
- Более корректный и универсальный способ управления плеерами. В Android 4.4 уже реализован нужный API (Remote controllers кажется называется), а вот для более старых – проблема.
- Сделать (придумать, найти) автоматический расчет координат для объектов, находящихся на вьюшке. Что бы руками не считать каждый раз, вдруг Sony создаст третьи часы с третьим разрешением.
Результат нашей работы
Исходный код примера из статьи
github.com/Newbilius/smartwatch_habra_demo
Источник в лице сайта Sony
developer.sonymobile.com/knowledge-base/sony-add-on-sdk/
И повторюсь, если возникли вопросы по другим фичам часов – смотрите папку examples (полный путь был приведён выше), там есть примеры использования абсолютно всех датчиков и возможностей. Цель этой статьи – дать вам возможность совершить “быстрый старт” и заинтересовать, надеюсь, у меня это получилось сделать.
P.S. Если вам нужно готовое приложение, описанное в этой статье, но нет желание заниматься разработкой – в Google Play уже есть такое, и даже более функциональные — но или платные, или не совсем такие или с подобными же недостатками.
P.P.S. У меня нет на руках второй версии часов, так что всё что о них написано — это информация из примеров или документации, плюс проверка на эмуляторе, на реальных часах второй версии не проверял. Первая же версия часов есть, там всё точно и проверено.
Автор: Newbilius