/** * Copyright 2017 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ai.api.services; import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.speech.RecognitionListener; import android.speech.RecognizerIntent; import android.speech.SpeechRecognizer; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import ai.api.android.AIConfiguration; import ai.api.android.AIService; import ai.api.AIServiceException; import ai.api.PartialResultsListener; import ai.api.RequestExtras; import ai.api.model.AIContext; import ai.api.model.AIError; import ai.api.model.AIRequest; import ai.api.model.AIResponse; import ai.api.util.RecognizerChecker; import ai.api.util.VersionConfig; public class GoogleRecognitionServiceImpl extends AIService { private static final String TAG = GoogleRecognitionServiceImpl.class.getName(); private static final long STOP_DELAY = 1000; private SpeechRecognizer speechRecognizer; private final Object speechRecognizerLock = new Object(); private RequestExtras requestExtras; private PartialResultsListener partialResultsListener; private final VersionConfig versionConfig; private volatile boolean recognitionActive = false; private volatile boolean wasReadyForSpeech; private final Handler handler = new Handler(); private Runnable stopRunnable; private final Map<Integer, String> errorMessages = new HashMap<>(); { errorMessages.put(SpeechRecognizer.ERROR_NETWORK_TIMEOUT, "Network operation timed out."); errorMessages.put(SpeechRecognizer.ERROR_NETWORK, "Other network related errors."); errorMessages.put(SpeechRecognizer.ERROR_AUDIO, "Audio recording error."); errorMessages.put(SpeechRecognizer.ERROR_SERVER, "Server sends error status."); errorMessages.put(SpeechRecognizer.ERROR_CLIENT, "Other client side errors."); errorMessages.put(SpeechRecognizer.ERROR_SPEECH_TIMEOUT, "No speech input."); errorMessages.put(SpeechRecognizer.ERROR_NO_MATCH, "No recognition result matched."); errorMessages.put(SpeechRecognizer.ERROR_RECOGNIZER_BUSY, "RecognitionService busy."); errorMessages.put(SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS, "Insufficient permissions."); } public GoogleRecognitionServiceImpl(final Context context, final AIConfiguration config) { super(config, context); final ComponentName component = RecognizerChecker.findGoogleRecognizer(context); if (component == null) { Log.w(TAG, "Google Recognizer application not found on device. " + "Quality of the recognition may be low. Please check if Google Search application installed and enabled."); } versionConfig = VersionConfig.init(context); if (versionConfig.isAutoStopRecognizer()) { stopRunnable = new Runnable() { @Override public void run() { stopListening(); } }; } } /** * Manage recognizer cancellation runnable. * * @param action (int) (0 - stop, 1 - restart) */ private void updateStopRunnable(final int action) { if (stopRunnable != null) { if (action == 0) { handler.removeCallbacks(stopRunnable); } else if (action == 1) { handler.removeCallbacks(stopRunnable); handler.postDelayed(stopRunnable, STOP_DELAY); } } } protected void initializeRecognizer() { if (speechRecognizer != null) { return; } synchronized (speechRecognizerLock) { if (speechRecognizer != null) { speechRecognizer.destroy(); speechRecognizer = null; } final ComponentName component = RecognizerChecker.findGoogleRecognizer(context); speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context, component); speechRecognizer.setRecognitionListener(new InternalRecognitionListener()); } } protected void clearRecognizer() { Log.d(TAG, "clearRecognizer"); if (speechRecognizer != null) { synchronized (speechRecognizerLock) { if (speechRecognizer != null) { speechRecognizer.destroy(); speechRecognizer = null; } } } } private void sendRequest(@NonNull final AIRequest aiRequest, @Nullable final RequestExtras requestExtras) { if (aiRequest == null) { throw new IllegalArgumentException("aiRequest must be not null"); } final AsyncTask<AIRequest, Integer, AIResponse> task = new AsyncTask<AIRequest, Integer, AIResponse>() { private AIError aiError; @Override protected AIResponse doInBackground(final AIRequest... params) { final AIRequest request = params[0]; try { return aiDataService.request(request, requestExtras); } catch (final AIServiceException e) { aiError = new AIError(e); return null; } } @Override protected void onPostExecute(final AIResponse response) { if (response != null) { onResult(response); } else { onError(aiError); } } }; task.execute(aiRequest); } @Override public void startListening() { startListening(new RequestExtras()); } @Override public void startListening(final List<AIContext> contexts) { startListening(new RequestExtras(contexts, null)); } @Override public void startListening(final RequestExtras requestExtras) { if (!recognitionActive) { synchronized (speechRecognizerLock) { this.requestExtras = requestExtras; if (!checkPermissions()) { final AIError aiError = new AIError("RECORD_AUDIO permission is denied. Please request permission from user."); onError(aiError); return; } initializeRecognizer(); recognitionActive = true; final Intent sttIntent = createRecognitionIntent(); try { wasReadyForSpeech = false; speechRecognizer.startListening(sttIntent); } catch (final SecurityException e) { //Error occurs only on HTC devices. } } } else { Log.w(TAG, "Trying to start recognition while another recognition active"); if (!wasReadyForSpeech) { cancel(); } } } private Intent createRecognitionIntent() { final Intent sttIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); sttIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); final String language = config.getLanguage().replace('-', '_'); sttIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); sttIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE, language); sttIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true); sttIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, context.getPackageName()); // WORKAROUND for https://code.google.com/p/android/issues/detail?id=75347 sttIntent.putExtra("android.speech.extra.EXTRA_ADDITIONAL_LANGUAGES", new String[]{language}); return sttIntent; } @Override public void stopListening() { synchronized (speechRecognizerLock) { if (speechRecognizer != null) { speechRecognizer.stopListening(); } } } @Override public void cancel() { synchronized (speechRecognizerLock) { if (recognitionActive) { recognitionActive = false; if (speechRecognizer != null) { speechRecognizer.cancel(); } onListeningCancelled(); } } } private void restartRecognition() { updateStopRunnable(0); recognitionActive = false; synchronized (speechRecognizerLock) { try { if (speechRecognizer != null) { speechRecognizer.cancel(); final Intent intent = createRecognitionIntent(); wasReadyForSpeech = false; speechRecognizer.startListening(intent); recognitionActive = true; } } catch (Exception e) { stopListening(); } } } /** * This method must be called from UI thread */ @Override public void pause() { clearRecognizer(); } /** * This method must be called from UI thread */ @Override public void resume() { } public void setPartialResultsListener(PartialResultsListener partialResultsListener) { this.partialResultsListener = partialResultsListener; } protected void onPartialResults(final List<String> partialResults) { if (partialResultsListener != null) { partialResultsListener.onPartialResults(partialResults); } } private void stopInternal() { updateStopRunnable(0); if (versionConfig.isDestroyRecognizer()) clearRecognizer(); recognitionActive = false; } private class InternalRecognitionListener implements RecognitionListener { @Override public void onReadyForSpeech(final Bundle params) { if (recognitionActive) { onListeningStarted(); } wasReadyForSpeech = true; } @Override public void onBeginningOfSpeech() { } @Override public void onRmsChanged(final float rmsdB) { if (recognitionActive) { onAudioLevelChanged(rmsdB); } } @Override public void onBufferReceived(final byte[] buffer) { } @Override public void onEndOfSpeech() { if (recognitionActive) { onListeningFinished(); } } @Override public void onError(final int error) { if (error == SpeechRecognizer.ERROR_NO_MATCH && !wasReadyForSpeech) { Log.d(TAG, "SpeechRecognizer.ERROR_NO_MATCH, restartRecognition()"); restartRecognition(); return; } if (recognitionActive) { final AIError aiError; if (errorMessages.containsKey(error)) { final String description = errorMessages.get(error); aiError = new AIError("Speech recognition engine error: " + description); } else { aiError = new AIError("Speech recognition engine error: " + error); } GoogleRecognitionServiceImpl.this.onError(aiError); } stopInternal(); } @TargetApi(14) @Override public void onResults(final Bundle results) { if (recognitionActive) { final ArrayList<String> recognitionResults = results .getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); float[] rates = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { rates = results.getFloatArray(SpeechRecognizer.CONFIDENCE_SCORES); } if (recognitionResults == null || recognitionResults.isEmpty()) { // empty response GoogleRecognitionServiceImpl.this.onResult(new AIResponse()); } else { final AIRequest aiRequest = new AIRequest(); if (rates != null) { aiRequest.setQuery(recognitionResults.toArray(new String[recognitionResults.size()]), rates); } else { aiRequest.setQuery(recognitionResults.get(0)); } // notify listeners about the last recogntion result for more accurate user feedback GoogleRecognitionServiceImpl.this.onPartialResults(recognitionResults); GoogleRecognitionServiceImpl.this.sendRequest(aiRequest, requestExtras); } } stopInternal(); } @Override public void onPartialResults(final Bundle partialResults) { if (recognitionActive) { updateStopRunnable(1); final ArrayList<String> partialRecognitionResults = partialResults.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); if (partialRecognitionResults != null && !partialRecognitionResults.isEmpty()) { GoogleRecognitionServiceImpl.this.onPartialResults(partialRecognitionResults); } } } @Override public void onEvent(final int eventType, final Bundle params) { } } }