diff --git a/res/xml/account_type_settings.xml b/res/xml/account_type_settings.xml index 6663d7b85ab..ab997a577f1 100644 --- a/res/xml/account_type_settings.xml +++ b/res/xml/account_type_settings.xml @@ -23,22 +23,22 @@ android:key="account_header" android:layout="@layout/account_header" android:selectable="false" - android:order="0"/> + android:order="-10000"/> + android:order="-9999"/> + android:order="-9998"/> diff --git a/src/com/android/settings/accounts/AccountDetailDashboardFragment.java b/src/com/android/settings/accounts/AccountDetailDashboardFragment.java index 8143e96cd35..801a20b3f89 100644 --- a/src/com/android/settings/accounts/AccountDetailDashboardFragment.java +++ b/src/com/android/settings/accounts/AccountDetailDashboardFragment.java @@ -24,6 +24,7 @@ import android.os.UserManager; import android.support.annotation.VisibleForTesting; import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.Utils; @@ -84,7 +85,7 @@ public class AccountDetailDashboardFragment extends DashboardFragment { if (mAccountLabel != null) { getActivity().setTitle(mAccountLabel); } - updateAccountHeader(); + updateUi(); } @Override @@ -125,7 +126,7 @@ public class AccountDetailDashboardFragment extends DashboardFragment { } @VisibleForTesting - void updateAccountHeader() { + void updateUi() { final Preference headerPreference = findPreference(KEY_ACCOUNT_HEADER); headerPreference.setTitle(mAccount.name); final Context context = getContext(); @@ -136,6 +137,13 @@ public class AccountDetailDashboardFragment extends DashboardFragment { } final AuthenticatorHelper helper = new AuthenticatorHelper(context, userHandle, null); headerPreference.setIcon(helper.getDrawableForType(context, mAccountType)); + final AccountTypePreferenceLoader accountTypePreferenceLoader = + new AccountTypePreferenceLoader(this, helper, userHandle); + PreferenceScreen prefs = + accountTypePreferenceLoader.addPreferencesForType(mAccountType, getPreferenceScreen()); + if (prefs != null) { + accountTypePreferenceLoader.updatePreferenceIntents(prefs, mAccountType, mAccount); + } } } \ No newline at end of file diff --git a/src/com/android/settings/accounts/AccountPreferenceBase.java b/src/com/android/settings/accounts/AccountPreferenceBase.java index aa5c5183eac..605688ea4f0 100644 --- a/src/com/android/settings/accounts/AccountPreferenceBase.java +++ b/src/com/android/settings/accounts/AccountPreferenceBase.java @@ -58,6 +58,7 @@ abstract class AccountPreferenceBase extends SettingsPreferenceFragment private Object mStatusChangeListenerHandle; protected AuthenticatorHelper mAuthenticatorHelper; protected UserHandle mUserHandle; + protected AccountTypePreferenceLoader mAccountTypePreferenceLoader; private java.text.DateFormat mDateFormat; private java.text.DateFormat mTimeFormat; @@ -70,6 +71,8 @@ abstract class AccountPreferenceBase extends SettingsPreferenceFragment mUserHandle = Utils.getSecureTargetUser(activity.getActivityToken(), mUm, getArguments(), activity.getIntent().getExtras()); mAuthenticatorHelper = new AuthenticatorHelper(activity, mUserHandle, this); + mAccountTypePreferenceLoader = + new AccountTypePreferenceLoader(this, mAuthenticatorHelper, mUserHandle); } /** @@ -142,35 +145,7 @@ abstract class AccountPreferenceBase extends SettingsPreferenceFragment */ public PreferenceScreen addPreferencesForType(final String accountType, PreferenceScreen parent) { - PreferenceScreen prefs = null; - if (mAuthenticatorHelper.containsAccountType(accountType)) { - AuthenticatorDescription desc = null; - try { - desc = mAuthenticatorHelper.getAccountTypeDescription(accountType); - if (desc != null && desc.accountPreferencesId != 0) { - // Load the context of the target package, then apply the - // base Settings theme (no references to local resources) - // and create a context theme wrapper so that we get the - // correct text colors. Control colors will still be wrong, - // but there's not much we can do about it since we can't - // reference local color resources. - final Context targetCtx = getActivity().createPackageContextAsUser( - desc.packageName, 0, mUserHandle); - final Theme baseTheme = getResources().newTheme(); - baseTheme.applyStyle(com.android.settings.R.style.Theme_SettingsBase, true); - final Context themedCtx = - new LocalClassLoaderContextThemeWrapper(getClass(), targetCtx, 0); - themedCtx.getTheme().setTo(baseTheme); - prefs = getPreferenceManager().inflateFromResource(themedCtx, - desc.accountPreferencesId, parent); - } - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Couldn't load preferences.xml file from " + desc.packageName); - } catch (Resources.NotFoundException e) { - Log.w(TAG, "Couldn't load preferences.xml file from " + desc.packageName); - } - } - return prefs; + return mAccountTypePreferenceLoader.addPreferencesForType(accountType, parent); } public void updateAuthDescriptions() { diff --git a/src/com/android/settings/accounts/AccountTypePreferenceLoader.java b/src/com/android/settings/accounts/AccountTypePreferenceLoader.java new file mode 100644 index 00000000000..87f9c77373a --- /dev/null +++ b/src/com/android/settings/accounts/AccountTypePreferenceLoader.java @@ -0,0 +1,245 @@ +/* + + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accounts; + +import android.accounts.Account; +import android.accounts.AuthenticatorDescription; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.content.res.Resources.Theme; +import android.os.UserHandle; +import android.support.v14.preference.PreferenceFragment; +import android.support.v7.preference.Preference; +import android.support.v7.preference.Preference.OnPreferenceClickListener; +import android.support.v7.preference.PreferenceGroup; +import android.support.v7.preference.PreferenceScreen; +import android.util.Log; + +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.location.LocationSettings; +import com.android.settings.utils.LocalClassLoaderContextThemeWrapper; +import com.android.settingslib.accounts.AuthenticatorHelper; + +/** + * Class to load the preference screen to be added to the settings page for the specific account + * type as specified in the account-authenticator. + */ +public class AccountTypePreferenceLoader { + + private static final String TAG = "AccountTypePrefLoader"; + private static final String ACCOUNT_KEY = "account"; // to pass to auth settings + // Action name for the broadcast intent when the Google account preferences page is launching + // the location settings. + private static final String LAUNCHING_LOCATION_SETTINGS = + "com.android.settings.accounts.LAUNCHING_LOCATION_SETTINGS"; + + + private AuthenticatorHelper mAuthenticatorHelper; + private UserHandle mUserHandle; + private PreferenceFragment mFragment; + + public AccountTypePreferenceLoader(PreferenceFragment fragment, + AuthenticatorHelper authenticatorHelper, UserHandle userHandle) { + mFragment = fragment; + mAuthenticatorHelper = authenticatorHelper; + mUserHandle = userHandle; + } + + /** + * Gets the preferences.xml file associated with a particular account type. + * @param accountType the type of account + * @return a PreferenceScreen inflated from accountPreferenceId. + */ + public PreferenceScreen addPreferencesForType(final String accountType, + PreferenceScreen parent) { + PreferenceScreen prefs = null; + if (mAuthenticatorHelper.containsAccountType(accountType)) { + AuthenticatorDescription desc = null; + try { + desc = mAuthenticatorHelper.getAccountTypeDescription(accountType); + if (desc != null && desc.accountPreferencesId != 0) { + // Load the context of the target package, then apply the + // base Settings theme (no references to local resources) + // and create a context theme wrapper so that we get the + // correct text colors. Control colors will still be wrong, + // but there's not much we can do about it since we can't + // reference local color resources. + final Context targetCtx = mFragment.getActivity().createPackageContextAsUser( + desc.packageName, 0, mUserHandle); + final Theme baseTheme = mFragment.getResources().newTheme(); + baseTheme.applyStyle(R.style.Theme_SettingsBase, true); + final Context themedCtx = + new LocalClassLoaderContextThemeWrapper(getClass(), targetCtx, 0); + themedCtx.getTheme().setTo(baseTheme); + prefs = mFragment.getPreferenceManager().inflateFromResource(themedCtx, + desc.accountPreferencesId, parent); + } + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Couldn't load preferences.xml file from " + desc.packageName); + } catch (Resources.NotFoundException e) { + Log.w(TAG, "Couldn't load preferences.xml file from " + desc.packageName); + } + } + return prefs; + } + + /** + * Recursively filters through the preference list provided by GoogleLoginService. + * + * This method removes all the invalid intent from the list, adds account name as extra into the + * intent, and hack the location settings to start it as a fragment. + */ + public void updatePreferenceIntents(PreferenceGroup prefs, final String acccountType, + Account account) { + final PackageManager pm = mFragment.getActivity().getPackageManager(); + for (int i = 0; i < prefs.getPreferenceCount(); ) { + Preference pref = prefs.getPreference(i); + if (pref instanceof PreferenceGroup) { + updatePreferenceIntents((PreferenceGroup) pref, acccountType, account); + } + Intent intent = pref.getIntent(); + if (intent != null) { + // Hack. Launch "Location" as fragment instead of as activity. + // + // When "Location" is launched as activity via Intent, there's no "Up" button at the + // top left, and if there's another running instance of "Location" activity, the + // back stack would usually point to some other place so the user won't be able to + // go back to the previous page by "back" key. Using fragment is a much easier + // solution to those problems. + // + // If we set Intent to null and assign a fragment to the PreferenceScreen item here, + // in order to make it work as expected, we still need to modify the container + // PreferenceActivity, override onPreferenceStartFragment() and call + // startPreferencePanel() there. In order to inject the title string there, more + // dirty further hack is still needed. It's much easier and cleaner to listen to + // preference click event here directly. + if (intent.getAction().equals( + android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)) { + // The OnPreferenceClickListener overrides the click event completely. No intent + // will get fired. + pref.setOnPreferenceClickListener(new FragmentStarter( + LocationSettings.class.getName(), R.string.location_settings_title)); + } else { + ResolveInfo ri = pm.resolveActivityAsUser(intent, + PackageManager.MATCH_DEFAULT_ONLY, mUserHandle.getIdentifier()); + if (ri == null) { + prefs.removePreference(pref); + continue; + } + intent.putExtra(ACCOUNT_KEY, account); + intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); + pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Intent prefIntent = preference.getIntent(); + /* + * Check the intent to see if it resolves to a exported=false + * activity that doesn't share a uid with the authenticator. + * + * Otherwise the intent is considered unsafe in that it will be + * exploiting the fact that settings has system privileges. + */ + if (isSafeIntent(pm, prefIntent, acccountType)) { + mFragment.getActivity().startActivityAsUser( + prefIntent, mUserHandle); + } else { + Log.e(TAG, + "Refusing to launch authenticator intent because" + + "it exploits Settings permissions: " + + prefIntent); + } + return true; + } + }); + } + } + i++; + } + } + + /** + * Determines if the supplied Intent is safe. A safe intent is one that is + * will launch a exported=true activity or owned by the same uid as the + * authenticator supplying the intent. + */ + private boolean isSafeIntent(PackageManager pm, Intent intent, String acccountType) { + AuthenticatorDescription authDesc = + mAuthenticatorHelper.getAccountTypeDescription(acccountType); + ResolveInfo resolveInfo = pm.resolveActivityAsUser(intent, 0, mUserHandle.getIdentifier()); + if (resolveInfo == null) { + return false; + } + ActivityInfo resolvedActivityInfo = resolveInfo.activityInfo; + ApplicationInfo resolvedAppInfo = resolvedActivityInfo.applicationInfo; + try { + if (resolvedActivityInfo.exported) { + if (resolvedActivityInfo.permission == null) { + return true; // exported activity without permission. + } else if (pm.checkPermission(resolvedActivityInfo.permission, + authDesc.packageName) == PackageManager.PERMISSION_GRANTED) { + return true; + } + } + ApplicationInfo authenticatorAppInf = pm.getApplicationInfo(authDesc.packageName, 0); + return resolvedAppInfo.uid == authenticatorAppInf.uid; + } catch (NameNotFoundException e) { + Log.e(TAG, + "Intent considered unsafe due to exception.", + e); + return false; + } + } + + /** Listens to a preference click event and starts a fragment */ + private class FragmentStarter + implements Preference.OnPreferenceClickListener { + private final String mClass; + private final int mTitleRes; + + /** + * @param className the class name of the fragment to be started. + * @param title the title resource id of the started preference panel. + */ + public FragmentStarter(String className, int title) { + mClass = className; + mTitleRes = title; + } + + @Override + public boolean onPreferenceClick(Preference preference) { + ((SettingsActivity) mFragment.getActivity()).startPreferencePanel( + mClass, null, mTitleRes, null, null, 0); + // Hack: announce that the Google account preferences page is launching the location + // settings + if (mClass.equals(LocationSettings.class.getName())) { + Intent intent = new Intent(LAUNCHING_LOCATION_SETTINGS); + mFragment.getActivity().sendBroadcast( + intent, android.Manifest.permission.WRITE_SECURE_SETTINGS); + } + return true; + } + } + +} diff --git a/src/com/android/settings/accounts/ManageAccountsSettings.java b/src/com/android/settings/accounts/ManageAccountsSettings.java index e6569e9e2e6..7c5ee003179 100644 --- a/src/com/android/settings/accounts/ManageAccountsSettings.java +++ b/src/com/android/settings/accounts/ManageAccountsSettings.java @@ -18,26 +18,17 @@ package com.android.settings.accounts; import android.accounts.Account; import android.accounts.AccountManager; -import android.accounts.AuthenticatorDescription; import android.app.ActionBar; import android.app.Activity; import android.content.ContentResolver; -import android.content.Intent; import android.content.SyncAdapterType; import android.content.SyncInfo; import android.content.SyncStatusInfo; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; import android.support.annotation.VisibleForTesting; import android.support.v7.preference.Preference; -import android.support.v7.preference.Preference.OnPreferenceClickListener; -import android.support.v7.preference.PreferenceGroup; import android.support.v7.preference.PreferenceScreen; import android.util.ArraySet; import android.util.Log; @@ -53,7 +44,6 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.Utils; -import com.android.settings.location.LocationSettings; import com.android.settingslib.accounts.AuthenticatorHelper; import java.util.ArrayList; @@ -66,15 +56,9 @@ import static android.content.Intent.EXTRA_USER; /** Manages settings for Google Account. */ public class ManageAccountsSettings extends AccountPreferenceBase implements AuthenticatorHelper.OnAccountsUpdateListener { - private static final String ACCOUNT_KEY = "account"; // to pass to auth settings public static final String KEY_ACCOUNT_TYPE = "account_type"; public static final String KEY_ACCOUNT_LABEL = "account_label"; - // Action name for the broadcast intent when the Google account preferences page is launching - // the location settings. - private static final String LAUNCHING_LOCATION_SETTINGS = - "com.android.settings.accounts.LAUNCHING_LOCATION_SETTINGS"; - private static final int MENU_SYNC_NOW_ID = Menu.FIRST; private static final int MENU_SYNC_CANCEL_ID = Menu.FIRST + 1; @@ -414,145 +398,7 @@ public class ManageAccountsSettings extends AccountPreferenceBase private void addAuthenticatorSettings() { PreferenceScreen prefs = addPreferencesForType(mAccountType, getPreferenceScreen()); if (prefs != null) { - updatePreferenceIntents(prefs); - } - } - - /** Listens to a preference click event and starts a fragment */ - private class FragmentStarter - implements Preference.OnPreferenceClickListener { - private final String mClass; - private final int mTitleRes; - - /** - * @param className the class name of the fragment to be started. - * @param title the title resource id of the started preference panel. - */ - public FragmentStarter(String className, int title) { - mClass = className; - mTitleRes = title; - } - - @Override - public boolean onPreferenceClick(Preference preference) { - ((SettingsActivity) getActivity()).startPreferencePanel( - mClass, null, mTitleRes, null, null, 0); - // Hack: announce that the Google account preferences page is launching the location - // settings - if (mClass.equals(LocationSettings.class.getName())) { - Intent intent = new Intent(LAUNCHING_LOCATION_SETTINGS); - getActivity().sendBroadcast( - intent, android.Manifest.permission.WRITE_SECURE_SETTINGS); - } - return true; - } - } - - /** - * Recursively filters through the preference list provided by GoogleLoginService. - * - * This method removes all the invalid intent from the list, adds account name as extra into the - * intent, and hack the location settings to start it as a fragment. - */ - private void updatePreferenceIntents(PreferenceGroup prefs) { - final PackageManager pm = getActivity().getPackageManager(); - for (int i = 0; i < prefs.getPreferenceCount(); ) { - Preference pref = prefs.getPreference(i); - if (pref instanceof PreferenceGroup) { - updatePreferenceIntents((PreferenceGroup) pref); - } - Intent intent = pref.getIntent(); - if (intent != null) { - // Hack. Launch "Location" as fragment instead of as activity. - // - // When "Location" is launched as activity via Intent, there's no "Up" button at the - // top left, and if there's another running instance of "Location" activity, the - // back stack would usually point to some other place so the user won't be able to - // go back to the previous page by "back" key. Using fragment is a much easier - // solution to those problems. - // - // If we set Intent to null and assign a fragment to the PreferenceScreen item here, - // in order to make it work as expected, we still need to modify the container - // PreferenceActivity, override onPreferenceStartFragment() and call - // startPreferencePanel() there. In order to inject the title string there, more - // dirty further hack is still needed. It's much easier and cleaner to listen to - // preference click event here directly. - if (intent.getAction().equals( - android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)) { - // The OnPreferenceClickListener overrides the click event completely. No intent - // will get fired. - pref.setOnPreferenceClickListener(new FragmentStarter( - LocationSettings.class.getName(), - R.string.location_settings_title)); - } else { - ResolveInfo ri = pm.resolveActivityAsUser(intent, - PackageManager.MATCH_DEFAULT_ONLY, mUserHandle.getIdentifier()); - if (ri == null) { - prefs.removePreference(pref); - continue; - } else { - intent.putExtra(ACCOUNT_KEY, mFirstAccount); - intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); - pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - Intent prefIntent = preference.getIntent(); - /* - * Check the intent to see if it resolves to a exported=false - * activity that doesn't share a uid with the authenticator. - * - * Otherwise the intent is considered unsafe in that it will be - * exploiting the fact that settings has system privileges. - */ - if (isSafeIntent(pm, prefIntent)) { - getActivity().startActivityAsUser(prefIntent, mUserHandle); - } else { - Log.e(TAG, - "Refusing to launch authenticator intent because" - + "it exploits Settings permissions: " - + prefIntent); - } - return true; - } - }); - } - } - } - i++; - } - } - - /** - * Determines if the supplied Intent is safe. A safe intent is one that is - * will launch a exported=true activity or owned by the same uid as the - * authenticator supplying the intent. - */ - private boolean isSafeIntent(PackageManager pm, Intent intent) { - AuthenticatorDescription authDesc = - mAuthenticatorHelper.getAccountTypeDescription(mAccountType); - ResolveInfo resolveInfo = - pm.resolveActivityAsUser(intent, 0, mUserHandle.getIdentifier()); - if (resolveInfo == null) { - return false; - } - ActivityInfo resolvedActivityInfo = resolveInfo.activityInfo; - ApplicationInfo resolvedAppInfo = resolvedActivityInfo.applicationInfo; - try { - if (resolvedActivityInfo.exported) { - if (resolvedActivityInfo.permission == null) { - return true; // exported activity without permission. - } else if (pm.checkPermission(resolvedActivityInfo.permission, - authDesc.packageName) == PackageManager.PERMISSION_GRANTED) { - return true; - } - } - ApplicationInfo authenticatorAppInf = pm.getApplicationInfo(authDesc.packageName, 0); - return resolvedAppInfo.uid == authenticatorAppInf.uid; - } catch (NameNotFoundException e) { - Log.e(TAG, - "Intent considered unsafe due to exception.", - e); - return false; + mAccountTypePreferenceLoader.updatePreferenceIntents(prefs, mAccountType, mFirstAccount); } } diff --git a/tests/robotests/src/com/android/settings/accounts/AccountDetailDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/accounts/AccountDetailDashboardFragmentTest.java index 31090be284b..fd0ab4b9480 100644 --- a/tests/robotests/src/com/android/settings/accounts/AccountDetailDashboardFragmentTest.java +++ b/tests/robotests/src/com/android/settings/accounts/AccountDetailDashboardFragmentTest.java @@ -22,6 +22,7 @@ import android.content.Context; import android.os.Bundle; import android.os.UserHandle; import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.SettingsRobolectricTestRunner; @@ -61,6 +62,8 @@ public class AccountDetailDashboardFragmentTest { private AccountManager mAccountManager; @Mock private Preference mPreference; + @Mock + private PreferenceScreen mScreen; private AccountDetailDashboardFragment mFragment; private Context mContext; @@ -125,9 +128,10 @@ public class AccountDetailDashboardFragmentTest { new AuthenticatorDescription[0]); when(mAccountManager.getAccountsAsUser(anyInt())).thenReturn(new Account[0]); when(mFragment.getContext()).thenReturn(mContext); + doReturn(mScreen).when(mFragment).getPreferenceScreen(); doReturn(mPreference).when(mFragment).findPreference(PREF_ACCOUNT_HEADER); - mFragment.updateAccountHeader(); + mFragment.updateUi(); verify(mPreference).setTitle("name1@abc.com"); } diff --git a/tests/robotests/src/com/android/settings/accounts/AccountTypePreferenceLoaderTest.java b/tests/robotests/src/com/android/settings/accounts/AccountTypePreferenceLoaderTest.java new file mode 100644 index 00000000000..29f254d9aad --- /dev/null +++ b/tests/robotests/src/com/android/settings/accounts/AccountTypePreferenceLoaderTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accounts; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorDescription; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.UserHandle; +import android.support.v14.preference.PreferenceFragment; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceGroup; +import android.support.v7.preference.PreferenceManager; +import android.support.v7.preference.PreferenceScreen; + +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.testutils.shadow.ShadowAccountManager; +import com.android.settings.testutils.shadow.ShadowContentResolver; +import com.android.settingslib.accounts.AuthenticatorHelper; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowApplication; + +import static org.mockito.Answers.RETURNS_DEEP_STUBS; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class AccountTypePreferenceLoaderTest { + + @Mock(answer = RETURNS_DEEP_STUBS) + private AccountManager mAccountManager; + @Mock(answer = RETURNS_DEEP_STUBS) + private PreferenceFragment mPreferenceFragment; + @Mock + private PackageManager mPackageManager; + + private Context mContext; + private Account mAccount; + private AccountTypePreferenceLoader mPrefLoader; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + ShadowApplication shadowContext = ShadowApplication.getInstance(); + shadowContext.setSystemService(Context.ACCOUNT_SERVICE, mAccountManager); + when(mAccountManager.getAuthenticatorTypesAsUser(anyInt())).thenReturn( + new AuthenticatorDescription[0]); + when(mAccountManager.getAccountsAsUser(anyInt())).thenReturn(new Account[0]); + when(mPreferenceFragment.getActivity().getPackageManager()).thenReturn(mPackageManager); + mContext = shadowContext.getApplicationContext(); + mAccount = new Account("name", "type"); + final AuthenticatorHelper helper = new AuthenticatorHelper(mContext, UserHandle.CURRENT, + null /* OnAccountsUpdateListener */); + mPrefLoader = spy(new AccountTypePreferenceLoader(mPreferenceFragment, helper, + UserHandle.CURRENT)); + } + + @Test + @Config(shadows = {ShadowAccountManager.class, ShadowContentResolver.class}) + public void updatePreferenceIntents_shouldRunRecursively() { + final PreferenceManager preferenceManager = mock(PreferenceManager.class); + // Top level + PreferenceGroup prefRoot = spy(new PreferenceScreen(mContext, null)); + when(prefRoot.getPreferenceManager()).thenReturn(preferenceManager); + Preference pref1 = mock(Preference.class); + PreferenceGroup prefGroup2 = spy(new PreferenceScreen(mContext, null)); + when(prefGroup2.getPreferenceManager()).thenReturn(preferenceManager); + Preference pref3 = mock(Preference.class); + PreferenceGroup prefGroup4 = spy(new PreferenceScreen(mContext, null)); + when(prefGroup4.getPreferenceManager()).thenReturn(preferenceManager); + prefRoot.addPreference(pref1); + prefRoot.addPreference(prefGroup2); + prefRoot.addPreference(pref3); + prefRoot.addPreference(prefGroup4); + + // 2nd level + Preference pref21 = mock(Preference.class); + Preference pref22 = mock(Preference.class); + prefGroup2.addPreference(pref21); + prefGroup2.addPreference(pref22); + PreferenceGroup prefGroup41 = spy(new PreferenceScreen(mContext, null)); + when(prefGroup41.getPreferenceManager()).thenReturn(preferenceManager); + Preference pref42 = mock(Preference.class); + prefGroup4.addPreference(prefGroup41); + prefGroup4.addPreference(pref42); + + // 3rd level + Preference pref411 = mock(Preference.class); + Preference pref412 = mock(Preference.class); + prefGroup41.addPreference(pref411); + prefGroup41.addPreference(pref412); + + final String acctType = "testType"; + mPrefLoader.updatePreferenceIntents(prefRoot, acctType, mAccount); + + verify(mPrefLoader).updatePreferenceIntents(prefGroup2, acctType, mAccount); + verify(mPrefLoader).updatePreferenceIntents(prefGroup4, acctType, mAccount); + verify(mPrefLoader).updatePreferenceIntents(prefGroup41, acctType, mAccount); + } +}