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 onCreate(Bundle savedInstanceState)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 onCreateOptionsMenu(Menu menu, MenuInflater inflater)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 onPrepareOptionsMenu(Menu menu)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 onResume()166 public void onResume() { 167 super.onResume(); 168 updateUi(); 169 mManagedProfileBroadcastReceiver.register(getActivity()); 170 listenToAccountUpdates(); 171 } 172 173 @Override onPause()174 public void onPause() { 175 super.onPause(); 176 stopListeningToAccountUpdates(); 177 mManagedProfileBroadcastReceiver.unregister(getActivity()); 178 cleanUpPreferences(); 179 } 180 181 @Override onAccountsUpdate(UserHandle userHandle)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 onPreferenceClick(Preference preference)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 updateUi()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 updateProfileUi(final UserInfo userInfo, boolean addCategory, PreferenceScreen parent)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 newAddAccountPreference(Context context)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 newRemoveWorkProfilePreference(Context context)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 cleanUpPreferences()302 private void cleanUpPreferences() { 303 PreferenceScreen preferenceScreen = getPreferenceScreen(); 304 if (preferenceScreen != null) { 305 preferenceScreen.removeAll(); 306 } 307 mProfiles.clear(); 308 } 309 listenToAccountUpdates()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 stopListeningToAccountUpdates()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 updateAccountTypes(ProfileData profileData)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 getAccountTypePreferences(AuthenticatorHelper helper, UserHandle userHandle)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 373 final Account[] accounts = AccountManager.get(getActivity()) 374 .getAccountsByTypeAsUser(accountType, userHandle); 375 final boolean skipToAccount = accounts.length == 1 376 && !helper.hasAccountPreferences(accountType); 377 378 if (skipToAccount) { 379 final Bundle fragmentArguments = new Bundle(); 380 fragmentArguments.putParcelable(AccountSyncSettings.ACCOUNT_KEY, 381 accounts[0]); 382 fragmentArguments.putParcelable(EXTRA_USER, userHandle); 383 384 accountTypePreferences.add(new AccountPreference(getActivity(), label, 385 AccountSyncSettings.class.getName(), fragmentArguments, 386 helper.getDrawableForType(getActivity(), accountType))); 387 } else { 388 final Bundle fragmentArguments = new Bundle(); 389 fragmentArguments.putString(ManageAccountsSettings.KEY_ACCOUNT_TYPE, accountType); 390 fragmentArguments.putString(ManageAccountsSettings.KEY_ACCOUNT_LABEL, 391 label.toString()); 392 fragmentArguments.putParcelable(EXTRA_USER, userHandle); 393 394 accountTypePreferences.add(new AccountPreference(getActivity(), label, 395 ManageAccountsSettings.class.getName(), fragmentArguments, 396 helper.getDrawableForType(getActivity(), accountType))); 397 } 398 helper.preloadDrawableForType(getActivity(), accountType); 399 } 400 // Sort by label 401 Collections.sort(accountTypePreferences, new Comparator<AccountPreference>() { 402 @Override 403 public int compare(AccountPreference t1, AccountPreference t2) { 404 return t1.mTitle.toString().compareTo(t2.mTitle.toString()); 405 } 406 }); 407 return accountTypePreferences; 408 } 409 accountTypeHasAnyRequestedAuthorities(AuthenticatorHelper helper, String accountType)410 private boolean accountTypeHasAnyRequestedAuthorities(AuthenticatorHelper helper, 411 String accountType) { 412 if (mAuthoritiesCount == 0) { 413 // No authorities required 414 return true; 415 } 416 final ArrayList<String> authoritiesForType = helper.getAuthoritiesForAccountType( 417 accountType); 418 if (authoritiesForType == null) { 419 Log.d(TAG, "No sync authorities for account type: " + accountType); 420 return false; 421 } 422 for (int j = 0; j < mAuthoritiesCount; j++) { 423 if (authoritiesForType.contains(mAuthorities[j])) { 424 return true; 425 } 426 } 427 return false; 428 } 429 430 private class AccountPreference extends Preference implements OnPreferenceClickListener { 431 /** 432 * Title of the tile that is shown to the user. 433 * @attr ref android.R.styleable#PreferenceHeader_title 434 */ 435 private final CharSequence mTitle; 436 437 /** 438 * Full class name of the fragment to display when this tile is 439 * selected. 440 * @attr ref android.R.styleable#PreferenceHeader_fragment 441 */ 442 private final String mFragment; 443 444 /** 445 * Optional arguments to supply to the fragment when it is 446 * instantiated. 447 */ 448 private final Bundle mFragmentArguments; 449 AccountPreference(Context context, CharSequence title, String fragment, Bundle fragmentArguments, Drawable icon)450 public AccountPreference(Context context, CharSequence title, String fragment, 451 Bundle fragmentArguments, Drawable icon) { 452 super(context); 453 mTitle = title; 454 mFragment = fragment; 455 mFragmentArguments = fragmentArguments; 456 setWidgetLayoutResource(R.layout.account_type_preference); 457 458 setTitle(title); 459 setIcon(icon); 460 461 setOnPreferenceClickListener(this); 462 } 463 464 @Override onPreferenceClick(Preference preference)465 public boolean onPreferenceClick(Preference preference) { 466 if (mFragment != null) { 467 Utils.startWithFragment( 468 getContext(), mFragment, mFragmentArguments, null, 0, 0, mTitle); 469 return true; 470 } 471 return false; 472 } 473 } 474 475 private class ManagedProfileBroadcastReceiver extends BroadcastReceiver { 476 private boolean listeningToManagedProfileEvents; 477 478 @Override onReceive(Context context, Intent intent)479 public void onReceive(Context context, Intent intent) { 480 if (intent.getAction().equals(Intent.ACTION_MANAGED_PROFILE_REMOVED) 481 || intent.getAction().equals(Intent.ACTION_MANAGED_PROFILE_ADDED)) { 482 Log.v(TAG, "Received broadcast: " + intent.getAction()); 483 // Clean old state 484 stopListeningToAccountUpdates(); 485 cleanUpPreferences(); 486 // Build new state 487 updateUi(); 488 listenToAccountUpdates(); 489 // Force the menu to update. Note that #onPrepareOptionsMenu uses data built by 490 // #updateUi so we must call this later 491 getActivity().invalidateOptionsMenu(); 492 return; 493 } 494 Log.w(TAG, "Cannot handle received broadcast: " + intent.getAction()); 495 } 496 register(Context context)497 public void register(Context context) { 498 if (!listeningToManagedProfileEvents) { 499 IntentFilter intentFilter = new IntentFilter(); 500 intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED); 501 intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); 502 context.registerReceiver(this, intentFilter); 503 listeningToManagedProfileEvents = true; 504 } 505 } 506 unregister(Context context)507 public void unregister(Context context) { 508 if (listeningToManagedProfileEvents) { 509 context.unregisterReceiver(this); 510 listeningToManagedProfileEvents = false; 511 } 512 } 513 } 514 515 private class MasterSyncStateClickListener implements MenuItem.OnMenuItemClickListener { 516 private final UserHandle mUserHandle; 517 MasterSyncStateClickListener(UserHandle userHandle)518 public MasterSyncStateClickListener(UserHandle userHandle) { 519 mUserHandle = userHandle; 520 } 521 522 @Override onMenuItemClick(MenuItem item)523 public boolean onMenuItemClick(MenuItem item) { 524 if (ActivityManager.isUserAMonkey()) { 525 Log.d(TAG, "ignoring monkey's attempt to flip sync state"); 526 } else { 527 ConfirmAutoSyncChangeFragment.show(AccountSettings.this, !item.isChecked(), 528 mUserHandle); 529 } 530 return true; 531 } 532 } 533 534 /** 535 * Dialog to inform user about changing auto-sync setting 536 */ 537 public static class ConfirmAutoSyncChangeFragment extends DialogFragment { 538 private static final String SAVE_ENABLING = "enabling"; 539 private boolean mEnabling; 540 private UserHandle mUserHandle; 541 show(AccountSettings parent, boolean enabling, UserHandle userHandle)542 public static void show(AccountSettings parent, boolean enabling, UserHandle userHandle) { 543 if (!parent.isAdded()) return; 544 545 final ConfirmAutoSyncChangeFragment dialog = new ConfirmAutoSyncChangeFragment(); 546 dialog.mEnabling = enabling; 547 dialog.mUserHandle = userHandle; 548 dialog.setTargetFragment(parent, 0); 549 dialog.show(parent.getFragmentManager(), TAG_CONFIRM_AUTO_SYNC_CHANGE); 550 } 551 552 @Override onCreateDialog(Bundle savedInstanceState)553 public Dialog onCreateDialog(Bundle savedInstanceState) { 554 final Context context = getActivity(); 555 if (savedInstanceState != null) { 556 mEnabling = savedInstanceState.getBoolean(SAVE_ENABLING); 557 } 558 559 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 560 if (!mEnabling) { 561 builder.setTitle(R.string.data_usage_auto_sync_off_dialog_title); 562 builder.setMessage(R.string.data_usage_auto_sync_off_dialog); 563 } else { 564 builder.setTitle(R.string.data_usage_auto_sync_on_dialog_title); 565 builder.setMessage(R.string.data_usage_auto_sync_on_dialog); 566 } 567 568 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 569 @Override 570 public void onClick(DialogInterface dialog, int which) { 571 ContentResolver.setMasterSyncAutomaticallyAsUser(mEnabling, 572 mUserHandle.getIdentifier()); 573 } 574 }); 575 builder.setNegativeButton(android.R.string.cancel, null); 576 577 return builder.create(); 578 } 579 580 @Override onSaveInstanceState(Bundle outState)581 public void onSaveInstanceState(Bundle outState) { 582 super.onSaveInstanceState(outState); 583 outState.putBoolean(SAVE_ENABLING, mEnabling); 584 } 585 } 586 // TODO Implement a {@link SearchIndexProvider} to allow Indexing and Search of account types 587 // See http://b/15403806 588 } 589