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 android.adservices.ondevicepersonalization;
18 
19 import android.adservices.ondevicepersonalization.aidl.IDataAccessService;
20 import android.adservices.ondevicepersonalization.aidl.IFederatedComputeService;
21 import android.adservices.ondevicepersonalization.aidl.IIsolatedModelService;
22 import android.adservices.ondevicepersonalization.aidl.IIsolatedService;
23 import android.adservices.ondevicepersonalization.aidl.IIsolatedServiceCallback;
24 import android.annotation.FlaggedApi;
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.app.Service;
28 import android.content.Intent;
29 import android.os.Binder;
30 import android.os.Bundle;
31 import android.os.IBinder;
32 import android.os.OutcomeReceiver;
33 import android.os.Parcelable;
34 import android.os.RemoteException;
35 import android.os.SystemClock;
36 
37 import com.android.adservices.ondevicepersonalization.flags.Flags;
38 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
39 import com.android.ondevicepersonalization.internal.util.OdpParceledListSlice;
40 
41 import java.util.Objects;
42 import java.util.function.Function;
43 
44 // TODO(b/289102463): Add a link to the public ODP developer documentation.
45 /**
46  * Base class for services that are started by ODP on a call to
47  * {@code OnDevicePersonalizationManager#execute(ComponentName, PersistableBundle,
48  * java.util.concurrent.Executor, OutcomeReceiver)}
49  * and run in an <a
50  * href="https://developer.android.com/guide/topics/manifest/service-element#isolated">isolated
51  * process</a>. The service can produce content to be displayed in a
52  * {@link android.view.SurfaceView} in a calling app and write persistent results to on-device
53  * storage, which can be consumed by Federated Analytics for cross-device statistical analysis or
54  * by Federated Learning for model training.
55  * Client apps use {@link OnDevicePersonalizationManager} to interact with an {@link
56  * IsolatedService}.
57  */
58 @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
59 public abstract class IsolatedService extends Service {
60     private static final String TAG = IsolatedService.class.getSimpleName();
61     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
62     private IBinder mBinder;
63 
64     /** Creates a binder for an {@link IsolatedService}. */
65     @Override
onCreate()66     public void onCreate() {
67         mBinder = new ServiceBinder();
68     }
69 
70     /**
71      * Handles binding to the {@link IsolatedService}.
72      *
73      * @param intent The Intent that was used to bind to this service, as given to {@link
74      *     android.content.Context#bindService Context.bindService}. Note that any extras that were
75      *     included with the Intent at that point will <em>not</em> be seen here.
76      */
77     @Override
78     @Nullable
onBind(@onNull Intent intent)79     public IBinder onBind(@NonNull Intent intent) {
80         return mBinder;
81     }
82 
83     /**
84      * Return an instance of an {@link IsolatedWorker} that handles client requests.
85      *
86      * @param requestToken an opaque token that identifies the current request to the service that
87      *     must be passed to service methods that depend on per-request state.
88      */
89     @NonNull
onRequest(@onNull RequestToken requestToken)90     public abstract IsolatedWorker onRequest(@NonNull RequestToken requestToken);
91 
92     /**
93      * Returns a Data Access Object for the REMOTE_DATA table. The REMOTE_DATA table is a read-only
94      * key-value store that contains data that is periodically downloaded from an endpoint declared
95      * in the <download> tag in the ODP manifest of the service, as shown in the following example.
96      *
97      * <pre>{@code
98      * <!-- Contents of res/xml/OdpSettings.xml -->
99      * <on-device-personalization>
100      * <!-- Name of the service subclass -->
101      * <service "com.example.odpsample.SampleService">
102      *   <!-- If this tag is present, ODP will periodically poll this URL and
103      *    download content to populate REMOTE_DATA. Adopters that do not need to
104      *    download content from their servers can skip this tag. -->
105      *   <download-settings url="https://example.com/get" />
106      * </service>
107      * </on-device-personalization>
108      * }</pre>
109      *
110      * @param requestToken an opaque token that identifies the current request to the service.
111      * @return A {@link KeyValueStore} object that provides access to the REMOTE_DATA table. The
112      *     methods in the returned {@link KeyValueStore} are blocking operations and should be
113      *     called from a worker thread and not the main thread or a binder thread.
114      * @see #onRequest(RequestToken)
115      */
116     @NonNull
getRemoteData(@onNull RequestToken requestToken)117     public final KeyValueStore getRemoteData(@NonNull RequestToken requestToken) {
118         return new RemoteDataImpl(requestToken.getDataAccessService());
119     }
120 
121     /**
122      * Returns a Data Access Object for the LOCAL_DATA table. The LOCAL_DATA table is a persistent
123      * key-value store that the service can use to store any data. The contents of this table are
124      * visible only to the service running in an isolated process and cannot be sent outside the
125      * device.
126      *
127      * @param requestToken an opaque token that identifies the current request to the service.
128      * @return A {@link MutableKeyValueStore} object that provides access to the LOCAL_DATA table.
129      *     The methods in the returned {@link MutableKeyValueStore} are blocking operations and
130      *     should be called from a worker thread and not the main thread or a binder thread.
131      * @see #onRequest(RequestToken)
132      */
133     @NonNull
getLocalData(@onNull RequestToken requestToken)134     public final MutableKeyValueStore getLocalData(@NonNull RequestToken requestToken) {
135         return new LocalDataImpl(requestToken.getDataAccessService());
136     }
137 
138     /**
139      * Returns a DAO for the REQUESTS and EVENTS tables that provides
140      * access to the rows that are readable by the IsolatedService.
141      *
142      * @param requestToken an opaque token that identifies the current request to the service.
143      * @return A {@link LogReader} object that provides access to the REQUESTS and EVENTS table.
144      *     The methods in the returned {@link LogReader} are blocking operations and
145      *     should be called from a worker thread and not the main thread or a binder thread.
146      * @see #onRequest(RequestToken)
147      */
148     @NonNull
getLogReader(@onNull RequestToken requestToken)149     public final LogReader getLogReader(@NonNull RequestToken requestToken) {
150         return new LogReader(requestToken.getDataAccessService());
151     }
152 
153     /**
154      * Returns an {@link EventUrlProvider} for the current request. The {@link EventUrlProvider}
155      * provides URLs that can be embedded in HTML. When the HTML is rendered in an
156      * {@link android.webkit.WebView}, the platform intercepts requests to these URLs and invokes
157      * {@code IsolatedWorker#onEvent(EventInput, Consumer)}.
158      *
159      * @param requestToken an opaque token that identifies the current request to the service.
160      * @return An {@link EventUrlProvider} that returns event tracking URLs.
161      * @see #onRequest(RequestToken)
162      */
163     @NonNull
getEventUrlProvider(@onNull RequestToken requestToken)164     public final EventUrlProvider getEventUrlProvider(@NonNull RequestToken requestToken) {
165         return new EventUrlProvider(requestToken.getDataAccessService());
166     }
167 
168     /**
169      * Returns the platform-provided {@link UserData} for the current request.
170      *
171      * @param requestToken an opaque token that identifies the current request to the service.
172      * @return A {@link UserData} object.
173      * @see #onRequest(RequestToken)
174      */
175     @Nullable
getUserData(@onNull RequestToken requestToken)176     public final UserData getUserData(@NonNull RequestToken requestToken) {
177         return requestToken.getUserData();
178     }
179 
180     /**
181      * Returns an {@link FederatedComputeScheduler} for the current request. The {@link
182      * FederatedComputeScheduler} can be used to schedule and cancel federated computation jobs.
183      * The federated computation includes federated learning and federated analytic jobs.
184      *
185      * @param requestToken an opaque token that identifies the current request to the service.
186      * @return An {@link FederatedComputeScheduler} that returns a federated computation job
187      *     scheduler.
188      * @see #onRequest(RequestToken)
189      */
190     @NonNull
getFederatedComputeScheduler( @onNull RequestToken requestToken)191     public final FederatedComputeScheduler getFederatedComputeScheduler(
192             @NonNull RequestToken requestToken) {
193         return new FederatedComputeScheduler(
194                 requestToken.getFederatedComputeService(),
195                 requestToken.getDataAccessService());
196     }
197 
198     /**
199      * Returns an {@link ModelManager} for the current request. The {@link ModelManager} can be used
200      * to do model inference. It only supports Tensorflow Lite model inference now.
201      *
202      * @param requestToken an opaque token that identifies the current request to the service.
203      * @return An {@link ModelManager} that can be used for model inference.
204      */
205     @NonNull
getModelManager(@onNull RequestToken requestToken)206     public final ModelManager getModelManager(@NonNull RequestToken requestToken) {
207         return new ModelManager(
208                 requestToken.getDataAccessService(), requestToken.getModelService());
209     }
210 
211     // TODO(b/228200518): Add onBidRequest()/onBidResponse() methods.
212 
213     class ServiceBinder extends IIsolatedService.Stub {
214         @Override
onRequest( int operationCode, @NonNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback)215         public void onRequest(
216                 int operationCode,
217                 @NonNull Bundle params,
218                 @NonNull IIsolatedServiceCallback resultCallback) {
219             Objects.requireNonNull(params);
220             Objects.requireNonNull(resultCallback);
221             final long token = Binder.clearCallingIdentity();
222             // TODO(b/228200518): Ensure that caller is ODP Service.
223             // TODO(b/323592348): Add model inference in other flows.
224             try {
225                 performRequest(operationCode, params, resultCallback);
226             } finally {
227                 Binder.restoreCallingIdentity(token);
228             }
229         }
230 
performRequest( int operationCode, @NonNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback)231         private void performRequest(
232                 int operationCode,
233                 @NonNull Bundle params,
234                 @NonNull IIsolatedServiceCallback resultCallback) {
235 
236             if (operationCode == Constants.OP_EXECUTE) {
237                 performExecute(params, resultCallback);
238             } else if (operationCode == Constants.OP_DOWNLOAD) {
239                 performDownload(params, resultCallback);
240             } else if (operationCode == Constants.OP_RENDER) {
241                 performRender(params, resultCallback);
242             } else if (operationCode == Constants.OP_WEB_VIEW_EVENT) {
243                 performOnWebViewEvent(params, resultCallback);
244             } else if (operationCode == Constants.OP_TRAINING_EXAMPLE) {
245                 performOnTrainingExample(params, resultCallback);
246             } else if (operationCode == Constants.OP_WEB_TRIGGER) {
247                 performOnWebTrigger(params, resultCallback);
248             } else {
249                 throw new IllegalArgumentException("Invalid op code: " + operationCode);
250             }
251         }
252 
performOnWebTrigger( @onNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback)253         private void performOnWebTrigger(
254                 @NonNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback) {
255             try {
256                 WebTriggerInputParcel inputParcel =
257                         Objects.requireNonNull(
258                                 params.getParcelable(
259                                         Constants.EXTRA_INPUT, WebTriggerInputParcel.class),
260                                 () ->
261                                         String.format(
262                                                 "Missing '%s' from input params!",
263                                                 Constants.EXTRA_INPUT));
264                 WebTriggerInput input = new WebTriggerInput(inputParcel);
265                 IDataAccessService binder = getDataAccessService(params);
266                 UserData userData = params.getParcelable(Constants.EXTRA_USER_DATA, UserData.class);
267                 RequestToken requestToken = new RequestToken(binder, null, null, userData);
268                 IsolatedWorker isolatedWorker = IsolatedService.this.onRequest(requestToken);
269                 isolatedWorker.onWebTrigger(
270                         input,
271                         new WrappedCallback<WebTriggerOutput, WebTriggerOutputParcel>(
272                                 resultCallback, requestToken, v -> new WebTriggerOutputParcel(v)));
273             } catch (Exception e) {
274                 sLogger.e(e, TAG + ": Exception during Isolated Service web trigger operation.");
275                 try {
276                     resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
277                 } catch (RemoteException re) {
278                     sLogger.e(re, TAG + ": Isolated Service Callback failed.");
279                 }
280             }
281         }
282 
performOnTrainingExample( @onNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback)283         private void performOnTrainingExample(
284                 @NonNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback) {
285             try {
286                 TrainingExamplesInputParcel inputParcel =
287                         Objects.requireNonNull(
288                                 params.getParcelable(
289                                         Constants.EXTRA_INPUT, TrainingExamplesInputParcel.class),
290                                 () ->
291                                         String.format(
292                                                 "Missing '%s' from input params!",
293                                                 Constants.EXTRA_INPUT));
294                 TrainingExamplesInput input = new TrainingExamplesInput(inputParcel);
295                 IDataAccessService binder = getDataAccessService(params);
296                 UserData userData = params.getParcelable(Constants.EXTRA_USER_DATA, UserData.class);
297                 RequestToken requestToken = new RequestToken(binder, null, null, userData);
298                 IsolatedWorker isolatedWorker = IsolatedService.this.onRequest(requestToken);
299                 isolatedWorker.onTrainingExamples(
300                         input,
301                         new WrappedCallback<TrainingExamplesOutput, TrainingExamplesOutputParcel>(
302                                 resultCallback,
303                                 requestToken,
304                                 v ->
305                                         new TrainingExamplesOutputParcel.Builder()
306                                                 .setTrainingExampleRecords(
307                                                         new OdpParceledListSlice<
308                                                                 TrainingExampleRecord>(
309                                                                 v.getTrainingExampleRecords()))
310                                                 .build()));
311             } catch (Exception e) {
312                 sLogger.e(e,
313                         TAG + ": Exception during Isolated Service training example operation.");
314                 try {
315                     resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
316                 } catch (RemoteException re) {
317                     sLogger.e(re, TAG + ": Isolated Service Callback failed.");
318                 }
319             }
320         }
321 
performOnWebViewEvent( @onNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback)322         private void performOnWebViewEvent(
323                 @NonNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback) {
324             try {
325                 EventInputParcel inputParcel =
326                         Objects.requireNonNull(
327                                 params.getParcelable(Constants.EXTRA_INPUT, EventInputParcel.class),
328                                 () ->
329                                         String.format(
330                                                 "Missing '%s' from input params!",
331                                                 Constants.EXTRA_INPUT));
332                 EventInput input = new EventInput(inputParcel);
333                 IDataAccessService binder = getDataAccessService(params);
334                 UserData userData = params.getParcelable(Constants.EXTRA_USER_DATA, UserData.class);
335                 RequestToken requestToken = new RequestToken(binder, null, null, userData);
336                 IsolatedWorker isolatedWorker = IsolatedService.this.onRequest(requestToken);
337                 isolatedWorker.onEvent(
338                         input,
339                         new WrappedCallback<EventOutput, EventOutputParcel>(
340                                 resultCallback, requestToken, v -> new EventOutputParcel(v)));
341             } catch (Exception e) {
342                 sLogger.e(e, TAG + ": Exception during Isolated Service web view event operation.");
343                 try {
344                     resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
345                 } catch (RemoteException re) {
346                     sLogger.e(re, TAG + ": Isolated Service Callback failed.");
347                 }
348             }
349         }
350 
performRender( @onNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback)351         private void performRender(
352                 @NonNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback) {
353             try {
354                 RenderInputParcel inputParcel =
355                         Objects.requireNonNull(
356                                 params.getParcelable(
357                                         Constants.EXTRA_INPUT, RenderInputParcel.class),
358                                 () ->
359                                         String.format(
360                                                 "Missing '%s' from input params!",
361                                                 Constants.EXTRA_INPUT));
362                 RenderInput input = new RenderInput(inputParcel);
363                 Objects.requireNonNull(input.getRenderingConfig());
364                 IDataAccessService binder = getDataAccessService(params);
365                 RequestToken requestToken = new RequestToken(binder, null, null, null);
366                 IsolatedWorker isolatedWorker = IsolatedService.this.onRequest(requestToken);
367                 isolatedWorker.onRender(
368                         input,
369                         new WrappedCallback<RenderOutput, RenderOutputParcel>(
370                                 resultCallback, requestToken, v -> new RenderOutputParcel(v)));
371             } catch (Exception e) {
372                 sLogger.e(e, TAG + ": Exception during Isolated Service render operation.");
373                 try {
374                     resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
375                 } catch (RemoteException re) {
376                     sLogger.e(re, TAG + ": Isolated Service Callback failed.");
377                 }
378             }
379         }
380 
performDownload( @onNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback)381         private void performDownload(
382                 @NonNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback) {
383             try {
384                 DownloadInputParcel inputParcel =
385                         Objects.requireNonNull(
386                                 params.getParcelable(
387                                         Constants.EXTRA_INPUT, DownloadInputParcel.class),
388                                 () ->
389                                         String.format(
390                                                 "Missing '%s' from input params!",
391                                                 Constants.EXTRA_INPUT));
392                 KeyValueStore downloadedContents =
393                         new RemoteDataImpl(
394                                 IDataAccessService.Stub.asInterface(
395                                         Objects.requireNonNull(
396                                                 inputParcel.getDataAccessServiceBinder(),
397                             "Failed to get IDataAccessService binder from the input params!")));
398 
399                 DownloadCompletedInput input =
400                         new DownloadCompletedInput.Builder()
401                                 .setDownloadedContents(downloadedContents)
402                                 .build();
403 
404                 IDataAccessService binder = getDataAccessService(params);
405 
406                 IFederatedComputeService fcBinder = getFederatedComputeService(params);
407                 UserData userData = params.getParcelable(Constants.EXTRA_USER_DATA, UserData.class);
408                 RequestToken requestToken = new RequestToken(binder, fcBinder, null, userData);
409                 IsolatedWorker isolatedWorker = IsolatedService.this.onRequest(requestToken);
410                 isolatedWorker.onDownloadCompleted(
411                         input,
412                         new WrappedCallback<DownloadCompletedOutput, DownloadCompletedOutputParcel>(
413                                 resultCallback,
414                                 requestToken,
415                                 v -> new DownloadCompletedOutputParcel(v)));
416             } catch (Exception e) {
417                 sLogger.e(e, TAG + ": Exception during Isolated Service download operation.");
418                 try {
419                     resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
420                 } catch (RemoteException re) {
421                     sLogger.e(re, TAG + ": Isolated Service Callback failed.");
422                 }
423             }
424         }
425 
getIsolatedModelService(@onNull Bundle params)426         private static IIsolatedModelService getIsolatedModelService(@NonNull Bundle params) {
427             IIsolatedModelService modelServiceBinder =
428                     IIsolatedModelService.Stub.asInterface(
429                             Objects.requireNonNull(
430                                     params.getBinder(Constants.EXTRA_MODEL_SERVICE_BINDER),
431                                     () ->
432                                             String.format(
433                                                     "Missing '%s' from input params!",
434                                                     Constants.EXTRA_MODEL_SERVICE_BINDER)));
435             Objects.requireNonNull(
436                     modelServiceBinder,
437                     "Failed to get IIsolatedModelService binder from the input params!");
438             return modelServiceBinder;
439         }
440 
getFederatedComputeService(@onNull Bundle params)441         private static IFederatedComputeService getFederatedComputeService(@NonNull Bundle params) {
442             IFederatedComputeService fcBinder =
443                     IFederatedComputeService.Stub.asInterface(
444                             Objects.requireNonNull(
445                                     params.getBinder(
446                                             Constants.EXTRA_FEDERATED_COMPUTE_SERVICE_BINDER),
447                                     () ->
448                                             String.format(
449                                                     "Missing '%s' from input params!",
450                                                     Constants
451                                                         .EXTRA_FEDERATED_COMPUTE_SERVICE_BINDER)));
452             Objects.requireNonNull(
453                     fcBinder,
454                     "Failed to get IFederatedComputeService binder from the input params!");
455             return fcBinder;
456         }
457 
getDataAccessService(@onNull Bundle params)458         private static IDataAccessService getDataAccessService(@NonNull Bundle params) {
459             IDataAccessService binder =
460                     IDataAccessService.Stub.asInterface(
461                             Objects.requireNonNull(
462                                     params.getBinder(Constants.EXTRA_DATA_ACCESS_SERVICE_BINDER),
463                                     () ->
464                                             String.format(
465                                                     "Missing '%s' from input params!",
466                                                     Constants.EXTRA_DATA_ACCESS_SERVICE_BINDER)));
467             Objects.requireNonNull(
468                     binder, "Failed to get IDataAccessService binder from the input params!");
469             return binder;
470         }
471 
performExecute( @onNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback)472         private void performExecute(
473                 @NonNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback) {
474             try {
475                 ExecuteInputParcel inputParcel =
476                         Objects.requireNonNull(
477                                 params.getParcelable(
478                                         Constants.EXTRA_INPUT, ExecuteInputParcel.class),
479                                 () ->
480                                         String.format(
481                                                 "Missing '%s' from input params!",
482                                                 Constants.EXTRA_INPUT));
483                 ExecuteInput input = new ExecuteInput(inputParcel);
484                 Objects.requireNonNull(
485                         input.getAppPackageName(),
486                         "Failed to get AppPackageName from the input params!");
487                 IDataAccessService binder = getDataAccessService(params);
488                 IFederatedComputeService fcBinder = getFederatedComputeService(params);
489                 IIsolatedModelService modelServiceBinder = getIsolatedModelService(params);
490                 UserData userData = params.getParcelable(Constants.EXTRA_USER_DATA, UserData.class);
491                 RequestToken requestToken =
492                         new RequestToken(binder, fcBinder, modelServiceBinder, userData);
493                 IsolatedWorker isolatedWorker = IsolatedService.this.onRequest(requestToken);
494                 isolatedWorker.onExecute(
495                         input,
496                         new WrappedCallback<ExecuteOutput, ExecuteOutputParcel>(
497                                 resultCallback, requestToken, v -> new ExecuteOutputParcel(v)));
498             } catch (Exception e) {
499                 sLogger.e(e, TAG + ": Exception during Isolated Service execute operation.");
500                 try {
501                     resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
502                 } catch (RemoteException re) {
503                     sLogger.e(re, TAG + ": Isolated Service Callback failed.");
504                 }
505             }
506         }
507     }
508 
509     private static class WrappedCallback<T, U extends Parcelable>
510                 implements OutcomeReceiver<T, IsolatedServiceException> {
511         @NonNull private final IIsolatedServiceCallback mCallback;
512         @NonNull private final RequestToken mRequestToken;
513         @NonNull private final Function<T, U> mConverter;
514 
WrappedCallback( IIsolatedServiceCallback callback, RequestToken requestToken, Function<T, U> converter)515         WrappedCallback(
516                 IIsolatedServiceCallback callback,
517                 RequestToken requestToken,
518                 Function<T, U> converter) {
519             mCallback = Objects.requireNonNull(callback);
520             mRequestToken = Objects.requireNonNull(requestToken);
521             mConverter = Objects.requireNonNull(converter);
522         }
523 
524         @Override
onResult(T result)525         public void onResult(T result) {
526             long elapsedTimeMillis =
527                     SystemClock.elapsedRealtime() - mRequestToken.getStartTimeMillis();
528             if (result == null) {
529                 try {
530                     mCallback.onError(Constants.STATUS_SERVICE_FAILED, 0);
531                 } catch (RemoteException e) {
532                     sLogger.w(TAG + ": Callback failed.", e);
533                 }
534             } else {
535                 Bundle bundle = new Bundle();
536                 U wrappedResult = mConverter.apply(result);
537                 bundle.putParcelable(Constants.EXTRA_RESULT, wrappedResult);
538                 bundle.putParcelable(Constants.EXTRA_CALLEE_METADATA,
539                         new CalleeMetadata.Builder()
540                             .setElapsedTimeMillis(elapsedTimeMillis)
541                             .build());
542                 try {
543                     mCallback.onSuccess(bundle);
544                 } catch (RemoteException e) {
545                     sLogger.w(TAG + ": Callback failed.", e);
546                 }
547             }
548         }
549 
550         @Override
onError(IsolatedServiceException e)551         public void onError(IsolatedServiceException e) {
552             try {
553                 // TODO(b/324478256): Log and report the error code from e.
554                 mCallback.onError(Constants.STATUS_SERVICE_FAILED, e.getErrorCode());
555             } catch (RemoteException re) {
556                 sLogger.w(TAG + ": Callback failed.", re);
557             }
558         }
559     }
560 }
561