content: /*
   * 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;
      }
  }
  