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.hardware.location.ContextHubTransaction; 20 import android.hardware.location.IContextHubTransactionCallback; 21 import android.hardware.location.NanoAppBinary; 22 import android.hardware.location.NanoAppState; 23 import android.os.RemoteException; 24 import android.util.Log; 25 26 import java.util.ArrayDeque; 27 import java.util.Collections; 28 import java.util.Iterator; 29 import java.util.List; 30 import java.util.concurrent.ScheduledFuture; 31 import java.util.concurrent.ScheduledThreadPoolExecutor; 32 import java.util.concurrent.TimeUnit; 33 import java.util.concurrent.atomic.AtomicInteger; 34 35 /** 36 * Manages transactions at the Context Hub Service. 37 * <p> 38 * This class maintains a queue of transaction requests made to the ContextHubService by clients, 39 * and executes them through the Context Hub. At any point in time, either the transaction queue is 40 * empty, or there is a pending transaction that is waiting for an asynchronous response from the 41 * hub. This class also handles synchronous errors and timeouts of each transaction. 42 * 43 * @hide 44 */ 45 /* package */ class ContextHubTransactionManager { 46 private static final String TAG = "ContextHubTransactionManager"; 47 48 /* 49 * Maximum number of transaction requests that can be pending at a time 50 */ 51 private static final int MAX_PENDING_REQUESTS = 10000; 52 53 /* 54 * The proxy to talk to the Context Hub 55 */ 56 private final IContextHubWrapper mContextHubProxy; 57 58 /* 59 * The manager for all clients for the service. 60 */ 61 private final ContextHubClientManager mClientManager; 62 63 /* 64 * The nanoapp state manager for the service 65 */ 66 private final NanoAppStateManager mNanoAppStateManager; 67 68 /* 69 * A queue containing the current transactions 70 */ 71 private final ArrayDeque<ContextHubServiceTransaction> mTransactionQueue = new ArrayDeque<>(); 72 73 /* 74 * The next available transaction ID 75 */ 76 private final AtomicInteger mNextAvailableId = new AtomicInteger(); 77 78 /* 79 * An executor and the future object for scheduling timeout timers 80 */ 81 private final ScheduledThreadPoolExecutor mTimeoutExecutor = new ScheduledThreadPoolExecutor(1); 82 private ScheduledFuture<?> mTimeoutFuture = null; 83 84 /* 85 * The list of previous transaction records. 86 */ 87 private static final int NUM_TRANSACTION_RECORDS = 20; 88 private final ConcurrentLinkedEvictingDeque<TransactionRecord> mTransactionRecordDeque = 89 new ConcurrentLinkedEvictingDeque<>(NUM_TRANSACTION_RECORDS); 90 91 /** 92 * A container class to store a record of transactions. 93 */ 94 private class TransactionRecord { 95 private final String mTransaction; 96 private final long mTimestamp; 97 TransactionRecord(String transaction)98 TransactionRecord(String transaction) { 99 mTransaction = transaction; 100 mTimestamp = System.currentTimeMillis(); 101 } 102 103 // TODO: Add dump to proto here 104 105 @Override toString()106 public String toString() { 107 return ContextHubServiceUtil.formatDateFromTimestamp(mTimestamp) + " " + mTransaction; 108 } 109 } 110 ContextHubTransactionManager( IContextHubWrapper contextHubProxy, ContextHubClientManager clientManager, NanoAppStateManager nanoAppStateManager)111 /* package */ ContextHubTransactionManager( 112 IContextHubWrapper contextHubProxy, ContextHubClientManager clientManager, 113 NanoAppStateManager nanoAppStateManager) { 114 mContextHubProxy = contextHubProxy; 115 mClientManager = clientManager; 116 mNanoAppStateManager = nanoAppStateManager; 117 } 118 119 /** 120 * Creates a transaction for loading a nanoapp. 121 * 122 * @param contextHubId the ID of the hub to load the nanoapp to 123 * @param nanoAppBinary the binary of the nanoapp to load 124 * @param onCompleteCallback the client on complete callback 125 * @return the generated transaction 126 */ createLoadTransaction( int contextHubId, NanoAppBinary nanoAppBinary, IContextHubTransactionCallback onCompleteCallback, String packageName)127 /* package */ ContextHubServiceTransaction createLoadTransaction( 128 int contextHubId, NanoAppBinary nanoAppBinary, 129 IContextHubTransactionCallback onCompleteCallback, String packageName) { 130 return new ContextHubServiceTransaction( 131 mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_LOAD_NANOAPP, 132 nanoAppBinary.getNanoAppId(), packageName) { 133 @Override 134 /* package */ int onTransact() { 135 try { 136 return mContextHubProxy.loadNanoapp( 137 contextHubId, nanoAppBinary, this.getTransactionId()); 138 } catch (RemoteException e) { 139 Log.e(TAG, "RemoteException while trying to load nanoapp with ID 0x" + 140 Long.toHexString(nanoAppBinary.getNanoAppId()), e); 141 return ContextHubTransaction.RESULT_FAILED_UNKNOWN; 142 } 143 } 144 145 @Override 146 /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { 147 ContextHubStatsLog.write( 148 ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED, 149 nanoAppBinary.getNanoAppId(), 150 nanoAppBinary.getNanoAppVersion(), 151 ContextHubStatsLog 152 .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_LOAD, 153 toStatsTransactionResult(result)); 154 155 ContextHubEventLogger.getInstance().logNanoappLoad( 156 contextHubId, 157 nanoAppBinary.getNanoAppId(), 158 nanoAppBinary.getNanoAppVersion(), 159 nanoAppBinary.getBinary().length, 160 result == ContextHubTransaction.RESULT_SUCCESS); 161 162 if (result == ContextHubTransaction.RESULT_SUCCESS) { 163 // NOTE: The legacy JNI code used to do a query right after a load success 164 // to synchronize the service cache. Instead store the binary that was 165 // requested to load to update the cache later without doing a query. 166 mNanoAppStateManager.addNanoAppInstance( 167 contextHubId, nanoAppBinary.getNanoAppId(), 168 nanoAppBinary.getNanoAppVersion()); 169 } 170 try { 171 onCompleteCallback.onTransactionComplete(result); 172 if (result == ContextHubTransaction.RESULT_SUCCESS) { 173 mClientManager.onNanoAppLoaded(contextHubId, nanoAppBinary.getNanoAppId()); 174 } 175 } catch (RemoteException e) { 176 Log.e(TAG, "RemoteException while calling client onTransactionComplete", e); 177 } 178 } 179 }; 180 } 181 182 /** 183 * Creates a transaction for unloading a nanoapp. 184 * 185 * @param contextHubId the ID of the hub to unload the nanoapp from 186 * @param nanoAppId the ID of the nanoapp to unload 187 * @param onCompleteCallback the client on complete callback 188 * @return the generated transaction 189 */ 190 /* package */ ContextHubServiceTransaction createUnloadTransaction( 191 int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback, 192 String packageName) { 193 return new ContextHubServiceTransaction( 194 mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_UNLOAD_NANOAPP, 195 nanoAppId, packageName) { 196 @Override 197 /* package */ int onTransact() { 198 try { 199 return mContextHubProxy.unloadNanoapp( 200 contextHubId, nanoAppId, this.getTransactionId()); 201 } catch (RemoteException e) { 202 Log.e(TAG, "RemoteException while trying to unload nanoapp with ID 0x" + 203 Long.toHexString(nanoAppId), e); 204 return ContextHubTransaction.RESULT_FAILED_UNKNOWN; 205 } 206 } 207 208 @Override 209 /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { 210 ContextHubStatsLog.write( 211 ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED, nanoAppId, 212 0 /* nanoappVersion */, 213 ContextHubStatsLog 214 .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_UNLOAD, 215 toStatsTransactionResult(result)); 216 217 ContextHubEventLogger.getInstance().logNanoappUnload( 218 contextHubId, 219 nanoAppId, 220 result == ContextHubTransaction.RESULT_SUCCESS); 221 222 if (result == ContextHubTransaction.RESULT_SUCCESS) { 223 mNanoAppStateManager.removeNanoAppInstance(contextHubId, nanoAppId); 224 } 225 try { 226 onCompleteCallback.onTransactionComplete(result); 227 if (result == ContextHubTransaction.RESULT_SUCCESS) { 228 mClientManager.onNanoAppUnloaded(contextHubId, nanoAppId); 229 } 230 } catch (RemoteException e) { 231 Log.e(TAG, "RemoteException while calling client onTransactionComplete", e); 232 } 233 } 234 }; 235 } 236 237 /** 238 * Creates a transaction for enabling a nanoapp. 239 * 240 * @param contextHubId the ID of the hub to enable the nanoapp on 241 * @param nanoAppId the ID of the nanoapp to enable 242 * @param onCompleteCallback the client on complete callback 243 * @return the generated transaction 244 */ 245 /* package */ ContextHubServiceTransaction createEnableTransaction( 246 int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback, 247 String packageName) { 248 return new ContextHubServiceTransaction( 249 mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_ENABLE_NANOAPP, 250 packageName) { 251 @Override 252 /* package */ int onTransact() { 253 try { 254 return mContextHubProxy.enableNanoapp( 255 contextHubId, nanoAppId, this.getTransactionId()); 256 } catch (RemoteException e) { 257 Log.e(TAG, "RemoteException while trying to enable nanoapp with ID 0x" + 258 Long.toHexString(nanoAppId), e); 259 return ContextHubTransaction.RESULT_FAILED_UNKNOWN; 260 } 261 } 262 263 @Override 264 /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { 265 try { 266 onCompleteCallback.onTransactionComplete(result); 267 } catch (RemoteException e) { 268 Log.e(TAG, "RemoteException while calling client onTransactionComplete", e); 269 } 270 } 271 }; 272 } 273 274 /** 275 * Creates a transaction for disabling a nanoapp. 276 * 277 * @param contextHubId the ID of the hub to disable the nanoapp on 278 * @param nanoAppId the ID of the nanoapp to disable 279 * @param onCompleteCallback the client on complete callback 280 * @return the generated transaction 281 */ 282 /* package */ ContextHubServiceTransaction createDisableTransaction( 283 int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback, 284 String packageName) { 285 return new ContextHubServiceTransaction( 286 mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_DISABLE_NANOAPP, 287 packageName) { 288 @Override 289 /* package */ int onTransact() { 290 try { 291 return mContextHubProxy.disableNanoapp( 292 contextHubId, nanoAppId, this.getTransactionId()); 293 } catch (RemoteException e) { 294 Log.e(TAG, "RemoteException while trying to disable nanoapp with ID 0x" + 295 Long.toHexString(nanoAppId), e); 296 return ContextHubTransaction.RESULT_FAILED_UNKNOWN; 297 } 298 } 299 300 @Override 301 /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { 302 try { 303 onCompleteCallback.onTransactionComplete(result); 304 } catch (RemoteException e) { 305 Log.e(TAG, "RemoteException while calling client onTransactionComplete", e); 306 } 307 } 308 }; 309 } 310 311 /** 312 * Creates a transaction for querying for a list of nanoapps. 313 * 314 * @param contextHubId the ID of the hub to query 315 * @param onCompleteCallback the client on complete callback 316 * @return the generated transaction 317 */ 318 /* package */ ContextHubServiceTransaction createQueryTransaction( 319 int contextHubId, IContextHubTransactionCallback onCompleteCallback, 320 String packageName) { 321 return new ContextHubServiceTransaction( 322 mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_QUERY_NANOAPPS, 323 packageName) { 324 @Override 325 /* package */ int onTransact() { 326 try { 327 return mContextHubProxy.queryNanoapps(contextHubId); 328 } catch (RemoteException e) { 329 Log.e(TAG, "RemoteException while trying to query for nanoapps", e); 330 return ContextHubTransaction.RESULT_FAILED_UNKNOWN; 331 } 332 } 333 334 @Override 335 /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { 336 onQueryResponse(result, Collections.emptyList()); 337 } 338 339 @Override 340 /* package */ void onQueryResponse( 341 @ContextHubTransaction.Result int result, List<NanoAppState> nanoAppStateList) { 342 try { 343 onCompleteCallback.onQueryResponse(result, nanoAppStateList); 344 } catch (RemoteException e) { 345 Log.e(TAG, "RemoteException while calling client onQueryComplete", e); 346 } 347 } 348 }; 349 } 350 351 /** 352 * Adds a new transaction to the queue. 353 * <p> 354 * If there was no pending transaction at the time, the transaction that was added will be 355 * started in this method. If there were too many transactions in the queue, an exception will 356 * be thrown. 357 * 358 * @param transaction the transaction to add 359 * @throws IllegalStateException if the queue is full 360 */ 361 /* package */ 362 synchronized void addTransaction( 363 ContextHubServiceTransaction transaction) throws IllegalStateException { 364 if (mTransactionQueue.size() == MAX_PENDING_REQUESTS) { 365 throw new IllegalStateException("Transaction queue is full (capacity = " 366 + MAX_PENDING_REQUESTS + ")"); 367 } 368 mTransactionQueue.add(transaction); 369 mTransactionRecordDeque.add(new TransactionRecord(transaction.toString())); 370 371 if (mTransactionQueue.size() == 1) { 372 startNextTransaction(); 373 } 374 } 375 376 /** 377 * Handles a transaction response from a Context Hub. 378 * 379 * @param transactionId the transaction ID of the response 380 * @param success true if the transaction succeeded 381 */ 382 /* package */ 383 synchronized void onTransactionResponse(int transactionId, boolean success) { 384 ContextHubServiceTransaction transaction = mTransactionQueue.peek(); 385 if (transaction == null) { 386 Log.w(TAG, "Received unexpected transaction response (no transaction pending)"); 387 return; 388 } 389 if (transaction.getTransactionId() != transactionId) { 390 Log.w(TAG, "Received unexpected transaction response (expected ID = " 391 + transaction.getTransactionId() + ", received ID = " + transactionId + ")"); 392 return; 393 } 394 395 transaction.onTransactionComplete(success ? ContextHubTransaction.RESULT_SUCCESS : 396 ContextHubTransaction.RESULT_FAILED_AT_HUB); 397 removeTransactionAndStartNext(); 398 } 399 400 /** 401 * Handles a query response from a Context Hub. 402 * 403 * @param nanoAppStateList the list of nanoapps included in the response 404 */ 405 /* package */ 406 synchronized void onQueryResponse(List<NanoAppState> nanoAppStateList) { 407 ContextHubServiceTransaction transaction = mTransactionQueue.peek(); 408 if (transaction == null) { 409 Log.w(TAG, "Received unexpected query response (no transaction pending)"); 410 return; 411 } 412 if (transaction.getTransactionType() != ContextHubTransaction.TYPE_QUERY_NANOAPPS) { 413 Log.w(TAG, "Received unexpected query response (expected " + transaction + ")"); 414 return; 415 } 416 417 transaction.onQueryResponse(ContextHubTransaction.RESULT_SUCCESS, nanoAppStateList); 418 removeTransactionAndStartNext(); 419 } 420 421 /** 422 * Handles a hub reset event by stopping a pending transaction and starting the next. 423 */ 424 /* package */ 425 synchronized void onHubReset() { 426 ContextHubServiceTransaction transaction = mTransactionQueue.peek(); 427 if (transaction == null) { 428 return; 429 } 430 431 removeTransactionAndStartNext(); 432 } 433 434 /** 435 * Pops the front transaction from the queue and starts the next pending transaction request. 436 * <p> 437 * Removing elements from the transaction queue must only be done through this method. When a 438 * pending transaction is removed, the timeout timer is cancelled and the transaction is marked 439 * complete. 440 * <p> 441 * It is assumed that the transaction queue is non-empty when this method is invoked, and that 442 * the caller has obtained a lock on this ContextHubTransactionManager object. 443 */ 444 private void removeTransactionAndStartNext() { 445 mTimeoutFuture.cancel(false /* mayInterruptIfRunning */); 446 447 ContextHubServiceTransaction transaction = mTransactionQueue.remove(); 448 transaction.setComplete(); 449 450 if (!mTransactionQueue.isEmpty()) { 451 startNextTransaction(); 452 } 453 } 454 455 /** 456 * Starts the next pending transaction request. 457 * <p> 458 * Starting new transactions must only be done through this method. This method continues to 459 * process the transaction queue as long as there are pending requests, and no transaction is 460 * pending. 461 * <p> 462 * It is assumed that the caller has obtained a lock on this ContextHubTransactionManager 463 * object. 464 */ 465 private void startNextTransaction() { 466 int result = ContextHubTransaction.RESULT_FAILED_UNKNOWN; 467 while (result != ContextHubTransaction.RESULT_SUCCESS && !mTransactionQueue.isEmpty()) { 468 ContextHubServiceTransaction transaction = mTransactionQueue.peek(); 469 result = transaction.onTransact(); 470 471 if (result == ContextHubTransaction.RESULT_SUCCESS) { 472 Runnable onTimeoutFunc = () -> { 473 synchronized (this) { 474 if (!transaction.isComplete()) { 475 Log.d(TAG, transaction + " timed out"); 476 transaction.onTransactionComplete( 477 ContextHubTransaction.RESULT_FAILED_TIMEOUT); 478 479 removeTransactionAndStartNext(); 480 } 481 } 482 }; 483 484 long timeoutSeconds = transaction.getTimeout(TimeUnit.SECONDS); 485 try { 486 mTimeoutFuture = mTimeoutExecutor.schedule(onTimeoutFunc, timeoutSeconds, 487 TimeUnit.SECONDS); 488 } catch (Exception e) { 489 Log.e(TAG, "Error when schedule a timer", e); 490 } 491 } else { 492 transaction.onTransactionComplete( 493 ContextHubServiceUtil.toTransactionResult(result)); 494 mTransactionQueue.remove(); 495 } 496 } 497 } 498 499 private int toStatsTransactionResult(@ContextHubTransaction.Result int result) { 500 switch (result) { 501 case ContextHubTransaction.RESULT_SUCCESS: 502 return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_SUCCESS; 503 case ContextHubTransaction.RESULT_FAILED_BAD_PARAMS: 504 return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BAD_PARAMS; 505 case ContextHubTransaction.RESULT_FAILED_UNINITIALIZED: 506 return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNINITIALIZED; 507 case ContextHubTransaction.RESULT_FAILED_BUSY: 508 return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BUSY; 509 case ContextHubTransaction.RESULT_FAILED_AT_HUB: 510 return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_AT_HUB; 511 case ContextHubTransaction.RESULT_FAILED_TIMEOUT: 512 return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_TIMEOUT; 513 case ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE: 514 return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_SERVICE_INTERNAL_FAILURE; 515 case ContextHubTransaction.RESULT_FAILED_HAL_UNAVAILABLE: 516 return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_HAL_UNAVAILABLE; 517 case ContextHubTransaction.RESULT_FAILED_UNKNOWN: 518 default: /* fall through */ 519 return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNKNOWN; 520 } 521 } 522 523 @Override 524 public String toString() { 525 StringBuilder sb = new StringBuilder(100); 526 ContextHubServiceTransaction[] arr; 527 synchronized (this) { 528 arr = mTransactionQueue.toArray(new ContextHubServiceTransaction[0]); 529 } 530 for (int i = 0; i < arr.length; i++) { 531 sb.append(i + ": " + arr[i] + "\n"); 532 } 533 534 sb.append("Transaction History:\n"); 535 Iterator<TransactionRecord> iterator = mTransactionRecordDeque.descendingIterator(); 536 while (iterator.hasNext()) { 537 sb.append(iterator.next() + "\n"); 538 } 539 return sb.toString(); 540 } 541 } 542