Wear 上のネットワーク アクセスと同期

Wear OS by Google 搭載のスマートウォッチは、Android または iOS スマートフォンにアクセスせずにネットワークと直接通信できます。このネットワークへの直接アクセスにより、Data Layer API を(Wear 1.x で)使用せずにネットワークに接続することができます。

ネットワーク アクセス

Wear OS アプリはネットワーク リクエストを送信できます。スマートウォッチからスマートフォンへの Bluetooth 接続では、通常、スマートウォッチのネットワーク トラフィックはスマートフォンを介してプロキシ接続されます。ただし、スマートフォンを使用できない場合は、ハードウェアに応じて Wi-Fi ネットワークとモバイル ネットワークが使用されます。ネットワーク間の遷移は Wear プラットフォームによって処理されます。

HTTP、TCP、UDP などのプロトコルは使用できますが、android.webkit API(CookieManager クラスを含む)は使用できません。リクエストとレスポンスのヘッダーの読み取りと書き込みを行うと、Cookie を使用できます。

また、以下を使用することをおすすめします。

  • 非同期ジョブ(一定間隔でのポーリングなど)用の JobScheduler API(下記を参照)
  • マルチネットワーキング API(特定のタイプのネットワークに接続する必要がある場合。詳しくは複数のネットワーク接続を参照)

高帯域幅ネットワークへのアクセス

Wear OS プラットフォームは、全体的なユーザー エクスペリエンスを最大限に高めることを目標としてネットワーク接続を管理します。このプラットフォームは、デフォルトのアクティブなネットワークを選択するうえで、以下の 2 つの要素のバランスを考慮します。

  • 電池の節約に関するニーズ
  • ネットワーク帯域幅に関するニーズ

電池の節約を優先すると、アクティブなネットワークにおける帯域幅の不足により、高帯域幅を必要とするネットワーク タスク(大きなファイルの転送やメディアのストリーミング)を実行できなくなることがあります。

このセクションでは、ConnectivityManager クラスを使用して、アプリに必要なネットワーク帯域幅を使用できるようにする方法について説明します。ネットワーク リソースをきめ細かく管理する一般的な方法については、ネットワーク使用状況を管理するをご覧ください。

高帯域幅ネットワークを確保する

Wear OS では、高帯域ネットワークを常に利用できるとは限りません。高帯域幅ネットワークにアクセスする必要があるユースケース(大きなファイルの転送やメディアのストリーミング)では、次の手順を行うことをおすすめします。

  1. アクティブなネットワークが存在するかどうかを確認し、存在する場合はその帯域幅を確認します。
  2. アクティブなネットワークが存在しないか、その帯域幅が不十分な場合は、定額制の Wi-Fi ネットワークまたはモバイル ネットワークへのアクセスをリクエストします。

ConnectivityManager クラスを使用すると、アクティブなネットワークが存在するかどうか、その帯域幅が十分かどうかを確認できます。

Kotlin

const val MIN_BANDWIDTH_KBPS = 320
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val bandwidth: Int = connectivityManager.activeNetwork?.let { activeNetwork ->
    connectivityManager.getNetworkCapabilities(activeNetwork).linkDownstreamBandwidthKbps
} ?: -1

when {
    bandwidth < 0 -> {
        // No active network
    }
    bandwidth in (0 until MIN_BANDWIDTH_KBPS) -> {
        // Request a high-bandwidth network
    }
    else -> {
        // You already are on a high-bandwidth network, so start your network request
    }
}

Java

int MIN_BANDWIDTH_KBPS = 320;
connectivityManager =
  (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
Network activeNetwork = connectivityManager.getActiveNetwork();

if (activeNetwork != null) {
  int bandwidth =
    connectivityManager.getNetworkCapabilities(activeNetwork).getLinkDownstreamBandwidthKbps();

  if (bandwidth < MIN_BANDWIDTH_KBPS) {
    // Request a high-bandwidth network
  } else {
    // You already are on a high-bandwidth network, so start your network request
  }
} else {
  // No active network
}

定額制の高帯域幅ネットワークをリクエストするには、ConnectivityManager を使用します。1 つのネットワーク リクエストで定額制の Wi-Fi ネットワークまたはモバイル ネットワークをリクエストできます。ネットワークの準備が整うと(たとえば デバイスの Wi-Fi 無線が保存済みネットワークに接続されると)、NetworkCallback インスタンスの onAvailable() メソッドが呼び出されます。適切なネットワークが見つからない場合、onAvailable() メソッドは呼び出されません。そのため、リクエストを手動でタイムアウトさせる必要があります。詳しくは、ネットワークを使用できるようになるまで待機するをご覧ください。

Kotlin

networkCallback = object : ConnectivityManager.NetworkCallback() {
    override fun onAvailable(network: Network) {
        if (bindProcessToNetwork(network)) {
            // socket connections will now use this network
        } else {
            // app doesn't have android.permission.INTERNET permission
        }
    }
}

val request: NetworkRequest = NetworkRequest.Builder().run {
    addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
    addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
    addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
    addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    build()
}

connectivityManager.requestNetwork(request, networkCallback)

Java

networkCallback = new ConnectivityManager.NetworkCallback() {
  @Override
  public void onAvailable(Network network) {
    if (bindProcessToNetwork(network)) {
      // socket connections will now use this network
    } else {
      // app doesn't have android.permission.INTERNET permission
    }
  }
};

NetworkRequest request = new NetworkRequest.Builder()
  .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
  .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
  .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
  .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
  .build();

connectivityManager.requestNetwork(request, networkCallback);

ネットワークを解放する

アプリで高帯域幅ネットワークが不要になったら、ConnectivityManager クラスを使用してネットワークを解放し、プラットフォームがネットワーク アクセスの管理を再開できるようにする必要があります。

Kotlin

connectivityManager.bindProcessToNetwork(null)
connectivityManager.unregisterNetworkCallback(networkCallback)

Java

connectivityManager.bindProcessToNetwork(null);
connectivityManager.unregisterNetworkCallback(networkCallback);

電池の消費を最適化するには、ネットワーク接続の登録をアクティビティの存続期間中のみ維持するようにします。そのため、アクティビティの onStop() メソッドでネットワークを解放することを検討する必要があります。

ネットワークを使用できるようになるまで待機する

スマートウォッチの Wi-Fi ネットワークまたはモバイル ネットワークは、電池を節約するために切断されることがあるので、ネットワークをすぐに確保できない場合があります。また、スマートウォッチがネットワークに接続できない場合、NetworkCallback インスタンスの onAvailable() メソッドは呼び出されません。そのため、あらかじめ決められた時間が経過したらリクエストをタイムアウトさせて、関連リソースを解放する必要があります。

Kotlin

const val MESSAGE_CONNECTIVITY_TIMEOUT = 1
const val NETWORK_CONNECTIVITY_TIMEOUT_MS: Long = 10000
...
handler = MyHandler()

networkCallback = object : ConnectivityManager.NetworkCallback() {
    override fun onAvailable(network: Network) {
        handler.removeMessages(MESSAGE_CONNECTIVITY_TIMEOUT)
        ...
    }
}

connectivityManager.requestNetwork(request, networkCallback)

handler.sendMessageDelayed(
        handler.obtainMessage(MESSAGE_CONNECTIVITY_TIMEOUT),
        NETWORK_CONNECTIVITY_TIMEOUT_MS
)
...
// Don't make this an inner class otherwise there is the potential to leak the parent class
private class MyHandler : Handler() {
    override fun handleMessage(msg: Message) {
        when (msg.what) {
            MESSAGE_CONNECTIVITY_TIMEOUT -> {
                // unregister the network
            }
        }
    }
}

Java

int MESSAGE_CONNECTIVITY_TIMEOUT = 1;
long NETWORK_CONNECTIVITY_TIMEOUT_MS = 10000;

handler = new MyHandler();

networkCallback = new ConnectivityManager.NetworkCallback() {
  @Override
  public void onAvailable(Network network) {
    handler.removeMessages(MESSAGE_CONNECTIVITY_TIMEOUT);
    ...
  }
};

connectivityManager.requestNetwork(request, networkCallback);

handler.sendMessageDelayed(
  handler.obtainMessage(MESSAGE_CONNECTIVITY_TIMEOUT),
  NETWORK_CONNECTIVITY_TIMEOUT_MS);

...
// This needs to be static to avoid potentially leaking the parent class
private static class MyHandler extends Handler {
   @Override
   public void handleMessage(Message msg) {
       switch (msg.what) {
           case MESSAGE_CONNECTIVITY_TIMEOUT:
               // unregister the network
               break;
       }
   }
}

ネットワークの状態をモニタリングする

NetworkCallback インターフェースには、バインドされているネットワークの状態の変化(帯域幅の変化や接続の切断)をモニタリングするためのメソッドが用意されています。

Kotlin

networkCallback = object : ConnectivityManager.NetworkCallback() {
    override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
        val bandwidth: Int = networkCapabilities.linkDownstreamBandwidthKbps

        if (bandwidth < MIN_BANDWIDTH_KBPS) {
            // handle insufficient network bandwidth
        }
    }

    override fun onLost(network: Network) {
        // handle network loss
    }
}

Java

networkCallback = ConnectivityManager.NetworkCallback {
  @Override
  public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
    int bandwidth = networkCapabilities.getLinkDownstreamBandwidthKbps();

      if (bandwidth < MIN_BANDWIDTH_KBPS) {
        // handle insufficient network bandwidth
      }
  }

  @Override
  public void onLost(Network network) {
    // handle network loss
  }
}

Wi-Fi 設定アクティビティを開始する

Wi-Fi ネットワークをリクエストすると、保存済みネットワークが設定されていて範囲内にある場合、システムは保存済みネットワークに接続しようとします。ただし、使用可能な保存済み Wi-Fi ネットワークが存在しない場合、NetworkCallback インスタンスの onAvailable() コールバック メソッドは呼び出されません。ネットワーク リクエストをタイムアウトさせるために Handler を使用している場合は、タイムアウトが発生したときに Wi-Fi ネットワークを追加するようにユーザーを誘導できます。次のインテントを使用して、Wi-Fi ネットワークを追加するアクティビティに直接ユーザーを誘導できます。

Kotlin

context.startActivity(Intent("com.google.android.clockwork.settings.connectivity.wifi.ADD_NETWORK_SETTINGS"))

Java

context.startActivity(new Intent("com.google.android.clockwork.settings.connectivity.wifi.ADD_NETWORK_SETTINGS"));

設定アクティビティを開始するには、アプリに android.permission.CHANGE_WIFI_STATE 権限が付与されている必要があります。

ユーザー インターフェースに関する考慮事項

アプリが高帯域幅オペレーションのために新しい Wi-Fi ネットワークへの接続を必要とする場合は、Wi-Fi の設定を開始する前に、接続する理由をユーザーに明確に示す必要があります。高帯域幅ネットワークが必要な場合にのみ、新しい Wi-Fi ネットワークを追加するようユーザーにリクエストします。高帯域幅ネットワークを必要としないアプリの機能をユーザーが利用することを妨げないようにしてください。

図 1 は音楽アプリの例を示しています。このアプリは、音楽をブラウジングする機能をユーザーに提供し、音楽をダウンロードまたはストリーミングする場合にのみ、新しい Wi-Fi ネットワークを追加するようユーザーにリクエストします。

音楽のダウンロード

図 1: 音楽アプリのダウンロード フロー

アプリが動作するうえで高帯域幅ネットワークを必要とする場合は、新しい Wi-Fi ネットワークを追加するようユーザーにリクエストする前に、その明確な根拠をユーザーに示す必要があります。また、時間がかかるネットワーク オペレーション(ユーザーのメディア プレイリストのダウンロードなど)では、実行中のオペレーションの説明とともに進行状況インジケーターを表示する必要があります。

図 2 は、音楽アプリのストリーミング フローを示しています。音楽をストリーミングするためにアプリが高帯域幅ネットワークを必要とする場合は、ユーザーを Wi-Fi の設定に誘導する前に、新しい Wi-Fi ネットワークが必要な理由を明確に説明する必要があります。

音楽のストリーミング

図 2. 音楽アプリのストリーミング フロー

クラウド メッセージング

アプリが通知を送信する場合、Google Cloud Messaging(GCM)の後継機能である Firebase Cloud Messaging(FCM)を直接使用できます。FCM は Wear 2.0 でサポートされていますが、GCM は Wear 2.0 ではサポートされていません。

ネットワーク アクセス用または FCM 用の API に、Wear OS に固有のものはありません。ネットワークへの接続クラウド メッセージングについては、既存のドキュメントをご確認ください。

FCM は Doze モードで適切に機能するので、スマートウォッチに通知を送信するのに適しています。

Wear アプリの実行中にデバイスの登録トークンを収集することで、FCM からのメッセージに備えます。さらに、サーバーから FCM REST エンドポイントにメッセージを送信する際に、このトークンを宛先の一部として含めます。FCM は、トークンで指定されたデバイスにメッセージを送信します。

FCM メッセージは JSON 形式であり、以下のペイロードのいずれかまたは両方を含めることができます。

  • 通知ペイロード: 通知ペイロードをスマートウォッチが受け取ると、通知ストリーム内でデータがユーザーに直接表示されます。ユーザーが通知をタップすると、アプリが起動します。
  • データ ペイロード: このペイロードにはカスタムの Key-Value ペアのセットが含まれており、Wear アプリにデータとして配信されます。

ペイロードの詳細と例については、FCM メッセージについてをご覧ください。

デフォルトでは、通知はスマートフォン アプリからスマートウォッチにブリッジ(共有)されます。スタンドアロンの Wear アプリとそれに対応するスマートフォン アプリを使用している場合、通知が重複することがあります。たとえば、スマートフォンとスマートウォッチの両方が FCM から受け取った同じ通知が、両方のデバイスで別々に表示されることがあります。

バックグラウンド サービスを使用する

バックグラウンド タスクが適切に実行されるようにするには、各タスクで Doze を考慮する必要があります。Android 6.0 では、Doze とアプリ スタンバイによって電池寿命が向上しました。

Doze は Android Nougat と Wear OS で機能拡張されています。画面がオフになるか、常に画面表示モードの状態が長く続くと、Doze のサブセットが開始され、バックグラウンド タスクが一定期間遅延されることがあります。その後、デバイスの操作が長時間行われないと、通常の Doze が開始されます。

ジョブのスケジュール設定には JobScheduler API を使用する必要があります。これにより、アプリは Doze 対応のコード実行を登録できるようになります。ジョブをスケジュール設定する際に、定期実行などの制約と、接続性またはデバイスの充電に関するニーズを選択できます。電池寿命に悪影響を及ぼさないようにジョブを設定してください。ジョブでは、JobInfo.Builder オブジェクトを使用して、制約とメタデータを指定する必要があります。たとえば、1 つのタスクに対して以下のメソッドを 1 つ以上使用します。

  • ネットワーク接続を必要とするタスクのスケジュールを設定するには、setRequiredNetworkType(int networkType) を使用して NETWORK_TYPE_ANY または NETWORK_TYPE_UNMETERED を指定します。NETWORK_TYPE_UNMETERED は大規模なデータ転送、NETWORK_TYPE_ANY は小規模なデータ転送に使用します。
  • 充電中のタスクのスケジュールを設定するには、setRequiresCharging(boolean requiresCharging) を使用します。
  • デバイスがアイドル状態のときのタスクのスケジュールを設定するには、setRequiresDeviceIdle(boolean requiresDeviceIdle) を使用します。このメソッドは、優先度が低いバックグラウンド処理または同期処理で、特に setRequiresCharging とともに使用すると便利です。

一部の低帯域幅ネットワーク(Bluetooth LE など)は従量制と見なされることにご注意ください。

制約を含むスケジュールを設定する

制約を必要とするタスクのスケジュールを設定できます。下記の例の JobScheduler オブジェクトは、次の制約を満たす場合に MyJobService をアクティブ化します。

  • ネットワークが定額制
  • デバイスが充電中

ビルダー メソッド setExtras を使用すると、アプリ固有のメタデータのバンドルをジョブ リクエストにアタッチできます。ジョブの実行時に、このバンドルがジョブサービスに提供されます。JobInfo.Builder コンストラクタに渡される MY_JOB_ID 値にご注意ください。この MY_JOB_ID の値は、アプリから提供される識別子です。キャンセルを行う後続の呼び出しと、同じ値を使って作成された後続のジョブにより、既存のジョブが更新されます。

Kotlin

JobInfo.Builder(MY_JOB_ID,
        ComponentName(this, MyJobService::class.java))
        .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
        .setRequiresCharging(true)
        .setExtras(extras)
        .build()
        .also { jobInfo ->
            (getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler)
                    .schedule(jobInfo)
        }

Java

JobInfo jobInfo = new JobInfo.Builder(MY_JOB_ID,
        new ComponentName(this, MyJobService.class))
        .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
        .setRequiresCharging(true)
        .setExtras(extras)
        .build();
((JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE))
        .schedule(jobInfo);

上記のジョブを処理する JobService の実装を下記に示します。ジョブが実行されると、JobParameters オブジェクトが onStartJob メソッドに渡されます。JobParameters オブジェクトを通じて、ジョブ ID の値と、ジョブのスケジュール設定の際に提供されるエクストラのバンドルを取得できます。onStartJob メソッドはメインのアプリスレッドで呼び出されるため、高コストのロジックは別のスレッドで実行する必要があります。下記の例では、バックグラウンドでコードを実行するために AsyncTask を使用しています。タスクが完了したら、jobFinished メソッドを呼び出して、タスクが完了したことを JobScheduler に通知します。

Kotlin

private class MyJobService : JobService() {

    override fun onStartJob(params: JobParameters): Boolean {
        JobAsyncTask().execute(params)
        return true
    }

    private class JobAsyncTask : AsyncTask<...>() { ... }
}

Java

public class MyJobService extends JobService {
    @Override public boolean onStartJob(JobParameters params) {
        new JobAsyncTask().execute(params);
        return true;
    }

    private class JobAsyncTask extends AsyncTask