デベロッパー リレーション エンジニア
ユーザーがスマートフォン、ウェアラブル、オーディオ周辺機器を切り替えて使用する中で、エンゲージメントを高めるには、スムーズなクロスデバイス エクスペリエンスを提供することが不可欠になっています。しかし、標準的なネットワーキング アプローチに依存すると、バッテリーの消耗、レイテンシの急増、ユーザー セッションの断片化につながることがよくあります。
この特集では、マルチデバイス通信用に構築された専用の Android API を紹介します。これらの API は、メモリと電力を効率的に使用するパターンを提供し、デバイス間の移行を迅速、確実、直感的に行えるようにします。
文字起こし
こんにちは。
「シームレスなクロスデバイス インタラクションの実現」へようこそ。
Hanrui です。本日は、同僚の Yacine と Nataraj も参加しています。
接続性というと、基本的なネットワーク リンクや Bluetooth ヘッドセットを思い浮かべることが多いでしょう。
Android では、接続性はそれよりもはるかに広範なものです。
これは、デバイスの連携を強化することを目的としています。
本日は、一般的なクロスデバイス シナリオをいくつか取り上げ、それぞれに適した API をご紹介します。
長い間、アプリはスマートフォンという 1 つのデバイス向けに構築されていました。
その後、ウェアラブルが普及しました。
やがて、スマートウォッチをスマートフォンとペア設定し、バックグラウンドでデータを同期したり、直接通話したりすることが一般的になりました。
ワイヤレスイヤホンを追加。
通話中に、スマートウォッチからスマートフォン、イヤホン、車へとオーディオ機器を切り替えることができます。
最近では、折りたたみ式デバイスやタブレットなど、2 台目の大画面デバイスを持ち歩くユーザーも増えています。
ユーザーは、これらのデバイス間でアプリがスムーズに動作することを期待しています。
この機能はノートパソコンや Chromebook にも拡張されており、ユーザーはデスクから Android アプリを実行して接続を維持できます。
2023 年の時点で、平均的なユーザーは 3 ~ 4 台のデバイスを所有していました。
ユーザーは、これらのデバイスに単独の機能以上のものを期待しています。
お客様は、連携効果の高いエクスペリエンスを求めています。
この「連携による優れたエクスペリエンス」は、今日のように同じ部屋にいる人たちと、写真、リンク、アプリを簡単に共有したい場合に特に実感できます。
数字を見ると、エコシステムは巨大で、急速に成長しています。
今年、世界中の IoT デバイスの数は約 220 億台に達し、前年比で 14% 増加しています。
これらのエンドポイントのうち、Wi-Fi が 3 分の 1、Bluetooth が 4 分の 1 を占めています。
クロスデバイス接続は、もはや特殊なケースではありません。
AI は私たちの日常生活の一部になっています。
しかし、現在でもほとんどのアプリは、低レベルのネットワーク API と Bluetooth API を使用してクロスデバイス機能を構築しています。これには 3 つの課題があります。
1 つ目は、バッテリーとメモリです。
バックグラウンド スキャンとサービスを実行し続けると、バッテリーとメモリが消耗します。
2 つ目はレイテンシです。
予測できない遅延により、リアルタイムの通話やデータ同期が中断される。
3 つ目は、接続の摩擦です。
すべてのデバイスで検出、ペア設定、再接続を管理することは複雑であり、ユーザー エクスペリエンスの低下につながりやすくなります。
こうした課題を解決するため、Google は 3 つの主要な分野にわたって専用の API を構築し、クロスデバイス機能を非常にシンプルにしました。
まず、私がコンパニオン アプリについて説明し、次に Nataraj が VoIP アプリについて、最後に Yacine が P2P 共有について説明します。
まず、コンパニオン アプリについて見ていきましょう。
コンパニオン アプリとは、スマートウォッチや血糖値センサーなどのコンパニオン デバイスをスマートフォンと連携させるためにスマートフォンにインストールするアプリです。
このセクションでは、Bluetooth 接続デバイスについて説明します。
コンパニオン デバイスは通常、4 つのカテゴリに分類されます。
1 つ目は、グルコース センサーやバイタルサイン モニターなどの医療機器です。
スマートウォッチやフィットネス バンドなどのウェアラブル。
ヘッドセット(ファスト ペアリングとスマート コントロールに対応)
自動車用デジタルキーやハンズフリー通話などの車載キット。
各カテゴリには独自の主要なユーザー ジャーニーがありますが、すべてにおいて、デバイスとスマートフォンの間の信頼性の高い接続に依存しています。
医療機器の場合、その接続は生命に関わることもあります。
これらのカテゴリに共通する 4 つのニーズについて見ていきましょう。
1 つ目はデバイス管理です。
アプリは、イヤホンやスピーカーではなく、このスマートウォッチのように、近くにあるすべてのデバイスの中から適切なデバイスを見つけて、ペア設定フローを開始して接続する必要があります。
2 つ目は、存在確認です。
アプリは、デバイスが近くにあるときと、デバイスがなくなったときを把握する必要があります。
デバイスが検出されたら、アプリを起動して、ユーザーがアプリを開いていなくても処理を実行できるようにする必要があります。
3 つ目は、接続のメンテナンスです。
接続が確立されたら、アプリはその接続が信頼性が高く、持続可能であることを必要とします。
接続が切断された場合(ユーザーが範囲外に移動してから戻ってきた場合など)、アプリは自動的に再接続する必要があります。その時点でアプリが実行されていなくても、再接続する必要があります。
4 つ目は、データの同期と転送です。
ここにこそ、真の価値があります。
センサーの読み取り値の同期、ファームウェアの更新のプッシュ、デバイスへの設定の送信。
医療機器の場合、これは非常に重要です。
測定値が取得されないと、アラートが送信されない可能性があります。
これらはすべて、アプリのプロセスが適切なタイミングで実行され、接続されている必要があります。
では、アプリはこれらのニーズをどのように満たしてきたのでしょうか。
従来、アプリはフォアグラウンド サービスに大きく依存していました。
一般的な実装は次のとおりです。
コンパニオン アプリを作成したことがある場合は、このコードに見覚えがあるかもしれません。
Bluetooth のスキャンと接続の権限、フォアグラウンド サービスの権限、接続済みデバイスのフォアグラウンド サービスのタイプを申告し、フォアグラウンド サービスは永続的な通知を表示する必要があるため、通知権限を申告します。
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />次に、スティッキー サービスを実行してアプリのプロセスを維持します。
class BleForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
…
return START_STICKY
}
}この方法でも機能しますが、デバイスが近くにないときでもサービスが常に実行されるため、バッテリーとメモリを消費します。
そこで、コンパニオン デバイス マネージャー(CDM)を開発しました。
まず、デバイスのプレゼンスに基づいてアプリを復帰させることができます。デバイスが範囲内にある限り、システムはアプリのプロセスを維持します。
フォアグラウンド サービスは不要。
2 つ目は、CDM がデバイス プロファイルをサポートしていることです。
デバイス プロファイルを使用すると、ユーザーは権限ダイアログを何度もタップする代わりに、1 回タップするだけでアプリに必要なすべての権限を付与できます。
これにより、ユーザー エクスペリエンスとコンバージョン率が大幅に向上します。
スマートウォッチ プロファイルは Android 12 以降、グラス プロファイルは Android 14 以降で利用できます。
Android 17 では、医療機器とフィットネス トラッカー専用の 2 つの新しいプロファイルが追加されました。
現在、システムアプリ専用の特権プロファイルもいくつかあります。
エコシステムからより幅広いニーズが寄せられた場合は、より多くのデベロッパーに公開することを検討する可能性があります。
CDM の使用は非常に簡単です。
医療機器を例に考えてみましょう。
まず、必要な権限を宣言します。1 つは医療機器用、1 つはバックグラウンドでの実行用、1 つはデバイスの存在を監視するためのものです。
<uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_MEDICAL" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />
<uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" />1 つ注意すべき点として、フォアグラウンド サービスの実装で確認した権限とは異なり、ここに記載されている権限はすべて標準の権限です。つまり、インストール時に付与され、プロンプト ダイアログは表示されません。
ユーザーが承認する必要があるのは、関連付けのみです。
その関連付けを作成する方法を見てみましょう。
ここでは、適切なデバイスのみが表示されるように、デバイス フィルタを使用して関連付けリクエストを作成します。
val paringRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.setSingleDevice(true)
.setDeviceProfile(AssociationRequest.DEVICE_PROFILE_MEDICAL)
.build()
companionDeviceManager.associate(paringRequest, ContextCompat.getMainExecutor(this),
object : CompanionDeviceManager.Callback() {
override fun onAssociationPending(intentSender: IntentSender) {
intentSenderLauncher.launch(IntentSenderRequest.Builder(intentSender).build())
}
override fun onAssociationCreated(associationInfo: AssociationInfo) { … }
override fun onFailure(error: CharSequence?) { … }
}
)setSingleDevice を true に設定します。これは、一致するものを 1 つだけ取得するためです。また、プロファイルを medical に設定します。
次に、associate を呼び出します。
CDM がスキャンを処理します。
onAssociationPending で IntentSender を取得します。
起動すると、システムに関連付けダイアログが表示されます。
医療プロファイルの場合、ここを 1 回タップするだけで、関連付け自体が承認されるだけでなく、プロファイルに必要なすべての権限が付与されます。サイレント モードもバイパスされます。これは、重要なアラートにとって重要です。
ユーザーが [許可] をタップすると、AssociationInfo を含む onAssociationCreated が取得されます。これには、関連付け ID が含まれます。
その後、その関連付け ID を指定して startObservingDevicePresence を呼び出すことで、デバイスのプレゼンスを監視できます。
また、CompanionDeviceService を拡張するサービスを実装する必要もあります。
class BleCompanionDeviceService : CompanionDeviceService() {
override fun onDevicePresenceEvent(event: DevicePresenceEvent) {
when (event.event) {
DevicePresenceEvent.EVENT_BLE_APPEARED -> onDeviceAppeared(event.associationId)
DevicePresenceEvent.EVENT_BLE_DISAPPEARED -> onDeviceDisappeared(event.associationId)
}
}
private fun onDeviceAppeared(associationId: Int) { … }
private fun onDeviceDisappeared(associationId: Int) { … }
}デバイスが範囲内に入ると、アプリが実行されていなくても、システムはサービスにバインドします。
システムは EVENT_BLE_APPEARED を指定して onDevicePresenceEvent を呼び出します。
その後、接続して同期できます。
デバイスが離れると、EVENT_BLE_DISAPPEARED が表示されます。
その後、クリーンアップを行います。
常時接続サービスはなく、通知もありません。
ただし、CDM にはデベロッパーが留意すべき制限事項がいくつかあります。
1 つ目は OS のバージョンです。
CDM の機能のほとんどは Android 12 と 13 で導入されました。
しかし、Android 16 でいくつかの優れた改善が導入され、Android 17 で新しいプロファイルが導入されました。
そのため、実行時に OS バージョンを確認し、古いバージョン用のフォールバックを保持します。
とはいえ、ターゲット SDK は毎年更新され、最小 SDK もそれに追随する傾向があります。
アプリが現在直面している、または今後直面するメモリ不足に対処するため、できるだけ早くフォアグラウンド サービスから CDM に移行することを強くおすすめします。
また、現在 CDM でサポートされていないユースケースがある場合は、お知らせください。
次に、現在、CDM はバックグラウンド スキャンをサポートしていますが、バックグラウンド アドバタイズはサポートしていません。
この機能が必要とされることはほとんどありません。
ただし、アプリがスマートフォンをペリフェラルとして動作させ、バックグラウンドで継続的にアドバタイズする必要がある場合は、詳細をお知らせください。
3 つ目は、CDM が低電力スキャンモードを使用することです。
リアルタイムの応答性を少し犠牲にして、バッテリーとメモリを節約します。
そのため、すぐに起動するとは限りません。
最後に、ペイロードの覗き見は行いません。
特定の特性または広告データに登録することはできません。
ただし、デバイスが範囲内に入ると、アプリが起動します。
そのため、必要に応じて接続してペイロードを確認できます。
コンパニオン アプリと CDM については以上です。
VoIP アプリについては、Nataraj に引き継ぎます。
アプリが専用の周辺機器を所有し、その周辺機器と継続的に通信する場合は、CompanionDeviceManager が適切なツールです。
皆さん、こんにちは。
Nataraj K R です。
次に、2 つ目のクロスデバイス シナリオであるリアルタイム通話について説明します。
最近では、電話の対応方法が変化しています。
通話の 40% 以上が VoIP ネットワーク経由で行われています。
Android はその中で大きな役割を果たしています。
実際、これらの通話の 71% は Android デバイスで行われており、これは 2 回に 1 回以上の割合です。
通話だけではありません。
ユーザーが所有するデバイスのエコシステムも大幅に拡大しています。
Android Auto を搭載した車は 2 億 5, 000 万台以上あり、10 億台以上の Bluetooth デバイスがユーザーに提供されています。
イヤホン、Wear OS スマートウォッチ、Android(Android Auto 搭載の車)、Bluetooth スピーカーは、ユーザーがスマートフォンの周辺で使用するデバイスであり、ユーザーはこれらのデバイス間で通話をシームレスに切り替えたいと考えています。
たとえば、ユーザーは電話がかかってきたときに、スマートウォッチをちらっと見て発信者情報を確認します。
スマートウォッチで通話に応答します。
必要に応じてミュートまたはミュート解除し、イヤホンを装着して通話を続けます。
外出する際は、車に乗り込むと、通話がシームレスに車内に切り替わることを期待します。
このため、ユーザーにとってこれは日常的な作業となり、プレミアムなエクスペリエンスとは言えません。
ユーザーにとって、これは基本的な要件です。VoIP アプリでシームレスなエクスペリエンスをユーザーに提供することは、Android デベロッパーの責任です。
しかし、これらすべてをゼロから構築しようとすると、3 つの大きな問題に直面することになります。
1 つ目は、Bluetooth ソケットの断片化です。
システムは、通話が通話を処理していることを認識しないため、通話をスマートウォッチや Android Auto に投影できるように、カスタム Bluetooth ソケットとカスタム メディア セッションを構築することになります。
2 つ目の問題は、オーディオ フォーカスです。
低レベルの AudioManager API と Bluetooth API を処理する必要があるため、音声リークが発生します。これはシステムにとって優先事項ではありません。
アプリで VoIP 通話を行っているときに、携帯通信会社の通話が着信すると、割り込みが発生し、モデムが音声を占有するため、アプリで音声が聞こえなくなります。
さらに悪いことに、モバイル通話が終了しても、オーディオ フォーカスはアプリに戻りません。
3 つ目の問題は同期です。
スマートウォッチでミュートをタップしても、スマートフォンの UI では通話がミュート解除されたままになる。
これは大きな混乱を招きます。
ユーザーが車のステアリング コラムから通話を切断しても、スマートフォンには通話中の通知が表示されたままになります。
このようなことが起きると、ユーザーは大きな不満を感じます。
そこで、これらの問題をすべて解決するために、Jetpack Telecom を構築しました。
そのため、CallsManager を使用して Jetpack Telecom に通話を 1 回登録すると、Jetpack Telecom が中央システム通話コーディネーターとして機能します。
スマートウォッチ、Android Auto、ユーザーが所有するその他のリモート サーフェスに通話を投影する役割を担います。
また、音声フォーカスの処理も行います。
そして最も重要なのは、これらすべてが Android 8 までシームレスに動作することです。
通話をスマートウォッチや車の画面に投影するだけでは不十分です。ユーザーは当然、これらのデバイスから通話の状態を制御したいと考えます。
スマートウォッチから通話をミュートしたり、必要に応じてミュートを解除したりしたいと考えています。
車のステアリング コラムから、またはイヤホンをダブルタップして、通話を終了したいと考えています。
Jetpack Telecom は、これらのハードウェアイベントをリッスンし、CallControlScope という 1 つの場所ですべてのコールバックとして提供します。
必要なのは、これらのコールバックをリッスンし、通話エンジンを処理して、それに応じて通話状態を更新することだけです。
Jetpack Telecom は、デバイス間でこの通話状態を同期する役割を担います。
つまり、このライブラリは内部的に 2 つのバケットでメリットを提供します。
1 つ目は、これまで説明してきたすべての問題を解決できることです。
そのため、デベロッパーが低レベルの AudioManager API を処理する代わりに、ライブラリが宣言型の通話エンドポイントを提供し、オーディオ フォーカスを管理し、デバイス間で同期します。
デバイス間の通話の引き継ぎを担当します。
また、モバイル通信の着信にも対応します。
このライブラリのもう 1 つのメリットは、Kotlin をベースに構築されているため、Jetpack Telecom で呼び出しを追加するたびに Kotlin コルーチン スコープが開かれることです。
Kotlin コルーチンを活用し、Kotlin コルーチンは呼び出しの期間中のみ存続します。
そのため、通話に関して実行したジョブは、通話が切断されるとすぐに自動的にクリーンアップされます。
そのため、不正なジョブが残ることはなく、呼び出しのライフサイクルもライブラリによって処理されます。
この素晴らしいライブラリを実装したいとお考えのことでしょう。
手順は至って簡単です。
ステップ 1 は、依存関係を明示的に宣言することです。
その後、必要な権限(MANAGE_OWN_CALLS)を宣言し、CallsManager オブジェクトをインスタンス化します。
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />CallsManager オブジェクトをインスタンス化したら、アプリで提供する機能(ビデオ通話や通話ストリーミングなど)をアプリに登録します。
// Initialize CallsManager & register supported capabilities once
val callsManager = CallsManager(context)
callsManager.registerAppWithTelecom(
capabilities = CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING or
CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING
)その後、ステップ 2 に進みます。
Jetpack Telecom にアプリを登録したら、着信と発信を処理します。
そのためには、CallAttributesCompat オブジェクトをビルドする必要があります。このオブジェクトには、通話のメタデータ(発信者または着信者の displayName、発信者のエンドポイントであるアドレス、通話が着信か発信かを指定する方向(Jetpack Telecom がこれらのリモート サーフェスにそれぞれの UI を表示できるようにするため)、callType(音声通話かビデオ通話か(必要なリソースを取得できるようにするため))など)が保持されます。
val attributes = CallAttributesCompat(
displayName = "Kukk Nat",
address = Uri.parse("tel:+16505550199"),
direction = CallAttributesCompat.DIRECTION_INCOMING,
callType = CallAttributesCompat.CALL_TYPE_AUDIO_CALL
)
callsManager.addCall(
attributes, onAnswer, onDisconnect, onSetActive, onSetInactive
) {
// Coroutine CallControlScope active for the duration of the call
}メタデータ オブジェクトの準備ができたら、callsManager.addCall メソッドを使用して追加します。
これにより、通話制御コルーチン スコープが開きます。
通話を追加したら、ステップ 3 に進みます。
ステップ 3 は、ユーザーがそれらのリモート サーフェスから実行するアクションをリッスンすることです。
CallControlScope があるため、必要なのは Kotlin フローを収集することだけです。
callsManager.addCall(attributes, onAnswer, onDisconnect, onSetActive, onSetInactive) {
// 1. React to hardware mute toggles from Watch, Car, or Headset
launch {
isMuted.collect { muted -> audioEngine.setMicMuted(muted) }
}
// 2. Observe available endpoints & request declarative switches
launch {
currentCallEndpoint.collect { endpoint -> updateUi(endpoint) }
}
requestEndpointChange(preferredBluetoothEndpoint)
}isMuted を収集して、ユーザーがリモート サーフェスから通話をミュートしたときに反応し、それに応じて通話状態を更新できます。
currentCallEndpoint を収集して、現在通話が行われているエンドポイントを把握し、それに応じて UI を更新できるようにします。
特定のデバイスをスマートフォンの UI に表示できます。
ユーザーがデバイスを切り替える場合は、ユーザーが希望するエンドポイントを指定して requestEndpointChange メソッドを呼び出します。
このように、とても簡単です。
この 3 つのステップで、アプリに Jetpack Telecom が統合され、準備が整います。
それだけではありません。
Jetpack Telecom 1.1.0 では、統合通話履歴などの機能が追加され、すべての VoIP 通話がシステム ダイヤラーにネイティブに表示されるようになります。
この機能のメリットは、ユーザーがシステム ダイヤラーからアプリにワンタップでコールバックを開始できることです。
そのため、すべてのアプリの中から特定のアプリを探す必要はありません。
使い慣れたシステム ダイヤラーからコールバックをインスタンス化するだけで済みます。
しかし、きめ細かい制御も提供しています。
特定の通話をシステムに記録したくない場合は、CallAttributesCompat オブジェクト内で isLogExcluded フラグを true に設定するだけです。
先ほど説明したメタデータ オブジェクトを true に設定するだけで、通話はシステムに記録されず、システム ダイヤラーにも表示されません。
1.1.0 で提供される 2 つ目の優れた機能は、5G ネットワーク スライシングです。
サポートされている携帯通信会社のネットワークでは、プレミアム ネットワーク スライスが提供されます。つまり、特定のユースケースに対して、携帯通信会社は低レイテンシで中断のないネットワーク スライスを提供するため、品質とエクスペリエンスが向上します。
そのため、サポートされている携帯通信会社のネットワークでは、この機能がすぐに利用可能になります。デベロッパー側で追加の作業を行う必要はなく、1 行のコードも必要ありません。
ライブラリを統合すると、通話でこれらのネットワーク スライスが利用され、レイテンシとパフォーマンスが向上します。
ただし、なんらかの理由でこの機能が不要な場合は、CAPABILITY_OPT_OUT_OF_PREMIUM_NETWORK フラグを使用してオプトアウトすることもできます。
Jetpack Telecom は、Google Meet、WhatsApp、KakaoTalk、Zoho Arattai などの世界的なトップ コミュニケーション アプリですでに実装されています。
このライブラリをアプリに統合して、ユーザーにシームレスで優れたエクスペリエンスを提供するかどうかは、あなた次第です。
リアルタイム通話については以上です。ここからは、タップして共有について、同僚の Yacine に説明してもらいます。
ありがとうございました
皆さん、少しお疲れのことと思いますが、ご安心ください。これが最後のセクションです。
ここでは、ピアツーピアのファイル共有について説明しますが、ピアツーピア共有では他のユースケースも可能です。
モバイル エコシステム全体で、毎日 140 億を超える写真や動画がソーシャル メディアやメッセージ アプリで共有されています。これは膨大な量です。
そして、それらのファイルはほとんどの場合、クラウドを介して共有されます。つまり、写真を撮って、お気に入りのサービスを介してアップロードし、相手は同じ方法でダウンロードする必要があります。
ソーシャル ネットワークに友人を招待したり、プレイリストやドキュメントの共同編集に誰かを招待したりする場合も同様です。通常は、ユーザー名やメールアドレスを入力する必要がありますが、キーボードでの入力には少し時間がかかります。
間違えることもあります。特に、2 人が文字通り隣り合って座っているときに、クラウド経由で送信して待たなければならないのは、あまり良い体験ではありません。
あまり良くありません。
最近では、スマートフォン、デバイス、タブレット、折りたたみ式デバイスなど、さまざまな高性能デバイスがあり、それらで素晴らしい写真を撮影できます。
しかし、一部のソーシャル メディアやメッセージ アプリで共有すると、パフォーマンスの問題やストレージの問題により、画像が圧縮されてしまいます。その結果、素晴らしい画面に表示される画像が、かなり粗くなったり、画質が低下したりします。
プレミアム デバイスでこのようなことが起こるのは、望ましいことではありません。
2 つ目は、ファイルが実際にかなり大きくなっていることです。
1 枚の写真が 10 MB、5 分の動画が 500 MB になることも珍しくありません。そのため、アップロードやダウンロードにかなりの時間がかかります。
そして最後に、もう一方の側では、そのレイテンシを待つ必要があります。
たとえば、混雑したバスに乗っているときに、バスが移動するにつれて、レイテンシやネットワーク カバレッジが最適ではなくなり、ファイル転送が中断されることがあります。また、レイテンシが非常に長いためにタイムアウトが発生し、ファイル転送がキャンセルされることもあります。これはユーザーにとって非常に不満なことです。
そこで、クラウドに依存してすべてのファイルを共有し、そのすべてを待つのではなく、Bluetooth や Wi-Fi などのローカル ネットワークを実際に使用してみてはいかがでしょうか。
しかし、これを自分で行おうとすると、Bluetooth マネージャーや Wi-Fi マネージャーを操作し、独自のプロトコルを作成する必要があります。誰もがそのような作業に時間をかけたいわけではありません。
しかし、実際には Android にはすでに存在する機能があります。
すでにリリースされているので、ご存じの方もいらっしゃるかもしれませんが、Quick Share です。
Android の Quick Share では、この抽象化がすでにシステムに組み込まれているため、別のデバイスとファイルを共有するだけで、すべてを管理する必要はありません。
共有シートですでに利用可能で、両方のデバイス間でどのプロトコルがデバイスに最適かを切り替えます。Marshmallow 以降で利用可能で、最近では非常に多くのユーザーが利用しています。
最後に、右側に表示されているおなじみの UI と UX は、ユーザーにとって非常に便利です。
UI がどのようなものか、どのように使用するのかを学ぶ必要はありません。すでに過去に見たことがあるからです。
しかし、デベロッパーはエクスペリエンスを所有したいとも考えています。
アプリのことも、ユーザーのことも、Google よりデベロッパーの皆様のほうがよくご存じのはずです。
Quick Share をアプリに直接組み込んでみてはいかがでしょうか。
そこで、デベロッパーと話し合いを始め、Quick Share がすでにかなり普及していることを確認しました。
Android、Chromebook、最近では Google ブックス、そして Windows でも利用できます。30 億台以上のデバイスで利用できる、非常に大きなプラットフォームです。
また、Google Pixel 11 では、タップして共有する機能も追加されました。
共有するデバイスを選択する代わりに、タップするだけで、NFC を使用して検出を直接行うことができます。
写真だけでなく、ファイルも選択できます。2 台のデバイスをタップするだけで、バンプで直接共有されます。
また、エクスペリエンスを所有したいデベロッパーもいます。
この方法は、ユーザーが特定の機能(この場合はファイル共有)を使用できるようにするための最適な方法です。
そこで、Quick Share をアプリに組み込んでみてはいかがでしょうか。
そのため、来年には Quick Share SDK を直接提供する予定です。
この機能はアプリで直接使用でき、両方のデバイス間のエンドツーエンドの暗号化として、セキュリティがすでに組み込まれています。
転送はローカル ネットワーク間で行われるため、自宅の Wi-Fi ではもちろん、2 人の間でも自動的に作成されるアクセス ポイントを介して、最高速度で転送できます。
Wi-Fi Direct をサポートするデバイスには、すでに含まれています。
また、高性能カメラで撮影したファイルは、最高の解像度とディテールを維持できるため、大きな画面でもきれいに表示できます。
ローカル ネットワークに依存しているため、地下鉄の最も深い場所でも、ネットワークを使用して友だちと共有できます。
飛行機の中でも、何千フィートも上空にいるにもかかわらず、ファイルを共有できます。
最後に、デベロッパーにとって、データに関して大きなコスト削減になります。まず、ユーザーがデータプランを使用する必要がありません。これは、ユーザーが旅行者として移動しているときに大きなメリットとなります。
また、独自のサーバーで、それを維持する必要はありません。
また、昨年、デベロッパーとユーザーの皆様に、Quick Share を使用して AirDrop 経由で iPhone デバイスとファイルを共有できるようになることをお知らせしました。
Quick Share では、デベロッパー向けに同じ機能を提供しています。
iOS アプリを開発しているデベロッパー向けに Quick Share iOS SDK を提供する予定です。これにより、デバイスに関係なく、両方のプラットフォーム間でファイルを共有できるようになります。
ここまでは、プレゼンテーション用の見栄えの良いコードでした。次に、実際のコードを簡単に見てみましょう。
できるだけシンプルにしようと努めました。
QuickShareClient を開始し、共有するファイルを添付する必要があります。セッションを開始すると、横に表示されているような美しいモーダル ウィンドウが表示されます。ボトムシートでは、ユーザーは送信されるファイルをプレビューとして確認できます。コンテンツが機密性の高いものである場合は、画面に表示されるオプションで有効にすることもできます。
// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val attachment = FileAttachment(
fileName = "report.pdf",
size = 1_048_576L,
uri = fileUri,
mimeType = "application/pdf",
// Sensitive content will be masked in preview
isSensitiveContent = false,
)
val content = ShareableContent.withFiles(listOf(attachment))
val request = SharingRequest.withContent(content)
val session = quickShareClient.startSharingSession(request, lifecycleOwner)アプリ内でファイルを受信する場合も同様です。もちろん、この場合は添付ファイルを追加しません。セッションが開始されるとすぐに、それらの処理を行う必要がなくなります。
// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val request = SharingRequest.withoutContent()
val session = quickShareClient.startSharingSession(request, lifecycleOwner)
// Open the Quick Share overlay in receive mode
session.launchWindow(activity, contentSelectionIntent = null)ここまでで、両側で文字どおり 20 行のコードしか見ていません。
Quick Share とタップして共有の機能により、デバイスをタップするだけでファイルを共有できるため、新しいユースケースが実現します。
アプリが別のデバイスで終了している場合でも、タップすると、ファイルが転送されるとすぐにアプリが自動的に開き、URI を受け取って、デバイス上の処理を実行したり、アプリ内でユーザーに直接何かを表示したりできます。
// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val urlAttachment = TextAttachment(
text = groupAddUrl,
type = TextAttachment.Type.URL,
)
val content = ShareableContent.withText(urlAttachment)
val appLink = AppLink("https://myapp.com/group/invite")
val request = SharingRequest.withContent(content, appLink)
val session = quickShareClient.startSharingSession(request, lifecycleOwner)この場合も、受信側のデバイスでアプリを開く必要はありません。
タップして共有すると、自動的に開きます。
もちろん、UI でファイル転送に反応させたい場合は、Kotlin Flow を使用するだけで、処理可能なすべてのイベントを取得できます。
// Final API may undergo further updates
scope.launch {
for (event in session.sharingEvents) {
when (event) {
is SharingEvent.SendComplete ->
println("Send finished")
is SharingEvent.ReceiveComplete ->
println("Received, files=${event.content?.files?.size}")
else -> println("other event")
}
}
}本日は多くの内容を取り上げましたので、まとめに入ります。
休憩時間も数分ありますので、ご安心ください。
CompanionDeviceManager は、Hanrui が先ほど説明したように、Bluetooth のインタラクションや継続的な Bluetooth のインタラクションを処理するのに最適な API です。
アプリに Voice over IP 通話機能がある場合、それがメインのユースケースである必要はありません。たとえば、アプリにサポート クライアント機能があり、ユーザーがサポートに電話をかけられる場合などが該当します。
この場合も、Android で VoIP 通話を処理する最適な方法は、Jetpack Telecom です。
最後に、ピアツーピアのファイル共有やアプリの起動については、先ほど説明したように、タップして共有する Quick Share がソリューションとなります。
皆様、ありがとうございました。最後にお知らせです。この後、会場の外で皆様からのご質問にお答えします。
ご紹介したすべてのユースケースについてご質問がある場合は、オフィスアワーをご利用ください。
また、講演やその他のセッションで学んだことをソーシャル メディアで共有することも忘れないでください。
#AndroidDevSummit があります。
繰り返しになりますが、エンゲージメントがあること、イベントを本当に楽しんでいる人がいることを知ることは、私たちにとって非常に重要です。
また、再び会いたいと思えるコミュニティがあることを実感できます。
インフォデスクで、Android Bot のキーリングに追加できるソーシャル チャームを受け取ります。
正しく行えたかどうか
ありがとうございました
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />class BleForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
…
return START_STICKY
}
}<uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_MEDICAL" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />
<uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" />val paringRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.setSingleDevice(true)
.setDeviceProfile(AssociationRequest.DEVICE_PROFILE_MEDICAL)
.build()
companionDeviceManager.associate(paringRequest, ContextCompat.getMainExecutor(this),
object : CompanionDeviceManager.Callback() {
override fun onAssociationPending(intentSender: IntentSender) {
intentSenderLauncher.launch(IntentSenderRequest.Builder(intentSender).build())
}
override fun onAssociationCreated(associationInfo: AssociationInfo) { … }
override fun onFailure(error: CharSequence?) { … }
}
)class BleCompanionDeviceService : CompanionDeviceService() {
override fun onDevicePresenceEvent(event: DevicePresenceEvent) {
when (event.event) {
DevicePresenceEvent.EVENT_BLE_APPEARED -> onDeviceAppeared(event.associationId)
DevicePresenceEvent.EVENT_BLE_DISAPPEARED -> onDeviceDisappeared(event.associationId)
}
}
private fun onDeviceAppeared(associationId: Int) { … }
private fun onDeviceDisappeared(associationId: Int) { … }
}<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />// Initialize CallsManager & register supported capabilities once
val callsManager = CallsManager(context)
callsManager.registerAppWithTelecom(
capabilities = CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING or
CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING
)val attributes = CallAttributesCompat(
displayName = "Kukk Nat",
address = Uri.parse("tel:+16505550199"),
direction = CallAttributesCompat.DIRECTION_INCOMING,
callType = CallAttributesCompat.CALL_TYPE_AUDIO_CALL
)
callsManager.addCall(
attributes, onAnswer, onDisconnect, onSetActive, onSetInactive
) {
// Coroutine CallControlScope active for the duration of the call
}callsManager.addCall(attributes, onAnswer, onDisconnect, onSetActive, onSetInactive) {
// 1. React to hardware mute toggles from Watch, Car, or Headset
launch {
isMuted.collect { muted -> audioEngine.setMicMuted(muted) }
}
// 2. Observe available endpoints & request declarative switches
launch {
currentCallEndpoint.collect { endpoint -> updateUi(endpoint) }
}
requestEndpointChange(preferredBluetoothEndpoint)
}// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val attachment = FileAttachment(
fileName = "report.pdf",
size = 1_048_576L,
uri = fileUri,
mimeType = "application/pdf",
// Sensitive content will be masked in preview
isSensitiveContent = false,
)
val content = ShareableContent.withFiles(listOf(attachment))
val request = SharingRequest.withContent(content)
val session = quickShareClient.startSharingSession(request, lifecycleOwner)// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val request = SharingRequest.withoutContent()
val session = quickShareClient.startSharingSession(request, lifecycleOwner)
// Open the Quick Share overlay in receive mode
session.launchWindow(activity, contentSelectionIntent = null)// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val urlAttachment = TextAttachment(
text = groupAddUrl,
type = TextAttachment.Type.URL,
)
val content = ShareableContent.withText(urlAttachment)
val appLink = AppLink("https://myapp.com/group/invite")
val request = SharingRequest.withContent(content, appLink)
val session = quickShareClient.startSharingSession(request, lifecycleOwner)// Final API may undergo further updates
scope.launch {
for (event in session.sharingEvents) {
when (event) {
is SharingEvent.SendComplete ->
println("Send finished")
is SharingEvent.ReceiveComplete ->
println("Received, files=${event.content?.files?.size}")
else -> println("other event")
}
}
}