1 /* 2 * Copyright (C) 2014 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 20 import android.accounts.Account; 21 import android.accounts.AccountManager; 22 import android.app.ActivityManager; 23 import android.app.AlertDialog; 24 import android.app.Dialog; 25 import android.app.DialogFragment; 26 import android.content.BroadcastReceiver; 27 import android.content.ContentResolver; 28 import android.content.Context; 29 import android.content.DialogInterface; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.content.pm.UserInfo; 33 import android.graphics.drawable.Drawable; 34 import android.os.Bundle; 35 import android.os.UserHandle; 36 import android.os.UserManager; 37 import android.os.Process; 38 import android.util.Log; 39 import android.util.SparseArray; 40 import android.view.Menu; 41 import android.view.MenuInflater; 42 import android.view.MenuItem; 43 import android.preference.Preference; 44 import android.preference.Preference.OnPreferenceClickListener; 45 import android.preference.PreferenceGroup; 46 import android.preference.PreferenceCategory; 47 import android.preference.PreferenceScreen; 48 49 import com.android.settings.R; 50 import com.android.settings.SettingsPreferenceFragment; 51 import com.android.settings.Utils; 52 53 import java.util.ArrayList; 54 import java.util.Collections; 55 import java.util.Comparator; 56 import java.util.List; 57 58 import static android.content.Intent.EXTRA_USER; 59 import static android.os.UserManager.DISALLOW_MODIFY_ACCOUNTS; 60 import static android.provider.Settings.EXTRA_AUTHORITIES; 61 62 /** 63 * Settings screen for the account types on the device. 64 * This shows all account types available for personal and work profiles. 65 * 66 * An extra {@link UserHandle} can be specified in the intent as {@link EXTRA_USER}, if the user for 67 * which the action needs to be performed is different to the one the Settings App will run in. 68 */ 69 public class AccountSettings extends SettingsPreferenceFragment 70 implements AuthenticatorHelper.OnAccountsUpdateListener, 71 OnPreferenceClickListener { 72 public static final String TAG = "AccountSettings"; 73 74 private static final String KEY_ACCOUNT = "account"; 75 76 private static final String ADD_ACCOUNT_ACTION = "android.settings.ADD_ACCOUNT_SETTINGS"; 77 private static final String TAG_CONFIRM_AUTO_SYNC_CHANGE = "confirmAutoSyncChange"; 78 79 private static final int ORDER_LAST = 1001; 80 private static final int ORDER_NEXT_TO_LAST = 1000; 81 82 private UserManager mUm; 83 private SparseArray<ProfileData> mProfiles = new SparseArray<ProfileData>(); 84 private ManagedProfileBroadcastReceiver mManagedProfileBroadcastReceiver 85 = new ManagedProfileBroadcastReceiver(); 86 private Preference mProfileNotAvailablePreference; 87 private String[] mAuthorities; 88 private int mAuthoritiesCount = 0; 89 90 /** 91 * Holds data related to the accounts belonging to one profile. 92 */ 93 private static class ProfileData { 94 /** 95 * The preference that displays the accounts. 96 */ 97 public PreferenceGroup preferenceGroup; 98 /** 99 * The preference that displays the add account button. 100 */ 101 public Preference addAccountPreference; 102 /** 103 * The preference that displays the button to remove the managed profile 104 */ 105 public Preference removeWorkProfilePreference; 106 /** 107 * The {@link AuthenticatorHelper} that holds accounts data for this profile. 108 */ 109 public AuthenticatorHelper authenticatorHelper; 110 /** 111 * The {@link UserInfo} of the profile. 112 */ 113 public UserInfo userInfo; 114 } 115 116 @Override 117 public void onCreate(Bundle savedInstanceState) { 118 super.onCreate(savedInstanceState); 119 mUm = (UserManager) getSystemService(Context.USER_SERVICE); 120 mProfileNotAvailablePreference = new Preference(getActivity()); 121 mAuthorities = getActivity().getIntent().getStringArrayExtra(EXTRA_AUTHORITIES); 122 if (mAuthorities != null) { 123 mAuthoritiesCount = mAuthorities.length; 124 } 125 setHasOptionsMenu(true); 126 } 127 128 @Override 129 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 130 inflater.inflate(R.menu.account_settings, menu); 131 super.onCreateOptionsMenu(menu, inflater); 132 } 133 134 @Override 135 public void onPrepareOptionsMenu(Menu menu) { 136 final UserHandle currentProfile = Process.myUserHandle(); 137 if (mProfiles.size() == 1) { 138 menu.findItem(R.id.account_settings_menu_auto_sync) 139 .setVisible(true) 140 .setOnMenuItemClickListener(new MasterSyncStateClickListener(currentProfile)) 141 .setChecked(ContentResolver.getMasterSyncAutomaticallyAsUser( 142 currentProfile.getIdentifier())); 143 menu.findItem(R.id.account_settings_menu_auto_sync_personal).setVisible(false); 144 menu.findItem(R.id.account_settings_menu_auto_sync_work).setVisible(false); 145 } else if (mProfiles.size() > 1) { 146 // We assume there's only one managed profile, otherwise UI needs to change 147 final UserHandle managedProfile = mProfiles.valueAt(1).userInfo.getUserHandle(); 148 149 menu.findItem(R.id.account_settings_menu_auto_sync_personal) 150 .setVisible(true) 151 .setOnMenuItemClickListener(new MasterSyncStateClickListener(currentProfile)) 152 .setChecked(ContentResolver.getMasterSyncAutomaticallyAsUser( 153 currentProfile.getIdentifier())); 154 menu.findItem(R.id.account_settings_menu_auto_sync_work) 155 .setVisible(true) 156 .setOnMenuItemClickListener(new MasterSyncStateClickListener(managedProfile)) 157 .setChecked(ContentResolver.getMasterSyncAutomaticallyAsUser( 158 managedProfile.getIdentifier())); 159 menu.findItem(R.id.account_settings_menu_auto_sync).setVisible(false); 160 } else { 161 Log.w(TAG, "Method onPrepareOptionsMenu called before mProfiles was initialized"); 162 } 163 } 164 165 @Override 166 public void onResume() { 167 super.onResume(); 168 updateUi(); 169 mManagedProfileBroadcastReceiver.register(getActivity()); 170 listenToAccountUpdates(); 171 } 172 173 @Override 174 public void onPause() { 175 super.onPause(); 176 stopListeningToAccountUpdates(); 177 mManagedProfileBroadcastReceiver.unregister(getActivity()); 178 cleanUpPreferences(); 179 } 180 181 @Override 182 public void onAccountsUpdate(UserHandle userHandle) { 183 final ProfileData profileData = mProfiles.get(userHandle.getIdentifier()); 184 if (profileData != null) { 185 updateAccountTypes(profileData); 186 } else { 187 Log.w(TAG, "Missing Settings screen for: " + userHandle.getIdentifier()); 188 } 189 } 190 191 @Override 192 public boolean onPreferenceClick(Preference preference) { 193 // Check the preference 194 final int count = mProfiles.size(); 195 for (int i = 0; i < count; i++) { 196 ProfileData profileData = mProfiles.valueAt(i); 197 if (preference == profileData.addAccountPreference) { 198 Intent intent = new Intent(ADD_ACCOUNT_ACTION); 199 intent.putExtra(EXTRA_USER, profileData.userInfo.getUserHandle()); 200 intent.putExtra(EXTRA_AUTHORITIES, mAuthorities); 201 startActivity(intent); 202 return true; 203 } 204 if (preference == profileData.removeWorkProfilePreference) { 205 final int userId = profileData.userInfo.id; 206 Utils.createRemoveConfirmationDialog(getActivity(), userId, 207 new DialogInterface.OnClickListener() { 208 @Override 209 public void onClick(DialogInterface dialog, int which) { 210 mUm.removeUser(userId); 211 } 212 } 213 ).show(); 214 return true; 215 } 216 } 217 return false; 218 } 219 220 void updateUi() { 221 // Load the preferences from an XML resource 222 addPreferencesFromResource(R.xml.account_settings); 223 224 if (Utils.isManagedProfile(mUm)) { 225 // This should not happen 226 Log.e(TAG, "We should not be showing settings for a managed profile"); 227 finish(); 228 return; 229 } 230 231 final PreferenceScreen preferenceScreen = (PreferenceScreen) findPreference(KEY_ACCOUNT); 232 if(mUm.isLinkedUser()) { 233 // Restricted user or similar 234 UserInfo userInfo = mUm.getUserInfo(UserHandle.myUserId()); 235 updateProfileUi(userInfo, false /* no category needed */, preferenceScreen); 236 } else { 237 List<UserInfo> profiles = mUm.getProfiles(UserHandle.myUserId()); 238 final int profilesCount = profiles.size(); 239 final boolean addCategory = profilesCount > 1; 240 for (int i = 0; i < profilesCount; i++) { 241 updateProfileUi(profiles.get(i), addCategory, preferenceScreen); 242 } 243 } 244 245 // Add all preferences, starting with one for the primary profile. 246 // Note that we're relying on the ordering given by the SparseArray keys, and on the 247 // value of UserHandle.USER_OWNER being smaller than all the rest. 248 final int profilesCount = mProfiles.size(); 249 for (int i = 0; i < profilesCount; i++) { 250 ProfileData profileData = mProfiles.valueAt(i); 251 if (!profileData.preferenceGroup.equals(preferenceScreen)) { 252 preferenceScreen.addPreference(profileData.preferenceGroup); 253 } 254 updateAccountTypes(profileData); 255 } 256 } 257 258 private void updateProfileUi(final UserInfo userInfo, boolean addCategory, 259 PreferenceScreen parent) { 260 final Context context = getActivity(); 261 final ProfileData profileData = new ProfileData(); 262 profileData.userInfo = userInfo; 263 if (addCategory) { 264 profileData.preferenceGroup = new PreferenceCategory(context); 265 profileData.preferenceGroup.setTitle(userInfo.isManagedProfile() 266 ? R.string.category_work : R.string.category_personal); 267 parent.addPreference(profileData.preferenceGroup); 268 } else { 269 profileData.preferenceGroup = parent; 270 } 271 if (userInfo.isEnabled()) { 272 profileData.authenticatorHelper = new AuthenticatorHelper(context, 273 userInfo.getUserHandle(), mUm, this); 274 if (!mUm.hasUserRestriction(DISALLOW_MODIFY_ACCOUNTS, userInfo.getUserHandle())) { 275 profileData.addAccountPreference = newAddAccountPreference(context); 276 } 277 } 278 if (userInfo.isManagedProfile()) { 279 profileData.removeWorkProfilePreference = newRemoveWorkProfilePreference(context); 280 } 281 mProfiles.put(userInfo.id, profileData); 282 } 283 284 private Preference newAddAccountPreference(Context context) { 285 Preference preference = new Preference(context); 286 preference.setTitle(R.string.add_account_label); 287 preference.setIcon(R.drawable.ic_menu_add_dark); 288 preference.setOnPreferenceClickListener(this); 289 preference.setOrder(ORDER_NEXT_TO_LAST); 290 return preference; 291 } 292 293 private Preference newRemoveWorkProfilePreference(Context context) { 294 Preference preference = new Preference(context); 295 preference.setTitle(R.string.remove_managed_profile_label); 296 preference.setIcon(R.drawable.ic_menu_delete); 297 preference.setOnPreferenceClickListener(this); 298 preference.setOrder(ORDER_LAST); 299 return preference; 300 } 301 302 private void cleanUpPreferences() { 303 PreferenceScreen preferenceScreen = getPreferenceScreen(); 304 if (preferenceScreen != null) { 305 preferenceScreen.removeAll(); 306 } 307 mProfiles.clear(); 308 } 309 310 private void listenToAccountUpdates() { 311 final int count = mProfiles.size(); 312 for (int i = 0; i < count; i++) { 313 AuthenticatorHelper authenticatorHelper = mProfiles.valueAt(i).authenticatorHelper; 314 if (authenticatorHelper != null) { 315 authenticatorHelper.listenToAccountUpdates(); 316 } 317 } 318 } 319 320 private void stopListeningToAccountUpdates() { 321 final int count = mProfiles.size(); 322 for (int i = 0; i < count; i++) { 323 AuthenticatorHelper authenticatorHelper = mProfiles.valueAt(i).authenticatorHelper; 324 if (authenticatorHelper != null) { 325 authenticatorHelper.stopListeningToAccountUpdates(); 326 } 327 } 328 } 329 330 private void updateAccountTypes(ProfileData profileData) { 331 profileData.preferenceGroup.removeAll(); 332 if (profileData.userInfo.isEnabled()) { 333 final ArrayList<AccountPreference> preferences = getAccountTypePreferences( 334 profileData.authenticatorHelper, profileData.userInfo.getUserHandle()); 335 final int count = preferences.size(); 336 for (int i = 0; i < count; i++) { 337 profileData.preferenceGroup.addPreference(preferences.get(i)); 338 } 339 if (profileData.addAccountPreference != null) { 340 profileData.preferenceGroup.addPreference(profileData.addAccountPreference); 341 } 342 } else { 343 // Put a label instead of the accounts list 344 mProfileNotAvailablePreference.setEnabled(false); 345 mProfileNotAvailablePreference.setIcon(R.drawable.empty_icon); 346 mProfileNotAvailablePreference.setTitle(null); 347 mProfileNotAvailablePreference.setSummary( 348 R.string.managed_profile_not_available_label); 349 profileData.preferenceGroup.addPreference(mProfileNotAvailablePreference); 350 } 351 if (profileData.removeWorkProfilePreference != null) { 352 profileData.preferenceGroup.addPreference(profileData.removeWorkProfilePreference); 353 } 354 } 355 356 private ArrayList<AccountPreference> getAccountTypePreferences(AuthenticatorHelper helper, 357 UserHandle userHandle) { 358 final String[] accountTypes = helper.getEnabledAccountTypes(); 359 final ArrayList<AccountPreference> accountTypePreferences = 360 new ArrayList<AccountPreference>(accountTypes.length); 361 362 for (int i = 0; i < accountTypes.length; i++) { 363 final String accountType = accountTypes[i]; 364 // Skip showing any account that does not have any of the requested authorities 365 if (!accountTypeHasAnyRequestedAuthorities(helper, accountType)) { 366 continue; 367 } 368 final CharSequence label = helper.getLabelForType(getActivity(), accountType); 369 if (label == null) { 370 continue; 371 } 372 final String titleResPackageName = helper.getPackageForType(accountType); 373 final int titleResId = helper.getLabelIdForType(accountType); 374 375 final Account[] accounts = AccountManager.get(getActivity()) 376 .getAccountsByTypeAsUser(accountType, userHandle); 377 final boolean skipToAccount = accounts.length == 1 378 && !helper.hasAccountPreferences(accountType); 379 380 if (skipToAccount) { 381 final Bundle fragmentArguments = new Bundle(); 382 fragmentArguments.putParcelable(AccountSyncSettings.ACCOUNT_KEY, 383 accounts[0]); 384 fragmentArguments.putParcelable(EXTRA_USER, userHandle); 385 386 accountTypePreferences.add(new AccountPreference(getActivity(), label, 387 titleResPackageName, titleResId, AccountSyncSettings.class.getName(), 388 fragmentArguments, 389 helper.getDrawableForType(getActivity(), accountType))); 390 } else { 391 final Bundle fragmentArguments = new Bundle(); 392 fragmentArguments.putString(ManageAccountsSettings.KEY_ACCOUNT_TYPE, accountType); 393 fragmentArguments.putString(ManageAccountsSettings.KEY_ACCOUNT_LABEL, 394 label.toString()); 395 fragmentArguments.putParcelable(EXTRA_USER, userHandle); 396 397 accountTypePreferences.add(new AccountPreference(getActivity(), label, 398 titleResPackageName, titleResId, ManageAccountsSettings.class.getName(), 399 fragmentArguments, 400 helper.getDrawableForType(getActivity(), accountType))); 401 } 402 helper.preloadDrawableForType(getActivity(), accountType); 403 } 404 // Sort by label 405 Collections.sort(accountTypePreferences, new Comparator<AccountPreference>() { 406 @Override 407 public int compare(AccountPreference t1, AccountPreference t2) { 408 return t1.mTitle.toString().compareTo(t2.mTitle.toString()); 409 } 410 }); 411 return accountTypePreferences; 412 } 413 414 private boolean accountTypeHasAnyRequestedAuthorities(AuthenticatorHelper helper, 415 String accountType) { 416 if (mAuthoritiesCount == 0) { 417 // No authorities required 418 return true; 419 } 420 final ArrayList<String> authoritiesForType = helper.getAuthoritiesForAccountType( 421 accountType); 422 if (authoritiesForType == null) { 423 Log.d(TAG, "No sync authorities for account type: " + accountType); 424 return false; 425 } 426 for (int j = 0; j < mAuthoritiesCount; j++) { 427 if (authoritiesForType.contains(mAuthorities[j])) { 428 return true; 429 } 430 } 431 return false; 432 } 433 434 private class AccountPreference extends Preference implements OnPreferenceClickListener { 435 /** 436 * Title of the tile that is shown to the user. 437 * @attr ref android.R.styleable#PreferenceHeader_title 438 */ 439 private final CharSequence mTitle; 440 441 /** 442 * Packange name used to resolve the resources of the title shown to the user in the new 443 * fragment. 444 */ 445 private final String mTitleResPackageName; 446 447 /** 448 * Resource id of the title shown to the user in the new fragment. 449 */ 450 private final int mTitleResId; 451 452 /** 453 * Full class name of the fragment to display when this tile is 454 * selected. 455 * @attr ref android.R.styleable#PreferenceHeader_fragment 456 */ 457 private final String mFragment; 458 459 /** 460 * Optional arguments to supply to the fragment when it is 461 * instantiated. 462 */ 463 private final Bundle mFragmentArguments; 464 465 public AccountPreference(Context context, CharSequence title, String titleResPackageName, 466 int titleResId, String fragment, Bundle fragmentArguments, 467 Drawable icon) { 468 super(context); 469 mTitle = title; 470 mTitleResPackageName = titleResPackageName; 471 mTitleResId = titleResId; 472 mFragment = fragment; 473 mFragmentArguments = fragmentArguments; 474 setWidgetLayoutResource(R.layout.account_type_preference); 475 476 setTitle(title); 477 setIcon(icon); 478 479 setOnPreferenceClickListener(this); 480 } 481 482 @Override 483 public boolean onPreferenceClick(Preference preference) { 484 if (mFragment != null) { 485 Utils.startWithFragment(getContext(), mFragment, mFragmentArguments, 486 null /* resultTo */, 0 /* resultRequestCode */, mTitleResPackageName, 487 mTitleResId, null /* title */); 488 return true; 489 } 490 return false; 491 } 492 } 493 494 private class ManagedProfileBroadcastReceiver extends BroadcastReceiver { 495 private boolean listeningToManagedProfileEvents; 496 497 @Override 498 public void onReceive(Context context, Intent intent) { 499 if (intent.getAction().equals(Intent.ACTION_MANAGED_PROFILE_REMOVED) 500 || intent.getAction().equals(Intent.ACTION_MANAGED_PROFILE_ADDED)) { 501 Log.v(TAG, "Received broadcast: " + intent.getAction()); 502 // Clean old state 503 stopListeningToAccountUpdates(); 504 cleanUpPreferences(); 505 // Build new state 506 updateUi(); 507 listenToAccountUpdates(); 508 // Force the menu to update. Note that #onPrepareOptionsMenu uses data built by 509 // #updateUi so we must call this later 510 getActivity().invalidateOptionsMenu(); 511 return; 512 } 513 Log.w(TAG, "Cannot handle received broadcast: " + intent.getAction()); 514 } 515 516 public void register(Context context) { 517 if (!listeningToManagedProfileEvents) { 518 IntentFilter intentFilter = new IntentFilter(); 519 intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED); 520 intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); 521 context.registerReceiver(this, intentFilter); 522 listeningToManagedProfileEvents = true; 523 } 524 } 525 526 public void unregister(Context context) { 527 if (listeningToManagedProfileEvents) { 528 context.unregisterReceiver(this); 529 listeningToManagedProfileEvents = false; 530 } 531 } 532 } 533 534 private class MasterSyncStateClickListener implements MenuItem.OnMenuItemClickListener { 535 private final UserHandle mUserHandle; 536 537 public MasterSyncStateClickListener(UserHandle userHandle) { 538 mUserHandle = userHandle; 539 } 540 541 @Override 542 public boolean onMenuItemClick(MenuItem item) { 543 if (ActivityManager.isUserAMonkey()) { 544 Log.d(TAG, "ignoring monkey's attempt to flip sync state"); 545 } else { 546 ConfirmAutoSyncChangeFragment.show(AccountSettings.this, !item.isChecked(), 547 mUserHandle); 548 } 549 return true; 550 } 551 } 552 553 /** 554 * Dialog to inform user about changing auto-sync setting 555 */ 556 public static class ConfirmAutoSyncChangeFragment extends DialogFragment { 557 private static final String SAVE_ENABLING = "enabling"; 558 private static final String SAVE_USER_HANDLE = "userHandle"; 559 private boolean mEnabling; 560 private UserHandle mUserHandle; 561 562 public static void show(AccountSettings parent, boolean enabling, UserHandle userHandle) { 563 if (!parent.isAdded()) return; 564 565 final ConfirmAutoSyncChangeFragment dialog = new ConfirmAutoSyncChangeFragment(); 566 dialog.mEnabling = enabling; 567 dialog.mUserHandle = userHandle; 568 dialog.setTargetFragment(parent, 0); 569 dialog.show(parent.getFragmentManager(), TAG_CONFIRM_AUTO_SYNC_CHANGE); 570 } 571 572 @Override 573 public Dialog onCreateDialog(Bundle savedInstanceState) { 574 final Context context = getActivity(); 575 if (savedInstanceState != null) { 576 mEnabling = savedInstanceState.getBoolean(SAVE_ENABLING); 577 mUserHandle = (UserHandle) savedInstanceState.getParcelable(SAVE_USER_HANDLE); 578 } 579 580 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 581 if (!mEnabling) { 582 builder.setTitle(R.string.data_usage_auto_sync_off_dialog_title); 583 builder.setMessage(R.string.data_usage_auto_sync_off_dialog); 584 } else { 585 builder.setTitle(R.string.data_usage_auto_sync_on_dialog_title); 586 builder.setMessage(R.string.data_usage_auto_sync_on_dialog); 587 } 588 589 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 590 @Override 591 public void onClick(DialogInterface dialog, int which) { 592 ContentResolver.setMasterSyncAutomaticallyAsUser(mEnabling, 593 mUserHandle.getIdentifier()); 594 } 595 }); 596 builder.setNegativeButton(android.R.string.cancel, null); 597 598 return builder.create(); 599 } 600 601 @Override 602 public void onSaveInstanceState(Bundle outState) { 603 super.onSaveInstanceState(outState); 604 outState.putBoolean(SAVE_ENABLING, mEnabling); 605 outState.putParcelable(SAVE_USER_HANDLE, mUserHandle); 606 } 607 } 608 // TODO Implement a {@link SearchIndexProvider} to allow Indexing and Search of account types 609 // See http://b/15403806 610 } 611