語音助理觸控朗讀

Android Automotive 認為語音是確保行車安全互動的關鍵要素,也是使用者在行車時與 Android Automotive OS 互動的最安全方式之一。因此,我們擴充了 Android 語音助理 API (包括 VoiceInteractionSession),讓語音助理可為使用者執行開車時難以完成的任務。

當使用者與訊息通知互動時,輕觸朗讀功能可讓語音助理代替使用者朗讀及回覆簡訊。如要提供這項功能,您可以將語音助理與 CarVoiceInteractionSession 整合。

在 Automotive 中,發布至通知中心的通知會標示為 INBOXINBOX_IN_GROUP (例如簡訊),並包含播放按鈕。使用者可以點選「Play」,讓所選語音助理朗讀通知內容,並視需要透過語音回覆。

輕觸即可閱讀通知

圖 1. 含有「輕觸即可朗讀」按鈕的通知。

與 CarVoiceInteractionSession 整合

接下來的章節將說明如何將語音助理與 CarVoiceInteractionSession 整合。

支援語音互動

提供車用語音互動服務的應用程式必須整合現有的 Android 語音互動功能。如需更多資訊,請參閱 Android 版 Google 助理 (VoiceInteractionSession 除外)。雖然所有語音互動 API 元素都與行動裝置上實作的方式相同,但 CarVoiceInteractionSession (請參閱「實作 CarVoiceInteractionSession」一文) 會取代 VoiceInteractionSession。如需詳細資訊,請參閱下列頁面:

實作 CarVoiceInteractionSession

CarVoiceInteractionSession 會公開 API,讓您啟用語音助理朗讀簡訊,然後代表使用者回覆這些訊息。

CarVoiceInteractionSessionVoiceInteractionSession 類別之間的主要差異在於,CarVoiceInteractionSession 會在 onShow 中傳入動作,因此語音助理可以在 CarVoiceInteractionSession 開始工作階段時,立即偵測使用者要求的內容。下表列出每個類別的 onShow 參數:

CarVoiceInteractionSession VoiceInteractionSession
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 向使用者朗讀訊息,然後在成功讀取訊息時觸發「Mark as Read」待處理意圖。視需要提示使用者回覆。
VOICE_ACTION_REPLY_NOTIFICATION 可透過鍵盤分割的物件。
KEY_NOTIFICATION 對應至 StatusBarNotification
需要 android.permission.BIND_NOTIFICATION_LISTENER_SERVICE
提示使用者說明回覆訊息,將回覆訊息輸入待處理意圖的 RemoteInputReply,然後觸發待處理意圖。
VOICE_ACTION_HANDLE_EXCEPTION 含有鍵的字串。
KEY_EXCEPTION 對應至 ExceptionValue (請參閱「例外值」)。
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,這個類別提供方法,可從通知中擷取訊息,並將回覆訊息寫入通知的適當待處理意圖。語音助理讀出訊息後,必須觸發「Mark as Read」意圖。

滿足輕觸閱讀先決條件

當使用者觸發語音操作來讀取及回覆訊息時,系統只會通知預設語音助理的 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 的流程圖。

讀取應用程式名稱

如果您希望語音助理在朗讀訊息時大聲念出訊息應用程式的名稱 (例如「Hangouts 的 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;
}