1 /* 2 * Copyright (C) 2021 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.voicerecognition.cts; 18 19 import static android.voicerecognition.cts.TestObjects.ERROR_CODE; 20 import static android.voicerecognition.cts.TestObjects.LANGUAGE_DETECTION_BUNDLE; 21 import static android.voicerecognition.cts.TestObjects.PARTIAL_RESULTS_BUNDLE; 22 import static android.voicerecognition.cts.TestObjects.READY_FOR_SPEECH_BUNDLE; 23 import static android.voicerecognition.cts.TestObjects.RESULTS_BUNDLE; 24 import static android.voicerecognition.cts.TestObjects.RMS_CHANGED_VALUE; 25 import static android.voicerecognition.cts.TestObjects.SEGMENT_RESULTS_BUNDLE; 26 27 import static com.google.common.truth.Truth.assertThat; 28 29 import static org.junit.Assert.fail; 30 31 import android.content.AttributionSource; 32 import android.content.Intent; 33 import android.os.RemoteException; 34 import android.speech.ModelDownloadListener; 35 import android.speech.RecognitionService; 36 import android.speech.SpeechRecognizer; 37 import android.util.Log; 38 import android.util.Pair; 39 40 import androidx.annotation.NonNull; 41 42 import java.util.ArrayDeque; 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Queue; 48 import java.util.Random; 49 import java.util.concurrent.atomic.AtomicBoolean; 50 import java.util.concurrent.atomic.AtomicInteger; 51 import java.util.function.Consumer; 52 53 public class CtsRecognitionService extends RecognitionService { 54 private static final String TAG = CtsRecognitionService.class.getSimpleName(); 55 56 /** Map of the recognizer methods invoked, indexed by their id. */ 57 public static Map<Integer, List<RecognizerMethod>> sInvokedRecognizerMethods = new HashMap<>(); 58 59 /** 60 * Queue of instructions for callbacks on main tasks ({@link 61 * CtsRecognitionService#onStartListening}, {@link CtsRecognitionService#onStopListening}, 62 * {@link CtsRecognitionService#onCancel}). Each instruction is a pair 63 * consisting of a recognizer id and the callback to be run on the recognizer's listener. 64 */ 65 public static Queue<Pair<Integer, CallbackMethod>> sInstructedCallbackMethods = 66 new ArrayDeque<>(); 67 68 /** 69 * Queue of instructions for callbacks on the model download task - {@link 70 * CtsRecognitionService#onTriggerModelDownload( 71 * Intent, AttributionSource, ModelDownloadListener)}. 72 * 73 * <p> Each instruction represents the callback to be run on the given listener. 74 */ 75 public static Queue<ModelDownloadCallback> sInstructedModelDownloadCallbacks = null; 76 77 public static AtomicBoolean sIsActive = new AtomicBoolean(false); 78 public static Queue<Consumer<SupportCallback>> sConsumerQueue = new ArrayDeque<>(); 79 public static List<Intent> sDownloadTriggers = new ArrayList<>(); 80 81 public static AtomicInteger sBindCount = new AtomicInteger(0); 82 83 static final int MAX_CONCURRENT_SESSIONS_COUNT = 3; 84 85 private final Random mRandom = new Random(); 86 87 @Override onStartListening(Intent recognizerIntent, Callback listener)88 protected void onStartListening(Intent recognizerIntent, Callback listener) { 89 sIsActive.set(true); 90 assertThat(listener.getCallingUid()).isEqualTo(android.os.Process.myUid()); 91 92 int recognizerId = processInstructedCallback(listener); 93 if (recognizerId >= 0) { 94 sInvokedRecognizerMethods.putIfAbsent(recognizerId, new ArrayList<>()); 95 sInvokedRecognizerMethods.get(recognizerId) 96 .add(RecognizerMethod.RECOGNIZER_METHOD_START_LISTENING); 97 } 98 sIsActive.set(false); 99 } 100 101 @Override onBindInternal()102 public void onBindInternal() { 103 Log.d(TAG, "onBind"); 104 sBindCount.incrementAndGet(); 105 } 106 107 @Override onUnbind(Intent intent)108 public boolean onUnbind(Intent intent) { 109 Log.d(TAG, "onUnbind"); 110 sBindCount.decrementAndGet(); 111 return super.onUnbind(intent); 112 } 113 114 @Override onStopListening(Callback listener)115 protected void onStopListening(Callback listener) { 116 sIsActive.set(true); 117 assertThat(listener.getCallingUid()).isEqualTo(android.os.Process.myUid()); 118 119 int recognizerId = processInstructedCallback(listener); 120 if (recognizerId >= 0) { 121 sInvokedRecognizerMethods.putIfAbsent(recognizerId, new ArrayList<>()); 122 sInvokedRecognizerMethods.get(recognizerId) 123 .add(RecognizerMethod.RECOGNIZER_METHOD_STOP_LISTENING); 124 } 125 sIsActive.set(false); 126 } 127 128 @Override onCheckRecognitionSupport( @onNull Intent recognizerIntent, @NonNull AttributionSource attributionSource, @NonNull SupportCallback supportCallback)129 public void onCheckRecognitionSupport( 130 @NonNull Intent recognizerIntent, 131 @NonNull AttributionSource attributionSource, 132 @NonNull SupportCallback supportCallback) { 133 Consumer<SupportCallback> consumer = sConsumerQueue.poll(); 134 if (consumer == null) { 135 supportCallback.onError(SpeechRecognizer.ERROR_CANNOT_CHECK_SUPPORT); 136 } else { 137 assertThat(attributionSource.getUid()).isEqualTo(android.os.Process.myUid()); 138 consumer.accept(supportCallback); 139 } 140 } 141 142 @Override onTriggerModelDownload( @onNull Intent recognizerIntent, @NonNull AttributionSource attributionSource)143 public void onTriggerModelDownload( 144 @NonNull Intent recognizerIntent, 145 @NonNull AttributionSource attributionSource) { 146 assertThat(attributionSource.getUid()).isEqualTo(android.os.Process.myUid()); 147 sDownloadTriggers.add(recognizerIntent); 148 } 149 150 @Override onTriggerModelDownload( @onNull Intent recognizerIntent, @NonNull AttributionSource attributionSource, @NonNull ModelDownloadListener listener)151 public void onTriggerModelDownload( 152 @NonNull Intent recognizerIntent, 153 @NonNull AttributionSource attributionSource, 154 @NonNull ModelDownloadListener listener) { 155 assertThat(attributionSource.getUid()).isEqualTo(android.os.Process.myUid()); 156 157 while (!sInstructedModelDownloadCallbacks.isEmpty()) { 158 ModelDownloadCallback callback = sInstructedModelDownloadCallbacks.poll(); 159 switch (callback) { 160 case ON_PROGRESS: 161 listener.onProgress(50); 162 break; 163 case ON_SUCCESS: 164 listener.onSuccess(); 165 break; 166 case ON_SCHEDULED: 167 listener.onScheduled(); 168 break; 169 case ON_ERROR: 170 listener.onError(0); 171 break; 172 default: 173 break; 174 } 175 } 176 sDownloadTriggers.add(recognizerIntent); 177 } 178 179 @Override onCancel(Callback listener)180 protected void onCancel(Callback listener) { 181 sIsActive.set(true); 182 assertThat(listener.getCallingUid()).isEqualTo(android.os.Process.myUid()); 183 184 int recognizerId = processInstructedCallback(listener); 185 if (recognizerId >= 0) { 186 sInvokedRecognizerMethods.putIfAbsent(recognizerId, new ArrayList<>()); 187 sInvokedRecognizerMethods.get(recognizerId).add( 188 RecognizerMethod.RECOGNIZER_METHOD_CANCEL); 189 } 190 sIsActive.set(false); 191 } 192 193 @Override getMaxConcurrentSessionsCount()194 public int getMaxConcurrentSessionsCount() { 195 return MAX_CONCURRENT_SESSIONS_COUNT; 196 } 197 198 /** 199 * Process the next callback instruction in the queue by callback on the given listener. 200 * Return the id of the corresponding recognizer object. 201 * 202 * @param listener listener on which the callback should be invoked 203 * @return the id of the corresponding recognizer 204 */ processInstructedCallback(Callback listener)205 private int processInstructedCallback(Callback listener) { 206 if (sInstructedCallbackMethods.isEmpty()) { 207 return -1; 208 } 209 210 Pair<Integer, CallbackMethod> callbackInstruction = sInstructedCallbackMethods.poll(); 211 int recognizerId = callbackInstruction.first; 212 CallbackMethod callbackMethod = callbackInstruction.second; 213 214 Log.i(TAG, "Responding with " + callbackMethod.name() + "."); 215 216 try { 217 switch (callbackMethod) { 218 case CALLBACK_METHOD_UNSPECIFIED: 219 // ignore 220 break; 221 case CALLBACK_METHOD_BEGINNING_OF_SPEECH: 222 listener.beginningOfSpeech(); 223 break; 224 case CALLBACK_METHOD_BUFFER_RECEIVED: 225 byte[] buffer = new byte[100]; 226 mRandom.nextBytes(buffer); 227 listener.bufferReceived(buffer); 228 break; 229 case CALLBACK_METHOD_END_OF_SPEECH: 230 listener.endOfSpeech(); 231 break; 232 case CALLBACK_METHOD_ERROR: 233 listener.error(ERROR_CODE); 234 break; 235 case CALLBACK_METHOD_RESULTS: 236 listener.results(RESULTS_BUNDLE); 237 break; 238 case CALLBACK_METHOD_PARTIAL_RESULTS: 239 listener.partialResults(PARTIAL_RESULTS_BUNDLE); 240 break; 241 case CALLBACK_METHOD_READY_FOR_SPEECH: 242 listener.readyForSpeech(READY_FOR_SPEECH_BUNDLE); 243 break; 244 case CALLBACK_METHOD_RMS_CHANGED: 245 listener.rmsChanged(RMS_CHANGED_VALUE); 246 break; 247 case CALLBACK_METHOD_SEGMENTS_RESULTS: 248 listener.segmentResults(SEGMENT_RESULTS_BUNDLE); 249 break; 250 case CALLBACK_METHOD_END_SEGMENTED_SESSION: 251 listener.endOfSegmentedSession(); 252 break; 253 case CALLBACK_METHOD_LANGUAGE_DETECTION: 254 listener.languageDetection(LANGUAGE_DETECTION_BUNDLE); 255 break; 256 default: 257 fail(); 258 } 259 } catch (RemoteException e) { 260 fail(); 261 } 262 263 return recognizerId; 264 } 265 totalInvokedRecognizerMethodsCount()266 static int totalInvokedRecognizerMethodsCount() { 267 return sInvokedRecognizerMethods.values().stream().mapToInt(List::size).sum(); 268 } 269 } 270