語音助理點擊閱讀

Android Automotive 認為語音是駕駛安全互動的重要組成部分,也是使用者在駕駛時與 Android Automotive 作業系統互動最安全的方式之一。因此,我們擴展了 Android 語音助理 API(包括VoiceInteractionSession ),使語音助理能夠為用戶執行駕駛時難以完成的任務。

當用戶與訊息通知互動時,點擊閱讀使語音助理能夠代表用戶閱讀和回覆簡訊。要提供此功能,您可以將語音助理與CarVoiceInteractionSession整合。

在汽車中,發佈到標識為INBOXINBOX_IN_GROUP的通知中心的通知(例如,SMS 訊息)包含播放按鈕。使用者可點選「播放」 ,讓選定的語音助理大聲朗讀通知,並可選擇透過語音回覆。

點擊閱讀通知

圖 1.有播放按鈕的點擊閱讀通知。

與 CarVoiceInteractionSession 集成

接下來的部分介紹如何將語音助理與CarVoiceInteractionSession整合。

支援語音交互

提供汽車語音互動服務的應用程式必須與現有的Android語音互動整合。要了解更多信息,請參閱Android 版 Google AssistantVoiceInteractionSession除外)。雖然所有語音互動 API 元素與在行動裝置上實現的相同,但CarVoiceInteractionSession (在實作 CarVoiceInteractionSession中描述)取代了VoiceInteractionSession 。有關更多信息,請參閱以下頁面:

實作 CarVoiceInteractionSession

CarVoiceInteractionSession公開了一些 API,您可以使用這些 API 來讓語音助理大聲朗讀文字訊息,然後代表使用者回覆這些訊息。

CarVoiceInteractionSessionVoiceInteractionSession類別之間的主要差異在於CarVoiceInteractionSessiononShow中傳遞操作,因此語音助理可以在CarVoiceInteractionSession啟動會話時立即偵測使用者要求的上下文。各個類別的onShow參數如下表所示:

汽車語音互動會話語音互動會話
onShow採用以下三個參數:
  • args
  • showFlags
  • actions
onShow採用這兩個參數:
  • args
  • showFlags

Android 10 的變化

從 Android 10 開始,平台會呼叫VoiceInteractionService.onGetSupportedVoiceActions來偵測支援哪些操作。語音助理重寫並實作VoiceInteractionService.onGetSupportedVoiceActions ,如以下範例所示:

public class MyInteractionService extends VoiceInteractionService {
    private static final List SUPPORTED_VOICE_ACTIONS = Arrays.asList(
        CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION);

    @Override
    public Set onGetSupportedVoiceActions(@NonNull Set voiceActions) {
       Set result = new HashSet<>(voiceActions);
       result.retainAll(SUPPORTED_VOICE_ACTIONS);
       return result;
   }
}

下表描述了有效操作。有關每個操作的詳細信息,請參閱序列圖

行動預期有效載荷預期的語音互動動作
VOICE_ACTION_READ_NOTIFICATION向使用者大聲朗讀訊息,然後在成功讀取訊息時觸發「標記為已讀掛起」意圖。 (可選)提示使用者進行回覆。
VOICE_ACTION_REPLY_NOTIFICATION可以用鑰匙包裹。
映射到StatusBarNotification KEY_NOTIFICATION
需要android.permission.BIND_NOTIFICATION_LISTENER_SERVICE
提示使用者陳述回覆訊息,將回覆訊息輸入到掛起意圖的RemoteInputReply中,然後觸發掛起意圖。
VOICE_ACTION_HANDLE_EXCEPTION帶鍵的字串。
映射到ExceptionValueKEY_EXCEPTION (在異常值中描述)。
KEY_FALLBACK_ASSISTANT_ENABLED對應到布林值。如果值為true ,則可以處理使用者要求的後備助理已停用。
針對異常要採取的預期操作在異常文件中定義。

異常值

EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING向語音助理指示它缺少Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE權限,並從使用者取得此權限。

請求通知偵聽器權限

如果預設語音助理沒有通知偵聽器權限,則平台的FallbackAssistant (如果汽車製造商啟用了)可能會在通知語音助理要求權限之前大聲朗讀訊息。若要確定FallbackAssistant是否已啟用並已讀取訊息,語音助理應檢查有效負載中的KEY_FALLBACK_ASSISTANT_ENABLED布林值。

平台建議語音助理針對請求該權限的次數添加限速邏輯。這樣做是為了尊重那些不想授予語音助理此權限並希望FallbackAssistant大聲朗讀簡訊的用戶。每次用戶在訊息通知上按下「播放」時提示用戶獲得許可可能會帶來負面的用戶體驗。該平台不會對語音助理施加速率限制。

當請求通知偵聽器權限時,語音助理應使用CarUxRestrictionsManager來確定使用者是在停車還是在開車。如果使用者正在開車,語音助理會顯示一則通知,提供有關如何授予權限的說明。這樣做可以幫助(並提醒)用戶在更安全的情況下授予權限。

使用 StatusBarNotification

隨讀取和回覆語音操作傳入的StatusBarNotification始終位於汽車相容的訊息通知中,如通知用戶訊息所述。雖然某些通知可能沒有「回覆待處理」意圖,但它們都具有「標記為已讀待處理」意圖。

要簡化與通知的交互,請使用NotificationPayloadHandler ,它提供了從通知中提取訊息並將回覆訊息寫入通知的適當待處理意圖的方法。語音助理讀取訊息後,語音助理必須觸發「標記為已讀」意圖。

滿足點擊閱讀先決條件

當使用者觸發語音操作閱讀和回覆訊息時,僅通知預設語音助理的VoiceInteractionSession 。如上所述,這個預設的語音助理也必須具有通知監聽權限。

時序圖

這些圖顯示了CarVoiceInteractionSession actions的邏輯流程:

VOICE_ACTION_READ_NOTIFICATION

圖 2. VOICE_ACTION_READ_NOTIFICATION 的序列圖。

在圖3的情況下,建議對權限請求進行速率限制:

VOICE_ACTION_REPLY_NOTIFICATION

圖 3. VOICE_ACTION_REPLY_NOTIFICATION 的序列圖。

VOICE_ACTION_HANDLE_EXCEPTION

圖 4. VOICE_ACTION_HANDLE_EXCEPTION 的序列圖。

讀取應用程式名稱

如果您希望語音助理在訊息讀出期間大聲朗讀訊息應用程式的名稱(例如,「環聊的Sam 說...」),請建立一個類似於以下程式碼範例所示的函數,以確保助理正在朗讀訊息應用程式的名稱。正確名稱:

@Nullable
String getMessageApplicationName(Context context, StatusBarNotification statusBarNotification) {
    ApplicationInfo info = getApplicationInfo(context, statusBarNotification.getPackageName());
    if (info == null) return null;

    Notification notification = statusBarNotification.getNotification();

    // Sometimes system packages will post on behalf of other apps, so check this
    // field for a system app notification.
    if (isSystemApp(info)
            && notification.extras.containsKey(Notification.EXTRA_SUBSTITUTE_APP_NAME)) {
        return notification.extras.getString(Notification.EXTRA_SUBSTITUTE_APP_NAME);
    } else {
        PackageManager pm = context.getPackageManager();
        return String.valueOf(pm.getApplicationLabel(info));
    }
}

@Nullable
ApplicationInfo getApplicationInfo(Context context, String packageName) {
    final PackageManager pm = context.getPackageManager();
    ApplicationInfo info;
    try {
        info = pm.getApplicationInfo(packageName, 0);
    } catch (PackageManager.NameNotFoundException e) {
        return null;
    }
    return info;
}

boolean isSystemApp(ApplicationInfo info) {
    return (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
}