1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.location.contexthub; 18 19 import android.annotation.IntDef; 20 import android.app.PendingIntent; 21 import android.content.Context; 22 import android.hardware.location.ContextHubInfo; 23 import android.hardware.location.IContextHubClient; 24 import android.hardware.location.IContextHubClientCallback; 25 import android.hardware.location.NanoAppMessage; 26 import android.os.RemoteException; 27 import android.util.Log; 28 import android.util.proto.ProtoOutputStream; 29 30 import com.android.server.location.ClientManagerProto; 31 32 import java.lang.annotation.Retention; 33 import java.lang.annotation.RetentionPolicy; 34 import java.util.Iterator; 35 import java.util.List; 36 import java.util.concurrent.ConcurrentHashMap; 37 import java.util.function.Consumer; 38 39 /** 40 * A class that manages registration/unregistration of clients and manages messages to/from clients. 41 * 42 * @hide 43 */ 44 /* package */ class ContextHubClientManager { 45 private static final String TAG = "ContextHubClientManager"; 46 47 /* 48 * The maximum host endpoint ID value that a client can be assigned. 49 */ 50 private static final int MAX_CLIENT_ID = 0x7fff; 51 52 /* 53 * Local flag to enable debug logging. 54 */ 55 private static final boolean DEBUG_LOG_ENABLED = false; 56 57 /* 58 * The context of the service. 59 */ 60 private final Context mContext; 61 62 /* 63 * The proxy to talk to the Context Hub. 64 */ 65 private final IContextHubWrapper mContextHubProxy; 66 67 /* 68 * A mapping of host endpoint IDs to the ContextHubClientBroker object of registered clients. 69 * A concurrent data structure is used since the registration/unregistration can occur in 70 * multiple threads. 71 */ 72 private final ConcurrentHashMap<Short, ContextHubClientBroker> mHostEndPointIdToClientMap = 73 new ConcurrentHashMap<>(); 74 75 /* 76 * The next host endpoint ID to start iterating for the next available host endpoint ID. 77 */ 78 private int mNextHostEndPointId = 0; 79 80 /* 81 * The list of previous registration records. 82 */ 83 private static final int NUM_CLIENT_RECORDS = 20; 84 private final ConcurrentLinkedEvictingDeque<RegistrationRecord> mRegistrationRecordDeque = 85 new ConcurrentLinkedEvictingDeque<>(NUM_CLIENT_RECORDS); 86 87 @Retention(RetentionPolicy.SOURCE) 88 @IntDef(prefix = { "ACTION_" }, value = { 89 ACTION_REGISTERED, 90 ACTION_UNREGISTERED, 91 ACTION_CANCELLED, 92 }) 93 public @interface Action {} 94 public static final int ACTION_REGISTERED = 0; 95 public static final int ACTION_UNREGISTERED = 1; 96 public static final int ACTION_CANCELLED = 2; 97 98 /** 99 * A container class to store a record of ContextHubClient registration. 100 */ 101 private class RegistrationRecord { 102 private final String mBroker; 103 private final int mAction; 104 private final long mTimestamp; 105 RegistrationRecord(String broker, @Action int action)106 RegistrationRecord(String broker, @Action int action) { 107 mBroker = broker; 108 mAction = action; 109 mTimestamp = System.currentTimeMillis(); 110 } 111 dump(ProtoOutputStream proto)112 void dump(ProtoOutputStream proto) { 113 proto.write(ClientManagerProto.RegistrationRecord.TIMESTAMP_MS, mTimestamp); 114 proto.write(ClientManagerProto.RegistrationRecord.ACTION, mAction); 115 proto.write(ClientManagerProto.RegistrationRecord.BROKER, mBroker); 116 } 117 118 @Override toString()119 public String toString() { 120 StringBuilder sb = new StringBuilder(); 121 sb.append(ContextHubServiceUtil.formatDateFromTimestamp(mTimestamp)); 122 sb.append(" "); 123 sb.append(mAction == ACTION_REGISTERED ? "+ " : "- "); 124 sb.append(mBroker); 125 if (mAction == ACTION_CANCELLED) { 126 sb.append(" (cancelled)"); 127 } 128 return sb.toString(); 129 } 130 } 131 ContextHubClientManager(Context context, IContextHubWrapper contextHubProxy)132 /* package */ ContextHubClientManager(Context context, IContextHubWrapper contextHubProxy) { 133 mContext = context; 134 mContextHubProxy = contextHubProxy; 135 } 136 137 /** 138 * Registers a new client with the service. 139 * 140 * @param contextHubInfo the object describing the hub this client is attached to 141 * @param clientCallback the callback interface of the client to register 142 * @param attributionTag an optional attribution tag within the given package 143 * 144 * @return the client interface 145 * 146 * @throws IllegalStateException if max number of clients have already registered 147 */ registerClient( ContextHubInfo contextHubInfo, IContextHubClientCallback clientCallback, String attributionTag, ContextHubTransactionManager transactionManager, String packageName)148 /* package */ IContextHubClient registerClient( 149 ContextHubInfo contextHubInfo, IContextHubClientCallback clientCallback, 150 String attributionTag, ContextHubTransactionManager transactionManager, 151 String packageName) { 152 ContextHubClientBroker broker; 153 synchronized (this) { 154 short hostEndPointId = getHostEndPointId(); 155 broker = new ContextHubClientBroker( 156 mContext, mContextHubProxy, this /* clientManager */, contextHubInfo, 157 hostEndPointId, clientCallback, attributionTag, transactionManager, 158 packageName); 159 mHostEndPointIdToClientMap.put(hostEndPointId, broker); 160 mRegistrationRecordDeque.add( 161 new RegistrationRecord(broker.toString(), ACTION_REGISTERED)); 162 } 163 164 try { 165 broker.attachDeathRecipient(); 166 } catch (RemoteException e) { 167 // The client process has died, so we close the connection and return null 168 Log.e(TAG, "Failed to attach death recipient to client"); 169 broker.close(); 170 return null; 171 } 172 173 Log.d(TAG, "Registered client with host endpoint ID " + broker.getHostEndPointId()); 174 return IContextHubClient.Stub.asInterface(broker); 175 } 176 177 /** 178 * Registers a new client with the service. 179 * 180 * @param pendingIntent the callback interface of the client to register 181 * @param contextHubInfo the object describing the hub this client is attached to 182 * @param nanoAppId the ID of the nanoapp to receive Intent events for 183 * @param attributionTag an optional attribution tag within the given package 184 * 185 * @return the client interface 186 * 187 * @throws IllegalStateException if there were too many registered clients at the service 188 */ registerClient( ContextHubInfo contextHubInfo, PendingIntent pendingIntent, long nanoAppId, String attributionTag, ContextHubTransactionManager transactionManager)189 /* package */ IContextHubClient registerClient( 190 ContextHubInfo contextHubInfo, PendingIntent pendingIntent, long nanoAppId, 191 String attributionTag, ContextHubTransactionManager transactionManager) { 192 ContextHubClientBroker broker; 193 String registerString = "Regenerated"; 194 synchronized (this) { 195 broker = getClientBroker(contextHubInfo.getId(), pendingIntent, nanoAppId); 196 197 if (broker == null) { 198 short hostEndPointId = getHostEndPointId(); 199 broker = new ContextHubClientBroker( 200 mContext, mContextHubProxy, this /* clientManager */, contextHubInfo, 201 hostEndPointId, pendingIntent, nanoAppId, attributionTag, 202 transactionManager); 203 mHostEndPointIdToClientMap.put(hostEndPointId, broker); 204 registerString = "Registered"; 205 mRegistrationRecordDeque.add( 206 new RegistrationRecord(broker.toString(), ACTION_REGISTERED)); 207 } else { 208 // Update the attribution tag to the latest value provided by the client app in 209 // case the app was updated and decided to change its tag. 210 broker.setAttributionTag(attributionTag); 211 } 212 } 213 214 Log.d(TAG, registerString + " client with host endpoint ID " + broker.getHostEndPointId()); 215 return IContextHubClient.Stub.asInterface(broker); 216 } 217 218 /** 219 * Handles a message sent from a nanoapp. 220 * 221 * @param contextHubId the ID of the hub where the nanoapp sent the message from 222 * @param hostEndpointId The host endpoint ID of the client that this message is for. 223 * @param message the message send by a nanoapp 224 * @param nanoappPermissions the set of permissions the nanoapp holds 225 * @param messagePermissions the set of permissions that should be used for attributing 226 * permissions when this message is consumed by a client 227 */ onMessageFromNanoApp( int contextHubId, short hostEndpointId, NanoAppMessage message, List<String> nanoappPermissions, List<String> messagePermissions)228 /* package */ void onMessageFromNanoApp( 229 int contextHubId, short hostEndpointId, NanoAppMessage message, 230 List<String> nanoappPermissions, List<String> messagePermissions) { 231 if (DEBUG_LOG_ENABLED) { 232 Log.v(TAG, "Received " + message); 233 } 234 235 if (message.isBroadcastMessage()) { 236 // Broadcast messages shouldn't be sent with any permissions tagged per CHRE API 237 // requirements. 238 if (!messagePermissions.isEmpty()) { 239 Log.wtf(TAG, "Received broadcast message with permissions from " 240 + message.getNanoAppId()); 241 } 242 243 ContextHubEventLogger.getInstance().logMessageFromNanoapp(contextHubId, message, true); 244 broadcastMessage(contextHubId, message, nanoappPermissions, messagePermissions); 245 } else { 246 ContextHubClientBroker proxy = mHostEndPointIdToClientMap.get(hostEndpointId); 247 if (proxy != null) { 248 ContextHubEventLogger.getInstance().logMessageFromNanoapp(contextHubId, message, 249 true); 250 proxy.sendMessageToClient(message, nanoappPermissions, messagePermissions); 251 } else { 252 ContextHubEventLogger.getInstance().logMessageFromNanoapp(contextHubId, message, 253 false); 254 Log.e(TAG, "Cannot send message to unregistered client (host endpoint ID = " 255 + hostEndpointId + ")"); 256 } 257 } 258 } 259 260 /** 261 * Unregisters a client from the service. 262 * 263 * This method should be invoked as a result of a client calling the ContextHubClient.close(), 264 * or if the client process has died. 265 * 266 * @param hostEndPointId the host endpoint ID of the client that has died 267 */ unregisterClient(short hostEndPointId)268 /* package */ void unregisterClient(short hostEndPointId) { 269 ContextHubClientBroker broker = mHostEndPointIdToClientMap.get(hostEndPointId); 270 if (broker != null) { 271 @Action int action = 272 broker.isPendingIntentCancelled() ? ACTION_CANCELLED : ACTION_UNREGISTERED; 273 mRegistrationRecordDeque.add(new RegistrationRecord(broker.toString(), action)); 274 } 275 276 if (mHostEndPointIdToClientMap.remove(hostEndPointId) != null) { 277 Log.d(TAG, "Unregistered client with host endpoint ID " + hostEndPointId); 278 } else { 279 Log.e(TAG, "Cannot unregister non-existing client with host endpoint ID " 280 + hostEndPointId); 281 } 282 } 283 284 /** 285 * @param contextHubId the ID of the hub where the nanoapp was loaded 286 * @param nanoAppId the ID of the nanoapp that was loaded 287 */ onNanoAppLoaded(int contextHubId, long nanoAppId)288 /* package */ void onNanoAppLoaded(int contextHubId, long nanoAppId) { 289 forEachClientOfHub(contextHubId, client -> client.onNanoAppLoaded(nanoAppId)); 290 } 291 292 /** 293 * @param contextHubId the ID of the hub where the nanoapp was unloaded 294 * @param nanoAppId the ID of the nanoapp that was unloaded 295 */ onNanoAppUnloaded(int contextHubId, long nanoAppId)296 /* package */ void onNanoAppUnloaded(int contextHubId, long nanoAppId) { 297 forEachClientOfHub(contextHubId, client -> client.onNanoAppUnloaded(nanoAppId)); 298 } 299 300 /** 301 * @param contextHubId the ID of the hub that has reset 302 */ onHubReset(int contextHubId)303 /* package */ void onHubReset(int contextHubId) { 304 forEachClientOfHub(contextHubId, client -> client.onHubReset()); 305 } 306 307 /** 308 * @param contextHubId the ID of the hub that contained the nanoapp that aborted 309 * @param nanoAppId the ID of the nanoapp that aborted 310 * @param abortCode the nanoapp specific abort code 311 */ onNanoAppAborted(int contextHubId, long nanoAppId, int abortCode)312 /* package */ void onNanoAppAborted(int contextHubId, long nanoAppId, int abortCode) { 313 forEachClientOfHub(contextHubId, client -> client.onNanoAppAborted(nanoAppId, abortCode)); 314 } 315 316 /** 317 * Runs a command for each client that is attached to a hub with the given ID. 318 * 319 * @param contextHubId the ID of the hub 320 * @param callback the command to invoke for the client 321 */ forEachClientOfHub( int contextHubId, Consumer<ContextHubClientBroker> callback)322 /* package */ void forEachClientOfHub( 323 int contextHubId, Consumer<ContextHubClientBroker> callback) { 324 for (ContextHubClientBroker broker : mHostEndPointIdToClientMap.values()) { 325 if (broker.getAttachedContextHubId() == contextHubId) { 326 callback.accept(broker); 327 } 328 } 329 } 330 331 /** 332 * Returns an available host endpoint ID. 333 * 334 * @returns an available host endpoint ID 335 * 336 * @throws IllegalStateException if max number of clients have already registered 337 */ getHostEndPointId()338 private short getHostEndPointId() { 339 if (mHostEndPointIdToClientMap.size() == MAX_CLIENT_ID + 1) { 340 throw new IllegalStateException("Could not register client - max limit exceeded"); 341 } 342 343 int id = mNextHostEndPointId; 344 for (int i = 0; i <= MAX_CLIENT_ID; i++) { 345 if (!mHostEndPointIdToClientMap.containsKey((short) id)) { 346 mNextHostEndPointId = (id == MAX_CLIENT_ID) ? 0 : id + 1; 347 break; 348 } 349 350 id = (id == MAX_CLIENT_ID) ? 0 : id + 1; 351 } 352 353 return (short) id; 354 } 355 356 /** 357 * Broadcasts a message from a nanoapp to all clients attached to the associated hub. 358 * 359 * @param contextHubId the ID of the hub where the nanoapp sent the message from 360 * @param message the message send by a nanoapp 361 */ broadcastMessage( int contextHubId, NanoAppMessage message, List<String> nanoappPermissions, List<String> messagePermissions)362 private void broadcastMessage( 363 int contextHubId, NanoAppMessage message, List<String> nanoappPermissions, 364 List<String> messagePermissions) { 365 forEachClientOfHub(contextHubId, 366 client -> client.sendMessageToClient( 367 message, nanoappPermissions, messagePermissions)); 368 } 369 370 /** 371 * Retrieves a ContextHubClientBroker object with a matching PendingIntent and Context Hub ID. 372 * 373 * @param pendingIntent the PendingIntent to match 374 * @param contextHubId the ID of the Context Hub the client is attached to 375 * @return the matching ContextHubClientBroker, null if not found 376 */ getClientBroker( int contextHubId, PendingIntent pendingIntent, long nanoAppId)377 private ContextHubClientBroker getClientBroker( 378 int contextHubId, PendingIntent pendingIntent, long nanoAppId) { 379 for (ContextHubClientBroker broker : mHostEndPointIdToClientMap.values()) { 380 if (broker.hasPendingIntent(pendingIntent, nanoAppId) 381 && broker.getAttachedContextHubId() == contextHubId) { 382 return broker; 383 } 384 } 385 386 return null; 387 } 388 389 /** 390 * Dump debugging info as ClientManagerProto 391 * 392 * If the output belongs to a sub message, the caller is responsible for wrapping this function 393 * between {@link ProtoOutputStream#start(long)} and {@link ProtoOutputStream#end(long)}. 394 * 395 * @param proto the ProtoOutputStream to write to 396 */ dump(ProtoOutputStream proto)397 void dump(ProtoOutputStream proto) { 398 for (ContextHubClientBroker broker : mHostEndPointIdToClientMap.values()) { 399 long token = proto.start(ClientManagerProto.CLIENT_BROKERS); 400 broker.dump(proto); 401 proto.end(token); 402 } 403 Iterator<RegistrationRecord> it = mRegistrationRecordDeque.descendingIterator(); 404 while (it.hasNext()) { 405 long token = proto.start(ClientManagerProto.REGISTRATION_RECORDS); 406 it.next().dump(proto); 407 proto.end(token); 408 } 409 } 410 411 @Override toString()412 public String toString() { 413 StringBuilder sb = new StringBuilder(); 414 for (ContextHubClientBroker broker : mHostEndPointIdToClientMap.values()) { 415 sb.append(broker); 416 sb.append(System.lineSeparator()); 417 } 418 419 sb.append(System.lineSeparator()); 420 sb.append("Registration History:"); 421 sb.append(System.lineSeparator()); 422 Iterator<RegistrationRecord> it = mRegistrationRecordDeque.descendingIterator(); 423 while (it.hasNext()) { 424 sb.append(it.next()); 425 sb.append(System.lineSeparator()); 426 } 427 428 return sb.toString(); 429 } 430 } 431