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.app.PendingIntent;
  import android.content.Context;
  import android.hardware.contexthub.V1_0.ContextHubMsg;
  import android.hardware.contexthub.V1_0.IContexthub;
  import android.hardware.location.ContextHubInfo;
  import android.hardware.location.IContextHubClient;
  import android.hardware.location.IContextHubClientCallback;
  import android.hardware.location.NanoAppMessage;
  import android.os.RemoteException;
  import android.util.Log;
  
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.function.Consumer;
  
  /**
   * A class that manages registration/unregistration of clients and manages messages to/from clients.
   *
   * @hide
   */
  /* package */ class ContextHubClientManager {
      private static final String TAG = "ContextHubClientManager";
  
      /*
       * The maximum host endpoint ID value that a client can be assigned.
       */
      private static final int MAX_CLIENT_ID = 0x7fff;
  
      /*
       * Local flag to enable debug logging.
       */
      private static final boolean DEBUG_LOG_ENABLED = false;
  
      /*
       * The context of the service.
       */
      private final Context mContext;
  
      /*
       * The proxy to talk to the Context Hub.
       */
      private final IContexthub mContextHubProxy;
  
      /*
       * A mapping of host endpoint IDs to the ContextHubClientBroker object of registered clients.
       * A concurrent data structure is used since the registration/unregistration can occur in
       * multiple threads.
       */
      private final ConcurrentHashMap<Short, ContextHubClientBroker> mHostEndPointIdToClientMap =
              new ConcurrentHashMap<>();
  
      /*
       * The next host endpoint ID to start iterating for the next available host endpoint ID.
       */
      private int mNextHostEndPointId = 0;
  
      /* package */ ContextHubClientManager(
              Context context, IContexthub contextHubProxy) {
          mContext = context;
          mContextHubProxy = contextHubProxy;
      }
  
      /**
       * Registers a new client with the service.
       *
       * @param contextHubInfo the object describing the hub this client is attached to
       * @param clientCallback the callback interface of the client to register
       *
       * @return the client interface
       *
       * @throws IllegalStateException if max number of clients have already registered
       */
      /* package */ IContextHubClient registerClient(
              ContextHubInfo contextHubInfo, IContextHubClientCallback clientCallback) {
          ContextHubClientBroker broker;
          synchronized (this) {
              short hostEndPointId = getHostEndPointId();
              broker = new ContextHubClientBroker(
                      mContext, mContextHubProxy, this /* clientManager */, contextHubInfo,
                      hostEndPointId, clientCallback);
              mHostEndPointIdToClientMap.put(hostEndPointId, broker);
          }
  
          try {
              broker.attachDeathRecipient();
          } catch (RemoteException e) {
              // The client process has died, so we close the connection and return null
              Log.e(TAG, "Failed to attach death recipient to client");
              broker.close();
              return null;
          }
  
          Log.d(TAG, "Registered client with host endpoint ID " + broker.getHostEndPointId());
          return IContextHubClient.Stub.asInterface(broker);
      }
  
      /**
       * Registers a new client with the service.
       *
       * @param pendingIntent  the callback interface of the client to register
       * @param contextHubInfo the object describing the hub this client is attached to
       * @param nanoAppId      the ID of the nanoapp to receive Intent events for
       *
       * @return the client interface
       *
       * @throws IllegalStateException    if there were too many registered clients at the service
       */
      /* package */ IContextHubClient registerClient(
              ContextHubInfo contextHubInfo, PendingIntent pendingIntent, long nanoAppId) {
          ContextHubClientBroker broker;
          String registerString = "Regenerated";
          synchronized (this) {
              broker = getClientBroker(contextHubInfo.getId(), pendingIntent, nanoAppId);
  
              if (broker == null) {
                  short hostEndPointId = getHostEndPointId();
                  broker = new ContextHubClientBroker(
                          mContext, mContextHubProxy, this /* clientManager */, contextHubInfo,
                          hostEndPointId, pendingIntent, nanoAppId);
                  mHostEndPointIdToClientMap.put(hostEndPointId, broker);
                  registerString = "Registered";
              }
          }
  
          Log.d(TAG, registerString + " client with host endpoint ID " + broker.getHostEndPointId());
          return IContextHubClient.Stub.asInterface(broker);
      }
  
      /**
       * Handles a message sent from a nanoapp.
       *
       * @param contextHubId the ID of the hub where the nanoapp sent the message from
       * @param message      the message send by a nanoapp
       */
      /* package */ void onMessageFromNanoApp(int contextHubId, ContextHubMsg message) {
          NanoAppMessage clientMessage = ContextHubServiceUtil.createNanoAppMessage(message);
  
          if (DEBUG_LOG_ENABLED) {
              Log.v(TAG, "Received " + clientMessage);
          }
  
          if (clientMessage.isBroadcastMessage()) {
              broadcastMessage(contextHubId, clientMessage);
          } else {
              ContextHubClientBroker proxy = mHostEndPointIdToClientMap.get(message.hostEndPoint);
              if (proxy != null) {
                  proxy.sendMessageToClient(clientMessage);
              } else {
                  Log.e(TAG, "Cannot send message to unregistered client (host endpoint ID = "
                          + message.hostEndPoint + ")");
              }
          }
      }
  
      /**
       * Unregisters a client from the service.
       *
       * This method should be invoked as a result of a client calling the ContextHubClient.close(),
       * or if the client process has died.
       *
       * @param hostEndPointId the host endpoint ID of the client that has died
       */
      /* package */ void unregisterClient(short hostEndPointId) {
          if (mHostEndPointIdToClientMap.remove(hostEndPointId) != null) {
              Log.d(TAG, "Unregistered client with host endpoint ID " + hostEndPointId);
          } else {
              Log.e(TAG, "Cannot unregister non-existing client with host endpoint ID "
                      + hostEndPointId);
          }
      }
  
      /**
       * @param contextHubId the ID of the hub where the nanoapp was loaded
       * @param nanoAppId    the ID of the nanoapp that was loaded
       */
      /* package */ void onNanoAppLoaded(int contextHubId, long nanoAppId) {
          forEachClientOfHub(contextHubId, client -> client.onNanoAppLoaded(nanoAppId));
      }
  
      /**
       * @param contextHubId the ID of the hub where the nanoapp was unloaded
       * @param nanoAppId    the ID of the nanoapp that was unloaded
       */
      /* package */ void onNanoAppUnloaded(int contextHubId, long nanoAppId) {
          forEachClientOfHub(contextHubId, client -> client.onNanoAppUnloaded(nanoAppId));
      }
  
      /**
       * @param contextHubId the ID of the hub that has reset
       */
      /* package */ void onHubReset(int contextHubId) {
          forEachClientOfHub(contextHubId, client -> client.onHubReset());
      }
  
      /**
       * @param contextHubId the ID of the hub that contained the nanoapp that aborted
       * @param nanoAppId the ID of the nanoapp that aborted
       * @param abortCode the nanoapp specific abort code
       */
      /* package */ void onNanoAppAborted(int contextHubId, long nanoAppId, int abortCode) {
          forEachClientOfHub(contextHubId, client -> client.onNanoAppAborted(nanoAppId, abortCode));
      }
  
      /**
       * Returns an available host endpoint ID.
       *
       * @returns an available host endpoint ID
       *
       * @throws IllegalStateException if max number of clients have already registered
       */
      private short getHostEndPointId() {
          if (mHostEndPointIdToClientMap.size() == MAX_CLIENT_ID + 1) {
              throw new IllegalStateException("Could not register client - max limit exceeded");
          }
  
          int id = mNextHostEndPointId;
          for (int i = 0; i <= MAX_CLIENT_ID; i++) {
              if (!mHostEndPointIdToClientMap.containsKey((short) id)) {
                  mNextHostEndPointId = (id == MAX_CLIENT_ID) ? 0 : id + 1;
                  break;
              }
  
              id = (id == MAX_CLIENT_ID) ? 0 : id + 1;
          }
  
          return (short) id;
      }
  
      /**
       * Broadcasts a message from a nanoapp to all clients attached to the associated hub.
       *
       * @param contextHubId the ID of the hub where the nanoapp sent the message from
       * @param message      the message send by a nanoapp
       */
      private void broadcastMessage(int contextHubId, NanoAppMessage message) {
          forEachClientOfHub(contextHubId, client -> client.sendMessageToClient(message));
      }
  
      /**
       * Runs a command for each client that is attached to a hub with the given ID.
       *
       * @param contextHubId the ID of the hub
       * @param callback     the command to invoke for the client
       */
      private void forEachClientOfHub(int contextHubId, Consumer<ContextHubClientBroker> callback) {
          for (ContextHubClientBroker broker : mHostEndPointIdToClientMap.values()) {
              if (broker.getAttachedContextHubId() == contextHubId) {
                  callback.accept(broker);
              }
          }
      }
  
      /**
       * Retrieves a ContextHubClientBroker object with a matching PendingIntent and Context Hub ID.
       *
       * @param pendingIntent the PendingIntent to match
       * @param contextHubId  the ID of the Context Hub the client is attached to
       * @return the matching ContextHubClientBroker, null if not found
       */
      private ContextHubClientBroker getClientBroker(
              int contextHubId, PendingIntent pendingIntent, long nanoAppId) {
          for (ContextHubClientBroker broker : mHostEndPointIdToClientMap.values()) {
              if (broker.hasPendingIntent(pendingIntent, nanoAppId)
                      && broker.getAttachedContextHubId() == contextHubId) {
                  return broker;
              }
          }
  
          return null;
      }
  }
  