Опыт разработки под Android Wear

Спешу поделиться с коллегами накопленным опытом при разработке для Android Wear.

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

Загрузим Android Studio.

Создадим новый проект:



Выбираем оба устройства:



Далее все стандартно:






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



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

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

Вы должны подключиться к GoogleApiClient, потом отправить в отдельном потоке сообщение. В примерах это описано подробно, но я решил вынести всю работу с сообщениями в отдельный сервис и получилось довольно компактно.
Вот наш ListenerService, он одинаков для обоих частей проекта.

package com.rusdelphi.batterywatcher;

import android.content.Intent;
import android.content.SharedPreferences;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.wearable.MessageApi;
import com.google.android.gms.wearable.MessageEvent;
import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.NodeApi;
import com.google.android.gms.wearable.Wearable;
import com.google.android.gms.wearable.WearableListenerService;

import java.util.concurrent.TimeUnit;

/**
 * Created by User on 04.01.2015.
 */
public class ListenerService extends WearableListenerService {

    private SharedPreferences prefs;
    private boolean mIsAlarmOn;
    GoogleApiClient googleClient;
    public static final String ACTION_SM = "com.rusdelphi.batterywatcher.action.SM";
    public static final String ACTION_SM_PARAM = "com.rusdelphi.batterywatcher.action.SM.PARAM";
    private static final String WEAR_MESSAGE_PATH = "batterywatcher_message_path";

    public ListenerService() {

    }

    @Override
    public void onCreate() {
        super.onCreate();
        googleClient = new GoogleApiClient.Builder(this)
                .addApi(Wearable.API)
                .build();
        googleClient.connect();
    }

    @Override
    public void onDestroy() {
        if (null != googleClient && googleClient.isConnected()) {
            googleClient.disconnect();
        }
        super.onDestroy();
    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent != null) {
            final String action = intent.getAction();
            if (ACTION_SM.equals(action)) {
                final String param1 = intent.getStringExtra(ACTION_SM_PARAM);
                if (googleClient.isConnected()) {
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(googleClient).await();
                            for (Node node : nodes.getNodes()) {
                                MessageApi.SendMessageResult result = Wearable.MessageApi.sendMessage(googleClient, node.getId(), WEAR_MESSAGE_PATH, param1.getBytes()).await();
                                if (result.getStatus().isSuccess()) {
                                    Log.d("main", "Message: {" + param1 + "} sent to: " + node.getDisplayName());
                                } else {
                                    // Log an error
                                    Log.d("main", "ERROR: failed to send Message");
                                }
                            }
                        }
                    }).start();

                }
                if (!googleClient.isConnected()) new Thread(new Runnable() {
                    @Override
                    public void run() {
                        ConnectionResult connectionResult = googleClient.blockingConnect(30, TimeUnit.SECONDS);

                        NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(googleClient).await();
                        for (Node node : nodes.getNodes()) {
                            MessageApi.SendMessageResult result = Wearable.MessageApi.sendMessage(googleClient, node.getId(), WEAR_MESSAGE_PATH, param1.getBytes()).await();
                            if (result.getStatus().isSuccess()) {
                                Log.d("main", "Message: {" + param1 + "} sent to: " + node.getDisplayName());
                            } else {
                                // Log an error
                                Log.d("main", "ERROR: failed to send Message");
                            }
                        }

                    }
                }).start();
            }
        }
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onMessageReceived(MessageEvent messageEvent) {

        if (messageEvent.getPath().equals(WEAR_MESSAGE_PATH)) {
            final String message = new String(messageEvent.getData());  
            Intent messageIntent = new Intent();
            messageIntent.setAction(Intent.ACTION_SEND);
            messageIntent.putExtra("message", message);
            LocalBroadcastManager.getInstance(this).sendBroadcast(messageIntent);
        } else {
            super.onMessageReceived(messageEvent);
        }
    }
}


В оба манифеста нужно его добавить:

<service android:name=".ListenerService">
            <intent-filter>
                <action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
            </intent-filter>
        </service>

Во время создания сервиса, мы собираем GoogleApiClient и подключаемся к слою данных.

Событие onMessageReceived вызывается при получении сообщения. У полученного события (MessageEvent) мы смотрим папку назначения (getPath()). Если это наша папка, получаем данные (messageEvent.getData()). Далее эти данные можно сохранить в настройки, базу данных. В общем, использовать как будет нужно. А мы с помощью LocalBroadcastManager отправим их в нашу основную программу (MainActivity). Но для этого локальный приёмник в ней нужно зарегистрировать. Мы это будем делать при старте, а разрегистрировать будем при выходе.

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

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

Вот код MainActivity для мобильного устройства:

package com.rusdelphi.batterywatcher;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;


public class MainActivity extends Activity {
    public static String mWatchLevel = "?", mSmartphoneLevel = "?";
    private TextView mWatch;
    private TextView mSmartphone;
    MessageReceiver messageReceiver = new MessageReceiver();
    private BroadcastReceiver mBatteryLevelReceiver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mWatch = (TextView) findViewById(R.id.tv_watch);
        mSmartphone = (TextView) findViewById(R.id.tv_smartphone);
        IntentFilter batteryLevelFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
        mBatteryLevelReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent i) {
                int level = i.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
                int scale = i.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
                mSmartphoneLevel = new java.text.DecimalFormat("0.0")
                        .format((((float) level / (float) scale) * 100.0f)) + "%";
                sendMessage(MainActivity.this, mSmartphoneLevel);
                updateUI();
            }
        };
        registerReceiver(mBatteryLevelReceiver, batteryLevelFilter);
        IntentFilter messageFilter = new IntentFilter(Intent.ACTION_SEND);
        LocalBroadcastManager.getInstance(this).registerReceiver(messageReceiver, messageFilter);
    }

    public static void sendMessage(Context context, String param1) {
        Intent intent = new Intent(context, ListenerService.class);
        intent.setAction(ListenerService.ACTION_SM);
        intent.putExtra(ListenerService.ACTION_SM_PARAM, param1);
        context.startService(intent);
    }

    public void updateUI() {
        mWatch.setText(mWatchLevel);
        mSmartphone.setText(mSmartphoneLevel);
    }

    @Override
    protected void onResume() {
        super.onResume();
        updateUI();
    }


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    protected void onDestroy() {
        LocalBroadcastManager.getInstance(this).unregisterReceiver(messageReceiver);
        if (mBatteryLevelReceiver!=null) {
            unregisterReceiver(mBatteryLevelReceiver);
            mBatteryLevelReceiver=null;
        }
        super.onDestroy();
    }

    public class MessageReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String message = intent.getStringExtra("message");
            mWatchLevel = message;
            updateUI();
        }

    }
}


Тут мы при старте создаем приемник, получающий сообщения о батареи устройства. Как только получили сообщение (onReceive), отправляем его сообщением в слой данных (sendMessage) и обновляем значения переменных (updateUI). Далее регистрируем локальный приемник (MessageReceiver), он при приеме также обновит экран приложения (updateUI).

Вот код MainActivity для wear-устройства, т.е. для часов:

package com.rusdelphi.batterywatcher;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.support.wearable.view.WatchViewStub;
import android.widget.TextView;

public class MainActivity extends Activity {
    public static String mWatchLevel = "?", mSmartphoneLevel = "?";
    private TextView mWatch;
    private TextView mSmartphone;
    MessageReceiver messageReceiver = new MessageReceiver();
    private BroadcastReceiver mBatteryLevelReceiver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);
        stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
            @Override
            public void onLayoutInflated(WatchViewStub stub) {
                mWatch = (TextView) stub.findViewById(R.id.tv_watch);
                mSmartphone = (TextView) stub.findViewById(R.id.tv_smartphone);
                updateUI();
            }
        });
        IntentFilter batteryLevelFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
        mBatteryLevelReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent i) {
                int level = i.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
                int scale = i.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
                mWatchLevel = new java.text.DecimalFormat("0.0")
                        .format((((float) level / (float) scale) * 100.0f)) + "%";
                sendMessage(MainActivity.this, mWatchLevel);
                updateUI();
            }
        };
        registerReceiver(mBatteryLevelReceiver, batteryLevelFilter);
        IntentFilter messageFilter = new IntentFilter(Intent.ACTION_SEND);
        LocalBroadcastManager.getInstance(this).registerReceiver(messageReceiver, messageFilter);
    }

    public void updateUI() {
        if (mWatch != null)
            mWatch.setText(mWatchLevel);
        if (mSmartphone != null)
            mSmartphone.setText(mSmartphoneLevel);
    }

    public static void sendMessage(Context context, String param1) {
        Intent intent = new Intent(context, ListenerService.class);
        intent.setAction(ListenerService.ACTION_SM);
        intent.putExtra(ListenerService.ACTION_SM_PARAM, param1);
        context.startService(intent);
    }

    @Override
    protected void onDestroy() {
        LocalBroadcastManager.getInstance(this).unregisterReceiver(messageReceiver);
        if (mBatteryLevelReceiver != null) {
            unregisterReceiver(mBatteryLevelReceiver);
            mBatteryLevelReceiver = null;
        }
        super.onDestroy();
    }

    public class MessageReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String message = intent.getStringExtra("message");
            mSmartphoneLevel = message;
            updateUI();
        }
    }

}


Он, в принципе, похож на старшего брата, но есть отличия в постройке разметки, т.к. устройства могут быть с квадратными и круглыми экранами. Компонент WatchViewStub дает нам возможность упростить выбор разметки, для этого нужно просто спроектировать 2 файла rect_activity_main.xml и round_activity_main.xml.

Вот код первого:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity"
    tools:deviceIds="wear_square">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true">

        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

            <ImageView
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:id="@+id/imageView"
                android:src="@drawable/watch" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/tv_watch"
                android:layout_gravity="bottom"
                style="@style/TextAppearance.Wearable.Large" />
        </LinearLayout>

        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="5dp">

            <ImageView
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:id="@+id/imageView2"
                android:src="@drawable/smartphone" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/tv_smartphone"
                android:layout_gravity="bottom"
                style="@style/TextAppearance.Wearable.Large" />
        </LinearLayout>


    </LinearLayout>
</RelativeLayout>


Вот второго:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    tools:deviceIds="wear_round">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true">

        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

            <ImageView
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:id="@+id/imageView"
                android:src="@drawable/watch" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/tv_watch"
                android:layout_gravity="bottom"
                style="@style/TextAppearance.Wearable.Large" />
        </LinearLayout>

        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="5dp">

            <ImageView
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:id="@+id/imageView2"
                android:src="@drawable/smartphone" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/tv_smartphone"
                android:layout_gravity="bottom"
                style="@style/TextAppearance.Wearable.Large" />
        </LinearLayout>


    </LinearLayout>
</RelativeLayout>


Здесь при создании разметки мы получаем два компонента TextView и обновляем их содержание (updateUI). Остальной код работает так же как и в мобильной версии. Запускаем приложение, получаем текущий уровень батареи, отправляем его соседнему устройству и слушаем его сообщения. Как что-то изменилось -> обновляем показания.

При публикации приложения в гугл маркете у обоих модулей должен быть одинакова версия (versionCode) кода и одинаковое имя пакета. По умолчанию Android Studio эту работу за нас сделает. Когда соберем apk файл для мобильного устройства, внутри него будет находиться apk для wear. Эту работу можно сделать и в Eclipse. В общем, кому как проще. При установке приложения из маркета на мобильное устройство придет толстый apk, который сам установит apk для wear устройства.

Скриншоты с устройств:


  • +4
  • 14.9k
  • 8
Share post

Comments 8

    0
    А что такие маленькие скриншоты? С ними статья кажется маленькой.
      –1
      Опят разработки под Android Wear
      Эти опята? )
        0
        Скажите а какое железо поддерживается Android Wear? Хочу на nrf51822
          0
          Точно знаю Sony SmartWatch3. Вот полезные сслылки Список
          А тут вроде как проверялка наверно нужно заходить с устройства.
          0
          А как бы вы реализовали эту задачку, если бы не было хитрого слоя для обмена сообщениями?
          В двух словах?
            0
            Через блютус. По сути писать свой такой же слой.
            0
            Доступ к сенсорам там есть, типа аксель, гироскоп, магнетометр?
              0
              Да есть, получать показания можно так же, как и раньше

            Only users with full accounts can post comments. Log in, please.