Merge changes from topic "presubmit-am-08bbcb587d4e47b6afcc900c23899344" into sc-v2-dev am: 0f608efde9
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17184230 Change-Id: I35d157de8376b43f4359cd7243d12973704bf893
This commit is contained in:
@@ -44,14 +44,17 @@ abstract class AbstractHotwordDetector implements HotwordDetector {
|
||||
private final IVoiceInteractionManagerService mManagerService;
|
||||
private final Handler mHandler;
|
||||
private final HotwordDetector.Callback mCallback;
|
||||
private final int mDetectorType;
|
||||
|
||||
AbstractHotwordDetector(
|
||||
IVoiceInteractionManagerService managerService,
|
||||
HotwordDetector.Callback callback) {
|
||||
HotwordDetector.Callback callback,
|
||||
int detectorType) {
|
||||
mManagerService = managerService;
|
||||
// TODO: this needs to be supplied from above
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
mCallback = callback;
|
||||
mDetectorType = detectorType;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,19 +107,20 @@ abstract class AbstractHotwordDetector implements HotwordDetector {
|
||||
Slog.d(TAG, "updateState()");
|
||||
}
|
||||
synchronized (mLock) {
|
||||
updateStateLocked(options, sharedMemory, null /* callback */);
|
||||
updateStateLocked(options, sharedMemory, null /* callback */, mDetectorType);
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateStateLocked(@Nullable PersistableBundle options,
|
||||
@Nullable SharedMemory sharedMemory, IHotwordRecognitionStatusCallback callback) {
|
||||
@Nullable SharedMemory sharedMemory, IHotwordRecognitionStatusCallback callback,
|
||||
int detectorType) {
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "updateStateLocked()");
|
||||
}
|
||||
Identity identity = new Identity();
|
||||
identity.packageName = ActivityThread.currentOpPackageName();
|
||||
try {
|
||||
mManagerService.updateState(identity, options, sharedMemory, callback);
|
||||
mManagerService.updateState(identity, options, sharedMemory, callback, detectorType);
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
|
||||
@@ -578,7 +578,9 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
|
||||
IVoiceInteractionManagerService modelManagementService, int targetSdkVersion,
|
||||
boolean supportHotwordDetectionService, @Nullable PersistableBundle options,
|
||||
@Nullable SharedMemory sharedMemory) {
|
||||
super(modelManagementService, callback);
|
||||
super(modelManagementService, callback,
|
||||
supportHotwordDetectionService ? DETECTOR_TYPE_TRUSTED_HOTWORD_DSP
|
||||
: DETECTOR_TYPE_NORMAL);
|
||||
|
||||
mHandler = new MyHandler();
|
||||
mText = text;
|
||||
@@ -590,7 +592,8 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
|
||||
mTargetSdkVersion = targetSdkVersion;
|
||||
mSupportHotwordDetectionService = supportHotwordDetectionService;
|
||||
if (mSupportHotwordDetectionService) {
|
||||
updateStateLocked(options, sharedMemory, mInternalCallback);
|
||||
updateStateLocked(options, sharedMemory, mInternalCallback,
|
||||
DETECTOR_TYPE_TRUSTED_HOTWORD_DSP);
|
||||
}
|
||||
try {
|
||||
Identity identity = new Identity();
|
||||
|
||||
@@ -36,6 +36,27 @@ import android.os.SharedMemory;
|
||||
@SystemApi
|
||||
public interface HotwordDetector {
|
||||
|
||||
/**
|
||||
* Indicates that it is a non-trusted hotword detector.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
int DETECTOR_TYPE_NORMAL = 0;
|
||||
|
||||
/**
|
||||
* Indicates that it is a DSP trusted hotword detector.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
int DETECTOR_TYPE_TRUSTED_HOTWORD_DSP = 1;
|
||||
|
||||
/**
|
||||
* Indicates that it is a software trusted hotword detector.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
int DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE = 2;
|
||||
|
||||
/**
|
||||
* Starts hotword recognition.
|
||||
* <p>
|
||||
@@ -97,6 +118,22 @@ public interface HotwordDetector {
|
||||
*/
|
||||
void updateState(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory);
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
static String detectorTypeToString(int detectorType) {
|
||||
switch (detectorType) {
|
||||
case DETECTOR_TYPE_NORMAL:
|
||||
return "normal";
|
||||
case DETECTOR_TYPE_TRUSTED_HOTWORD_DSP:
|
||||
return "trusted_hotword_dsp";
|
||||
case DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE:
|
||||
return "trusted_hotword_software";
|
||||
default:
|
||||
return Integer.toString(detectorType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback to notify of detection events.
|
||||
*/
|
||||
|
||||
@@ -60,14 +60,15 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector {
|
||||
PersistableBundle options,
|
||||
SharedMemory sharedMemory,
|
||||
HotwordDetector.Callback callback) {
|
||||
super(managerService, callback);
|
||||
super(managerService, callback, DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE);
|
||||
|
||||
mManagerService = managerService;
|
||||
mAudioFormat = audioFormat;
|
||||
mCallback = callback;
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
updateStateLocked(options, sharedMemory,
|
||||
new InitializationStateListener(mHandler, mCallback));
|
||||
new InitializationStateListener(mHandler, mCallback),
|
||||
DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE);
|
||||
}
|
||||
|
||||
@RequiresPermission(RECORD_AUDIO)
|
||||
|
||||
@@ -242,12 +242,14 @@ interface IVoiceInteractionManagerService {
|
||||
* {@link HotwordDetectionService}. Use this to provide the hotword models data or other
|
||||
* such data to the trusted process.
|
||||
* @param callback Use this to report {@link HotwordDetectionService} status.
|
||||
* @param detectorType Indicate which detector is used.
|
||||
*/
|
||||
void updateState(
|
||||
in Identity originatorIdentity,
|
||||
in PersistableBundle options,
|
||||
in SharedMemory sharedMemory,
|
||||
in IHotwordRecognitionStatusCallback callback);
|
||||
in IHotwordRecognitionStatusCallback callback,
|
||||
int detectorType);
|
||||
|
||||
/**
|
||||
* Requests to shutdown hotword detection service.
|
||||
|
||||
@@ -20,9 +20,30 @@ import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
|
||||
import static android.Manifest.permission.RECORD_AUDIO;
|
||||
import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_EXTERNAL;
|
||||
import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_MICROPHONE;
|
||||
import static android.service.voice.HotwordDetectionService.INITIALIZATION_STATUS_SUCCESS;
|
||||
import static android.service.voice.HotwordDetectionService.INITIALIZATION_STATUS_UNKNOWN;
|
||||
import static android.service.voice.HotwordDetectionService.KEY_INITIALIZATION_STATUS;
|
||||
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_ERROR;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_SUCCESS;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_UNKNOWN_NO_VALUE;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_UNKNOWN_OVER_MAX_CUSTOM_VALUE;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_UNKNOWN_TIMEOUT;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__AUDIO_SERVICE_DIED;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__SCHEDULE;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_UPDATE_STATE_AFTER_TIMEOUT;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__ON_CONNECTED;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__REQUEST_BIND_SERVICE;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__REQUEST_BIND_SERVICE_FAIL;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__REQUEST_UPDATE_STATE;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_EXCEPTION;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_TIMEOUT;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.ComponentName;
|
||||
@@ -89,12 +110,24 @@ final class HotwordDetectionConnection {
|
||||
static final boolean DEBUG = false;
|
||||
|
||||
// TODO: These constants need to be refined.
|
||||
private static final long VALIDATION_TIMEOUT_MILLIS = 3000;
|
||||
private static final long VALIDATION_TIMEOUT_MILLIS = 4000;
|
||||
private static final long MAX_UPDATE_TIMEOUT_MILLIS = 6000;
|
||||
private static final Duration MAX_UPDATE_TIMEOUT_DURATION =
|
||||
Duration.ofMillis(MAX_UPDATE_TIMEOUT_MILLIS);
|
||||
private static final long RESET_DEBUG_HOTWORD_LOGGING_TIMEOUT_MILLIS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
// Hotword metrics
|
||||
private static final int METRICS_INIT_UNKNOWN_TIMEOUT =
|
||||
HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_UNKNOWN_TIMEOUT;
|
||||
private static final int METRICS_INIT_UNKNOWN_NO_VALUE =
|
||||
HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_UNKNOWN_NO_VALUE;
|
||||
private static final int METRICS_INIT_UNKNOWN_OVER_MAX_CUSTOM_VALUE =
|
||||
HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_UNKNOWN_OVER_MAX_CUSTOM_VALUE;
|
||||
private static final int METRICS_INIT_CALLBACK_STATE_ERROR =
|
||||
HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_ERROR;
|
||||
private static final int METRICS_INIT_CALLBACK_STATE_SUCCESS =
|
||||
HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_SUCCESS;
|
||||
|
||||
private final Executor mAudioCopyExecutor = Executors.newCachedThreadPool();
|
||||
// TODO: This may need to be a Handler(looper)
|
||||
private final ScheduledExecutorService mScheduledExecutorService =
|
||||
@@ -103,6 +136,7 @@ final class HotwordDetectionConnection {
|
||||
private final IBinder.DeathRecipient mAudioServerDeathRecipient = this::audioServerDied;
|
||||
private final @NonNull ServiceConnectionFactory mServiceConnectionFactory;
|
||||
private final IHotwordRecognitionStatusCallback mCallback;
|
||||
private final int mDetectorType;
|
||||
|
||||
final Object mLock;
|
||||
final int mVoiceInteractionServiceUid;
|
||||
@@ -114,6 +148,7 @@ final class HotwordDetectionConnection {
|
||||
private Instant mLastRestartInstant;
|
||||
|
||||
private ScheduledFuture<?> mCancellationTaskFuture;
|
||||
private ScheduledFuture<?> mCancellationKeyPhraseDetectionFuture;
|
||||
private ScheduledFuture<?> mDebugHotwordLoggingTimeoutFuture = null;
|
||||
|
||||
/** Identity used for attributing app ops when delivering data to the Interactor. */
|
||||
@@ -134,7 +169,7 @@ final class HotwordDetectionConnection {
|
||||
Identity voiceInteractorIdentity, ComponentName serviceName, int userId,
|
||||
boolean bindInstantServiceAllowed, @Nullable PersistableBundle options,
|
||||
@Nullable SharedMemory sharedMemory,
|
||||
@NonNull IHotwordRecognitionStatusCallback callback) {
|
||||
@NonNull IHotwordRecognitionStatusCallback callback, int detectorType) {
|
||||
if (callback == null) {
|
||||
Slog.w(TAG, "Callback is null while creating connection");
|
||||
throw new IllegalArgumentException("Callback is null while creating connection");
|
||||
@@ -146,6 +181,7 @@ final class HotwordDetectionConnection {
|
||||
mDetectionComponentName = serviceName;
|
||||
mUser = userId;
|
||||
mCallback = callback;
|
||||
mDetectorType = detectorType;
|
||||
final Intent intent = new Intent(HotwordDetectionService.SERVICE_INTERFACE);
|
||||
intent.setComponent(mDetectionComponentName);
|
||||
initAudioFlingerLocked();
|
||||
@@ -163,6 +199,8 @@ final class HotwordDetectionConnection {
|
||||
Slog.v(TAG, "Time to restart the process, TTL has passed");
|
||||
synchronized (mLock) {
|
||||
restartProcessLocked();
|
||||
HotwordMetricsLogger.writeServiceRestartEvent(mDetectorType,
|
||||
HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__SCHEDULE);
|
||||
}
|
||||
}, 30, 30, TimeUnit.MINUTES);
|
||||
}
|
||||
@@ -195,6 +233,8 @@ final class HotwordDetectionConnection {
|
||||
// We restart the process instead of simply sending over the new binder, to avoid race
|
||||
// conditions with audio reading in the service.
|
||||
restartProcessLocked();
|
||||
HotwordMetricsLogger.writeServiceRestartEvent(mDetectorType,
|
||||
HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__AUDIO_SERVICE_DIED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,26 +254,30 @@ final class HotwordDetectionConnection {
|
||||
future.complete(null);
|
||||
if (mUpdateStateAfterStartFinished.getAndSet(true)) {
|
||||
Slog.w(TAG, "call callback after timeout");
|
||||
HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
|
||||
HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_UPDATE_STATE_AFTER_TIMEOUT,
|
||||
mVoiceInteractionServiceUid);
|
||||
return;
|
||||
}
|
||||
int status = bundle != null ? bundle.getInt(
|
||||
KEY_INITIALIZATION_STATUS,
|
||||
INITIALIZATION_STATUS_UNKNOWN)
|
||||
: INITIALIZATION_STATUS_UNKNOWN;
|
||||
// Add the protection to avoid unexpected status
|
||||
if (status > HotwordDetectionService.getMaxCustomInitializationStatus()
|
||||
&& status != INITIALIZATION_STATUS_UNKNOWN) {
|
||||
status = INITIALIZATION_STATUS_UNKNOWN;
|
||||
}
|
||||
Pair<Integer, Integer> statusResultPair = getInitStatusAndMetricsResult(bundle);
|
||||
int status = statusResultPair.first;
|
||||
int initResultMetricsResult = statusResultPair.second;
|
||||
try {
|
||||
mCallback.onStatusReported(status);
|
||||
HotwordMetricsLogger.writeServiceInitResultEvent(mDetectorType,
|
||||
initResultMetricsResult);
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "Failed to report initialization status: " + e);
|
||||
HotwordMetricsLogger.writeServiceInitResultEvent(mDetectorType,
|
||||
METRICS_INIT_CALLBACK_STATE_ERROR);
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
service.updateState(options, sharedMemory, statusCallback);
|
||||
HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
|
||||
HOTWORD_DETECTOR_EVENTS__EVENT__REQUEST_UPDATE_STATE,
|
||||
mVoiceInteractionServiceUid);
|
||||
} catch (RemoteException e) {
|
||||
// TODO: (b/181842909) Report an error to voice interactor
|
||||
Slog.w(TAG, "Failed to updateState for HotwordDetectionService", e);
|
||||
@@ -248,8 +292,12 @@ final class HotwordDetectionConnection {
|
||||
}
|
||||
try {
|
||||
mCallback.onStatusReported(INITIALIZATION_STATUS_UNKNOWN);
|
||||
HotwordMetricsLogger.writeServiceInitResultEvent(mDetectorType,
|
||||
METRICS_INIT_UNKNOWN_TIMEOUT);
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "Failed to report initialization status UNKNOWN", e);
|
||||
HotwordMetricsLogger.writeServiceInitResultEvent(mDetectorType,
|
||||
METRICS_INIT_CALLBACK_STATE_ERROR);
|
||||
}
|
||||
} else if (err != null) {
|
||||
Slog.w(TAG, "Failed to update state: " + err);
|
||||
@@ -259,6 +307,24 @@ final class HotwordDetectionConnection {
|
||||
});
|
||||
}
|
||||
|
||||
private static Pair<Integer, Integer> getInitStatusAndMetricsResult(Bundle bundle) {
|
||||
if (bundle == null) {
|
||||
return new Pair<>(INITIALIZATION_STATUS_UNKNOWN, METRICS_INIT_UNKNOWN_NO_VALUE);
|
||||
}
|
||||
int status = bundle.getInt(KEY_INITIALIZATION_STATUS, INITIALIZATION_STATUS_UNKNOWN);
|
||||
if (status > HotwordDetectionService.getMaxCustomInitializationStatus()) {
|
||||
return new Pair<>(INITIALIZATION_STATUS_UNKNOWN,
|
||||
status == INITIALIZATION_STATUS_UNKNOWN
|
||||
? METRICS_INIT_UNKNOWN_NO_VALUE
|
||||
:METRICS_INIT_UNKNOWN_OVER_MAX_CUSTOM_VALUE);
|
||||
}
|
||||
// TODO: should guard against negative here
|
||||
int metricsResult = status == INITIALIZATION_STATUS_SUCCESS
|
||||
? METRICS_INIT_CALLBACK_STATE_SUCCESS
|
||||
: METRICS_INIT_CALLBACK_STATE_ERROR;
|
||||
return new Pair<>(status, metricsResult);
|
||||
}
|
||||
|
||||
private boolean isBound() {
|
||||
synchronized (mLock) {
|
||||
return mRemoteHotwordDetectionService.isBound();
|
||||
@@ -473,12 +539,31 @@ final class HotwordDetectionConnection {
|
||||
Slog.d(TAG, "onDetected");
|
||||
}
|
||||
synchronized (mLock) {
|
||||
// TODO: If the dsp trigger comes in after the timeout, we will log both events.
|
||||
// Because we don't enforce the timeout yet. We should add some synchronizations
|
||||
// within the runnable to prevent the race condition to log both events.
|
||||
if (mCancellationKeyPhraseDetectionFuture != null) {
|
||||
mCancellationKeyPhraseDetectionFuture.cancel(true);
|
||||
}
|
||||
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
|
||||
mDetectorType,
|
||||
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED);
|
||||
if (!mValidatingDspTrigger) {
|
||||
Slog.i(TAG, "Ignoring #onDetected due to a process restart");
|
||||
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
|
||||
mDetectorType,
|
||||
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_EXCEPTION);
|
||||
return;
|
||||
}
|
||||
mValidatingDspTrigger = false;
|
||||
enforcePermissionsForDataDelivery();
|
||||
try {
|
||||
enforcePermissionsForDataDelivery();
|
||||
} catch (SecurityException e) {
|
||||
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
|
||||
mDetectorType,
|
||||
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_EXCEPTION);
|
||||
throw e;
|
||||
}
|
||||
externalCallback.onKeyphraseDetected(recognitionEvent, result);
|
||||
if (result != null) {
|
||||
Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result)
|
||||
@@ -496,8 +581,17 @@ final class HotwordDetectionConnection {
|
||||
Slog.d(TAG, "onRejected");
|
||||
}
|
||||
synchronized (mLock) {
|
||||
if (mCancellationKeyPhraseDetectionFuture != null) {
|
||||
mCancellationKeyPhraseDetectionFuture.cancel(true);
|
||||
}
|
||||
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
|
||||
mDetectorType,
|
||||
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED);
|
||||
if (!mValidatingDspTrigger) {
|
||||
Slog.i(TAG, "Ignoring #onRejected due to a process restart");
|
||||
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
|
||||
mDetectorType,
|
||||
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_EXCEPTION);
|
||||
return;
|
||||
}
|
||||
mValidatingDspTrigger = false;
|
||||
@@ -512,11 +606,20 @@ final class HotwordDetectionConnection {
|
||||
synchronized (mLock) {
|
||||
mValidatingDspTrigger = true;
|
||||
mRemoteHotwordDetectionService.run(
|
||||
service -> service.detectFromDspSource(
|
||||
recognitionEvent,
|
||||
recognitionEvent.getCaptureFormat(),
|
||||
VALIDATION_TIMEOUT_MILLIS,
|
||||
internalCallback));
|
||||
service -> {
|
||||
// TODO: avoid allocate every time
|
||||
mCancellationKeyPhraseDetectionFuture = mScheduledExecutorService.schedule(
|
||||
() -> HotwordMetricsLogger
|
||||
.writeKeyphraseTriggerEvent(mDetectorType,
|
||||
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_TIMEOUT),
|
||||
VALIDATION_TIMEOUT_MILLIS,
|
||||
TimeUnit.MILLISECONDS);
|
||||
service.detectFromDspSource(
|
||||
recognitionEvent,
|
||||
recognitionEvent.getCaptureFormat(),
|
||||
VALIDATION_TIMEOUT_MILLIS,
|
||||
internalCallback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,10 +720,16 @@ final class HotwordDetectionConnection {
|
||||
}
|
||||
final boolean useHotwordDetectionService = mHotwordDetectionConnection != null;
|
||||
if (useHotwordDetectionService) {
|
||||
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
|
||||
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP,
|
||||
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER);
|
||||
mRecognitionEvent = recognitionEvent;
|
||||
mHotwordDetectionConnection.detectFromDspSource(
|
||||
recognitionEvent, mExternalCallback);
|
||||
} else {
|
||||
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
|
||||
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR,
|
||||
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER);
|
||||
mExternalCallback.onKeyphraseDetected(recognitionEvent, null);
|
||||
}
|
||||
}
|
||||
@@ -783,6 +892,7 @@ final class HotwordDetectionConnection {
|
||||
|
||||
private boolean mRespectServiceConnectionStatusChanged = true;
|
||||
private boolean mIsBound = false;
|
||||
private boolean mIsLoggedFirstConnect = false;
|
||||
|
||||
ServiceConnection(@NonNull Context context,
|
||||
@NonNull Intent intent, int bindingFlags, int userId,
|
||||
@@ -806,6 +916,12 @@ final class HotwordDetectionConnection {
|
||||
return;
|
||||
}
|
||||
mIsBound = connected;
|
||||
if (connected && !mIsLoggedFirstConnect) {
|
||||
mIsLoggedFirstConnect = true;
|
||||
HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
|
||||
HOTWORD_DETECTOR_EVENTS__EVENT__ON_CONNECTED,
|
||||
mVoiceInteractionServiceUid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -836,13 +952,25 @@ final class HotwordDetectionConnection {
|
||||
protected boolean bindService(
|
||||
@NonNull android.content.ServiceConnection serviceConnection) {
|
||||
try {
|
||||
return mContext.bindIsolatedService(
|
||||
HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
|
||||
HOTWORD_DETECTOR_EVENTS__EVENT__REQUEST_BIND_SERVICE,
|
||||
mVoiceInteractionServiceUid);
|
||||
boolean bindResult = mContext.bindIsolatedService(
|
||||
mIntent,
|
||||
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE | mBindingFlags,
|
||||
"hotword_detector_" + mInstanceNumber,
|
||||
mExecutor,
|
||||
serviceConnection);
|
||||
if (!bindResult) {
|
||||
HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
|
||||
HOTWORD_DETECTOR_EVENTS__EVENT__REQUEST_BIND_SERVICE_FAIL,
|
||||
mVoiceInteractionServiceUid);
|
||||
}
|
||||
return bindResult;
|
||||
} catch (IllegalArgumentException e) {
|
||||
HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
|
||||
HOTWORD_DETECTOR_EVENTS__EVENT__REQUEST_BIND_SERVICE_FAIL,
|
||||
mVoiceInteractionServiceUid);
|
||||
Slog.wtf(TAG, "Can't bind to the hotword detection service!", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,24 @@
|
||||
|
||||
package com.android.server.voiceinteraction;
|
||||
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__DETECTOR_TYPE__NORMAL_DETECTOR;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__DETECTOR_TYPE__TRUSTED_DETECTOR_SOFTWARE;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_RESTARTED__DETECTOR_TYPE__NORMAL_DETECTOR;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_RESTARTED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_RESTARTED__DETECTOR_TYPE__TRUSTED_DETECTOR_SOFTWARE;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_CREATE_REQUESTED__DETECTOR_TYPE__NORMAL_DETECTOR;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_CREATE_REQUESTED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_CREATE_REQUESTED__DETECTOR_TYPE__TRUSTED_DETECTOR_SOFTWARE;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__DETECTOR_TYPE__NORMAL_DETECTOR;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__DETECTOR_TYPE__TRUSTED_DETECTOR_SOFTWARE;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
|
||||
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_SOFTWARE;
|
||||
|
||||
import android.service.voice.HotwordDetector;
|
||||
|
||||
import com.android.internal.util.FrameworkStatsLog;
|
||||
|
||||
/**
|
||||
@@ -23,6 +41,13 @@ import com.android.internal.util.FrameworkStatsLog;
|
||||
*/
|
||||
public final class HotwordMetricsLogger {
|
||||
|
||||
private static final int METRICS_INIT_DETECTOR_SOFTWARE =
|
||||
HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__DETECTOR_TYPE__TRUSTED_DETECTOR_SOFTWARE;
|
||||
private static final int METRICS_INIT_DETECTOR_DSP =
|
||||
HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
|
||||
private static final int METRICS_INIT_NORMAL_DETECTOR =
|
||||
HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__DETECTOR_TYPE__NORMAL_DETECTOR;
|
||||
|
||||
private HotwordMetricsLogger() {
|
||||
// Class only contains static utility functions, and should not be instantiated
|
||||
}
|
||||
@@ -31,39 +56,99 @@ public final class HotwordMetricsLogger {
|
||||
* Logs information related to create hotword detector.
|
||||
*/
|
||||
public static void writeDetectorCreateEvent(int detectorType, boolean isCreated, int uid) {
|
||||
FrameworkStatsLog.write(FrameworkStatsLog.HOTWORD_DETECTOR_CREATE_REQUESTED, detectorType,
|
||||
isCreated, uid);
|
||||
int metricsDetectorType = getCreateMetricsDetectorType(detectorType);
|
||||
FrameworkStatsLog.write(FrameworkStatsLog.HOTWORD_DETECTOR_CREATE_REQUESTED,
|
||||
metricsDetectorType, isCreated, uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs information related to hotword detection service init result.
|
||||
*/
|
||||
public static void writeServiceInitResultEvent(int detectorType, int result) {
|
||||
int metricsDetectorType = getInitMetricsDetectorType(detectorType);
|
||||
FrameworkStatsLog.write(FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED,
|
||||
detectorType, result);
|
||||
metricsDetectorType, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs information related to hotword detection service restarting.
|
||||
*/
|
||||
public static void writeServiceRestartEvent(int detectorType, int reason) {
|
||||
int metricsDetectorType = getRestartMetricsDetectorType(detectorType);
|
||||
FrameworkStatsLog.write(FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_RESTARTED,
|
||||
detectorType, reason);
|
||||
metricsDetectorType, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs information related to keyphrase trigger.
|
||||
*/
|
||||
public static void writeKeyphraseTriggerEvent(int detectorType, int result) {
|
||||
int metricsDetectorType = getKeyphraseMetricsDetectorType(detectorType);
|
||||
FrameworkStatsLog.write(FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED,
|
||||
detectorType, result);
|
||||
metricsDetectorType, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs information related to hotword detector events.
|
||||
*/
|
||||
public static void writeDetectorEvent(int detectorType, int event, int uid) {
|
||||
int metricsDetectorType = getDetectorMetricsDetectorType(detectorType);
|
||||
FrameworkStatsLog.write(FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS,
|
||||
detectorType, event, uid);
|
||||
metricsDetectorType, event, uid);
|
||||
}
|
||||
|
||||
private static int getCreateMetricsDetectorType(int detectorType) {
|
||||
switch (detectorType) {
|
||||
case HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE:
|
||||
return HOTWORD_DETECTOR_CREATE_REQUESTED__DETECTOR_TYPE__TRUSTED_DETECTOR_SOFTWARE;
|
||||
case HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP:
|
||||
return HOTWORD_DETECTOR_CREATE_REQUESTED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
|
||||
default:
|
||||
return HOTWORD_DETECTOR_CREATE_REQUESTED__DETECTOR_TYPE__NORMAL_DETECTOR;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getRestartMetricsDetectorType(int detectorType) {
|
||||
switch (detectorType) {
|
||||
case HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE:
|
||||
return HOTWORD_DETECTION_SERVICE_RESTARTED__DETECTOR_TYPE__TRUSTED_DETECTOR_SOFTWARE;
|
||||
case HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP:
|
||||
return HOTWORD_DETECTION_SERVICE_RESTARTED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
|
||||
default:
|
||||
return HOTWORD_DETECTION_SERVICE_RESTARTED__DETECTOR_TYPE__NORMAL_DETECTOR;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getInitMetricsDetectorType(int detectorType) {
|
||||
switch (detectorType) {
|
||||
case HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE:
|
||||
return METRICS_INIT_DETECTOR_SOFTWARE;
|
||||
case HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP:
|
||||
return METRICS_INIT_DETECTOR_DSP;
|
||||
default:
|
||||
return METRICS_INIT_NORMAL_DETECTOR;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getKeyphraseMetricsDetectorType(int detectorType) {
|
||||
switch (detectorType) {
|
||||
case HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE:
|
||||
return HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_SOFTWARE;
|
||||
case HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP:
|
||||
return HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
|
||||
default:
|
||||
return HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getDetectorMetricsDetectorType(int detectorType) {
|
||||
switch (detectorType) {
|
||||
case HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE:
|
||||
return HOTWORD_DETECTOR_EVENTS__DETECTOR_TYPE__TRUSTED_DETECTOR_SOFTWARE;
|
||||
case HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP:
|
||||
return HOTWORD_DETECTOR_EVENTS__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
|
||||
default:
|
||||
return HOTWORD_DETECTOR_EVENTS__DETECTOR_TYPE__NORMAL_DETECTOR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1168,7 +1168,8 @@ public class VoiceInteractionManagerService extends SystemService {
|
||||
@NonNull Identity voiceInteractorIdentity,
|
||||
@Nullable PersistableBundle options,
|
||||
@Nullable SharedMemory sharedMemory,
|
||||
IHotwordRecognitionStatusCallback callback) {
|
||||
IHotwordRecognitionStatusCallback callback,
|
||||
int detectorType) {
|
||||
enforceCallingPermission(Manifest.permission.MANAGE_HOTWORD_DETECTION);
|
||||
synchronized (this) {
|
||||
enforceIsCurrentVoiceInteractionService();
|
||||
@@ -1184,7 +1185,7 @@ public class VoiceInteractionManagerService extends SystemService {
|
||||
final long caller = Binder.clearCallingIdentity();
|
||||
try {
|
||||
mImpl.updateStateLocked(
|
||||
voiceInteractorIdentity, options, sharedMemory, callback);
|
||||
voiceInteractorIdentity, options, sharedMemory, callback, detectorType);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(caller);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
import android.os.SharedMemory;
|
||||
import android.os.UserHandle;
|
||||
import android.service.voice.HotwordDetector;
|
||||
import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
|
||||
import android.service.voice.IVoiceInteractionService;
|
||||
import android.service.voice.IVoiceInteractionSession;
|
||||
@@ -102,6 +103,7 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne
|
||||
|
||||
VoiceInteractionSessionConnection mActiveSession;
|
||||
int mDisabledShowContext;
|
||||
int mDetectorType;
|
||||
|
||||
final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
@@ -456,26 +458,36 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne
|
||||
@NonNull Identity voiceInteractorIdentity,
|
||||
@Nullable PersistableBundle options,
|
||||
@Nullable SharedMemory sharedMemory,
|
||||
IHotwordRecognitionStatusCallback callback) {
|
||||
IHotwordRecognitionStatusCallback callback,
|
||||
int detectorType) {
|
||||
Slog.v(TAG, "updateStateLocked");
|
||||
int voiceInteractionServiceUid = mInfo.getServiceInfo().applicationInfo.uid;
|
||||
if (mHotwordDetectionComponentName == null) {
|
||||
Slog.w(TAG, "Hotword detection service name not found");
|
||||
logDetectorCreateEventIfNeeded(callback, detectorType, false,
|
||||
voiceInteractionServiceUid);
|
||||
throw new IllegalStateException("Hotword detection service name not found");
|
||||
}
|
||||
ServiceInfo hotwordDetectionServiceInfo = getServiceInfoLocked(
|
||||
mHotwordDetectionComponentName, mUser);
|
||||
if (hotwordDetectionServiceInfo == null) {
|
||||
Slog.w(TAG, "Hotword detection service info not found");
|
||||
logDetectorCreateEventIfNeeded(callback, detectorType, false,
|
||||
voiceInteractionServiceUid);
|
||||
throw new IllegalStateException("Hotword detection service info not found");
|
||||
}
|
||||
if (!isIsolatedProcessLocked(hotwordDetectionServiceInfo)) {
|
||||
Slog.w(TAG, "Hotword detection service not in isolated process");
|
||||
logDetectorCreateEventIfNeeded(callback, detectorType, false,
|
||||
voiceInteractionServiceUid);
|
||||
throw new IllegalStateException("Hotword detection service not in isolated process");
|
||||
}
|
||||
if (!Manifest.permission.BIND_HOTWORD_DETECTION_SERVICE.equals(
|
||||
hotwordDetectionServiceInfo.permission)) {
|
||||
Slog.w(TAG, "Hotword detection service does not require permission "
|
||||
+ Manifest.permission.BIND_HOTWORD_DETECTION_SERVICE);
|
||||
logDetectorCreateEventIfNeeded(callback, detectorType, false,
|
||||
voiceInteractionServiceUid);
|
||||
throw new SecurityException("Hotword detection service does not require permission "
|
||||
+ Manifest.permission.BIND_HOTWORD_DETECTION_SERVICE);
|
||||
}
|
||||
@@ -484,25 +496,40 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne
|
||||
mInfo.getServiceInfo().packageName) == PackageManager.PERMISSION_GRANTED) {
|
||||
Slog.w(TAG, "Voice interaction service should not hold permission "
|
||||
+ Manifest.permission.BIND_HOTWORD_DETECTION_SERVICE);
|
||||
logDetectorCreateEventIfNeeded(callback, detectorType, false,
|
||||
voiceInteractionServiceUid);
|
||||
throw new SecurityException("Voice interaction service should not hold permission "
|
||||
+ Manifest.permission.BIND_HOTWORD_DETECTION_SERVICE);
|
||||
}
|
||||
|
||||
if (sharedMemory != null && !sharedMemory.setProtect(OsConstants.PROT_READ)) {
|
||||
Slog.w(TAG, "Can't set sharedMemory to be read-only");
|
||||
logDetectorCreateEventIfNeeded(callback, detectorType, false,
|
||||
voiceInteractionServiceUid);
|
||||
throw new IllegalStateException("Can't set sharedMemory to be read-only");
|
||||
}
|
||||
mDetectorType = detectorType;
|
||||
logDetectorCreateEventIfNeeded(callback, detectorType, true,
|
||||
voiceInteractionServiceUid);
|
||||
|
||||
if (mHotwordDetectionConnection == null) {
|
||||
mHotwordDetectionConnection = new HotwordDetectionConnection(mServiceStub, mContext,
|
||||
mInfo.getServiceInfo().applicationInfo.uid, voiceInteractorIdentity,
|
||||
mHotwordDetectionComponentName, mUser, /* bindInstantServiceAllowed= */ false,
|
||||
options, sharedMemory, callback);
|
||||
options, sharedMemory, callback, detectorType);
|
||||
} else {
|
||||
mHotwordDetectionConnection.updateStateLocked(options, sharedMemory);
|
||||
}
|
||||
}
|
||||
|
||||
private void logDetectorCreateEventIfNeeded(IHotwordRecognitionStatusCallback callback,
|
||||
int detectorType, boolean isCreated, int voiceInteractionServiceUid) {
|
||||
if (callback != null) {
|
||||
HotwordMetricsLogger.writeDetectorCreateEvent(detectorType, true,
|
||||
voiceInteractionServiceUid);
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdownHotwordDetectionServiceLocked() {
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "shutdownHotwordDetectionServiceLocked");
|
||||
@@ -667,6 +694,8 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne
|
||||
pw.println(Integer.toHexString(mDisabledShowContext));
|
||||
}
|
||||
pw.print(" mBound="); pw.print(mBound); pw.print(" mService="); pw.println(mService);
|
||||
pw.print(" mDetectorType=");
|
||||
pw.println(HotwordDetector.detectorTypeToString(mDetectorType));
|
||||
if (mHotwordDetectionConnection != null) {
|
||||
pw.println(" Hotword detection connection:");
|
||||
mHotwordDetectionConnection.dump(" ", pw);
|
||||
|
||||
Reference in New Issue
Block a user