1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.voiceinteraction.service; 18 19 import static android.voiceinteraction.service.MainHotwordDetectionService.FAKE_HOTWORD_AUDIO_DATA; 20 21 import android.app.ambientcontext.AmbientContextEventRequest; 22 import android.app.wearable.WearableSensingManager; 23 import android.media.AudioFormat; 24 import android.os.ParcelFileDescriptor; 25 import android.os.PersistableBundle; 26 import android.os.SharedMemory; 27 import android.service.ambientcontext.AmbientContextDetectionResult; 28 import android.service.ambientcontext.AmbientContextDetectionServiceStatus; 29 import android.service.voice.HotwordAudioStream; 30 import android.service.wearable.WearableSensingService; 31 import android.util.Log; 32 import android.voiceinteraction.common.Utils; 33 34 import java.io.OutputStream; 35 import java.util.Set; 36 import java.util.function.Consumer; 37 38 /** The {@link WearableSensingService} to use with voice interaction CTS tests. */ 39 public class MainWearableSensingService extends WearableSensingService { 40 41 /** PersistableBundle key that represents an action, such as setup and send audio. */ 42 public static final String BUNDLE_ACTION_KEY = "ACTION"; 43 44 /** PersistableBundle value that represents a request to reset the service. */ 45 public static final String ACTION_RESET = "RESET"; 46 47 /** PersistableBundle value that represents a request to send audio to the audioConsumer. */ 48 public static final String ACTION_SEND_AUDIO = "SEND_AUDIO"; 49 50 /** 51 * PersistableBundle value that represents a request to send non-hotword audio to the 52 * audioConsumer. 53 */ 54 public static final String ACTION_SEND_NON_HOTWORD_AUDIO = "SEND_NON_HOTWORD_AUDIO"; 55 56 /** 57 * PersistableBundle value that represents a request to send more audio data to the stream 58 * previously sent to audioConsumer. 59 */ 60 public static final String ACTION_SEND_MORE_AUDIO_DATA = "SEND_MORE_AUDIO_DATA"; 61 62 /** 63 * PersistableBundle value that represents a request to verify 64 * onValidatedByHotwordDetectionService is called. 65 */ 66 public static final String ACTION_VERIFY_HOTWORD_VALIDATED_CALLED = 67 "VERIFY_HOTWORD_VALIDATED_CALLED"; 68 69 /** 70 * PersistableBundle value that represents a request to verify onStopHotwordAudioStream 71 * is called. 72 */ 73 public static final String ACTION_VERIFY_AUDIO_STOP_CALLED = "VERIFY_DATA_STOP_CALLED"; 74 75 /** 76 * PersistableBundle value that represents a request to send non-hotword audio to the 77 * audioConsumer along with an option that overrides the hotword detection result to positive. 78 */ 79 public static final String ACTION_SEND_NON_HOTWORD_AUDIO_WITH_ACCEPT_DETECTION_OPTIONS = 80 "SEND_NON_HOTWORD_AUDIO_WITH_ACCEPT_DETECTION_OPTIONS"; 81 82 private static final String TAG = "MainWearableSensingService"; 83 private static final AudioFormat FAKE_AUDIO_FORMAT = 84 new AudioFormat.Builder() 85 .setSampleRate(10000) 86 .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) 87 .setEncoding(AudioFormat.ENCODING_PCM_16BIT) 88 .build(); 89 private static final int PIPE_READ_INDEX = 0; 90 private static final int PIPE_WRITE_INDEX = 1; 91 // MainHotwordDetectionService will reject this byte stream as non-hotword 92 private static final byte[] NON_HOTWORD_AUDIO = 93 new byte[] {'n', 'o', 'n', 'h', 'o', 't', 'w', 'o', 'r', 'd'}; 94 95 private Consumer<HotwordAudioStream> mAudioConsumer; 96 private volatile boolean mOnValidatedByHotwordDetectionServiceCalled = false; 97 private volatile boolean mOnStopHotwordAudioStreamCalled = false; 98 private OutputStream mAudioOutputStream; 99 100 @Override onCreate()101 public void onCreate() { 102 Log.i(TAG, "#onCreate"); 103 } 104 105 @Override onStartHotwordRecognition( Consumer<HotwordAudioStream> audioConsumer, Consumer<Integer> statusConsumer)106 public void onStartHotwordRecognition( 107 Consumer<HotwordAudioStream> audioConsumer, Consumer<Integer> statusConsumer) { 108 mAudioConsumer = audioConsumer; 109 statusConsumer.accept(WearableSensingManager.STATUS_SUCCESS); 110 } 111 112 @Override onValidatedByHotwordDetectionService()113 public void onValidatedByHotwordDetectionService() { 114 Log.i(TAG, "#onValidatedByHotwordDetectionService"); 115 mOnValidatedByHotwordDetectionServiceCalled = true; 116 } 117 118 @Override onStopHotwordAudioStream()119 public void onStopHotwordAudioStream() { 120 Log.i(TAG, "#onStopHotwordAudioStream"); 121 mOnStopHotwordAudioStreamCalled = true; 122 } 123 124 /** Unrelated to voice interaction, but used to set up the service and verify interactions. */ 125 @Override onDataProvided( PersistableBundle data, SharedMemory sharedMemory, Consumer<Integer> statusConsumer)126 public void onDataProvided( 127 PersistableBundle data, SharedMemory sharedMemory, Consumer<Integer> statusConsumer) { 128 String action = data.getString(BUNDLE_ACTION_KEY); 129 Log.i(TAG, "#onDataProvided, action: " + action); 130 try { 131 switch (action) { 132 case ACTION_RESET: 133 reset(); 134 statusConsumer.accept(WearableSensingManager.STATUS_SUCCESS); 135 return; 136 case ACTION_SEND_AUDIO: 137 sendAudio(statusConsumer); 138 return; 139 case ACTION_SEND_NON_HOTWORD_AUDIO: 140 sendNonHotwordAudio(statusConsumer); 141 return; 142 case ACTION_SEND_NON_HOTWORD_AUDIO_WITH_ACCEPT_DETECTION_OPTIONS: 143 sendNonHotwordAudioWithAcceptDetectionOptions(statusConsumer); 144 return; 145 case ACTION_SEND_MORE_AUDIO_DATA: 146 sendMoreAudioData(statusConsumer); 147 return; 148 case ACTION_VERIFY_HOTWORD_VALIDATED_CALLED: 149 verifyHotwordValidatedCalled(statusConsumer); 150 return; 151 case ACTION_VERIFY_AUDIO_STOP_CALLED: 152 verifyAudioStopCalled(statusConsumer); 153 return; 154 default: 155 Log.w(TAG, "Unknown action: " + action); 156 statusConsumer.accept(WearableSensingManager.STATUS_UNKNOWN); 157 return; 158 } 159 } catch (Exception ex) { 160 // Exception in this process will not show up in the test runner, so just Log it and 161 // return an unknown status code. 162 Log.e(TAG, "Unexpected exception in onDataProvided.", ex); 163 statusConsumer.accept(WearableSensingManager.STATUS_UNKNOWN); 164 } 165 } 166 sendAudio(Consumer<Integer> statusConsumer)167 private void sendAudio(Consumer<Integer> statusConsumer) throws Exception { 168 // MainHotwordDetectionService will accept this data as hotword audio data 169 sendAudio(FAKE_HOTWORD_AUDIO_DATA, statusConsumer, PersistableBundle.EMPTY); 170 } 171 sendNonHotwordAudio(Consumer<Integer> statusConsumer)172 private void sendNonHotwordAudio(Consumer<Integer> statusConsumer) throws Exception { 173 // MainHotwordDetectionService will reject this as non-hotword audio data 174 sendAudio(NON_HOTWORD_AUDIO, statusConsumer, PersistableBundle.EMPTY); 175 } 176 sendNonHotwordAudioWithAcceptDetectionOptions(Consumer<Integer> statusConsumer)177 private void sendNonHotwordAudioWithAcceptDetectionOptions(Consumer<Integer> statusConsumer) 178 throws Exception { 179 PersistableBundle options = new PersistableBundle(); 180 // MainHotwordDetectionService will accept this result after reading the options 181 options.putBoolean(Utils.KEY_ACCEPT_DETECTION, true); 182 sendAudio(NON_HOTWORD_AUDIO, statusConsumer, options); 183 } 184 sendAudio( byte[] audioData, Consumer<Integer> statusConsumer, PersistableBundle options)185 private void sendAudio( 186 byte[] audioData, 187 Consumer<Integer> statusConsumer, 188 PersistableBundle options) 189 throws Exception { 190 Log.i(TAG, "#sendAudio"); 191 if (mAudioConsumer == null) { 192 Log.e(TAG, "Cannot send audio. mAudioConsumer is null"); 193 statusConsumer.accept(WearableSensingManager.STATUS_UNKNOWN); 194 return; 195 } 196 ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe(); 197 mAudioOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pipe[PIPE_WRITE_INDEX]); 198 mAudioOutputStream.write(audioData); 199 mAudioConsumer.accept( 200 new HotwordAudioStream.Builder(FAKE_AUDIO_FORMAT, pipe[PIPE_READ_INDEX]) 201 .setMetadata(options) 202 .build()); 203 pipe[PIPE_READ_INDEX].close(); 204 statusConsumer.accept(WearableSensingManager.STATUS_SUCCESS); 205 } 206 sendMoreAudioData(Consumer<Integer> statusConsumer)207 private void sendMoreAudioData(Consumer<Integer> statusConsumer) throws Exception { 208 if (mAudioOutputStream == null) { 209 Log.w(TAG, "Cannot send more audio data. mAudioOutputStream is null"); 210 statusConsumer.accept(WearableSensingManager.STATUS_UNKNOWN); 211 return; 212 } 213 mAudioOutputStream.write('i'); // the exact value sent doesn't matter 214 statusConsumer.accept(WearableSensingManager.STATUS_SUCCESS); 215 } 216 verifyHotwordValidatedCalled(Consumer<Integer> statusConsumer)217 private void verifyHotwordValidatedCalled(Consumer<Integer> statusConsumer) throws Exception { 218 // A better alternative is to have this wait on a latch because the callback is async, 219 // but somehow awaiting here prevents other methods on WearableSensingService to be 220 // called despite the AIDL being annotated with oneway. 221 Log.i(TAG, "#verifyHotwordValidatedCalled"); 222 if (mOnValidatedByHotwordDetectionServiceCalled) { 223 statusConsumer.accept(WearableSensingManager.STATUS_SUCCESS); 224 } else { 225 statusConsumer.accept(WearableSensingManager.STATUS_UNKNOWN); 226 } 227 } 228 verifyAudioStopCalled(Consumer<Integer> statusConsumer)229 private void verifyAudioStopCalled(Consumer<Integer> statusConsumer) throws Exception { 230 Log.i(TAG, "#verifyAudioStopCalled"); 231 if (mOnStopHotwordAudioStreamCalled) { 232 statusConsumer.accept(WearableSensingManager.STATUS_SUCCESS); 233 } else { 234 statusConsumer.accept(WearableSensingManager.STATUS_UNKNOWN); 235 } 236 } 237 reset()238 private void reset() throws Exception { 239 mOnValidatedByHotwordDetectionServiceCalled = false; 240 mOnStopHotwordAudioStreamCalled = false; 241 mAudioConsumer = null; 242 if (mAudioOutputStream != null) { 243 mAudioOutputStream.close(); 244 mAudioOutputStream = null; 245 } 246 } 247 248 /******************************************************************************** 249 * Placeholder implementation of abstract methods unrelated to voice interaction. 250 ********************************************************************************/ 251 @Override onDataStreamProvided( ParcelFileDescriptor parcelFileDescriptor, Consumer<Integer> statusConsumer)252 public void onDataStreamProvided( 253 ParcelFileDescriptor parcelFileDescriptor, Consumer<Integer> statusConsumer) {} 254 255 @Override onStartDetection( AmbientContextEventRequest request, String packageName, Consumer<AmbientContextDetectionServiceStatus> statusConsumer, Consumer<AmbientContextDetectionResult> detectionResultConsumer)256 public void onStartDetection( 257 AmbientContextEventRequest request, 258 String packageName, 259 Consumer<AmbientContextDetectionServiceStatus> statusConsumer, 260 Consumer<AmbientContextDetectionResult> detectionResultConsumer) {} 261 262 @Override onStopDetection(String packageName)263 public void onStopDetection(String packageName) {} 264 265 @Override onQueryServiceStatus( Set<Integer> eventTypes, String packageName, Consumer<AmbientContextDetectionServiceStatus> consumer)266 public void onQueryServiceStatus( 267 Set<Integer> eventTypes, 268 String packageName, 269 Consumer<AmbientContextDetectionServiceStatus> consumer) {} 270 } 271