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