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