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