| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * 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 android.speech; |
| |
| import android.Manifest; |
| import android.annotation.CallbackExecutor; |
| import android.annotation.IntDef; |
| import android.annotation.MainThread; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.RequiresPermission; |
| import android.annotation.TestApi; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.ResolveInfo; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.provider.Settings; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.util.Slog; |
| |
| import com.android.internal.R; |
| |
| import java.lang.annotation.Documented; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Queue; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.LinkedBlockingQueue; |
| |
| /** |
| * This class provides access to the speech recognition service. This service allows access to the |
| * speech recognizer. Do not instantiate this class directly, instead, call |
| * {@link SpeechRecognizer#createSpeechRecognizer(Context)}, or |
| * {@link SpeechRecognizer#createOnDeviceSpeechRecognizer(Context)}. This class's methods must be |
| * invoked only from the main application thread. |
| * |
| * <p>The implementation of this API is likely to stream audio to remote servers to perform speech |
| * recognition. As such this API is not intended to be used for continuous recognition, which would |
| * consume a significant amount of battery and bandwidth. |
| * |
| * <p>Please note that the application must have {@link android.Manifest.permission#RECORD_AUDIO} |
| * permission to use this class. |
| */ |
| public class SpeechRecognizer { |
| /** DEBUG value to enable verbose debug prints */ |
| private static final boolean DBG = false; |
| |
| /** Log messages identifier */ |
| private static final String TAG = "SpeechRecognizer"; |
| |
| /** |
| * Key used to retrieve an {@code ArrayList<String>} from the {@link Bundle} passed to the |
| * {@link RecognitionListener#onResults(Bundle)} and |
| * {@link RecognitionListener#onPartialResults(Bundle)} methods. These strings are the possible |
| * recognition results, where the first element is the most likely candidate. |
| */ |
| public static final String RESULTS_RECOGNITION = "results_recognition"; |
| |
| /** |
| * Key used to retrieve a float array from the {@link Bundle} passed to the |
| * {@link RecognitionListener#onResults(Bundle)} and |
| * {@link RecognitionListener#onPartialResults(Bundle)} methods. The array should be |
| * the same size as the ArrayList provided in {@link #RESULTS_RECOGNITION}, and should contain |
| * values ranging from 0.0 to 1.0, or -1 to represent an unavailable confidence score. |
| * <p> |
| * Confidence values close to 1.0 indicate high confidence (the speech recognizer is confident |
| * that the recognition result is correct), while values close to 0.0 indicate low confidence. |
| * <p> |
| * This value is optional and might not be provided. |
| */ |
| public static final String CONFIDENCE_SCORES = "confidence_scores"; |
| |
| /** |
| * Key used to retrieve an ArrayList<{@link AlternativeSpans}> from the {@link Bundle} |
| * passed to the {@link RecognitionListener#onResults(Bundle)} and |
| * {@link RecognitionListener#onPartialResults(Bundle)} methods. The list should be the same |
| * size as the ArrayList provided in {@link #RESULTS_RECOGNITION}. |
| * |
| * <p> A single {@link SpeechRecognizer} result is represented as a {@link String}. For a |
| * specific span (substring) of the originally recognized result string the recognizer provides |
| * a list of alternative hypotheses in the form of an {@link AlternativeSpan} object. |
| * Alternatives for different spans of a result string are listed in an {@link AlternativeSpans} |
| * object. Each item from the ArrayList retrieved by this key corresponds to a single result |
| * string provided in {@link #RESULTS_RECOGNITION}. |
| * |
| * <p> This value is optional and might not be provided. |
| */ |
| public static final String RESULTS_ALTERNATIVES = "results_alternatives"; |
| |
| /** |
| * Key used to receive an ArrayList<{@link RecognitionPart}> object from the |
| * {@link Bundle} passed to the {@link RecognitionListener#onResults(Bundle)} and |
| * {@link RecognitionListener#onSegmentResults(Bundle)} methods. |
| * |
| * <p> A single {@link SpeechRecognizer} result is represented as a {@link String}. Each word of |
| * the resulting String, as well as any potential adjacent punctuation, is represented by a |
| * {@link RecognitionPart} item from the ArrayList retrieved by this key. |
| */ |
| public static final String RECOGNITION_PARTS = "recognition_parts"; |
| |
| /** |
| * Key used to retrieve a {@link String} representation of the IETF language tag (as defined by |
| * BCP 47, e.g., "en-US", "de-DE") of the detected language of the most recent audio chunk. |
| * |
| * <p> This info is returned to the client in the {@link Bundle} passed to |
| * {@link RecognitionListener#onLanguageDetection(Bundle)} only if |
| * {@link RecognizerIntent#EXTRA_ENABLE_LANGUAGE_DETECTION} is set. Additionally, if |
| * {@link RecognizerIntent#EXTRA_LANGUAGE_DETECTION_ALLOWED_LANGUAGES} are listed, |
| * the detected language is constrained to be one from the list. |
| */ |
| public static final String DETECTED_LANGUAGE = "detected_language"; |
| |
| /** |
| * Key used to retrieve the level of confidence of the detected language |
| * of the most recent audio chunk, |
| * represented by an {@code int} value prefixed by {@code LANGUAGE_DETECTION_CONFIDENCE_LEVEL_}. |
| * |
| * <p> This info is returned to the client in the {@link Bundle} passed to |
| * {@link RecognitionListener#onLanguageDetection(Bundle)} only if |
| * {@link RecognizerIntent#EXTRA_ENABLE_LANGUAGE_DETECTION} is set. |
| */ |
| public static final String LANGUAGE_DETECTION_CONFIDENCE_LEVEL = |
| "language_detection_confidence_level"; |
| |
| /** |
| * The level of language detection confidence. |
| * |
| * @hide |
| */ |
| @Documented |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef(prefix = {"LANGUAGE_DETECTION_CONFIDENCE_LEVEL_"}, value = { |
| LANGUAGE_DETECTION_CONFIDENCE_LEVEL_UNKNOWN, |
| LANGUAGE_DETECTION_CONFIDENCE_LEVEL_NOT_CONFIDENT, |
| LANGUAGE_DETECTION_CONFIDENCE_LEVEL_CONFIDENT, |
| LANGUAGE_DETECTION_CONFIDENCE_LEVEL_HIGHLY_CONFIDENT |
| }) |
| public @interface LanguageDetectionConfidenceLevel {} |
| |
| public static final int LANGUAGE_DETECTION_CONFIDENCE_LEVEL_UNKNOWN = 0; |
| public static final int LANGUAGE_DETECTION_CONFIDENCE_LEVEL_NOT_CONFIDENT = 1; |
| public static final int LANGUAGE_DETECTION_CONFIDENCE_LEVEL_CONFIDENT = 2; |
| public static final int LANGUAGE_DETECTION_CONFIDENCE_LEVEL_HIGHLY_CONFIDENT = 3; |
| |
| /** |
| * Key used to retrieve an ArrayList<{@link String}> containing representations of the |
| * IETF language tags (as defined by BCP 47, e.g., "en-US", "en-UK") denoting the alternative |
| * locales for the same language retrieved by the key {@link #DETECTED_LANGUAGE}. |
| * |
| * This info is returned to the client in the {@link Bundle} passed to |
| * {@link RecognitionListener#onLanguageDetection(Bundle)} only if |
| * {@link RecognizerIntent#EXTRA_ENABLE_LANGUAGE_DETECTION} is set. |
| */ |
| public static final String TOP_LOCALE_ALTERNATIVES = "top_locale_alternatives"; |
| |
| /** |
| * Key used to retrieve the result of the language switch of the most recent audio chunk, |
| * represented by an {@code int} value prefixed by {@code LANGUAGE_SWITCH_}. |
| * |
| * <p> This info is returned to the client in the {@link Bundle} passed to the |
| * {@link RecognitionListener#onLanguageDetection(Bundle)} only if |
| * {@link RecognizerIntent#EXTRA_ENABLE_LANGUAGE_SWITCH} is set. |
| */ |
| public static final String LANGUAGE_SWITCH_RESULT = "language_switch_result"; |
| |
| /** |
| * The result of the language switch. |
| * |
| * @hide |
| */ |
| @Documented |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef(prefix = {"LANGUAGE_SWITCH_RESULT_"}, value = { |
| LANGUAGE_SWITCH_RESULT_NOT_ATTEMPTED, |
| LANGUAGE_SWITCH_RESULT_SUCCEEDED, |
| LANGUAGE_SWITCH_RESULT_FAILED, |
| LANGUAGE_SWITCH_RESULT_SKIPPED_NO_MODEL |
| }) |
| public @interface LanguageSwitchResult {} |
| |
| /** Switch not attempted. */ |
| public static final int LANGUAGE_SWITCH_RESULT_NOT_ATTEMPTED = 0; |
| |
| /** Switch attempted and succeeded. */ |
| public static final int LANGUAGE_SWITCH_RESULT_SUCCEEDED = 1; |
| |
| /** Switch attempted and failed. */ |
| public static final int LANGUAGE_SWITCH_RESULT_FAILED = 2; |
| |
| /** |
| * Switch skipped because the language model is missing |
| * or the language is not allowlisted for auto switch. |
| */ |
| public static final int LANGUAGE_SWITCH_RESULT_SKIPPED_NO_MODEL = 3; |
| |
| /** |
| * The reason speech recognition failed. |
| * |
| * @hide |
| */ |
| @Documented |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef(prefix = {"ERROR_"}, value = { |
| ERROR_NETWORK_TIMEOUT, |
| ERROR_NETWORK, |
| ERROR_AUDIO, |
| ERROR_SERVER, |
| ERROR_CLIENT, |
| ERROR_SPEECH_TIMEOUT, |
| ERROR_NO_MATCH, |
| ERROR_RECOGNIZER_BUSY, |
| ERROR_INSUFFICIENT_PERMISSIONS, |
| ERROR_TOO_MANY_REQUESTS, |
| ERROR_SERVER_DISCONNECTED, |
| ERROR_LANGUAGE_NOT_SUPPORTED, |
| ERROR_LANGUAGE_UNAVAILABLE, |
| ERROR_CANNOT_CHECK_SUPPORT, |
| ERROR_CANNOT_LISTEN_TO_DOWNLOAD_EVENTS, |
| }) |
| public @interface RecognitionError {} |
| |
| /** Network operation timed out. */ |
| public static final int ERROR_NETWORK_TIMEOUT = 1; |
| |
| /** Other network related errors. */ |
| public static final int ERROR_NETWORK = 2; |
| |
| /** Audio recording error. */ |
| public static final int ERROR_AUDIO = 3; |
| |
| /** Server sends error status. */ |
| public static final int ERROR_SERVER = 4; |
| |
| /** Other client side errors. */ |
| public static final int ERROR_CLIENT = 5; |
| |
| /** No speech input */ |
| public static final int ERROR_SPEECH_TIMEOUT = 6; |
| |
| /** No recognition result matched. */ |
| public static final int ERROR_NO_MATCH = 7; |
| |
| /** RecognitionService busy. */ |
| public static final int ERROR_RECOGNIZER_BUSY = 8; |
| |
| /** Insufficient permissions */ |
| public static final int ERROR_INSUFFICIENT_PERMISSIONS = 9; |
| |
| /** Too many requests from the same client. */ |
| public static final int ERROR_TOO_MANY_REQUESTS = 10; |
| |
| /** Server has been disconnected, e.g. because the app has crashed. */ |
| public static final int ERROR_SERVER_DISCONNECTED = 11; |
| |
| /** Requested language is not available to be used with the current recognizer. */ |
| public static final int ERROR_LANGUAGE_NOT_SUPPORTED = 12; |
| |
| /** Requested language is supported, but not available currently (e.g. not downloaded yet). */ |
| public static final int ERROR_LANGUAGE_UNAVAILABLE = 13; |
| |
| /** The service does not allow to check for support. */ |
| public static final int ERROR_CANNOT_CHECK_SUPPORT = 14; |
| |
| /** The service does not support listening to model downloads events. */ |
| public static final int ERROR_CANNOT_LISTEN_TO_DOWNLOAD_EVENTS = 15; |
| |
| /** action codes */ |
| private static final int MSG_START = 1; |
| private static final int MSG_STOP = 2; |
| private static final int MSG_CANCEL = 3; |
| private static final int MSG_CHANGE_LISTENER = 4; |
| private static final int MSG_SET_TEMPORARY_ON_DEVICE_COMPONENT = 5; |
| private static final int MSG_CHECK_RECOGNITION_SUPPORT = 6; |
| private static final int MSG_TRIGGER_MODEL_DOWNLOAD = 7; |
| |
| /** The actual RecognitionService endpoint */ |
| private IRecognitionService mService; |
| |
| /** Context with which the manager was created */ |
| private final Context mContext; |
| |
| /** Component to direct service intent to */ |
| private final ComponentName mServiceComponent; |
| |
| /** Whether to use on-device speech recognizer. */ |
| private final boolean mOnDevice; |
| |
| private IRecognitionServiceManager mManagerService; |
| |
| /** Handler that will execute the main tasks */ |
| private Handler mHandler = new Handler(Looper.getMainLooper()) { |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_START: |
| handleStartListening((Intent) msg.obj); |
| break; |
| case MSG_STOP: |
| handleStopMessage(); |
| break; |
| case MSG_CANCEL: |
| handleCancelMessage(); |
| break; |
| case MSG_CHANGE_LISTENER: |
| handleChangeListener((RecognitionListener) msg.obj); |
| break; |
| case MSG_SET_TEMPORARY_ON_DEVICE_COMPONENT: |
| handleSetTemporaryComponent((ComponentName) msg.obj); |
| break; |
| case MSG_CHECK_RECOGNITION_SUPPORT: |
| CheckRecognitionSupportArgs args = (CheckRecognitionSupportArgs) msg.obj; |
| handleCheckRecognitionSupport( |
| args.mIntent, args.mCallbackExecutor, args.mCallback); |
| break; |
| case MSG_TRIGGER_MODEL_DOWNLOAD: |
| ModelDownloadListenerArgs modelDownloadListenerArgs = |
| (ModelDownloadListenerArgs) msg.obj; |
| handleTriggerModelDownload( |
| modelDownloadListenerArgs.mIntent, |
| modelDownloadListenerArgs.mExecutor, |
| modelDownloadListenerArgs.mModelDownloadListener); |
| break; |
| } |
| } |
| }; |
| |
| /** |
| * Temporary queue, saving the messages until the connection will be established, afterwards, |
| * only mHandler will receive the messages |
| */ |
| private final Queue<Message> mPendingTasks = new LinkedBlockingQueue<>(); |
| |
| /** The Listener that will receive all the callbacks */ |
| private final InternalRecognitionListener mListener = new InternalRecognitionListener(); |
| |
| private final IBinder mClientToken = new Binder(); |
| |
| /** |
| * The right way to create a {@code SpeechRecognizer} is by using |
| * {@link #createSpeechRecognizer} static factory method |
| */ |
| private SpeechRecognizer(final Context context, final ComponentName serviceComponent) { |
| mContext = context; |
| mServiceComponent = serviceComponent; |
| mOnDevice = false; |
| } |
| |
| /** |
| * The right way to create a {@code SpeechRecognizer} is by using |
| * {@link #createOnDeviceSpeechRecognizer} static factory method |
| */ |
| private SpeechRecognizer(final Context context, boolean onDevice) { |
| mContext = context; |
| mServiceComponent = null; |
| mOnDevice = onDevice; |
| } |
| |
| /** |
| * Checks whether a speech recognition service is available on the system. If this method |
| * returns {@code false}, {@link SpeechRecognizer#createSpeechRecognizer(Context)} will |
| * fail. |
| * |
| * @param context with which {@code SpeechRecognizer} will be created |
| * @return {@code true} if recognition is available, {@code false} otherwise |
| */ |
| public static boolean isRecognitionAvailable(@NonNull final Context context) { |
| // TODO(b/176578753): make sure this works well with system speech recognizers. |
| final List<ResolveInfo> list = context.getPackageManager().queryIntentServices( |
| new Intent(RecognitionService.SERVICE_INTERFACE), 0); |
| return list != null && list.size() != 0; |
| } |
| |
| /** |
| * Checks whether an on-device speech recognition service is available on the system. If this |
| * method returns {@code false}, |
| * {@link SpeechRecognizer#createOnDeviceSpeechRecognizer(Context)} will |
| * fail. |
| * |
| * @param context with which on-device {@code SpeechRecognizer} will be created |
| * @return {@code true} if on-device recognition is available, {@code false} otherwise |
| */ |
| public static boolean isOnDeviceRecognitionAvailable(@NonNull final Context context) { |
| ComponentName componentName = |
| ComponentName.unflattenFromString( |
| context.getString(R.string.config_defaultOnDeviceSpeechRecognitionService)); |
| return componentName != null; |
| } |
| |
| /** |
| * Factory method to create a new {@code SpeechRecognizer}. Please note that |
| * {@link #setRecognitionListener(RecognitionListener)} should be called before dispatching any |
| * command to the created {@code SpeechRecognizer}, otherwise no notifications will be |
| * received. |
| * |
| * <p>For apps targeting Android 11 (API level 30) interaction with a speech recognition |
| * service requires <queries> element to be added to the manifest file: |
| * <pre>{@code |
| * <queries> |
| * <intent> |
| * <action |
| * android:name="android.speech.RecognitionService" /> |
| * </intent> |
| * </queries> |
| * }</pre> |
| * |
| * @param context in which to create {@code SpeechRecognizer} |
| * @return a new {@code SpeechRecognizer} |
| */ |
| @MainThread |
| public static SpeechRecognizer createSpeechRecognizer(final Context context) { |
| return createSpeechRecognizer(context, null); |
| } |
| |
| /** |
| * Factory method to create a new {@code SpeechRecognizer}. Please note that |
| * {@link #setRecognitionListener(RecognitionListener)} should be called before dispatching any |
| * command to the created {@code SpeechRecognizer}, otherwise no notifications will be |
| * received. |
| * Use this version of the method to specify a specific service to direct this |
| * {@link SpeechRecognizer} to. |
| * |
| * <p><strong>Important</strong>: before calling this method, please check via |
| * {@link android.content.pm.PackageManager#queryIntentServices(Intent, int)} that {@code |
| * serviceComponent} actually exists and provides |
| * {@link RecognitionService#SERVICE_INTERFACE}. Normally you would not use this; call |
| * {@link #createSpeechRecognizer(Context)} to use the system default recognition |
| * service instead or {@link #createOnDeviceSpeechRecognizer(Context)} to use on-device |
| * recognition.</p> |
| * |
| * <p>For apps targeting Android 11 (API level 30) interaction with a speech recognition |
| * service requires <queries> element to be added to the manifest file: |
| * <pre>{@code |
| * <queries> |
| * <intent> |
| * <action |
| * android:name="android.speech.RecognitionService" /> |
| * </intent> |
| * </queries> |
| * }</pre> |
| * |
| * @param context in which to create {@code SpeechRecognizer} |
| * @param serviceComponent the {@link ComponentName} of a specific service to direct this |
| * {@code SpeechRecognizer} to |
| * @return a new {@code SpeechRecognizer} |
| */ |
| @MainThread |
| public static SpeechRecognizer createSpeechRecognizer(final Context context, |
| final ComponentName serviceComponent) { |
| if (context == null) { |
| throw new IllegalArgumentException("Context cannot be null"); |
| } |
| checkIsCalledFromMainThread(); |
| return new SpeechRecognizer(context, serviceComponent); |
| } |
| |
| /** |
| * Factory method to create a new {@code SpeechRecognizer}. |
| * |
| * <p>Please note that {@link #setRecognitionListener(RecognitionListener)} should be called |
| * before dispatching any command to the created {@code SpeechRecognizer}, otherwise no |
| * notifications will be received. |
| * |
| * @param context in which to create {@code SpeechRecognizer} |
| * @return a new on-device {@code SpeechRecognizer}. |
| * @throws UnsupportedOperationException iff {@link #isOnDeviceRecognitionAvailable(Context)} |
| * is false |
| */ |
| @NonNull |
| @MainThread |
| public static SpeechRecognizer createOnDeviceSpeechRecognizer(@NonNull final Context context) { |
| if (!isOnDeviceRecognitionAvailable(context)) { |
| throw new UnsupportedOperationException("On-device recognition is not available"); |
| } |
| return lenientlyCreateOnDeviceSpeechRecognizer(context); |
| } |
| |
| /** |
| * Helper method to create on-device SpeechRecognizer in tests even when the device does not |
| * support on-device speech recognition. |
| * |
| * @hide |
| */ |
| @TestApi |
| @NonNull |
| @MainThread |
| public static SpeechRecognizer createOnDeviceTestingSpeechRecognizer( |
| @NonNull final Context context) { |
| return lenientlyCreateOnDeviceSpeechRecognizer(context); |
| } |
| |
| @NonNull |
| @MainThread |
| private static SpeechRecognizer lenientlyCreateOnDeviceSpeechRecognizer( |
| @NonNull final Context context) { |
| if (context == null) { |
| throw new IllegalArgumentException("Context cannot be null"); |
| } |
| checkIsCalledFromMainThread(); |
| return new SpeechRecognizer(context, /* onDevice */ true); |
| } |
| |
| /** |
| * Sets the listener that will receive all the callbacks. The previous unfinished commands will |
| * be executed with the old listener, while any following command will be executed with the new |
| * listener. |
| * |
| * @param listener listener that will receive all the callbacks from the created |
| * {@link SpeechRecognizer}, this must not be null. |
| */ |
| @MainThread |
| public void setRecognitionListener(RecognitionListener listener) { |
| checkIsCalledFromMainThread(); |
| putMessage(Message.obtain(mHandler, MSG_CHANGE_LISTENER, listener)); |
| } |
| |
| /** |
| * Starts listening for speech. Please note that |
| * {@link #setRecognitionListener(RecognitionListener)} should be called beforehand, otherwise |
| * no notifications will be received. |
| * |
| * @param recognizerIntent contains parameters for the recognition to be performed. The intent |
| * may also contain optional extras, see {@link RecognizerIntent}. If these values are |
| * not set explicitly, default values will be used by the recognizer. |
| */ |
| @MainThread |
| public void startListening(final Intent recognizerIntent) { |
| if (recognizerIntent == null) { |
| throw new IllegalArgumentException("intent must not be null"); |
| } |
| checkIsCalledFromMainThread(); |
| |
| if (DBG) { |
| Slog.i(TAG, "#startListening called"); |
| if (mService == null) { |
| Slog.i(TAG, "Connection is not established yet"); |
| } |
| } |
| |
| if (mService == null) { |
| // First time connection: first establish a connection, then dispatch #startListening. |
| connectToSystemService(); |
| } |
| putMessage(Message.obtain(mHandler, MSG_START, recognizerIntent)); |
| } |
| |
| /** |
| * Stops listening for speech. Speech captured so far will be recognized as if the user had |
| * stopped speaking at this point. |
| * |
| * <p>Note that in the default case, this does not need to be called, as the speech endpointer |
| * will automatically stop the recognizer listening when it determines speech has completed. |
| * However, you can manipulate endpointer parameters directly using the intent extras defined in |
| * {@link RecognizerIntent}, in which case you may sometimes want to manually call this method |
| * to stop listening sooner. |
| * |
| * <p>Upon invocation clients must wait until {@link RecognitionListener#onResults} or |
| * {@link RecognitionListener#onError} are invoked before calling |
| * {@link SpeechRecognizer#startListening} again. Otherwise such an attempt would be rejected by |
| * recognition service. |
| * |
| * <p>Please note that |
| * {@link #setRecognitionListener(RecognitionListener)} should be called beforehand, otherwise |
| * no notifications will be received. |
| */ |
| @MainThread |
| public void stopListening() { |
| checkIsCalledFromMainThread(); |
| |
| if (DBG) { |
| Slog.i(TAG, "#stopListening called"); |
| if (mService == null) { |
| Slog.i(TAG, "Connection is not established yet"); |
| } |
| } |
| |
| putMessage(Message.obtain(mHandler, MSG_STOP)); |
| } |
| |
| /** |
| * Cancels the speech recognition. Please note that |
| * {@link #setRecognitionListener(RecognitionListener)} should be called beforehand, otherwise |
| * no notifications will be received. |
| */ |
| @MainThread |
| public void cancel() { |
| checkIsCalledFromMainThread(); |
| putMessage(Message.obtain(mHandler, MSG_CANCEL)); |
| } |
| |
| /** |
| * Checks whether {@code recognizerIntent} is supported by |
| * {@link SpeechRecognizer#startListening(Intent)}. |
| * |
| * @param recognizerIntent contains parameters for the recognition to be performed. The intent |
| * may also contain optional extras. See {@link RecognizerIntent} for the list of |
| * supported extras, any unlisted extra might be ignored. |
| * @param supportListener the listener on which to receive the support query results. |
| */ |
| public void checkRecognitionSupport( |
| @NonNull Intent recognizerIntent, |
| @NonNull @CallbackExecutor Executor executor, |
| @NonNull RecognitionSupportCallback supportListener) { |
| Objects.requireNonNull(recognizerIntent, "intent must not be null"); |
| Objects.requireNonNull(supportListener, "listener must not be null"); |
| |
| if (DBG) { |
| Slog.i(TAG, "#checkRecognitionSupport called"); |
| if (mService == null) { |
| Slog.i(TAG, "Connection is not established yet"); |
| } |
| } |
| |
| if (mService == null) { |
| // First time connection: first establish a connection, then dispatch. |
| connectToSystemService(); |
| } |
| putMessage(Message.obtain(mHandler, MSG_CHECK_RECOGNITION_SUPPORT, |
| new CheckRecognitionSupportArgs(recognizerIntent, executor, supportListener))); |
| } |
| |
| /** |
| * Attempts to download the support for the given {@code recognizerIntent}. This might trigger |
| * user interaction to approve the download. Callers can verify the status of the request via |
| * {@link #checkRecognitionSupport(Intent, Executor, RecognitionSupportCallback)}. |
| * |
| * @param recognizerIntent contains parameters for the recognition to be performed. The intent |
| * may also contain optional extras, see {@link RecognizerIntent}. |
| */ |
| public void triggerModelDownload(@NonNull Intent recognizerIntent) { |
| Objects.requireNonNull(recognizerIntent, "intent must not be null"); |
| if (DBG) { |
| Slog.i(TAG, "#triggerModelDownload without a listener called"); |
| if (mService == null) { |
| Slog.i(TAG, "Connection is not established yet"); |
| } |
| } |
| if (mService == null) { |
| // First time connection: first establish a connection, then dispatch. |
| connectToSystemService(); |
| } |
| putMessage(Message.obtain( |
| mHandler, MSG_TRIGGER_MODEL_DOWNLOAD, |
| new ModelDownloadListenerArgs(recognizerIntent, null, null))); |
| } |
| |
| /** |
| * Attempts to download the support for the given {@code recognizerIntent}. This might trigger |
| * user interaction to approve the download. Callers can verify the status of the request via |
| * {@link #checkRecognitionSupport(Intent, Executor, RecognitionSupportCallback)}. |
| * |
| * <p> The updates about the model download request are received via the given |
| * {@link ModelDownloadListener}: |
| * |
| * <li> If the model is already available, {@link ModelDownloadListener#onSuccess()} will be |
| * called directly. The model can be safely used afterwards. |
| * |
| * <li> If the {@link RecognitionService} has started the download, |
| * {@link ModelDownloadListener#onProgress(int)} will be called an unspecified (zero or more) |
| * number of times until the download is complete. |
| * When the download finishes, {@link ModelDownloadListener#onSuccess()} will be called. |
| * The model can be safely used afterwards. |
| * |
| * <li> If the {@link RecognitionService} has only scheduled the download, but won't satisfy it |
| * immediately, {@link ModelDownloadListener#onScheduled()} will be called. |
| * There will be no further updates on this listener. |
| * |
| * <li> If the request fails at any time due to a network or scheduling error, |
| * {@link ModelDownloadListener#onError(int)} will be called. |
| * |
| * @param recognizerIntent contains parameters for the recognition to be performed. The intent |
| * may also contain optional extras, see {@link RecognizerIntent}. |
| * @param executor for dispatching listener callbacks |
| * @param listener on which to receive updates about the model download request. |
| */ |
| public void triggerModelDownload( |
| @NonNull Intent recognizerIntent, |
| @NonNull @CallbackExecutor Executor executor, |
| @NonNull ModelDownloadListener listener) { |
| Objects.requireNonNull(recognizerIntent, "intent must not be null"); |
| if (DBG) { |
| Slog.i(TAG, "#triggerModelDownload with a listener called"); |
| if (mService == null) { |
| Slog.i(TAG, "Connection is not established yet"); |
| } |
| } |
| if (mService == null) { |
| // First time connection: first establish a connection, then dispatch. |
| connectToSystemService(); |
| } |
| putMessage(Message.obtain( |
| mHandler, MSG_TRIGGER_MODEL_DOWNLOAD, |
| new ModelDownloadListenerArgs(recognizerIntent, executor, listener))); |
| } |
| |
| /** |
| * Sets a temporary component to power on-device speech recognizer. |
| * |
| * <p>This is only expected to be called in tests, system would reject calls from client apps. |
| * |
| * @param componentName name of the component to set temporary replace speech recognizer. {@code |
| * null} value resets the recognizer to default. |
| * |
| * @hide |
| */ |
| @TestApi |
| @RequiresPermission(Manifest.permission.MANAGE_SPEECH_RECOGNITION) |
| public void setTemporaryOnDeviceRecognizer(@Nullable ComponentName componentName) { |
| mHandler.sendMessage( |
| Message.obtain(mHandler, MSG_SET_TEMPORARY_ON_DEVICE_COMPONENT, componentName)); |
| } |
| |
| private static void checkIsCalledFromMainThread() { |
| if (Looper.myLooper() != Looper.getMainLooper()) { |
| throw new RuntimeException( |
| "SpeechRecognizer should be used only from the application's main thread"); |
| } |
| } |
| |
| private void putMessage(Message msg) { |
| if (mService == null) { |
| mPendingTasks.offer(msg); |
| } else { |
| mHandler.sendMessage(msg); |
| } |
| } |
| |
| /** sends the actual message to the service */ |
| private void handleStartListening(Intent recognizerIntent) { |
| if (!checkOpenConnection()) { |
| return; |
| } |
| try { |
| mService.startListening(recognizerIntent, mListener, mContext.getAttributionSource()); |
| if (DBG) Log.d(TAG, "service start listening command succeeded"); |
| } catch (final RemoteException e) { |
| Log.e(TAG, "startListening() failed", e); |
| mListener.onError(ERROR_CLIENT); |
| } |
| } |
| |
| /** sends the actual message to the service */ |
| private void handleStopMessage() { |
| if (!checkOpenConnection()) { |
| return; |
| } |
| try { |
| mService.stopListening(mListener); |
| if (DBG) Log.d(TAG, "service stop listening command succeeded"); |
| } catch (final RemoteException e) { |
| Log.e(TAG, "stopListening() failed", e); |
| mListener.onError(ERROR_CLIENT); |
| } |
| } |
| |
| /** sends the actual message to the service */ |
| private void handleCancelMessage() { |
| if (!checkOpenConnection()) { |
| return; |
| } |
| try { |
| mService.cancel(mListener, /*isShutdown*/ false); |
| if (DBG) Log.d(TAG, "service cancel command succeeded"); |
| } catch (final RemoteException e) { |
| Log.e(TAG, "cancel() failed", e); |
| mListener.onError(ERROR_CLIENT); |
| } |
| } |
| |
| private void handleSetTemporaryComponent(ComponentName componentName) { |
| if (DBG) { |
| Log.d(TAG, "handleSetTemporaryComponent, componentName=" + componentName); |
| } |
| |
| if (!maybeInitializeManagerService()) { |
| return; |
| } |
| |
| try { |
| mManagerService.setTemporaryComponent(componentName); |
| } catch (final RemoteException e) { |
| e.rethrowFromSystemServer(); |
| } |
| } |
| |
| private void handleCheckRecognitionSupport( |
| Intent recognizerIntent, |
| Executor callbackExecutor, |
| RecognitionSupportCallback recognitionSupportCallback) { |
| if (!maybeInitializeManagerService()) { |
| return; |
| } |
| try { |
| mService.checkRecognitionSupport( |
| recognizerIntent, |
| mContext.getAttributionSource(), |
| new InternalSupportCallback(callbackExecutor, recognitionSupportCallback)); |
| if (DBG) Log.d(TAG, "service support command succeeded"); |
| } catch (final RemoteException e) { |
| Log.e(TAG, "checkRecognitionSupport() failed", e); |
| callbackExecutor.execute(() -> recognitionSupportCallback.onError(ERROR_CLIENT)); |
| } |
| } |
| |
| private void handleTriggerModelDownload( |
| Intent recognizerIntent, |
| @Nullable Executor callbackExecutor, |
| @Nullable ModelDownloadListener modelDownloadListener) { |
| if (!maybeInitializeManagerService()) { |
| return; |
| } |
| |
| // Trigger model download without a listener. |
| if (modelDownloadListener == null) { |
| try { |
| mService.triggerModelDownload( |
| recognizerIntent, mContext.getAttributionSource(), null); |
| if (DBG) Log.d(TAG, "triggerModelDownload() without a listener"); |
| } catch (final RemoteException e) { |
| Log.e(TAG, "triggerModelDownload() without a listener failed", e); |
| mListener.onError(ERROR_CLIENT); |
| } |
| } |
| // Trigger model download with a listener. |
| else { |
| try { |
| mService.triggerModelDownload( |
| recognizerIntent, mContext.getAttributionSource(), |
| new InternalModelDownloadListener(callbackExecutor, modelDownloadListener)); |
| if (DBG) Log.d(TAG, "triggerModelDownload() with a listener"); |
| } catch (final RemoteException e) { |
| Log.e(TAG, "triggerModelDownload() with a listener failed", e); |
| callbackExecutor.execute(() -> modelDownloadListener.onError(ERROR_CLIENT)); |
| } |
| } |
| } |
| |
| private boolean checkOpenConnection() { |
| if (mService != null) { |
| return true; |
| } |
| mListener.onError(ERROR_CLIENT); |
| Log.e(TAG, "not connected to the recognition service"); |
| return false; |
| } |
| |
| /** changes the listener */ |
| private void handleChangeListener(RecognitionListener listener) { |
| if (DBG) Log.d(TAG, "handleChangeListener, listener=" + listener); |
| mListener.mInternalListener = listener; |
| } |
| |
| /** Destroys the {@code SpeechRecognizer} object. */ |
| public void destroy() { |
| if (mService != null) { |
| try { |
| mService.cancel(mListener, /*isShutdown*/ true); |
| } catch (final RemoteException e) { |
| // Not important |
| } |
| } |
| |
| mService = null; |
| mPendingTasks.clear(); |
| mListener.mInternalListener = null; |
| } |
| |
| /** Establishes a connection to system server proxy and initializes the session. */ |
| private void connectToSystemService() { |
| if (!maybeInitializeManagerService()) { |
| return; |
| } |
| |
| ComponentName componentName = getSpeechRecognizerComponentName(); |
| |
| if (!mOnDevice && componentName == null) { |
| mListener.onError(ERROR_CLIENT); |
| return; |
| } |
| |
| try { |
| mManagerService.createSession( |
| componentName, |
| mClientToken, |
| mOnDevice, |
| new IRecognitionServiceManagerCallback.Stub(){ |
| @Override |
| public void onSuccess(IRecognitionService service) throws RemoteException { |
| if (DBG) { |
| Log.i(TAG, "Connected to speech recognition service"); |
| } |
| mService = service; |
| while (!mPendingTasks.isEmpty()) { |
| mHandler.sendMessage(mPendingTasks.poll()); |
| } |
| } |
| |
| @Override |
| public void onError(int errorCode) throws RemoteException { |
| Log.e(TAG, "Bind to system recognition service failed with error " |
| + errorCode); |
| mListener.onError(errorCode); |
| } |
| }); |
| } catch (RemoteException e) { |
| e.rethrowFromSystemServer(); |
| } |
| } |
| |
| private synchronized boolean maybeInitializeManagerService() { |
| if (DBG) { |
| Log.i(TAG, "#maybeInitializeManagerService found = " + mManagerService); |
| } |
| if (mManagerService != null) { |
| return true; |
| } |
| |
| IBinder service = ServiceManager.getService(Context.SPEECH_RECOGNITION_SERVICE); |
| if (service == null && mOnDevice) { |
| service = (IBinder) mContext.getSystemService(Context.SPEECH_RECOGNITION_SERVICE); |
| } |
| mManagerService = IRecognitionServiceManager.Stub.asInterface(service); |
| |
| if (mManagerService == null) { |
| if (mListener != null) { |
| mListener.onError(ERROR_CLIENT); |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Returns the component name to be used for establishing a connection, based on the parameters |
| * used during initialization. |
| * |
| * <p>Note the 3 different scenarios: |
| * <ol> |
| * <li>On-device speech recognizer which is determined by the manufacturer and not |
| * changeable by the user |
| * <li>Default user-selected speech recognizer as specified by |
| * {@code Settings.Secure.VOICE_RECOGNITION_SERVICE} |
| * <li>Custom speech recognizer supplied by the client. |
| */ |
| private ComponentName getSpeechRecognizerComponentName() { |
| if (mOnDevice) { |
| return null; |
| } |
| |
| if (mServiceComponent != null) { |
| return mServiceComponent; |
| } |
| |
| String serviceComponent = Settings.Secure.getString(mContext.getContentResolver(), |
| Settings.Secure.VOICE_RECOGNITION_SERVICE); |
| |
| if (TextUtils.isEmpty(serviceComponent)) { |
| Log.e(TAG, "no selected voice recognition service"); |
| mListener.onError(ERROR_CLIENT); |
| return null; |
| } |
| |
| return ComponentName.unflattenFromString(serviceComponent); |
| } |
| |
| private static class CheckRecognitionSupportArgs { |
| final Intent mIntent; |
| final Executor mCallbackExecutor; |
| final RecognitionSupportCallback mCallback; |
| |
| private CheckRecognitionSupportArgs( |
| Intent intent, |
| Executor callbackExecutor, |
| RecognitionSupportCallback callback) { |
| mIntent = intent; |
| mCallbackExecutor = callbackExecutor; |
| mCallback = callback; |
| } |
| } |
| |
| private static class ModelDownloadListenerArgs { |
| final Intent mIntent; |
| final Executor mExecutor; |
| final ModelDownloadListener mModelDownloadListener; |
| |
| private ModelDownloadListenerArgs(Intent intent, Executor executor, |
| ModelDownloadListener modelDownloadListener) { |
| mIntent = intent; |
| mExecutor = executor; |
| mModelDownloadListener = modelDownloadListener; |
| } |
| } |
| |
| /** |
| * Internal wrapper of IRecognitionListener which will propagate the results to |
| * RecognitionListener |
| */ |
| private static class InternalRecognitionListener extends IRecognitionListener.Stub { |
| private RecognitionListener mInternalListener; |
| |
| private static final int MSG_BEGINNING_OF_SPEECH = 1; |
| private static final int MSG_BUFFER_RECEIVED = 2; |
| private static final int MSG_END_OF_SPEECH = 3; |
| private static final int MSG_ERROR = 4; |
| private static final int MSG_READY_FOR_SPEECH = 5; |
| private static final int MSG_RESULTS = 6; |
| private static final int MSG_PARTIAL_RESULTS = 7; |
| private static final int MSG_RMS_CHANGED = 8; |
| private static final int MSG_ON_EVENT = 9; |
| private static final int MSG_SEGMENT_RESULTS = 10; |
| private static final int MSG_SEGMENT_END_SESSION = 11; |
| private static final int MSG_LANGUAGE_DETECTION = 12; |
| |
| private final Handler mInternalHandler = new Handler(Looper.getMainLooper()) { |
| @Override |
| public void handleMessage(Message msg) { |
| if (mInternalListener == null) { |
| return; |
| } |
| switch (msg.what) { |
| case MSG_BEGINNING_OF_SPEECH: |
| mInternalListener.onBeginningOfSpeech(); |
| break; |
| case MSG_BUFFER_RECEIVED: |
| mInternalListener.onBufferReceived((byte[]) msg.obj); |
| break; |
| case MSG_END_OF_SPEECH: |
| mInternalListener.onEndOfSpeech(); |
| break; |
| case MSG_ERROR: |
| mInternalListener.onError((Integer) msg.obj); |
| break; |
| case MSG_READY_FOR_SPEECH: |
| mInternalListener.onReadyForSpeech((Bundle) msg.obj); |
| break; |
| case MSG_RESULTS: |
| mInternalListener.onResults((Bundle) msg.obj); |
| break; |
| case MSG_PARTIAL_RESULTS: |
| mInternalListener.onPartialResults((Bundle) msg.obj); |
| break; |
| case MSG_RMS_CHANGED: |
| mInternalListener.onRmsChanged((Float) msg.obj); |
| break; |
| case MSG_ON_EVENT: |
| mInternalListener.onEvent(msg.arg1, (Bundle) msg.obj); |
| break; |
| case MSG_SEGMENT_RESULTS: |
| mInternalListener.onSegmentResults((Bundle) msg.obj); |
| break; |
| case MSG_SEGMENT_END_SESSION: |
| mInternalListener.onEndOfSegmentedSession(); |
| break; |
| case MSG_LANGUAGE_DETECTION: |
| mInternalListener.onLanguageDetection((Bundle) msg.obj); |
| break; |
| } |
| } |
| }; |
| |
| public void onBeginningOfSpeech() { |
| Message.obtain(mInternalHandler, MSG_BEGINNING_OF_SPEECH).sendToTarget(); |
| } |
| |
| public void onBufferReceived(final byte[] buffer) { |
| Message.obtain(mInternalHandler, MSG_BUFFER_RECEIVED, buffer).sendToTarget(); |
| } |
| |
| public void onEndOfSpeech() { |
| Message.obtain(mInternalHandler, MSG_END_OF_SPEECH).sendToTarget(); |
| } |
| |
| public void onError(final int error) { |
| Message.obtain(mInternalHandler, MSG_ERROR, error).sendToTarget(); |
| } |
| |
| public void onReadyForSpeech(final Bundle noiseParams) { |
| Message.obtain(mInternalHandler, MSG_READY_FOR_SPEECH, noiseParams).sendToTarget(); |
| } |
| |
| public void onResults(final Bundle results) { |
| Message.obtain(mInternalHandler, MSG_RESULTS, results).sendToTarget(); |
| } |
| |
| public void onPartialResults(final Bundle results) { |
| Message.obtain(mInternalHandler, MSG_PARTIAL_RESULTS, results).sendToTarget(); |
| } |
| |
| public void onRmsChanged(final float rmsdB) { |
| Message.obtain(mInternalHandler, MSG_RMS_CHANGED, rmsdB).sendToTarget(); |
| } |
| |
| public void onSegmentResults(final Bundle bundle) { |
| Message.obtain(mInternalHandler, MSG_SEGMENT_RESULTS, bundle).sendToTarget(); |
| } |
| |
| public void onEndOfSegmentedSession() { |
| Message.obtain(mInternalHandler, MSG_SEGMENT_END_SESSION).sendToTarget(); |
| } |
| |
| public void onLanguageDetection(final Bundle results) { |
| Message.obtain(mInternalHandler, MSG_LANGUAGE_DETECTION, results).sendToTarget(); |
| } |
| |
| public void onEvent(final int eventType, final Bundle params) { |
| Message.obtain(mInternalHandler, MSG_ON_EVENT, eventType, eventType, params) |
| .sendToTarget(); |
| } |
| } |
| |
| private static class InternalSupportCallback extends IRecognitionSupportCallback.Stub { |
| private final Executor mExecutor; |
| private final RecognitionSupportCallback mCallback; |
| |
| private InternalSupportCallback(Executor executor, RecognitionSupportCallback callback) { |
| this.mExecutor = executor; |
| this.mCallback = callback; |
| } |
| |
| @Override |
| public void onSupportResult(RecognitionSupport recognitionSupport) throws RemoteException { |
| mExecutor.execute(() -> mCallback.onSupportResult(recognitionSupport)); |
| } |
| |
| @Override |
| public void onError(int errorCode) throws RemoteException { |
| mExecutor.execute(() -> mCallback.onError(errorCode)); |
| } |
| } |
| |
| private static class InternalModelDownloadListener extends IModelDownloadListener.Stub { |
| private final Executor mExecutor; |
| private final ModelDownloadListener mModelDownloadListener; |
| |
| private InternalModelDownloadListener( |
| Executor executor, |
| @NonNull ModelDownloadListener modelDownloadListener) { |
| mExecutor = executor; |
| mModelDownloadListener = modelDownloadListener; |
| } |
| |
| @Override |
| public void onProgress(int completedPercent) throws RemoteException { |
| mExecutor.execute(() -> mModelDownloadListener.onProgress(completedPercent)); |
| } |
| |
| @Override |
| public void onSuccess() throws RemoteException { |
| mExecutor.execute(() -> mModelDownloadListener.onSuccess()); |
| } |
| |
| @Override |
| public void onScheduled() throws RemoteException { |
| mExecutor.execute(() -> mModelDownloadListener.onScheduled()); |
| } |
| |
| @Override |
| public void onError(int error) throws RemoteException { |
| mExecutor.execute(() -> mModelDownloadListener.onError(error)); |
| } |
| } |
| } |