Статья предназначена для тех, кто уже хотя бы минимально знает, с какой стороны держать инструменты для разработки под 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); } }
Виджет
Итак, виджет. Каноничный виджет имеет разрешение 92×92 пикселя для первой версии часов и не поддерживаются в принципе для второй. Можно растянуть его и на бОльшие разрешения (вплоть до 128×110), но он тогда будет выбиваться из стилистики и закрывать стандартные элементы управления и индикации.
Нам от него понадобится только одно действие – по клику запускать наше основное приложение на часах. Класс, отвечающий за это тоже очень простой, все подробности в комментариях.
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 уже есть такое. В нём уже есть инвертирование горизонтальных жестов, возможность скрыть кнопку “пауза”, плюс режим с “кнопками” вместо жестов. Надеюсь, это не будет посчитано рекламой, ибо ни ссылок, ни названия тут не будет, да и тематика статьи явно иная. Если администрация посчитает этот абзац рекламой – я его удалю.
ссылка на оригинал статьи http://habrahabr.ru/post/210898/