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