Add account listing and preference integration for current account

Bug: 17464068
Change-Id: Idb68a6012b285d6bc4632414bb6d11131148cf67
diff --git a/java-overridable/src/com/android/inputmethod/latin/utils/LoginAccountUtils.java b/java-overridable/src/com/android/inputmethod/latin/utils/LoginAccountUtils.java
new file mode 100644
index 0000000..faada29
--- /dev/null
+++ b/java-overridable/src/com/android/inputmethod/latin/utils/LoginAccountUtils.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2014 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.inputmethod.latin.utils;
+
+import android.content.Context;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Utility class for retrieving accounts that may be used for login.
+ */
+public class LoginAccountUtils {
+    private LoginAccountUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    /**
+     * Get the accounts available for login.
+     *
+     * @return an array of accounts. Empty (never null) if no accounts are available for login.
+     */
+    @Nonnull
+    public static String[] getAccountsForLogin(final Context context) {
+        return new String[0];
+    }
+}
diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml
index 054c415..b29a6e2 100644
--- a/java/AndroidManifest.xml
+++ b/java/AndroidManifest.xml
@@ -158,5 +158,9 @@
                 <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
+
+        <!-- Unexported activity used for tests. -->
+        <activity android:name=".settings.TestFragmentActivity"
+                android:exported="false" />
     </application>
 </manifest>
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index 3486cf5..d64444e 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -181,14 +181,20 @@
 
     <!-- Title of the preference item for switching accounts [CHAR LIMIT=30] -->
     <string name="switch_accounts">Switch accounts</string>
-
     <!-- Summary of the preference item for switching accounts when no accounts
          are selected [CHAR LIMIT=65] -->
     <string name="no_accounts_selected">No accounts selected</string>
-
     <!-- Summary of the preference item for switching accounts when an account
          is selected [CHAR LIMIT=65] -->
     <string name="account_selected">Currently using <xliff:g id="EMAIL_ADDRESS" example="someone@example.com">%1$s</xliff:g></string>
+    <!-- Positive text for selecting an account -->
+    <string name="account_select_ok">OK</string>
+    <!-- Negative text for selecting an account -->
+    <string name="account_select_cancel">Cancel</string>
+    <!-- Text for signing out of an account -->
+    <string name="account_select_sign_out">Sign out</string>
+    <!-- Title of the account picker dialog for selecting an account [CHAR LIMIT=40] -->
+    <string name="account_select_title">Select an account to use</string>
 
     <!-- Description for English (UK) keyboard subtype [CHAR LIMIT=25]
          (UK) should be an abbreviation of United Kingdom to fit in the CHAR LIMIT. -->
diff --git a/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java
index e9e73c7..06ab1e2 100644
--- a/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java
@@ -16,25 +16,41 @@
 
 package com.android.inputmethod.latin.settings;
 
+import android.app.AlertDialog;
 import android.content.Context;
+import android.content.DialogInterface;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
 import android.os.Bundle;
 import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.text.TextUtils;
+import android.widget.ListView;
+import android.widget.Toast;
 
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.SubtypeSwitcher;
 import com.android.inputmethod.latin.define.ProductionFlags;
+import com.android.inputmethod.latin.utils.LoginAccountUtils;
+
+import javax.annotation.Nullable;
 
 /**
  * "Accounts & Privacy" settings sub screen.
  *
  * This settings sub screen handles the following preferences:
- * - TODO: Account selection/management for IME
- * - TODO: Sync preferences
- * - TODO: Privacy preferences
+ * <li> Account selection/management for IME
+ * <li> TODO: Sync preferences
+ * <li> TODO: Privacy preferences
  */
 public final class AccountsSettingsFragment extends SubScreenFragment {
+    static final String PREF_ACCCOUNT_SWITCHER = "account_switcher";
+
+    private final DialogInterface.OnClickListener mAccountSelectedListener =
+            new AccountSelectedListener();
+    private final DialogInterface.OnClickListener mAccountSignedOutListener =
+            new AccountSignedOutListener();
+
     @Override
     public void onCreate(final Bundle icicle) {
         super.onCreate(icicle);
@@ -74,9 +90,104 @@
     }
 
     private void refreshAccountSelection() {
-        // TODO: Fetch the currently selected account.
-        // Set the summary for the account preference.
-        // Depending on the account selection, enable/disable preferences that
+        final String currentAccount = getCurrentlySelectedAccount();
+        final Preference accountSwitcher = findPreference(PREF_ACCCOUNT_SWITCHER);
+        if (currentAccount == null) {
+            // No account is currently selected.
+            accountSwitcher.setSummary(getString(R.string.no_accounts_selected));
+        } else {
+            // Set the currently selected account.
+            accountSwitcher.setSummary(getString(R.string.account_selected, currentAccount));
+        }
+        final Context context = getActivity();
+        final String[] accountsForLogin = LoginAccountUtils.getAccountsForLogin(context);
+        accountSwitcher.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+            @Override
+            public boolean onPreferenceClick(Preference preference) {
+                if (accountsForLogin.length == 0) {
+                    // TODO: Handle account addition.
+                    Toast.makeText(getActivity(),
+                            getString(R.string.account_select_cancel), Toast.LENGTH_SHORT).show();
+                } else {
+                    createAccountPicker(accountsForLogin, currentAccount).show();
+                }
+                return true;
+            }
+        });
+
+        // TODO: Depending on the account selection, enable/disable preferences that
         // depend on an account.
     }
+
+    @Nullable
+    private String getCurrentlySelectedAccount() {
+        return getSharedPreferences().getString(Settings.PREF_ACCOUNT_NAME, null);
+    }
+
+    /**
+     * Creates an account picker dialog showing the given accounts in a list and selecting
+     * the selected account by default.
+     * The list of accounts must not be null/empty.
+     *
+     * Package-private for testing.
+     */
+    AlertDialog createAccountPicker(final String[] accounts,
+            final String selectedAccount) {
+        if (accounts == null || accounts.length == 0) {
+            throw new IllegalArgumentException("List of accounts must not be empty");
+        }
+
+        // See if the currently selected account is in the list.
+        // If it is, the entry is selected, and a sign-out button is provided.
+        // If it isn't, select the 0th account by default which will get picked up
+        // if the user presses OK.
+        int index = 0;
+        boolean isSignedIn = false;
+        for (int i = 0;  i < accounts.length; i++) {
+            if (TextUtils.equals(accounts[i], selectedAccount)) {
+                index = i;
+                isSignedIn = true;
+                break;
+            }
+        }
+        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+                .setTitle(R.string.account_select_title)
+                .setSingleChoiceItems(accounts, index, null)
+                .setPositiveButton(R.string.account_select_ok, mAccountSelectedListener)
+                .setNegativeButton(R.string.account_select_cancel, null);
+        if (isSignedIn) {
+            builder.setNeutralButton(R.string.account_select_sign_out, mAccountSignedOutListener);
+        }
+        return builder.create();
+    }
+
+    /**
+     * Listener for an account being selected from the picker.
+     * Persists the account to shared preferences.
+     */
+    class AccountSelectedListener implements DialogInterface.OnClickListener {
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            final ListView lv = ((AlertDialog)dialog).getListView();
+            final Object selectedItem = lv.getItemAtPosition(lv.getCheckedItemPosition());
+            getSharedPreferences()
+                    .edit()
+                    .putString(Settings.PREF_ACCOUNT_NAME, (String) selectedItem)
+                    .apply();
+        }
+    }
+
+    /**
+     * Listener for sign-out being initiated from from the picker.
+     * Removed the account from shared preferences.
+     */
+    class AccountSignedOutListener implements DialogInterface.OnClickListener {
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            getSharedPreferences()
+                    .edit()
+                    .remove(Settings.PREF_ACCOUNT_NAME)
+                    .apply();
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java b/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java
new file mode 100644
index 0000000..254bc65
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 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.inputmethod.latin.settings;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * Test activity to use when testing preference fragments. <br/>
+ * Usage: <br/>
+ * Create an ActivityInstrumentationTestCase2 for this activity
+ * and call setIntent() with an intent that specifies the fragment to load in the activity.
+ * The fragment can then be obtained from this activity and used for testing/verification.
+ */
+public final class TestFragmentActivity extends Activity {
+    /**
+     * The fragment name that should be loaded when starting this activity.
+     * This must be specified when starting this activity, as this activity is only
+     * meant to test fragments from instrumentation tests.
+     */
+    public static final String EXTRA_SHOW_FRAGMENT = "show_fragment";
+
+    public Fragment mFragment;
+
+    @Override
+    protected void onCreate(final Bundle savedState) {
+        super.onCreate(savedState);
+        final Intent intent = getIntent();
+        final String fragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
+        if (fragmentName == null) {
+            throw new IllegalArgumentException("No fragment name specified for testing");
+        }
+
+        mFragment = Fragment.instantiate(this, fragmentName);
+        FragmentManager fragmentManager = getFragmentManager();
+        fragmentManager.beginTransaction().add(mFragment, fragmentName).commit();
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/settings/AccountsSettingsFragmentTests.java b/tests/src/com/android/inputmethod/latin/settings/AccountsSettingsFragmentTests.java
new file mode 100644
index 0000000..2ef8b54
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/settings/AccountsSettingsFragmentTests.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2014 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.inputmethod.latin.settings;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Intent;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.View;
+import android.widget.ListView;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+public class AccountsSettingsFragmentTests
+        extends ActivityInstrumentationTestCase2<TestFragmentActivity> {
+    private static final String FRAG_NAME = AccountsSettingsFragment.class.getName();
+    private static final long TEST_TIMEOUT_MILLIS = 5000;
+
+    private AlertDialog mDialog;
+
+    public AccountsSettingsFragmentTests() {
+        super(TestFragmentActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        Intent intent = new Intent();
+        intent.putExtra(TestFragmentActivity.EXTRA_SHOW_FRAGMENT, FRAG_NAME);
+        setActivityIntent(intent);
+    }
+
+    public void testEmptyAccounts() {
+        final AccountsSettingsFragment fragment =
+                (AccountsSettingsFragment) getActivity().mFragment;
+        try {
+            fragment.createAccountPicker(new String[0], null);
+            fail("Expected IllegalArgumentException, never thrown");
+        } catch (IllegalArgumentException expected) {
+            // Expected.
+        }
+    }
+
+    public void testMultipleAccounts_noCurrentAccount() {
+        final AccountsSettingsFragment fragment =
+                (AccountsSettingsFragment) getActivity().mFragment;
+        final CountDownLatch latch = new CountDownLatch(1);
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mDialog = fragment.createAccountPicker(
+                        new String[] {
+                                "1@example.com",
+                                "2@example.com",
+                                "3@example.com",
+                                "4@example.com"},
+                        null);
+                mDialog.show();
+                latch.countDown();
+            }
+        });
+
+        try {
+            latch.await(TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException ex) {
+            fail();
+        }
+        getInstrumentation().waitForIdleSync();
+        final ListView lv = mDialog.getListView();
+        // The 1st account should be checked by default.
+        assertEquals("checked-item", 0, lv.getCheckedItemPosition());
+        // There should be 4 accounts in the list.
+        assertEquals("count", 4, lv.getCount());
+        // The sign-out button shouldn't exist
+        assertEquals(View.GONE, mDialog.getButton(Dialog.BUTTON_NEUTRAL).getVisibility());
+        assertEquals(View.VISIBLE, mDialog.getButton(Dialog.BUTTON_NEGATIVE).getVisibility());
+        assertEquals(View.VISIBLE, mDialog.getButton(Dialog.BUTTON_POSITIVE).getVisibility());
+    }
+
+    public void testMultipleAccounts_currentAccount() {
+        final AccountsSettingsFragment fragment =
+                (AccountsSettingsFragment) getActivity().mFragment;
+        final CountDownLatch latch = new CountDownLatch(1);
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mDialog = fragment.createAccountPicker(
+                        new String[] {
+                                "1@example.com",
+                                "2@example.com",
+                                "3@example.com",
+                                "4@example.com"},
+                        "3@example.com");
+                mDialog.show();
+                latch.countDown();
+            }
+        });
+
+        try {
+            latch.await(TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException ex) {
+            fail();
+        }
+        getInstrumentation().waitForIdleSync();
+        final ListView lv = mDialog.getListView();
+        // The 3rd account should be checked by default.
+        assertEquals("checked-item", 2, lv.getCheckedItemPosition());
+        // There should be 4 accounts in the list.
+        assertEquals("count", 4, lv.getCount());
+        // The sign-out button should be shown
+        assertEquals(View.VISIBLE, mDialog.getButton(Dialog.BUTTON_NEUTRAL).getVisibility());
+        assertEquals(View.VISIBLE, mDialog.getButton(Dialog.BUTTON_NEGATIVE).getVisibility());
+        assertEquals(View.VISIBLE, mDialog.getButton(Dialog.BUTTON_POSITIVE).getVisibility());
+    }
+}