1 /* 2 * Copyright (C) 2018 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.server.connectivity; 18 19 import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_HANDOVER; 20 import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY; 21 import static android.net.ConnectivityManager.TYPE_MOBILE; 22 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; 23 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; 24 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; 25 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; 26 import static android.net.NetworkPolicy.LIMIT_DISABLED; 27 import static android.net.NetworkPolicy.WARNING_DISABLED; 28 import static android.net.NetworkTemplate.NETWORK_TYPE_ALL; 29 import static android.net.NetworkTemplate.OEM_MANAGED_ALL; 30 import static android.net.NetworkTemplate.SUBSCRIBER_ID_MATCH_RULE_EXACT; 31 import static android.provider.Settings.Global.NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES; 32 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; 33 34 import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH; 35 import static com.android.server.net.NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN; 36 37 import android.app.usage.NetworkStatsManager; 38 import android.app.usage.NetworkStatsManager.UsageCallback; 39 import android.content.BroadcastReceiver; 40 import android.content.ContentResolver; 41 import android.content.Context; 42 import android.content.Intent; 43 import android.content.IntentFilter; 44 import android.database.ContentObserver; 45 import android.net.ConnectivityManager; 46 import android.net.ConnectivityManager.NetworkCallback; 47 import android.net.Network; 48 import android.net.NetworkCapabilities; 49 import android.net.NetworkIdentity; 50 import android.net.NetworkPolicy; 51 import android.net.NetworkPolicyManager; 52 import android.net.NetworkRequest; 53 import android.net.NetworkSpecifier; 54 import android.net.NetworkStats; 55 import android.net.NetworkTemplate; 56 import android.net.TelephonyNetworkSpecifier; 57 import android.net.Uri; 58 import android.os.BestClock; 59 import android.os.Handler; 60 import android.os.SystemClock; 61 import android.os.UserHandle; 62 import android.provider.Settings; 63 import android.telephony.TelephonyManager; 64 import android.util.DebugUtils; 65 import android.util.Log; 66 import android.util.Range; 67 68 import com.android.internal.R; 69 import com.android.internal.annotations.VisibleForTesting; 70 import com.android.internal.util.IndentingPrintWriter; 71 import com.android.server.LocalServices; 72 import com.android.server.net.NetworkPolicyManagerInternal; 73 import com.android.server.net.NetworkStatsManagerInternal; 74 75 import java.time.Clock; 76 import java.time.ZoneId; 77 import java.time.ZoneOffset; 78 import java.time.ZonedDateTime; 79 import java.time.temporal.ChronoUnit; 80 import java.util.concurrent.ConcurrentHashMap; 81 import java.util.concurrent.TimeUnit; 82 83 /** 84 * Manages multipath data budgets. 85 * 86 * Informs the return value of ConnectivityManager#getMultipathPreference() based on: 87 * - The user's data plan, as returned by getSubscriptionOpportunisticQuota(). 88 * - The amount of data usage that occurs on mobile networks while they are not the system default 89 * network (i.e., when the app explicitly selected such networks). 90 * 91 * Currently, quota is determined on a daily basis, from midnight to midnight local time. 92 * 93 * @hide 94 */ 95 public class MultipathPolicyTracker { 96 private static String TAG = MultipathPolicyTracker.class.getSimpleName(); 97 98 private static final boolean DBG = false; 99 100 // This context is for the current user. 101 private final Context mContext; 102 // This context is for all users, so register a BroadcastReceiver which can receive intents from 103 // all users. 104 private final Context mUserAllContext; 105 private final Handler mHandler; 106 private final Clock mClock; 107 private final Dependencies mDeps; 108 private final ContentResolver mResolver; 109 private final ConfigChangeReceiver mConfigChangeReceiver; 110 111 @VisibleForTesting 112 final ContentObserver mSettingsObserver; 113 114 private ConnectivityManager mCM; 115 private NetworkPolicyManager mNPM; 116 private NetworkStatsManager mStatsManager; 117 118 private NetworkCallback mMobileNetworkCallback; 119 private NetworkPolicyManager.Listener mPolicyListener; 120 121 122 /** 123 * Divider to calculate opportunistic quota from user-set data limit or warning: 5% of user-set 124 * limit. 125 */ 126 private static final int OPQUOTA_USER_SETTING_DIVIDER = 20; 127 128 public static class Dependencies { getClock()129 public Clock getClock() { 130 return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(), 131 Clock.systemUTC()); 132 } 133 } 134 MultipathPolicyTracker(Context ctx, Handler handler)135 public MultipathPolicyTracker(Context ctx, Handler handler) { 136 this(ctx, handler, new Dependencies()); 137 } 138 MultipathPolicyTracker(Context ctx, Handler handler, Dependencies deps)139 public MultipathPolicyTracker(Context ctx, Handler handler, Dependencies deps) { 140 mContext = ctx; 141 mUserAllContext = ctx.createContextAsUser(UserHandle.ALL, 0 /* flags */); 142 mHandler = handler; 143 mClock = deps.getClock(); 144 mDeps = deps; 145 mResolver = mContext.getContentResolver(); 146 mSettingsObserver = new SettingsObserver(mHandler); 147 mConfigChangeReceiver = new ConfigChangeReceiver(); 148 // Because we are initialized by the ConnectivityService constructor, we can't touch any 149 // connectivity APIs. Service initialization is done in start(). 150 } 151 start()152 public void start() { 153 mCM = mContext.getSystemService(ConnectivityManager.class); 154 mNPM = mContext.getSystemService(NetworkPolicyManager.class); 155 mStatsManager = mContext.getSystemService(NetworkStatsManager.class); 156 157 registerTrackMobileCallback(); 158 registerNetworkPolicyListener(); 159 final Uri defaultSettingUri = 160 Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES); 161 mResolver.registerContentObserver(defaultSettingUri, false, mSettingsObserver); 162 163 final IntentFilter intentFilter = new IntentFilter(); 164 intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); 165 mUserAllContext.registerReceiver( 166 mConfigChangeReceiver, intentFilter, null /* broadcastPermission */, mHandler); 167 } 168 shutdown()169 public void shutdown() { 170 maybeUnregisterTrackMobileCallback(); 171 unregisterNetworkPolicyListener(); 172 for (MultipathTracker t : mMultipathTrackers.values()) { 173 t.shutdown(); 174 } 175 mMultipathTrackers.clear(); 176 mResolver.unregisterContentObserver(mSettingsObserver); 177 mUserAllContext.unregisterReceiver(mConfigChangeReceiver); 178 } 179 180 // Called on an arbitrary binder thread. getMultipathPreference(Network network)181 public Integer getMultipathPreference(Network network) { 182 if (network == null) { 183 return null; 184 } 185 MultipathTracker t = mMultipathTrackers.get(network); 186 if (t != null) { 187 return t.getMultipathPreference(); 188 } 189 return null; 190 } 191 192 // Track information on mobile networks as they come and go. 193 class MultipathTracker { 194 final Network network; 195 final String subscriberId; 196 197 private long mQuota; 198 /** Current multipath budget. Nonzero iff we have budget and a UsageCallback is armed. */ 199 private long mMultipathBudget; 200 private final NetworkTemplate mNetworkTemplate; 201 private final UsageCallback mUsageCallback; 202 private NetworkCapabilities mNetworkCapabilities; 203 MultipathTracker(Network network, NetworkCapabilities nc)204 public MultipathTracker(Network network, NetworkCapabilities nc) { 205 this.network = network; 206 this.mNetworkCapabilities = new NetworkCapabilities(nc); 207 NetworkSpecifier specifier = nc.getNetworkSpecifier(); 208 int subId = INVALID_SUBSCRIPTION_ID; 209 if (specifier instanceof TelephonyNetworkSpecifier) { 210 subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId(); 211 } else { 212 throw new IllegalStateException(String.format( 213 "Can't get subId from mobile network %s (%s)", 214 network, nc)); 215 } 216 217 TelephonyManager tele = mContext.getSystemService(TelephonyManager.class); 218 if (tele == null) { 219 throw new IllegalStateException(String.format("Missing TelephonyManager")); 220 } 221 tele = tele.createForSubscriptionId(subId); 222 if (tele == null) { 223 throw new IllegalStateException(String.format( 224 "Can't get TelephonyManager for subId %d", subId)); 225 } 226 227 subscriberId = tele.getSubscriberId(); 228 mNetworkTemplate = new NetworkTemplate( 229 NetworkTemplate.MATCH_MOBILE, subscriberId, new String[] { subscriberId }, 230 null, NetworkStats.METERED_YES, NetworkStats.ROAMING_ALL, 231 NetworkStats.DEFAULT_NETWORK_NO, NETWORK_TYPE_ALL, OEM_MANAGED_ALL, 232 SUBSCRIBER_ID_MATCH_RULE_EXACT); 233 mUsageCallback = new UsageCallback() { 234 @Override 235 public void onThresholdReached(int networkType, String subscriberId) { 236 if (DBG) Log.d(TAG, "onThresholdReached for network " + network); 237 mMultipathBudget = 0; 238 updateMultipathBudget(); 239 } 240 }; 241 242 updateMultipathBudget(); 243 } 244 setNetworkCapabilities(NetworkCapabilities nc)245 public void setNetworkCapabilities(NetworkCapabilities nc) { 246 mNetworkCapabilities = new NetworkCapabilities(nc); 247 } 248 249 // TODO: calculate with proper timezone information getDailyNonDefaultDataUsage()250 private long getDailyNonDefaultDataUsage() { 251 final ZonedDateTime end = 252 ZonedDateTime.ofInstant(mClock.instant(), ZoneId.systemDefault()); 253 final ZonedDateTime start = end.truncatedTo(ChronoUnit.DAYS); 254 255 final long bytes = getNetworkTotalBytes( 256 start.toInstant().toEpochMilli(), 257 end.toInstant().toEpochMilli()); 258 if (DBG) Log.d(TAG, "Non-default data usage: " + bytes); 259 return bytes; 260 } 261 getNetworkTotalBytes(long start, long end)262 private long getNetworkTotalBytes(long start, long end) { 263 try { 264 return LocalServices.getService(NetworkStatsManagerInternal.class) 265 .getNetworkTotalBytes(mNetworkTemplate, start, end); 266 } catch (RuntimeException e) { 267 Log.w(TAG, "Failed to get data usage: " + e); 268 return -1; 269 } 270 } 271 getTemplateMatchingNetworkIdentity(NetworkCapabilities nc)272 private NetworkIdentity getTemplateMatchingNetworkIdentity(NetworkCapabilities nc) { 273 return new NetworkIdentity( 274 ConnectivityManager.TYPE_MOBILE, 275 0 /* subType, unused for template matching */, 276 subscriberId, 277 null /* networkId, unused for matching mobile networks */, 278 !nc.hasCapability(NET_CAPABILITY_NOT_ROAMING), 279 !nc.hasCapability(NET_CAPABILITY_NOT_METERED), 280 false /* defaultNetwork, templates should have DEFAULT_NETWORK_ALL */, 281 OEM_MANAGED_ALL); 282 } 283 getRemainingDailyBudget(long limitBytes, Range<ZonedDateTime> cycle)284 private long getRemainingDailyBudget(long limitBytes, 285 Range<ZonedDateTime> cycle) { 286 final long start = cycle.getLower().toInstant().toEpochMilli(); 287 final long end = cycle.getUpper().toInstant().toEpochMilli(); 288 final long totalBytes = getNetworkTotalBytes(start, end); 289 final long remainingBytes = totalBytes == -1 ? 0 : Math.max(0, limitBytes - totalBytes); 290 // 1 + ((end - now - 1) / millisInDay with integers is equivalent to: 291 // ceil((double)(end - now) / millisInDay) 292 final long remainingDays = 293 1 + ((end - mClock.millis() - 1) / TimeUnit.DAYS.toMillis(1)); 294 295 return remainingBytes / Math.max(1, remainingDays); 296 } 297 getUserPolicyOpportunisticQuotaBytes()298 private long getUserPolicyOpportunisticQuotaBytes() { 299 // Keep the most restrictive applicable policy 300 long minQuota = Long.MAX_VALUE; 301 final NetworkIdentity identity = getTemplateMatchingNetworkIdentity( 302 mNetworkCapabilities); 303 304 final NetworkPolicy[] policies = mNPM.getNetworkPolicies(); 305 for (NetworkPolicy policy : policies) { 306 if (policy.hasCycle() && policy.template.matches(identity)) { 307 final long cycleStart = policy.cycleIterator().next().getLower() 308 .toInstant().toEpochMilli(); 309 // Prefer user-defined warning, otherwise use hard limit 310 final long activeWarning = getActiveWarning(policy, cycleStart); 311 final long policyBytes = (activeWarning == WARNING_DISABLED) 312 ? getActiveLimit(policy, cycleStart) 313 : activeWarning; 314 315 if (policyBytes != LIMIT_DISABLED && policyBytes != WARNING_DISABLED) { 316 final long policyBudget = getRemainingDailyBudget(policyBytes, 317 policy.cycleIterator().next()); 318 minQuota = Math.min(minQuota, policyBudget); 319 } 320 } 321 } 322 323 if (minQuota == Long.MAX_VALUE) { 324 return OPPORTUNISTIC_QUOTA_UNKNOWN; 325 } 326 327 return minQuota / OPQUOTA_USER_SETTING_DIVIDER; 328 } 329 updateMultipathBudget()330 void updateMultipathBudget() { 331 long quota = LocalServices.getService(NetworkPolicyManagerInternal.class) 332 .getSubscriptionOpportunisticQuota(this.network, QUOTA_TYPE_MULTIPATH); 333 if (DBG) Log.d(TAG, "Opportunistic quota from data plan: " + quota + " bytes"); 334 335 // Fallback to user settings-based quota if not available from phone plan 336 if (quota == OPPORTUNISTIC_QUOTA_UNKNOWN) { 337 quota = getUserPolicyOpportunisticQuotaBytes(); 338 if (DBG) Log.d(TAG, "Opportunistic quota from user policy: " + quota + " bytes"); 339 } 340 341 if (quota == OPPORTUNISTIC_QUOTA_UNKNOWN) { 342 quota = getDefaultDailyMultipathQuotaBytes(); 343 if (DBG) Log.d(TAG, "Setting quota: " + quota + " bytes"); 344 } 345 346 // TODO: re-register if day changed: budget may have run out but should be refreshed. 347 if (haveMultipathBudget() && quota == mQuota) { 348 // If there is already a usage callback pending , there's no need to re-register it 349 // if the quota hasn't changed. The callback will simply fire as expected when the 350 // budget is spent. 351 if (DBG) Log.d(TAG, "Quota still " + quota + ", not updating."); 352 return; 353 } 354 mQuota = quota; 355 356 // If we can't get current usage, assume the worst and don't give 357 // ourselves any budget to work with. 358 final long usage = getDailyNonDefaultDataUsage(); 359 final long budget = (usage == -1) ? 0 : Math.max(0, quota - usage); 360 361 // Only consider budgets greater than MIN_THRESHOLD_BYTES, otherwise the callback will 362 // fire late, after data usage went over budget. Also budget should be 0 if remaining 363 // data is close to 0. 364 // This is necessary because the usage callback does not accept smaller thresholds. 365 // Because it snaps everything to MIN_THRESHOLD_BYTES, the lesser of the two evils is 366 // to snap to 0 here. 367 // This will only be called if the total quota for the day changed, not if usage changed 368 // since last time, so even if this is called very often the budget will not snap to 0 369 // as soon as there are less than 2MB left for today. 370 if (budget > NetworkStatsManager.MIN_THRESHOLD_BYTES) { 371 if (DBG) { 372 Log.d(TAG, "Setting callback for " + budget + " bytes on network " + network); 373 } 374 registerUsageCallback(budget); 375 } else { 376 maybeUnregisterUsageCallback(); 377 } 378 } 379 getMultipathPreference()380 public int getMultipathPreference() { 381 if (haveMultipathBudget()) { 382 return MULTIPATH_PREFERENCE_HANDOVER | MULTIPATH_PREFERENCE_RELIABILITY; 383 } 384 return 0; 385 } 386 387 // For debugging only. getQuota()388 public long getQuota() { 389 return mQuota; 390 } 391 392 // For debugging only. getMultipathBudget()393 public long getMultipathBudget() { 394 return mMultipathBudget; 395 } 396 haveMultipathBudget()397 private boolean haveMultipathBudget() { 398 return mMultipathBudget > 0; 399 } 400 registerUsageCallback(long budget)401 private void registerUsageCallback(long budget) { 402 maybeUnregisterUsageCallback(); 403 mStatsManager.registerUsageCallback(mNetworkTemplate, TYPE_MOBILE, budget, 404 mUsageCallback, mHandler); 405 mMultipathBudget = budget; 406 } 407 maybeUnregisterUsageCallback()408 private void maybeUnregisterUsageCallback() { 409 if (haveMultipathBudget()) { 410 if (DBG) Log.d(TAG, "Unregistering callback, budget was " + mMultipathBudget); 411 mStatsManager.unregisterUsageCallback(mUsageCallback); 412 mMultipathBudget = 0; 413 } 414 } 415 shutdown()416 void shutdown() { 417 maybeUnregisterUsageCallback(); 418 } 419 } 420 getActiveWarning(NetworkPolicy policy, long cycleStart)421 private static long getActiveWarning(NetworkPolicy policy, long cycleStart) { 422 return policy.lastWarningSnooze < cycleStart 423 ? policy.warningBytes 424 : WARNING_DISABLED; 425 } 426 getActiveLimit(NetworkPolicy policy, long cycleStart)427 private static long getActiveLimit(NetworkPolicy policy, long cycleStart) { 428 return policy.lastLimitSnooze < cycleStart 429 ? policy.limitBytes 430 : LIMIT_DISABLED; 431 } 432 433 // Only ever updated on the handler thread. Accessed from other binder threads to retrieve 434 // the tracker for a specific network. 435 private final ConcurrentHashMap <Network, MultipathTracker> mMultipathTrackers = 436 new ConcurrentHashMap<>(); 437 getDefaultDailyMultipathQuotaBytes()438 private long getDefaultDailyMultipathQuotaBytes() { 439 final String setting = Settings.Global.getString(mContext.getContentResolver(), 440 NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES); 441 if (setting != null) { 442 try { 443 return Long.parseLong(setting); 444 } catch(NumberFormatException e) { 445 // fall through 446 } 447 } 448 449 return mContext.getResources().getInteger( 450 R.integer.config_networkDefaultDailyMultipathQuotaBytes); 451 } 452 453 // TODO: this races with app code that might respond to onAvailable() by immediately calling 454 // getMultipathPreference. Fix this by adding to ConnectivityService the ability to directly 455 // invoke NetworkCallbacks on tightly-coupled classes such as this one which run on its 456 // handler thread. registerTrackMobileCallback()457 private void registerTrackMobileCallback() { 458 final NetworkRequest request = new NetworkRequest.Builder() 459 .addCapability(NET_CAPABILITY_INTERNET) 460 .addTransportType(TRANSPORT_CELLULAR) 461 .build(); 462 mMobileNetworkCallback = new ConnectivityManager.NetworkCallback() { 463 @Override 464 public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { 465 MultipathTracker existing = mMultipathTrackers.get(network); 466 if (existing != null) { 467 existing.setNetworkCapabilities(nc); 468 existing.updateMultipathBudget(); 469 return; 470 } 471 472 try { 473 mMultipathTrackers.put(network, new MultipathTracker(network, nc)); 474 } catch (IllegalStateException e) { 475 Log.e(TAG, "Can't track mobile network " + network + ": " + e.getMessage()); 476 } 477 if (DBG) Log.d(TAG, "Tracking mobile network " + network); 478 } 479 480 @Override 481 public void onLost(Network network) { 482 MultipathTracker existing = mMultipathTrackers.get(network); 483 if (existing != null) { 484 existing.shutdown(); 485 mMultipathTrackers.remove(network); 486 } 487 if (DBG) Log.d(TAG, "No longer tracking mobile network " + network); 488 } 489 }; 490 491 mCM.registerNetworkCallback(request, mMobileNetworkCallback, mHandler); 492 } 493 494 /** 495 * Update multipath budgets for all trackers. To be called on the mHandler thread. 496 */ updateAllMultipathBudgets()497 private void updateAllMultipathBudgets() { 498 for (MultipathTracker t : mMultipathTrackers.values()) { 499 t.updateMultipathBudget(); 500 } 501 } 502 maybeUnregisterTrackMobileCallback()503 private void maybeUnregisterTrackMobileCallback() { 504 if (mMobileNetworkCallback != null) { 505 mCM.unregisterNetworkCallback(mMobileNetworkCallback); 506 } 507 mMobileNetworkCallback = null; 508 } 509 registerNetworkPolicyListener()510 private void registerNetworkPolicyListener() { 511 mPolicyListener = new NetworkPolicyManager.Listener() { 512 @Override 513 public void onMeteredIfacesChanged(String[] meteredIfaces) { 514 // Dispatched every time opportunistic quota is recalculated. 515 mHandler.post(() -> updateAllMultipathBudgets()); 516 } 517 }; 518 mNPM.registerListener(mPolicyListener); 519 } 520 unregisterNetworkPolicyListener()521 private void unregisterNetworkPolicyListener() { 522 mNPM.unregisterListener(mPolicyListener); 523 } 524 525 private final class SettingsObserver extends ContentObserver { SettingsObserver(Handler handler)526 public SettingsObserver(Handler handler) { 527 super(handler); 528 } 529 530 @Override onChange(boolean selfChange)531 public void onChange(boolean selfChange) { 532 Log.wtf(TAG, "Should never be reached."); 533 } 534 535 @Override onChange(boolean selfChange, Uri uri)536 public void onChange(boolean selfChange, Uri uri) { 537 if (!Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES) 538 .equals(uri)) { 539 Log.wtf(TAG, "Unexpected settings observation: " + uri); 540 } 541 if (DBG) Log.d(TAG, "Settings change: updating budgets."); 542 updateAllMultipathBudgets(); 543 } 544 } 545 546 private final class ConfigChangeReceiver extends BroadcastReceiver { 547 @Override onReceive(Context context, Intent intent)548 public void onReceive(Context context, Intent intent) { 549 if (DBG) Log.d(TAG, "Configuration change: updating budgets."); 550 updateAllMultipathBudgets(); 551 } 552 } 553 dump(IndentingPrintWriter pw)554 public void dump(IndentingPrintWriter pw) { 555 // Do not use in production. Access to class data is only safe on the handler thrad. 556 pw.println("MultipathPolicyTracker:"); 557 pw.increaseIndent(); 558 for (MultipathTracker t : mMultipathTrackers.values()) { 559 pw.println(String.format("Network %s: quota %d, budget %d. Preference: %s", 560 t.network, t.getQuota(), t.getMultipathBudget(), 561 DebugUtils.flagsToString(ConnectivityManager.class, "MULTIPATH_PREFERENCE_", 562 t.getMultipathPreference()))); 563 } 564 pw.decreaseIndent(); 565 } 566 } 567