• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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