| /* |
| * 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. |
| * |
| * <p> 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. |
| * |
| * <p>This service has to be explicitly started by an app, the system does not scan for and start |
| * these services. |
| * |
| * <p>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. |
| * |
| * <p>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<UUID, ISoundTriggerDetectionServiceClient> 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}. |
| * |
| * <p> 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}. |
| * |
| * <p>Once this is called {@link #operationFinished} cannot be called anymore for |
| * {@code uuid}. |
| * |
| * <p> {@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<UUID, Bundle> 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; |
| } |
| } |