1 /* 2 * Copyright (C) 2016 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.settings.accounts; 18 19 import static android.content.Intent.EXTRA_USER; 20 import static android.os.UserManager.DISALLOW_MODIFY_ACCOUNTS; 21 import static android.os.UserManager.DISALLOW_REMOVE_MANAGED_PROFILE; 22 import static android.provider.Settings.ACTION_ADD_ACCOUNT; 23 import static android.provider.Settings.EXTRA_AUTHORITIES; 24 25 import android.accounts.Account; 26 import android.accounts.AccountManager; 27 import android.content.BroadcastReceiver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.content.pm.ApplicationInfo; 32 import android.content.pm.PackageManager; 33 import android.content.pm.UserInfo; 34 import android.content.res.Resources; 35 import android.graphics.drawable.Drawable; 36 import android.os.Bundle; 37 import android.os.UserHandle; 38 import android.os.UserManager; 39 import android.support.v7.preference.Preference; 40 import android.support.v7.preference.Preference.OnPreferenceClickListener; 41 import android.support.v7.preference.PreferenceGroup; 42 import android.support.v7.preference.PreferenceScreen; 43 import android.text.BidiFormatter; 44 import android.util.ArrayMap; 45 import android.util.Log; 46 import android.util.SparseArray; 47 48 import com.android.internal.annotations.VisibleForTesting; 49 import com.android.settings.AccessiblePreferenceCategory; 50 import com.android.settings.R; 51 import com.android.settings.SettingsPreferenceFragment; 52 import com.android.settings.Utils; 53 import com.android.settings.core.PreferenceControllerMixin; 54 import com.android.settings.core.SubSettingLauncher; 55 import com.android.settings.overlay.FeatureFactory; 56 import com.android.settings.search.SearchIndexableRaw; 57 import com.android.settingslib.RestrictedPreference; 58 import com.android.settingslib.accounts.AuthenticatorHelper; 59 import com.android.settingslib.core.AbstractPreferenceController; 60 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 61 import com.android.settingslib.core.lifecycle.LifecycleObserver; 62 import com.android.settingslib.core.lifecycle.events.OnPause; 63 import com.android.settingslib.core.lifecycle.events.OnResume; 64 65 import java.util.ArrayList; 66 import java.util.Collections; 67 import java.util.Comparator; 68 import java.util.List; 69 70 public class AccountPreferenceController extends AbstractPreferenceController 71 implements PreferenceControllerMixin, AuthenticatorHelper.OnAccountsUpdateListener, 72 OnPreferenceClickListener, LifecycleObserver, OnPause, OnResume { 73 74 private static final String TAG = "AccountPrefController"; 75 76 private static final int ORDER_ACCOUNT_PROFILES = 1; 77 private static final int ORDER_LAST = 1002; 78 private static final int ORDER_NEXT_TO_LAST = 1001; 79 private static final int ORDER_NEXT_TO_NEXT_TO_LAST = 1000; 80 81 private UserManager mUm; 82 private SparseArray<ProfileData> mProfiles = new SparseArray<ProfileData>(); 83 private ManagedProfileBroadcastReceiver mManagedProfileBroadcastReceiver 84 = new ManagedProfileBroadcastReceiver(); 85 private Preference mProfileNotAvailablePreference; 86 private String[] mAuthorities; 87 private int mAuthoritiesCount = 0; 88 private SettingsPreferenceFragment mParent; 89 private int mAccountProfileOrder = ORDER_ACCOUNT_PROFILES; 90 private AccountRestrictionHelper mHelper; 91 private MetricsFeatureProvider mMetricsFeatureProvider; 92 93 /** 94 * Holds data related to the accounts belonging to one profile. 95 */ 96 public static class ProfileData { 97 /** 98 * The preference that displays the accounts. 99 */ 100 public PreferenceGroup preferenceGroup; 101 /** 102 * The preference that displays the add account button. 103 */ 104 public RestrictedPreference addAccountPreference; 105 /** 106 * The preference that displays the button to remove the managed profile 107 */ 108 public RestrictedPreference removeWorkProfilePreference; 109 /** 110 * The preference that displays managed profile settings. 111 */ 112 public Preference managedProfilePreference; 113 /** 114 * The {@link AuthenticatorHelper} that holds accounts data for this profile. 115 */ 116 public AuthenticatorHelper authenticatorHelper; 117 /** 118 * The {@link UserInfo} of the profile. 119 */ 120 public UserInfo userInfo; 121 /** 122 * The {@link UserInfo} of the profile. 123 */ 124 public boolean pendingRemoval; 125 /** 126 * The map from account key to account preference 127 */ 128 public ArrayMap<String, AccountTypePreference> accountPreferences = new ArrayMap<>(); 129 } 130 131 public AccountPreferenceController(Context context, SettingsPreferenceFragment parent, 132 String[] authorities) { 133 this(context, parent, authorities, new AccountRestrictionHelper(context)); 134 } 135 136 @VisibleForTesting 137 AccountPreferenceController(Context context, SettingsPreferenceFragment parent, 138 String[] authorities, AccountRestrictionHelper helper) { 139 super(context); 140 mUm = (UserManager) context.getSystemService(Context.USER_SERVICE); 141 mAuthorities = authorities; 142 mParent = parent; 143 if (mAuthorities != null) { 144 mAuthoritiesCount = mAuthorities.length; 145 } 146 final FeatureFactory featureFactory = FeatureFactory.getFactory(mContext); 147 mMetricsFeatureProvider = featureFactory.getMetricsFeatureProvider(); 148 mHelper = helper; 149 } 150 151 @Override 152 public boolean isAvailable() { 153 return !mUm.isManagedProfile(); 154 } 155 156 @Override 157 public String getPreferenceKey() { 158 return null; 159 } 160 161 @Override 162 public void displayPreference(PreferenceScreen screen) { 163 super.displayPreference(screen); 164 updateUi(); 165 } 166 167 @Override 168 public void updateRawDataToIndex(List<SearchIndexableRaw> rawData) { 169 if (!isAvailable()) { 170 return; 171 } 172 final Resources res = mContext.getResources(); 173 final String screenTitle = res.getString(R.string.account_settings_title); 174 175 List<UserInfo> profiles = mUm.getProfiles(UserHandle.myUserId()); 176 final int profilesCount = profiles.size(); 177 for (int i = 0; i < profilesCount; i++) { 178 UserInfo userInfo = profiles.get(i); 179 if (userInfo.isEnabled()) { 180 if (!mHelper.hasBaseUserRestriction(DISALLOW_MODIFY_ACCOUNTS, userInfo.id)) { 181 SearchIndexableRaw data = new SearchIndexableRaw(mContext); 182 data.title = res.getString(R.string.add_account_label); 183 data.screenTitle = screenTitle; 184 rawData.add(data); 185 } 186 if (userInfo.isManagedProfile()) { 187 if (!mHelper.hasBaseUserRestriction(DISALLOW_REMOVE_MANAGED_PROFILE, 188 UserHandle.myUserId())) { 189 SearchIndexableRaw data = new SearchIndexableRaw(mContext); 190 data.title = res.getString(R.string.remove_managed_profile_label); 191 data.screenTitle = screenTitle; 192 rawData.add(data); 193 } 194 { 195 SearchIndexableRaw data = new SearchIndexableRaw(mContext); 196 data.title = res.getString(R.string.managed_profile_settings_title); 197 data.screenTitle = screenTitle; 198 rawData.add(data); 199 } 200 } 201 } 202 } 203 } 204 205 @Override 206 public void onResume() { 207 updateUi(); 208 mManagedProfileBroadcastReceiver.register(mContext); 209 listenToAccountUpdates(); 210 } 211 212 @Override 213 public void onPause() { 214 stopListeningToAccountUpdates(); 215 mManagedProfileBroadcastReceiver.unregister(mContext); 216 } 217 218 @Override 219 public void onAccountsUpdate(UserHandle userHandle) { 220 final ProfileData profileData = mProfiles.get(userHandle.getIdentifier()); 221 if (profileData != null) { 222 updateAccountTypes(profileData); 223 } else { 224 Log.w(TAG, "Missing Settings screen for: " + userHandle.getIdentifier()); 225 } 226 } 227 228 @Override 229 public boolean onPreferenceClick(Preference preference) { 230 // Check the preference 231 final int count = mProfiles.size(); 232 for (int i = 0; i < count; i++) { 233 ProfileData profileData = mProfiles.valueAt(i); 234 if (preference == profileData.addAccountPreference) { 235 Intent intent = new Intent(ACTION_ADD_ACCOUNT); 236 intent.putExtra(EXTRA_USER, profileData.userInfo.getUserHandle()); 237 intent.putExtra(EXTRA_AUTHORITIES, mAuthorities); 238 mContext.startActivity(intent); 239 return true; 240 } 241 if (preference == profileData.removeWorkProfilePreference) { 242 final int userId = profileData.userInfo.id; 243 RemoveUserFragment.newInstance(userId).show(mParent.getFragmentManager(), 244 "removeUser"); 245 return true; 246 } 247 if (preference == profileData.managedProfilePreference) { 248 Bundle arguments = new Bundle(); 249 arguments.putParcelable(Intent.EXTRA_USER, profileData.userInfo.getUserHandle()); 250 new SubSettingLauncher(mContext) 251 .setSourceMetricsCategory(mParent.getMetricsCategory()) 252 .setDestination(ManagedProfileSettings.class.getName()) 253 .setTitle(R.string.managed_profile_settings_title) 254 .setArguments(arguments) 255 .launch(); 256 257 return true; 258 } 259 } 260 return false; 261 } 262 263 private void updateUi() { 264 if (!isAvailable()) { 265 // This should not happen 266 Log.e(TAG, "We should not be showing settings for a managed profile"); 267 return; 268 } 269 270 for (int i = 0, size = mProfiles.size(); i < size; i++) { 271 mProfiles.valueAt(i).pendingRemoval = true; 272 } 273 if (mUm.isRestrictedProfile()) { 274 // Restricted user or similar 275 UserInfo userInfo = mUm.getUserInfo(UserHandle.myUserId()); 276 updateProfileUi(userInfo); 277 } else { 278 List<UserInfo> profiles = mUm.getProfiles(UserHandle.myUserId()); 279 final int profilesCount = profiles.size(); 280 for (int i = 0; i < profilesCount; i++) { 281 updateProfileUi(profiles.get(i)); 282 } 283 } 284 cleanUpPreferences(); 285 286 // Add all preferences, starting with one for the primary profile. 287 // Note that we're relying on the ordering given by the SparseArray keys, and on the 288 // value of UserHandle.USER_OWNER being smaller than all the rest. 289 final int profilesCount = mProfiles.size(); 290 for (int i = 0; i < profilesCount; i++) { 291 updateAccountTypes(mProfiles.valueAt(i)); 292 } 293 } 294 295 private void updateProfileUi(final UserInfo userInfo) { 296 if (mParent.getPreferenceManager() == null) { 297 return; 298 } 299 final ProfileData data = mProfiles.get(userInfo.id); 300 if (data != null) { 301 data.pendingRemoval = false; 302 if (userInfo.isEnabled()) { 303 // recreate the authentication helper to refresh the list of enabled accounts 304 data.authenticatorHelper = 305 new AuthenticatorHelper(mContext, userInfo.getUserHandle(), this); 306 } 307 return; 308 } 309 final Context context = mContext; 310 final ProfileData profileData = new ProfileData(); 311 profileData.userInfo = userInfo; 312 AccessiblePreferenceCategory preferenceGroup = 313 mHelper.createAccessiblePreferenceCategory(mParent.getPreferenceManager().getContext()); 314 preferenceGroup.setOrder(mAccountProfileOrder++); 315 if (isSingleProfile()) { 316 preferenceGroup.setTitle(context.getString(R.string.account_for_section_header, 317 BidiFormatter.getInstance().unicodeWrap(userInfo.name))); 318 preferenceGroup.setContentDescription( 319 mContext.getString(R.string.account_settings)); 320 } else if (userInfo.isManagedProfile()) { 321 preferenceGroup.setTitle(R.string.category_work); 322 String workGroupSummary = getWorkGroupSummary(context, userInfo); 323 preferenceGroup.setSummary(workGroupSummary); 324 preferenceGroup.setContentDescription( 325 mContext.getString(R.string.accessibility_category_work, workGroupSummary)); 326 profileData.removeWorkProfilePreference = newRemoveWorkProfilePreference(); 327 mHelper.enforceRestrictionOnPreference(profileData.removeWorkProfilePreference, 328 DISALLOW_REMOVE_MANAGED_PROFILE, UserHandle.myUserId()); 329 profileData.managedProfilePreference = newManagedProfileSettings(); 330 } else { 331 preferenceGroup.setTitle(R.string.category_personal); 332 preferenceGroup.setContentDescription( 333 mContext.getString(R.string.accessibility_category_personal)); 334 } 335 final PreferenceScreen screen = mParent.getPreferenceScreen(); 336 if (screen != null) { 337 screen.addPreference(preferenceGroup); 338 } 339 profileData.preferenceGroup = preferenceGroup; 340 if (userInfo.isEnabled()) { 341 profileData.authenticatorHelper = new AuthenticatorHelper(context, 342 userInfo.getUserHandle(), this); 343 profileData.addAccountPreference = newAddAccountPreference(); 344 mHelper.enforceRestrictionOnPreference(profileData.addAccountPreference, 345 DISALLOW_MODIFY_ACCOUNTS, userInfo.id); 346 } 347 mProfiles.put(userInfo.id, profileData); 348 } 349 350 private RestrictedPreference newAddAccountPreference() { 351 RestrictedPreference preference = 352 new RestrictedPreference(mParent.getPreferenceManager().getContext()); 353 preference.setTitle(R.string.add_account_label); 354 preference.setIcon(R.drawable.ic_menu_add); 355 preference.setOnPreferenceClickListener(this); 356 preference.setOrder(ORDER_NEXT_TO_NEXT_TO_LAST); 357 return preference; 358 } 359 360 private RestrictedPreference newRemoveWorkProfilePreference() { 361 RestrictedPreference preference = new RestrictedPreference( 362 mParent.getPreferenceManager().getContext()); 363 preference.setTitle(R.string.remove_managed_profile_label); 364 preference.setIcon(R.drawable.ic_delete); 365 preference.setOnPreferenceClickListener(this); 366 preference.setOrder(ORDER_LAST); 367 return preference; 368 } 369 370 371 private Preference newManagedProfileSettings() { 372 Preference preference = new Preference(mParent.getPreferenceManager().getContext()); 373 preference.setTitle(R.string.managed_profile_settings_title); 374 preference.setIcon(R.drawable.ic_settings_24dp); 375 preference.setOnPreferenceClickListener(this); 376 preference.setOrder(ORDER_NEXT_TO_LAST); 377 return preference; 378 } 379 380 private String getWorkGroupSummary(Context context, UserInfo userInfo) { 381 PackageManager packageManager = context.getPackageManager(); 382 ApplicationInfo adminApplicationInfo = Utils.getAdminApplicationInfo(context, userInfo.id); 383 if (adminApplicationInfo == null) { 384 return null; 385 } 386 CharSequence appLabel = packageManager.getApplicationLabel(adminApplicationInfo); 387 return mContext.getString(R.string.managing_admin, appLabel); 388 } 389 390 void cleanUpPreferences() { 391 PreferenceScreen screen = mParent.getPreferenceScreen(); 392 if (screen == null) { 393 return; 394 } 395 final int count = mProfiles.size(); 396 for (int i = count-1; i >= 0; i--) { 397 final ProfileData data = mProfiles.valueAt(i); 398 if (data.pendingRemoval) { 399 screen.removePreference(data.preferenceGroup); 400 mProfiles.removeAt(i); 401 } 402 } 403 } 404 405 private void listenToAccountUpdates() { 406 final int count = mProfiles.size(); 407 for (int i = 0; i < count; i++) { 408 AuthenticatorHelper authenticatorHelper = mProfiles.valueAt(i).authenticatorHelper; 409 if (authenticatorHelper != null) { 410 authenticatorHelper.listenToAccountUpdates(); 411 } 412 } 413 } 414 415 private void stopListeningToAccountUpdates() { 416 final int count = mProfiles.size(); 417 for (int i = 0; i < count; i++) { 418 AuthenticatorHelper authenticatorHelper = mProfiles.valueAt(i).authenticatorHelper; 419 if (authenticatorHelper != null) { 420 authenticatorHelper.stopListeningToAccountUpdates(); 421 } 422 } 423 } 424 425 private void updateAccountTypes(ProfileData profileData) { 426 if (mParent.getPreferenceManager() == null 427 || profileData.preferenceGroup.getPreferenceManager() == null) { 428 // This could happen if activity is finishing 429 return; 430 } 431 if (profileData.userInfo.isEnabled()) { 432 final ArrayMap<String, AccountTypePreference> preferenceToRemove = 433 new ArrayMap<>(profileData.accountPreferences); 434 final ArrayList<AccountTypePreference> preferences = getAccountTypePreferences( 435 profileData.authenticatorHelper, profileData.userInfo.getUserHandle(), 436 preferenceToRemove); 437 final int count = preferences.size(); 438 for (int i = 0; i < count; i++) { 439 final AccountTypePreference preference = preferences.get(i); 440 preference.setOrder(i); 441 final String key = preference.getKey(); 442 if (!profileData.accountPreferences.containsKey(key)) { 443 profileData.preferenceGroup.addPreference(preference); 444 profileData.accountPreferences.put(key, preference); 445 } 446 } 447 if (profileData.addAccountPreference != null) { 448 profileData.preferenceGroup.addPreference(profileData.addAccountPreference); 449 } 450 for (String key : preferenceToRemove.keySet()) { 451 profileData.preferenceGroup.removePreference( 452 profileData.accountPreferences.get(key)); 453 profileData.accountPreferences.remove(key); 454 } 455 } else { 456 profileData.preferenceGroup.removeAll(); 457 // Put a label instead of the accounts list 458 if (mProfileNotAvailablePreference == null) { 459 mProfileNotAvailablePreference = 460 new Preference(mParent.getPreferenceManager().getContext()); 461 } 462 mProfileNotAvailablePreference.setEnabled(false); 463 mProfileNotAvailablePreference.setIcon(R.drawable.empty_icon); 464 mProfileNotAvailablePreference.setTitle(null); 465 mProfileNotAvailablePreference.setSummary( 466 R.string.managed_profile_not_available_label); 467 profileData.preferenceGroup.addPreference(mProfileNotAvailablePreference); 468 } 469 if (profileData.removeWorkProfilePreference != null) { 470 profileData.preferenceGroup.addPreference(profileData.removeWorkProfilePreference); 471 } 472 if (profileData.managedProfilePreference != null) { 473 profileData.preferenceGroup.addPreference(profileData.managedProfilePreference); 474 } 475 } 476 477 private ArrayList<AccountTypePreference> getAccountTypePreferences(AuthenticatorHelper helper, 478 UserHandle userHandle, ArrayMap<String, AccountTypePreference> preferenceToRemove) { 479 final String[] accountTypes = helper.getEnabledAccountTypes(); 480 final ArrayList<AccountTypePreference> accountTypePreferences = 481 new ArrayList<>(accountTypes.length); 482 483 for (int i = 0; i < accountTypes.length; i++) { 484 final String accountType = accountTypes[i]; 485 // Skip showing any account that does not have any of the requested authorities 486 if (!accountTypeHasAnyRequestedAuthorities(helper, accountType)) { 487 continue; 488 } 489 final CharSequence label = helper.getLabelForType(mContext, accountType); 490 if (label == null) { 491 continue; 492 } 493 final String titleResPackageName = helper.getPackageForType(accountType); 494 final int titleResId = helper.getLabelIdForType(accountType); 495 496 final Account[] accounts = AccountManager.get(mContext) 497 .getAccountsByTypeAsUser(accountType, userHandle); 498 final Drawable icon = helper.getDrawableForType(mContext, accountType); 499 final Context prefContext = mParent.getPreferenceManager().getContext(); 500 501 // Add a preference row for each individual account 502 for (Account account : accounts) { 503 final AccountTypePreference preference = 504 preferenceToRemove.remove(AccountTypePreference.buildKey(account)); 505 if (preference != null) { 506 accountTypePreferences.add(preference); 507 continue; 508 } 509 final ArrayList<String> auths = 510 helper.getAuthoritiesForAccountType(account.type); 511 if (!AccountRestrictionHelper.showAccount(mAuthorities, auths)) { 512 continue; 513 } 514 final Bundle fragmentArguments = new Bundle(); 515 fragmentArguments.putParcelable(AccountDetailDashboardFragment.KEY_ACCOUNT, 516 account); 517 fragmentArguments.putParcelable(AccountDetailDashboardFragment.KEY_USER_HANDLE, 518 userHandle); 519 fragmentArguments.putString(AccountDetailDashboardFragment.KEY_ACCOUNT_TYPE, 520 accountType); 521 fragmentArguments.putString(AccountDetailDashboardFragment.KEY_ACCOUNT_LABEL, 522 label.toString()); 523 fragmentArguments.putInt(AccountDetailDashboardFragment.KEY_ACCOUNT_TITLE_RES, 524 titleResId); 525 fragmentArguments.putParcelable(EXTRA_USER, userHandle); 526 accountTypePreferences.add(new AccountTypePreference( 527 prefContext, mMetricsFeatureProvider.getMetricsCategory(mParent), 528 account, titleResPackageName, titleResId, label, 529 AccountDetailDashboardFragment.class.getName(), fragmentArguments, icon)); 530 } 531 helper.preloadDrawableForType(mContext, accountType); 532 } 533 // Sort by label 534 Collections.sort(accountTypePreferences, new Comparator<AccountTypePreference>() { 535 @Override 536 public int compare(AccountTypePreference t1, AccountTypePreference t2) { 537 int result = t1.getSummary().toString().compareTo(t2.getSummary().toString()); 538 return result != 0 539 ? result : t1.getTitle().toString().compareTo(t2.getTitle().toString()); 540 } 541 }); 542 return accountTypePreferences; 543 } 544 545 private boolean accountTypeHasAnyRequestedAuthorities(AuthenticatorHelper helper, 546 String accountType) { 547 if (mAuthoritiesCount == 0) { 548 // No authorities required 549 return true; 550 } 551 final ArrayList<String> authoritiesForType = helper.getAuthoritiesForAccountType( 552 accountType); 553 if (authoritiesForType == null) { 554 Log.d(TAG, "No sync authorities for account type: " + accountType); 555 return false; 556 } 557 for (int j = 0; j < mAuthoritiesCount; j++) { 558 if (authoritiesForType.contains(mAuthorities[j])) { 559 return true; 560 } 561 } 562 return false; 563 } 564 565 private boolean isSingleProfile() { 566 return mUm.isLinkedUser() || mUm.getProfiles(UserHandle.myUserId()).size() == 1; 567 } 568 569 private class ManagedProfileBroadcastReceiver extends BroadcastReceiver { 570 private boolean mListeningToManagedProfileEvents; 571 572 @Override 573 public void onReceive(Context context, Intent intent) { 574 final String action = intent.getAction(); 575 Log.v(TAG, "Received broadcast: " + action); 576 if (action.equals(Intent.ACTION_MANAGED_PROFILE_REMOVED) 577 || action.equals(Intent.ACTION_MANAGED_PROFILE_ADDED)) { 578 // Clean old state 579 stopListeningToAccountUpdates(); 580 // Build new state 581 updateUi(); 582 listenToAccountUpdates(); 583 return; 584 } 585 Log.w(TAG, "Cannot handle received broadcast: " + intent.getAction()); 586 } 587 588 public void register(Context context) { 589 if (!mListeningToManagedProfileEvents) { 590 IntentFilter intentFilter = new IntentFilter(); 591 intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED); 592 intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); 593 context.registerReceiver(this, intentFilter); 594 mListeningToManagedProfileEvents = true; 595 } 596 } 597 598 public void unregister(Context context) { 599 if (mListeningToManagedProfileEvents) { 600 context.unregisterReceiver(this); 601 mListeningToManagedProfileEvents = false; 602 } 603 } 604 } 605 } 606