Merge "Migrate ST to new error APIs" into udc-dev
This commit is contained in:
@@ -41,12 +41,6 @@ oneway interface IRecognitionStatusCallback {
|
||||
|
||||
void onGenericSoundTriggerDetected(in SoundTrigger.GenericRecognitionEvent recognitionEvent);
|
||||
|
||||
/**
|
||||
* Called when the detection fails due to an error.
|
||||
*
|
||||
* @param status The error code that was seen.
|
||||
*/
|
||||
void onError(int status);
|
||||
/**
|
||||
* Called when the recognition is paused temporarily for some reason.
|
||||
*/
|
||||
@@ -55,4 +49,28 @@ oneway interface IRecognitionStatusCallback {
|
||||
* Called when the recognition is resumed after it was temporarily paused.
|
||||
*/
|
||||
void onRecognitionResumed();
|
||||
|
||||
// Error callbacks to follow
|
||||
/**
|
||||
* Called when this recognition has been preempted by another.
|
||||
*/
|
||||
void onPreempted();
|
||||
|
||||
/**
|
||||
* Called when the underlying ST module service has died.
|
||||
*/
|
||||
void onModuleDied();
|
||||
|
||||
/**
|
||||
* Called when the service failed to gracefully resume recognition following a pause.
|
||||
* @param status - The received error code.
|
||||
*/
|
||||
void onResumeFailed(int status);
|
||||
|
||||
/**
|
||||
* Called when the service failed to pause recognition when required.
|
||||
* TODO(b/276507281) Remove. This should never happen, so we should abort instead.
|
||||
* @param status - The received error code.
|
||||
*/
|
||||
void onPauseFailed(int status);
|
||||
}
|
||||
|
||||
@@ -1572,16 +1572,6 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
|
||||
Message.obtain(mHandler, MSG_HOTWORD_REJECTED, result).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int status) {
|
||||
Slog.i(TAG, "onError: " + status);
|
||||
// TODO(b/271534248): This is a workaround before the sound trigger uses the new error
|
||||
// method.
|
||||
Message.obtain(mHandler, MSG_DETECTION_SOUND_TRIGGER_FAILURE,
|
||||
new SoundTriggerFailure(SoundTriggerFailure.ERROR_CODE_UNKNOWN,
|
||||
"Sound trigger error")).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHotwordDetectionServiceFailure(
|
||||
HotwordDetectionServiceFailure hotwordDetectionServiceFailure) {
|
||||
@@ -1604,6 +1594,12 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
|
||||
"onVisualQueryDetectionServiceFailure: " + visualQueryDetectionServiceFailure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSoundTriggerFailure(SoundTriggerFailure soundTriggerFailure) {
|
||||
Message.obtain(mHandler, MSG_DETECTION_SOUND_TRIGGER_FAILURE,
|
||||
Objects.requireNonNull(soundTriggerFailure)).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnknownFailure(String errorMessage) throws RemoteException {
|
||||
Slog.v(TAG, "onUnknownFailure: " + errorMessage);
|
||||
|
||||
@@ -233,14 +233,6 @@ class SoftwareHotwordDetector extends AbstractDetector {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int status) throws RemoteException {
|
||||
if (DEBUG) {
|
||||
Slog.i(TAG, "Ignored #onError (" + status + ") event");
|
||||
}
|
||||
// TODO: Check if we still need to implement this method with DetectorFailure mechanism.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHotwordDetectionServiceFailure(
|
||||
HotwordDetectionServiceFailure hotwordDetectionServiceFailure)
|
||||
@@ -264,6 +256,13 @@ class SoftwareHotwordDetector extends AbstractDetector {
|
||||
+ visualQueryDetectionServiceFailure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSoundTriggerFailure(SoundTriggerFailure onSoundTriggerFailure)
|
||||
throws RemoteException {
|
||||
// It should never be called here.
|
||||
Slog.wtf(TAG, "Unexpected STFailure in software detector: " + onSoundTriggerFailure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnknownFailure(String errorMessage) throws RemoteException {
|
||||
Slog.v(TAG, "onUnknownFailure: " + errorMessage);
|
||||
|
||||
@@ -73,18 +73,28 @@ public final class SoundTriggerFailure implements Parcelable {
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface SoundTriggerErrorCode {}
|
||||
|
||||
private int mErrorCode = ERROR_CODE_UNKNOWN;
|
||||
private String mErrorMessage = "Unknown";
|
||||
private final int mErrorCode;
|
||||
private final String mErrorMessage;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@TestApi
|
||||
public SoundTriggerFailure(int errorCode, @NonNull String errorMessage) {
|
||||
public SoundTriggerFailure(@SoundTriggerErrorCode int errorCode,
|
||||
@NonNull String errorMessage) {
|
||||
if (TextUtils.isEmpty(errorMessage)) {
|
||||
throw new IllegalArgumentException("errorMessage is empty or null.");
|
||||
}
|
||||
mErrorCode = errorCode;
|
||||
switch (errorCode) {
|
||||
case ERROR_CODE_UNKNOWN:
|
||||
case ERROR_CODE_MODULE_DIED:
|
||||
case ERROR_CODE_RECOGNITION_RESUME_FAILED:
|
||||
case ERROR_CODE_UNEXPECTED_PREEMPTION:
|
||||
mErrorCode = errorCode;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid ErrorCode: " + errorCode);
|
||||
}
|
||||
mErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
@@ -110,13 +120,14 @@ public final class SoundTriggerFailure implements Parcelable {
|
||||
@FailureSuggestedAction.FailureSuggestedActionDef
|
||||
public int getSuggestedAction() {
|
||||
switch (mErrorCode) {
|
||||
case ERROR_CODE_UNKNOWN:
|
||||
case ERROR_CODE_MODULE_DIED:
|
||||
case ERROR_CODE_UNEXPECTED_PREEMPTION:
|
||||
return FailureSuggestedAction.RECREATE_DETECTOR;
|
||||
case ERROR_CODE_RECOGNITION_RESUME_FAILED:
|
||||
return FailureSuggestedAction.RESTART_RECOGNITION;
|
||||
default:
|
||||
return FailureSuggestedAction.NONE;
|
||||
throw new AssertionError("Unexpected error code");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -390,12 +390,6 @@ public class VisualQueryDetector {
|
||||
() -> mCallback.onVisualQueryDetectionServiceRestarted()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int status) throws RemoteException {
|
||||
Slog.v(TAG, "Initialization Error: (" + status + ")");
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHotwordDetectionServiceFailure(
|
||||
HotwordDetectionServiceFailure hotwordDetectionServiceFailure)
|
||||
@@ -419,6 +413,11 @@ public class VisualQueryDetector {
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSoundTriggerFailure(SoundTriggerFailure soundTriggerFailure) {
|
||||
Slog.wtf(TAG, "Unexpected STFailure in VisualQueryDetector" + soundTriggerFailure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnknownFailure(String errorMessage) throws RemoteException {
|
||||
Slog.v(TAG, "onUnknownFailure: " + errorMessage);
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.hardware.soundtrigger.SoundTrigger;
|
||||
import android.service.voice.HotwordDetectedResult;
|
||||
import android.service.voice.HotwordDetectionServiceFailure;
|
||||
import android.service.voice.HotwordRejectedResult;
|
||||
import android.service.voice.SoundTriggerFailure;
|
||||
import android.service.voice.VisualQueryDetectionServiceFailure;
|
||||
|
||||
/**
|
||||
@@ -56,13 +57,6 @@ oneway interface IHotwordRecognitionStatusCallback {
|
||||
*/
|
||||
void onRejected(in HotwordRejectedResult result);
|
||||
|
||||
/**
|
||||
* Called when the detection fails due to an error.
|
||||
*
|
||||
* @param status The error code that was seen.
|
||||
*/
|
||||
void onError(int status);
|
||||
|
||||
/**
|
||||
* Called when the detection fails due to an error occurs in the
|
||||
* {@link HotwordDetectionService}.
|
||||
@@ -83,6 +77,15 @@ oneway interface IHotwordRecognitionStatusCallback {
|
||||
void onVisualQueryDetectionServiceFailure(
|
||||
in VisualQueryDetectionServiceFailure visualQueryDetectionServiceFailure);
|
||||
|
||||
/**
|
||||
* Called when the detection fails due to an error occurs in the
|
||||
* {@link com.android.server.soundtrigger.SoundTriggerService}.
|
||||
*
|
||||
* @param soundTriggerFailure It provides the error code, error message and
|
||||
* suggested action.
|
||||
*/
|
||||
void onSoundTriggerFailure(in SoundTriggerFailure soundTriggerFailure);
|
||||
|
||||
/**
|
||||
* Called when the detection fails due to an unknown error occurs.
|
||||
*
|
||||
|
||||
@@ -387,15 +387,6 @@ public final class SoundTriggerDetector {
|
||||
Slog.e(TAG, "Ignoring onKeyphraseDetected() called for " + event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public void onError(int status) {
|
||||
Slog.d(TAG, "onError()" + status);
|
||||
mHandler.sendEmptyMessage(MSG_DETECTION_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@@ -413,6 +404,42 @@ public final class SoundTriggerDetector {
|
||||
Slog.d(TAG, "onRecognitionResumed()");
|
||||
mHandler.sendEmptyMessage(MSG_DETECTION_RESUME);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public void onPreempted() {
|
||||
Slog.d(TAG, "onPreempted()");
|
||||
mHandler.sendEmptyMessage(MSG_DETECTION_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public void onModuleDied() {
|
||||
Slog.d(TAG, "onModuleDied()");
|
||||
mHandler.sendEmptyMessage(MSG_DETECTION_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public void onResumeFailed(int status) {
|
||||
Slog.d(TAG, "onResumeFailed()" + status);
|
||||
mHandler.sendEmptyMessage(MSG_DETECTION_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public void onPauseFailed(int status) {
|
||||
Slog.d(TAG, "onPauseFailed()" + status);
|
||||
mHandler.sendEmptyMessage(MSG_DETECTION_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private class MyHandler extends Handler {
|
||||
|
||||
@@ -315,12 +315,13 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig,
|
||||
int keyphraseId, boolean runInBatterySaverMode) {
|
||||
synchronized (mLock) {
|
||||
// TODO Remove previous callback handling
|
||||
IRecognitionStatusCallback oldCallback = modelData.getCallback();
|
||||
if (oldCallback != null && oldCallback.asBinder() != callback.asBinder()) {
|
||||
Slog.w(TAG, "Canceling previous recognition for model id: "
|
||||
+ modelData.getModelId());
|
||||
try {
|
||||
oldCallback.onError(STATUS_ERROR);
|
||||
oldCallback.onPreempted();
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "RemoteException in onDetectionStopped", e);
|
||||
}
|
||||
@@ -759,15 +760,12 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
onRecognitionAbortLocked(event);
|
||||
break;
|
||||
case SoundTrigger.RECOGNITION_STATUS_FAILURE:
|
||||
// Fire failures to all listeners since it's not tied to a keyphrase.
|
||||
onRecognitionFailureLocked();
|
||||
break;
|
||||
case SoundTrigger.RECOGNITION_STATUS_SUCCESS:
|
||||
case SoundTrigger.RECOGNITION_STATUS_GET_STATE_RESPONSE:
|
||||
if (isKeyphraseRecognitionEvent(event)) {
|
||||
onKeyphraseRecognitionSuccessLocked((KeyphraseRecognitionEvent) event);
|
||||
onKeyphraseRecognitionLocked((KeyphraseRecognitionEvent) event);
|
||||
} else {
|
||||
onGenericRecognitionSuccessLocked((GenericRecognitionEvent) event);
|
||||
onGenericRecognitionLocked((GenericRecognitionEvent) event);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -778,7 +776,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
return event instanceof KeyphraseRecognitionEvent;
|
||||
}
|
||||
|
||||
private void onGenericRecognitionSuccessLocked(GenericRecognitionEvent event) {
|
||||
private void onGenericRecognitionLocked(GenericRecognitionEvent event) {
|
||||
MetricsLogger.count(mContext, "sth_generic_recognition_event", 1);
|
||||
if (event.status != SoundTrigger.RECOGNITION_STATUS_SUCCESS
|
||||
&& event.status != SoundTrigger.RECOGNITION_STATUS_GET_STATE_RESPONSE) {
|
||||
@@ -901,17 +899,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
}
|
||||
}
|
||||
|
||||
private void onRecognitionFailureLocked() {
|
||||
Slog.w(TAG, "Recognition failure");
|
||||
MetricsLogger.count(mContext, "sth_recognition_failure_event", 1);
|
||||
try {
|
||||
sendErrorCallbacksToAllLocked(STATUS_ERROR);
|
||||
} finally {
|
||||
internalClearModelStateLocked();
|
||||
internalClearGlobalStateLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private int getKeyphraseIdFromEvent(KeyphraseRecognitionEvent event) {
|
||||
if (event == null) {
|
||||
Slog.w(TAG, "Null RecognitionEvent received.");
|
||||
@@ -927,7 +914,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
return keyphraseExtras[0].id;
|
||||
}
|
||||
|
||||
private void onKeyphraseRecognitionSuccessLocked(KeyphraseRecognitionEvent event) {
|
||||
private void onKeyphraseRecognitionLocked(KeyphraseRecognitionEvent event) {
|
||||
Slog.i(TAG, "Recognition success");
|
||||
MetricsLogger.count(mContext, "sth_keyphrase_recognition_event", 1);
|
||||
int keyphraseId = getKeyphraseIdFromEvent(event);
|
||||
@@ -1001,7 +988,17 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
private void onServiceDiedLocked() {
|
||||
try {
|
||||
MetricsLogger.count(mContext, "sth_service_died", 1);
|
||||
sendErrorCallbacksToAllLocked(SoundTrigger.STATUS_DEAD_OBJECT);
|
||||
for (ModelData modelData : mModelDataMap.values()) {
|
||||
IRecognitionStatusCallback callback = modelData.getCallback();
|
||||
if (callback != null) {
|
||||
try {
|
||||
callback.onModuleDied();
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "RemoteException send moduleDied for model handle " +
|
||||
modelData.getHandle(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
internalClearModelStateLocked();
|
||||
internalClearGlobalStateLocked();
|
||||
@@ -1111,21 +1108,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
}
|
||||
}
|
||||
|
||||
// Sends an error callback to all models with a valid registered callback.
|
||||
private void sendErrorCallbacksToAllLocked(int errorCode) {
|
||||
for (ModelData modelData : mModelDataMap.values()) {
|
||||
IRecognitionStatusCallback callback = modelData.getCallback();
|
||||
if (callback != null) {
|
||||
try {
|
||||
callback.onError(errorCode);
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "RemoteException sendErrorCallbacksToAllLocked for model handle " +
|
||||
modelData.getHandle(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops and unloads all models. This is intended as a clean-up call with the expectation that
|
||||
* this instance is not used after.
|
||||
@@ -1342,11 +1324,11 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
// Notify of error if needed.
|
||||
if (notifyClientOnError) {
|
||||
try {
|
||||
callback.onError(status);
|
||||
callback.onResumeFailed(status);
|
||||
} catch (DeadObjectException e) {
|
||||
forceStopAndUnloadModelLocked(modelData, e);
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "RemoteException in onError", e);
|
||||
Slog.w(TAG, "RemoteException in onResumeFailed", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1382,15 +1364,15 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
status = mModule.stopRecognition(modelData.getHandle());
|
||||
|
||||
if (status != SoundTrigger.STATUS_OK) {
|
||||
Slog.w(TAG, "stopRecognition call failed with " + status);
|
||||
Slog.e(TAG, "stopRecognition call failed with " + status);
|
||||
MetricsLogger.count(mContext, "sth_stop_recognition_error", 1);
|
||||
if (notify) {
|
||||
try {
|
||||
callback.onError(status);
|
||||
callback.onPauseFailed(status);
|
||||
} catch (DeadObjectException e) {
|
||||
forceStopAndUnloadModelLocked(modelData, e);
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "RemoteException in onError", e);
|
||||
Slog.w(TAG, "RemoteException in onPauseFailed", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -25,6 +25,7 @@ import static android.content.pm.PackageManager.GET_META_DATA;
|
||||
import static android.content.pm.PackageManager.GET_SERVICES;
|
||||
import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
|
||||
import static android.hardware.soundtrigger.SoundTrigger.STATUS_BAD_VALUE;
|
||||
import static android.hardware.soundtrigger.SoundTrigger.STATUS_DEAD_OBJECT;
|
||||
import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
|
||||
import static android.hardware.soundtrigger.SoundTrigger.STATUS_OK;
|
||||
import static android.provider.Settings.Global.MAX_SOUND_TRIGGER_DETECTION_SERVICE_OPS_PER_DAY;
|
||||
@@ -1388,8 +1389,7 @@ public class SoundTriggerService extends SystemService {
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int status) {
|
||||
private void onError(int status) {
|
||||
if (DEBUG) Slog.v(TAG, mPuuid + ": onError: " + status);
|
||||
|
||||
sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
|
||||
@@ -1411,6 +1411,30 @@ public class SoundTriggerService extends SystemService {
|
||||
null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreempted() {
|
||||
if (DEBUG) Slog.v(TAG, mPuuid + ": onPreempted");
|
||||
onError(STATUS_ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModuleDied() {
|
||||
if (DEBUG) Slog.v(TAG, mPuuid + ": onModuleDied");
|
||||
onError(STATUS_DEAD_OBJECT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResumeFailed(int status) {
|
||||
if (DEBUG) Slog.v(TAG, mPuuid + ": onResumeFailed: " + status);
|
||||
onError(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPauseFailed(int status) {
|
||||
if (DEBUG) Slog.v(TAG, mPuuid + ": onPauseFailed: " + status);
|
||||
onError(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecognitionPaused() {
|
||||
Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionPaused");
|
||||
|
||||
@@ -56,6 +56,7 @@ import android.service.voice.HotwordDetector;
|
||||
import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
|
||||
import android.service.voice.ISandboxedDetectionService;
|
||||
import android.service.voice.IVisualQueryDetectionVoiceInteractionCallback;
|
||||
import android.service.voice.SoundTriggerFailure;
|
||||
import android.service.voice.VisualQueryDetectionService;
|
||||
import android.service.voice.VisualQueryDetectionServiceFailure;
|
||||
import android.service.voice.VoiceInteractionManagerInternal.HotwordDetectionServiceIdentity;
|
||||
@@ -576,8 +577,31 @@ final class HotwordDetectionConnection {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int status) throws RemoteException {
|
||||
mExternalCallback.onError(status);
|
||||
public void onPreempted() throws RemoteException {
|
||||
mExternalCallback.onSoundTriggerFailure(new SoundTriggerFailure(
|
||||
SoundTriggerFailure.ERROR_CODE_UNEXPECTED_PREEMPTION,
|
||||
"Unexpected startRecognition on already started ST session"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModuleDied() throws RemoteException {
|
||||
mExternalCallback.onSoundTriggerFailure(new SoundTriggerFailure(
|
||||
SoundTriggerFailure.ERROR_CODE_MODULE_DIED,
|
||||
"STHAL died"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResumeFailed(int status) throws RemoteException {
|
||||
mExternalCallback.onSoundTriggerFailure(new SoundTriggerFailure(
|
||||
SoundTriggerFailure.ERROR_CODE_RECOGNITION_RESUME_FAILED,
|
||||
"STService recognition resume failed with: " + status));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPauseFailed(int status) throws RemoteException {
|
||||
mExternalCallback.onSoundTriggerFailure(new SoundTriggerFailure(
|
||||
SoundTriggerFailure.ERROR_CODE_RECOGNITION_RESUME_FAILED,
|
||||
"STService recognition pause failed with: " + status));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user