/* * Copyright (C) 2018 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.media.soundtrigger; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import android.annotation.CallSuper; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.app.Service; import android.content.Context; import android.content.Intent; import android.hardware.soundtrigger.SoundTrigger; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.ParcelUuid; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; import com.android.internal.annotations.GuardedBy; import java.util.UUID; /** * A service that allows interaction with the actual sound trigger detection on the system. * *

Sound trigger detection refers to detectors that match generic sound patterns that are * not voice-based. The voice-based recognition models should utilize the {@link * android.service.voice.VoiceInteractionService} instead. Access to this class needs to be * protected by the {@value android.Manifest.permission.BIND_SOUND_TRIGGER_DETECTION_SERVICE} * permission granted only to the system. * *

This service has to be explicitly started by an app, the system does not scan for and start * these services. * *

If an operation ({@link #onGenericRecognitionEvent}, {@link #onError}, * {@link #onRecognitionPaused}, {@link #onRecognitionResumed}) is triggered the service is * considered as running in the foreground. Once the operation is processed the service should call * {@link #operationFinished(UUID, int)}. If this does not happen in * {@link SoundTriggerManager#getDetectionServiceOperationsTimeout()} milliseconds * {@link #onStopOperation(UUID, Bundle, int)} is called and the service is unbound. * *

The total amount of operations per day might be limited. * * @hide */ @SystemApi public abstract class SoundTriggerDetectionService extends Service { private static final String LOG_TAG = SoundTriggerDetectionService.class.getSimpleName(); private static final boolean DEBUG = false; private final Object mLock = new Object(); /** * Client indexed by model uuid. This is needed for the {@link #operationFinished(UUID, int)} * callbacks. */ @GuardedBy("mLock") private final ArrayMap mClients = new ArrayMap<>(); private Handler mHandler; /** * @hide */ @Override protected final void attachBaseContext(Context base) { super.attachBaseContext(base); mHandler = new Handler(base.getMainLooper()); } private void setClient(@NonNull UUID uuid, @Nullable Bundle params, @NonNull ISoundTriggerDetectionServiceClient client) { if (DEBUG) Log.i(LOG_TAG, uuid + ": handle setClient"); synchronized (mLock) { mClients.put(uuid, client); } onConnected(uuid, params); } private void removeClient(@NonNull UUID uuid, @Nullable Bundle params) { if (DEBUG) Log.i(LOG_TAG, uuid + ": handle removeClient"); synchronized (mLock) { mClients.remove(uuid); } onDisconnected(uuid, params); } /** * The system has connected to this service for the recognition registered for the model * {@code uuid}. * *

This is called before any operations are delivered. * * @param uuid The {@code uuid} of the model the recognitions is registered for * @param params The {@code params} passed when the recognition was started */ @MainThread public void onConnected(@NonNull UUID uuid, @Nullable Bundle params) { /* do nothing */ } /** * The system has disconnected from this service for the recognition registered for the model * {@code uuid}. * *

Once this is called {@link #operationFinished} cannot be called anymore for * {@code uuid}. * *

{@link #onConnected(UUID, Bundle)} is called before any further operations are delivered. * * @param uuid The {@code uuid} of the model the recognitions is registered for * @param params The {@code params} passed when the recognition was started */ @MainThread public void onDisconnected(@NonNull UUID uuid, @Nullable Bundle params) { /* do nothing */ } /** * A new generic sound trigger event has been detected. * * @param uuid The {@code uuid} of the model the recognition is registered for * @param params The {@code params} passed when the recognition was started * @param opId The id of this operation. Once the operation is done, this service needs to call * {@link #operationFinished(UUID, int)} * @param event The event that has been detected */ @MainThread public void onGenericRecognitionEvent(@NonNull UUID uuid, @Nullable Bundle params, int opId, @NonNull SoundTrigger.RecognitionEvent event) { operationFinished(uuid, opId); } /** * A error has been detected. * * @param uuid The {@code uuid} of the model the recognition is registered for * @param params The {@code params} passed when the recognition was started * @param opId The id of this operation. Once the operation is done, this service needs to call * {@link #operationFinished(UUID, int)} * @param status The error code detected */ @MainThread public void onError(@NonNull UUID uuid, @Nullable Bundle params, int opId, int status) { operationFinished(uuid, opId); } /** * An operation took too long and should be stopped. * * @param uuid The {@code uuid} of the model the recognition is registered for * @param params The {@code params} passed when the recognition was started * @param opId The id of the operation that took too long */ @MainThread public abstract void onStopOperation(@NonNull UUID uuid, @Nullable Bundle params, int opId); /** * Tell that the system that an operation has been fully processed. * * @param uuid The {@code uuid} of the model the recognition is registered for * @param opId The id of the operation that is processed */ public final void operationFinished(@Nullable UUID uuid, int opId) { try { ISoundTriggerDetectionServiceClient client; synchronized (mLock) { client = mClients.get(uuid); if (client == null) { Log.w(LOG_TAG, "operationFinished called, but no client for " + uuid + ". Was this called after onDisconnected?"); return; } } client.onOpFinished(opId); } catch (RemoteException e) { Log.e(LOG_TAG, "operationFinished, remote exception for client " + uuid, e); } } /** * @hide */ @Override public final IBinder onBind(Intent intent) { return new ISoundTriggerDetectionService.Stub() { private final Object mBinderLock = new Object(); /** Cached params bundles indexed by the model uuid */ @GuardedBy("mBinderLock") public final ArrayMap mParams = new ArrayMap<>(); @Override public void setClient(ParcelUuid puuid, Bundle params, ISoundTriggerDetectionServiceClient client) { UUID uuid = puuid.getUuid(); synchronized (mBinderLock) { mParams.put(uuid, params); } if (DEBUG) Log.i(LOG_TAG, uuid + ": setClient(" + params + ")"); mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::setClient, SoundTriggerDetectionService.this, uuid, params, client)); } @Override public void removeClient(ParcelUuid puuid) { UUID uuid = puuid.getUuid(); Bundle params; synchronized (mBinderLock) { params = mParams.remove(uuid); } if (DEBUG) Log.i(LOG_TAG, uuid + ": removeClient"); mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::removeClient, SoundTriggerDetectionService.this, uuid, params)); } @Override public void onGenericRecognitionEvent(ParcelUuid puuid, int opId, SoundTrigger.GenericRecognitionEvent event) { UUID uuid = puuid.getUuid(); Bundle params; synchronized (mBinderLock) { params = mParams.get(uuid); } if (DEBUG) Log.i(LOG_TAG, uuid + "(" + opId + "): onGenericRecognitionEvent"); mHandler.sendMessage( obtainMessage(SoundTriggerDetectionService::onGenericRecognitionEvent, SoundTriggerDetectionService.this, uuid, params, opId, event)); } @Override public void onError(ParcelUuid puuid, int opId, int status) { UUID uuid = puuid.getUuid(); Bundle params; synchronized (mBinderLock) { params = mParams.get(uuid); } if (DEBUG) Log.i(LOG_TAG, uuid + "(" + opId + "): onError(" + status + ")"); mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::onError, SoundTriggerDetectionService.this, uuid, params, opId, status)); } @Override public void onStopOperation(ParcelUuid puuid, int opId) { UUID uuid = puuid.getUuid(); Bundle params; synchronized (mBinderLock) { params = mParams.get(uuid); } if (DEBUG) Log.i(LOG_TAG, uuid + "(" + opId + "): onStopOperation"); mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::onStopOperation, SoundTriggerDetectionService.this, uuid, params, opId)); } }; } @CallSuper @Override public boolean onUnbind(Intent intent) { mClients.clear(); return false; } }