Merge changes Ibf8cd200,I4ea2f293,I17b545da,Ia139fd6d,I3a7e69f8

* changes:
  Handles load and unload lifecycle callbacks
  Handles hub reset at ContextHubClientManager
  Converts old API message clients to the new API format
  Implements client close and death notification
  Creates framework for new API messaging
diff --git a/Android.mk b/Android.mk
index 7a3c46d..06f64f1 100644
--- a/Android.mk
+++ b/Android.mk
@@ -219,6 +219,8 @@
 	core/java/android/hardware/location/IGeofenceHardwareCallback.aidl \
 	core/java/android/hardware/location/IGeofenceHardwareMonitorCallback.aidl \
 	core/java/android/hardware/location/IContextHubCallback.aidl \
+	core/java/android/hardware/location/IContextHubClient.aidl \
+	core/java/android/hardware/location/IContextHubClientCallback.aidl \
 	core/java/android/hardware/location/IContextHubService.aidl \
 	core/java/android/hardware/location/IContextHubTransactionCallback.aidl \
 	core/java/android/hardware/radio/IRadioService.aidl \
diff --git a/core/java/android/hardware/location/ContextHubClient.java b/core/java/android/hardware/location/ContextHubClient.java
index b7e353a..52527ed 100644
--- a/core/java/android/hardware/location/ContextHubClient.java
+++ b/core/java/android/hardware/location/ContextHubClient.java
@@ -16,43 +16,48 @@
 package android.hardware.location;
 
 import android.annotation.RequiresPermission;
-import android.os.Handler;
+import android.os.RemoteException;
+
+import dalvik.system.CloseGuard;
 
 import java.io.Closeable;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * A class describing a client of the Context Hub Service.
  *
- * Clients can send messages to nanoapps at a Context Hub through this object.
+ * Clients can send messages to nanoapps at a Context Hub through this object. The APIs supported
+ * by this object are thread-safe and can be used without external synchronization.
  *
  * @hide
  */
 public class ContextHubClient implements Closeable {
     /*
-     * The ContextHubClient interface associated with this client.
+     * The proxy to the client interface at the service.
      */
-    // TODO: Implement this interface and associate with ContextHubClient object
-    // private final IContextHubClient mClientInterface;
+    private final IContextHubClient mClientProxy;
 
     /*
-     * The listening callback associated with this client.
+     * The callback interface associated with this client.
      */
-    private ContextHubClientCallback mCallback;
+    private final IContextHubClientCallback mCallbackInterface;
 
     /*
      * The Context Hub that this client is attached to.
      */
-    private ContextHubInfo mAttachedHub;
+    private final ContextHubInfo mAttachedHub;
 
-    /*
-     * The handler to invoke mCallback.
-     */
-    private Handler mCallbackHandler;
+    private final CloseGuard mCloseGuard = CloseGuard.get();
 
-    ContextHubClient(ContextHubClientCallback callback, Handler handler, ContextHubInfo hubInfo) {
-        mCallback = callback;
-        mCallbackHandler = handler;
+    private final AtomicBoolean mIsClosed = new AtomicBoolean(false);
+
+    /* package */ ContextHubClient(
+            IContextHubClient clientProxy, IContextHubClientCallback callback,
+            ContextHubInfo hubInfo) {
+        mClientProxy = clientProxy;
+        mCallbackInterface = callback;
         mAttachedHub = hubInfo;
+        mCloseGuard.open("close");
     }
 
     /**
@@ -71,7 +76,14 @@
      * All futures messages targeted for this client are dropped at the service.
      */
     public void close() {
-        throw new UnsupportedOperationException("TODO: Implement this");
+        if (!mIsClosed.getAndSet(true)) {
+            mCloseGuard.close();
+            try {
+                mClientProxy.close();
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
     }
 
     /**
@@ -90,6 +102,22 @@
     @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
     @ContextHubTransaction.Result
     public int sendMessageToNanoApp(NanoAppMessage message) {
-        throw new UnsupportedOperationException("TODO: Implement this");
+        try {
+            return mClientProxy.sendMessageToNanoApp(message);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+            close();
+        } finally {
+            super.finalize();
+        }
     }
 }
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 2411727..b31c7bc 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -456,6 +456,54 @@
     }
 
     /**
+     * Creates an interface to the ContextHubClient to send down to the service.
+     *
+     * @param callback the callback to invoke at the client process
+     * @param handler the handler to post callbacks for this client
+     *
+     * @return the callback interface
+     */
+    private IContextHubClientCallback createClientCallback(
+            ContextHubClientCallback callback, Handler handler) {
+        return new IContextHubClientCallback.Stub() {
+            @Override
+            public void onMessageFromNanoApp(NanoAppMessage message) {
+                handler.post(() -> callback.onMessageFromNanoApp(message));
+            }
+
+            @Override
+            public void onHubReset() {
+                handler.post(() -> callback.onHubReset());
+            }
+
+            @Override
+            public void onNanoAppAborted(long nanoAppId, int abortCode) {
+                handler.post(() -> callback.onNanoAppAborted(nanoAppId, abortCode));
+            }
+
+            @Override
+            public void onNanoAppLoaded(long nanoAppId) {
+                handler.post(() -> callback.onNanoAppLoaded(nanoAppId));
+            }
+
+            @Override
+            public void onNanoAppUnloaded(long nanoAppId) {
+                handler.post(() -> callback.onNanoAppUnloaded(nanoAppId));
+            }
+
+            @Override
+            public void onNanoAppEnabled(long nanoAppId) {
+                handler.post(() -> callback.onNanoAppEnabled(nanoAppId));
+            }
+
+            @Override
+            public void onNanoAppDisabled(long nanoAppId) {
+                handler.post(() -> callback.onNanoAppDisabled(nanoAppId));
+            }
+        };
+    }
+
+    /**
      * Creates and registers a client and its callback with the Context Hub Service.
      *
      * A client is registered with the Context Hub Service for a specified Context Hub. When the
@@ -463,19 +511,37 @@
      * {@link ContextHubClient} object, and receive notifications through the provided callback.
      *
      * @param callback the notification callback to register
-     * @param hubInfo the hub to attach this client to
-     * @param handler the handler to invoke the callback, if null uses the main thread's Looper
-     *
+     * @param hubInfo  the hub to attach this client to
+     * @param handler  the handler to invoke the callback, if null uses the main thread's Looper
      * @return the registered client object
      *
-     * @see ContextHubClientCallback
+     * @throws IllegalArgumentException if hubInfo does not represent a valid hub
+     * @throws IllegalStateException    if there were too many registered clients at the service
+     * @throws NullPointerException     if callback or hubInfo is null
      *
      * @hide
+     * @see ContextHubClientCallback
      */
     public ContextHubClient createClient(
             ContextHubClientCallback callback, ContextHubInfo hubInfo, @Nullable Handler handler) {
-        throw new UnsupportedOperationException(
-                "TODO: Implement this, and throw an exception on error");
+        if (callback == null) {
+            throw new NullPointerException("Callback cannot be null");
+        }
+        if (hubInfo == null) {
+            throw new NullPointerException("Hub info cannot be null");
+        }
+
+        Handler realHandler = (handler == null) ? new Handler(mMainLooper) : handler;
+        IContextHubClientCallback clientInterface = createClientCallback(callback, realHandler);
+
+        IContextHubClient client;
+        try {
+            client = mService.createClient(clientInterface, hubInfo.getId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        return new ContextHubClient(client, clientInterface, hubInfo);
     }
 
     /**
diff --git a/core/java/android/hardware/location/IContextHubClient.aidl b/core/java/android/hardware/location/IContextHubClient.aidl
new file mode 100644
index 0000000..d81126a
--- /dev/null
+++ b/core/java/android/hardware/location/IContextHubClient.aidl
@@ -0,0 +1,31 @@
+/*
+ * 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 android.hardware.location;
+
+import android.hardware.location.NanoAppMessage;
+
+/**
+ * @hide
+ */
+interface IContextHubClient {
+
+    // Sends a message to a nanoapp
+    int sendMessageToNanoApp(in NanoAppMessage message);
+
+    // Closes the connection with the Context Hub
+    void close();
+}
diff --git a/core/java/android/hardware/location/IContextHubClientCallback.aidl b/core/java/android/hardware/location/IContextHubClientCallback.aidl
new file mode 100644
index 0000000..1c76bcb
--- /dev/null
+++ b/core/java/android/hardware/location/IContextHubClientCallback.aidl
@@ -0,0 +1,49 @@
+/*
+ * 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 android.hardware.location;
+
+import android.hardware.location.NanoAppMessage;
+
+/**
+ * An interface used by the Context Hub Service to invoke callbacks for lifecycle notifications of a
+ * Context Hub and nanoapps, as well as for nanoapp messaging.
+ *
+ * @hide
+ */
+oneway interface IContextHubClientCallback {
+
+    // Callback invoked when receiving a message from a nanoapp.
+    void onMessageFromNanoApp(in NanoAppMessage message);
+
+    // Callback invoked when the attached Context Hub has reset.
+    void onHubReset();
+
+    // Callback invoked when a nanoapp aborts at the attached Context Hub.
+    void onNanoAppAborted(long nanoAppId, int abortCode);
+
+    // Callback invoked when a nanoapp is loaded at the attached Context Hub.
+    void onNanoAppLoaded(long nanoAppId);
+
+    // Callback invoked when a nanoapp is unloaded from the attached Context Hub.
+    void onNanoAppUnloaded(long nanoAppId);
+
+    // Callback invoked when a nanoapp is enabled at the attached Context Hub.
+    void onNanoAppEnabled(long nanoAppId);
+
+    // Callback invoked when a nanoapp is disabled at the attached Context Hub.
+    void onNanoAppDisabled(long nanoAppId);
+}
diff --git a/core/java/android/hardware/location/IContextHubService.aidl b/core/java/android/hardware/location/IContextHubService.aidl
index ff8c1d0..1bb7c8f 100644
--- a/core/java/android/hardware/location/IContextHubService.aidl
+++ b/core/java/android/hardware/location/IContextHubService.aidl
@@ -17,12 +17,14 @@
 package android.hardware.location;
 
 // Declare any non-default types here with import statements
-import android.hardware.location.ContextHubMessage;
 import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubMessage;
 import android.hardware.location.NanoApp;
-import android.hardware.location.NanoAppInstanceInfo;
 import android.hardware.location.NanoAppFilter;
+import android.hardware.location.NanoAppInstanceInfo;
 import android.hardware.location.IContextHubCallback;
+import android.hardware.location.IContextHubClient;
+import android.hardware.location.IContextHubClientCallback;
 
 /**
  * @hide
@@ -52,4 +54,7 @@
 
     // send a message to a nanoApp
     int sendMessage(int hubHandle, int nanoAppHandle, in ContextHubMessage msg);
+
+    // Creates a client to send and receive messages
+    IContextHubClient createClient(in IContextHubClientCallback client, int contextHubId);
 }
diff --git a/services/core/java/com/android/server/location/ContextHubClientBroker.java b/services/core/java/com/android/server/location/ContextHubClientBroker.java
new file mode 100644
index 0000000..41d9feb
--- /dev/null
+++ b/services/core/java/com/android/server/location/ContextHubClientBroker.java
@@ -0,0 +1,226 @@
+/*
+ * 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.content.Context;
+import android.hardware.contexthub.V1_0.ContextHubMsg;
+import android.hardware.contexthub.V1_0.IContexthub;
+import android.hardware.contexthub.V1_0.Result;
+import android.hardware.location.ContextHubTransaction;
+import android.hardware.location.IContextHubClient;
+import android.hardware.location.IContextHubClientCallback;
+import android.hardware.location.NanoAppMessage;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A class that acts as a broker for the ContextHubClient, which handles messaging and life-cycle
+ * notification callbacks. This class implements the IContextHubClient object, and the implemented
+ * APIs must be thread-safe.
+ *
+ * @hide
+ */
+public class ContextHubClientBroker extends IContextHubClient.Stub
+        implements IBinder.DeathRecipient {
+    private static final String TAG = "ContextHubClientBroker";
+
+    /*
+     * The context of the service.
+     */
+    private final Context mContext;
+
+    /*
+     * The proxy to talk to the Context Hub HAL.
+     */
+    private final IContexthub mContextHubProxy;
+
+    /*
+     * The manager that registered this client.
+     */
+    private final ContextHubClientManager mClientManager;
+
+    /*
+     * The ID of the hub that this client is attached to.
+     */
+    private final int mAttachedContextHubId;
+
+    /*
+     * The host end point ID of this client.
+     */
+    private final short mHostEndPointId;
+
+    /*
+     * The remote callback interface for this client.
+     */
+    private final IContextHubClientCallback mCallbackInterface;
+
+    /*
+     * false if the connection has been closed by the client, true otherwise.
+     */
+    private final AtomicBoolean mConnectionOpen = new AtomicBoolean(true);
+
+    /* package */ ContextHubClientBroker(
+            Context context, IContexthub contextHubProxy, ContextHubClientManager clientManager,
+            int contextHubId, short hostEndPointId, IContextHubClientCallback callback) {
+        mContext = context;
+        mContextHubProxy = contextHubProxy;
+        mClientManager = clientManager;
+        mAttachedContextHubId = contextHubId;
+        mHostEndPointId = hostEndPointId;
+        mCallbackInterface = callback;
+    }
+
+    /**
+     * Attaches a death recipient for this client
+     *
+     * @throws RemoteException if the client has already died
+     */
+    /* package */ void attachDeathRecipient() throws RemoteException {
+        mCallbackInterface.asBinder().linkToDeath(this, 0 /* flags */);
+    }
+
+    /**
+     * Sends from this client to a nanoapp.
+     *
+     * @param message the message to send
+     * @return the error code of sending the message
+     */
+    @ContextHubTransaction.Result
+    @Override
+    public int sendMessageToNanoApp(NanoAppMessage message) {
+        ContextHubServiceUtil.checkPermissions(mContext);
+
+        int result;
+        if (mConnectionOpen.get()) {
+            ContextHubMsg messageToNanoApp = ContextHubServiceUtil.createHidlContextHubMessage(
+                    mHostEndPointId, message);
+
+            try {
+                result = mContextHubProxy.sendMessageToHub(mAttachedContextHubId, messageToNanoApp);
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException in sendMessageToNanoApp (target hub ID = "
+                        + mAttachedContextHubId + ")", e);
+                result = Result.UNKNOWN_FAILURE;
+            }
+        } else {
+            Log.e(TAG, "Failed to send message to nanoapp: client connection is closed");
+            result = Result.UNKNOWN_FAILURE;
+        }
+
+        return ContextHubServiceUtil.toTransactionResult(result);
+    }
+
+    /**
+     * Closes the connection for this client with the service.
+     */
+    @Override
+    public void close() {
+        if (mConnectionOpen.getAndSet(false)) {
+            mClientManager.unregisterClient(mHostEndPointId);
+        }
+    }
+
+    /**
+     * Invoked when the underlying binder of this broker has died at the client process.
+     */
+    public void binderDied() {
+        try {
+            IContextHubClient.Stub.asInterface(this).close();
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException while closing client on death", e);
+        }
+    }
+
+    /**
+     * @return the ID of the context hub this client is attached to
+     */
+    /* package */ int getAttachedContextHubId() {
+        return mAttachedContextHubId;
+    }
+
+    /**
+     * @return the host endpoint ID of this client
+     */
+    /* package */ short getHostEndPointId() {
+        return mHostEndPointId;
+    }
+
+    /**
+     * Sends a message to the client associated with this object.
+     *
+     * @param message the message that came from a nanoapp
+     */
+    /* package */ void sendMessageToClient(NanoAppMessage message) {
+        if (mConnectionOpen.get()) {
+            try {
+                mCallbackInterface.onMessageFromNanoApp(message);
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException while sending message to client (host endpoint ID = "
+                        + mHostEndPointId + ")", e);
+            }
+        }
+    }
+
+    /**
+     * Handles a nanoapp load event.
+     *
+     * @param nanoAppId the ID of the nanoapp that was loaded.
+     */
+    /* package */ void onNanoAppLoaded(long nanoAppId) {
+        if (mConnectionOpen.get()) {
+            try {
+                mCallbackInterface.onNanoAppLoaded(nanoAppId);
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException while calling onNanoAppLoaded on client"
+                        + " (host endpoint ID = " + mHostEndPointId + ")", e);
+            }
+        }
+    }
+
+    /**
+     * Handles a nanoapp unload event.
+     *
+     * @param nanoAppId the ID of the nanoapp that was unloaded.
+     */
+    /* package */ void onNanoAppUnloaded(long nanoAppId) {
+        if (mConnectionOpen.get()) {
+            try {
+                mCallbackInterface.onNanoAppUnloaded(nanoAppId);
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException while calling onNanoAppUnloaded on client"
+                        + " (host endpoint ID = " + mHostEndPointId + ")", e);
+            }
+        }
+    }
+
+    /**
+     * Handles a hub reset for this client.
+     */
+    /* package */ void onHubReset() {
+        if (mConnectionOpen.get()) {
+            try {
+                mCallbackInterface.onHubReset();
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException while calling onHubReset on client" +
+                        " (host endpoint ID = " + mHostEndPointId + ")", e);
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/location/ContextHubClientManager.java b/services/core/java/com/android/server/location/ContextHubClientManager.java
new file mode 100644
index 0000000..d58a746
--- /dev/null
+++ b/services/core/java/com/android/server/location/ContextHubClientManager.java
@@ -0,0 +1,237 @@
+/*
+ * 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.content.Context;
+import android.hardware.contexthub.V1_0.ContextHubMsg;
+import android.hardware.contexthub.V1_0.IContexthub;
+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.NoSuchElementException;
+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 = true;
+
+    /*
+     * 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 clientCallback the callback interface of the client to register
+     * @param contextHubId   the ID of the hub this client is attached to
+     *
+     * @return the client interface
+     *
+     * @throws IllegalStateException if max number of clients have already registered
+     */
+    /* package */ IContextHubClient registerClient(
+            IContextHubClientCallback clientCallback, int contextHubId) {
+        ContextHubClientBroker broker = createNewClientBroker(clientCallback, contextHubId);
+
+        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);
+    }
+
+    /**
+     * 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) {
+            String targetAudience = clientMessage.isBroadcastMessage() ? "broadcast" : "unicast";
+            Log.v(TAG, "Received a " + targetAudience + " message from nanoapp 0x"
+                    + Long.toHexString(clientMessage.getNanoAppId()));
+        }
+
+        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);
+        }
+    }
+
+    /**
+     * Handles a nanoapp load event.
+     *
+     * @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));
+    }
+
+    /**
+     * Handles a nanoapp unload event.
+     *
+     * @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));
+    }
+
+    /**
+     * Handles a hub reset.
+     *
+     * @param contextHubId the ID of the hub that has reset.
+     */
+    /* package */ void onHubReset(int contextHubId) {
+        forEachClientOfHub(contextHubId, client -> client.onHubReset());
+    }
+
+    /**
+     * Creates a new ContextHubClientBroker object for a client and registers it with the client
+     * manager.
+     *
+     * @param clientCallback the callback interface of the client to register
+     * @param contextHubId   the ID of the hub this client is attached to
+     *
+     * @return the ContextHubClientBroker object
+     *
+     * @throws IllegalStateException if max number of clients have already registered
+     */
+    private synchronized ContextHubClientBroker createNewClientBroker(
+            IContextHubClientCallback clientCallback, int contextHubId) {
+        if (mHostEndPointIdToClientMap.size() == MAX_CLIENT_ID + 1) {
+            throw new IllegalStateException("Could not register client - max limit exceeded");
+        }
+
+        ContextHubClientBroker broker = null;
+        int id = mNextHostEndpointId;
+        for (int i = 0; i <= MAX_CLIENT_ID; i++) {
+            if (!mHostEndPointIdToClientMap.containsKey(id)) {
+                broker = new ContextHubClientBroker(
+                        mContext, mContextHubProxy, this, contextHubId, (short)id, clientCallback);
+                mHostEndPointIdToClientMap.put((short)id, broker);
+                mNextHostEndpointId = (id == MAX_CLIENT_ID) ? 0 : id + 1;
+                break;
+            }
+
+            id = (id == MAX_CLIENT_ID) ? 0 : id + 1;
+        }
+
+        return broker;
+    }
+
+    /**
+     * 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);
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/location/ContextHubService.java b/services/core/java/com/android/server/location/ContextHubService.java
index da481a8..e08c659 100644
--- a/services/core/java/com/android/server/location/ContextHubService.java
+++ b/services/core/java/com/android/server/location/ContextHubService.java
@@ -16,12 +16,10 @@
 
 package com.android.server.location;
 
-import android.Manifest;
 import android.content.Context;
 import android.hardware.contexthub.V1_0.AsyncEventType;
 import android.hardware.contexthub.V1_0.ContextHub;
 import android.hardware.contexthub.V1_0.ContextHubMsg;
-import android.hardware.contexthub.V1_0.HostEndPoint;
 import android.hardware.contexthub.V1_0.HubAppInfo;
 import android.hardware.contexthub.V1_0.IContexthub;
 import android.hardware.contexthub.V1_0.IContexthubCallback;
@@ -29,13 +27,17 @@
 import android.hardware.contexthub.V1_0.TransactionResult;
 import android.hardware.location.ContextHubInfo;
 import android.hardware.location.ContextHubMessage;
+import android.hardware.location.ContextHubTransaction;
 import android.hardware.location.IContextHubCallback;
+import android.hardware.location.IContextHubClient;
+import android.hardware.location.IContextHubClientCallback;
 import android.hardware.location.IContextHubService;
 import android.hardware.location.IContextHubTransactionCallback;
 import android.hardware.location.NanoApp;
 import android.hardware.location.NanoAppBinary;
 import android.hardware.location.NanoAppFilter;
 import android.hardware.location.NanoAppInstanceInfo;
+import android.hardware.location.NanoAppMessage;
 import android.hardware.location.NanoAppState;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
@@ -49,7 +51,9 @@
 import java.nio.ByteOrder;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -58,9 +62,6 @@
  */
 public class ContextHubService extends IContextHubService.Stub {
     private static final String TAG = "ContextHubService";
-    private static final String HARDWARE_PERMISSION = Manifest.permission.LOCATION_HARDWARE;
-    private static final String ENFORCE_HW_PERMISSION_MESSAGE = "Permission '"
-            + HARDWARE_PERMISSION + "' not granted to access ContextHub Hardware";
 
     /*
      * Constants for the type of transaction that is defined by ContextHubService.
@@ -104,6 +105,12 @@
     // The manager for transaction queue
     private final ContextHubTransactionManager mTransactionManager;
 
+    // The manager for sending messages to/from clients
+    private final ContextHubClientManager mClientManager;
+
+    // The default client for old API clients
+    private final Map<Integer, IContextHubClient> mDefaultClientMap;
+
     /**
      * Class extending the callback to register with a Context Hub.
      */
@@ -146,21 +153,34 @@
         mContextHubProxy = getContextHubProxy();
         if (mContextHubProxy == null) {
             mTransactionManager = null;
+            mClientManager = null;
+            mDefaultClientMap = Collections.EMPTY_MAP;
             mContextHubInfo = new ContextHubInfo[0];
             return;
         }
 
-        mTransactionManager = new ContextHubTransactionManager(mContextHubProxy);
+        mClientManager = new ContextHubClientManager(mContext, mContextHubProxy);
+        mTransactionManager = new ContextHubTransactionManager(mContextHubProxy, mClientManager);
 
         List<ContextHub> hubList;
         try {
             hubList = mContextHubProxy.getHubs();
         } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException while getting Context Hub info");
+            Log.e(TAG, "RemoteException while getting Context Hub info", e);
             hubList = Collections.emptyList();
         }
         mContextHubInfo = ContextHubServiceUtil.createContextHubInfoArray(hubList);
 
+        HashMap<Integer, IContextHubClient> defaultClientMap = new HashMap<>();
+        for (ContextHubInfo contextHubInfo : mContextHubInfo) {
+            int contextHubId = contextHubInfo.getId();
+
+            IContextHubClient client = mClientManager.registerClient(
+                    createDefaultClientCallback(contextHubId), contextHubId);
+            defaultClientMap.put(contextHubId, client);
+        }
+        mDefaultClientMap = Collections.unmodifiableMap(defaultClientMap);
+
         for (ContextHubInfo contextHubInfo : mContextHubInfo) {
             int contextHubId = contextHubInfo.getId();
             try {
@@ -168,7 +188,7 @@
                         contextHubId, new ContextHubServiceCallback(contextHubId));
             } catch (RemoteException e) {
                 Log.e(TAG, "RemoteException while registering service callback for hub (ID = "
-                        + contextHubId + ")");
+                        + contextHubId + ")", e);
             }
         }
 
@@ -185,6 +205,53 @@
     }
 
     /**
+     * Creates a default client callback for old API clients.
+     *
+     * @param contextHubId the ID of the hub to attach this client to
+     * @return the internal callback interface
+     */
+    private IContextHubClientCallback createDefaultClientCallback(int contextHubId) {
+        return new IContextHubClientCallback.Stub() {
+            @Override
+            public void onMessageFromNanoApp(NanoAppMessage message) {
+                int nanoAppInstanceId =
+                        mNanoAppIdToInstanceMap.containsKey(message.getNanoAppId()) ?
+                        mNanoAppIdToInstanceMap.get(message.getNanoAppId()) : -1;
+
+                onMessageReceiptOldApi(
+                        message.getMessageType(), contextHubId, nanoAppInstanceId,
+                        message.getMessageBody());
+            }
+
+            @Override
+            public void onHubReset() {
+                byte[] data = {TransactionResult.SUCCESS};
+                onMessageReceiptOldApi(MSG_HUB_RESET, contextHubId, OS_APP_INSTANCE, data);
+            }
+
+            @Override
+            public void onNanoAppAborted(long nanoAppId, int abortCode) {
+            }
+
+            @Override
+            public void onNanoAppLoaded(long nanoAppId) {
+            }
+
+            @Override
+            public void onNanoAppUnloaded(long nanoAppId) {
+            }
+
+            @Override
+            public void onNanoAppEnabled(long nanoAppId) {
+            }
+
+            @Override
+            public void onNanoAppDisabled(long nanoAppId) {
+            }
+        };
+    }
+
+    /**
      * @return the IContexthub proxy interface
      */
     private IContexthub getContextHubProxy() {
@@ -192,7 +259,7 @@
         try {
             proxy = IContexthub.getService(true /* retry */);
         } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException while attaching to Context Hub HAL proxy");
+            Log.e(TAG, "RemoteException while attaching to Context Hub HAL proxy", e);
         } catch (NoSuchElementException e) {
             Log.i(TAG, "Context Hub HAL service not found");
         }
@@ -204,6 +271,7 @@
     public int registerCallback(IContextHubCallback callback) throws RemoteException {
         checkPermissions();
         mCallbacksList.register(callback);
+
         Log.d(TAG, "Added callback, total callbacks " +
                 mCallbacksList.getRegisteredCallbackCount());
         return 0;
@@ -292,7 +360,7 @@
             @Override
             public void onQueryResponse(int result, List<NanoAppState> nanoAppStateList) {
                 byte[] data = {(byte) result};
-                onMessageReceipt(MSG_QUERY_NANO_APPS, contextHubId, OS_APP_INSTANCE, data);
+                onMessageReceiptOldApi(MSG_QUERY_NANO_APPS, contextHubId, OS_APP_INSTANCE, data);
             }
         };
     }
@@ -449,41 +517,37 @@
             return -1;
         }
         if (msg.getData() == null) {
-            Log.w(TAG, "ContextHubMessage message body cannot be null");
+            Log.e(TAG, "ContextHubMessage message body cannot be null");
+            return -1;
+        }
+        if (!mDefaultClientMap.containsKey(hubHandle)) {
+            Log.e(TAG, "Hub with ID " + hubHandle + " does not exist");
             return -1;
         }
 
-        int result;
+        boolean success = false;
         if (nanoAppHandle == OS_APP_INSTANCE) {
             if (msg.getMsgType() == MSG_QUERY_NANO_APPS) {
-                result = queryNanoAppsInternal(hubHandle);
+                success = (queryNanoAppsInternal(hubHandle) == Result.OK);
             } else {
                 Log.e(TAG, "Invalid OS message params of type " + msg.getMsgType());
-                result = Result.BAD_PARAMS;
             }
         } else {
             NanoAppInstanceInfo info = getNanoAppInstanceInfo(nanoAppHandle);
             if (info != null) {
-                ContextHubMsg hubMessage = new ContextHubMsg();
-                hubMessage.appName = info.getAppId();
-                hubMessage.msgType = msg.getMsgType();
-                hubMessage.hostEndPoint = HostEndPoint.UNSPECIFIED;
-                ContextHubServiceUtil.copyToByteArrayList(msg.getData(), hubMessage.msg);
+                NanoAppMessage message = NanoAppMessage.createMessageToNanoApp(
+                        info.getAppId(), msg.getMsgType(), msg.getData());
 
-                try {
-                    result = mContextHubProxy.sendMessageToHub(hubHandle, hubMessage);
-                } catch (RemoteException e) {
-                    Log.e(TAG, "Failed to send nanoapp message - RemoteException");
-                    result = Result.UNKNOWN_FAILURE;
-                }
+                IContextHubClient client = mDefaultClientMap.get(hubHandle);
+                success = (client.sendMessageToNanoApp(message) ==
+                        ContextHubTransaction.TRANSACTION_SUCCESS);
             } else {
                 Log.e(TAG, "Failed to send nanoapp message - nanoapp with instance ID "
                         + nanoAppHandle + " does not exist.");
-                result = Result.BAD_PARAMS;
             }
         }
 
-        return (result == Result.OK ? 0 : -1);
+        return success ? 0 : -1;
     }
 
     /**
@@ -493,12 +557,7 @@
      * @param message      the message contents
      */
     private void handleClientMessageCallback(int contextHubId, ContextHubMsg message) {
-        // TODO(b/67734082): Send to new API clients
-        byte[] data = ContextHubServiceUtil.createPrimitiveByteArray(message.msg);
-
-        int nanoAppInstanceId = mNanoAppIdToInstanceMap.containsKey(message.appName) ?
-                mNanoAppIdToInstanceMap.get(message.appName) : -1;
-        onMessageReceipt(message.msgType, contextHubId, nanoAppInstanceId, data);
+        mClientManager.onMessageFromNanoApp(contextHubId, message);
     }
 
     /**
@@ -534,7 +593,7 @@
         data[0] = (byte) result;
         ByteBuffer.wrap(data, 1, 4).order(ByteOrder.nativeOrder()).putInt(instanceId);
 
-        onMessageReceipt(MSG_LOAD_NANO_APP, contextHubId, OS_APP_INSTANCE, data);
+        onMessageReceiptOldApi(MSG_LOAD_NANO_APP, contextHubId, OS_APP_INSTANCE, data);
     }
 
     /**
@@ -552,7 +611,7 @@
 
         byte[] data = new byte[1];
         data[0] = (byte) result;
-        onMessageReceipt(MSG_UNLOAD_NANO_APP, contextHubId, OS_APP_INSTANCE, data);
+        onMessageReceiptOldApi(MSG_UNLOAD_NANO_APP, contextHubId, OS_APP_INSTANCE, data);
     }
 
     /**
@@ -577,8 +636,7 @@
             mTransactionManager.onHubReset();
             queryNanoAppsInternal(contextHubId);
 
-            byte[] data = {TransactionResult.SUCCESS};
-            onMessageReceipt(MSG_HUB_RESET, contextHubId, OS_APP_INSTANCE, data);
+            mClientManager.onHubReset(contextHubId);
         } else {
             Log.i(TAG, "Received unknown hub event (hub ID = " + contextHubId + ", type = "
                     + eventType + ")");
@@ -641,6 +699,45 @@
         }
     }
 
+    /**
+     * @param contextHubId the hub ID to validate
+     * @return {@code true} if the ID represents that of an available hub, {@code false} otherwise
+     */
+    private boolean isValidContextHubId(int contextHubId) {
+        for (ContextHubInfo hubInfo : mContextHubInfo) {
+            if (hubInfo.getId() == contextHubId) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Creates and registers a client at the service for the specified Context Hub.
+     *
+     * @param clientCallback the client interface to register with the service
+     * @param contextHubId   the ID of the hub this client is attached to
+     * @return the generated client interface, null if registration was unsuccessful
+     *
+     * @throws IllegalArgumentException if contextHubId is not a valid ID
+     * @throws IllegalStateException if max number of clients have already registered
+     * @throws NullPointerException if clientCallback is null
+     */
+    @Override
+    public IContextHubClient createClient(
+            IContextHubClientCallback clientCallback, int contextHubId) throws RemoteException {
+        checkPermissions();
+        if (!isValidContextHubId(contextHubId)) {
+            throw new IllegalArgumentException("Invalid context hub ID " + contextHubId);
+        }
+        if (clientCallback == null) {
+            throw new NullPointerException("Cannot register client with null callback");
+        }
+
+        return mClientManager.registerClient(clientCallback, contextHubId);
+    }
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
@@ -664,10 +761,10 @@
     }
 
     private void checkPermissions() {
-        mContext.enforceCallingPermission(HARDWARE_PERMISSION, ENFORCE_HW_PERMISSION_MESSAGE);
+        ContextHubServiceUtil.checkPermissions(mContext);
     }
 
-    private int onMessageReceipt(int msgType, int hubHandle, int appInstance, byte[] data) {
+    private int onMessageReceiptOldApi(int msgType, int hubHandle, int appInstance, byte[] data) {
         if (data == null) {
             return -1;
         }
diff --git a/services/core/java/com/android/server/location/ContextHubServiceUtil.java b/services/core/java/com/android/server/location/ContextHubServiceUtil.java
index ddbaf86..6faeb72 100644
--- a/services/core/java/com/android/server/location/ContextHubServiceUtil.java
+++ b/services/core/java/com/android/server/location/ContextHubServiceUtil.java
@@ -16,11 +16,15 @@
 
 package com.android.server.location;
 
+import android.Manifest;
+import android.content.Context;
 import android.hardware.contexthub.V1_0.ContextHub;
 import android.hardware.contexthub.V1_0.ContextHubMsg;
 import android.hardware.contexthub.V1_0.HostEndPoint;
 import android.hardware.contexthub.V1_0.HubAppInfo;
+import android.hardware.contexthub.V1_0.Result;
 import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubTransaction;
 import android.hardware.location.NanoAppBinary;
 import android.hardware.location.NanoAppMessage;
 import android.hardware.location.NanoAppState;
@@ -34,6 +38,9 @@
  */
 /* package */ class ContextHubServiceUtil {
     private static final String TAG = "ContextHubServiceUtil";
+    private static final String HARDWARE_PERMISSION = Manifest.permission.LOCATION_HARDWARE;
+    private static final String ENFORCE_HW_PERMISSION_MESSAGE = "Permission '"
+            + HARDWARE_PERMISSION + "' not granted to access ContextHub Hardware";
 
     /**
      * Creates a ContextHubInfo array from an ArrayList of HIDL ContextHub objects.
@@ -165,4 +172,40 @@
                 message.appName, message.msgType, messageArray,
                 message.hostEndPoint == HostEndPoint.BROADCAST);
     }
+
+    /**
+     * Checks for location hardware permissions.
+     *
+     * @param context the context of the service
+     */
+    /* package */
+    static void checkPermissions(Context context) {
+        context.enforceCallingPermission(HARDWARE_PERMISSION, ENFORCE_HW_PERMISSION_MESSAGE);
+    }
+
+    /**
+     * Helper function to convert from the HAL Result enum error code to the
+     * ContextHubTransaction.Result type.
+     *
+     * @param halResult the Result enum error code
+     * @return the ContextHubTransaction.Result equivalent
+     */
+    @ContextHubTransaction.Result
+    /* package */
+    static int toTransactionResult(int halResult) {
+        switch (halResult) {
+            case Result.OK:
+                return ContextHubTransaction.TRANSACTION_SUCCESS;
+            case Result.BAD_PARAMS:
+                return ContextHubTransaction.TRANSACTION_FAILED_BAD_PARAMS;
+            case Result.NOT_INIT:
+                return ContextHubTransaction.TRANSACTION_FAILED_UNINITIALIZED;
+            case Result.TRANSACTION_PENDING:
+                return ContextHubTransaction.TRANSACTION_FAILED_PENDING;
+            case Result.TRANSACTION_FAILED:
+            case Result.UNKNOWN_FAILURE:
+            default: /* fall through */
+                return ContextHubTransaction.TRANSACTION_FAILED_UNKNOWN;
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/location/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/ContextHubTransactionManager.java
index 898b76c..47d9d56 100644
--- a/services/core/java/com/android/server/location/ContextHubTransactionManager.java
+++ b/services/core/java/com/android/server/location/ContextHubTransactionManager.java
@@ -58,6 +58,11 @@
     private final IContexthub mContextHubProxy;
 
     /*
+     * The manager for all clients for the service.
+     */
+    private final ContextHubClientManager mClientManager;
+
+    /*
      * A queue containing the current transactions
      */
     private final ArrayDeque<ContextHubServiceTransaction> mTransactionQueue = new ArrayDeque<>();
@@ -73,8 +78,10 @@
     private final ScheduledThreadPoolExecutor mTimeoutExecutor = new ScheduledThreadPoolExecutor(1);
     private ScheduledFuture<?> mTimeoutFuture = null;
 
-    /* package */ ContextHubTransactionManager(IContexthub contextHubProxy) {
+    /* package */ ContextHubTransactionManager(
+            IContexthub contextHubProxy, ContextHubClientManager clientManager) {
         mContextHubProxy = contextHubProxy;
+        mClientManager = clientManager;
     }
 
     /**
@@ -113,6 +120,9 @@
             /* package */ void onTransactionComplete(int result) {
                 try {
                     onCompleteCallback.onTransactionComplete(result);
+                    if (result == Result.OK) {
+                        mClientManager.onNanoAppLoaded(contextHubId, nanoAppBinary.getNanoAppId());
+                    }
                 } catch (RemoteException e) {
                     Log.e(TAG, "RemoteException while calling client onTransactionComplete");
                 }
@@ -153,6 +163,9 @@
             /* package */ void onTransactionComplete(int result) {
                 try {
                     onCompleteCallback.onTransactionComplete(result);
+                    if (result == Result.OK) {
+                        mClientManager.onNanoAppUnloaded(contextHubId, nanoAppId);
+                    }
                 } catch (RemoteException e) {
                     Log.e(TAG, "RemoteException while calling client onTransactionComplete");
                 }