| /* |
| * 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.annotation.IntDef; |
| 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 android.util.proto.ProtoOutputStream; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.text.DateFormat; |
| import java.text.SimpleDateFormat; |
| import java.util.Date; |
| import java.util.Iterator; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentLinkedDeque; |
| 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 DateFormat for printing RegistrationRecord. |
| */ |
| private static final DateFormat DATE_FORMAT = new SimpleDateFormat("MM/dd HH:mm:ss.SSS"); |
| |
| /* |
| * 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; |
| |
| /* |
| * The list of previous registration records. |
| */ |
| private static final int NUM_CLIENT_RECORDS = 20; |
| private final ConcurrentLinkedEvictingDeque<RegistrationRecord> mRegistrationRecordDeque = |
| new ConcurrentLinkedEvictingDeque<>(NUM_CLIENT_RECORDS); |
| |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef(prefix = { "ACTION_" }, value = { |
| ACTION_REGISTERED, |
| ACTION_UNREGISTERED, |
| ACTION_CANCELLED, |
| }) |
| public @interface Action {} |
| public static final int ACTION_REGISTERED = 0; |
| public static final int ACTION_UNREGISTERED = 1; |
| public static final int ACTION_CANCELLED = 2; |
| |
| /** |
| * Helper class to make a ConcurrentLinkedDeque fixed-size, evicting old entries when full. |
| */ |
| private class ConcurrentLinkedEvictingDeque<E> extends ConcurrentLinkedDeque<E> { |
| private int mSize; |
| |
| ConcurrentLinkedEvictingDeque(int size) { |
| mSize = size; |
| } |
| |
| @Override |
| public boolean add(E elem) { |
| synchronized (this) { |
| if (size() == mSize) { |
| poll(); |
| } |
| |
| return super.add(elem); |
| } |
| } |
| } |
| |
| /** |
| * A container class to store a record of ContextHubClient registration. |
| */ |
| private class RegistrationRecord { |
| private final String mBroker; |
| private final int mAction; |
| private final long mTimestamp; |
| |
| RegistrationRecord(String broker, @Action int action) { |
| mBroker = broker; |
| mAction = action; |
| mTimestamp = System.currentTimeMillis(); |
| } |
| |
| void dump(ProtoOutputStream proto) { |
| proto.write(ClientManagerProto.RegistrationRecord.TIMESTAMP_MS, mTimestamp); |
| proto.write(ClientManagerProto.RegistrationRecord.ACTION, mAction); |
| proto.write(ClientManagerProto.RegistrationRecord.BROKER, mBroker); |
| } |
| |
| @Override |
| public String toString() { |
| String out = ""; |
| out += DATE_FORMAT.format(new Date(mTimestamp)) + " "; |
| out += mAction == ACTION_REGISTERED ? "+ " : "- "; |
| out += mBroker; |
| if (mAction == ACTION_CANCELLED) { |
| out += " (cancelled)"; |
| } |
| return out; |
| } |
| } |
| |
| /* 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); |
| mRegistrationRecordDeque.add( |
| new RegistrationRecord(broker.toString(), ACTION_REGISTERED)); |
| } |
| |
| 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"; |
| mRegistrationRecordDeque.add( |
| new RegistrationRecord(broker.toString(), ACTION_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) { |
| ContextHubClientBroker broker = mHostEndPointIdToClientMap.get(hostEndPointId); |
| if (broker != null) { |
| @Action int action = |
| broker.isPendingIntentCancelled() ? ACTION_CANCELLED : ACTION_UNREGISTERED; |
| mRegistrationRecordDeque.add(new RegistrationRecord(broker.toString(), action)); |
| } |
| |
| 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; |
| } |
| |
| /** |
| * Dump debugging info as ClientManagerProto |
| * |
| * If the output belongs to a sub message, the caller is responsible for wrapping this function |
| * between {@link ProtoOutputStream#start(long)} and {@link ProtoOutputStream#end(long)}. |
| * |
| * @param proto the ProtoOutputStream to write to |
| */ |
| void dump(ProtoOutputStream proto) { |
| for (ContextHubClientBroker broker : mHostEndPointIdToClientMap.values()) { |
| long token = proto.start(ClientManagerProto.CLIENT_BROKERS); |
| broker.dump(proto); |
| proto.end(token); |
| } |
| Iterator<RegistrationRecord> it = mRegistrationRecordDeque.descendingIterator(); |
| while (it.hasNext()) { |
| long token = proto.start(ClientManagerProto.REGISTRATION_RECORDS); |
| it.next().dump(proto); |
| proto.end(token); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| String out = ""; |
| for (ContextHubClientBroker broker : mHostEndPointIdToClientMap.values()) { |
| out += broker + "\n"; |
| } |
| |
| out += "\nRegistration history:\n"; |
| Iterator<RegistrationRecord> it = mRegistrationRecordDeque.descendingIterator(); |
| while (it.hasNext()) { |
| out += it.next() + "\n"; |
| } |
| |
| return out; |
| } |
| } |