content: /*
   * Copyright 2017 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 com.android.server.location;
  
  import android.hardware.contexthub.V1_0.IContexthub;
  import android.hardware.contexthub.V1_0.Result;
  import android.hardware.contexthub.V1_0.TransactionResult;
  import android.hardware.location.ContextHubTransaction;
  import android.hardware.location.IContextHubTransactionCallback;
  import android.hardware.location.NanoAppBinary;
  import android.hardware.location.NanoAppState;
  import android.os.RemoteException;
  import android.util.Log;
  
  import java.util.ArrayDeque;
  import java.util.Collections;
  import java.util.List;
  import java.util.concurrent.ScheduledThreadPoolExecutor;
  import java.util.concurrent.ScheduledFuture;
  import java.util.concurrent.TimeUnit;
  import java.util.concurrent.atomic.AtomicInteger;
  
  /**
   * Manages transactions at the Context Hub Service.
   *
   * This class maintains a queue of transaction requests made to the ContextHubService by clients,
   * and executes them through the Context Hub. At any point in time, either the transaction queue is
   * empty, or there is a pending transaction that is waiting for an asynchronous response from the
   * hub. This class also handles synchronous errors and timeouts of each transaction.
   *
   * @hide
   */
  /* package */ class ContextHubTransactionManager {
      private static final String TAG = "ContextHubTransactionManager";
  
      /*
       * Maximum number of transaction requests that can be pending at a time
       */
      private static final int MAX_PENDING_REQUESTS = 10000;
  
      /*
       * The proxy to talk to the Context Hub
       */
      private final IContexthub mContextHubProxy;
  
      /*
       * The manager for all clients for the service.
       */
      private final ContextHubClientManager mClientManager;
  
      /*
       * The nanoapp state manager for the service
       */
      private final NanoAppStateManager mNanoAppStateManager;
  
      /*
       * A queue containing the current transactions
       */
      private final ArrayDeque<ContextHubServiceTransaction> mTransactionQueue = new ArrayDeque<>();
  
      /*
       * The next available transaction ID
       */
      private final AtomicInteger mNextAvailableId = new AtomicInteger();
  
      /*
       * An executor and the future object for scheduling timeout timers
       */
      private final ScheduledThreadPoolExecutor mTimeoutExecutor = new ScheduledThreadPoolExecutor(1);
      private ScheduledFuture<?> mTimeoutFuture = null;
  
      /* package */ ContextHubTransactionManager(
              IContexthub contextHubProxy, ContextHubClientManager clientManager,
              NanoAppStateManager nanoAppStateManager) {
          mContextHubProxy = contextHubProxy;
          mClientManager = clientManager;
          mNanoAppStateManager = nanoAppStateManager;
      }
  
      /**
       * Creates a transaction for loading a nanoapp.
       *
       * @param contextHubId       the ID of the hub to load the nanoapp to
       * @param nanoAppBinary      the binary of the nanoapp to load
       * @param onCompleteCallback the client on complete callback
       * @return the generated transaction
       */
      /* package */ ContextHubServiceTransaction createLoadTransaction(
              int contextHubId, NanoAppBinary nanoAppBinary,
              IContextHubTransactionCallback onCompleteCallback) {
          return new ContextHubServiceTransaction(
                  mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_LOAD_NANOAPP) {
              @Override
              /* package */ int onTransact() {
                  android.hardware.contexthub.V1_0.NanoAppBinary hidlNanoAppBinary =
                          ContextHubServiceUtil.createHidlNanoAppBinary(nanoAppBinary);
                  try {
                      return mContextHubProxy.loadNanoApp(
                              contextHubId, hidlNanoAppBinary, this.getTransactionId());
                  } catch (RemoteException e) {
                      Log.e(TAG, "RemoteException while trying to load nanoapp with ID 0x" +
                              Long.toHexString(nanoAppBinary.getNanoAppId()), e);
                      return Result.UNKNOWN_FAILURE;
                  }
              }
  
              @Override
              /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
                  if (result == ContextHubTransaction.RESULT_SUCCESS) {
                      // NOTE: The legacy JNI code used to do a query right after a load success
                      // to synchronize the service cache. Instead store the binary that was
                      // requested to load to update the cache later without doing a query.
                      mNanoAppStateManager.addNanoAppInstance(
                              contextHubId, nanoAppBinary.getNanoAppId(),
                              nanoAppBinary.getNanoAppVersion());
                  }
                  try {
                      onCompleteCallback.onTransactionComplete(result);
                      if (result == ContextHubTransaction.RESULT_SUCCESS) {
                          mClientManager.onNanoAppLoaded(contextHubId, nanoAppBinary.getNanoAppId());
                      }
                  } catch (RemoteException e) {
                      Log.e(TAG, "RemoteException while calling client onTransactionComplete", e);
                  }
              }
          };
      }
  
      /**
       * Creates a transaction for unloading a nanoapp.
       *
       * @param contextHubId       the ID of the hub to unload the nanoapp from
       * @param nanoAppId          the ID of the nanoapp to unload
       * @param onCompleteCallback the client on complete callback
       * @return the generated transaction
       */
      /* package */ ContextHubServiceTransaction createUnloadTransaction(
              int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback) {
          return new ContextHubServiceTransaction(
                  mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_UNLOAD_NANOAPP) {
              @Override
              /* package */ int onTransact() {
                  try {
                      return mContextHubProxy.unloadNanoApp(
                              contextHubId, nanoAppId, this.getTransactionId());
                  } catch (RemoteException e) {
                      Log.e(TAG, "RemoteException while trying to unload nanoapp with ID 0x" +
                              Long.toHexString(nanoAppId), e);
                      return Result.UNKNOWN_FAILURE;
                  }
              }
  
              @Override
              /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
                  if (result == ContextHubTransaction.RESULT_SUCCESS) {
                      mNanoAppStateManager.removeNanoAppInstance(contextHubId, nanoAppId);
                  }
                  try {
                      onCompleteCallback.onTransactionComplete(result);
                      if (result == ContextHubTransaction.RESULT_SUCCESS) {
                          mClientManager.onNanoAppUnloaded(contextHubId, nanoAppId);
                      }
                  } catch (RemoteException e) {
                      Log.e(TAG, "RemoteException while calling client onTransactionComplete", e);
                  }
              }
          };
      }
  
      /**
       * Creates a transaction for enabling a nanoapp.
       *
       * @param contextHubId       the ID of the hub to enable the nanoapp on
       * @param nanoAppId          the ID of the nanoapp to enable
       * @param onCompleteCallback the client on complete callback
       * @return the generated transaction
       */
      /* package */ ContextHubServiceTransaction createEnableTransaction(
              int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback) {
          return new ContextHubServiceTransaction(
                  mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_ENABLE_NANOAPP) {
              @Override
              /* package */ int onTransact() {
                  try {
                      return mContextHubProxy.enableNanoApp(
                              contextHubId, nanoAppId, this.getTransactionId());
                  } catch (RemoteException e) {
                      Log.e(TAG, "RemoteException while trying to enable nanoapp with ID 0x" +
                              Long.toHexString(nanoAppId), e);
                      return Result.UNKNOWN_FAILURE;
                  }
              }
  
              @Override
              /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
                  try {
                      onCompleteCallback.onTransactionComplete(result);
                  } catch (RemoteException e) {
                      Log.e(TAG, "RemoteException while calling client onTransactionComplete", e);
                  }
              }
          };
      }
  
      /**
       * Creates a transaction for disabling a nanoapp.
       *
       * @param contextHubId       the ID of the hub to disable the nanoapp on
       * @param nanoAppId          the ID of the nanoapp to disable
       * @param onCompleteCallback the client on complete callback
       * @return the generated transaction
       */
      /* package */ ContextHubServiceTransaction createDisableTransaction(
              int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback) {
          return new ContextHubServiceTransaction(
                  mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_DISABLE_NANOAPP) {
              @Override
              /* package */ int onTransact() {
                  try {
                      return mContextHubProxy.disableNanoApp(
                              contextHubId, nanoAppId, this.getTransactionId());
                  } catch (RemoteException e) {
                      Log.e(TAG, "RemoteException while trying to disable nanoapp with ID 0x" +
                              Long.toHexString(nanoAppId), e);
                      return Result.UNKNOWN_FAILURE;
                  }
              }
  
              @Override
              /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
                  try {
                      onCompleteCallback.onTransactionComplete(result);
                  } catch (RemoteException e) {
                      Log.e(TAG, "RemoteException while calling client onTransactionComplete", e);
                  }
              }
          };
      }
  
      /**
       * Creates a transaction for querying for a list of nanoapps.
       *
       * @param contextHubId       the ID of the hub to query
       * @param onCompleteCallback the client on complete callback
       * @return the generated transaction
       */
      /* package */ ContextHubServiceTransaction createQueryTransaction(
              int contextHubId, IContextHubTransactionCallback onCompleteCallback) {
          return new ContextHubServiceTransaction(
                  mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_QUERY_NANOAPPS) {
              @Override
              /* package */ int onTransact() {
                  try {
                      return mContextHubProxy.queryApps(contextHubId);
                  } catch (RemoteException e) {
                      Log.e(TAG, "RemoteException while trying to query for nanoapps", e);
                      return Result.UNKNOWN_FAILURE;
                  }
              }
  
              @Override
              /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
                  onQueryResponse(result, Collections.emptyList());
              }
  
              @Override
              /* package */ void onQueryResponse(
                      @ContextHubTransaction.Result int result, List<NanoAppState> nanoAppStateList) {
                  try {
                      onCompleteCallback.onQueryResponse(result, nanoAppStateList);
                  } catch (RemoteException e) {
                      Log.e(TAG, "RemoteException while calling client onQueryComplete", e);
                  }
              }
          };
      }
  
      /**
       * Adds a new transaction to the queue.
       *
       * If there was no pending transaction at the time, the transaction that was added will be
       * started in this method. If there were too many transactions in the queue, an exception will
       * be thrown.
       *
       * @param transaction the transaction to add
       * @throws IllegalStateException if the queue is full
       */
      /* package */
      synchronized void addTransaction(
              ContextHubServiceTransaction transaction) throws IllegalStateException {
          if (mTransactionQueue.size() == MAX_PENDING_REQUESTS) {
              throw new IllegalStateException("Transaction queue is full (capacity = "
                      + MAX_PENDING_REQUESTS + ")");
          }
          mTransactionQueue.add(transaction);
  
          if (mTransactionQueue.size() == 1) {
              startNextTransaction();
          }
      }
  
      /**
       * Handles a transaction response from a Context Hub.
       *
       * @param transactionId the transaction ID of the response
       * @param result        the result of the transaction as defined by the HAL TransactionResult
       */
      /* package */
      synchronized void onTransactionResponse(int transactionId, int result) {
          ContextHubServiceTransaction transaction = mTransactionQueue.peek();
          if (transaction == null) {
              Log.w(TAG, "Received unexpected transaction response (no transaction pending)");
              return;
          }
          if (transaction.getTransactionId() != transactionId) {
              Log.w(TAG, "Received unexpected transaction response (expected ID = "
                      + transaction.getTransactionId() + ", received ID = " + transactionId + ")");
              return;
          }
  
          transaction.onTransactionComplete(
                  (result == TransactionResult.SUCCESS) ?
                          ContextHubTransaction.RESULT_SUCCESS :
                          ContextHubTransaction.RESULT_FAILED_AT_HUB);
          removeTransactionAndStartNext();
      }
  
      /**
       * Handles a query response from a Context Hub.
       *
       * @param nanoAppStateList the list of nanoapps included in the response
       */
      /* package */
      synchronized void onQueryResponse(List<NanoAppState> nanoAppStateList) {
          ContextHubServiceTransaction transaction = mTransactionQueue.peek();
          if (transaction == null) {
              Log.w(TAG, "Received unexpected query response (no transaction pending)");
              return;
          }
          if (transaction.getTransactionType() != ContextHubTransaction.TYPE_QUERY_NANOAPPS) {
              Log.w(TAG, "Received unexpected query response (expected " + transaction + ")");
              return;
          }
  
          transaction.onQueryResponse(ContextHubTransaction.RESULT_SUCCESS, nanoAppStateList);
          removeTransactionAndStartNext();
      }
  
      /**
       * Handles a hub reset event by stopping a pending transaction and starting the next.
       */
      /* package */
      synchronized void onHubReset() {
          ContextHubServiceTransaction transaction = mTransactionQueue.peek();
          if (transaction == null) {
              return;
          }
  
          removeTransactionAndStartNext();
      }
  
      /**
       * Pops the front transaction from the queue and starts the next pending transaction request.
       *
       * Removing elements from the transaction queue must only be done through this method. When a
       * pending transaction is removed, the timeout timer is cancelled and the transaction is marked
       * complete.
       *
       * It is assumed that the transaction queue is non-empty when this method is invoked, and that
       * the caller has obtained a lock on this ContextHubTransactionManager object.
       */
      private void removeTransactionAndStartNext() {
          mTimeoutFuture.cancel(false /* mayInterruptIfRunning */);
  
          ContextHubServiceTransaction transaction = mTransactionQueue.remove();
          transaction.setComplete();
  
          if (!mTransactionQueue.isEmpty()) {
              startNextTransaction();
          }
      }
  
      /**
       * Starts the next pending transaction request.
       *
       * Starting new transactions must only be done through this method. This method continues to
       * process the transaction queue as long as there are pending requests, and no transaction is
       * pending.
       *
       * It is assumed that the caller has obtained a lock on this ContextHubTransactionManager
       * object.
       */
      private void startNextTransaction() {
          int result = Result.UNKNOWN_FAILURE;
          while (result != Result.OK && !mTransactionQueue.isEmpty()) {
              ContextHubServiceTransaction transaction = mTransactionQueue.peek();
              result = transaction.onTransact();
  
              if (result == Result.OK) {
                  Runnable onTimeoutFunc = () -> {
                      synchronized (this) {
                          if (!transaction.isComplete()) {
                              Log.d(TAG, transaction + " timed out");
                              transaction.onTransactionComplete(
                                      ContextHubTransaction.RESULT_FAILED_TIMEOUT);
  
                              removeTransactionAndStartNext();
                          }
                      }
                  };
  
                  long timeoutSeconds = transaction.getTimeout(TimeUnit.SECONDS);
                  mTimeoutFuture = mTimeoutExecutor.schedule(onTimeoutFunc, timeoutSeconds,
                          TimeUnit.SECONDS);
              } else {
                  transaction.onTransactionComplete(
                          ContextHubServiceUtil.toTransactionResult(result));
                  mTransactionQueue.remove();
              }
          }
      }
  }
  