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 com.android.adservices.service.devapi; 18 19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN; 20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__OVERRIDE_AD_SELECTION_CONFIG_REMOTE_INFO; 21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__REMOVE_AD_SELECTION_CONFIG_REMOTE_INFO_OVERRIDE; 22 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__RESET_ALL_AD_SELECTION_CONFIG_REMOTE_OVERRIDES; 23 24 import android.adservices.adselection.AdSelectionConfig; 25 import android.adservices.adselection.AdSelectionFromOutcomesConfig; 26 import android.adservices.adselection.AdSelectionOverrideCallback; 27 import android.adservices.adselection.PerBuyerDecisionLogic; 28 import android.adservices.common.AdSelectionSignals; 29 import android.adservices.common.AdServicesStatusUtils; 30 import android.adservices.common.FledgeErrorResponse; 31 import android.annotation.NonNull; 32 import android.content.pm.PackageManager; 33 import android.os.Build; 34 import android.os.RemoteException; 35 36 import androidx.annotation.RequiresApi; 37 38 import com.android.adservices.LoggerFactory; 39 import com.android.adservices.data.adselection.AdSelectionEntryDao; 40 import com.android.adservices.service.Flags; 41 import com.android.adservices.service.common.AppImportanceFilter; 42 import com.android.adservices.service.common.AppImportanceFilter.WrongCallingApplicationStateException; 43 import com.android.adservices.service.consent.AdServicesApiConsent; 44 import com.android.adservices.service.consent.AdServicesApiType; 45 import com.android.adservices.service.consent.ConsentManager; 46 import com.android.adservices.service.stats.AdServicesLogger; 47 48 import com.google.common.util.concurrent.FluentFuture; 49 import com.google.common.util.concurrent.FutureCallback; 50 import com.google.common.util.concurrent.ListeningExecutorService; 51 import com.google.common.util.concurrent.MoreExecutors; 52 53 import java.util.Objects; 54 import java.util.concurrent.ExecutorService; 55 56 /** Encapsulates the AdSelection Override Logic */ 57 @RequiresApi(Build.VERSION_CODES.S) 58 public class AdSelectionOverrider { 59 private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); 60 @NonNull private final AdSelectionEntryDao mAdSelectionEntryDao; 61 @NonNull private final ListeningExecutorService mLightweightExecutorService; 62 @NonNull private final ListeningExecutorService mBackgroundExecutorService; 63 @NonNull private final AdSelectionDevOverridesHelper mAdSelectionDevOverridesHelper; 64 @NonNull private final PackageManager mPackageManager; 65 @NonNull private final ConsentManager mConsentManager; 66 @NonNull private final AdServicesLogger mAdServicesLogger; 67 @NonNull private final Flags mFlags; 68 @NonNull private final AppImportanceFilter mAppImportanceFilter; 69 private final int mCallerUid; 70 @NonNull private final String mCallerAppPackageName; 71 72 /** 73 * Creates an instance of {@link AdSelectionOverrider} with the given {@link DevContext}, {@link 74 * AdSelectionEntryDao}, executor, and {@link AdSelectionDevOverridesHelper}. 75 */ AdSelectionOverrider( @onNull DevContext devContext, @NonNull AdSelectionEntryDao adSelectionEntryDao, @NonNull ExecutorService lightweightExecutorService, @NonNull ExecutorService backgroundExecutorService, @NonNull PackageManager packageManager, @NonNull ConsentManager consentManager, @NonNull AdServicesLogger adServicesLogger, @NonNull AppImportanceFilter appImportanceFilter, @NonNull final Flags flags, int callerUid)76 public AdSelectionOverrider( 77 @NonNull DevContext devContext, 78 @NonNull AdSelectionEntryDao adSelectionEntryDao, 79 @NonNull ExecutorService lightweightExecutorService, 80 @NonNull ExecutorService backgroundExecutorService, 81 @NonNull PackageManager packageManager, 82 @NonNull ConsentManager consentManager, 83 @NonNull AdServicesLogger adServicesLogger, 84 @NonNull AppImportanceFilter appImportanceFilter, 85 @NonNull final Flags flags, 86 int callerUid) { 87 Objects.requireNonNull(devContext); 88 Objects.requireNonNull(adSelectionEntryDao); 89 Objects.requireNonNull(lightweightExecutorService); 90 Objects.requireNonNull(backgroundExecutorService); 91 Objects.requireNonNull(packageManager); 92 Objects.requireNonNull(consentManager); 93 Objects.requireNonNull(adServicesLogger); 94 Objects.requireNonNull(appImportanceFilter); 95 Objects.requireNonNull(flags); 96 97 this.mAdSelectionEntryDao = adSelectionEntryDao; 98 this.mLightweightExecutorService = 99 MoreExecutors.listeningDecorator(lightweightExecutorService); 100 this.mBackgroundExecutorService = 101 MoreExecutors.listeningDecorator(backgroundExecutorService); 102 this.mAdSelectionDevOverridesHelper = 103 new AdSelectionDevOverridesHelper(devContext, mAdSelectionEntryDao); 104 this.mPackageManager = packageManager; 105 this.mConsentManager = consentManager; 106 this.mAdServicesLogger = adServicesLogger; 107 mFlags = flags; 108 mAppImportanceFilter = appImportanceFilter; 109 mCallerUid = callerUid; 110 mCallerAppPackageName = devContext.getCallingAppPackageName(); 111 } 112 113 /** 114 * Configures our fetching logic relating to {@code adSelectionConfig} to use {@code 115 * decisionLogicJS} instead of fetching from remote servers 116 * 117 * @param callback callback function to be called in case of success or failure 118 */ addOverride( @onNull AdSelectionConfig adSelectionConfig, @NonNull String decisionLogicJS, @NonNull AdSelectionSignals trustedScoringSignals, @NonNull PerBuyerDecisionLogic perBuyerDecisionLogic, @NonNull AdSelectionOverrideCallback callback)119 public void addOverride( 120 @NonNull AdSelectionConfig adSelectionConfig, 121 @NonNull String decisionLogicJS, 122 @NonNull AdSelectionSignals trustedScoringSignals, 123 @NonNull PerBuyerDecisionLogic perBuyerDecisionLogic, 124 @NonNull AdSelectionOverrideCallback callback) { 125 // Auto-generated variable name is too long for lint check 126 int shortApiName = 127 AD_SERVICES_API_CALLED__API_NAME__OVERRIDE_AD_SELECTION_CONFIG_REMOTE_INFO; 128 129 FluentFuture.from( 130 mLightweightExecutorService.submit( 131 () -> { 132 // Cannot read pH flags in the binder thread so this 133 // checks will be done in a spawn thread. 134 if (mFlags.getEnforceForegroundStatusForFledgeOverrides()) { 135 mAppImportanceFilter.assertCallerIsInForeground( 136 mCallerUid, shortApiName, null); 137 } 138 139 return null; 140 })) 141 .transformAsync( 142 ignoredVoid -> 143 callAddOverride( 144 adSelectionConfig, 145 decisionLogicJS, 146 trustedScoringSignals, 147 perBuyerDecisionLogic), 148 mLightweightExecutorService) 149 .addCallback( 150 new FutureCallback<Integer>() { 151 @Override 152 public void onSuccess(Integer result) { 153 sLogger.d("Add dev override for ad selection config succeeded!"); 154 invokeSuccess(callback, shortApiName, result); 155 } 156 157 @Override 158 public void onFailure(Throwable t) { 159 sLogger.e(t, "Add dev override for ad selection config failed!"); 160 notifyFailureToCaller(callback, t, shortApiName); 161 } 162 }, 163 mLightweightExecutorService); 164 } 165 166 /** 167 * Removes a decision logic override matching this {@code adSelectionConfig} and {@code 168 * appPackageName} 169 * 170 * @param callback callback function to be called in case of success or failure 171 */ removeOverride( @onNull AdSelectionConfig adSelectionConfig, @NonNull AdSelectionOverrideCallback callback)172 public void removeOverride( 173 @NonNull AdSelectionConfig adSelectionConfig, 174 @NonNull AdSelectionOverrideCallback callback) { 175 // Auto-generated variable name is too long for lint check 176 int shortApiName = 177 AD_SERVICES_API_CALLED__API_NAME__REMOVE_AD_SELECTION_CONFIG_REMOTE_INFO_OVERRIDE; 178 179 FluentFuture.from( 180 mLightweightExecutorService.submit( 181 () -> { 182 // Cannot read pH flags in the binder thread so this 183 // checks will be done in a spawn thread. 184 if (mFlags.getEnforceForegroundStatusForFledgeOverrides()) { 185 mAppImportanceFilter.assertCallerIsInForeground( 186 mCallerUid, shortApiName, null); 187 } 188 189 return null; 190 })) 191 .transformAsync( 192 ignoredVoid -> callRemoveOverride(adSelectionConfig), 193 mLightweightExecutorService) 194 .addCallback( 195 new FutureCallback<Integer>() { 196 @Override 197 public void onSuccess(Integer result) { 198 sLogger.d( 199 "Removing dev override for ad selection config succeeded!"); 200 invokeSuccess(callback, shortApiName, result); 201 } 202 203 @Override 204 public void onFailure(Throwable t) { 205 sLogger.e( 206 t, "Removing dev override for ad selection config failed!"); 207 notifyFailureToCaller(callback, t, shortApiName); 208 } 209 }, 210 mLightweightExecutorService); 211 } 212 213 /** 214 * Removes all ad selection overrides matching the {@code appPackageName} 215 * 216 * @param callback callback function to be called in case of success or failure 217 */ removeAllOverridesForAdSelectionConfig( @onNull AdSelectionOverrideCallback callback)218 public void removeAllOverridesForAdSelectionConfig( 219 @NonNull AdSelectionOverrideCallback callback) { 220 // Auto-generated variable name is too long for lint check 221 int shortApiName = 222 AD_SERVICES_API_CALLED__API_NAME__RESET_ALL_AD_SELECTION_CONFIG_REMOTE_OVERRIDES; 223 224 FluentFuture.from( 225 mLightweightExecutorService.submit( 226 () -> { 227 // Cannot read pH flags in the binder thread so this 228 // checks will be done in a spawn thread. 229 if (mFlags.getEnforceForegroundStatusForFledgeOverrides()) { 230 mAppImportanceFilter.assertCallerIsInForeground( 231 mCallerUid, shortApiName, null); 232 } 233 234 return null; 235 })) 236 .transformAsync( 237 ignoredVoid -> callRemoveAllOverrides(), mLightweightExecutorService) 238 .addCallback( 239 new FutureCallback<Integer>() { 240 @Override 241 public void onSuccess(Integer result) { 242 sLogger.d( 243 "Removing all dev overrides for ad selection config" 244 + " succeeded!"); 245 invokeSuccess(callback, shortApiName, result); 246 } 247 248 @Override 249 public void onFailure(Throwable t) { 250 sLogger.e( 251 t, 252 "Removing all dev overrides for ad selection config" 253 + " failed!"); 254 notifyFailureToCaller(callback, t, shortApiName); 255 } 256 }, 257 mLightweightExecutorService); 258 } 259 260 /** 261 * Configures our fetching logic relating to {@code config} to use {@code selectionLogicJs} 262 * instead of fetching from remote servers 263 * 264 * @param callback callback function to be called in case of success or failure 265 */ addOverride( @onNull AdSelectionFromOutcomesConfig config, @NonNull String selectionLogicJs, @NonNull AdSelectionSignals selectionSignals, @NonNull AdSelectionOverrideCallback callback)266 public void addOverride( 267 @NonNull AdSelectionFromOutcomesConfig config, 268 @NonNull String selectionLogicJs, 269 @NonNull AdSelectionSignals selectionSignals, 270 @NonNull AdSelectionOverrideCallback callback) { 271 // Auto-generated variable name is too long for lint check 272 int shortApiName = AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN; 273 274 FluentFuture.from( 275 mLightweightExecutorService.submit( 276 () -> { 277 // Cannot read pH flags in the binder thread so this 278 // checks will be done in a spawn thread. 279 if (mFlags.getEnforceForegroundStatusForFledgeOverrides()) { 280 mAppImportanceFilter.assertCallerIsInForeground( 281 mCallerUid, shortApiName, null); 282 } 283 return null; 284 })) 285 .transformAsync( 286 ignoredVoid -> callAddOverride(config, selectionLogicJs, selectionSignals), 287 mLightweightExecutorService) 288 .addCallback( 289 new FutureCallback<Integer>() { 290 @Override 291 public void onSuccess(Integer result) { 292 sLogger.d( 293 "Add dev override for ad selection config from outcomes" 294 + " succeeded!"); 295 invokeSuccess(callback, shortApiName, result); 296 } 297 298 @Override 299 public void onFailure(Throwable t) { 300 sLogger.e( 301 t, 302 "Add dev override for ad selection config from outcomes" 303 + " failed!"); 304 notifyFailureToCaller(callback, t, shortApiName); 305 } 306 }, 307 mLightweightExecutorService); 308 } 309 310 /** 311 * Removes a decision logic override matching this {@code config} and {@code appPackageName} 312 * 313 * @param callback callback function to be called in case of success or failure 314 */ removeOverride( @onNull AdSelectionFromOutcomesConfig config, @NonNull AdSelectionOverrideCallback callback)315 public void removeOverride( 316 @NonNull AdSelectionFromOutcomesConfig config, 317 @NonNull AdSelectionOverrideCallback callback) { 318 // Auto-generated variable name is too long for lint check 319 int shortApiName = AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN; 320 321 FluentFuture.from( 322 mLightweightExecutorService.submit( 323 () -> { 324 // Cannot read pH flags in the binder thread so this 325 // checks will be done in a spawn thread. 326 if (mFlags.getEnforceForegroundStatusForFledgeOverrides()) { 327 mAppImportanceFilter.assertCallerIsInForeground( 328 mCallerUid, shortApiName, null); 329 } 330 331 return null; 332 })) 333 .transformAsync( 334 ignoredVoid -> callRemoveOverride(config), mLightweightExecutorService) 335 .addCallback( 336 new FutureCallback<Integer>() { 337 @Override 338 public void onSuccess(Integer result) { 339 sLogger.d( 340 "Removing dev override for ad selection config from" 341 + " outcomes succeeded!"); 342 invokeSuccess(callback, shortApiName, result); 343 } 344 345 @Override 346 public void onFailure(Throwable t) { 347 sLogger.e( 348 t, 349 "Removing dev override for ad selection config from" 350 + " outcomes failed!"); 351 notifyFailureToCaller(callback, t, shortApiName); 352 } 353 }, 354 mLightweightExecutorService); 355 } 356 357 /** 358 * Removes all ad selection overrides matching the {@code appPackageName} 359 * 360 * @param callback callback function to be called in case of success or failure 361 */ removeAllOverridesForAdSelectionFromOutcomes( @onNull AdSelectionOverrideCallback callback)362 public void removeAllOverridesForAdSelectionFromOutcomes( 363 @NonNull AdSelectionOverrideCallback callback) { 364 // Auto-generated variable name is too long for lint check 365 int shortApiName = AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN; 366 367 FluentFuture.from( 368 mLightweightExecutorService.submit( 369 () -> { 370 // Cannot read pH flags in the binder thread so this 371 // checks will be done in a spawn thread. 372 if (mFlags.getEnforceForegroundStatusForFledgeOverrides()) { 373 mAppImportanceFilter.assertCallerIsInForeground( 374 mCallerUid, shortApiName, null); 375 } 376 377 return null; 378 })) 379 .transformAsync( 380 ignoredVoid -> callRemoveAllSelectionLogicOverrides(), 381 mLightweightExecutorService) 382 .addCallback( 383 new FutureCallback<Integer>() { 384 @Override 385 public void onSuccess(Integer result) { 386 sLogger.d( 387 "Removing all dev overrides for ad selection config from" 388 + " outcomes succeeded!"); 389 invokeSuccess(callback, shortApiName, result); 390 } 391 392 @Override 393 public void onFailure(Throwable t) { 394 sLogger.e( 395 t, 396 "Removing all dev overrides for ad selection config from" 397 + " outcomes failed!"); 398 notifyFailureToCaller(callback, t, shortApiName); 399 } 400 }, 401 mLightweightExecutorService); 402 } 403 callAddOverride( @onNull AdSelectionConfig adSelectionConfig, @NonNull String decisionLogicJS, @NonNull AdSelectionSignals trustedScoringSignals, @NonNull PerBuyerDecisionLogic perBuyerDecisionLogic)404 private FluentFuture<Integer> callAddOverride( 405 @NonNull AdSelectionConfig adSelectionConfig, 406 @NonNull String decisionLogicJS, 407 @NonNull AdSelectionSignals trustedScoringSignals, 408 @NonNull PerBuyerDecisionLogic perBuyerDecisionLogic) { 409 return FluentFuture.from( 410 mBackgroundExecutorService.submit( 411 () -> { 412 AdServicesApiConsent userConsent = getAdServicesApiConsent(); 413 414 if (!userConsent.isGiven()) { 415 return AdServicesStatusUtils.STATUS_USER_CONSENT_REVOKED; 416 } 417 418 mAdSelectionDevOverridesHelper.addAdSelectionSellerOverride( 419 adSelectionConfig, 420 decisionLogicJS, 421 trustedScoringSignals, 422 perBuyerDecisionLogic); 423 return AdServicesStatusUtils.STATUS_SUCCESS; 424 })); 425 } 426 callRemoveOverride(@onNull AdSelectionConfig adSelectionConfig)427 private FluentFuture<Integer> callRemoveOverride(@NonNull AdSelectionConfig adSelectionConfig) { 428 return FluentFuture.from( 429 mBackgroundExecutorService.submit( 430 () -> { 431 AdServicesApiConsent userConsent = getAdServicesApiConsent(); 432 433 if (!userConsent.isGiven()) { 434 return AdServicesStatusUtils.STATUS_USER_CONSENT_REVOKED; 435 } 436 437 mAdSelectionDevOverridesHelper.removeAdSelectionSellerOverride( 438 adSelectionConfig); 439 return AdServicesStatusUtils.STATUS_SUCCESS; 440 })); 441 } 442 443 private FluentFuture<Integer> callRemoveAllOverrides() { 444 return FluentFuture.from( 445 mBackgroundExecutorService.submit( 446 () -> { 447 AdServicesApiConsent userConsent = getAdServicesApiConsent(); 448 449 if (!userConsent.isGiven()) { 450 return AdServicesStatusUtils.STATUS_USER_CONSENT_REVOKED; 451 } 452 453 mAdSelectionDevOverridesHelper.removeAllDecisionLogicOverrides(); 454 return AdServicesStatusUtils.STATUS_SUCCESS; 455 })); 456 } 457 458 private FluentFuture<Integer> callAddOverride( 459 @NonNull AdSelectionFromOutcomesConfig config, 460 @NonNull String selectionLogicJs, 461 @NonNull AdSelectionSignals selectionSignals) { 462 return FluentFuture.from( 463 mBackgroundExecutorService.submit( 464 () -> { 465 AdServicesApiConsent userConsent = getAdServicesApiConsent(); 466 467 if (!userConsent.isGiven()) { 468 return AdServicesStatusUtils.STATUS_USER_CONSENT_REVOKED; 469 } 470 471 mAdSelectionDevOverridesHelper.addAdSelectionOutcomeSelectorOverride( 472 config, selectionLogicJs, selectionSignals); 473 return AdServicesStatusUtils.STATUS_SUCCESS; 474 })); 475 } 476 477 private FluentFuture<Integer> callRemoveOverride( 478 @NonNull AdSelectionFromOutcomesConfig config) { 479 return FluentFuture.from( 480 mBackgroundExecutorService.submit( 481 () -> { 482 AdServicesApiConsent userConsent = getAdServicesApiConsent(); 483 484 if (!userConsent.isGiven()) { 485 return AdServicesStatusUtils.STATUS_USER_CONSENT_REVOKED; 486 } 487 488 mAdSelectionDevOverridesHelper.removeAdSelectionOutcomeSelectorOverride( 489 config); 490 return AdServicesStatusUtils.STATUS_SUCCESS; 491 })); 492 } 493 494 private FluentFuture<Integer> callRemoveAllSelectionLogicOverrides() { 495 return FluentFuture.from( 496 mBackgroundExecutorService.submit( 497 () -> { 498 AdServicesApiConsent userConsent = getAdServicesApiConsent(); 499 500 if (!userConsent.isGiven()) { 501 return AdServicesStatusUtils.STATUS_USER_CONSENT_REVOKED; 502 } 503 504 mAdSelectionDevOverridesHelper.removeAllSelectionLogicOverrides(); 505 return AdServicesStatusUtils.STATUS_SUCCESS; 506 })); 507 } 508 509 private AdServicesApiConsent getAdServicesApiConsent() { 510 AdServicesApiConsent userConsent = mConsentManager.getConsent(AdServicesApiType.FLEDGE); 511 512 return userConsent; 513 } 514 515 /** Invokes the onFailure function from the callback and handles the exception. */ 516 private void invokeFailure( 517 @NonNull AdSelectionOverrideCallback callback, 518 int statusCode, 519 String errorMessage, 520 int apiName) { 521 int resultCode = statusCode; 522 try { 523 callback.onFailure( 524 new FledgeErrorResponse.Builder() 525 .setStatusCode(statusCode) 526 .setErrorMessage(errorMessage) 527 .build()); 528 } catch (RemoteException e) { 529 sLogger.e(e, "Unable to send failed result to the callback"); 530 resultCode = AdServicesStatusUtils.STATUS_UNKNOWN_ERROR; 531 throw e.rethrowFromSystemServer(); 532 } finally { 533 mAdServicesLogger.logFledgeApiCallStats( 534 apiName, mCallerAppPackageName, resultCode, /*latencyMs=*/ 0); 535 } 536 } 537 538 /** Invokes the onSuccess function from the callback and handles the exception. */ 539 private void invokeSuccess( 540 @NonNull AdSelectionOverrideCallback callback, int apiName, Integer resultCode) { 541 int resultCodeInt = AdServicesStatusUtils.STATUS_UNSET; 542 if (resultCode != null) { 543 resultCodeInt = resultCode; 544 } 545 try { 546 callback.onSuccess(); 547 } catch (RemoteException e) { 548 sLogger.e(e, "Unable to send successful result to the callback"); 549 resultCodeInt = AdServicesStatusUtils.STATUS_UNKNOWN_ERROR; 550 throw e.rethrowFromSystemServer(); 551 } finally { 552 mAdServicesLogger.logFledgeApiCallStats( 553 apiName, mCallerAppPackageName, resultCodeInt, /*latencyMs=*/ 0); 554 } 555 } 556 557 private void notifyFailureToCaller( 558 @NonNull AdSelectionOverrideCallback callback, @NonNull Throwable t, int apiName) { 559 if (t instanceof IllegalArgumentException) { 560 invokeFailure( 561 callback, 562 AdServicesStatusUtils.STATUS_INVALID_ARGUMENT, 563 t.getMessage(), 564 apiName); 565 } else if (t instanceof WrongCallingApplicationStateException) { 566 invokeFailure( 567 callback, 568 AdServicesStatusUtils.STATUS_BACKGROUND_CALLER, 569 t.getMessage(), 570 apiName); 571 } else if (t instanceof IllegalStateException) { 572 invokeFailure( 573 callback, AdServicesStatusUtils.STATUS_INTERNAL_ERROR, t.getMessage(), apiName); 574 } else { 575 invokeFailure( 576 callback, AdServicesStatusUtils.STATUS_UNAUTHORIZED, t.getMessage(), apiName); 577 } 578 } 579 } 580