應用程式基礎

實現流暢的跨裝置互動

喇叭

關於

講座簡介

使用者會在手機、穿戴式裝置和音訊周邊裝置之間切換,因此提供流暢的跨裝置體驗,是提升參與度的關鍵。不過,如果採用標準網路做法,往往會導致電池耗電、延遲時間激增,以及使用者工作階段不連貫。

本深度探討課程將介紹專為支援多種裝置通訊打造的 Android API,提供記憶體和省電模式,讓裝置間的轉換即時、可靠且直覺。

摘要

  • 隨附裝置管理工具會根據裝置是否在附近喚醒應用程式,並在不耗盡電池電量的情況下管理連線,取代藍牙周邊裝置的前景服務。Android 17 新增了醫療器材和運動智慧手環專用的設定檔。
  • Jetpack Telecom 可做為 VoIP 應用程式的系統通話協調工具,適用於手錶和汽車等連結裝置,並可回溯至 Android 8。此外,也會透過 CallControlScope 同步處理靜音和掛斷動作。
  • Jetpack Telecom 1.1.0 即將推出的功能包括:系統撥號程式中的整合通話記錄 (SDK 36.1 以上版本),以及在支援的電信業者網路上自動進行 5G 網路切片。
  • 快速分享 SDK 將於明年推出,開發人員可直接在 Android 和 iOS 應用程式中,嵌入本機端對端加密檔案分享功能。「輕碰分享」功能 (隨附於 Pixel 11) 甚至能自動喚醒已關閉的接收應用程式。

資源

Gemini Notebook

主題

  • 跨裝置
  • companion-device-manager
  • jetpack-telecom
  • 快速分享
  • 藍牙
  • voip
  • 點對點

轉錄稿

跨裝置互動簡介

  • 大家好,

  • 歡迎參加「實現流暢的跨裝置互動」課程。

  • 我是 Hanrui,今天我的同事 Yacine 和 Nataraj 也會一起主持。

  • 談到連線能力時,我們通常會想到基本的網路連結或藍牙頭戴式耳機。

  • 在 Android 上,連線能力遠不只如此。

  • 重點在於讓裝置搭配運作,相得益彰。

  • 今天,我們將逐步說明常見的跨裝置情境,並介紹適合各情境的 API。

  • 在很長一段時間內,我們都只為單一裝置 (手機) 建構應用程式。

  • 接著,穿戴式裝置開始蓬勃發展。

  • 不久後,智慧手錶與手機配對、在背景同步資料或直接接聽來電,就成了非常普遍的做法。

  • 新增耳機,新增無線耳機。

  • 現在,使用者可以在通話期間切換音訊裝置,從手錶、手機、耳機到車輛都能切換。

  • 最近,許多使用者也會攜帶第二部大螢幕裝置,例如摺疊式裝置或平板電腦。

  • 他們預期應用程式能在這些裝置上順暢運作。

  • 這項功能也延伸到筆電和 Chromebook,使用者可以直接在桌上執行 Android 應用程式並保持連線。

  • 早在 2023 年,一般使用者就擁有三到四部裝置。

  • 使用者對這些裝置的期望不只是獨立功能。

  • 他們希望獲得更優質的搭配使用體驗。

  • 當使用者身處同一個空間 (就像我們今天一樣),並想輕鬆與身邊的人分享相片、連結或應用程式時,這種「合作無間」的體驗就特別明顯。

  • 從數據來看,這個生態系統規模龐大,而且成長迅速。

  • 今年全球約有 220 億部物聯網裝置,年增率為 14%。

  • 在所有端點中,Wi-Fi 占三分之一,藍牙占四分之一。

  • 跨裝置連線能力不再是邊緣案例。

  • 已成為日常生活的一部分。

  • 然而,目前大多數應用程式仍以低階網路和藍牙 API 建構跨裝置功能,這會造成三個問題。

  • 首先是電池和記憶體。

  • 持續執行背景掃描和服務會耗用電池和記憶體。

  • 第二,延遲時間。

  • 無法預測的延遲會干擾即時通話和資料同步。

  • 第三,連線不順暢。

  • 管理所有裝置的探索、配對和重新連線功能相當複雜,容易導致使用者體驗不佳。

  • 為解決這些難題,我們在三大領域打造了專用 API,讓跨裝置功能變得非常簡單。

  • 我會先介紹隨附應用程式,接著由 Nataraj 說明 VoIP 應用程式,最後由 Yacine 介紹 P2P 共用。

隨附應用程式總覽

  • 首先,我們來看看隨附應用程式。

  • 隨附應用程式是指安裝在手機上的應用程式,可讓智慧手錶或葡萄糖感應器等配對裝置與手機搭配運作。

  • 本節將著重介紹藍牙連線裝置。

  • 配對裝置通常分為四類。

  • 首先是醫療器材,例如血糖感測器和生命徵象監測器。

  • 以及智慧手錶和健身手環等穿戴式裝置。

  • 耳機,具備快速配對和智慧控制功能。

  • 車用套件,用於數位車鑰和免持通話等功能。

  • 每個類別都有自己的主要使用者歷程,但所有類別都仰賴裝置和手機之間的可靠連線。

  • 對醫療器材而言,這項連線甚至可能攸關生命。

  • 以下介紹這幾類客戶的四種常見需求。

  • 首先是裝置管理。

  • 應用程式必須在附近所有裝置中找到正確的裝置 (例如這裡的手錶,而非耳機或喇叭),然後啟動配對流程進行連線。

  • 第二,檢查是否存在。

  • 應用程式需要知道裝置何時在附近,何時不在。

  • 當裝置出現時,應用程式需要一種喚醒方式,以便在使用者未開啟應用程式的情況下執行工作。

  • 第三,維護連線。

  • 連線後,應用程式會希望連線穩定且持續。

  • 如果連線中斷 (例如使用者走出範圍後又返回),應用程式必須自動重新連線,即使當時並未執行也一樣。

  • 第四,資料同步和移轉。

  • 這才是真正的價值所在。

  • 同步感應器讀數、推送韌體更新,或將設定傳送至裝置。

  • 這對醫療器材來說至關重要。

  • 如果錯過讀數,可能會錯過警示。

  • 這些都需要應用程式程序在適當時間保持運作並連線。

  • 那麼,應用程式如何滿足所有這些需求?

  • 傳統上,應用程式高度仰賴前景服務。

  • 以下是常見的實作方式。

  • 如果您曾建構隨附應用程式,可能對此不陌生。

  • 您需要宣告藍牙掃描和連線權限、前景服務權限、連結裝置的前景服務類型,以及通知權限,因為前景服務必須顯示永久通知。

    <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)。

  • 首先,系統可以根據裝置存在狀態喚醒應用程式,只要裝置保持在範圍內,系統就會讓應用程式程序保持運作。

  • 不需要前景服務。

  • 其次,CDM 支援裝置設定檔。

  • 有了裝置設定檔,使用者只要輕觸一下,就能授予應用程式所需的所有權限,不必逐一回應權限對話方塊。

  • 這能大幅改善使用者體驗和轉換率。

  • 手錶設定檔自 Android 12 起提供,眼鏡設定檔自 Android 14 起提供。

  • Android 17 新增了兩個設定檔,專為醫療器材和運動智慧手環設計。

  • 目前也有一些僅限系統應用程式使用的特殊權限設定檔。

  • 如果生態系統有更廣泛的需求,我們可能會考慮開放更多開發人員使用。

使用隨附裝置管理工具導入

  • CDM 使用起來非常簡單。

  • 以醫療器材為例。

  • 首先,請宣告必要權限:一項用於醫療器材、一項用於在背景執行,另一項用於觀測裝置是否存在。

    <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?) { … }
       }
    )
  • setSingleDevice 設為 true,因為我們只需要一個相符項目,並將設定檔設為醫療。

  • 然後呼叫 associate。

  • CDM 會處理掃描作業。

  • 在 onAssociationPending 中,我們會取得 IntentSender。

  • 啟動後,系統會顯示關聯對話方塊。

  • 有了醫療設定檔,只要輕觸一下,不僅能核准關聯本身,還能授予設定檔所需的所有權限,甚至略過「零打擾」模式,這對重大警示至關重要。

  • 使用者輕觸「允許」後,我們會透過 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 確實有一些限制,開發人員必須留意。

  • 首先是 OS 版本。

  • 大部分 CDM 功能是在 Android 12 和 13 中推出。

  • 不過,Android 16 推出了一些很棒的改善項目,Android 17 則推出新的設定檔。

  • 因此,請在執行階段檢查 OS 版本,並為舊版本保留備用方案。

  • 不過,目標 SDK 每年都會提高,最低 SDK 也會隨之提高。

  • 為解決應用程式目前和未來面臨的記憶體壓力,強烈建議您盡快從前景服務遷移至 CDM。

  • 如果 CDM 目前不支援您的用途,請告訴我們。

  • 其次,CDM 目前支援背景掃描,但不支援背景廣告。

  • 我們很少聽說有這項需求。

  • 不過,如果您的應用程式需要手機做為周邊裝置,並持續在背景廣播,我們很樂意進一步瞭解。

  • 第三,CDM 會使用低功耗掃描模式。

  • 這項功能會稍微犧牲即時回應能力,以節省電池和記憶體用量。

  • 因此,請不要期待能立即喚醒。

  • 最後,不窺探酬載。

  • 您無法註冊特定特徵或廣告資料。

  • 但只要裝置進入範圍,應用程式就會喚醒。

  • 因此,它可視需要連線並檢查酬載。

  • 以上就是隨附應用程式和 CDM 的介紹。

  • 接下來,Nataraj 會介紹 VoIP 應用程式。

跨裝置 VoIP 通話的挑戰

  • 如果應用程式擁有專屬周邊裝置,且會持續與其通訊,則 CompanionDeviceManager 是合適的工具。

  • 大家好。

  • 我是 Nataraj K R。

  • 接下來,我會介紹下一個跨裝置情境:即時通話。

  • 現在,人們處理來電的方式正在改變。

  • 超過 40% 的通話是透過 VoIP 網路進行。

  • Android 在其中扮演重要角色。

  • 事實上,有 71% 的通話是在 Android 裝置上進行,幾乎每兩通電話就有一通。

  • 不只是通話。

  • 使用者擁有的裝置生態系統也大幅成長。

  • 目前有超過 2.5 億輛汽車搭載 Android Auto,我們也向使用者提供超過 10 億部藍牙裝置。

  • 耳機、Wear OS 手錶、Android 手機、搭載 Android Auto 的車輛、藍牙喇叭,都是使用者在手機周圍擁有的裝置,他們希望通話能在這些裝置之間順暢切換。

  • 舉例來說,使用者接到來電時,會看一眼手錶,瞭解來電者資訊。

  • 他們會在手錶上接聽來電。

  • 他們會視需要將麥克風設為靜音或取消靜音,並戴上耳塞式耳機繼續通話。

  • 如果他們想出門,就會進入車內,並希望通話能流暢切換到車內。

  • 因此,這已成為使用者的日常,並非優質體驗。

  • 對使用者來說,這是基本要求,而 Android 開發人員有責任在 VoIP 應用程式中,為使用者提供流暢的體驗。

  • 但如果嘗試從頭開始自行建構,最終會面臨三大問題。

  • 第一,藍牙通訊端的分散問題。

  • 由於系統不知道您的通話正在處理通話,因此您最終會建構自訂藍牙通訊端和自訂媒體工作階段,以便將通話投射到手錶或 Android Auto。

  • 第二個問題是音訊焦點。

  • 因為你必須處理低階 AudioManager API 和藍牙 API,所以會發生音訊外洩,而且這不是系統的優先事項,對吧?

  • 應用程式正在進行 VoIP 通話,因此每當有電信業者來電時,就會中斷通話,且數據機也會擷取音訊,導致應用程式沒有音訊。

  • 更糟的是,即使行動網路通話結束,音訊焦點也不會回到應用程式。

  • 第三個問題是同步處理。

  • 使用者在手錶上輕觸靜音,但手機 UI 仍會顯示通話未靜音。

  • 這會造成很大的混淆。

  • 使用者從車輛方向機柱掛斷電話,但手機仍顯示通話中的通知。

  • 發生這種情況時,使用者會感到非常挫折。

Jetpack Telecom 簡介

  • 因此,為瞭解決上述所有問題,我們打造了 Jetpack Telecom。

  • 因此,您可以使用 CallsManager 向 Jetpack Telecom 註冊通話一次,Jetpack Telecom 就會做為中央系統通話協調工具。

  • 負責將通話投射到手錶、Android Auto 和使用者擁有的其他遠端介面。

  • 此外,它也會處理音訊焦點。

  • 最重要的是,所有功能都能順暢運作,甚至支援 Android 8。

  • 將通話投射到手錶或車輛螢幕上只是其中一部分,因為使用者顯然也想透過這些裝置控制通話狀態。

  • 他們希望視需要透過手錶將通話設為靜音或取消靜音。

  • 他們想透過車輛方向機柱或輕觸兩下耳機來掛斷通話。

  • 因此,Jetpack Telecom 會監聽這些硬體事件,並在 CallControlScope 集中顯示為回呼。

  • 您只需要監聽這些回呼、處理通話引擎,並據此更新通話狀態。

  • Jetpack Telecom 則負責在裝置之間同步處理通話狀態。

  • 因此,程式庫在幕後提供兩大好處。

  • 首先,這顯然能解決我們談到的所有問題。

  • 因此,您不必處理低階 AudioManager API,程式庫會提供宣告式呼叫端點、管理音訊焦點,並在裝置之間同步處理。

  • 負責在裝置之間轉接通話。

  • 此外,也能處理行動通訊來電。

  • 不過,這個程式庫還有另一個好處,那就是它是以 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 方法即可。

  • 就是這麼簡單。

  • 完成這三個步驟後,應用程式就已整合 Jetpack Telecom,可以開始使用。

  • 但這還不是全部。

  • Jetpack Telecom 1.1.0 帶來更多功能,例如整合通話記錄,也就是所有 VoIP 通話現在都會原生顯示在系統撥號工具中。

  • 這樣一來,使用者就能直接從系統撥號程式,一鍵回撥到您的應用程式。

  • 因此,您不必在所有應用程式中搜尋特定應用程式。

  • 他們只要從熟悉的系統撥號程式啟動回撥即可。

  • 不過,我們也提供精細的控制選項。

  • 如果不想將特定呼叫記錄到系統中,只要在 CallAttributesCompat 物件中將 isLogExcluded 標記設為 true 即可。

  • 只要將我剛才提到的中繼資料物件設為 true,系統就不會記錄通話,也不會顯示在系統撥號器中。

  • 1.1.0 版的第二項實用功能是 5G 網路切片。

  • 因此,在支援的電信業者網路上,網路會提供優質的網路切片,也就是說,針對特定用途,電信業者會提供低延遲且無其他中斷的網路切片,讓您享有更優質的體驗。

  • 因此,在支援的電信業者網路上,您不需額外設定,就能直接使用這項功能,甚至不需要撰寫任何一行程式碼。

  • 整合程式庫後,通話就會利用這些網路切片,縮短延遲時間並提升效能。

  • 但如果因故不想使用這項功能,也可以使用 CAPABILITY_OPT_OUT_OF_PREMIUM_NETWORK 旗標選擇不採用。

  • Jetpack Telecom 已由 Google Meet、WhatsApp、KakaoTalk 和 Zoho Arattai 等全球頂尖通訊應用程式導入。

  • 現在,您需要將這個程式庫整合到應用程式中,為使用者提供更流暢、更優質的體驗。

  • 以上就是即時通話功能的介紹。接下來,我要將麥克風交給優秀的同事 Yacine,由他介紹「輕碰分享」功能。

  • 感謝您!

點對點分享挑戰

  • 各位,我知道大家有點累了,別擔心,這是最後一個部分。

  • 因此,我會介紹點對點檔案共用,但其他用途也可以透過點對點共用完成。

  • 我們觀察到,在整個行動生態系統中,每天有超過 140 億張相片和影片在社群媒體和訊息應用程式上分享,數量非常龐大。

  • 而且這些檔案大多是透過雲端分享,因此你拍下相片後,要透過你偏好的服務上傳,然後對方也必須透過相同的方式下載。

  • 當你邀請某人或朋友加入社群網路,或邀請某人協作播放清單或文件時,通常必須輸入他們的使用者名稱或電子郵件,這在鍵盤上需要花一點時間。

  • 你可能會犯錯,這其實不是很好的體驗,尤其是當你們兩個都坐在彼此旁邊時,所以就像是,哦,我必須透過雲端傳送,我必須等待。

  • 但效果不佳。

  • 現在,我們都有各種精美的手機、裝置、平板電腦、折疊式裝置,並用這些裝置拍出精彩相片。

  • 但透過部分社群媒體或訊息應用程式分享時,基於效能和儲存空間考量,相片會經過壓縮,因此在精美螢幕上顯示時,會出現明顯像素化或畫質不佳的情況。

  • 這絕對不是你希望在頂級裝置上獲得的體驗。

  • 第二點,現在的檔案其實相當大。

  • 一張相片的大小很容易就達到 10 MB,5 分鐘的影片則約為 500 MB,因此上傳和下載這些內容都需要相當長的時間。

  • 最後,在另一端,您必須等待該延遲。

  • 因此,有時您可能會搭上公車,車上很擁擠,而且公車在移動時,您沒有最佳的延遲時間、最佳的網路覆蓋範圍,有時檔案傳輸會中斷,有時因為延遲時間真的很長,您可能會逾時,檔案傳輸會取消,這對使用者來說真的很令人沮喪。

  • 因此,與其依賴雲端分享所有檔案,並等待所有檔案上傳,不如使用藍牙或 Wi-Fi 等區域網路。

  • 但如果想自行完成,顯然必須處理藍牙管理員、Wi-Fi 管理員,還得自行編寫通訊協定,並非每個人都想花時間做這些事。

  • 但其實 Android 已經有類似功能。

  • 這項功能已內建於裝置中,你可能聽過,那就是「快速分享」。

快速分享總覽

  • Android 的快速分享功能已在系統中提供這種抽象化機制,您只要與其他裝置分享檔案,不必管理所有相關作業。

  • 這項功能已在分享工作表中提供,並會在兩部裝置之間切換最適合裝置的通訊協定,而且適用於 Marshmallow 以上版本,這在現今是相當龐大的受眾群體,對吧?

  • 最後,您在側邊看到的熟悉使用者介面和使用者體驗,確實能協助使用者。

  • 因此,他們知道使用者介面是什麼,不需要學習如何使用,因為他們過去已經看過了。

  • 但我們知道開發人員也想擁有自己的體驗。

  • 您比我們更瞭解自己的應用程式和使用者。

  • 因此,何不直接在應用程式中加入「快速分享」功能?

  • 因此,我們開始與開發人員討論,發現「快速分享」已相當普及。

  • Google Play 支援 Android、Chromebook、Windows,以及最近加入的 Google 圖書,涵蓋超過 30 億部裝置,規模相當龐大。

  • Pixel 11 還支援輕碰分享功能。

  • 因此,你不需要選擇要分享的裝置,只要輕觸一下,我們就會透過 NFC 直接為你偵測。

  • 因此,你可以選取相片或檔案,然後輕觸兩部裝置,就能直接透過碰觸分享。

  • 我們也知道,有些開發人員希望自行掌控體驗。

  • 我們知道,這是讓使用者能使用特定功能 (在本例中為檔案共用) 的最佳方式。

  • 因此,何不在應用程式中嵌入「快速分享」功能?

  • 因此,我們明年將直接推出「快速分享」SDK。

  • 您可以直接在應用程式中使用這項功能,而且這項功能已內建安全性,可在兩部裝置之間進行端對端加密。

  • 由於傳輸作業是在這些區域網路之間進行,因此傳輸速度會達到最快,這在住家 Wi-Fi 網路中顯而易見,但兩部裝置之間也可以自動建立熱點,你不需要手動設定。

  • 如果裝置支援 Wi-Fi Direct,則已內建這項功能。

  • 使用 Pixel 絕佳相機拍攝的相片和影片,也會保留最佳解析度和細節,在大螢幕上看起來更清晰。

  • 由於這項功能仰賴區域網路,因此即使在倫敦地鐵最深處,你仍可透過網路與好友分享內容。

  • 即使在飛機上,你也能在幾千英尺的高空中分享檔案。

  • 最後,對開發人員來說,這能大幅節省資料成本,因為使用者不必使用數據方案,這對旅客來說非常重要。

  • 此外,您也不必維護自己的伺服器。

  • 去年,我們向開發人員和使用者提到,他們將能透過「快速分享」功能,使用 AirDrop 與 iPhone 裝置分享檔案。

  • 現在,我們也透過「快速分享」為開發人員提供相同功能。

  • 我們將推出「快速分享」iOS SDK,讓 iOS 應用程式開發人員能跨平台分享檔案,不受裝置限制。

  • 以上是精美的簡報,現在我們來快速看看程式碼。

  • 我們盡可能簡化了這項程序。

  • 您需要啟動 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 行。

  • 我們知道,透過快速分享和輕碰分享功能,只要輕觸裝置就能分享檔案,這也確實能實現新的用途。

  • 您的應用程式可能會在其他裝置上關閉,只要輕觸一下,檔案就會立即傳輸,您的應用程式也會自動開啟,您將收到 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")
        }
      }
    }

摘要和總結

  • 今天介紹了許多內容,現在來總結一下。

  • 別擔心,您會有幾分鐘的休息時間。

  • 如 Hanrui 先前所述,CompanionDeviceManager 是處理任何藍牙互動、持續藍牙互動的最佳 API。

  • 如果應用程式有任何網路語音通話功能,不一定要是主要用途,例如應用程式有支援用戶端,使用者可以撥打電話給支援團隊。

  • 在這種情況下,Jetpack Telecom 也是在 Android 上處理網路語音通話的最佳方式。

  • 最後,如要分享點對點檔案或開啟應用程式,只要輕碰分享,快速分享功能就是最佳解決方案。

  • 感謝各位,在大家離開前,我想提醒一下,我們稍後會在外面回答各位的問題。

  • 如果對我們介紹的用途有任何疑問,歡迎在諮詢時間提問。

  • 當然,也別忘了在社群媒體上分享你從演講或其他講座中學到的知識。

  • 我們有 #AndroidDevSummit。

  • 再次強調,我們非常重視參與度,希望大家能真正享受活動。

  • 這讓我們知道,我們確實有一個想再次見到的社群。

  • 別忘了到服務台領取社群吊飾,掛在 Android 機器人鑰匙圈上。

  • 我做得對嗎?

  • 感謝您!

程式碼

程式碼片段

「隨附應用程式總覽」影片 06:17 處的 XML 程式碼片段
<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" />
「隨附應用程式總覽」影片 06:34 處的 Kotlin 程式碼片段
class BleForegroundService : Service() {
   override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
      …
      return START_STICKY
   }
}
「使用隨附裝置管理工具導入」影片 08:03 處的 XML 程式碼片段
<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" />
「使用隨附裝置管理工具實作」中 08:38 的 Kotlin 程式碼片段
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?) { … }
   }
)
「使用隨附裝置管理工具實作」影片 09:47 處的 Kotlin 程式碼片段
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) { … }
}
「Jetpack Telecom 簡介」影片 18:29 處的 XML 程式碼片段
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
「Introducing Jetpack Telecom」影片 18:36 處的 Kotlin 程式碼片段
// Initialize CallsManager & register supported capabilities once
val callsManager = CallsManager(context)

callsManager.registerAppWithTelecom(
    capabilities = CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING or
                   CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING
)
「Jetpack Telecom 簡介」影片 18:55 處的 Kotlin 程式碼片段
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
}
「Jetpack Telecom 簡介」影片 19:49 處的 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)
}
「快速分享總覽」中 29:31 的 Kotlin 程式碼片段
// 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)
「快速分享總覽」影片 29:57 的 Kotlin 程式碼片段
// 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)
「快速分享總覽」中 30:23 的 Kotlin 程式碼片段
// 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)
「快速分享總覽」影片 30:49 處的 Kotlin 程式碼片段
// 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")
    }
  }
}