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