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