1 /*
2  * Copyright (C) 2022 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 androidx.core.uwb.backend.impl.internal;
18 
19 import static androidx.core.uwb.backend.impl.internal.RangingSessionCallback.REASON_FAILED_TO_START;
20 import static androidx.core.uwb.backend.impl.internal.RangingSessionCallback.REASON_STOP_RANGING_CALLED;
21 import static androidx.core.uwb.backend.impl.internal.RangingSessionCallback.REASON_WRONG_PARAMETERS;
22 import static androidx.core.uwb.backend.impl.internal.Utils.INVALID_API_CALL;
23 import static androidx.core.uwb.backend.impl.internal.Utils.RANGING_ALREADY_STARTED;
24 import static androidx.core.uwb.backend.impl.internal.Utils.STATUS_OK;
25 import static androidx.core.uwb.backend.impl.internal.Utils.TAG;
26 import static androidx.core.uwb.backend.impl.internal.Utils.UWB_RECONFIGURATION_FAILURE;
27 import static androidx.core.uwb.backend.impl.internal.Utils.UWB_SYSTEM_CALLBACK_FAILURE;
28 
29 import static com.google.uwb.support.fira.FiraParams.RANGING_DEVICE_DT_TAG;
30 
31 import static java.util.Objects.requireNonNull;
32 
33 import android.os.Build.VERSION;
34 import android.os.Build.VERSION_CODES;
35 import android.os.PersistableBundle;
36 import android.util.Log;
37 import android.uwb.RangingMeasurement;
38 import android.uwb.RangingReport;
39 import android.uwb.RangingSession;
40 import android.uwb.UwbManager;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 import androidx.annotation.WorkerThread;
45 
46 import com.google.common.hash.Hashing;
47 import com.google.uwb.support.dltdoa.DlTDoARangingRoundsUpdate;
48 import com.google.uwb.support.fira.FiraOpenSessionParams;
49 import com.google.uwb.support.multichip.ChipInfoParams;
50 
51 import java.util.Arrays;
52 import java.util.HashMap;
53 import java.util.List;
54 import java.util.concurrent.Executor;
55 import java.util.concurrent.ExecutorService;
56 
57 /** Implements start/stop ranging operations. */
58 public abstract class RangingDevice {
59 
60     public static final int SESSION_ID_UNSET = 0;
61     private static final String NO_MULTICHIP_SUPPORT = "NO_MULTICHIP_SUPPORT";
62 
63     /** Timeout value after ranging start call */
64     private static final int RANGING_START_TIMEOUT_MILLIS = 3100;
65 
66     protected final UwbManager mUwbManager;
67 
68     private final OpAsyncCallbackRunner<Boolean> mOpAsyncCallbackRunner;
69 
70     @Nullable
71     private UwbAddress mLocalAddress;
72 
73     @Nullable
74     protected UwbComplexChannel mComplexChannel;
75 
76     @Nullable
77     protected RangingParameters mRangingParameters;
78 
79     /** A serial thread used by System API to handle session callbacks. */
80     private Executor mSystemCallbackExecutor;
81 
82     /** A serial thread used in system API callbacks to handle Backend callbacks */
83     @Nullable
84     private ExecutorService mBackendCallbackExecutor;
85 
86     /** NotNull when session opening is successful. Set to Null when session is closed. */
87     @Nullable
88     private RangingSession mRangingSession;
89 
90     private boolean mIsRanging = false;
91 
92     /** If true, local address and complex channel will be hardcoded */
93     private Boolean mForTesting = false;
94 
95     @Nullable
96     private RangingRoundFailureCallback mRangingRoundFailureCallback = null;
97 
98     private boolean mRangingReportedAllowed = false;
99 
100     @Nullable
101     private String mChipId = null;
102 
103     @NonNull
104     protected final UwbFeatureFlags mUwbFeatureFlags;
105 
106     private final HashMap<String, UwbAddress> mMultiChipMap;
107 
RangingDevice(UwbManager manager, Executor executor, OpAsyncCallbackRunner<Boolean> opAsyncCallbackRunner, UwbFeatureFlags uwbFeatureFlags)108     RangingDevice(UwbManager manager, Executor executor,
109             OpAsyncCallbackRunner<Boolean> opAsyncCallbackRunner, UwbFeatureFlags uwbFeatureFlags) {
110         mUwbManager = manager;
111         this.mSystemCallbackExecutor = executor;
112         mOpAsyncCallbackRunner = opAsyncCallbackRunner;
113         mOpAsyncCallbackRunner.setOperationTimeoutMillis(RANGING_START_TIMEOUT_MILLIS);
114         mUwbFeatureFlags = uwbFeatureFlags;
115         this.mMultiChipMap = new HashMap<>();
116         initializeUwbAddress();
117     }
118 
119     /** Sets the chip ID. By default, the default chip is used. */
setChipId(String chipId)120     public void setChipId(String chipId) {
121         mChipId = chipId;
122     }
123 
isForTesting()124     public Boolean isForTesting() {
125         return mForTesting;
126     }
127 
setForTesting(Boolean forTesting)128     public void setForTesting(Boolean forTesting) {
129         mForTesting = forTesting;
130     }
131 
132     /** Gets local address. The first call will return a randomized short address. */
getLocalAddress()133     public UwbAddress getLocalAddress() {
134         if (isLocalAddressSet()) {
135           return mLocalAddress;
136         }
137         // UwbManager#getDefaultChipId is supported from Android T.
138         if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) {
139             return getLocalAddress(NO_MULTICHIP_SUPPORT);
140         }
141         String defaultChipId = mUwbManager.getDefaultChipId();
142         return getLocalAddress(defaultChipId);
143     }
144 
145     /** Gets local address given chip ID. The first call will return a randomized short address. */
getLocalAddress(String chipId)146     public UwbAddress getLocalAddress(String chipId) {
147         if (mMultiChipMap.get(chipId) == null) {
148             mMultiChipMap.put(chipId, getRandomizedLocalAddress());
149         }
150         mLocalAddress = mMultiChipMap.get(chipId);
151         return mLocalAddress;
152     }
153 
154     /** Check whether local address was previously set. */
isLocalAddressSet()155     public boolean isLocalAddressSet() {
156         return mLocalAddress != null;
157     }
158 
159     /** Sets local address. */
setLocalAddress(UwbAddress localAddress)160     public void setLocalAddress(UwbAddress localAddress) {
161         mLocalAddress = localAddress;
162     }
163 
164     /** Gets a randomized short address. */
getRandomizedLocalAddress()165     private UwbAddress getRandomizedLocalAddress() {
166         return UwbAddress.getRandomizedShortAddress();
167     }
168 
hashSessionId(RangingParameters rangingParameters)169     protected abstract int hashSessionId(RangingParameters rangingParameters);
170 
calculateHashedSessionId( UwbAddress controllerAddress, UwbComplexChannel complexChannel)171     static int calculateHashedSessionId(
172             UwbAddress controllerAddress, UwbComplexChannel complexChannel) {
173         return Hashing.sha256()
174                 .newHasher()
175                 .putBytes(controllerAddress.toBytes())
176                 .putInt(complexChannel.encode())
177                 .hash()
178                 .asInt();
179     }
180 
181     /** Sets the ranging parameter for this session. */
setRangingParameters(RangingParameters rangingParameters)182     public synchronized void setRangingParameters(RangingParameters rangingParameters) {
183         if (rangingParameters.getSessionId() == SESSION_ID_UNSET) {
184             int sessionId = hashSessionId(rangingParameters);
185             mRangingParameters =
186                     new RangingParameters(
187                             rangingParameters.getUwbConfigId(),
188                             sessionId,
189                             rangingParameters.getSubSessionId(),
190                             rangingParameters.getSessionKeyInfo(),
191                             rangingParameters.getSubSessionKeyInfo(),
192                             rangingParameters.getComplexChannel(),
193                             rangingParameters.getPeerAddresses(),
194                             rangingParameters.getRangingUpdateRate(),
195                             rangingParameters.getUwbRangeDataNtfConfig(),
196                             rangingParameters.getSlotDuration(),
197                             rangingParameters.isAoaDisabled());
198         } else {
199             mRangingParameters = rangingParameters;
200         }
201     }
202 
203     /** Alive means the session is open. */
isAlive()204     public boolean isAlive() {
205         return mRangingSession != null;
206     }
207 
208     /**
209      * Is the ranging ongoing or not. Since the device can be stopped by peer or scheduler, the
210      * session can be open but not ranging
211      */
isRanging()212     public boolean isRanging() {
213         return mIsRanging;
214     }
215 
isKnownPeer(UwbAddress address)216     protected boolean isKnownPeer(UwbAddress address) {
217         requireNonNull(mRangingParameters);
218         return mRangingParameters.getPeerAddresses().contains(address);
219     }
220 
221     /**
222      * Converts the {@link RangingReport} to {@link RangingPosition} and invokes the GMSCore
223      * callback.
224      */
225     // Null-guard prevents this from being null
onRangingDataReceived( RangingReport rangingReport, RangingSessionCallback callback)226     private synchronized void onRangingDataReceived(
227             RangingReport rangingReport, RangingSessionCallback callback) {
228         List<RangingMeasurement> measurements = rangingReport.getMeasurements();
229         for (RangingMeasurement measurement : measurements) {
230             byte[] remoteAddressBytes = measurement.getRemoteDeviceAddress().toBytes();
231             if (mUwbFeatureFlags.isReversedByteOrderFiraParams()) {
232                 remoteAddressBytes = Conversions.getReverseBytes(remoteAddressBytes);
233             }
234 
235 
236             UwbAddress peerAddress = UwbAddress.fromBytes(remoteAddressBytes);
237             if (!isKnownPeer(peerAddress) && !Conversions.isDlTdoaMeasurement(measurement)) {
238                 Log.w(TAG,
239                         String.format("Received ranging data from unknown peer %s.", peerAddress));
240                 continue;
241             }
242 
243             if (measurement.getStatus() != RangingMeasurement.RANGING_STATUS_SUCCESS
244                     && mRangingRoundFailureCallback != null) {
245                 mRangingRoundFailureCallback.onRangingRoundFailed(peerAddress);
246             }
247 
248             RangingPosition currentPosition = Conversions.convertToPosition(measurement);
249             if (currentPosition == null) {
250                 continue;
251             }
252             UwbDevice uwbDevice = UwbDevice.createForAddress(peerAddress.toBytes());
253             callback.onRangingResult(uwbDevice, currentPosition);
254         }
255     }
256 
257     /**
258      * Run callbacks in {@link RangingSessionCallback} on this thread. Make sure that no lock is
259      * acquired when the callbacks are called since the code is out of this class.
260      */
runOnBackendCallbackThread(Runnable action)261     protected void runOnBackendCallbackThread(Runnable action) {
262         requireNonNull(mBackendCallbackExecutor);
263         mBackendCallbackExecutor.execute(action);
264     }
265 
getUwbDevice()266     private UwbDevice getUwbDevice() {
267         return UwbDevice.createForAddress(getLocalAddress().toBytes());
268     }
269 
initializeUwbAddress()270     private void initializeUwbAddress() {
271         // UwbManager#getChipInfos is supported from Android T.
272         if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
273             List<PersistableBundle> chipInfoBundles = mUwbManager.getChipInfos();
274             for (PersistableBundle chipInfo : chipInfoBundles) {
275                 mMultiChipMap.put(ChipInfoParams.fromBundle(chipInfo).getChipId(),
276                         getRandomizedLocalAddress());
277             }
278         } else {
279             mMultiChipMap.put(NO_MULTICHIP_SUPPORT, getRandomizedLocalAddress());
280         }
281     }
282 
convertCallback(RangingSessionCallback callback)283     protected RangingSession.Callback convertCallback(RangingSessionCallback callback) {
284         return new RangingSession.Callback() {
285 
286             @WorkerThread
287             @Override
288             public void onOpened(RangingSession session) {
289                 mRangingSession = session;
290                 mOpAsyncCallbackRunner.complete(true);
291             }
292 
293             @WorkerThread
294             @Override
295             public void onOpenFailed(int reason, PersistableBundle params) {
296                 Log.i(TAG, String.format("Session open failed: reason %s", reason));
297                 int suspendedReason = Conversions.convertReason(reason);
298                 if (suspendedReason == REASON_UNKNOWN) {
299                     suspendedReason = REASON_FAILED_TO_START;
300                 }
301                 int finalSuspendedReason = suspendedReason;
302                 runOnBackendCallbackThread(
303                         () -> callback.onRangingSuspended(getUwbDevice(), finalSuspendedReason));
304                 mRangingSession = null;
305                 mOpAsyncCallbackRunner.complete(false);
306             }
307 
308             @WorkerThread
309             @Override
310             public void onStarted(PersistableBundle sessionInfo) {
311                 callback.onRangingInitialized(getUwbDevice());
312                 mIsRanging = true;
313                 mOpAsyncCallbackRunner.complete(true);
314             }
315 
316             @WorkerThread
317             @Override
318             public void onStartFailed(int reason, PersistableBundle params) {
319 
320                 int suspendedReason = Conversions.convertReason(reason);
321                 if (suspendedReason != REASON_WRONG_PARAMETERS) {
322                     suspendedReason = REASON_FAILED_TO_START;
323                 }
324                 int finalSuspendedReason = suspendedReason;
325                 runOnBackendCallbackThread(
326                         () -> callback.onRangingSuspended(getUwbDevice(), finalSuspendedReason));
327                 if (mRangingSession != null) {
328                     mRangingSession.close();
329                 }
330                 mRangingSession = null;
331                 mOpAsyncCallbackRunner.complete(false);
332             }
333 
334             @WorkerThread
335             @Override
336             public void onReconfigured(PersistableBundle params) {
337                 mOpAsyncCallbackRunner.completeIfActive(true);
338             }
339 
340             @WorkerThread
341             @Override
342             public void onReconfigureFailed(int reason, PersistableBundle params) {
343                 mOpAsyncCallbackRunner.completeIfActive(false);
344             }
345 
346             @WorkerThread
347             @Override
348             public void onStopped(int reason, PersistableBundle params) {
349                 int suspendedReason = Conversions.convertReason(reason);
350                 UwbDevice device = getUwbDevice();
351                 runOnBackendCallbackThread(
352                         () -> {
353                             synchronized (RangingDevice.this) {
354                                 mIsRanging = false;
355                             }
356                             callback.onRangingSuspended(device, suspendedReason);
357                         });
358                 if (suspendedReason == REASON_STOP_RANGING_CALLED
359                         && mOpAsyncCallbackRunner.isActive()) {
360                     mOpAsyncCallbackRunner.complete(true);
361                 }
362             }
363 
364             @WorkerThread
365             @Override
366             public void onStopFailed(int reason, PersistableBundle params) {
367                 mOpAsyncCallbackRunner.completeIfActive(false);
368             }
369 
370             @WorkerThread
371             @Override
372             public void onClosed(int reason, PersistableBundle parameters) {
373                 mRangingSession = null;
374                 mOpAsyncCallbackRunner.completeIfActive(true);
375             }
376 
377             @WorkerThread
378             @Override
379             public void onReportReceived(RangingReport rangingReport) {
380                 if (mRangingReportedAllowed) {
381                     runOnBackendCallbackThread(
382                             () -> onRangingDataReceived(rangingReport, callback));
383                 }
384             }
385 
386             @WorkerThread
387             @Override
388             public void onRangingRoundsUpdateDtTagStatus(PersistableBundle params) {
389                 // Failure to set ranging rounds is not handled.
390                 mOpAsyncCallbackRunner.complete(true);
391             }
392 
393             @WorkerThread
394             @Override
395             public void onControleeAdded(PersistableBundle params) {
396                 mOpAsyncCallbackRunner.complete(true);
397             }
398 
399             @WorkerThread
400             @Override
401             public void onControleeAddFailed(int reason, PersistableBundle params) {
402                 mOpAsyncCallbackRunner.complete(false);
403             }
404 
405             @WorkerThread
406             @Override
407             public void onControleeRemoved(PersistableBundle params) {
408                 if (mOpAsyncCallbackRunner.isActive()) {
409                     mOpAsyncCallbackRunner.complete(true);
410                 }
411             }
412 
413             @WorkerThread
414             @Override
415             public void onControleeRemoveFailed(int reason, PersistableBundle params) {
416                 mOpAsyncCallbackRunner.complete(false);
417             }
418         };
419     }
420 
421     protected abstract FiraOpenSessionParams getOpenSessionParams();
422 
423     private String getString(@Nullable Object o) {
424         if (o == null) {
425             return "null";
426         }
427         if (o instanceof int[]) {
428             return Arrays.toString((int[]) o);
429         }
430 
431         if (o instanceof byte[]) {
432             return Arrays.toString((byte[]) o);
433         }
434 
435         if (o instanceof long[]) {
436             return Arrays.toString((long[]) o);
437         }
438 
439         return o.toString();
440     }
441 
442     private void printStartRangingParameters(PersistableBundle parameters) {
443         Log.i(TAG, "Opens UWB session with bundle parameters:");
444         for (String key : parameters.keySet()) {
445             Log.i(TAG, String.format(
446                     "UWB parameter: %s, value: %s", key, getString(parameters.get(key))));
447         }
448     }
449 
450     /**
451      * Starts ranging. if an active ranging session exists, return {@link
452      * RangingSessionCallback#REASON_FAILED_TO_START}
453      */
454     @Utils.UwbStatusCodes
455     public synchronized int startRanging(
456             RangingSessionCallback callback, ExecutorService backendCallbackExecutor) {
457         if (isAlive()) {
458             return RANGING_ALREADY_STARTED;
459         }
460 
461         if (getLocalAddress() == null) {
462             return INVALID_API_CALL;
463         }
464 
465         FiraOpenSessionParams openSessionParams = getOpenSessionParams();
466         printStartRangingParameters(openSessionParams.toBundle());
467         mBackendCallbackExecutor = backendCallbackExecutor;
468         boolean success =
469                 mOpAsyncCallbackRunner.execOperation(
470                         () -> {
471                             if (mChipId != null) {
472                                 mUwbManager.openRangingSession(
473                                         openSessionParams.toBundle(),
474                                         mSystemCallbackExecutor,
475                                         convertCallback(callback),
476                                         mChipId);
477                             } else {
478                                 mUwbManager.openRangingSession(
479                                         openSessionParams.toBundle(),
480                                         mSystemCallbackExecutor,
481                                         convertCallback(callback));
482                             }
483                         },
484                         "Open session");
485 
486         Boolean result = mOpAsyncCallbackRunner.getResult();
487         if (!success || result == null || !result) {
488             requireNonNull(mBackendCallbackExecutor);
489             mBackendCallbackExecutor.shutdown();
490             mBackendCallbackExecutor = null;
491             // onRangingSuspended should have been called in the callback.
492             return STATUS_OK;
493         }
494 
495         if (openSessionParams.getDeviceRole() == RANGING_DEVICE_DT_TAG) {
496             // Setting default ranging rounds value.
497             DlTDoARangingRoundsUpdate rangingRounds =
498                     new DlTDoARangingRoundsUpdate.Builder()
499                             .setSessionId(openSessionParams.getSessionId())
500                             .setNoOfRangingRounds(1)
501                             .setRangingRoundIndexes(new byte[]{0})
502                             .build();
503             success =
504                     mOpAsyncCallbackRunner.execOperation(
505                             () -> mRangingSession.updateRangingRoundsDtTag(
506                                     rangingRounds.toBundle()),
507                             "Update ranging rounds for Dt Tag");
508         }
509 
510         success =
511                 mOpAsyncCallbackRunner.execOperation(
512                         () -> mRangingSession.start(new PersistableBundle()), "Start ranging");
513 
514         result = mOpAsyncCallbackRunner.getResult();
515         requireNonNull(mBackendCallbackExecutor);
516         if (!success || result == null || !result) {
517             mBackendCallbackExecutor.shutdown();
518             mBackendCallbackExecutor = null;
519         } else {
520             mRangingReportedAllowed = true;
521         }
522         return STATUS_OK;
523     }
524 
525     /** Stops ranging if the session is ranging. */
526     public synchronized int stopRanging() {
527         if (!isAlive()) {
528             Log.w(TAG, "UWB stopRanging called without an active session.");
529             return INVALID_API_CALL;
530         }
531         mRangingReportedAllowed = false;
532         if (mIsRanging) {
533             mOpAsyncCallbackRunner.execOperation(
534                     () -> requireNonNull(mRangingSession).stop(), "Stop Ranging");
535         } else {
536             Log.i(TAG, "UWB stopRanging called but isRanging is false.");
537         }
538 
539         boolean success =
540                 mOpAsyncCallbackRunner.execOperation(
541                         () -> requireNonNull(mRangingSession).close(), "Close Session");
542 
543         if (mBackendCallbackExecutor != null) {
544             mBackendCallbackExecutor.shutdown();
545             mBackendCallbackExecutor = null;
546         }
547         mLocalAddress = null;
548         mComplexChannel = null;
549         Boolean result = mOpAsyncCallbackRunner.getResult();
550         if (!success || result == null || !result) {
551             return UWB_SYSTEM_CALLBACK_FAILURE;
552         }
553         return STATUS_OK;
554     }
555 
556     /**
557      * Supports ranging configuration change. For example, a new peer is added to the active ranging
558      * session.
559      *
560      * @return returns true if the session is not active or reconfiguration is successful.
561      */
562     protected synchronized boolean reconfigureRanging(PersistableBundle bundle) {
563         boolean success =
564                 mOpAsyncCallbackRunner.execOperation(
565                         () -> mRangingSession.reconfigure(bundle), "Reconfigure Ranging");
566         Boolean result = mOpAsyncCallbackRunner.getResult();
567         return success && result != null && result;
568     }
569 
570     /**
571      * Adds a controlee to the active UWB ranging session.
572      *
573      * @return true if controlee was successfully added.
574      */
575     protected synchronized boolean addControlee(PersistableBundle bundle) {
576         boolean success =
577                 mOpAsyncCallbackRunner.execOperation(
578                         () -> mRangingSession.addControlee(bundle), "Add controlee");
579         Boolean result = mOpAsyncCallbackRunner.getResult();
580         return success && result != null && result;
581     }
582 
583     /**
584      * Removes a controlee from active UWB ranging session.
585      *
586      * @return true if controlee was successfully removed.
587      */
588     protected synchronized boolean removeControlee(PersistableBundle bundle) {
589         boolean success =
590                 mOpAsyncCallbackRunner.execOperation(
591                         () -> mRangingSession.removeControlee(bundle), "Remove controlee");
592         Boolean result = mOpAsyncCallbackRunner.getResult();
593         return success && result != null && result;
594     }
595 
596 
597     /**
598      * Reconfigures range data notification for an ongoing session.
599      *
600      * @return STATUS_OK if reconfigure was successful.
601      * @return UWB_RECONFIGURATION_FAILURE if reconfigure failed.
602      * @return INVALID_API_CALL if ranging session is not active.
603      */
604     public synchronized int reconfigureRangeDataNtfConfig(UwbRangeDataNtfConfig config) {
605         if (!isAlive()) {
606             Log.w(TAG, "Attempt to set range data notification while session is not active.");
607             return INVALID_API_CALL;
608         }
609 
610         boolean success =
611                 reconfigureRanging(
612                         ConfigurationManager.createReconfigureParamsRangeDataNtf(
613                                 config).toBundle());
614 
615         if (!success) {
616             Log.w(TAG, "Reconfiguring range data notification config failed.");
617             return UWB_RECONFIGURATION_FAILURE;
618         }
619         return STATUS_OK;
620     }
621 
622     /** Notifies that a ranging round failed. We collect this info for Analytics only. */
623     public interface RangingRoundFailureCallback {
624         /** Reports ranging round failed. */
625         void onRangingRoundFailed(UwbAddress peerAddress);
626     }
627 
628     /** Sets RangingRoundFailureCallback. */
629     public void setRangingRoundFailureCallback(
630             @Nullable RangingRoundFailureCallback rangingRoundFailureCallback) {
631         this.mRangingRoundFailureCallback = rangingRoundFailureCallback;
632     }
633 
634     /** Sets the system callback executor. */
635     public void setSystemCallbackExecutor(Executor executor) {
636         this.mSystemCallbackExecutor = executor;
637     }
638 }
639