aboutsummaryrefslogtreecommitdiff
path: root/WordPress/src/main/java/org/wordpress/android/ui/prefs
diff options
context:
space:
mode:
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/prefs')
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/AboutActivity.java74
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsActivity.java43
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsFragment.java278
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java407
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsActivity.java74
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java194
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java354
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/DeleteSiteDialogFragment.java128
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/DetailListPreference.java265
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/DotComSiteSettings.java383
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/EditTextPreferenceWithValidation.java98
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/EmptyViewRecyclerView.java72
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/LearnMorePreference.java175
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/LicensesActivity.java22
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/MultiSelectRecyclerViewAdapter.java100
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileActivity.java42
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java175
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java166
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/PreferenceHint.java7
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/PrefsEvents.java20
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/ProfileInputDialogFragment.java111
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/RecyclerViewItemClickListener.java60
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/RelatedPostsDialog.java184
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/SelfHostedSiteSettings.java421
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java1392
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java871
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/SmoothScrollLinearLayoutManager.java58
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/SummaryEditTextPreference.java211
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/WPPreference.java65
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/WPStartOverPreference.java82
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java60
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.java76
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsDialogPreference.java171
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java451
34 files changed, 7290 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AboutActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AboutActivity.java
new file mode 100644
index 000000000..d463fa574
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AboutActivity.java
@@ -0,0 +1,74 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.widgets.WPTextView;
+import org.wordpress.passcodelock.AppLockManager;
+
+import java.util.Calendar;
+
+public class AboutActivity extends AppCompatActivity implements OnClickListener {
+ private static final String URL_TOS = "http://en.wordpress.com/tos";
+ private static final String URL_AUTOMATTIC = "http://automattic.com";
+ private static final String URL_PRIVACY_POLICY = "/privacy";
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.about_activity);
+
+ WPTextView version = (WPTextView) findViewById(R.id.about_version);
+ version.setText(getString(R.string.version) + " " + WordPress.versionName);
+
+ WPTextView tos = (WPTextView) findViewById(R.id.about_tos);
+ tos.setOnClickListener(this);
+
+ WPTextView pp = (WPTextView) findViewById(R.id.about_privacy);
+ pp.setOnClickListener(this);
+
+ WPTextView publisher = (WPTextView) findViewById(R.id.about_publisher);
+ publisher.setText(getString(R.string.publisher) + " " + getString(R.string.automattic_inc));
+
+ WPTextView copyright = (WPTextView) findViewById(R.id.about_copyright);
+ copyright.setText("©" + Calendar.getInstance().get(Calendar.YEAR) + " " + getString(R.string.automattic_inc));
+
+ WPTextView about = (WPTextView) findViewById(R.id.about_url);
+ about.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ Uri uri;
+ int id = v.getId();
+ if (id == R.id.about_url) {
+ uri = Uri.parse(URL_AUTOMATTIC);
+ } else if (id == R.id.about_tos) {
+ uri = Uri.parse(URL_TOS);
+ } else if (id == R.id.about_privacy) {
+ uri = Uri.parse(URL_AUTOMATTIC + URL_PRIVACY_POLICY);
+ } else {
+ return;
+ }
+ AppLockManager.getInstance().setExtendedTimeout();
+ startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ }
+
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsActivity.java
new file mode 100644
index 000000000..444e52da7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsActivity.java
@@ -0,0 +1,43 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+
+import org.wordpress.android.ui.ActivityLauncher;
+
+public class AccountSettingsActivity extends AppCompatActivity {
+ private static final String KEY_ACCOUNT_SETTINGS_FRAGMENT = "account-settings-fragment";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ FragmentManager fragmentManager = getFragmentManager();
+ AccountSettingsFragment accountSettingsFragment = (AccountSettingsFragment) fragmentManager.findFragmentByTag(KEY_ACCOUNT_SETTINGS_FRAGMENT);
+ if (accountSettingsFragment == null) {
+ accountSettingsFragment = new AccountSettingsFragment();
+
+ fragmentManager.beginTransaction()
+ .add(android.R.id.content, accountSettingsFragment, KEY_ACCOUNT_SETTINGS_FRAGMENT)
+ .commit();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsFragment.java
new file mode 100644
index 000000000..0cc8d0122
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsFragment.java
@@ -0,0 +1,278 @@
+package org.wordpress.android.ui.prefs;
+
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.support.design.widget.CoordinatorLayout;
+import android.support.design.widget.Snackbar;
+import android.support.v4.content.ContextCompat;
+import android.text.InputType;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Account;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.AccountModel;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.util.BlogUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+@SuppressWarnings("deprecation")
+public class AccountSettingsFragment extends PreferenceFragment implements Preference.OnPreferenceChangeListener {
+ private Preference mUsernamePreference;
+ private EditTextPreferenceWithValidation mEmailPreference;
+ private DetailListPreference mPrimarySitePreference;
+ private EditTextPreferenceWithValidation mWebAddressPreference;
+ private Snackbar mEmailSnackbar;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setRetainInstance(true);
+ addPreferencesFromResource(R.xml.account_settings);
+
+ mUsernamePreference = findPreference(getString(R.string.pref_key_username));
+ mEmailPreference = (EditTextPreferenceWithValidation) findPreference(getString(R.string.pref_key_email));
+ mPrimarySitePreference = (DetailListPreference) findPreference(getString(R.string.pref_key_primary_site));
+ mWebAddressPreference = (EditTextPreferenceWithValidation) findPreference(getString(R.string.pref_key_web_address));
+
+ mEmailPreference.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
+ mEmailPreference.setValidationType(EditTextPreferenceWithValidation.ValidationType.EMAIL);
+ mWebAddressPreference.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
+ mWebAddressPreference.setValidationType(EditTextPreferenceWithValidation.ValidationType.URL);
+ mWebAddressPreference.setDialogMessage(R.string.web_address_dialog_hint);
+
+ mEmailPreference.setOnPreferenceChangeListener(this);
+ mPrimarySitePreference.setOnPreferenceChangeListener(this);
+ mWebAddressPreference.setOnPreferenceChangeListener(this);
+
+ // load site list asynchronously
+ new LoadSitesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View coordinatorView = inflater.inflate(R.layout.preference_coordinator, container, false);
+ CoordinatorLayout coordinator = (CoordinatorLayout) coordinatorView.findViewById(R.id.coordinator);
+ View preferenceView = super.onCreateView(inflater, coordinator, savedInstanceState);
+ coordinator.addView(preferenceView);
+ return coordinatorView;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ refreshAccountDetails();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (NetworkUtils.isNetworkAvailable(getActivity())) {
+ AccountHelper.getDefaultAccount().fetchAccountSettings();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (newValue == null) return false;
+
+ if (preference == mEmailPreference) {
+ updateEmail(newValue.toString());
+ showPendingEmailChangeSnackbar(newValue.toString());
+ mEmailPreference.setEnabled(false);
+ return false;
+ } else if (preference == mPrimarySitePreference) {
+ changePrimaryBlogPreference(newValue.toString());
+ updatePrimaryBlog(newValue.toString());
+ return false;
+ } else if (preference == mWebAddressPreference) {
+ mWebAddressPreference.setSummary(newValue.toString());
+ updateWebAddress(newValue.toString());
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ getActivity().finish();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void refreshAccountDetails() {
+ Account account = AccountHelper.getDefaultAccount();
+ mUsernamePreference.setSummary(account.getUserName());
+ mEmailPreference.setSummary(account.getEmail());
+ mWebAddressPreference.setSummary(account.getWebAddress());
+
+ String blogId = String.valueOf(account.getPrimaryBlogId());
+ changePrimaryBlogPreference(blogId);
+
+ checkIfEmailChangeIsPending();
+ }
+
+ private void checkIfEmailChangeIsPending() {
+ final Account account = AccountHelper.getDefaultAccount();
+ if (account.getPendingEmailChange()) {
+ showPendingEmailChangeSnackbar(account.getNewEmail());
+ } else if (mEmailSnackbar != null && mEmailSnackbar.isShown()){
+ mEmailSnackbar.dismiss();
+ }
+ mEmailPreference.setEnabled(!account.getPendingEmailChange());
+ }
+
+ private void showPendingEmailChangeSnackbar(String newEmail) {
+ if (getView() != null) {
+ if (mEmailSnackbar == null || !mEmailSnackbar.isShown()) {
+ View.OnClickListener clickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ cancelPendingEmailChange();
+ }
+ };
+
+ mEmailSnackbar = Snackbar
+ .make(getView(), "", Snackbar.LENGTH_INDEFINITE).setAction(getString(R.string.button_revert), clickListener);
+ mEmailSnackbar.getView().setBackgroundColor(ContextCompat.getColor(getActivity(), R.color.grey_dark));
+ mEmailSnackbar.setActionTextColor(ContextCompat.getColor(getActivity(), R.color.blue_medium));
+ TextView textView = (TextView) mEmailSnackbar.getView().findViewById(android.support.design.R.id.snackbar_text);
+ textView.setMaxLines(4);
+ }
+ // instead of creating a new snackbar, update the current one to avoid the jumping animation
+ mEmailSnackbar.setText(getString(R.string.pending_email_change_snackbar, newEmail));
+ if (!mEmailSnackbar.isShown()) {
+ mEmailSnackbar.show();
+ }
+ }
+ }
+
+ private void cancelPendingEmailChange() {
+ Map<String, String> params = new HashMap<>();
+ params.put(AccountModel.RestParam.EMAIL_CHANGE_PENDING.getDescription(), "false");
+ AccountHelper.getDefaultAccount().postAccountSettings(params);
+ if (mEmailSnackbar != null && mEmailSnackbar.isShown()) {
+ mEmailSnackbar.dismiss();
+ }
+ }
+
+ private void changePrimaryBlogPreference(String blogId) {
+ mPrimarySitePreference.setValue(blogId);
+ Blog primaryBlog = WordPress.wpDB.getBlogForDotComBlogId(blogId);
+ if (primaryBlog != null) {
+ mPrimarySitePreference.setSummary(StringUtils.unescapeHTML(primaryBlog.getNameOrHostUrl()));
+ mPrimarySitePreference.refreshAdapter();
+ }
+ }
+
+ private void updateEmail(String newEmail) {
+ Account account = AccountHelper.getDefaultAccount();
+ Map<String, String> params = new HashMap<>();
+ params.put(AccountModel.RestParam.EMAIL.getDescription(), newEmail);
+ account.postAccountSettings(params);
+ }
+
+ private void updatePrimaryBlog(String blogId) {
+ Account account = AccountHelper.getDefaultAccount();
+ Map<String, String> params = new HashMap<>();
+ params.put(AccountModel.RestParam.PRIMARY_BLOG.getDescription(), blogId);
+ account.postAccountSettings(params);
+ }
+
+ public void updateWebAddress(String newWebAddress) {
+ Account account = AccountHelper.getDefaultAccount();
+ Map<String, String> params = new HashMap<>();
+ params.put(AccountModel.RestParam.WEB_ADDRESS.getDescription(), newWebAddress);
+ account.postAccountSettings(params);
+ }
+
+ public void onEventMainThread(PrefsEvents.AccountSettingsFetchSuccess event) {
+ if (isAdded()) {
+ refreshAccountDetails();
+ }
+ }
+
+ public void onEventMainThread(PrefsEvents.AccountSettingsPostSuccess event) {
+ if (isAdded()) {
+ refreshAccountDetails();
+ }
+ }
+
+ public void onEventMainThread(PrefsEvents.AccountSettingsFetchError event) {
+ if (isAdded()) {
+ ToastUtils.showToast(getActivity(), R.string.error_fetch_account_settings, ToastUtils.Duration.LONG);
+ }
+ }
+
+ public void onEventMainThread(PrefsEvents.AccountSettingsPostError event) {
+ if (isAdded()) {
+ ToastUtils.showToast(getActivity(), R.string.error_post_account_settings, ToastUtils.Duration.LONG);
+
+ // we optimistically show the email change snackbar, if that request fails, we should remove the snackbar
+ checkIfEmailChangeIsPending();
+ }
+ }
+
+ /*
+ * AsyncTask which loads sites from database for primary site preference
+ */
+ private class LoadSitesTask extends AsyncTask<Void, Void, Void> {
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ List<Map<String, Object>> blogList = WordPress.wpDB.getBlogsBy("dotcomFlag=1", new String[]{"homeURL"});
+ mPrimarySitePreference.setEntries(BlogUtils.getBlogNamesFromAccountMapList(blogList));
+ mPrimarySitePreference.setEntryValues(BlogUtils.getBlogIdsFromAccountMapList(blogList));
+ mPrimarySitePreference.setDetails(BlogUtils.getHomeURLOrHostNamesFromAccountMapList(blogList));
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void results) {
+ super.onPostExecute(results);
+ mPrimarySitePreference.refreshAdapter();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
new file mode 100644
index 000000000..60b088521
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
@@ -0,0 +1,407 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.PeopleListFilter;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.ui.stats.StatsTimeframe;
+
+public class AppPrefs {
+ private static final int THEME_IMAGE_SIZE_WIDTH_DEFAULT = 400;
+
+ public interface PrefKey {
+ String name();
+ String toString();
+ }
+
+ /**
+ * Application related preferences. When the user disconnects, these preferences are erased.
+ */
+ public enum DeletablePrefKey implements PrefKey {
+ // name of last shown activity
+ LAST_ACTIVITY_STR,
+
+ // last selected tag in the reader
+ READER_TAG_NAME,
+ READER_TAG_TYPE,
+
+ // title of the last active page in ReaderSubsActivity
+ READER_SUBS_PAGE_TITLE,
+
+ // email retrieved and attached to mixpanel profile
+ MIXPANEL_EMAIL_ADDRESS,
+
+ // index of the last active tab in main activity
+ MAIN_TAB_INDEX,
+
+ // index of the last active item in Stats activity
+ STATS_ITEM_INDEX,
+
+ // Keep the associations between each widget_id/blog_id added to the app
+ STATS_WIDGET_KEYS_BLOGS,
+
+ // last data stored for the Stats Widgets
+ STATS_WIDGET_DATA,
+
+ // visual editor enabled
+ VISUAL_EDITOR_ENABLED,
+
+ // Store the number of times Stats are loaded without errors. It's used to show the Widget promo dialog.
+ STATS_WIDGET_PROMO_ANALYTICS,
+
+ // index of the last active status type in Comments activity
+ COMMENTS_STATUS_TYPE_INDEX,
+
+ // index of the last active people list filter in People Management activity
+ PEOPLE_LIST_FILTER_INDEX,
+ }
+
+ /**
+ * These preferences won't be deleted when the user disconnects. They should be used for device specifics or user
+ * independent prefs.
+ */
+ public enum UndeletablePrefKey implements PrefKey {
+ // Theme image size retrieval
+ THEME_IMAGE_SIZE_WIDTH,
+
+ // index of the last app-version
+ LAST_APP_VERSION_INDEX,
+
+ // visual editor available
+ VISUAL_EDITOR_AVAILABLE,
+
+ // When we need to show the Visual Editor Promo Dialog
+ VISUAL_EDITOR_PROMO_REQUIRED,
+
+ // Global plans features
+ GLOBAL_PLANS_PLANS_FEATURES,
+
+ // When we need to sync IAP data with the wpcom backend
+ IAP_SYNC_REQUIRED,
+
+ // When we need to show the Gravatar Change Promo Tooltip
+ GRAVATAR_CHANGE_PROMO_REQUIRED,
+ }
+
+ private static SharedPreferences prefs() {
+ return PreferenceManager.getDefaultSharedPreferences(WordPress.getContext());
+ }
+
+ private static String getString(PrefKey key) {
+ return getString(key, "");
+ }
+
+ private static String getString(PrefKey key, String defaultValue) {
+ return prefs().getString(key.name(), defaultValue);
+ }
+
+ private static void setString(PrefKey key, String value) {
+ SharedPreferences.Editor editor = prefs().edit();
+ if (TextUtils.isEmpty(value)) {
+ editor.remove(key.name());
+ } else {
+ editor.putString(key.name(), value);
+ }
+ editor.apply();
+ }
+
+ private static long getLong(PrefKey key) {
+ try {
+ String value = getString(key);
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ private static void setLong(PrefKey key, long value) {
+ setString(key, Long.toString(value));
+ }
+
+ private static int getInt(PrefKey key) {
+ try {
+ String value = getString(key);
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ private static void setInt(PrefKey key, int value) {
+ setString(key, Integer.toString(value));
+ }
+
+ private static boolean getBoolean(PrefKey key, boolean def) {
+
+ String value = getString(key, Boolean.toString(def));
+ return Boolean.parseBoolean(value);
+ }
+
+ private static void setBoolean(PrefKey key, boolean value) {
+ setString(key, Boolean.toString(value));
+ }
+
+ private static void remove(PrefKey key) {
+ prefs().edit().remove(key.name()).apply();
+ }
+
+ // Exposed methods
+
+ /**
+ * remove all user-related preferences
+ */
+ public static void reset() {
+ SharedPreferences.Editor editor = prefs().edit();
+ for (DeletablePrefKey key : DeletablePrefKey.values()) {
+ editor.remove(key.name());
+ }
+ editor.apply();
+ }
+
+ public static ReaderTag getReaderTag() {
+ String tagName = getString(DeletablePrefKey.READER_TAG_NAME);
+ if (TextUtils.isEmpty(tagName)) {
+ return null;
+ }
+ int tagType = getInt(DeletablePrefKey.READER_TAG_TYPE);
+ return ReaderUtils.getTagFromTagName(tagName, ReaderTagType.fromInt(tagType));
+ }
+
+ public static void setReaderTag(ReaderTag tag) {
+ if (tag != null && !TextUtils.isEmpty(tag.getTagSlug())) {
+ setString(DeletablePrefKey.READER_TAG_NAME, tag.getTagSlug());
+ setInt(DeletablePrefKey.READER_TAG_TYPE, tag.tagType.toInt());
+ } else {
+ prefs().edit()
+ .remove(DeletablePrefKey.READER_TAG_NAME.name())
+ .remove(DeletablePrefKey.READER_TAG_TYPE.name())
+ .apply();
+ }
+ }
+
+ /**
+ * title of the last active page in ReaderSubsActivity - this is stored rather than
+ * the index of the page so we can re-order pages without affecting this value
+ */
+ public static String getReaderSubsPageTitle() {
+ return getString(DeletablePrefKey.READER_SUBS_PAGE_TITLE);
+ }
+
+ public static void setReaderSubsPageTitle(String pageTitle) {
+ setString(DeletablePrefKey.READER_SUBS_PAGE_TITLE, pageTitle);
+ }
+
+ public static StatsTimeframe getStatsTimeframe() {
+ int idx = getInt(DeletablePrefKey.STATS_ITEM_INDEX);
+ StatsTimeframe[] timeframeValues = StatsTimeframe.values();
+ if (timeframeValues.length < idx) {
+ return timeframeValues[0];
+ } else {
+ return timeframeValues[idx];
+ }
+ }
+
+ public static void setStatsTimeframe(StatsTimeframe timeframe) {
+ if (timeframe != null) {
+ setInt(DeletablePrefKey.STATS_ITEM_INDEX, timeframe.ordinal());
+ } else {
+ prefs().edit()
+ .remove(DeletablePrefKey.STATS_ITEM_INDEX.name())
+ .apply();
+ }
+ }
+
+ public static CommentStatus getCommentsStatusFilter() {
+ int idx = getInt(DeletablePrefKey.COMMENTS_STATUS_TYPE_INDEX);
+ CommentStatus[] commentStatusValues = CommentStatus.values();
+ if (commentStatusValues.length < idx) {
+ return commentStatusValues[0];
+ } else {
+ return commentStatusValues[idx];
+ }
+ }
+ public static void setCommentsStatusFilter(CommentStatus commentstatus) {
+ if (commentstatus != null) {
+ setInt(DeletablePrefKey.COMMENTS_STATUS_TYPE_INDEX, commentstatus.ordinal());
+ } else {
+ prefs().edit()
+ .remove(DeletablePrefKey.COMMENTS_STATUS_TYPE_INDEX.name())
+ .apply();
+ }
+ }
+
+ public static PeopleListFilter getPeopleListFilter() {
+ int idx = getInt(DeletablePrefKey.PEOPLE_LIST_FILTER_INDEX);
+ PeopleListFilter[] values = PeopleListFilter.values();
+ if (values.length < idx) {
+ return values[0];
+ } else {
+ return values[idx];
+ }
+ }
+ public static void setPeopleListFilter(PeopleListFilter peopleListFilter) {
+ if (peopleListFilter != null) {
+ setInt(DeletablePrefKey.PEOPLE_LIST_FILTER_INDEX, peopleListFilter.ordinal());
+ } else {
+ prefs().edit()
+ .remove(DeletablePrefKey.PEOPLE_LIST_FILTER_INDEX.name())
+ .apply();
+ }
+ }
+
+ // Store the version code of the app. Used to check it the app was upgraded.
+ public static int getLastAppVersionCode() {
+ return getInt(UndeletablePrefKey.LAST_APP_VERSION_INDEX);
+ }
+
+ public static void setLastAppVersionCode(int versionCode) {
+ setInt(UndeletablePrefKey.LAST_APP_VERSION_INDEX, versionCode);
+ }
+
+ /**
+ * name of the last shown activity - used at startup to restore the previously selected
+ * activity, also used by analytics tracker
+ */
+ public static String getLastActivityStr() {
+ return getString(DeletablePrefKey.LAST_ACTIVITY_STR, ActivityId.UNKNOWN.name());
+ }
+
+ public static void setLastActivityStr(String value) {
+ setString(DeletablePrefKey.LAST_ACTIVITY_STR, value);
+ }
+
+ public static void resetLastActivityStr() {
+ remove(DeletablePrefKey.LAST_ACTIVITY_STR);
+ }
+
+ // Mixpanel email retrieval check
+
+ public static String getMixpanelUserEmail() {
+ return getString(DeletablePrefKey.MIXPANEL_EMAIL_ADDRESS, null);
+ }
+
+ public static void setMixpanelUserEmail(String email) {
+ setString(DeletablePrefKey.MIXPANEL_EMAIL_ADDRESS, email);
+ }
+
+ public static int getMainTabIndex() {
+ return getInt(DeletablePrefKey.MAIN_TAB_INDEX);
+ }
+
+ public static void setMainTabIndex(int index) {
+ setInt(DeletablePrefKey.MAIN_TAB_INDEX, index);
+ }
+
+ // Stats Widgets
+ public static void resetStatsWidgetsKeys() {
+ remove(DeletablePrefKey.STATS_WIDGET_KEYS_BLOGS);
+ }
+
+ public static String getStatsWidgetsKeys() {
+ return getString(DeletablePrefKey.STATS_WIDGET_KEYS_BLOGS);
+ }
+
+ public static void setStatsWidgetsKeys(String widgetData) {
+ setString(DeletablePrefKey.STATS_WIDGET_KEYS_BLOGS, widgetData);
+ }
+
+ public static String getStatsWidgetsData() {
+ return getString(DeletablePrefKey.STATS_WIDGET_DATA);
+ }
+
+ public static void setStatsWidgetsData(String widgetData) {
+ setString(DeletablePrefKey.STATS_WIDGET_DATA, widgetData);
+ }
+
+ public static void resetStatsWidgetsData() {
+ remove(DeletablePrefKey.STATS_WIDGET_DATA);
+ }
+
+ // Themes
+ public static void setThemeImageSizeWidth(int width) {
+ setInt(UndeletablePrefKey.THEME_IMAGE_SIZE_WIDTH, width);
+ }
+
+ public static int getThemeImageSizeWidth() {
+ int value = getInt(UndeletablePrefKey.THEME_IMAGE_SIZE_WIDTH);
+ if (value == 0) {
+ return THEME_IMAGE_SIZE_WIDTH_DEFAULT;
+ } else {
+ return getInt(UndeletablePrefKey.THEME_IMAGE_SIZE_WIDTH);
+ }
+ }
+
+ // Visual Editor
+ public static void setVisualEditorEnabled(boolean visualEditorEnabled) {
+ setBoolean(DeletablePrefKey.VISUAL_EDITOR_ENABLED, visualEditorEnabled);
+ AnalyticsTracker.track(visualEditorEnabled ? Stat.EDITOR_TOGGLED_ON : Stat.EDITOR_TOGGLED_OFF);
+ }
+
+ public static void setVisualEditorAvailable(boolean visualEditorAvailable) {
+ setBoolean(UndeletablePrefKey.VISUAL_EDITOR_AVAILABLE, visualEditorAvailable);
+ if (visualEditorAvailable) {
+ AnalyticsTracker.track(Stat.EDITOR_ENABLED_NEW_VERSION);
+ }
+ }
+
+ public static boolean isVisualEditorAvailable() {
+ return getBoolean(UndeletablePrefKey.VISUAL_EDITOR_AVAILABLE, false);
+ }
+
+ public static boolean isVisualEditorEnabled() {
+ return isVisualEditorAvailable() && getBoolean(DeletablePrefKey.VISUAL_EDITOR_ENABLED, true);
+ }
+
+ public static boolean isVisualEditorPromoRequired() {
+ return getBoolean(UndeletablePrefKey.VISUAL_EDITOR_PROMO_REQUIRED, true);
+ }
+
+ public static void setVisualEditorPromoRequired(boolean required) {
+ setBoolean(UndeletablePrefKey.VISUAL_EDITOR_PROMO_REQUIRED, required);
+ }
+
+ public static boolean isGravatarChangePromoRequired() {
+ return getBoolean(UndeletablePrefKey.GRAVATAR_CHANGE_PROMO_REQUIRED, true);
+ }
+
+ public static void setGravatarChangePromoRequired(boolean required) {
+ setBoolean(UndeletablePrefKey.GRAVATAR_CHANGE_PROMO_REQUIRED, required);
+ }
+
+ // Store the number of times Stats are loaded successfully before showing the Promo Dialog
+ public static void bumpAnalyticsForStatsWidgetPromo() {
+ int current = getAnalyticsForStatsWidgetPromo();
+ setInt(DeletablePrefKey.STATS_WIDGET_PROMO_ANALYTICS, current + 1);
+ }
+
+ public static int getAnalyticsForStatsWidgetPromo() {
+ return getInt(DeletablePrefKey.STATS_WIDGET_PROMO_ANALYTICS);
+ }
+
+ public static void setGlobalPlansFeatures(String jsonOfFeatures) {
+ if (jsonOfFeatures != null) {
+ setString(UndeletablePrefKey.GLOBAL_PLANS_PLANS_FEATURES, jsonOfFeatures);
+ } else {
+ remove(UndeletablePrefKey.GLOBAL_PLANS_PLANS_FEATURES);
+ }
+ }
+ public static String getGlobalPlansFeatures() {
+ return getString(UndeletablePrefKey.GLOBAL_PLANS_PLANS_FEATURES, "");
+ }
+
+ public static boolean isInAppPurchaseRefreshRequired() {
+ return getBoolean(UndeletablePrefKey.IAP_SYNC_REQUIRED, false);
+ }
+ public static void setInAppPurchaseRefreshRequired(boolean required) {
+ setBoolean(UndeletablePrefKey.IAP_SYNC_REQUIRED, required);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsActivity.java
new file mode 100644
index 000000000..1d16b4253
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsActivity.java
@@ -0,0 +1,74 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.SwitchPreference;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+
+import org.wordpress.passcodelock.AppLockManager;
+import org.wordpress.passcodelock.PasscodePreferenceFragment;
+
+public class AppSettingsActivity extends AppCompatActivity {
+ private static final String KEY_APP_SETTINGS_FRAGMENT = "app-settings-fragment";
+ private static final String KEY_PASSCODE_FRAGMENT = "passcode-fragment";
+
+ private AppSettingsFragment mAppSettingsFragment;
+ private PasscodePreferenceFragment mPasscodePreferenceFragment;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ FragmentManager fragmentManager = getFragmentManager();
+ mAppSettingsFragment = (AppSettingsFragment) fragmentManager.findFragmentByTag(KEY_APP_SETTINGS_FRAGMENT);
+ mPasscodePreferenceFragment = (PasscodePreferenceFragment) fragmentManager.findFragmentByTag(KEY_PASSCODE_FRAGMENT);
+ if (mAppSettingsFragment == null || mPasscodePreferenceFragment == null) {
+ Bundle passcodeArgs = new Bundle();
+ passcodeArgs.putBoolean(PasscodePreferenceFragment.KEY_SHOULD_INFLATE, false);
+ mAppSettingsFragment = new AppSettingsFragment();
+ mPasscodePreferenceFragment = new PasscodePreferenceFragment();
+ mPasscodePreferenceFragment.setArguments(passcodeArgs);
+
+ fragmentManager.beginTransaction()
+ .replace(android.R.id.content, mPasscodePreferenceFragment, KEY_PASSCODE_FRAGMENT)
+ .add(android.R.id.content, mAppSettingsFragment, KEY_APP_SETTINGS_FRAGMENT)
+ .commit();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ Preference togglePref =
+ mAppSettingsFragment.findPreference(getString(org.wordpress.passcodelock.R.string
+ .pref_key_passcode_toggle));
+ Preference changePref =
+ mAppSettingsFragment.findPreference(getString(org.wordpress.passcodelock.R.string
+ .pref_key_change_passcode));
+
+ if (togglePref != null && changePref != null) {
+ mPasscodePreferenceFragment.setPreferences(togglePref, changePref);
+ ((SwitchPreference) togglePref).setChecked(
+ AppLockManager.getInstance().getAppLock().isPasswordLocked());
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java
new file mode 100644
index 000000000..2fb651203
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java
@@ -0,0 +1,194 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.view.MenuItem;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.LanguageUtils;
+import org.wordpress.android.util.WPPrefUtils;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+public class AppSettingsFragment extends PreferenceFragment implements OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
+ public static final String LANGUAGE_PREF_KEY = "language-pref";
+ public static final int LANGUAGE_CHANGED = 1000;
+
+ private DetailListPreference mLanguagePreference;
+ private SharedPreferences mSettings;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setRetainInstance(true);
+ addPreferencesFromResource(R.xml.app_settings);
+
+ mLanguagePreference = (DetailListPreference) findPreference(getString(R.string.pref_key_language));
+ mLanguagePreference.setOnPreferenceChangeListener(this);
+
+ findPreference(getString(R.string.pref_key_language))
+ .setOnPreferenceClickListener(this);
+ findPreference(getString(R.string.pref_key_app_about))
+ .setOnPreferenceClickListener(this);
+ findPreference(getString(R.string.pref_key_oss_licenses))
+ .setOnPreferenceClickListener(this);
+
+ mSettings = PreferenceManager.getDefaultSharedPreferences(getActivity());
+
+ updateVisualEditorSettings();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ updateLanguagePreference(getResources().getConfiguration().locale.toString());
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ String preferenceKey = preference != null ? preference.getKey() : "";
+
+ if (preferenceKey.equals(getString(R.string.pref_key_app_about))) {
+ return handleAboutPreferenceClick();
+ } else if (preferenceKey.equals(getString(R.string.pref_key_oss_licenses))) {
+ return handleOssPreferenceClick();
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (newValue == null) return false;
+
+ if (preference == mLanguagePreference) {
+ changeLanguage(newValue.toString());
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ getActivity().finish();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void updateVisualEditorSettings() {
+ if (!AppPrefs.isVisualEditorAvailable()) {
+ PreferenceScreen preferenceScreen = (PreferenceScreen) findPreference(getActivity()
+ .getString(R.string.pref_key_account_settings_root));
+ PreferenceCategory editor = (PreferenceCategory) findPreference(getActivity()
+ .getString(R.string.pref_key_editor));
+ if (preferenceScreen != null && editor != null) {
+ preferenceScreen.removePreference(editor);
+ }
+ } else {
+ final SwitchPreference visualEditorSwitch = (SwitchPreference) findPreference(getActivity()
+ .getString(R.string.pref_key_visual_editor_enabled));
+ visualEditorSwitch.setChecked(AppPrefs.isVisualEditorEnabled());
+ visualEditorSwitch.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(final Preference preference, final Object newValue) {
+ visualEditorSwitch.setChecked(!visualEditorSwitch.isChecked());
+ AppPrefs.setVisualEditorEnabled(visualEditorSwitch.isChecked());
+ return false;
+ }
+ });
+ }
+ }
+
+ private void changeLanguage(String languageCode) {
+ if (mLanguagePreference == null || TextUtils.isEmpty(languageCode)) return;
+
+ Resources res = getResources();
+ Configuration conf = res.getConfiguration();
+ Locale currentLocale = conf.locale != null ? conf.locale : LanguageUtils.getCurrentDeviceLanguage(WordPress.getContext());
+
+ if (currentLocale.toString().equals(languageCode)) return;
+
+ updateLanguagePreference(languageCode);
+
+ // update configuration
+ Locale newLocale = WPPrefUtils.languageLocale(languageCode);
+ conf.locale = newLocale;
+ res.updateConfiguration(conf, res.getDisplayMetrics());
+
+ if (LanguageUtils.getCurrentDeviceLanguage(WordPress.getContext()).equals(newLocale)) {
+ // remove custom locale key when original device locale is selected
+ mSettings.edit().remove(LANGUAGE_PREF_KEY).apply();
+ } else {
+ mSettings.edit().putString(LANGUAGE_PREF_KEY, newLocale.toString()).apply();
+ }
+
+ // Track language change on Mixpanel because we have both the device language and app selected language
+ // data in Tracks metadata.
+ Map<String, Object> properties = new HashMap<>();
+ properties.put("app_locale", conf.locale.toString());
+ AnalyticsTracker.track(Stat.ACCOUNT_SETTINGS_LANGUAGE_CHANGED, properties);
+
+ // Language is now part of metadata, so we need to refresh them
+ AnalyticsUtils.refreshMetadata();
+
+ // Refresh the app
+ Intent refresh = new Intent(getActivity(), getActivity().getClass());
+ startActivity(refresh);
+ getActivity().setResult(LANGUAGE_CHANGED);
+ getActivity().finish();
+ }
+
+ private void updateLanguagePreference(String languageCode) {
+ if (mLanguagePreference == null || TextUtils.isEmpty(languageCode)) return;
+
+ Locale languageLocale = WPPrefUtils.languageLocale(languageCode);
+ String[] availableLocales = getResources().getStringArray(R.array.available_languages);
+
+ Pair<String[], String[]> pair = WPPrefUtils.createSortedLanguageDisplayStrings(availableLocales, languageLocale);
+ // check for a possible NPE
+ if (pair == null) return;
+
+ String[] sortedEntries = pair.first;
+ String[] sortedValues = pair.second;
+
+ mLanguagePreference.setEntries(sortedEntries);
+ mLanguagePreference.setEntryValues(sortedValues);
+ mLanguagePreference.setDetails(WPPrefUtils.createLanguageDetailDisplayStrings(sortedValues));
+
+ mLanguagePreference.setValue(languageCode);
+ mLanguagePreference.setSummary(WPPrefUtils.getLanguageString(languageCode, languageLocale));
+ mLanguagePreference.refreshAdapter();
+ }
+
+ private boolean handleAboutPreferenceClick() {
+ startActivity(new Intent(getActivity(), AboutActivity.class));
+ return true;
+ }
+
+ private boolean handleOssPreferenceClick() {
+ startActivity(new Intent(getActivity(), LicensesActivity.class));
+ return true;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java
new file mode 100644
index 000000000..41a5417dc
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java
@@ -0,0 +1,354 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.networking.ConnectionChangeReceiver;
+import org.wordpress.android.ui.stats.StatsWidgetProvider;
+import org.wordpress.android.ui.stats.datasets.StatsTable;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.CoreEvents.UserSignedOutCompletely;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * Activity for configuring blog specific settings.
+ */
+public class BlogPreferencesActivity extends AppCompatActivity {
+ public static final String ARG_LOCAL_BLOG_ID = SiteSettingsFragment.ARG_LOCAL_BLOG_ID;
+ public static final int RESULT_BLOG_REMOVED = RESULT_FIRST_USER;
+
+ private static final String KEY_SETTINGS_FRAGMENT = "settings-fragment";
+
+ // The blog this activity is managing settings for.
+ private Blog blog;
+ private boolean mBlogDeleted;
+ private EditText mUsernameET;
+ private EditText mPasswordET;
+ private EditText mHttpUsernameET;
+ private EditText mHttpPasswordET;
+ private CheckBox mFullSizeCB;
+ private CheckBox mScaledCB;
+ private Spinner mImageWidthSpinner;
+ private EditText mScaledImageWidthET;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Integer id = getIntent().getIntExtra(ARG_LOCAL_BLOG_ID, -1);
+ blog = WordPress.getBlog(id);
+ if (WordPress.getBlog(id) == null) {
+ Toast.makeText(this, getString(R.string.blog_not_found), Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+
+ if (blog.isDotcomFlag()) {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ FragmentManager fragmentManager = getFragmentManager();
+ Fragment siteSettingsFragment = fragmentManager.findFragmentByTag(KEY_SETTINGS_FRAGMENT);
+
+ if (siteSettingsFragment == null) {
+ siteSettingsFragment = new SiteSettingsFragment();
+ siteSettingsFragment.setArguments(getIntent().getExtras());
+ fragmentManager.beginTransaction()
+ .replace(android.R.id.content, siteSettingsFragment, KEY_SETTINGS_FRAGMENT)
+ .commit();
+ }
+ } else {
+ setContentView(R.layout.blog_preferences);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(StringUtils.unescapeHTML(blog.getNameOrHostUrl()));
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ mUsernameET = (EditText) findViewById(R.id.username);
+ mPasswordET = (EditText) findViewById(R.id.password);
+ mHttpUsernameET = (EditText) findViewById(R.id.httpuser);
+ mHttpPasswordET = (EditText) findViewById(R.id.httppassword);
+ mScaledImageWidthET = (EditText) findViewById(R.id.scaledImageWidth);
+ mFullSizeCB = (CheckBox) findViewById(R.id.fullSizeImage);
+ mScaledCB = (CheckBox) findViewById(R.id.scaledImage);
+ mImageWidthSpinner = (Spinner) findViewById(R.id.maxImageWidth);
+ Button removeBlogButton = (Button) findViewById(R.id.remove_account);
+
+ // remove blog & credentials apply only to dot org
+ if (blog.isDotcomFlag()) {
+ View credentialsRL = findViewById(R.id.sectionContent);
+ credentialsRL.setVisibility(View.GONE);
+ removeBlogButton.setVisibility(View.GONE);
+ } else {
+ removeBlogButton.setVisibility(View.VISIBLE);
+ removeBlogButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ removeBlogWithConfirmation();
+ }
+ });
+ }
+
+ loadSettingsForBlog();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ if (blog.isDotcomFlag() || mBlogDeleted) {
+ return;
+ }
+
+ blog.setUsername(mUsernameET.getText().toString());
+ blog.setPassword(mPasswordET.getText().toString());
+ blog.setHttpuser(mHttpUsernameET.getText().toString());
+ blog.setHttppassword(mHttpPasswordET.getText().toString());
+
+ blog.setFullSizeImage(mFullSizeCB.isChecked());
+ blog.setScaledImage(mScaledCB.isChecked());
+ if (blog.isScaledImage()) {
+ EditText scaledImgWidth = (EditText) findViewById(R.id.scaledImageWidth);
+
+ boolean error = false;
+ int width = 0;
+ try {
+ width = Integer.parseInt(scaledImgWidth.getText().toString().trim());
+ } catch (NumberFormatException e) {
+ error = true;
+ }
+
+ if (width == 0) {
+ error = true;
+ }
+
+ if (error) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(BlogPreferencesActivity.this);
+ dialogBuilder.setTitle(getResources().getText(R.string.error));
+ dialogBuilder.setMessage(getResources().getText(R.string.scaled_image_error));
+ dialogBuilder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ }
+ });
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+ return;
+ } else {
+ blog.setScaledImageWidth(width);
+ }
+ }
+
+ blog.setMaxImageWidth(mImageWidthSpinner.getSelectedItem().toString());
+
+ WordPress.wpDB.saveBlog(blog);
+
+ if (WordPress.getCurrentBlog().getLocalTableBlogId() == blog.getLocalTableBlogId()) {
+ WordPress.currentBlog = blog;
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ protected void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int itemID = item.getItemId();
+ if (itemID == android.R.id.home) {
+ finish();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(ConnectionChangeReceiver.ConnectionChangeEvent event) {
+ FragmentManager fragmentManager = getFragmentManager();
+ SiteSettingsFragment siteSettingsFragment =
+ (SiteSettingsFragment) fragmentManager.findFragmentByTag(KEY_SETTINGS_FRAGMENT);
+
+ if (siteSettingsFragment != null) {
+ if (!event.isConnected()) {
+ ToastUtils.showToast(this, getString(R.string.site_settings_disconnected_toast));
+ }
+ siteSettingsFragment.setEditingEnabled(event.isConnected());
+
+ // TODO: add this back when delete blog is back
+ //https://github.com/wordpress-mobile/WordPress-Android/commit/6a90e3fe46e24ee40abdc4a7f8f0db06f157900c
+ // Checks for stats widgets that were synched with a blog that could be gone now.
+// StatsWidgetProvider.updateWidgetsOnLogout(this);
+ }
+ }
+
+ private void loadSettingsForBlog() {
+ ArrayAdapter<Object> spinnerArrayAdapter = new ArrayAdapter<Object>(this,
+ R.layout.simple_spinner_item, new String[]{
+ "Original Size", "100", "200", "300", "400", "500", "600", "700", "800",
+ "900", "1000", "1100", "1200", "1300", "1400", "1500", "1600", "1700",
+ "1800", "1900", "2000"
+ });
+ spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mImageWidthSpinner.setAdapter(spinnerArrayAdapter);
+ mImageWidthSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ CheckBox fullSizeImageCheckBox = (CheckBox) findViewById(R.id.fullSizeImage);
+ // Original size selected. Do not show the link to full image.
+ if (id == 0) {
+ fullSizeImageCheckBox.setVisibility(View.GONE);
+ } else {
+ fullSizeImageCheckBox.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> arg0) {
+ }
+ });
+
+ mUsernameET.setText(blog.getUsername());
+ mPasswordET.setText(blog.getPassword());
+ mHttpUsernameET.setText(blog.getHttpuser());
+ mHttpPasswordET.setText(blog.getHttppassword());
+ TextView httpUserLabel = (TextView) findViewById(R.id.l_httpuser);
+ if (blog.isDotcomFlag()) {
+ mHttpUsernameET.setVisibility(View.GONE);
+ mHttpPasswordET.setVisibility(View.GONE);
+ httpUserLabel.setVisibility(View.GONE);
+ } else {
+ mHttpUsernameET.setVisibility(View.VISIBLE);
+ mHttpPasswordET.setVisibility(View.VISIBLE);
+ httpUserLabel.setVisibility(View.VISIBLE);
+ }
+
+ mFullSizeCB.setChecked(blog.isFullSizeImage());
+ mScaledCB.setChecked(blog.isScaledImage());
+
+ this.mScaledImageWidthET.setText("" + blog.getScaledImageWidth());
+ showScaledSetting(blog.isScaledImage());
+
+ CheckBox scaledImage = (CheckBox) findViewById(R.id.scaledImage);
+ scaledImage.setChecked(false);
+ scaledImage.setVisibility(View.GONE);
+
+ // sets up a state listener for the full-size checkbox
+ CheckBox fullSizeImageCheckBox = (CheckBox) findViewById(R.id.fullSizeImage);
+ fullSizeImageCheckBox.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ CheckBox fullSize = (CheckBox) findViewById(R.id.fullSizeImage);
+ if (fullSize.isChecked()) {
+ CheckBox scaledImage = (CheckBox) findViewById(R.id.scaledImage);
+ if (scaledImage.isChecked()) {
+ scaledImage.setChecked(false);
+ showScaledSetting(false);
+ }
+ }
+ }
+ });
+
+ int imageWidthPosition = spinnerArrayAdapter.getPosition(blog.getMaxImageWidth());
+ mImageWidthSpinner.setSelection((imageWidthPosition >= 0) ? imageWidthPosition : 0);
+ if (mImageWidthSpinner.getSelectedItemPosition() ==
+ 0) //Original size selected. Do not show the link to full image.
+ {
+ fullSizeImageCheckBox.setVisibility(View.GONE);
+ } else {
+ fullSizeImageCheckBox.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Hides / shows the scaled image settings
+ */
+ private void showScaledSetting(boolean show) {
+ TextView tw = (TextView) findViewById(R.id.l_scaledImage);
+ EditText et = (EditText) findViewById(R.id.scaledImageWidth);
+ tw.setVisibility(show ? View.VISIBLE : View.GONE);
+ et.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+
+ /**
+ * Remove the blog this activity is managing settings for.
+ */
+ private void removeBlogWithConfirmation() {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
+ dialogBuilder.setTitle(getResources().getText(R.string.remove_account));
+ dialogBuilder.setMessage(getResources().getText(R.string.sure_to_remove_account));
+ dialogBuilder.setPositiveButton(getResources().getText(R.string.yes), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ removeBlog();
+ }
+ });
+ dialogBuilder.setNegativeButton(getResources().getText(R.string.no), null);
+ dialogBuilder.setCancelable(false);
+ dialogBuilder.create().show();
+ }
+
+ private void removeBlog() {
+ if (WordPress.wpDB.deleteBlog(this, blog.getLocalTableBlogId())) {
+ StatsTable.deleteStatsForBlog(this,blog.getLocalTableBlogId()); // Remove stats data
+ AnalyticsUtils.refreshMetadata();
+ ToastUtils.showToast(this, R.string.blog_removed_successfully);
+ WordPress.wpDB.deleteLastBlogId();
+ WordPress.currentBlog = null;
+ mBlogDeleted = true;
+ setResult(RESULT_BLOG_REMOVED);
+
+ // If the last blog is removed and the user is not signed in wpcom, broadcast a UserSignedOut event
+ if (!AccountHelper.isSignedIn()) {
+ EventBus.getDefault().post(new UserSignedOutCompletely());
+ }
+
+ // Checks for stats widgets that were synched with a blog that could be gone now.
+ StatsWidgetProvider.updateWidgetsOnLogout(this);
+
+ finish();
+ } else {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
+ dialogBuilder.setTitle(getResources().getText(R.string.error));
+ dialogBuilder.setMessage(getResources().getText(R.string.could_not_remove_account));
+ dialogBuilder.setPositiveButton("OK", null);
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/DeleteSiteDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DeleteSiteDialogFragment.java
new file mode 100644
index 000000000..bcc3119da
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DeleteSiteDialogFragment.java
@@ -0,0 +1,128 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextWatcher;
+import android.text.style.StyleSpan;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.util.AnalyticsUtils;
+
+public class DeleteSiteDialogFragment extends DialogFragment implements TextWatcher, DialogInterface.OnShowListener {
+ public static final String SITE_DOMAIN_KEY = "site-domain";
+
+ private AlertDialog mDeleteSiteDialog;
+ private EditText mUrlConfirmation;
+ private Button mDeleteButton;
+ private String mSiteDomain = "";
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_ACCESSED);
+ retrieveSiteDomain();
+ configureAlertViewBuilder(builder);
+
+ mDeleteSiteDialog = builder.create();
+ mDeleteSiteDialog.setOnShowListener(this);
+
+ return mDeleteSiteDialog;
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (isUrlConfirmationTextValid()) {
+ mDeleteButton.setEnabled(true);
+ } else {
+ mDeleteButton.setEnabled(false);
+ }
+ }
+
+ @Override
+ public void onShow(DialogInterface dialog) {
+ mDeleteButton = mDeleteSiteDialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ mDeleteButton.setEnabled(false);
+ }
+
+ private void configureAlertViewBuilder(AlertDialog.Builder builder) {
+ builder.setTitle(R.string.confirm_delete_site);
+ builder.setMessage(confirmationPromptString());
+
+ configureUrlConfirmation(builder);
+ configureButtons(builder);
+ }
+
+ private void configureButtons(AlertDialog.Builder builder) {
+ builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismiss();
+ }
+ });
+ builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Fragment target = getTargetFragment();
+ if (target != null) {
+ target.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, null);
+ }
+
+ dismiss();
+ }
+ });
+ }
+
+ private Spannable confirmationPromptString() {
+ String deletePrompt = String.format(getString(R.string.confirm_delete_site_prompt), mSiteDomain);
+ Spannable promptSpannable = new SpannableString(deletePrompt);
+ int beginning = deletePrompt.indexOf(mSiteDomain);
+ int end = beginning + mSiteDomain.length();
+ promptSpannable.setSpan(new StyleSpan(Typeface.BOLD), beginning, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ return promptSpannable;
+ }
+
+ private void configureUrlConfirmation(AlertDialog.Builder builder) {
+ View view = getActivity().getLayoutInflater().inflate(R.layout.delete_site_dialog, null);
+ mUrlConfirmation = (EditText) view.findViewById(R.id.url_confirmation);
+ mUrlConfirmation.addTextChangedListener(this);
+ builder.setView(view);
+ }
+
+ private void retrieveSiteDomain() {
+ Bundle args = getArguments();
+ mSiteDomain = getString(R.string.wordpress_dot_com).toLowerCase();
+ if (args != null) {
+ mSiteDomain = args.getString(SITE_DOMAIN_KEY);
+ }
+ }
+
+ private boolean isUrlConfirmationTextValid() {
+ String confirmationText = mUrlConfirmation.getText().toString().trim().toLowerCase();
+ String hintText = mSiteDomain.toLowerCase();
+
+ return confirmationText.equals(hintText);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/DetailListPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DetailListPreference.java
new file mode 100644
index 000000000..d8de0038c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DetailListPreference.java
@@ -0,0 +1,265 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AlertDialog;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.RadioButton;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.WPPrefUtils;
+
+/**
+ * Custom {@link ListPreference} used to display detail text per item.
+ */
+
+public class DetailListPreference extends ListPreference
+ implements PreferenceHint {
+ private DetailListAdapter mListAdapter;
+ private String[] mDetails;
+ private String mStartingValue;
+ private int mSelectedIndex;
+ private String mHint;
+ private AlertDialog mDialog;
+ private int mWhichButtonClicked;
+
+ public DetailListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DetailListPreference);
+
+ for (int i = 0; i < array.getIndexCount(); ++i) {
+ int index = array.getIndex(i);
+ if (index == R.styleable.DetailListPreference_entryDetails) {
+ int id = array.getResourceId(index, -1);
+ if (id != -1) {
+ mDetails = array.getResources().getStringArray(id);
+ }
+ } else if (index == R.styleable.DetailListPreference_longClickHint) {
+ mHint = array.getString(index);
+ }
+ }
+
+ array.recycle();
+
+ mSelectedIndex = -1;
+ }
+
+ @Override
+ protected void onBindView(@NonNull View view) {
+ super.onBindView(view);
+
+ setupView((TextView) view.findViewById(android.R.id.title),
+ R.dimen.text_sz_large, R.color.grey_dark, R.color.grey_lighten_10);
+ setupView((TextView) view.findViewById(android.R.id.summary),
+ R.dimen.text_sz_medium, R.color.grey_darken_10, R.color.grey_lighten_10);
+ }
+
+ @Override
+ public CharSequence getEntry() {
+ int index = findIndexOfValue(getValue());
+ CharSequence[] entries = getEntries();
+
+ if (entries != null && index >= 0 && index < entries.length) {
+ return entries[index];
+ }
+ return null;
+ }
+
+ @Override
+ protected void showDialog(Bundle state) {
+ Context context = getContext();
+ Resources res = context.getResources();
+ AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Calypso_AlertDialog);
+
+ mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;
+ builder.setPositiveButton(R.string.ok, this);
+ builder.setNegativeButton(res.getString(R.string.cancel).toUpperCase(), this);
+
+ if (mDetails == null) {
+ mDetails = new String[getEntries() == null ? 1 : getEntries().length];
+ }
+
+ mListAdapter = new DetailListAdapter(getContext(), R.layout.detail_list_preference, mDetails);
+ mStartingValue = getValue();
+ mSelectedIndex = findIndexOfValue(mStartingValue);
+
+ builder.setSingleChoiceItems(mListAdapter, mSelectedIndex,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mSelectedIndex = which;
+ }
+ });
+
+ View titleView = View.inflate(getContext(), R.layout.detail_list_preference_title, null);
+
+ if (titleView != null) {
+ TextView titleText = (TextView) titleView.findViewById(R.id.title);
+ if (titleText != null) {
+ titleText.setText(getTitle());
+ }
+
+ builder.setCustomTitle(titleView);
+ } else {
+ builder.setTitle(getTitle());
+ }
+
+ if ((mDialog = builder.create()) == null) return;
+
+ if (state != null) {
+ mDialog.onRestoreInstanceState(state);
+ }
+ mDialog.setOnDismissListener(this);
+ mDialog.show();
+
+ ListView listView = mDialog.getListView();
+ Button positive = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ Button negative = mDialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ Typeface typeface = WPPrefUtils.getSemiboldTypeface(getContext());
+
+ if (listView != null) {
+ listView.setDividerHeight(0);
+ listView.setClipToPadding(true);
+ listView.setPadding(0, 0, 0, res.getDimensionPixelSize(R.dimen.site_settings_divider_height));
+ }
+
+ if (positive != null) {
+ //noinspection deprecation
+ positive.setTextColor(res.getColor(R.color.blue_medium));
+ positive.setTypeface(typeface);
+ }
+
+ if (negative != null) {
+ //noinspection deprecation
+ negative.setTextColor(res.getColor(R.color.blue_medium));
+ negative.setTypeface(typeface);
+ }
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mWhichButtonClicked = which;
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ mDialog = null;
+ onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE);
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ int index = positiveResult ? mSelectedIndex : findIndexOfValue(mStartingValue);
+ CharSequence[] values = getEntryValues();
+ if (values != null && index >= 0 && index < values.length) {
+ String value = String.valueOf(values[index]);
+ callChangeListener(value);
+ }
+ }
+
+ @Override
+ public boolean hasHint() {
+ return !TextUtils.isEmpty(mHint);
+ }
+
+ @Override
+ public String getHint() {
+ return mHint;
+ }
+
+ @Override
+ public void setHint(String hint) {
+ mHint = hint;
+ }
+
+ public void refreshAdapter() {
+ if (mListAdapter != null) {
+ mListAdapter.notifyDataSetChanged();
+ }
+ }
+
+ public void setDetails(String[] details) {
+ mDetails = details;
+ refreshAdapter();
+ }
+
+ /**
+ * Helper method to style the Preference screen view
+ */
+ private void setupView(TextView view, int sizeRes, int enabledColorRes, int disabledColorRes) {
+ if (view != null) {
+ Resources res = getContext().getResources();
+ view.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(sizeRes));
+ //noinspection deprecation
+ view.setTextColor(res.getColor(isEnabled() ? enabledColorRes : disabledColorRes));
+ }
+ }
+
+ private class DetailListAdapter extends ArrayAdapter<String> {
+ public DetailListAdapter(Context context, int resource, String[] objects) {
+ super(context, resource, objects);
+ }
+
+ @Override
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = View.inflate(getContext(), R.layout.detail_list_preference, null);
+ }
+
+ final RadioButton radioButton = (RadioButton) convertView.findViewById(R.id.radio);
+ TextView mainText = (TextView) convertView.findViewById(R.id.main_text);
+ TextView detailText = (TextView) convertView.findViewById(R.id.detail_text);
+
+ if (mainText != null && getEntries() != null && position < getEntries().length) {
+ mainText.setText(getEntries()[position]);
+ }
+
+ if (detailText != null) {
+ if (mDetails != null && position < mDetails.length && !TextUtils.isEmpty(mDetails[position])) {
+ detailText.setVisibility(View.VISIBLE);
+ detailText.setText(mDetails[position]);
+ } else {
+ detailText.setVisibility(View.GONE);
+ }
+ }
+
+ if (radioButton != null) {
+ radioButton.setChecked(mSelectedIndex == position);
+ radioButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ changeSelection(position);
+ }
+ });
+ }
+
+ convertView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ changeSelection(position);
+ }
+ });
+
+ return convertView;
+ }
+
+ private void changeSelection(int position) {
+ mSelectedIndex = position;
+ notifyDataSetChanged();
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/DotComSiteSettings.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DotComSiteSettings.java
new file mode 100644
index 000000000..b59174ab5
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/DotComSiteSettings.java
@@ -0,0 +1,383 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Activity;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.SiteSettingsTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.CategoryModel;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+class DotComSiteSettings extends SiteSettingsInterface {
+ // WP.com REST keys used in response to a settings GET and POST request
+ public static final String LANGUAGE_ID_KEY = "lang_id";
+ public static final String PRIVACY_KEY = "blog_public";
+ public static final String URL_KEY = "URL";
+ public static final String DEF_CATEGORY_KEY = "default_category";
+ public static final String DEF_POST_FORMAT_KEY = "default_post_format";
+ public static final String RELATED_POSTS_ALLOWED_KEY = "jetpack_relatedposts_allowed";
+ public static final String RELATED_POSTS_ENABLED_KEY = "jetpack_relatedposts_enabled";
+ public static final String RELATED_POSTS_HEADER_KEY = "jetpack_relatedposts_show_headline";
+ public static final String RELATED_POSTS_IMAGES_KEY = "jetpack_relatedposts_show_thumbnails";
+ public static final String ALLOW_COMMENTS_KEY = "default_comment_status";
+ public static final String SEND_PINGBACKS_KEY = "default_pingback_flag";
+ public static final String RECEIVE_PINGBACKS_KEY = "default_ping_status";
+ public static final String CLOSE_OLD_COMMENTS_KEY = "close_comments_for_old_posts";
+ public static final String CLOSE_OLD_COMMENTS_DAYS_KEY = "close_comments_days_old";
+ public static final String THREAD_COMMENTS_KEY = "thread_comments";
+ public static final String THREAD_COMMENTS_DEPTH_KEY = "thread_comments_depth";
+ public static final String PAGE_COMMENTS_KEY = "page_comments";
+ public static final String PAGE_COMMENT_COUNT_KEY = "comments_per_page";
+ public static final String COMMENT_SORT_ORDER_KEY = "comment_order";
+ public static final String COMMENT_MODERATION_KEY = "comment_moderation";
+ public static final String REQUIRE_IDENTITY_KEY = "require_name_email";
+ public static final String REQUIRE_USER_ACCOUNT_KEY = "comment_registration";
+ public static final String WHITELIST_KNOWN_USERS_KEY = "comment_whitelist";
+ public static final String MAX_LINKS_KEY = "comment_max_links";
+ public static final String MODERATION_KEYS_KEY = "moderation_keys";
+ public static final String BLACKLIST_KEYS_KEY = "blacklist_keys";
+
+ // WP.com REST keys used to GET certain site settings
+ public static final String GET_TITLE_KEY = "name";
+ public static final String GET_DESC_KEY = "description";
+
+ // WP.com REST keys used to POST updates to site settings
+ private static final String SET_TITLE_KEY = "blogname";
+ private static final String SET_DESC_KEY = "blogdescription";
+
+ // JSON response keys
+ private static final String SETTINGS_KEY = "settings";
+ private static final String UPDATED_KEY = "updated";
+
+ // WP.com REST keys used in response to a categories GET request
+ private static final String CAT_ID_KEY = "ID";
+ private static final String CAT_NAME_KEY = "name";
+ private static final String CAT_SLUG_KEY = "slug";
+ private static final String CAT_DESC_KEY = "description";
+ private static final String CAT_PARENT_ID_KEY = "parent";
+ private static final String CAT_POST_COUNT_KEY = "post_count";
+ private static final String CAT_NUM_POSTS_KEY = "found";
+ private static final String CATEGORIES_KEY = "categories";
+
+ /**
+ * Only instantiated by {@link SiteSettingsInterface}.
+ */
+ DotComSiteSettings(Activity host, Blog blog, SiteSettingsListener listener) {
+ super(host, blog, listener);
+ }
+
+ @Override
+ public void saveSettings() {
+ super.saveSettings();
+
+ final Map<String, String> params = serializeDotComParams();
+ if (params == null || params.isEmpty()) return;
+
+ WordPress.getRestClientUtils().setGeneralSiteSettings(
+ String.valueOf(mBlog.getRemoteBlogId()), new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ AppLog.d(AppLog.T.API, "Site Settings saved remotely");
+ notifySavedOnUiThread(null);
+ mRemoteSettings.copyFrom(mSettings);
+
+ if (response != null) {
+ JSONObject updated = response.optJSONObject(UPDATED_KEY);
+ if (updated == null) return;
+ HashMap<String, Object> properties = new HashMap<>();
+ Iterator<String> keys = updated.keys();
+ while (keys.hasNext()) {
+ String currentKey = keys.next();
+ Object currentValue = updated.opt(currentKey);
+ if (currentValue != null) {
+ properties.put(SAVED_ITEM_PREFIX + currentKey, currentValue);
+ }
+ }
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_SAVED_REMOTELY, properties);
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ AppLog.w(AppLog.T.API, "Error POSTing site settings changes: " + error);
+ notifySavedOnUiThread(error);
+ }
+ }, params);
+ }
+
+ /**
+ * Request remote site data via the WordPress REST API.
+ */
+ @Override
+ protected void fetchRemoteData() {
+ fetchCategories();
+ WordPress.getRestClientUtils().getGeneralSettings(
+ String.valueOf(mBlog.getRemoteBlogId()), new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ AppLog.d(AppLog.T.API, "Received response to Settings REST request.");
+ credentialsVerified(true);
+
+ mRemoteSettings.localTableId = mBlog.getRemoteBlogId();
+ deserializeDotComRestResponse(mBlog, response);
+ if (!mRemoteSettings.equals(mSettings)) {
+ // postFormats setting is not returned by this api call so copy it over
+ final Map<String, String> currentPostFormats = mSettings.postFormats;
+
+ mSettings.copyFrom(mRemoteSettings);
+
+ mSettings.postFormats = currentPostFormats;
+
+ SiteSettingsTable.saveSettings(mSettings);
+ notifyUpdatedOnUiThread(null);
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ AppLog.w(AppLog.T.API, "Error response to Settings REST request: " + error);
+ notifyUpdatedOnUiThread(error);
+ }
+ });
+ }
+
+ /**
+ * Sets values from a .com REST response object.
+ */
+ public void deserializeDotComRestResponse(Blog blog, JSONObject response) {
+ if (blog == null || response == null) return;
+ JSONObject settingsObject = response.optJSONObject(SETTINGS_KEY);
+
+ mRemoteSettings.username = blog.getUsername();
+ mRemoteSettings.password = blog.getPassword();
+ mRemoteSettings.address = response.optString(URL_KEY, "");
+ mRemoteSettings.title = response.optString(GET_TITLE_KEY, "");
+ mRemoteSettings.tagline = response.optString(GET_DESC_KEY, "");
+ mRemoteSettings.languageId = settingsObject.optInt(LANGUAGE_ID_KEY, -1);
+ mRemoteSettings.privacy = settingsObject.optInt(PRIVACY_KEY, -2);
+ mRemoteSettings.defaultCategory = settingsObject.optInt(DEF_CATEGORY_KEY, 0);
+ mRemoteSettings.defaultPostFormat = settingsObject.optString(DEF_POST_FORMAT_KEY, "0");
+ mRemoteSettings.language = languageIdToLanguageCode(Integer.toString(mRemoteSettings.languageId));
+ mRemoteSettings.allowComments = settingsObject.optBoolean(ALLOW_COMMENTS_KEY, true);
+ mRemoteSettings.sendPingbacks = settingsObject.optBoolean(SEND_PINGBACKS_KEY, false);
+ mRemoteSettings.receivePingbacks = settingsObject.optBoolean(RECEIVE_PINGBACKS_KEY, true);
+ mRemoteSettings.shouldCloseAfter = settingsObject.optBoolean(CLOSE_OLD_COMMENTS_KEY, false);
+ mRemoteSettings.closeCommentAfter = settingsObject.optInt(CLOSE_OLD_COMMENTS_DAYS_KEY, 0);
+ mRemoteSettings.shouldThreadComments = settingsObject.optBoolean(THREAD_COMMENTS_KEY, false);
+ mRemoteSettings.threadingLevels = settingsObject.optInt(THREAD_COMMENTS_DEPTH_KEY, 0);
+ mRemoteSettings.shouldPageComments = settingsObject.optBoolean(PAGE_COMMENTS_KEY, false);
+ mRemoteSettings.commentsPerPage = settingsObject.optInt(PAGE_COMMENT_COUNT_KEY, 0);
+ mRemoteSettings.commentApprovalRequired = settingsObject.optBoolean(COMMENT_MODERATION_KEY, false);
+ mRemoteSettings.commentsRequireIdentity = settingsObject.optBoolean(REQUIRE_IDENTITY_KEY, false);
+ mRemoteSettings.commentsRequireUserAccount = settingsObject.optBoolean(REQUIRE_USER_ACCOUNT_KEY, true);
+ mRemoteSettings.commentAutoApprovalKnownUsers = settingsObject.optBoolean(WHITELIST_KNOWN_USERS_KEY, false);
+ mRemoteSettings.maxLinks = settingsObject.optInt(MAX_LINKS_KEY, 0);
+ mRemoteSettings.holdForModeration = new ArrayList<>();
+ mRemoteSettings.blacklist = new ArrayList<>();
+
+ String modKeys = settingsObject.optString(MODERATION_KEYS_KEY, "");
+ if (modKeys.length() > 0) {
+ Collections.addAll(mRemoteSettings.holdForModeration, modKeys.split("\n"));
+ }
+ String blacklistKeys = settingsObject.optString(BLACKLIST_KEYS_KEY, "");
+ if (blacklistKeys.length() > 0) {
+ Collections.addAll(mRemoteSettings.blacklist, blacklistKeys.split("\n"));
+ }
+
+ if (settingsObject.optString(COMMENT_SORT_ORDER_KEY, "").equals("asc")) {
+ mRemoteSettings.sortCommentsBy = ASCENDING_SORT;
+ } else {
+ mRemoteSettings.sortCommentsBy = DESCENDING_SORT;
+ }
+
+ if (settingsObject.optBoolean(RELATED_POSTS_ALLOWED_KEY, false)) {
+ mRemoteSettings.showRelatedPosts = settingsObject.optBoolean(RELATED_POSTS_ENABLED_KEY, false);
+ mRemoteSettings.showRelatedPostHeader = settingsObject.optBoolean(RELATED_POSTS_HEADER_KEY, false);
+ mRemoteSettings.showRelatedPostImages = settingsObject.optBoolean(RELATED_POSTS_IMAGES_KEY, false);
+ }
+ }
+
+ /**
+ * Helper method to create the parameters for the site settings POST request
+ *
+ * Using undocumented endpoint WPCOM_JSON_API_Site_Settings_Endpoint
+ * https://wpcom.trac.automattic.com/browser/trunk/public.api/rest/json-endpoints.php#L1903
+ */
+ public Map<String, String> serializeDotComParams() {
+ Map<String, String> params = new HashMap<>();
+
+ if (mSettings.title!= null && !mSettings.title.equals(mRemoteSettings.title)) {
+ params.put(SET_TITLE_KEY, mSettings.title);
+ }
+ if (mSettings.tagline != null && !mSettings.tagline.equals(mRemoteSettings.tagline)) {
+ params.put(SET_DESC_KEY, mSettings.tagline);
+ }
+ if (mSettings.languageId != mRemoteSettings.languageId) {
+ params.put(LANGUAGE_ID_KEY, String.valueOf((mSettings.languageId)));
+ }
+ if (mSettings.privacy != mRemoteSettings.privacy) {
+ params.put(PRIVACY_KEY, String.valueOf((mSettings.privacy)));
+ }
+ if (mSettings.defaultCategory != mRemoteSettings.defaultCategory) {
+ params.put(DEF_CATEGORY_KEY, String.valueOf(mSettings.defaultCategory));
+ }
+ if (mSettings.defaultPostFormat != null && !mSettings.defaultPostFormat.equals(mRemoteSettings.defaultPostFormat)) {
+ params.put(DEF_POST_FORMAT_KEY, mSettings.defaultPostFormat);
+ }
+ if (mSettings.showRelatedPosts != mRemoteSettings.showRelatedPosts ||
+ mSettings.showRelatedPostHeader != mRemoteSettings.showRelatedPostHeader ||
+ mSettings.showRelatedPostImages != mRemoteSettings.showRelatedPostImages) {
+ params.put(RELATED_POSTS_ENABLED_KEY, String.valueOf(mSettings.showRelatedPosts));
+ params.put(RELATED_POSTS_HEADER_KEY, String.valueOf(mSettings.showRelatedPostHeader));
+ params.put(RELATED_POSTS_IMAGES_KEY, String.valueOf(mSettings.showRelatedPostImages));
+ }
+ if (mSettings.allowComments != mRemoteSettings.allowComments) {
+ params.put(ALLOW_COMMENTS_KEY, String.valueOf(mSettings.allowComments));
+ }
+ if (mSettings.sendPingbacks != mRemoteSettings.sendPingbacks) {
+ params.put(SEND_PINGBACKS_KEY, String.valueOf(mSettings.sendPingbacks));
+ }
+ if (mSettings.receivePingbacks != mRemoteSettings.receivePingbacks) {
+ params.put(RECEIVE_PINGBACKS_KEY, String.valueOf(mSettings.receivePingbacks));
+ }
+ if (mSettings.commentApprovalRequired != mRemoteSettings.commentApprovalRequired) {
+ params.put(COMMENT_MODERATION_KEY, String.valueOf(mSettings.commentApprovalRequired));
+ }
+ if (mSettings.closeCommentAfter != mRemoteSettings.closeCommentAfter
+ || mSettings.shouldCloseAfter != mRemoteSettings.shouldCloseAfter) {
+ params.put(CLOSE_OLD_COMMENTS_KEY, String.valueOf(mSettings.shouldCloseAfter));
+ params.put(CLOSE_OLD_COMMENTS_DAYS_KEY, String.valueOf(mSettings.closeCommentAfter));
+ }
+ if (mSettings.sortCommentsBy != mRemoteSettings.sortCommentsBy) {
+ if (mSettings.sortCommentsBy == ASCENDING_SORT) {
+ params.put(COMMENT_SORT_ORDER_KEY, "asc");
+ } else if (mSettings.sortCommentsBy == DESCENDING_SORT) {
+ params.put(COMMENT_SORT_ORDER_KEY, "desc");
+ }
+ }
+ if (mSettings.threadingLevels != mRemoteSettings.threadingLevels
+ || mSettings.shouldThreadComments != mRemoteSettings.shouldThreadComments) {
+ params.put(THREAD_COMMENTS_KEY, String.valueOf(mSettings.shouldThreadComments));
+ params.put(THREAD_COMMENTS_DEPTH_KEY, String.valueOf(mSettings.threadingLevels));
+ }
+ if (mSettings.commentsPerPage != mRemoteSettings.commentsPerPage
+ || mSettings.shouldPageComments != mRemoteSettings.shouldPageComments) {
+ params.put(PAGE_COMMENTS_KEY, String.valueOf(mSettings.shouldPageComments));
+ params.put(PAGE_COMMENT_COUNT_KEY, String.valueOf(mSettings.commentsPerPage));
+ }
+ if (mSettings.commentsRequireIdentity != mRemoteSettings.commentsRequireIdentity) {
+ params.put(REQUIRE_IDENTITY_KEY, String.valueOf(mSettings.commentsRequireIdentity));
+ }
+ if (mSettings.commentsRequireUserAccount != mRemoteSettings.commentsRequireUserAccount) {
+ params.put(REQUIRE_USER_ACCOUNT_KEY, String.valueOf(mSettings.commentsRequireUserAccount));
+ }
+ if (mSettings.commentAutoApprovalKnownUsers != mRemoteSettings.commentAutoApprovalKnownUsers) {
+ params.put(WHITELIST_KNOWN_USERS_KEY, String.valueOf(mSettings.commentAutoApprovalKnownUsers));
+ }
+ if (mSettings.maxLinks != mRemoteSettings.maxLinks) {
+ params.put(MAX_LINKS_KEY, String.valueOf(mSettings.maxLinks));
+ }
+ if (mSettings.holdForModeration != null && !mSettings.holdForModeration.equals(mRemoteSettings.holdForModeration)) {
+ StringBuilder builder = new StringBuilder();
+ for (String key : mSettings.holdForModeration) {
+ builder.append(key);
+ builder.append("\n");
+ }
+ if (builder.length() > 1) {
+ params.put(MODERATION_KEYS_KEY, builder.substring(0, builder.length() - 1));
+ } else {
+ params.put(MODERATION_KEYS_KEY, "");
+ }
+ }
+ if (mSettings.blacklist != null && !mSettings.blacklist.equals(mRemoteSettings.blacklist)) {
+ StringBuilder builder = new StringBuilder();
+ for (String key : mSettings.blacklist) {
+ builder.append(key);
+ builder.append("\n");
+ }
+ if (builder.length() > 1) {
+ params.put(BLACKLIST_KEYS_KEY, builder.substring(0, builder.length() - 1));
+ } else {
+ params.put(BLACKLIST_KEYS_KEY, "");
+ }
+ }
+
+ return params;
+ }
+
+ /**
+ * Request a list of post categories for a site via the WordPress REST API.
+ */
+ private void fetchCategories() {
+ WordPress.getRestClientUtilsV1_1().getCategories(String.valueOf(mBlog.getRemoteBlogId()),
+ new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ AppLog.d(AppLog.T.API, "Received response to Categories REST request.");
+ credentialsVerified(true);
+
+ CategoryModel[] models = deserializeJsonRestResponse(response);
+ if (models == null) return;
+
+ SiteSettingsTable.saveCategories(models);
+ mRemoteSettings.categories = models;
+ mSettings.categories = models;
+ notifyUpdatedOnUiThread(null);
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ AppLog.d(AppLog.T.API, "Error fetching WP.com categories:" + error);
+ }
+ });
+ }
+
+ private CategoryModel deserializeCategoryFromJson(JSONObject category) throws JSONException {
+ if (category == null) return null;
+
+ CategoryModel model = new CategoryModel();
+ model.id = category.getInt(CAT_ID_KEY);
+ model.name = category.getString(CAT_NAME_KEY);
+ model.slug = category.getString(CAT_SLUG_KEY);
+ model.description = category.getString(CAT_DESC_KEY);
+ model.parentId = category.getInt(CAT_PARENT_ID_KEY);
+ model.postCount = category.getInt(CAT_POST_COUNT_KEY);
+
+ return model;
+ }
+
+ private CategoryModel[] deserializeJsonRestResponse(JSONObject response) {
+ try {
+ int num = response.getInt(CAT_NUM_POSTS_KEY);
+ JSONArray categories = response.getJSONArray(CATEGORIES_KEY);
+ CategoryModel[] models = new CategoryModel[num];
+
+ for (int i = 0; i < num; ++i) {
+ JSONObject category = categories.getJSONObject(i);
+ models[i] = deserializeCategoryFromJson(category);
+ }
+
+ AppLog.d(AppLog.T.API, "Successfully fetched WP.com categories");
+
+ return models;
+ } catch (JSONException exception) {
+ AppLog.d(AppLog.T.API, "Error parsing WP.com categories response:" + response);
+ return null;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/EditTextPreferenceWithValidation.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/EditTextPreferenceWithValidation.java
new file mode 100644
index 000000000..1ddb8ac80
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/EditTextPreferenceWithValidation.java
@@ -0,0 +1,98 @@
+package org.wordpress.android.ui.prefs;
+
+import android.support.v7.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Patterns;
+import android.view.View;
+import android.widget.Button;
+
+import org.wordpress.android.R;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class EditTextPreferenceWithValidation extends SummaryEditTextPreference {
+ private ValidationType mValidationType = ValidationType.NONE;
+
+ public EditTextPreferenceWithValidation(Context context) {
+ super(context);
+ }
+
+ public EditTextPreferenceWithValidation(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public EditTextPreferenceWithValidation(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void showDialog(Bundle state) {
+ super.showDialog(state);
+
+ final AlertDialog dialog = (AlertDialog) getDialog();
+ Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ if (positiveButton != null) {
+ positiveButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String error = null;
+ CharSequence text = getEditText().getText();
+ if (mValidationType == ValidationType.EMAIL) {
+ error = validateEmail(text);
+ } else if (!TextUtils.isEmpty(text) && mValidationType == ValidationType.URL) {
+ error = validateUrl(text);
+ }
+
+ if (error != null) {
+ getEditText().setError(error);
+ } else {
+ callChangeListener(text);
+ dialog.dismiss();
+ }
+ }
+ });
+ }
+
+ CharSequence summary = getSummary();
+ if (TextUtils.isEmpty(summary)) {
+ getEditText().setText("");
+ } else {
+ getEditText().setText(summary);
+ getEditText().setSelection(0, summary.length());
+ }
+
+ // clear previous errors
+ getEditText().setError(null);
+ }
+
+ private String validateEmail(CharSequence text) {
+ final Pattern emailRegExPattern = Patterns.EMAIL_ADDRESS;
+ Matcher matcher = emailRegExPattern.matcher(text);
+ if (!matcher.matches()) {
+ return getContext().getString(R.string.invalid_email_message);
+ }
+ return null;
+ }
+
+ private String validateUrl(CharSequence text) {
+ final Pattern urlRegExPattern = Patterns.WEB_URL;
+ Matcher matcher = urlRegExPattern.matcher(text);
+ if (!matcher.matches()) {
+ return getContext().getString(R.string.invalid_url_message);
+ }
+ return null;
+ }
+
+ public void setValidationType(ValidationType validationType) {
+ mValidationType = validationType;
+ }
+
+ public enum ValidationType {
+ NONE, EMAIL, URL
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/EmptyViewRecyclerView.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/EmptyViewRecyclerView.java
new file mode 100644
index 000000000..f48affb53
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/EmptyViewRecyclerView.java
@@ -0,0 +1,72 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * RecyclerView with setEmptyView method which displays a view when RecyclerView adapter is empty.
+ */
+public class EmptyViewRecyclerView extends RecyclerView {
+ private View mEmptyView;
+
+ final private AdapterDataObserver observer = new AdapterDataObserver() {
+ @Override
+ public void onChanged() {
+ toggleEmptyView();
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ toggleEmptyView();
+ }
+
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ toggleEmptyView();
+ }
+ };
+
+ public EmptyViewRecyclerView(Context context) {
+ super(context);
+ }
+
+ public EmptyViewRecyclerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public EmptyViewRecyclerView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public void setAdapter(Adapter adapterNew) {
+ final RecyclerView.Adapter adapterOld = getAdapter();
+
+ if (adapterOld != null) {
+ adapterOld.unregisterAdapterDataObserver(observer);
+ }
+
+ super.setAdapter(adapterNew);
+
+ if (adapterNew != null) {
+ adapterNew.registerAdapterDataObserver(observer);
+ }
+
+ toggleEmptyView();
+ }
+
+ public void setEmptyView(View emptyView) {
+ mEmptyView = emptyView;
+ toggleEmptyView();
+ }
+
+ private void toggleEmptyView() {
+ if (mEmptyView != null && getAdapter() != null) {
+ final boolean empty = getAdapter().getItemCount() == 0;
+ mEmptyView.setVisibility(empty ? VISIBLE : GONE);
+ this.setVisibility(empty ? GONE : VISIBLE);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/LearnMorePreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/LearnMorePreference.java
new file mode 100644
index 000000000..b06932c69
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/LearnMorePreference.java
@@ -0,0 +1,175 @@
+package org.wordpress.android.ui.prefs;
+
+import android.annotation.SuppressLint;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.preference.Preference;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.webkit.WebResourceError;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.ToastUtils;
+
+public class LearnMorePreference extends Preference
+ implements PreferenceHint, View.OnClickListener {
+ private static final String WP_SUPPORT_URL = "https://en.support.wordpress.com/settings/discussion-settings/#default-article-settings";
+ private static final String SUPPORT_MOBILE_ID = "mobile-only-usage";
+ private static final String SUPPORT_CONTENT_JS = "javascript:(function(){" +
+ "var mobileSupport = document.getElementById('" + SUPPORT_MOBILE_ID + "');" +
+ "mobileSupport.style.display = 'inline';" +
+ "var newHtml = '<' + mobileSupport.tagName + '>' + mobileSupport.innerHTML + '</' + mobileSupport.tagName + '>';" +
+ "document.body.innerHTML = newHtml;" +
+ "document.body.setAttribute('style', 'padding:24px 24px 0px 24px !important');" +
+ "})();";
+
+ private String mHint;
+ private Dialog mDialog;
+
+ public LearnMorePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected View onCreateView(@NonNull ViewGroup parent) {
+ super.onCreateView(parent);
+
+ View view = View.inflate(getContext(), R.layout.learn_more_pref, null);
+ view.findViewById(R.id.learn_more_button).setOnClickListener(this);
+
+ return view;
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ if (mDialog != null && mDialog.isShowing()) {
+ mDialog.dismiss();
+ return new SavedState(super.onSaveInstanceState());
+ }
+ return super.onSaveInstanceState();
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof SavedState)) {// per documentation, state is always non-null
+ super.onRestoreInstanceState(state);
+ } else {
+ super.onRestoreInstanceState(((SavedState) state).getSuperState());
+ showDialog();
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mDialog != null) return;
+
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_LEARN_MORE_CLICKED);
+ showDialog();
+ }
+
+ @Override
+ public boolean hasHint() {
+ return !TextUtils.isEmpty(mHint);
+ }
+
+ @Override
+ public String getHint() {
+ return mHint;
+ }
+
+ @Override
+ public void setHint(String hint) {
+ mHint = hint;
+ }
+
+ private void showDialog() {
+ final WebView webView = loadSupportWebView();
+ mDialog = new Dialog(getContext());
+ mDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ webView.stopLoading();
+ mDialog = null;
+ }
+ });
+ mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ mDialog.setContentView(R.layout.learn_more_pref_screen);
+ WindowManager.LayoutParams params = mDialog.getWindow().getAttributes();
+ params.width = WindowManager.LayoutParams.MATCH_PARENT;
+ params.height = WindowManager.LayoutParams.MATCH_PARENT;
+ params.gravity = Gravity.CENTER;
+ params.x = 12;
+ params.y = 12;
+ mDialog.getWindow().setAttributes(params);
+ mDialog.show();
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ private WebView loadSupportWebView() {
+ WebView webView = new WebView(getContext());
+ WebSettings webSettings = webView.getSettings();
+ webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
+ webSettings.setJavaScriptEnabled(true);
+ webView.setWebViewClient(new LearnMoreClient());
+ webView.loadUrl(WP_SUPPORT_URL);
+ return webView;
+ }
+
+ private static class SavedState extends BaseSavedState {
+ public SavedState(Parcel source) { super(source); }
+
+ public SavedState(Parcelable superState) { super(superState); }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) { return new SavedState(in); }
+
+ public SavedState[] newArray(int size) { return new SavedState[size]; }
+ };
+ }
+
+ private class LearnMoreClient extends WebViewClient {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView webView, String url) {
+ return !WP_SUPPORT_URL.equals(url) && !SUPPORT_CONTENT_JS.equals(url);
+ }
+
+ @Override
+ public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
+ super.onReceivedError(view, request, error);
+
+ if (mDialog != null && mDialog.isShowing()) {
+ ToastUtils.showToast(getContext(), R.string.could_not_load_page);
+ mDialog.dismiss();
+ }
+ }
+
+ @Override
+ public void onPageFinished(WebView webView, String url) {
+ super.onPageFinished(webView, url);
+ if (mDialog != null) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_LEARN_MORE_LOADED);
+ webView.loadUrl(SUPPORT_CONTENT_JS);
+ mDialog.setContentView(webView);
+ webView.scrollTo(0, 0);
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/LicensesActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/LicensesActivity.java
new file mode 100644
index 000000000..0b5ce8383
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/LicensesActivity.java
@@ -0,0 +1,22 @@
+package org.wordpress.android.ui.prefs;
+
+import android.os.Bundle;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.WebViewActivity;
+
+/**
+ * Display open source licenses for the application.
+ */
+public class LicensesActivity extends WebViewActivity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle(getResources().getText(R.string.open_source_licenses));
+ }
+
+ @Override
+ protected void loadContent() {
+ loadUrl("file:///android_asset/licenses.html");
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MultiSelectRecyclerViewAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MultiSelectRecyclerViewAdapter.java
new file mode 100644
index 000000000..7d70c6bac
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MultiSelectRecyclerViewAdapter.java
@@ -0,0 +1,100 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.SparseBooleanArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+
+import java.util.List;
+
+/**
+ * RecyclerView.Adapter for selecting multiple list items with simple layout (TextView + divider).
+ */
+public class MultiSelectRecyclerViewAdapter extends RecyclerView.Adapter<MultiSelectRecyclerViewAdapter.ItemHolder> {
+ private final List<String> mItems;
+ private final SparseBooleanArray mItemsSelected;
+ private final int mSelectedColor;
+ private final int mUnselectedColor;
+
+ public MultiSelectRecyclerViewAdapter(Context context, List<String> items) {
+ this.mSelectedColor = ContextCompat.getColor(context, R.color.white);
+ this.mUnselectedColor = ContextCompat.getColor(context, R.color.transparent);
+ this.mItems = items;
+ this.mItemsSelected = new SparseBooleanArray();
+ }
+
+ public class ItemHolder extends RecyclerView.ViewHolder {
+ private final LinearLayout container;
+ private final TextView text;
+
+ public ItemHolder(View view) {
+ super(view);
+ this.container = (LinearLayout) view.findViewById(R.id.container);
+ this.text = (TextView) view.findViewById(R.id.text);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mItems.size();
+ }
+
+ @Override
+ public void onBindViewHolder(final ItemHolder holder, int position) {
+ String item = getItem(holder.getAdapterPosition());
+ holder.text.setText(item);
+ holder.container.setBackgroundColor(
+ isItemSelected(position) ?
+ mSelectedColor :
+ mUnselectedColor
+ );
+ }
+
+ @Override
+ public ItemHolder onCreateViewHolder(ViewGroup parent, int type) {
+ return new ItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.wp_simple_list_item_1, parent, false));
+ }
+
+ public String getItem(int position) {
+ return mItems.get(position);
+ }
+
+ public SparseBooleanArray getItemsSelected() {
+ return mItemsSelected;
+ }
+
+ private boolean isItemSelected(int position) {
+ String item = getItem(position);
+ return item != null && mItemsSelected.get(position);
+ }
+
+ public void removeItemsSelected() {
+ mItemsSelected.clear();
+ notifyDataSetChanged();
+ }
+
+ public void setItemSelected(int position) {
+ if (!mItemsSelected.get(position)) {
+ mItemsSelected.put(position, true);
+ }
+
+ notifyItemChanged(position);
+ }
+
+ public void toggleItemSelected(int position) {
+ if (!mItemsSelected.get(position)) {
+ mItemsSelected.put(position, true);
+ } else {
+ mItemsSelected.delete(position);
+ }
+
+ notifyItemChanged(position);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileActivity.java
new file mode 100644
index 000000000..600ad13f0
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileActivity.java
@@ -0,0 +1,42 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+
+public class MyProfileActivity extends AppCompatActivity {
+ private static final String KEY_MY_PROFILE_FRAGMENT = "my-profile-fragment";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ FragmentManager fragmentManager = getFragmentManager();
+ MyProfileFragment myProfileFragment =
+ (MyProfileFragment) fragmentManager.findFragmentByTag(KEY_MY_PROFILE_FRAGMENT);
+ if (myProfileFragment == null) {
+ myProfileFragment = MyProfileFragment.newInstance();
+
+ fragmentManager.beginTransaction()
+ .add(android.R.id.content, myProfileFragment, KEY_MY_PROFILE_FRAGMENT)
+ .commit();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java
new file mode 100644
index 000000000..5b6272a8a
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java
@@ -0,0 +1,175 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.Account;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.AccountModel;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.widgets.WPTextView;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+public class MyProfileFragment extends Fragment implements ProfileInputDialogFragment.Callback {
+ private final String DIALOG_TAG = "DIALOG";
+
+ private WPTextView mFirstName;
+ private WPTextView mLastName;
+ private WPTextView mDisplayName;
+ private WPTextView mAboutMe;
+
+ public static MyProfileFragment newInstance() {
+ return new MyProfileFragment();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ refreshDetails();
+ if (NetworkUtils.isNetworkAvailable(getActivity())) {
+ AccountHelper.getDefaultAccount().fetchAccountSettings();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.my_profile_fragment, container, false);
+
+ mFirstName = (WPTextView) rootView.findViewById(R.id.first_name);
+ mLastName = (WPTextView) rootView.findViewById(R.id.last_name);
+ mDisplayName = (WPTextView) rootView.findViewById(R.id.display_name);
+ mAboutMe = (WPTextView) rootView.findViewById(R.id.about_me);
+
+ rootView.findViewById(R.id.first_name_row).setOnClickListener(
+ createOnClickListener(
+ getString(R.string.first_name),
+ null,
+ mFirstName,
+ false));
+ rootView.findViewById(R.id.last_name_row).setOnClickListener(
+ createOnClickListener(
+ getString(R.string.last_name),
+ null,
+ mLastName,
+ false));
+ rootView.findViewById(R.id.display_name_row).setOnClickListener(
+ createOnClickListener(
+ getString(R.string.public_display_name),
+ getString(R.string.public_display_name_hint),
+ mDisplayName,
+ false));
+ rootView.findViewById(R.id.about_me_row).setOnClickListener(
+ createOnClickListener(
+ getString(R.string.about_me),
+ getString(R.string.about_me_hint),
+ mAboutMe,
+ true));
+
+ return rootView;
+ }
+
+ private void refreshDetails() {
+ if (!isAdded()) return;
+
+ Account account = AccountHelper.getDefaultAccount();
+ updateLabel(mFirstName, account != null ? StringUtils.unescapeHTML(account.getFirstName()) : null);
+ updateLabel(mLastName, account != null ? StringUtils.unescapeHTML(account.getLastName()) : null);
+ updateLabel(mDisplayName, account != null ? StringUtils.unescapeHTML(account.getDisplayName()) : null);
+ updateLabel(mAboutMe, account != null ? StringUtils.unescapeHTML(account.getAboutMe()) : null);
+ }
+
+ private void updateLabel(WPTextView textView, String text) {
+ textView.setText(text);
+ if (TextUtils.isEmpty(text)) {
+ if (textView == mDisplayName) {
+ Account account = AccountHelper.getDefaultAccount();
+ mDisplayName.setText(account.getUserName());
+ } else {
+ textView.setVisibility(View.GONE);
+ }
+ }
+ else {
+ textView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ // helper method to create onClickListener to avoid code duplication
+ private View.OnClickListener createOnClickListener(final String dialogTitle,
+ final String hint,
+ final WPTextView textView,
+ final boolean isMultiline) {
+ return new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ProfileInputDialogFragment inputDialog = ProfileInputDialogFragment.newInstance(dialogTitle,
+ textView.getText().toString(), hint, isMultiline, textView.getId());
+ inputDialog.setTargetFragment(MyProfileFragment.this, 0);
+ inputDialog.show(getFragmentManager(), DIALOG_TAG);
+ }
+ };
+ }
+
+ @Override
+ public void onSuccessfulInput(String input, int callbackId) {
+ View rootView = getView();
+ if (rootView == null) return;
+
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ ToastUtils.showToast(getActivity(), R.string.error_post_my_profile_no_connection);
+ return;
+ }
+
+ WPTextView textView = (WPTextView) rootView.findViewById(callbackId);
+ updateLabel(textView, input);
+ updateMyProfileForLabel(textView);
+ }
+
+ private void updateMyProfileForLabel(TextView textView) {
+ Map<String, String> params = new HashMap<>();
+ params.put(restParamForTextView(textView), textView.getText().toString());
+ AccountHelper.getDefaultAccount().postAccountSettings(params);
+ }
+
+ // helper method to get the rest parameter for a text view
+ private String restParamForTextView(TextView textView) {
+ if (textView == mFirstName) {
+ return AccountModel.RestParam.FIRST_NAME.getDescription();
+ } else if (textView == mLastName) {
+ return AccountModel.RestParam.LAST_NAME.getDescription();
+ } else if (textView == mDisplayName) {
+ return AccountModel.RestParam.DISPLAY_NAME.getDescription();
+ } else if (textView == mAboutMe) {
+ return AccountModel.RestParam.ABOUT_ME.getDescription();
+ }
+ return null;
+ }
+
+ public void onEventMainThread(PrefsEvents.AccountSettingsFetchSuccess event) {
+ refreshDetails();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java
new file mode 100644
index 000000000..2f72ab6fe
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java
@@ -0,0 +1,166 @@
+package org.wordpress.android.ui.prefs;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.widget.SwitchCompat;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.NumberPicker;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.WPPrefUtils;
+
+public class NumberPickerDialog extends DialogFragment
+ implements DialogInterface.OnClickListener,
+ CompoundButton.OnCheckedChangeListener {
+
+ public static final String SHOW_SWITCH_KEY = "show-switch";
+ public static final String SWITCH_ENABLED_KEY = "switch-enabled";
+ public static final String SWITCH_TITLE_KEY = "switch-title";
+ public static final String SWITCH_DESC_KEY = "switch-description";
+ public static final String TITLE_KEY = "dialog-title";
+ public static final String HEADER_TEXT_KEY = "header-text";
+ public static final String MIN_VALUE_KEY = "min-value";
+ public static final String MAX_VALUE_KEY = "max-value";
+ public static final String CUR_VALUE_KEY = "cur-value";
+
+ private static final int DEFAULT_MIN_VALUE = 0;
+ private static final int DEFAULT_MAX_VALUE = 99;
+
+ private SwitchCompat mSwitch;
+ private TextView mHeaderText;
+ private NumberPicker mNumberPicker;
+ private NumberPicker.Formatter mFormat;
+ private int mMinValue;
+ private int mMaxValue;
+ private boolean mConfirmed;
+
+ public NumberPickerDialog() {
+ mMinValue = DEFAULT_MIN_VALUE;
+ mMaxValue = DEFAULT_MAX_VALUE;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.Calypso_AlertDialog);
+ View view = View.inflate(getActivity(), R.layout.number_picker_dialog, null);
+ TextView switchText = (TextView) view.findViewById(R.id.number_picker_text);
+ mSwitch = (SwitchCompat) view.findViewById(R.id.number_picker_switch);
+ mHeaderText = (TextView) view.findViewById(R.id.number_picker_header);
+ mNumberPicker = (NumberPicker) view.findViewById(R.id.number_picker);
+ int value = mMinValue;
+
+ Bundle args = getArguments();
+ if (args != null) {
+ if (args.getBoolean(SHOW_SWITCH_KEY, false)) {
+ mSwitch.setVisibility(View.VISIBLE);
+ mSwitch.setText(args.getString(SWITCH_TITLE_KEY, ""));
+ mSwitch.setChecked(args.getBoolean(SWITCH_ENABLED_KEY, false));
+ final View toggleContainer = view.findViewById(R.id.number_picker_toggleable);
+ toggleContainer.setEnabled(mSwitch.isChecked());
+ mNumberPicker.setEnabled(mSwitch.isChecked());
+ } else {
+ mSwitch.setVisibility(View.GONE);
+ }
+ switchText.setText(args.getString(SWITCH_DESC_KEY, ""));
+ mHeaderText.setText(args.getString(HEADER_TEXT_KEY, ""));
+ mMinValue = args.getInt(MIN_VALUE_KEY, DEFAULT_MIN_VALUE);
+ mMaxValue = args.getInt(MAX_VALUE_KEY, DEFAULT_MAX_VALUE);
+ value = args.getInt(CUR_VALUE_KEY, mMinValue);
+
+ builder.setCustomTitle(getDialogTitleView(args.getString(TITLE_KEY, "")));
+ }
+
+ mNumberPicker.setFormatter(mFormat);
+ mNumberPicker.setMinValue(mMinValue);
+ mNumberPicker.setMaxValue(mMaxValue);
+ mNumberPicker.setValue(value);
+
+ mSwitch.setOnCheckedChangeListener(this);
+
+ // hide empty text views
+ if (TextUtils.isEmpty(switchText.getText())) {
+ switchText.setVisibility(View.GONE);
+ }
+ if (TextUtils.isEmpty(mHeaderText.getText())) {
+ mHeaderText.setVisibility(View.GONE);
+ }
+
+ builder.setPositiveButton(R.string.ok, this);
+ builder.setNegativeButton(R.string.cancel, this);
+ builder.setView(view);
+
+ return builder.create();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ AlertDialog dialog = (AlertDialog) getDialog();
+ Button positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ Button negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ if (positive != null) WPPrefUtils.layoutAsFlatButton(positive);
+ if (negative != null) WPPrefUtils.layoutAsFlatButton(negative);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mConfirmed = which == DialogInterface.BUTTON_POSITIVE;
+ dismiss();
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ mNumberPicker.setEnabled(isChecked);
+ mHeaderText.setEnabled(isChecked);
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ Fragment target = getTargetFragment();
+ if (target != null) {
+ target.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, getResultIntent());
+ }
+
+ super.onDismiss(dialog);
+ }
+
+ public void setNumberFormat(NumberPicker.Formatter format) {
+ mFormat = format;
+ }
+
+ private View getDialogTitleView(String title) {
+ LayoutInflater inflater = LayoutInflater.from(getActivity());
+ @SuppressLint("InflateParams")
+ View titleView = inflater.inflate(R.layout.detail_list_preference_title, null);
+ TextView titleText = ((TextView) titleView.findViewById(R.id.title));
+ titleText.setText(title);
+ titleText.setLayoutParams(new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.MATCH_PARENT,
+ RelativeLayout.LayoutParams.WRAP_CONTENT));
+ return titleView;
+ }
+
+ private Intent getResultIntent() {
+ if (mConfirmed) {
+ return new Intent()
+ .putExtra(SWITCH_ENABLED_KEY, mSwitch.isChecked())
+ .putExtra(CUR_VALUE_KEY, mNumberPicker.getValue());
+ }
+
+ return null;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/PreferenceHint.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/PreferenceHint.java
new file mode 100644
index 000000000..5047ad17e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/PreferenceHint.java
@@ -0,0 +1,7 @@
+package org.wordpress.android.ui.prefs;
+
+public interface PreferenceHint {
+ boolean hasHint();
+ String getHint();
+ void setHint(String hint);
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/PrefsEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/PrefsEvents.java
new file mode 100644
index 000000000..a39849324
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/PrefsEvents.java
@@ -0,0 +1,20 @@
+package org.wordpress.android.ui.prefs;
+
+import com.android.volley.VolleyError;
+
+public class PrefsEvents {
+ public static class AccountSettingsFetchSuccess {}
+ public static class AccountSettingsPostSuccess {}
+ public static class AccountSettingsFetchError {
+ public final VolleyError mVolleyError;
+ public AccountSettingsFetchError(VolleyError error) {
+ mVolleyError = error;
+ }
+ }
+ public static class AccountSettingsPostError {
+ public final VolleyError mVolleyError;
+ public AccountSettingsPostError(VolleyError error) {
+ mVolleyError = error;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/ProfileInputDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/ProfileInputDialogFragment.java
new file mode 100644
index 000000000..dca7db6bb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/ProfileInputDialogFragment.java
@@ -0,0 +1,111 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.WPPrefUtils;
+import org.wordpress.android.widgets.WPEditText;
+import org.wordpress.android.widgets.WPTextView;
+
+public class ProfileInputDialogFragment extends DialogFragment {
+ private static final String TITLE_TAG = "title";
+ private static final String INITIAL_TEXT_TAG = "initial_text";
+ private static final String HINT_TAG = "hint";
+ private static final String IS_MULTILINE_TAG = "is_multiline";
+ private static final String CALLBACK_ID_TAG = "callback_id";
+
+ public static ProfileInputDialogFragment newInstance(String title,
+ String initialText,
+ String hint,
+ boolean isMultiline,
+ int callbackId) {
+
+ ProfileInputDialogFragment profileInputDialogFragment = new ProfileInputDialogFragment();
+ Bundle args = new Bundle();
+
+ args.putString(TITLE_TAG, title);
+ args.putString(INITIAL_TEXT_TAG, initialText);
+ args.putString(HINT_TAG, hint);
+ args.putBoolean(IS_MULTILINE_TAG, isMultiline);
+ args.putInt(CALLBACK_ID_TAG, callbackId);
+
+ profileInputDialogFragment.setArguments(args);
+ return profileInputDialogFragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
+ View promptView = layoutInflater.inflate(R.layout.my_profile_dialog, null);
+ AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity());
+ alertDialogBuilder.setView(promptView);
+
+ final WPTextView textView = (WPTextView) promptView.findViewById(R.id.my_profile_dialog_label);
+ final WPEditText editText = (WPEditText) promptView.findViewById(R.id.my_profile_dialog_input);
+ final WPTextView hintView = (WPTextView) promptView.findViewById(R.id.my_profile_dialog_hint);
+
+ Bundle args = getArguments();
+ String title = args.getString(TITLE_TAG);
+ String hint = args.getString(HINT_TAG);
+ Boolean isMultiline = args.getBoolean(IS_MULTILINE_TAG);
+ String initialText = args.getString(INITIAL_TEXT_TAG);
+ final int callbackId = args.getInt(CALLBACK_ID_TAG);
+
+ textView.setText(title);
+ if (!TextUtils.isEmpty(hint)) {
+ hintView.setText(hint);
+ } else {
+ hintView.setVisibility(View.GONE);
+ }
+
+ if (!isMultiline) {
+ editText.setMaxLines(1);
+ }
+ if (!TextUtils.isEmpty(initialText)) {
+ editText.setText(initialText);
+ editText.setSelection(0, initialText.length());
+ }
+
+ alertDialogBuilder.setCancelable(true)
+ .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ if (getTargetFragment() instanceof Callback) {
+ ((Callback) getTargetFragment()).onSuccessfulInput(editText.getText().toString(), callbackId);
+ } else {
+ AppLog.e(AppLog.T.UTILS, "Target fragment doesn't implement ProfileInputDialogFragment.Callback");
+ }
+ }
+ })
+ .setNegativeButton(R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+
+ return alertDialogBuilder.create();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ AlertDialog dialog = (AlertDialog) getDialog();
+ Button positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ Button negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ if (positive != null) WPPrefUtils.layoutAsFlatButton(positive);
+ if (negative != null) WPPrefUtils.layoutAsFlatButton(negative);
+ }
+
+ public interface Callback {
+ void onSuccessfulInput(String input, int callbackId);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/RecyclerViewItemClickListener.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/RecyclerViewItemClickListener.java
new file mode 100644
index 000000000..8c6752cfa
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/RecyclerViewItemClickListener.java
@@ -0,0 +1,60 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+
+public class RecyclerViewItemClickListener implements RecyclerView.OnItemTouchListener {
+ private final GestureDetector mGestureDetector;
+ private final OnItemClickListener mListener;
+
+ public interface OnItemClickListener {
+ public void onItemClick(View view, int position);
+ public void onLongItemClick(View view, int position);
+ }
+
+ public RecyclerViewItemClickListener(Context context, final RecyclerView recyclerView, OnItemClickListener listener) {
+ mListener = listener;
+
+ mGestureDetector = new GestureDetector(
+ context,
+ new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onSingleTapUp(MotionEvent motionEvent) {
+ return true;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
+
+ if (child != null && mListener != null) {
+ mListener.onLongItemClick(child, recyclerView.getChildAdapterPosition(child));
+ }
+ }
+ }
+ );
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent motionEvent) {
+ View childView = view.findChildViewUnder(motionEvent.getX(), motionEvent.getY());
+
+ if (childView != null && mListener != null && mGestureDetector.onTouchEvent(motionEvent)) {
+ mListener.onItemClick(childView, view.getChildAdapterPosition(childView));
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent (boolean disallowIntercept){
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) {
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/RelatedPostsDialog.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/RelatedPostsDialog.java
new file mode 100644
index 000000000..446545bde
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/RelatedPostsDialog.java
@@ -0,0 +1,184 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.WPPrefUtils;
+import org.wordpress.android.widgets.WPSwitch;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class RelatedPostsDialog extends DialogFragment
+ implements DialogInterface.OnClickListener,
+ CompoundButton.OnCheckedChangeListener {
+
+ /**
+ * boolean
+ *
+ * Sets the default state of the Show Related Posts switch. The switch is off by default.
+ */
+ public static final String SHOW_RELATED_POSTS_KEY = "related-posts";
+
+ /**
+ * boolean
+ *
+ * Sets the default state of the Show Headers checkbox. The checkbox is off by default.
+ */
+ public static final String SHOW_HEADER_KEY = "show-header";
+
+ /**
+ * boolean
+ *
+ * Sets the default state of the Show Images checkbox. The checkbox is off by default.
+ */
+ public static final String SHOW_IMAGES_KEY = "show-images";
+
+ private WPSwitch mShowRelatedPosts;
+ private CheckBox mShowHeader;
+ private CheckBox mShowImages;
+ private TextView mPreviewHeader;
+ private TextView mRelatedPostsListHeader;
+ private LinearLayout mRelatedPostsList;
+ private List<ImageView> mPreviewImages;
+ private boolean mConfirmed;
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ View v = inflater.inflate(R.layout.related_posts_dialog, null, false);
+
+ mShowRelatedPosts = (WPSwitch) v.findViewById(R.id.toggle_related_posts_switch);
+ mShowHeader = (CheckBox) v.findViewById(R.id.show_header_checkbox);
+ mShowImages = (CheckBox) v.findViewById(R.id.show_images_checkbox);
+ mPreviewHeader = (TextView) v.findViewById(R.id.preview_header);
+ mRelatedPostsListHeader = (TextView) v.findViewById(R.id.related_posts_list_header);
+ mRelatedPostsList = (LinearLayout) v.findViewById(R.id.related_posts_list);
+
+ mPreviewImages = new ArrayList<>();
+ mPreviewImages.add((ImageView) v.findViewById(R.id.related_post_image1));
+ mPreviewImages.add((ImageView) v.findViewById(R.id.related_post_image2));
+ mPreviewImages.add((ImageView) v.findViewById(R.id.related_post_image3));
+
+ Bundle args = getArguments();
+ if (args != null) {
+ mShowRelatedPosts.setChecked(args.getBoolean(SHOW_RELATED_POSTS_KEY));
+ mShowHeader.setChecked(args.getBoolean(SHOW_HEADER_KEY));
+ mShowImages.setChecked(args.getBoolean(SHOW_IMAGES_KEY));
+ }
+
+ toggleShowHeader(mShowHeader.isChecked());
+ toggleShowImages(mShowImages.isChecked());
+
+ mShowRelatedPosts.setOnCheckedChangeListener(this);
+ mShowHeader.setOnCheckedChangeListener(this);
+ mShowImages.setOnCheckedChangeListener(this);
+
+ toggleViews(mShowRelatedPosts.isChecked());
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.Calypso_AlertDialog);
+ View titleView = inflater.inflate(R.layout.detail_list_preference_title, null);
+ TextView titleText = ((TextView) titleView.findViewById(R.id.title));
+ titleText.setText(R.string.site_settings_related_posts_title);
+ titleText.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT));
+ builder.setCustomTitle(titleView);
+ builder.setPositiveButton(R.string.ok, this);
+ builder.setNegativeButton(R.string.cancel, this);
+ builder.setView(v);
+
+ return builder.create();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ AlertDialog dialog = (AlertDialog) getDialog();
+ Button positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ Button negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ if (positive != null) WPPrefUtils.layoutAsFlatButton(positive);
+ if (negative != null) WPPrefUtils.layoutAsFlatButton(negative);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mConfirmed = which == DialogInterface.BUTTON_POSITIVE;
+ dismiss();
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (buttonView == mShowRelatedPosts) {
+ toggleViews(isChecked);
+ } else if (buttonView == mShowHeader) {
+ toggleShowHeader(isChecked);
+ } else if (buttonView == mShowImages) {
+ toggleShowImages(isChecked);
+ }
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ Fragment target = getTargetFragment();
+ if (target != null) {
+ target.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, getResultIntent());
+ }
+
+ super.onDismiss(dialog);
+ }
+
+ private void toggleShowHeader(boolean show) {
+ if (show) {
+ mRelatedPostsListHeader.setVisibility(View.VISIBLE);
+ } else {
+ mRelatedPostsListHeader.setVisibility(View.GONE);
+ }
+ }
+
+ private void toggleShowImages(boolean show) {
+ int visibility = show ? View.VISIBLE : View.GONE;
+ for (ImageView view : mPreviewImages) {
+ view.setVisibility(visibility);
+ }
+ }
+
+ private Intent getResultIntent() {
+ if (mConfirmed) {
+ return new Intent()
+ .putExtra(SHOW_RELATED_POSTS_KEY, mShowRelatedPosts.isChecked())
+ .putExtra(SHOW_HEADER_KEY, mShowHeader.isChecked())
+ .putExtra(SHOW_IMAGES_KEY, mShowImages.isChecked());
+ }
+
+ return null;
+ }
+
+ private void toggleViews(boolean enabled) {
+ mShowHeader.setEnabled(enabled);
+ mShowImages.setEnabled(enabled);
+ mPreviewHeader.setEnabled(enabled);
+ mRelatedPostsListHeader.setEnabled(enabled);
+
+ if (enabled) {
+ mRelatedPostsList.setAlpha(1.0f);
+ } else {
+ mRelatedPostsList.setAlpha(0.5f);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SelfHostedSiteSettings.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SelfHostedSiteSettings.java
new file mode 100644
index 000000000..3389b9e8e
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SelfHostedSiteSettings.java
@@ -0,0 +1,421 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Activity;
+import android.text.TextUtils;
+
+import org.wordpress.android.R;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.datasets.SiteSettingsTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.CategoryModel;
+import org.wordpress.android.models.SiteSettingsModel;
+import org.wordpress.android.util.LanguageUtils;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.MapUtils;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.XMLRPCCallback;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+class SelfHostedSiteSettings extends SiteSettingsInterface {
+ // XML-RPC wp.getOptions keys
+ public static final String PRIVACY_KEY = "blog_public";
+ public static final String DEF_CATEGORY_KEY = "default_category";
+ public static final String DEF_POST_FORMAT_KEY = "default_post_format";
+ public static final String ALLOW_COMMENTS_KEY = "default_comment_status";
+ public static final String SEND_PINGBACKS_KEY = "default_pingback_flag";
+ public static final String RECEIVE_PINGBACKS_KEY = "default_ping_status";
+ public static final String CLOSE_OLD_COMMENTS_KEY = "close_comments_for_old_posts";
+ public static final String CLOSE_OLD_COMMENTS_DAYS_KEY = "close_comments_days_old";
+ public static final String THREAD_COMMENTS_KEY = "thread_comments";
+ public static final String THREAD_COMMENTS_DEPTH_KEY = "thread_comments_depth";
+ public static final String PAGE_COMMENTS_KEY = "page_comments";
+ public static final String PAGE_COMMENT_COUNT_KEY = "comments_per_page";
+ public static final String COMMENT_SORT_ORDER_KEY = "comment_order";
+ public static final String COMMENT_MODERATION_KEY = "comment_moderation";
+ public static final String REQUIRE_IDENTITY_KEY = "require_name_email";
+ public static final String REQUIRE_USER_ACCOUNT_KEY = "comment_registration";
+ public static final String WHITELIST_KNOWN_USERS_KEY = "comment_whitelist";
+ public static final String MAX_LINKS_KEY = "comment_max_links";
+ public static final String MODERATION_KEYS_KEY = "moderation_keys";
+ public static final String BLACKLIST_KEYS_KEY = "blacklist_keys";
+ public static final String SOFTWARE_VERSION_KEY = "software_version";
+
+ private static final String BLOG_URL_KEY = "blog_url";
+ private static final String BLOG_TITLE_KEY = "blog_title";
+ private static final String BLOG_USERNAME_KEY = "username";
+ private static final String BLOG_PASSWORD_KEY = "password";
+ private static final String BLOG_TAGLINE_KEY = "blog_tagline";
+ private static final String BLOG_CATEGORY_ID_KEY = "categoryId";
+ private static final String BLOG_CATEGORY_PARENT_ID_KEY = "parentId";
+ private static final String BLOG_CATEGORY_DESCRIPTION_KEY = "categoryDescription";
+ private static final String BLOG_CATEGORY_NAME_KEY = "categoryName";
+
+ // Requires WordPress 4.5.x or higher
+ private static final int REQUIRED_MAJOR_VERSION = 4;
+ private static final int REQUIRED_MINOR_VERSION = 3;
+
+ private static final String OPTION_ALLOWED = "open";
+ private static final String OPTION_DISALLOWED = "closed";
+
+ SelfHostedSiteSettings(Activity host, Blog blog, SiteSettingsListener listener) {
+ super(host, blog, listener);
+ }
+
+ @Override
+ public SiteSettingsInterface init(boolean fetch) {
+ super.init(fetch);
+
+ if (mSettings.defaultCategory == 0) {
+ mSettings.defaultCategory = siteSettingsPreferences(mActivity).getInt(DEF_CATEGORY_PREF_KEY, 0);
+ }
+ if (TextUtils.isEmpty(mSettings.defaultPostFormat) || mSettings.defaultPostFormat.equals("0")) {
+ mSettings.defaultPostFormat = siteSettingsPreferences(mActivity).getString(DEF_FORMAT_PREF_KEY, "0");
+ }
+ mSettings.language = siteSettingsPreferences(mActivity).getString(LANGUAGE_PREF_KEY, LanguageUtils.getPatchedCurrentDeviceLanguage(null));
+
+ return this;
+ }
+
+ @Override
+ public void saveSettings() {
+ super.saveSettings();
+
+ final Map<String, String> params = serializeSelfHostedParams();
+ if (params == null || params.isEmpty()) return;
+
+ XMLRPCCallback callback = new XMLRPCCallback() {
+ @Override
+ public void onSuccess(long id, final Object result) {
+ notifySavedOnUiThread(null);
+ mRemoteSettings.copyFrom(mSettings);
+
+ if (result != null) {
+ HashMap<String, Object> properties = new HashMap<>();
+ if (result instanceof Map) {
+ Map<String, Object> resultMap = (Map) result;
+ Set<String> keys = resultMap.keySet();
+ for (String key : keys) {
+ Object currentValue = resultMap.get(key);
+ if (currentValue != null) {
+ properties.put(SAVED_ITEM_PREFIX + key, currentValue);
+ }
+ }
+ }
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_SAVED_REMOTELY, properties);
+ }
+ }
+
+ @Override
+ public void onFailure(long id, final Exception error) {
+ notifySavedOnUiThread(error);
+ }
+ };
+ final Object[] callParams = {
+ mBlog.getRemoteBlogId(), mSettings.username, mSettings.password, params
+ };
+
+ XMLRPCClientInterface xmlrpcInterface = instantiateInterface();
+ if (xmlrpcInterface == null) return;
+ xmlrpcInterface.callAsync(callback, Method.SET_OPTIONS, callParams);
+ }
+
+ /**
+ * Request remote site data via XML-RPC.
+ */
+ @Override
+ protected void fetchRemoteData() {
+ new Thread() {
+ @Override
+ public void run() {
+ Object[] params = {mBlog.getRemoteBlogId(), mBlog.getUsername(), mBlog.getPassword()};
+
+ // Need two interfaces or the first call gets aborted
+ instantiateInterface().callAsync(mOptionsCallback, Method.GET_OPTIONS, params);
+ instantiateInterface().callAsync(mCategoriesCallback, Method.GET_CATEGORIES, params);
+ }
+ }.start();
+ }
+
+ /**
+ * Handles response to fetching self-hosted site categories via XML-RPC.
+ */
+ private final XMLRPCCallback mCategoriesCallback = new XMLRPCCallback() {
+ @Override
+ public void onSuccess(long id, Object result) {
+ if (result instanceof Object[]) {
+ AppLog.d(AppLog.T.API, "Received Categories XML-RPC response.");
+ credentialsVerified(true);
+
+ mRemoteSettings.localTableId = mBlog.getRemoteBlogId();
+ deserializeCategoriesResponse(mRemoteSettings, (Object[]) result);
+ mSettings.categories = mRemoteSettings.categories;
+ SiteSettingsTable.saveCategories(mSettings.categories);
+ notifyUpdatedOnUiThread(null);
+ } else {
+ // Response is considered an error if we are unable to parse it
+ AppLog.w(AppLog.T.API, "Error parsing Categories XML-RPC response: " + result);
+ notifyUpdatedOnUiThread(new XMLRPCException("Unknown response object"));
+ }
+ }
+
+ @Override
+ public void onFailure(long id, Exception error) {
+ AppLog.w(AppLog.T.API, "Error Categories XML-RPC response: " + error);
+ notifyUpdatedOnUiThread(error);
+ }
+ };
+
+ /**
+ * Handles response to fetching self-hosted site options via XML-RPC.
+ */
+ private final XMLRPCCallback mOptionsCallback = new XMLRPCCallback() {
+ @Override
+ public void onSuccess(long id, final Object result) {
+ if (result instanceof Map) {
+ AppLog.d(AppLog.T.API, "Received Options XML-RPC response.");
+
+ if (!versionSupported((Map) result) && mActivity != null) {
+ notifyUpdatedOnUiThread(new XMLRPCException(mActivity.getString(R.string.site_settings_unsupported_version_error)));
+ return;
+ }
+
+ credentialsVerified(true);
+
+ deserializeOptionsResponse(mRemoteSettings, (Map) result);
+
+ // postFormats setting is not returned by this api call so copy it over
+ final Map<String, String> currentPostFormats = mSettings.postFormats;
+
+ mSettings.copyFrom(mRemoteSettings);
+
+ mSettings.postFormats = currentPostFormats;
+
+ SiteSettingsTable.saveSettings(mSettings);
+ notifyUpdatedOnUiThread(null);
+ } else {
+ // Response is considered an error if we are unable to parse it
+ AppLog.w(AppLog.T.API, "Error parsing Options XML-RPC response: " + result);
+ notifyUpdatedOnUiThread(new XMLRPCException("Unknown response object"));
+ }
+ }
+
+ @Override
+ public void onFailure(long id, final Exception error) {
+ AppLog.w(AppLog.T.API, "Error Options XML-RPC response: " + error);
+ notifyUpdatedOnUiThread(error);
+ }
+ };
+
+ private boolean versionSupported(Map map) {
+ String version = getNestedMapValue(map, SOFTWARE_VERSION_KEY);
+ if (TextUtils.isEmpty(version)) return false;
+ String[] split = version.split("\\.");
+ return split.length > 0 &&
+ Integer.valueOf(split[0]) >= REQUIRED_MAJOR_VERSION &&
+ Integer.valueOf(split[1]) >= REQUIRED_MINOR_VERSION;
+ }
+
+ private Map<String, String> serializeSelfHostedParams() {
+ Map<String, String> params = new HashMap<>();
+
+ if (mSettings.title != null && !mSettings.title.equals(mRemoteSettings.title)) {
+ params.put(BLOG_TITLE_KEY, mSettings.title);
+ }
+ if (mSettings.tagline != null && !mSettings.tagline.equals(mRemoteSettings.tagline)) {
+ params.put(BLOG_TAGLINE_KEY, mSettings.tagline);
+ }
+ if (mSettings.privacy != mRemoteSettings.privacy) {
+ params.put(PRIVACY_KEY, String.valueOf(mSettings.privacy));
+ }
+ if (mSettings.defaultCategory != mRemoteSettings.defaultCategory) {
+ params.put(DEF_CATEGORY_KEY, String.valueOf(mSettings.defaultCategory));
+ }
+ if (mSettings.defaultPostFormat != null && !mSettings.defaultPostFormat.equals(mRemoteSettings.defaultPostFormat)) {
+ params.put(DEF_POST_FORMAT_KEY, mSettings.defaultPostFormat);
+ }
+ if (mSettings.allowComments != mRemoteSettings.allowComments) {
+ params.put(ALLOW_COMMENTS_KEY, String.valueOf(mSettings.allowComments));
+ }
+ if (mSettings.sendPingbacks != mRemoteSettings.sendPingbacks) {
+ params.put(SEND_PINGBACKS_KEY, mSettings.sendPingbacks ? "1" : "0");
+ }
+ if (mSettings.receivePingbacks != mRemoteSettings.receivePingbacks) {
+ params.put(RECEIVE_PINGBACKS_KEY, mSettings.receivePingbacks ? OPTION_ALLOWED : OPTION_DISALLOWED);
+ }
+ if (mSettings.commentApprovalRequired != mRemoteSettings.commentApprovalRequired) {
+ params.put(COMMENT_MODERATION_KEY, String.valueOf(mSettings.commentApprovalRequired));
+ }
+ if (mSettings.closeCommentAfter != mRemoteSettings.closeCommentAfter) {
+ if (mSettings.closeCommentAfter <= 0) {
+ params.put(CLOSE_OLD_COMMENTS_KEY, String.valueOf(0));
+ } else {
+ params.put(CLOSE_OLD_COMMENTS_KEY, String.valueOf(1));
+ params.put(CLOSE_OLD_COMMENTS_DAYS_KEY, String.valueOf(mSettings.closeCommentAfter));
+ }
+ }
+ if (mSettings.sortCommentsBy != mRemoteSettings.sortCommentsBy) {
+ if (mSettings.sortCommentsBy == ASCENDING_SORT) {
+ params.put(COMMENT_SORT_ORDER_KEY, "asc");
+ } else if (mSettings.sortCommentsBy == DESCENDING_SORT) {
+ params.put(COMMENT_SORT_ORDER_KEY, "desc");
+ }
+ }
+ if (mSettings.threadingLevels != mRemoteSettings.threadingLevels) {
+ if (mSettings.threadingLevels <= 1) {
+ params.put(THREAD_COMMENTS_KEY, String.valueOf(0));
+ } else {
+ params.put(PAGE_COMMENTS_KEY, String.valueOf(1));
+ params.put(THREAD_COMMENTS_DEPTH_KEY, String.valueOf(mSettings.threadingLevels));
+ }
+ }
+ if (mSettings.commentsPerPage != mRemoteSettings.commentsPerPage) {
+ if (mSettings.commentsPerPage <= 0) {
+ params.put(PAGE_COMMENTS_KEY, String.valueOf(0));
+ } else{
+ params.put(PAGE_COMMENTS_KEY, String.valueOf(1));
+ params.put(PAGE_COMMENT_COUNT_KEY, String.valueOf(mSettings.commentsPerPage));
+ }
+ }
+ if (mSettings.commentsRequireIdentity != mRemoteSettings.commentsRequireIdentity) {
+ params.put(REQUIRE_IDENTITY_KEY, String.valueOf(mSettings.commentsRequireIdentity ? 1 : 0));
+ }
+ if (mSettings.commentsRequireUserAccount != mRemoteSettings.commentsRequireUserAccount) {
+ params.put(REQUIRE_USER_ACCOUNT_KEY, String.valueOf(mSettings.commentsRequireUserAccount ? 1 : 0));
+ }
+ if (mSettings.commentAutoApprovalKnownUsers != mRemoteSettings.commentAutoApprovalKnownUsers) {
+ params.put(WHITELIST_KNOWN_USERS_KEY, String.valueOf(mSettings.commentAutoApprovalKnownUsers));
+ }
+ if (mSettings.maxLinks != mRemoteSettings.maxLinks) {
+ params.put(MAX_LINKS_KEY, String.valueOf(mSettings.maxLinks));
+ }
+ if (mSettings.holdForModeration != null && !mSettings.holdForModeration.equals(mRemoteSettings.holdForModeration)) {
+ StringBuilder builder = new StringBuilder();
+ for (String key : mSettings.holdForModeration) {
+ builder.append(key);
+ builder.append("\n");
+ }
+ if (builder.length() > 1) {
+ params.put(MODERATION_KEYS_KEY, builder.substring(0, builder.length() - 1));
+ } else {
+ params.put(MODERATION_KEYS_KEY, "");
+ }
+ }
+ if (mSettings.blacklist != null && !mSettings.blacklist.equals(mRemoteSettings.blacklist)) {
+ StringBuilder builder = new StringBuilder();
+ for (String key : mSettings.blacklist) {
+ builder.append(key);
+ builder.append("\n");
+ }
+ if (builder.length() > 1) {
+ params.put(BLACKLIST_KEYS_KEY, builder.substring(0, builder.length() - 1));
+ } else {
+ params.put(BLACKLIST_KEYS_KEY, "");
+ }
+ }
+
+ return params;
+ }
+
+ /**
+ * Sets values from a self-hosted XML-RPC response object.
+ */
+ private void deserializeOptionsResponse(SiteSettingsModel model, Map response) {
+ if (mBlog == null || response == null) return;
+
+ model.username = mBlog.getUsername();
+ model.password = mBlog.getPassword();
+ model.address = getNestedMapValue(response, BLOG_URL_KEY);
+ model.title = getNestedMapValue(response, BLOG_TITLE_KEY);
+ model.tagline = getNestedMapValue(response, BLOG_TAGLINE_KEY);
+ model.privacy = Integer.valueOf(getNestedMapValue(response, PRIVACY_KEY));
+ model.defaultCategory = Integer.valueOf(getNestedMapValue(response, DEF_CATEGORY_KEY));
+ model.defaultPostFormat = getNestedMapValue(response, DEF_POST_FORMAT_KEY);
+ model.allowComments = OPTION_ALLOWED.equals(getNestedMapValue(response, ALLOW_COMMENTS_KEY));
+ model.receivePingbacks = OPTION_ALLOWED.equals(getNestedMapValue(response, RECEIVE_PINGBACKS_KEY));
+ String sendPingbacks = getNestedMapValue(response, SEND_PINGBACKS_KEY);
+ String approvalRequired = getNestedMapValue(response, COMMENT_MODERATION_KEY);
+ String identityRequired = getNestedMapValue(response, REQUIRE_IDENTITY_KEY);
+ String accountRequired = getNestedMapValue(response, REQUIRE_USER_ACCOUNT_KEY);
+ String knownUsers = getNestedMapValue(response, WHITELIST_KNOWN_USERS_KEY);
+ model.sendPingbacks = !TextUtils.isEmpty(sendPingbacks) && Integer.valueOf(sendPingbacks) > 0;
+ model.commentApprovalRequired = !TextUtils.isEmpty(approvalRequired) && Boolean.valueOf(approvalRequired);
+ model.commentsRequireIdentity = !TextUtils.isEmpty(identityRequired) && Integer.valueOf(identityRequired) > 0;
+ model.commentsRequireUserAccount = !TextUtils.isEmpty(accountRequired) && Integer.valueOf(identityRequired) > 0;
+ model.commentAutoApprovalKnownUsers = !TextUtils.isEmpty(knownUsers) && Boolean.valueOf(knownUsers);
+ model.maxLinks = Integer.valueOf(getNestedMapValue(response, MAX_LINKS_KEY));
+ mRemoteSettings.holdForModeration = new ArrayList<>();
+ mRemoteSettings.blacklist = new ArrayList<>();
+
+ String modKeys = getNestedMapValue(response, MODERATION_KEYS_KEY);
+ if (modKeys.length() > 0) {
+ Collections.addAll(mRemoteSettings.holdForModeration, modKeys.split("\n"));
+ }
+ String blacklistKeys = getNestedMapValue(response, BLACKLIST_KEYS_KEY);
+ if (blacklistKeys.length() > 0) {
+ Collections.addAll(mRemoteSettings.blacklist, blacklistKeys.split("\n"));
+ }
+
+ String close = getNestedMapValue(response, CLOSE_OLD_COMMENTS_KEY);
+ if (!TextUtils.isEmpty(close) && Boolean.valueOf(close)) {
+ mRemoteSettings.closeCommentAfter = Integer.valueOf(getNestedMapValue(response, CLOSE_OLD_COMMENTS_DAYS_KEY));
+ } else {
+ mRemoteSettings.closeCommentAfter = 0;
+ }
+
+ String thread = getNestedMapValue(response, THREAD_COMMENTS_KEY);
+ if (!TextUtils.isEmpty(thread) && Integer.valueOf(thread) > 0) {
+ mRemoteSettings.threadingLevels = Integer.valueOf(getNestedMapValue(response, THREAD_COMMENTS_DEPTH_KEY));
+ } else {
+ mRemoteSettings.threadingLevels = 0;
+ }
+
+ String page = getNestedMapValue(response, PAGE_COMMENTS_KEY);
+ if (!TextUtils.isEmpty(page) && Boolean.valueOf(page)) {
+ mRemoteSettings.commentsPerPage = Integer.valueOf(getNestedMapValue(response, PAGE_COMMENT_COUNT_KEY));
+ } else {
+ mRemoteSettings.commentsPerPage = 0;
+ }
+
+ if (getNestedMapValue(response, COMMENT_SORT_ORDER_KEY).equals("asc")) {
+ mRemoteSettings.sortCommentsBy = ASCENDING_SORT;
+ } else {
+ mRemoteSettings.sortCommentsBy = DESCENDING_SORT;
+ }
+ }
+
+ private void deserializeCategoriesResponse(SiteSettingsModel model, Object[] response) {
+ model.categories = new CategoryModel[response.length];
+
+ for (int i = 0; i < response.length; ++i) {
+ if (response[i] instanceof Map) {
+ Map category = (Map) response[i];
+ CategoryModel categoryModel = new CategoryModel();
+ categoryModel.id = MapUtils.getMapInt(category, BLOG_CATEGORY_ID_KEY);
+ categoryModel.parentId = MapUtils.getMapInt(category, BLOG_CATEGORY_PARENT_ID_KEY);
+ categoryModel.description = MapUtils.getMapStr(category, BLOG_CATEGORY_DESCRIPTION_KEY);
+ categoryModel.name = MapUtils.getMapStr(category, BLOG_CATEGORY_NAME_KEY);
+ model.categories[i] = categoryModel;
+ }
+ }
+ }
+
+ /**
+ * Helper method to get a value from a nested Map. Used to parse self-hosted response objects.
+ */
+ private String getNestedMapValue(Map map, String key) {
+ if (map != null && key != null) {
+ return MapUtils.getMapStr((Map) map.get(key), "value");
+ }
+
+ return "";
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java
new file mode 100644
index 000000000..13fc10c5b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java
@@ -0,0 +1,1392 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.support.annotation.NonNull;
+import android.support.design.widget.Snackbar;
+import android.support.v7.widget.LinearLayoutManager;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.util.SparseBooleanArray;
+import android.view.ActionMode;
+import android.view.ContextThemeWrapper;
+import android.view.HapticFeedbackConstants;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager.LayoutParams;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.NumberPicker.Formatter;
+import android.widget.TextView;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.ui.WPWebViewActivity;
+import org.wordpress.android.ui.stats.StatsWidgetProvider;
+import org.wordpress.android.ui.stats.datasets.StatsTable;
+import org.wordpress.android.util.AnalyticsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.CoreEvents;
+import org.wordpress.android.util.HelpshiftHelper;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.WPActivityUtils;
+import org.wordpress.android.util.WPPrefUtils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * Allows interfacing with WordPress site settings. Works with WP.com and WP.org v4.5+ (pending).
+ *
+ * Settings are synced automatically when local changes are made.
+ */
+
+public class SiteSettingsFragment extends PreferenceFragment
+ implements Preference.OnPreferenceChangeListener,
+ Preference.OnPreferenceClickListener,
+ AdapterView.OnItemLongClickListener,
+ ViewGroup.OnHierarchyChangeListener,
+ Dialog.OnDismissListener,
+ SiteSettingsInterface.SiteSettingsListener {
+
+ /**
+ * Use this argument to pass the {@link Integer} local blog ID to this fragment.
+ */
+ public static final String ARG_LOCAL_BLOG_ID = "local_blog_id";
+
+ /**
+ * When the user removes a site (by selecting Delete Site) the parent {@link Activity} result
+ * is set to this value and {@link Activity#finish()} is invoked.
+ */
+ public static final int RESULT_BLOG_REMOVED = Activity.RESULT_FIRST_USER;
+
+ /**
+ * Provides the regex to identify domain HTTP(S) protocol and/or 'www' sub-domain.
+ *
+ * Used to format user-facing {@link String}'s in certain preferences.
+ */
+ public static final String ADDRESS_FORMAT_REGEX = "^(https?://(w{3})?|www\\.)";
+
+ /**
+ * url that points to wordpress.com purchases
+ */
+ public static final String WORDPRESS_PURCHASES_URL = "https://wordpress.com/purchases";
+
+ /**
+ * Used to move the Uncategorized category to the beginning of the category list.
+ */
+ private static final int UNCATEGORIZED_CATEGORY_ID = 1;
+
+ /**
+ * Request code used when creating the {@link RelatedPostsDialog}.
+ */
+ private static final int RELATED_POSTS_REQUEST_CODE = 1;
+ private static final int THREADING_REQUEST_CODE = 2;
+ private static final int PAGING_REQUEST_CODE = 3;
+ private static final int CLOSE_AFTER_REQUEST_CODE = 4;
+ private static final int MULTIPLE_LINKS_REQUEST_CODE = 5;
+ private static final int DELETE_SITE_REQUEST_CODE = 6;
+ private static final String DELETE_SITE_TAG = "delete-site";
+ private static final String PURCHASE_ORIGINAL_RESPONSE_KEY = "originalResponse";
+ private static final String PURCHASE_ACTIVE_KEY = "active";
+ private static final String ANALYTICS_ERROR_PROPERTY_KEY = "error";
+
+ private static final long FETCH_DELAY = 1000;
+
+ // Reference to blog obtained from passed ID (ARG_LOCAL_BLOG_ID)
+ private Blog mBlog;
+
+ // Can interface with WP.com or WP.org
+ private SiteSettingsInterface mSiteSettings;
+
+ // Reference to the list of items being edited in the current list editor
+ private List<String> mEditingList;
+
+ // Used to ensure that settings are only fetched once throughout the lifecycle of the fragment
+ private boolean mShouldFetch;
+
+ // General settings
+ private EditTextPreference mTitlePref;
+ private EditTextPreference mTaglinePref;
+ private EditTextPreference mAddressPref;
+ private DetailListPreference mPrivacyPref;
+ private DetailListPreference mLanguagePref;
+
+ // Account settings (NOTE: only for WP.org)
+ private EditTextPreference mUsernamePref;
+ private EditTextPreference mPasswordPref;
+
+ // Writing settings
+ private WPSwitchPreference mLocationPref;
+ private DetailListPreference mCategoryPref;
+ private DetailListPreference mFormatPref;
+ private Preference mRelatedPostsPref;
+
+ // Discussion settings preview
+ private WPSwitchPreference mAllowCommentsPref;
+ private WPSwitchPreference mSendPingbacksPref;
+ private WPSwitchPreference mReceivePingbacksPref;
+
+ // Discussion settings -> Defaults for New Posts
+ private WPSwitchPreference mAllowCommentsNested;
+ private WPSwitchPreference mSendPingbacksNested;
+ private WPSwitchPreference mReceivePingbacksNested;
+ private PreferenceScreen mMorePreference;
+
+ // Discussion settings -> Comments
+ private WPSwitchPreference mIdentityRequiredPreference;
+ private WPSwitchPreference mUserAccountRequiredPref;
+ private Preference mCloseAfterPref;
+ private DetailListPreference mSortByPref;
+ private Preference mThreadingPref;
+ private Preference mPagingPref;
+ private DetailListPreference mWhitelistPref;
+ private Preference mMultipleLinksPref;
+ private Preference mModerationHoldPref;
+ private Preference mBlacklistPref;
+
+ // This Device settings
+ private DetailListPreference mImageWidthPref;
+ private WPSwitchPreference mUploadAndLinkPref;
+
+ // Advanced settings
+ private Preference mStartOverPref;
+ private Preference mExportSitePref;
+ private Preference mDeleteSitePref;
+
+ private boolean mEditingEnabled = true;
+
+ // Reference to the state of the fragment
+ private boolean mIsFragmentPaused = false;
+
+ // Hold for Moderation and Blacklist settings
+ private Dialog mDialog;
+ private ActionMode mActionMode;
+ private MultiSelectRecyclerViewAdapter mAdapter;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Activity activity = getActivity();
+
+ // make sure we have local site data and a network connection, otherwise finish activity
+ mBlog = WordPress.getBlog(getArguments().getInt(ARG_LOCAL_BLOG_ID, -1));
+ if (mBlog == null || !NetworkUtils.checkConnection(activity)) {
+ getActivity().finish();
+ return;
+ }
+
+ // track successful settings screen access
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_ACCESSED);
+
+ // setup state to fetch remote settings
+ mShouldFetch = true;
+
+ // initialize the appropriate settings interface (WP.com or WP.org)
+ mSiteSettings = SiteSettingsInterface.getInterface(activity, mBlog, this);
+
+ setRetainInstance(true);
+ addPreferencesFromResource(R.xml.site_settings);
+
+ // toggle which preferences are shown and set references
+ initPreferences();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ WordPress.wpDB.saveBlog(mBlog);
+ mIsFragmentPaused = true;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ // Fragment#onResume() is called after FragmentActivity#onPostResume().
+ // The latter is the most secure way of keeping track of the activity's state, and avoid calls to commitAllowingStateLoss.
+ mIsFragmentPaused = false;
+
+ // always load cached settings
+ mSiteSettings.init(false);
+
+ if (mShouldFetch) {
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ // initialize settings with locally cached values, fetch remote on first pass
+ mSiteSettings.init(true);
+ }
+ }, FETCH_DELAY);
+ // stop future calls from fetching remote settings
+ mShouldFetch = false;
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ removeMoreScreenToolbar();
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (data != null) {
+ switch (requestCode) {
+ case RELATED_POSTS_REQUEST_CODE:
+ // data is null if user cancelled editing Related Posts settings
+ mSiteSettings.setShowRelatedPosts(data.getBooleanExtra(
+ RelatedPostsDialog.SHOW_RELATED_POSTS_KEY, false));
+ mSiteSettings.setShowRelatedPostHeader(data.getBooleanExtra(
+ RelatedPostsDialog.SHOW_HEADER_KEY, false));
+ mSiteSettings.setShowRelatedPostImages(data.getBooleanExtra(
+ RelatedPostsDialog.SHOW_IMAGES_KEY, false));
+ onPreferenceChange(mRelatedPostsPref, mSiteSettings.getRelatedPostsDescription());
+ break;
+ case THREADING_REQUEST_CODE:
+ int levels = data.getIntExtra(NumberPickerDialog.CUR_VALUE_KEY, -1);
+ mSiteSettings.setShouldThreadComments(levels > 1 && data.getBooleanExtra
+ (NumberPickerDialog.SWITCH_ENABLED_KEY, false));
+ onPreferenceChange(mThreadingPref, levels);
+ break;
+ case PAGING_REQUEST_CODE:
+ mSiteSettings.setShouldPageComments(data.getBooleanExtra
+ (NumberPickerDialog.SWITCH_ENABLED_KEY, false));
+ onPreferenceChange(mPagingPref, data.getIntExtra(
+ NumberPickerDialog.CUR_VALUE_KEY, -1));
+ break;
+ case CLOSE_AFTER_REQUEST_CODE:
+ mSiteSettings.setShouldCloseAfter(data.getBooleanExtra
+ (NumberPickerDialog.SWITCH_ENABLED_KEY, false));
+ onPreferenceChange(mCloseAfterPref, data.getIntExtra(
+ NumberPickerDialog.CUR_VALUE_KEY, -1));
+ break;
+ case MULTIPLE_LINKS_REQUEST_CODE:
+ int numLinks = data.getIntExtra(NumberPickerDialog.CUR_VALUE_KEY, -1);
+ if (numLinks < 0 || numLinks == mSiteSettings.getMultipleLinks()) return;
+ onPreferenceChange(mMultipleLinksPref, numLinks);
+ break;
+ }
+ } else {
+ switch (requestCode) {
+ case DELETE_SITE_REQUEST_CODE:
+ deleteSite();
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ ViewGroup container,
+ Bundle savedInstanceState) {
+ // use a wrapper to apply the Calypso theme
+ Context themer = new ContextThemeWrapper(getActivity(), R.style.Calypso_SiteSettingsTheme);
+ LayoutInflater localInflater = inflater.cloneInContext(themer);
+ View view = super.onCreateView(localInflater, container, savedInstanceState);
+
+ if (view != null) {
+ setupPreferenceList((ListView) view.findViewById(android.R.id.list), getResources());
+ }
+
+ return view;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ removeMoreScreenToolbar();
+ super.onSaveInstanceState(outState);
+ setupMorePreferenceScreen();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ if (savedInstanceState != null) setupMorePreferenceScreen();
+ }
+
+ @Override
+ public void onChildViewAdded(View parent, View child) {
+ if (child.getId() == android.R.id.title && child instanceof TextView) {
+ // style preference category title views
+ TextView title = (TextView) child;
+ WPPrefUtils.layoutAsBody2(title);
+ } else {
+ // style preference title views
+ TextView title = (TextView) child.findViewById(android.R.id.title);
+ if (title != null) WPPrefUtils.layoutAsSubhead(title);
+ }
+ }
+
+ @Override
+ public void onChildViewRemoved(View parent, View child) {
+ // NOP
+ }
+
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) {
+ super.onPreferenceTreeClick(screen, preference);
+
+ // More preference selected, style the Discussion screen
+ if (preference == mMorePreference) {
+ // track user accessing the full Discussion settings screen
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_ACCESSED_MORE_SETTINGS);
+
+ return setupMorePreferenceScreen();
+ } else if (preference == findPreference(getString(R.string.pref_key_site_start_over_screen))) {
+ Dialog dialog = ((PreferenceScreen) preference).getDialog();
+ if (dialog == null) return false;
+
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_START_OVER_ACCESSED);
+
+ setupPreferenceList((ListView) dialog.findViewById(android.R.id.list), getResources());
+ String title = getString(R.string.start_over);
+ WPActivityUtils.addToolbarToDialog(this, dialog, title);
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (preference == mRelatedPostsPref) {
+ showRelatedPostsDialog();
+ } else if (preference == mMultipleLinksPref) {
+ showMultipleLinksDialog();
+ } else if (preference == mModerationHoldPref) {
+ mEditingList = mSiteSettings.getModerationKeys();
+ showListEditorDialog(R.string.site_settings_moderation_hold_title,
+ R.string.site_settings_hold_for_moderation_description);
+ } else if (preference == mBlacklistPref) {
+ mEditingList = mSiteSettings.getBlacklistKeys();
+ showListEditorDialog(R.string.site_settings_blacklist_title,
+ R.string.site_settings_blacklist_description);
+ } else if (preference == mStartOverPref) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_START_OVER_CONTACT_SUPPORT_CLICKED);
+ HelpshiftHelper.getInstance().showConversation(getActivity(), HelpshiftHelper.Tag.ORIGIN_START_OVER);
+ } else if (preference == mCloseAfterPref) {
+ showCloseAfterDialog();
+ } else if (preference == mPagingPref) {
+ showPagingDialog();
+ } else if (preference == mThreadingPref) {
+ showThreadingDialog();
+ } else if (preference == mCategoryPref || preference == mFormatPref) {
+ return !shouldShowListPreference((DetailListPreference) preference);
+ } else if (preference == mExportSitePref) {
+ showExportContentDialog();
+ } else if (preference == mDeleteSitePref) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_ACCESSED);
+ requestPurchasesForDeletionCheck();
+ } else {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (newValue == null || !mEditingEnabled) return false;
+
+ if (preference == mTitlePref) {
+ mSiteSettings.setTitle(newValue.toString());
+ changeEditTextPreferenceValue(mTitlePref, mSiteSettings.getTitle());
+ } else if (preference == mTaglinePref) {
+ mSiteSettings.setTagline(newValue.toString());
+ changeEditTextPreferenceValue(mTaglinePref, mSiteSettings.getTagline());
+ } else if (preference == mAddressPref) {
+ mSiteSettings.setAddress(newValue.toString());
+ changeEditTextPreferenceValue(mAddressPref, mSiteSettings.getAddress());
+ } else if (preference == mLanguagePref) {
+ if (!mSiteSettings.setLanguageCode(newValue.toString())) {
+ AppLog.w(AppLog.T.SETTINGS, "Unknown language code " + newValue.toString() + " selected in Site Settings.");
+ ToastUtils.showToast(getActivity(), R.string.site_settings_unknown_language_code_error);
+ }
+ changeLanguageValue(mSiteSettings.getLanguageCode());
+ } else if (preference == mPrivacyPref) {
+ mSiteSettings.setPrivacy(Integer.parseInt(newValue.toString()));
+ setDetailListPreferenceValue(mPrivacyPref,
+ String.valueOf(mSiteSettings.getPrivacy()),
+ mSiteSettings.getPrivacyDescription());
+ } else if (preference == mAllowCommentsPref || preference == mAllowCommentsNested) {
+ setAllowComments((Boolean) newValue);
+ } else if (preference == mSendPingbacksPref || preference == mSendPingbacksNested) {
+ setSendPingbacks((Boolean) newValue);
+ } else if (preference == mReceivePingbacksPref || preference == mReceivePingbacksNested) {
+ setReceivePingbacks((Boolean) newValue);
+ } else if (preference == mCloseAfterPref) {
+ mSiteSettings.setCloseAfter(Integer.parseInt(newValue.toString()));
+ mCloseAfterPref.setSummary(mSiteSettings.getCloseAfterDescription());
+ } else if (preference == mSortByPref) {
+ mSiteSettings.setCommentSorting(Integer.parseInt(newValue.toString()));
+ setDetailListPreferenceValue(mSortByPref,
+ newValue.toString(),
+ mSiteSettings.getSortingDescription());
+ } else if (preference == mThreadingPref) {
+ mSiteSettings.setThreadingLevels(Integer.parseInt(newValue.toString()));
+ mThreadingPref.setSummary(mSiteSettings.getThreadingDescription());
+ } else if (preference == mPagingPref) {
+ mSiteSettings.setPagingCount(Integer.parseInt(newValue.toString()));
+ mPagingPref.setSummary(mSiteSettings.getPagingDescription());
+ } else if (preference == mIdentityRequiredPreference) {
+ mSiteSettings.setIdentityRequired((Boolean) newValue);
+ } else if (preference == mUserAccountRequiredPref) {
+ mSiteSettings.setUserAccountRequired((Boolean) newValue);
+ } else if (preference == mWhitelistPref) {
+ updateWhitelistSettings(Integer.parseInt(newValue.toString()));
+ } else if (preference == mMultipleLinksPref) {
+ mSiteSettings.setMultipleLinks(Integer.parseInt(newValue.toString()));
+ String s = StringUtils.getQuantityString(getActivity(), R.string.site_settings_multiple_links_summary_zero,
+ R.string.site_settings_multiple_links_summary_one,
+ R.string.site_settings_multiple_links_summary_other, mSiteSettings.getMultipleLinks());
+ mMultipleLinksPref.setSummary(s);
+ } else if (preference == mUsernamePref) {
+ mSiteSettings.setUsername(newValue.toString());
+ changeEditTextPreferenceValue(mUsernamePref, mSiteSettings.getUsername());
+ } else if (preference == mPasswordPref) {
+ mSiteSettings.setPassword(newValue.toString());
+ changeEditTextPreferenceValue(mPasswordPref, mSiteSettings.getPassword());
+ } else if (preference == mLocationPref) {
+ mSiteSettings.setLocation((Boolean) newValue);
+ } else if (preference == mCategoryPref) {
+ mSiteSettings.setDefaultCategory(Integer.parseInt(newValue.toString()));
+ setDetailListPreferenceValue(mCategoryPref,
+ newValue.toString(),
+ mSiteSettings.getDefaultCategoryForDisplay());
+ } else if (preference == mFormatPref) {
+ mSiteSettings.setDefaultFormat(newValue.toString());
+ setDetailListPreferenceValue(mFormatPref,
+ newValue.toString(),
+ mSiteSettings.getDefaultPostFormatDisplay());
+ } else if (preference == mImageWidthPref) {
+ mBlog.setMaxImageWidth(newValue.toString());
+ setDetailListPreferenceValue(mImageWidthPref,
+ mBlog.getMaxImageWidth(),
+ mBlog.getMaxImageWidth());
+ } else if (preference == mUploadAndLinkPref) {
+ mBlog.setFullSizeImage(Boolean.valueOf(newValue.toString()));
+ } else if (preference == mRelatedPostsPref) {
+ mRelatedPostsPref.setSummary(newValue.toString());
+ } else if (preference == mModerationHoldPref) {
+ mModerationHoldPref.setSummary(mSiteSettings.getModerationHoldDescription());
+ } else if (preference == mBlacklistPref) {
+ mBlacklistPref.setSummary(mSiteSettings.getBlacklistDescription());
+ } else {
+ return false;
+ }
+
+ mSiteSettings.saveSettings();
+
+ return true;
+ }
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ ListView listView = (ListView) parent;
+ ListAdapter listAdapter = listView.getAdapter();
+ Object obj = listAdapter.getItem(position);
+
+ if (obj != null) {
+ if (obj instanceof View.OnLongClickListener) {
+ View.OnLongClickListener longListener = (View.OnLongClickListener) obj;
+ return longListener.onLongClick(view);
+ } else if (obj instanceof PreferenceHint) {
+ PreferenceHint hintObj = (PreferenceHint) obj;
+ if (hintObj.hasHint()) {
+ HashMap<String, Object> properties = new HashMap<>();
+ properties.put("hint_shown", hintObj.getHint());
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_HINT_TOAST_SHOWN, properties);
+ ToastUtils.showToast(getActivity(), hintObj.getHint(), ToastUtils.Duration.SHORT);
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ if (mEditingList == mSiteSettings.getModerationKeys()) {
+ onPreferenceChange(mModerationHoldPref, mEditingList.size());
+ } else if (mEditingList == mSiteSettings.getBlacklistKeys()) {
+ onPreferenceChange(mBlacklistPref, mEditingList.size());
+ }
+ mEditingList = null;
+ }
+
+ @Override
+ public void onSettingsUpdated(Exception error) {
+ if (error != null) {
+ ToastUtils.showToast(getActivity(), R.string.error_fetch_remote_site_settings);
+ getActivity().finish();
+ return;
+ }
+
+ if (isAdded()) setPreferencesFromSiteSettings();
+ }
+
+ @Override
+ public void onSettingsSaved(Exception error) {
+ if (error != null) {
+ ToastUtils.showToast(WordPress.getContext(), R.string.error_post_remote_site_settings);
+ return;
+ }
+ mBlog.setBlogName(mSiteSettings.getTitle());
+ WordPress.wpDB.saveBlog(mBlog);
+
+ // update the global current Blog so WordPress.getCurrentBlog() callers will get the updated object
+ WordPress.setCurrentBlog(mBlog.getLocalTableBlogId());
+
+ EventBus.getDefault().post(new CoreEvents.BlogListChanged());
+ }
+
+ @Override
+ public void onCredentialsValidated(Exception error) {
+ if (error != null) {
+ ToastUtils.showToast(WordPress.getContext(), R.string.username_or_password_incorrect);
+ }
+ }
+
+ private void setupPreferenceList(ListView prefList, Resources res) {
+ if (prefList == null || res == null) return;
+
+ // customize list dividers
+ //noinspection deprecation
+ prefList.setDivider(res.getDrawable(R.drawable.preferences_divider));
+ prefList.setDividerHeight(res.getDimensionPixelSize(R.dimen.site_settings_divider_height));
+ // handle long clicks on preferences to display hints
+ prefList.setOnItemLongClickListener(this);
+ // required to customize (Calypso) preference views
+ prefList.setOnHierarchyChangeListener(this);
+ // remove footer divider bar
+ prefList.setFooterDividersEnabled(false);
+ //noinspection deprecation
+ prefList.setOverscrollFooter(res.getDrawable(R.color.transparent));
+ }
+
+ /**
+ * Helper method to retrieve {@link Preference} references and initialize any data.
+ */
+ private void initPreferences() {
+ mTitlePref = (EditTextPreference) getChangePref(R.string.pref_key_site_title);
+ mTaglinePref = (EditTextPreference) getChangePref(R.string.pref_key_site_tagline);
+ mAddressPref = (EditTextPreference) getChangePref(R.string.pref_key_site_address);
+ mPrivacyPref = (DetailListPreference) getChangePref(R.string.pref_key_site_visibility);
+ mLanguagePref = (DetailListPreference) getChangePref(R.string.pref_key_site_language);
+ mUsernamePref = (EditTextPreference) getChangePref(R.string.pref_key_site_username);
+ mPasswordPref = (EditTextPreference) getChangePref(R.string.pref_key_site_password);
+ mLocationPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_location);
+ mCategoryPref = (DetailListPreference) getChangePref(R.string.pref_key_site_category);
+ mFormatPref = (DetailListPreference) getChangePref(R.string.pref_key_site_format);
+ mAllowCommentsPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_allow_comments);
+ mAllowCommentsNested = (WPSwitchPreference) getChangePref(R.string.pref_key_site_allow_comments_nested);
+ mSendPingbacksPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_send_pingbacks);
+ mSendPingbacksNested = (WPSwitchPreference) getChangePref(R.string.pref_key_site_send_pingbacks_nested);
+ mReceivePingbacksPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_receive_pingbacks);
+ mReceivePingbacksNested = (WPSwitchPreference) getChangePref(R.string.pref_key_site_receive_pingbacks_nested);
+ mIdentityRequiredPreference = (WPSwitchPreference) getChangePref(R.string.pref_key_site_identity_required);
+ mUserAccountRequiredPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_user_account_required);
+ mSortByPref = (DetailListPreference) getChangePref(R.string.pref_key_site_sort_by);
+ mWhitelistPref = (DetailListPreference) getChangePref(R.string.pref_key_site_whitelist);
+ mMorePreference = (PreferenceScreen) getClickPref(R.string.pref_key_site_more_discussion);
+ mRelatedPostsPref = getClickPref(R.string.pref_key_site_related_posts);
+ mCloseAfterPref = getClickPref(R.string.pref_key_site_close_after);
+ mPagingPref = getClickPref(R.string.pref_key_site_paging);
+ mThreadingPref = getClickPref(R.string.pref_key_site_threading);
+ mMultipleLinksPref = getClickPref(R.string.pref_key_site_multiple_links);
+ mModerationHoldPref = getClickPref(R.string.pref_key_site_moderation_hold);
+ mBlacklistPref = getClickPref(R.string.pref_key_site_blacklist);
+ mImageWidthPref = (DetailListPreference) getChangePref(R.string.pref_key_site_image_width);
+ mUploadAndLinkPref = (WPSwitchPreference) getChangePref(R.string.pref_key_site_upload_and_link_image);
+ mStartOverPref = getClickPref(R.string.pref_key_site_start_over);
+ mExportSitePref = getClickPref(R.string.pref_key_site_export_site);
+ mDeleteSitePref = getClickPref(R.string.pref_key_site_delete_site);
+
+ sortLanguages();
+
+ // .com sites hide the Account category, self-hosted sites hide the Related Posts preference
+ if (mBlog.isDotcomFlag()) {
+ removeSelfHostedOnlyPreferences();
+ } else {
+ removeDotComOnlyPreferences();
+ }
+
+ // hide all options except for Delete site and Enable Location if user is not admin
+ if (!mBlog.isAdmin()) hideAdminRequiredPreferences();
+ }
+
+ public void setEditingEnabled(boolean enabled) {
+ // excludes mAddressPref, mMorePreference
+ final Preference[] editablePreference = {
+ mTitlePref , mTaglinePref, mPrivacyPref, mLanguagePref, mUsernamePref,
+ mPasswordPref, mLocationPref, mCategoryPref, mFormatPref, mAllowCommentsPref,
+ mAllowCommentsNested, mSendPingbacksPref, mSendPingbacksNested, mReceivePingbacksPref,
+ mReceivePingbacksNested, mIdentityRequiredPreference, mUserAccountRequiredPref,
+ mSortByPref, mWhitelistPref, mRelatedPostsPref, mCloseAfterPref, mPagingPref,
+ mThreadingPref, mMultipleLinksPref, mModerationHoldPref, mBlacklistPref,
+ mImageWidthPref, mUploadAndLinkPref, mDeleteSitePref
+ };
+
+ for(Preference preference : editablePreference) {
+ if(preference!=null) preference.setEnabled(enabled);
+ }
+
+ mEditingEnabled = enabled;
+ }
+
+ private void showRelatedPostsDialog() {
+ DialogFragment relatedPosts = new RelatedPostsDialog();
+ Bundle args = new Bundle();
+ args.putBoolean(RelatedPostsDialog.SHOW_RELATED_POSTS_KEY, mSiteSettings.getShowRelatedPosts());
+ args.putBoolean(RelatedPostsDialog.SHOW_HEADER_KEY, mSiteSettings.getShowRelatedPostHeader());
+ args.putBoolean(RelatedPostsDialog.SHOW_IMAGES_KEY, mSiteSettings.getShowRelatedPostImages());
+ relatedPosts.setArguments(args);
+ relatedPosts.setTargetFragment(this, RELATED_POSTS_REQUEST_CODE);
+ relatedPosts.show(getFragmentManager(), "related-posts");
+ }
+
+ private void showNumberPickerDialog(Bundle args, int requestCode, String tag) {
+ showNumberPickerDialog(args, requestCode, tag, null);
+ }
+
+ private void showNumberPickerDialog(Bundle args, int requestCode, String tag, Formatter format) {
+ NumberPickerDialog dialog = new NumberPickerDialog();
+ dialog.setNumberFormat(format);
+ dialog.setArguments(args);
+ dialog.setTargetFragment(this, requestCode);
+ dialog.show(getFragmentManager(), tag);
+ }
+
+ private void showPagingDialog() {
+ Bundle args = new Bundle();
+ args.putBoolean(NumberPickerDialog.SHOW_SWITCH_KEY, true);
+ args.putBoolean(NumberPickerDialog.SWITCH_ENABLED_KEY, mSiteSettings.getShouldPageComments());
+ args.putString(NumberPickerDialog.SWITCH_TITLE_KEY, getString(R.string.site_settings_paging_title));
+ args.putString(NumberPickerDialog.SWITCH_DESC_KEY, getString(R.string.site_settings_paging_dialog_description));
+ args.putString(NumberPickerDialog.TITLE_KEY, getString(R.string.site_settings_paging_title));
+ args.putString(NumberPickerDialog.HEADER_TEXT_KEY, getString(R.string.site_settings_paging_dialog_header));
+ args.putInt(NumberPickerDialog.MIN_VALUE_KEY, 1);
+ args.putInt(NumberPickerDialog.MAX_VALUE_KEY, getResources().getInteger(R.integer.paging_limit));
+ args.putInt(NumberPickerDialog.CUR_VALUE_KEY, mSiteSettings.getPagingCount());
+ showNumberPickerDialog(args, PAGING_REQUEST_CODE, "paging-dialog");
+ }
+
+ private void showThreadingDialog() {
+ Bundle args = new Bundle();
+ args.putBoolean(NumberPickerDialog.SHOW_SWITCH_KEY, true);
+ args.putBoolean(NumberPickerDialog.SWITCH_ENABLED_KEY, mSiteSettings.getShouldThreadComments());
+ args.putString(NumberPickerDialog.SWITCH_TITLE_KEY, getString(R.string.site_settings_threading_title));
+ args.putString(NumberPickerDialog.SWITCH_DESC_KEY, getString(R.string.site_settings_threading_dialog_description));
+ args.putString(NumberPickerDialog.TITLE_KEY, getString(R.string.site_settings_threading_title));
+ args.putString(NumberPickerDialog.HEADER_TEXT_KEY, getString(R.string.site_settings_threading_dialog_header));
+ args.putInt(NumberPickerDialog.MIN_VALUE_KEY, 2);
+ args.putInt(NumberPickerDialog.MAX_VALUE_KEY, getResources().getInteger(R.integer.threading_limit));
+ args.putInt(NumberPickerDialog.CUR_VALUE_KEY, mSiteSettings.getThreadingLevels());
+ showNumberPickerDialog(args, THREADING_REQUEST_CODE, "threading-dialog", new Formatter() {
+ @Override
+ public String format(int value) {
+ return mSiteSettings.getThreadingDescriptionForLevel(value);
+ }
+ });
+ }
+
+ private void showExportContentDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.export_your_content);
+ String email = AccountHelper.getDefaultAccount().getEmail();
+ builder.setMessage(getString(R.string.export_your_content_message, email));
+ builder.setPositiveButton(R.string.site_settings_export_content_title, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_EXPORT_SITE_REQUESTED);
+ exportSite();
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, null);
+
+ builder.show();
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_EXPORT_SITE_ACCESSED);
+ }
+
+ private void dismissProgressDialog(ProgressDialog progressDialog) {
+ if (progressDialog != null && progressDialog.isShowing()) {
+ try {
+ progressDialog.dismiss();
+ } catch (IllegalArgumentException e) {
+ // dialog doesn't exist
+ }
+ }
+ }
+
+ private void requestPurchasesForDeletionCheck() {
+ final Blog currentBlog = WordPress.getCurrentBlog();
+ final ProgressDialog progressDialog = ProgressDialog.show(getActivity(), "", getString(R.string.checking_purchases), true, false);
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_PURCHASES_REQUESTED);
+ WordPress.getRestClientUtils().getSitePurchases(currentBlog.getDotComBlogId(), new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ dismissProgressDialog(progressDialog);
+ if (isAdded()) {
+ showPurchasesOrDeleteSiteDialog(response, currentBlog);
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ dismissProgressDialog(progressDialog);
+ if (isAdded()) {
+ ToastUtils.showToast(getActivity(), getString(R.string.purchases_request_error));
+ AppLog.e(AppLog.T.API, "Error occurred while requesting purchases for deletion check: " + error.toString());
+ }
+ }
+ });
+ }
+
+ private void showPurchasesOrDeleteSiteDialog(JSONObject response, final Blog currentBlog) {
+ try {
+ JSONArray purchases = response.getJSONArray(PURCHASE_ORIGINAL_RESPONSE_KEY);
+ if (hasActivePurchases(purchases)) {
+ showPurchasesDialog(currentBlog);
+ } else {
+ showDeleteSiteDialog();
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.API, "Error occurred while trying to delete site: " + e.toString());
+ }
+ }
+
+ private void showPurchasesDialog(final Blog currentBlog) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.premium_upgrades_title);
+ builder.setMessage(R.string.premium_upgrades_message);
+ builder.setPositiveButton(R.string.show_purchases, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOW_CLICKED);
+ WPWebViewActivity.openUrlByUsingWPCOMCredentials(getActivity(), WORDPRESS_PURCHASES_URL, AccountHelper.getCurrentUsernameForBlog(currentBlog));
+ }
+ });
+ builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+ builder.show();
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_PURCHASES_SHOWN);
+ }
+
+ private boolean hasActivePurchases(JSONArray purchases) throws JSONException {
+ for (int i = 0; i < purchases.length(); i++) {
+ JSONObject purchase = purchases.getJSONObject(i);
+ int active = purchase.getInt(PURCHASE_ACTIVE_KEY);
+
+ if (active == 1) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void showDeleteSiteDialog() {
+ if (mIsFragmentPaused) return; // Do not show the DeleteSiteDialogFragment if the fragment was paused.
+ // DialogFragment internally uses commit(), and not commitAllowingStateLoss, crashing the app in case like that.
+ Bundle args = new Bundle();
+ args.putString(DeleteSiteDialogFragment.SITE_DOMAIN_KEY, UrlUtils.getHost(mBlog.getHomeURL()));
+ DeleteSiteDialogFragment deleteSiteDialogFragment = new DeleteSiteDialogFragment();
+ deleteSiteDialogFragment.setArguments(args);
+ deleteSiteDialogFragment.setTargetFragment(this, DELETE_SITE_REQUEST_CODE);
+ deleteSiteDialogFragment.show(getFragmentManager(), DELETE_SITE_TAG);
+ }
+
+ private void showCloseAfterDialog() {
+ Bundle args = new Bundle();
+ args.putBoolean(NumberPickerDialog.SHOW_SWITCH_KEY, true);
+ args.putBoolean(NumberPickerDialog.SWITCH_ENABLED_KEY, mSiteSettings.getShouldCloseAfter());
+ args.putString(NumberPickerDialog.SWITCH_TITLE_KEY, getString(R.string.site_settings_close_after_dialog_switch_text));
+ args.putString(NumberPickerDialog.SWITCH_DESC_KEY, getString(R.string.site_settings_close_after_dialog_description));
+ args.putString(NumberPickerDialog.TITLE_KEY, getString(R.string.site_settings_close_after_dialog_title));
+ args.putString(NumberPickerDialog.HEADER_TEXT_KEY, getString(R.string.site_settings_close_after_dialog_header));
+ args.putInt(NumberPickerDialog.MIN_VALUE_KEY, 1);
+ args.putInt(NumberPickerDialog.MAX_VALUE_KEY, getResources().getInteger(R.integer.close_after_limit));
+ args.putInt(NumberPickerDialog.CUR_VALUE_KEY, mSiteSettings.getCloseAfter());
+ showNumberPickerDialog(args, CLOSE_AFTER_REQUEST_CODE, "close-after-dialog");
+ }
+
+ private void showMultipleLinksDialog() {
+ Bundle args = new Bundle();
+ args.putBoolean(NumberPickerDialog.SHOW_SWITCH_KEY, false);
+ args.putString(NumberPickerDialog.TITLE_KEY, getString(R.string.site_settings_multiple_links_title));
+ args.putInt(NumberPickerDialog.MIN_VALUE_KEY, 0);
+ args.putInt(NumberPickerDialog.MAX_VALUE_KEY, getResources().getInteger(R.integer.max_links_limit));
+ args.putInt(NumberPickerDialog.CUR_VALUE_KEY, mSiteSettings.getMultipleLinks());
+ showNumberPickerDialog(args, MULTIPLE_LINKS_REQUEST_CODE, "multiple-links-dialog");
+ }
+
+ private void setPreferencesFromSiteSettings() {
+ mLocationPref.setChecked(mSiteSettings.getLocation());
+ changeEditTextPreferenceValue(mTitlePref, mSiteSettings.getTitle());
+ changeEditTextPreferenceValue(mTaglinePref, mSiteSettings.getTagline());
+ changeEditTextPreferenceValue(mAddressPref, mSiteSettings.getAddress());
+ changeEditTextPreferenceValue(mUsernamePref, mSiteSettings.getUsername());
+ changeEditTextPreferenceValue(mPasswordPref, mSiteSettings.getPassword());
+ changeLanguageValue(mSiteSettings.getLanguageCode());
+ setDetailListPreferenceValue(mPrivacyPref,
+ String.valueOf(mSiteSettings.getPrivacy()),
+ mSiteSettings.getPrivacyDescription());
+ setDetailListPreferenceValue(mImageWidthPref,
+ mBlog.getMaxImageWidth(),
+ mBlog.getMaxImageWidth());
+ setCategories();
+ setPostFormats();
+ setAllowComments(mSiteSettings.getAllowComments());
+ setSendPingbacks(mSiteSettings.getSendPingbacks());
+ setReceivePingbacks(mSiteSettings.getReceivePingbacks());
+ setDetailListPreferenceValue(mSortByPref,
+ String.valueOf(mSiteSettings.getCommentSorting()),
+ mSiteSettings.getSortingDescription());
+ int approval = mSiteSettings.getManualApproval() ?
+ mSiteSettings.getUseCommentWhitelist() ? 0
+ : -1 : 1;
+ setDetailListPreferenceValue(mWhitelistPref, String.valueOf(approval), getWhitelistSummary(approval));
+ String s = StringUtils.getQuantityString(getActivity(), R.string.site_settings_multiple_links_summary_zero,
+ R.string.site_settings_multiple_links_summary_one,
+ R.string.site_settings_multiple_links_summary_other, mSiteSettings.getMultipleLinks());
+ mMultipleLinksPref.setSummary(s);
+ mUploadAndLinkPref.setChecked(mBlog.isFullSizeImage());
+ mIdentityRequiredPreference.setChecked(mSiteSettings.getIdentityRequired());
+ mUserAccountRequiredPref.setChecked(mSiteSettings.getUserAccountRequired());
+ mThreadingPref.setSummary(mSiteSettings.getThreadingDescription());
+ mCloseAfterPref.setSummary(mSiteSettings.getCloseAfterDescriptionForPeriod());
+ mPagingPref.setSummary(mSiteSettings.getPagingDescription());
+ mRelatedPostsPref.setSummary(mSiteSettings.getRelatedPostsDescription());
+ mModerationHoldPref.setSummary(mSiteSettings.getModerationHoldDescription());
+ mBlacklistPref.setSummary(mSiteSettings.getBlacklistDescription());
+ }
+
+ private void setCategories() {
+ // Ignore if there are no changes
+ if (mSiteSettings.isSameCategoryList(mCategoryPref.getEntryValues())) {
+ mCategoryPref.setValue(String.valueOf(mSiteSettings.getDefaultCategory()));
+ mCategoryPref.setSummary(mSiteSettings.getDefaultCategoryForDisplay());
+ return;
+ }
+
+ Map<Integer, String> categories = mSiteSettings.getCategoryNames();
+ CharSequence[] entries = new CharSequence[categories.size()];
+ CharSequence[] values = new CharSequence[categories.size()];
+ int i = 0;
+ for (Integer key : categories.keySet()) {
+ entries[i] = categories.get(key);
+ values[i] = String.valueOf(key);
+ if (key == UNCATEGORIZED_CATEGORY_ID) {
+ CharSequence temp = entries[0];
+ entries[0] = entries[i];
+ entries[i] = temp;
+ temp = values[0];
+ values[0] = values[i];
+ values[i] = temp;
+ }
+ ++i;
+ }
+
+ mCategoryPref.setEntries(entries);
+ mCategoryPref.setEntryValues(values);
+ mCategoryPref.setValue(String.valueOf(mSiteSettings.getDefaultCategory()));
+ mCategoryPref.setSummary(mSiteSettings.getDefaultCategoryForDisplay());
+ }
+
+ private void setPostFormats() {
+ // Ignore if there are no changes
+ if (mSiteSettings.isSameFormatList(mFormatPref.getEntryValues())) {
+ mFormatPref.setValue(String.valueOf(mSiteSettings.getDefaultPostFormat()));
+ mFormatPref.setSummary(mSiteSettings.getDefaultPostFormatDisplay());
+ return;
+ }
+
+ // clone the post formats map
+ final Map<String, String> postFormats = new HashMap<>(mSiteSettings.getFormats());
+
+ // transform the keys and values into arrays and set the ListPreference's data
+ mFormatPref.setEntries(postFormats.values().toArray(new String[0]));
+ mFormatPref.setEntryValues(postFormats.keySet().toArray(new String[0]));
+ mFormatPref.setValue(String.valueOf(mSiteSettings.getDefaultPostFormat()));
+ mFormatPref.setSummary(mSiteSettings.getDefaultPostFormatDisplay());
+ }
+
+ private void setAllowComments(boolean newValue) {
+ mSiteSettings.setAllowComments(newValue);
+ mAllowCommentsPref.setChecked(newValue);
+ mAllowCommentsNested.setChecked(newValue);
+ }
+
+ private void setSendPingbacks(boolean newValue) {
+ mSiteSettings.setSendPingbacks(newValue);
+ mSendPingbacksPref.setChecked(newValue);
+ mSendPingbacksNested.setChecked(newValue);
+ }
+
+ private void setReceivePingbacks(boolean newValue) {
+ mSiteSettings.setReceivePingbacks(newValue);
+ mReceivePingbacksPref.setChecked(newValue);
+ mReceivePingbacksNested.setChecked(newValue);
+ }
+
+ private void setDetailListPreferenceValue(DetailListPreference pref, String value, String summary) {
+ pref.setValue(value);
+ pref.setSummary(summary);
+ pref.refreshAdapter();
+ }
+
+ /**
+ * Helper method to perform validation and set multiple properties on an EditTextPreference.
+ * If newValue is equal to the current preference text no action will be taken.
+ */
+ private void changeEditTextPreferenceValue(EditTextPreference pref, String newValue) {
+ if (newValue == null || pref == null || pref.getEditText().isInEditMode()) return;
+
+ if (!newValue.equals(pref.getSummary())) {
+ String formattedValue = StringUtils.unescapeHTML(newValue.replaceFirst(ADDRESS_FORMAT_REGEX, ""));
+
+ pref.setText(formattedValue);
+ pref.setSummary(formattedValue);
+ }
+ }
+
+ /**
+ * Detail strings for the dialog are generated in the selected language.
+ *
+ * @param newValue
+ * languageCode
+ */
+ private void changeLanguageValue(String newValue) {
+ if (mLanguagePref == null || newValue == null) return;
+
+ if (TextUtils.isEmpty(mLanguagePref.getSummary()) ||
+ !newValue.equals(mLanguagePref.getValue())) {
+ mLanguagePref.setValue(newValue);
+ String summary = WPPrefUtils.getLanguageString(newValue, WPPrefUtils.languageLocale(newValue));
+ mLanguagePref.setSummary(summary);
+ mLanguagePref.refreshAdapter();
+ }
+ }
+
+ private void sortLanguages() {
+ if (mLanguagePref == null) return;
+
+ Pair<String[], String[]> pair = WPPrefUtils.createSortedLanguageDisplayStrings(mLanguagePref.getEntryValues(), WPPrefUtils.languageLocale(null));
+ if (pair != null) {
+ String[] sortedEntries = pair.first;
+ String[] sortedValues = pair.second;
+
+ mLanguagePref.setEntries(sortedEntries);
+ mLanguagePref.setEntryValues(sortedValues);
+ mLanguagePref.setDetails(WPPrefUtils.createLanguageDetailDisplayStrings(sortedValues));
+ }
+ }
+
+ private String getWhitelistSummary(int value) {
+ if (isAdded()) {
+ switch (value) {
+ case -1:
+ return getString(R.string.site_settings_whitelist_none_summary);
+ case 0:
+ return getString(R.string.site_settings_whitelist_known_summary);
+ case 1:
+ return getString(R.string.site_settings_whitelist_all_summary);
+ }
+ }
+ return "";
+ }
+
+ private void updateWhitelistSettings(int val) {
+ mSiteSettings.setManualApproval(val == -1);
+ mSiteSettings.setUseCommentWhitelist(val == 0);
+ setDetailListPreferenceValue(mWhitelistPref,
+ String.valueOf(val),
+ getWhitelistSummary(val));
+ }
+
+ private void showListEditorDialog(int titleRes, int headerRes) {
+ mDialog = new Dialog(getActivity(), R.style.Calypso_SiteSettingsTheme);
+ mDialog.setOnDismissListener(this);
+ mDialog.setContentView(getListEditorView(getString(headerRes)));
+ mDialog.show();
+ WPActivityUtils.addToolbarToDialog(this, mDialog, getString(titleRes));
+ }
+
+ private View getListEditorView(String headerText) {
+ Context themer = new ContextThemeWrapper(getActivity(), R.style.Calypso_SiteSettingsTheme);
+ View view = View.inflate(themer, R.layout.list_editor, null);
+ ((TextView) view.findViewById(R.id.list_editor_header_text)).setText(headerText);
+
+ mAdapter = null;
+ final EmptyViewRecyclerView list = (EmptyViewRecyclerView) view.findViewById(android.R.id.list);
+ list.setLayoutManager(
+ new SmoothScrollLinearLayoutManager(
+ getActivity(),
+ LinearLayoutManager.VERTICAL,
+ false,
+ getResources().getInteger(android.R.integer.config_mediumAnimTime)
+ )
+ );
+ list.setAdapter(getAdapter());
+ list.setEmptyView(view.findViewById(R.id.empty_view));
+ list.addOnItemTouchListener(
+ new RecyclerViewItemClickListener(
+ getActivity(),
+ list,
+ new RecyclerViewItemClickListener.OnItemClickListener() {
+ @Override
+ public void onItemClick(View view, int position) {
+ if (mActionMode != null) {
+ getAdapter().toggleItemSelected(position);
+ mActionMode.invalidate();
+
+ if (getAdapter().getItemsSelected().size() <= 0) {
+ mActionMode.finish();
+ }
+ }
+ }
+
+ @Override
+ public void onLongItemClick(View view, int position) {
+ if (mActionMode == null) {
+ if (view.isHapticFeedbackEnabled()) {
+ view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
+
+ mDialog.getWindow().getDecorView().startActionMode(new ActionModeCallback());
+ getAdapter().setItemSelected(position);
+ mActionMode.invalidate();
+ }
+ }
+ }
+ )
+ );
+ view.findViewById(R.id.fab_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(getActivity(), R.style.Calypso_AlertDialog);
+ final EditText input = new EditText(getActivity());
+ WPPrefUtils.layoutAsInput(input);
+ input.setWidth(getResources().getDimensionPixelSize(R.dimen.list_editor_input_max_width));
+ input.setHint(R.string.site_settings_list_editor_input_hint);
+ builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String entry = input.getText().toString();
+ if (!TextUtils.isEmpty(entry) && !mEditingList.contains(entry)) {
+ mEditingList.add(entry);
+ getAdapter().notifyItemInserted(getAdapter().getItemCount() - 1);
+ list.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ list.smoothScrollToPosition(getAdapter().getItemCount() - 1);
+ }
+ }
+ );
+ mSiteSettings.saveSettings();
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_ADDED_LIST_ITEM);
+ }
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, null);
+ final AlertDialog alertDialog = builder.create();
+ int spacing = getResources().getDimensionPixelSize(R.dimen.dlp_padding_start);
+ alertDialog.setView(input, spacing, spacing, spacing, 0);
+ alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ alertDialog.getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+ alertDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ alertDialog.getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+ }
+ });
+ alertDialog.show();
+ Button positive = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ Button negative = alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ if (positive != null) WPPrefUtils.layoutAsFlatButton(positive);
+ if (negative != null) WPPrefUtils.layoutAsFlatButton(negative);
+ }
+ });
+
+ return view;
+ }
+
+ private void removeBlog() {
+ if (WordPress.wpDB.deleteBlog(getActivity(), mBlog.getLocalTableBlogId())) {
+ StatsTable.deleteStatsForBlog(getActivity(), mBlog.getLocalTableBlogId()); // Remove stats data
+ AnalyticsUtils.refreshMetadata();
+ ToastUtils.showToast(getActivity(), R.string.blog_removed_successfully);
+ WordPress.wpDB.deleteLastBlogId();
+ WordPress.currentBlog = null;
+ getActivity().setResult(RESULT_BLOG_REMOVED);
+
+ // If the last blog is removed and the user is not signed in wpcom, broadcast a UserSignedOut event
+ if (!AccountHelper.isSignedIn()) {
+ EventBus.getDefault().post(new CoreEvents.UserSignedOutCompletely());
+ }
+
+ // Checks for stats widgets that were synched with a blog that could be gone now.
+ StatsWidgetProvider.updateWidgetsOnLogout(getActivity());
+
+ getActivity().finish();
+ } else {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
+ dialogBuilder.setTitle(getResources().getText(R.string.error));
+ dialogBuilder.setMessage(getResources().getText(R.string.could_not_remove_account));
+ dialogBuilder.setPositiveButton(R.string.ok, null);
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+ }
+ }
+
+ private boolean shouldShowListPreference(DetailListPreference preference) {
+ return preference != null && preference.getEntries() != null && preference.getEntries().length > 0;
+ }
+
+ private boolean setupMorePreferenceScreen() {
+ if (mMorePreference == null || !isAdded()) return false;
+ String title = getString(R.string.site_settings_discussion_title);
+ Dialog dialog = mMorePreference.getDialog();
+ if (dialog != null) {
+ setupPreferenceList((ListView) dialog.findViewById(android.R.id.list), getResources());
+ WPActivityUtils.addToolbarToDialog(this, dialog, title);
+ return true;
+ }
+ return false;
+ }
+
+ private void removeMoreScreenToolbar() {
+ if (mMorePreference == null || !isAdded()) return;
+ Dialog moreDialog = mMorePreference.getDialog();
+ WPActivityUtils.removeToolbarFromDialog(this, moreDialog);
+ }
+
+ private void hideAdminRequiredPreferences() {
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_general);
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_account);
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_discussion);
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_category);
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_format);
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_related_posts);
+ }
+
+ private void removeDotComOnlyPreferences() {
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_general, R.string.pref_key_site_language);
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_writing, R.string.pref_key_site_related_posts);
+ }
+
+ private void removeSelfHostedOnlyPreferences() {
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_account);
+ WPPrefUtils.removePreference(this, R.string.pref_key_site_screen, R.string.pref_key_site_delete_site_screen);
+ }
+
+ private Preference getChangePref(int id) {
+ return WPPrefUtils.getPrefAndSetChangeListener(this, id, this);
+ }
+
+ private Preference getClickPref(int id) {
+ return WPPrefUtils.getPrefAndSetClickListener(this, id, this);
+ }
+
+ private void handleDeleteSiteError() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.error_deleting_site);
+ builder.setMessage(R.string.error_deleting_site_summary);
+ builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+ builder.setPositiveButton(R.string.contact_support, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ HelpshiftHelper.getInstance().showConversation(getActivity(), HelpshiftHelper.Tag.ORIGIN_DELETE_SITE);
+ }
+ });
+ builder.show();
+ }
+
+ private void exportSite() {
+ final Blog currentBlog = WordPress.getCurrentBlog();
+ if (currentBlog.isDotcomFlag()) {
+ final ProgressDialog progressDialog = ProgressDialog.show(getActivity(), "", getActivity().getString(R.string.exporting_content_progress), true, true);
+ WordPress.getRestClientUtils().exportContentAll(currentBlog.getDotComBlogId(), new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ if (isAdded()) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_EXPORT_SITE_RESPONSE_OK);
+ dismissProgressDialog(progressDialog);
+ Snackbar.make(getView(), R.string.export_email_sent, Snackbar.LENGTH_LONG).show();
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (isAdded()) {
+ HashMap<String, Object> errorProperty = new HashMap<>();
+ errorProperty.put(ANALYTICS_ERROR_PROPERTY_KEY, error.getMessage());
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_EXPORT_SITE_RESPONSE_ERROR, errorProperty);
+ dismissProgressDialog(progressDialog);
+ }
+ }
+ });
+ }
+ }
+
+ private void deleteSite() {
+ final Blog currentBlog = WordPress.getCurrentBlog();
+ if (currentBlog.isDotcomFlag()) {
+ final ProgressDialog progressDialog = ProgressDialog.show(getActivity(), "", getString(R.string.delete_site_progress), true, false);
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_REQUESTED);
+ WordPress.getRestClientUtils().deleteSite(currentBlog.getDotComBlogId(), new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_RESPONSE_OK);
+ progressDialog.dismiss();
+ removeBlog();
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ HashMap<String, Object> errorProperty = new HashMap<>();
+ errorProperty.put(ANALYTICS_ERROR_PROPERTY_KEY, error.getMessage());
+ AnalyticsUtils.trackWithCurrentBlogDetails(
+ AnalyticsTracker.Stat.SITE_SETTINGS_DELETE_SITE_RESPONSE_ERROR, errorProperty);
+ dismissProgressDialog(progressDialog);
+ handleDeleteSiteError();
+ }
+ });
+ }
+ }
+
+ private MultiSelectRecyclerViewAdapter getAdapter() {
+ if (mAdapter == null) {
+ mAdapter = new MultiSelectRecyclerViewAdapter(getActivity(), mEditingList);
+ }
+
+ return mAdapter;
+ }
+
+ private final class ActionModeCallback implements ActionMode.Callback {
+ @Override
+ public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
+ switch (menuItem.getItemId()) {
+ case R.id.menu_delete:
+ SparseBooleanArray checkedItems = getAdapter().getItemsSelected();
+
+ HashMap<String, Object> properties = new HashMap<>();
+ properties.put("num_items_deleted", checkedItems.size());
+ AnalyticsUtils.trackWithCurrentBlogDetails(AnalyticsTracker.Stat.SITE_SETTINGS_DELETED_LIST_ITEMS, properties);
+
+ for (int i = checkedItems.size() - 1; i >= 0; i--) {
+ final int index = checkedItems.keyAt(i);
+
+ if (checkedItems.get(index)) {
+ mEditingList.remove(index);
+ }
+ }
+
+ mSiteSettings.saveSettings();
+ mActionMode.finish();
+ return true;
+ case R.id.menu_select_all:
+ for (int i = 0; i < getAdapter().getItemCount(); i++) {
+ getAdapter().setItemSelected(i);
+ }
+
+ mActionMode.invalidate();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
+ WPActivityUtils.setStatusBarColor(mDialog.getWindow(), R.color.action_mode_status_bar_tint);
+ mActionMode = actionMode;
+ MenuInflater inflater = actionMode.getMenuInflater();
+ inflater.inflate(R.menu.list_editor, menu);
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ WPActivityUtils.setStatusBarColor(mDialog.getWindow(), R.color.status_bar_tint);
+ getAdapter().removeItemsSelected();
+ mActionMode = null;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
+ actionMode.setTitle(getString(
+ R.string.site_settings_list_editor_action_mode_title,
+ getAdapter().getItemsSelected().size())
+ );
+ return true;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java
new file mode 100644
index 000000000..0dc980e2d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java
@@ -0,0 +1,871 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.support.annotation.NonNull;
+import android.text.Html;
+import android.text.TextUtils;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.SiteSettingsTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.CategoryModel;
+import org.wordpress.android.models.SiteSettingsModel;
+import org.wordpress.android.util.LanguageUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.WPPrefUtils;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.ApiHelper.Param;
+import org.xmlrpc.android.XMLRPCCallback;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCFactory;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Interface for WordPress (.com and .org) Site Settings. The {@link SiteSettingsModel} class is
+ * used to store the following settings:
+ *
+ * - Title
+ * - Tagline
+ * - Address
+ * - Privacy
+ * - Language
+ * - Username (.org only)
+ * - Password (.org only)
+ * - Location (local device setting, not saved remotely)
+ * - Default Category
+ * - Default Format
+ * - Related Posts
+ * - Allow Comments
+ * - Send Pingbacks
+ * - Receive Pingbacks
+ * - Identity Required
+ * - User Account Required
+ * - Close Comments After
+ * - Comment Sort Order
+ * - Comment Threading
+ * - Comment Paging
+ * - Comment User Whitelist
+ * - Comment Link Limit
+ * - Comment Moderation Hold Filter
+ * - Comment Blacklist Filter
+ *
+ * This class is marked abstract. This is due to the fact that .org (self-hosted) and .com sites
+ * expose different API's to query and edit their respective settings (even though the options
+ * offered by each is roughly the same). To get an instance of this interface class use the
+ * {@link SiteSettingsInterface#getInterface(Activity, Blog, SiteSettingsListener)} method. It will
+ * determine which interface ({@link SelfHostedSiteSettings} or {@link DotComSiteSettings}) is
+ * appropriate for the given blog.
+ */
+
+public abstract class SiteSettingsInterface {
+
+ /**
+ * Name of the {@link SharedPreferences} that is used to store local settings.
+ */
+ public static final String SITE_SETTINGS_PREFS = "site-settings-prefs";
+
+ /**
+ * Key used to access the language preference stored in {@link SharedPreferences}.
+ */
+ public static final String LANGUAGE_PREF_KEY = "site-settings-language-pref";
+
+ /**
+ * Key used to access the location preference stored in {@link SharedPreferences}.
+ */
+ public static final String LOCATION_PREF_KEY = "site-settings-location-pref";
+
+ /**
+ * Key used to access the default category preference stored in {@link SharedPreferences}.
+ */
+ public static final String DEF_CATEGORY_PREF_KEY = "site-settings-category-pref";
+
+ /**
+ * Key used to access the default post format preference stored in {@link SharedPreferences}.
+ */
+ public static final String DEF_FORMAT_PREF_KEY = "site-settings-format-pref";
+
+ /**
+ * Identifies an Ascending (oldest to newest) sort order.
+ */
+ public static final int ASCENDING_SORT = 0;
+
+ /**
+ * Identifies an Descending (newest to oldest) sort order.
+ */
+ public static final int DESCENDING_SORT = 1;
+
+ /**
+ * Used to prefix keys in an analytics property list.
+ */
+ protected static final String SAVED_ITEM_PREFIX = "item_saved_";
+
+ /**
+ * Key for the Standard post format. Used as default if post format is not set/known.
+ */
+ private static final String STANDARD_POST_FORMAT_KEY = "standard";
+
+ /**
+ * Standard post format value. Used as default display value if post format is unknown.
+ */
+ private static final String STANDARD_POST_FORMAT = "Standard";
+
+ /**
+ * Instantiates the appropriate (self-hosted or .com) SiteSettingsInterface.
+ */
+ public static SiteSettingsInterface getInterface(Activity host, Blog blog, SiteSettingsListener listener) {
+ if (host == null || blog == null) return null;
+
+ if (blog.isDotcomFlag()) {
+ return new DotComSiteSettings(host, blog, listener);
+ } else {
+ return new SelfHostedSiteSettings(host, blog, listener);
+ }
+ }
+
+ /**
+ * Returns an instance of the {@link this#SITE_SETTINGS_PREFS} {@link SharedPreferences}.
+ */
+ public static SharedPreferences siteSettingsPreferences(Context context) {
+ return context.getSharedPreferences(SITE_SETTINGS_PREFS, Context.MODE_PRIVATE);
+ }
+
+ /**
+ * Gets the geo-tagging value stored in {@link SharedPreferences}, false by default.
+ */
+ public static boolean getGeotagging(Context context) {
+ return siteSettingsPreferences(context).getBoolean(LOCATION_PREF_KEY, false);
+ }
+
+ /**
+ * Gets the default category value stored in {@link SharedPreferences}, 0 by default.
+ */
+ public static String getDefaultCategory(Context context) {
+ int id = siteSettingsPreferences(context).getInt(DEF_CATEGORY_PREF_KEY, 0);
+
+ if (id != 0) {
+ CategoryModel category = new CategoryModel();
+ Cursor cursor = SiteSettingsTable.getCategory(id);
+ if (cursor != null && cursor.moveToFirst()) {
+ category.deserializeFromDatabase(cursor);
+ return category.name;
+ }
+ }
+
+ return "";
+ }
+
+ /**
+ * Gets the default post format value stored in {@link SharedPreferences}, "" by default.
+ */
+ public static String getDefaultFormat(Context context) {
+ return siteSettingsPreferences(context).getString(DEF_FORMAT_PREF_KEY, "");
+ }
+
+ /**
+ * Thrown when provided credentials are not valid.
+ */
+ public class AuthenticationError extends Exception { }
+
+ /**
+ * Interface callbacks for settings events.
+ */
+ public interface SiteSettingsListener {
+ /**
+ * Called when settings have been updated with remote changes.
+ *
+ * @param error
+ * null if successful
+ */
+ void onSettingsUpdated(Exception error);
+
+ /**
+ * Called when attempt to update remote settings is finished.
+ *
+ * @param error
+ * null if successful
+ */
+ void onSettingsSaved(Exception error);
+
+ /**
+ * Called when a request to validate current credentials has completed.
+ *
+ * @param error
+ * null if successful
+ */
+ void onCredentialsValidated(Exception error);
+ }
+
+ /**
+ * {@link SiteSettingsInterface} implementations should use this method to start a background
+ * task to load settings data from a remote source.
+ */
+ protected abstract void fetchRemoteData();
+
+ protected final Activity mActivity;
+ protected final Blog mBlog;
+ protected final SiteSettingsListener mListener;
+ protected final SiteSettingsModel mSettings;
+ protected final SiteSettingsModel mRemoteSettings;
+
+ private final Map<String, String> mLanguageCodes;
+
+ protected SiteSettingsInterface(Activity host, Blog blog, SiteSettingsListener listener) {
+ mActivity = host;
+ mBlog = blog;
+ mListener = listener;
+ mSettings = new SiteSettingsModel();
+ mRemoteSettings = new SiteSettingsModel();
+ mLanguageCodes = WPPrefUtils.generateLanguageMap(host);
+ }
+
+ public void saveSettings() {
+ SiteSettingsTable.saveSettings(mSettings);
+ siteSettingsPreferences(mActivity).edit().putString(LANGUAGE_PREF_KEY, mSettings.language).apply();
+ siteSettingsPreferences(mActivity).edit().putBoolean(LOCATION_PREF_KEY, mSettings.location).apply();
+ siteSettingsPreferences(mActivity).edit().putInt(DEF_CATEGORY_PREF_KEY, mSettings.defaultCategory).apply();
+ siteSettingsPreferences(mActivity).edit().putString(DEF_FORMAT_PREF_KEY, mSettings.defaultPostFormat).apply();
+ }
+
+ public @NonNull String getTitle() {
+ return mSettings.title == null ? "" : mSettings.title;
+ }
+
+ public @NonNull String getTagline() {
+ return mSettings.tagline == null ? "" : mSettings.tagline;
+ }
+
+ public @NonNull String getAddress() {
+ return mSettings.address == null ? "" : mSettings.address;
+ }
+
+ public int getPrivacy() {
+ return mSettings.privacy;
+ }
+
+ public @NonNull String getPrivacyDescription() {
+ if (mActivity != null) {
+ switch (getPrivacy()) {
+ case -1:
+ return mActivity.getString(R.string.site_settings_privacy_private_summary);
+ case 0:
+ return mActivity.getString(R.string.site_settings_privacy_hidden_summary);
+ case 1:
+ return mActivity.getString(R.string.site_settings_privacy_public_summary);
+ }
+ }
+ return "";
+ }
+
+ public @NonNull String getLanguageCode() {
+ return mSettings.language == null ? "" : mSettings.language;
+ }
+
+ public @NonNull String getUsername() {
+ return mSettings.username == null ? "" : mSettings.username;
+ }
+
+ public @NonNull String getPassword() {
+ return mSettings.password == null ? "" : mSettings.password;
+ }
+
+ public boolean getLocation() {
+ return mSettings.location;
+ }
+
+ public @NonNull Map<String, String> getFormats() {
+ if (mSettings.postFormats == null) mSettings.postFormats = new HashMap<>();
+ return mSettings.postFormats;
+ }
+
+ public @NonNull CategoryModel[] getCategories() {
+ if (mSettings.categories == null) mSettings.categories = new CategoryModel[0];
+ return mSettings.categories;
+ }
+
+ public @NonNull Map<Integer, String> getCategoryNames() {
+ Map<Integer, String> categoryNames = new HashMap<>();
+ if (mSettings.categories != null && mSettings.categories.length > 0) {
+ for (CategoryModel model : mSettings.categories) {
+ categoryNames.put(model.id, Html.fromHtml(model.name).toString());
+ }
+ }
+
+ return categoryNames;
+ }
+
+ public int getDefaultCategory() {
+ return mSettings.defaultCategory;
+ }
+
+ public @NonNull String getDefaultCategoryForDisplay() {
+ for (CategoryModel model : getCategories()) {
+ if (model != null && model.id == getDefaultCategory()) {
+ return Html.fromHtml(model.name).toString();
+ }
+ }
+
+ return "";
+ }
+
+ public @NonNull String getDefaultPostFormat() {
+ if (TextUtils.isEmpty(mSettings.defaultPostFormat) || !getFormats().containsKey(mSettings.defaultPostFormat)) {
+ mSettings.defaultPostFormat = STANDARD_POST_FORMAT_KEY;
+ }
+ return mSettings.defaultPostFormat;
+ }
+
+ public @NonNull String getDefaultPostFormatDisplay() {
+ String defaultFormat = getFormats().get(getDefaultPostFormat());
+ if (TextUtils.isEmpty(defaultFormat)) defaultFormat = STANDARD_POST_FORMAT;
+ return defaultFormat;
+ }
+
+ public boolean getShowRelatedPosts() {
+ return mSettings.showRelatedPosts;
+ }
+
+ public boolean getShowRelatedPostHeader() {
+ return mSettings.showRelatedPostHeader;
+ }
+
+ public boolean getShowRelatedPostImages() {
+ return mSettings.showRelatedPostImages;
+ }
+
+ public @NonNull String getRelatedPostsDescription() {
+ if (mActivity == null) return "";
+ String desc = mActivity.getString(getShowRelatedPosts() ? R.string.on : R.string.off);
+ return StringUtils.capitalize(desc);
+ }
+
+ public boolean getAllowComments() {
+ return mSettings.allowComments;
+ }
+
+ public boolean getSendPingbacks() {
+ return mSettings.sendPingbacks;
+ }
+
+ public boolean getReceivePingbacks() {
+ return mSettings.receivePingbacks;
+ }
+
+ public boolean getShouldCloseAfter() {
+ return mSettings.shouldCloseAfter;
+ }
+
+ public int getCloseAfter() {
+ return mSettings.closeCommentAfter;
+ }
+
+ public @NonNull String getCloseAfterDescriptionForPeriod() {
+ return getCloseAfterDescriptionForPeriod(getCloseAfter());
+ }
+
+ public int getCloseAfterPeriodForDescription() {
+ return !getShouldCloseAfter() ? 0 : getCloseAfter();
+ }
+
+ public @NonNull String getCloseAfterDescription() {
+ return getCloseAfterDescriptionForPeriod(getCloseAfterPeriodForDescription());
+ }
+
+ public @NonNull String getCloseAfterDescriptionForPeriod(int period) {
+ if (mActivity == null) return "";
+
+ if (!getShouldCloseAfter()) return mActivity.getString(R.string.never);
+
+ return StringUtils.getQuantityString(mActivity, R.string.never, R.string.days_quantity_one,
+ R.string.days_quantity_other, period);
+ }
+
+ public int getCommentSorting() {
+ return mSettings.sortCommentsBy;
+ }
+
+ public @NonNull String getSortingDescription() {
+ if (mActivity == null) return "";
+
+ int order = getCommentSorting();
+ switch (order) {
+ case SiteSettingsInterface.ASCENDING_SORT:
+ return mActivity.getString(R.string.oldest_first);
+ case SiteSettingsInterface.DESCENDING_SORT:
+ return mActivity.getString(R.string.newest_first);
+ default:
+ return mActivity.getString(R.string.unknown);
+ }
+ }
+
+ public boolean getShouldThreadComments() {
+ return mSettings.shouldThreadComments;
+ }
+
+ public int getThreadingLevels() {
+ return mSettings.threadingLevels;
+ }
+
+ public int getThreadingLevelsForDescription() {
+ return !getShouldThreadComments() ? 1 : getThreadingLevels();
+ }
+
+ public @NonNull String getThreadingDescription() {
+ return getThreadingDescriptionForLevel(getThreadingLevelsForDescription());
+ }
+
+ public @NonNull String getThreadingDescriptionForLevel(int level) {
+ if (mActivity == null) return "";
+
+ if (level <= 1) return mActivity.getString(R.string.none);
+ return String.format(mActivity.getString(R.string.site_settings_threading_summary), level);
+ }
+
+ public boolean getShouldPageComments() {
+ return mSettings.shouldPageComments;
+ }
+
+ public int getPagingCount() {
+ return mSettings.commentsPerPage;
+ }
+
+ public int getPagingCountForDescription() {
+ return !getShouldPageComments() ? 0 : getPagingCount();
+ }
+
+ public @NonNull String getPagingDescription() {
+ if (mActivity == null) return "";
+
+ if (!getShouldPageComments()) {
+ return mActivity.getString(R.string.disabled);
+ }
+
+ int count = getPagingCountForDescription();
+ return StringUtils.getQuantityString(mActivity, R.string.none, R.string.site_settings_paging_summary_one,
+ R.string.site_settings_paging_summary_other, count);
+ }
+
+ public boolean getManualApproval() {
+ return mSettings.commentApprovalRequired;
+ }
+
+ public boolean getIdentityRequired() {
+ return mSettings.commentsRequireIdentity;
+ }
+
+ public boolean getUserAccountRequired() {
+ return mSettings.commentsRequireUserAccount;
+ }
+
+ public boolean getUseCommentWhitelist() {
+ return mSettings.commentAutoApprovalKnownUsers;
+ }
+
+ public int getMultipleLinks() {
+ return mSettings.maxLinks;
+ }
+
+ public @NonNull List<String> getModerationKeys() {
+ if (mSettings.holdForModeration == null) mSettings.holdForModeration = new ArrayList<>();
+ return mSettings.holdForModeration;
+ }
+
+ public @NonNull String getModerationHoldDescription() {
+ return getKeysDescription(getModerationKeys().size());
+ }
+
+ public @NonNull List<String> getBlacklistKeys() {
+ if (mSettings.blacklist == null) mSettings.blacklist = new ArrayList<>();
+ return mSettings.blacklist;
+ }
+
+ public @NonNull String getBlacklistDescription() {
+ return getKeysDescription(getBlacklistKeys().size());
+ }
+
+ public @NonNull String getKeysDescription(int count) {
+ if (mActivity == null) return "";
+
+ return StringUtils.getQuantityString(mActivity, R.string.site_settings_list_editor_no_items_text,
+ R.string.site_settings_list_editor_summary_one,
+ R.string.site_settings_list_editor_summary_other, count);
+
+ }
+
+ public void setTitle(String title) {
+ mSettings.title = title;
+ }
+
+ public void setTagline(String tagline) {
+ mSettings.tagline = tagline;
+ }
+
+ public void setAddress(String address) {
+ mSettings.address = address;
+ }
+
+ public void setPrivacy(int privacy) {
+ mSettings.privacy = privacy;
+ }
+
+ public boolean setLanguageCode(String languageCode) {
+ if (!mLanguageCodes.containsKey(languageCode) ||
+ TextUtils.isEmpty(mLanguageCodes.get(languageCode))) return false;
+ mSettings.language = languageCode;
+ mSettings.languageId = Integer.valueOf(mLanguageCodes.get(languageCode));
+ return true;
+ }
+
+ public void setLanguageId(int languageId) {
+ // want to prevent O(n) language code lookup if there is no change
+ if (mSettings.languageId != languageId) {
+ mSettings.languageId = languageId;
+ mSettings.language = languageIdToLanguageCode(Integer.toString(languageId));
+ }
+ }
+
+ public void setUsername(String username) {
+ mSettings.username = username;
+ }
+
+ public void setPassword(String password) {
+ mSettings.password = password;
+ }
+
+ public void setLocation(boolean location) {
+ mSettings.location = location;
+ }
+
+ public void setAllowComments(boolean allowComments) {
+ mSettings.allowComments = allowComments;
+ }
+
+ public void setSendPingbacks(boolean sendPingbacks) {
+ mSettings.sendPingbacks = sendPingbacks;
+ }
+
+ public void setReceivePingbacks(boolean receivePingbacks) {
+ mSettings.receivePingbacks = receivePingbacks;
+ }
+
+ public void setShouldCloseAfter(boolean shouldCloseAfter) {
+ mSettings.shouldCloseAfter = shouldCloseAfter;
+ }
+
+ public void setCloseAfter(int period) {
+ mSettings.closeCommentAfter = period;
+ }
+
+ public void setCommentSorting(int method) {
+ mSettings.sortCommentsBy = method;
+ }
+
+ public void setShouldThreadComments(boolean shouldThread) {
+ mSettings.shouldThreadComments = shouldThread;
+ }
+
+ public void setThreadingLevels(int levels) {
+ mSettings.threadingLevels = levels;
+ }
+
+ public void setShouldPageComments(boolean shouldPage) {
+ mSettings.shouldPageComments= shouldPage;
+ }
+
+ public void setPagingCount(int count) {
+ mSettings.commentsPerPage = count;
+ }
+
+ public void setManualApproval(boolean required) {
+ mSettings.commentApprovalRequired = required;
+ }
+
+ public void setIdentityRequired(boolean required) {
+ mSettings.commentsRequireIdentity = required;
+ }
+
+ public void setUserAccountRequired(boolean required) {
+ mSettings.commentsRequireUserAccount = required;
+ }
+
+ public void setUseCommentWhitelist(boolean useWhitelist) {
+ mSettings.commentAutoApprovalKnownUsers = useWhitelist;
+ }
+
+ public void setMultipleLinks(int count) {
+ mSettings.maxLinks = count;
+ }
+
+ public void setModerationKeys(List<String> keys) {
+ mSettings.holdForModeration = keys;
+ }
+
+ public void setBlacklistKeys(List<String> keys) {
+ mSettings.blacklist = keys;
+ }
+
+ public void setDefaultCategory(int category) {
+ mSettings.defaultCategory = category;
+ }
+
+ /**
+ * Sets the default post format.
+ *
+ * @param format
+ * if null or empty default format is set to {@link SiteSettingsInterface#STANDARD_POST_FORMAT_KEY}
+ */
+ public void setDefaultFormat(String format) {
+ if (TextUtils.isEmpty(format)) {
+ mSettings.defaultPostFormat = STANDARD_POST_FORMAT_KEY;
+ } else {
+ mSettings.defaultPostFormat = format.toLowerCase();
+ }
+ }
+
+ public void setShowRelatedPosts(boolean relatedPosts) {
+ mSettings.showRelatedPosts = relatedPosts;
+ }
+
+ public void setShowRelatedPostHeader(boolean showHeader) {
+ mSettings.showRelatedPostHeader = showHeader;
+ }
+
+ public void setShowRelatedPostImages(boolean showImages) {
+ mSettings.showRelatedPostImages = showImages;
+ }
+
+ /**
+ * Determines if the current Moderation Hold list contains a given value.
+ */
+ public boolean moderationHoldListContains(String value) {
+ return getModerationKeys().contains(value);
+ }
+
+ /**
+ * Determines if the current Blacklist list contains a given value.
+ */
+ public boolean blacklistListContains(String value) {
+ return getBlacklistKeys().contains(value);
+ }
+
+ /**
+ * Checks if the provided list of post format IDs is the same (order dependent) as the current
+ * list of Post Formats in the local settings object.
+ *
+ * @param ids
+ * an array of post format IDs
+ * @return
+ * true unless the provided IDs are different from the current IDs or in a different order
+ */
+ public boolean isSameFormatList(CharSequence[] ids) {
+ if (ids == null) return mSettings.postFormats == null;
+ if (mSettings.postFormats == null || ids.length != mSettings.postFormats.size()) return false;
+
+ String[] keys = mSettings.postFormats.keySet().toArray(new String[mSettings.postFormats.size()]);
+ for (int i = 0; i < ids.length; ++i) {
+ if (!keys[i].equals(ids[i])) return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if the provided list of category IDs is the same (order dependent) as the current
+ * list of Categories in the local settings object.
+ *
+ * @param ids
+ * an array of integers stored as Strings (for convenience)
+ * @return
+ * true unless the provided IDs are different from the current IDs or in a different order
+ */
+ public boolean isSameCategoryList(CharSequence[] ids) {
+ if (ids == null) return mSettings.categories == null;
+ if (mSettings.categories == null || ids.length != mSettings.categories.length) return false;
+
+ for (int i = 0; i < ids.length; ++i) {
+ if (Integer.valueOf(ids[i].toString()) != mSettings.categories[i].id) return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Needed so that subclasses can be created before initializing. The final member variables
+ * are null until object has been created so XML-RPC callbacks will not run.
+ *
+ * @return
+ * returns itself for the convenience of
+ * {@link SiteSettingsInterface#getInterface(Activity, Blog, SiteSettingsListener)}
+ */
+ public SiteSettingsInterface init(boolean fetchRemote) {
+ loadCachedSettings();
+
+ if (fetchRemote) {
+ fetchRemoteData();
+ fetchPostFormats();
+ }
+
+ return this;
+ }
+
+ /**
+ * If there is a change in verification status the listener is notified.
+ */
+ protected void credentialsVerified(boolean valid) {
+ Exception e = valid ? null : new AuthenticationError();
+ if (mSettings.hasVerifiedCredentials != valid) notifyCredentialsVerifiedOnUiThread(e);
+ mRemoteSettings.hasVerifiedCredentials = mSettings.hasVerifiedCredentials = valid;
+ }
+
+ /**
+ * Helper method to create an XML-RPC interface for the current blog.
+ */
+ protected XMLRPCClientInterface instantiateInterface() {
+ if (mBlog == null) return null;
+ return XMLRPCFactory.instantiate(mBlog.getUri(), mBlog.getHttpuser(), mBlog.getHttppassword());
+ }
+
+ /**
+ * Language IDs, used only by WordPress, are integer values that map to a language code.
+ * https://github.com/Automattic/calypso-pre-oss/blob/72c2029b0805a73b749a2b64dd1d8655cae528d0/config/production.json#L86-L227
+ *
+ * Language codes are unique two-letter identifiers defined by ISO 639-1. Region dialects can
+ * be defined by appending a -** where ** is the region code (en-GB -> English, Great Britain).
+ * https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
+ */
+ protected String languageIdToLanguageCode(String id) {
+ if (id != null) {
+ for (String key : mLanguageCodes.keySet()) {
+ if (id.equals(mLanguageCodes.get(key))) {
+ return key;
+ }
+ }
+ }
+
+ return "";
+ }
+
+ /**
+ * Need to defer loading the cached settings to a thread so it completes after initialization.
+ */
+ private void loadCachedSettings() {
+ Cursor localSettings = SiteSettingsTable.getSettings(mBlog.getRemoteBlogId());
+
+ if (localSettings != null) {
+ Map<Integer, CategoryModel> cachedModels = SiteSettingsTable.getAllCategories();
+ mSettings.deserializeOptionsDatabaseCursor(localSettings, cachedModels);
+ mSettings.language = languageIdToLanguageCode(Integer.toString(mSettings.languageId));
+ if (mSettings.language == null) {
+ setLanguageCode(LanguageUtils.getPatchedCurrentDeviceLanguage(null));
+ }
+ mRemoteSettings.language = mSettings.language;
+ mRemoteSettings.languageId = mSettings.languageId;
+ mRemoteSettings.location = mSettings.location;
+ localSettings.close();
+ notifyUpdatedOnUiThread(null);
+ } else {
+ mSettings.isInLocalTable = false;
+ setAddress(mBlog.getHomeURL());
+ setUsername(mBlog.getUsername());
+ setPassword(mBlog.getPassword());
+ setTitle(mBlog.getBlogName());
+ }
+ }
+
+ /**
+ * Gets available post formats via XML-RPC. Since both self-hosted and .com sites retrieve the
+ * format list via XML-RPC there is no need to implement this in the sub-classes.
+ */
+ private void fetchPostFormats() {
+ XMLRPCClientInterface client = instantiateInterface();
+ if (client == null) return;
+
+ Map<String, String> args = new HashMap<>();
+ args.put(Param.SHOW_SUPPORTED_POST_FORMATS, "true");
+ Object[] params = { mBlog.getRemoteBlogId(), mBlog.getUsername(),
+ mBlog.getPassword(), args};
+ client.callAsync(new XMLRPCCallback() {
+ @Override
+ public void onSuccess(long id, Object result) {
+ credentialsVerified(true);
+
+ if (result != null && result instanceof HashMap) {
+ Map<?, ?> resultMap = (HashMap<?, ?>) result;
+ Map allFormats;
+ Object[] supportedFormats;
+ if (resultMap.containsKey("supported")) {
+ allFormats = (Map) resultMap.get("all");
+ supportedFormats = (Object[]) resultMap.get("supported");
+ } else {
+ allFormats = resultMap;
+ supportedFormats = allFormats.keySet().toArray();
+ }
+
+ mRemoteSettings.postFormats = new HashMap<>();
+ mRemoteSettings.postFormats.put("standard", "Standard");
+ for (Object supportedFormat : supportedFormats) {
+ if (allFormats.containsKey(supportedFormat)) {
+ mRemoteSettings.postFormats.put(supportedFormat.toString(), allFormats.get(supportedFormat).toString());
+ }
+ }
+ mSettings.postFormats = new HashMap<>(mRemoteSettings.postFormats);
+ SiteSettingsTable.saveSettings(mSettings);
+
+ notifyUpdatedOnUiThread(null);
+ }
+ }
+
+ @Override
+ public void onFailure(long id, Exception error) {
+ }
+ }, Method.GET_POST_FORMATS, params);
+ }
+
+ /**
+ * Notifies listener that credentials have been validated or are incorrect.
+ */
+ private void notifyCredentialsVerifiedOnUiThread(final Exception error) {
+ if (mActivity == null || mListener == null) return;
+
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onCredentialsValidated(error);
+ }
+ });
+ }
+
+ /**
+ * Notifies listener that settings have been updated with the latest remote data.
+ */
+ protected void notifyUpdatedOnUiThread(final Exception error) {
+ if (mActivity == null || mActivity.isFinishing() || mListener == null) return;
+
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onSettingsUpdated(error);
+ }
+ });
+ }
+
+ /**
+ * Notifies listener that settings have been saved or an error occurred while saving.
+ */
+ protected void notifySavedOnUiThread(final Exception error) {
+ if (mActivity == null || mListener == null) return;
+
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onSettingsSaved(error);
+ }
+ });
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SmoothScrollLinearLayoutManager.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SmoothScrollLinearLayoutManager.java
new file mode 100644
index 000000000..9a4b04467
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SmoothScrollLinearLayoutManager.java
@@ -0,0 +1,58 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.LinearSmoothScroller;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+/**
+ * LinearLayoutManager with smooth scrolling and custom duration (in milliseconds).
+ */
+public class SmoothScrollLinearLayoutManager extends LinearLayoutManager {
+ private final int mDuration;
+
+ public SmoothScrollLinearLayoutManager(Context context, int orientation, boolean reverseLayout, int duration) {
+ super(context, orientation, reverseLayout);
+ this.mDuration = duration;
+ }
+
+ @Override
+ public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
+ final View firstVisibleChild = recyclerView.getChildAt(0);
+ final int itemHeight = firstVisibleChild.getHeight();
+ final int currentPosition = recyclerView.getChildPosition(firstVisibleChild);
+ int distanceInPixels = Math.abs((currentPosition - position) * itemHeight);
+
+ if (distanceInPixels == 0) {
+ distanceInPixels = (int) Math.abs(firstVisibleChild.getY());
+ }
+
+ final SmoothScroller smoothScroller = new SmoothScroller(recyclerView.getContext(), distanceInPixels, mDuration);
+ smoothScroller.setTargetPosition(position);
+ startSmoothScroll(smoothScroller);
+ }
+
+ private class SmoothScroller extends LinearSmoothScroller {
+ private final float mDistanceInPixels;
+ private final float mDuration;
+
+ public SmoothScroller(Context context, int distanceInPixels, int duration) {
+ super(context);
+ this.mDistanceInPixels = distanceInPixels;
+ this.mDuration = duration;
+ }
+
+ @Override
+ protected int calculateTimeForScrolling(int distance) {
+ final float proportion = (float) distance / mDistanceInPixels;
+ return (int) (mDuration * proportion);
+ }
+
+ @Override
+ public PointF computeScrollVectorForPosition(int targetPosition) {
+ return SmoothScrollLinearLayoutManager.this.computeScrollVectorForPosition(targetPosition);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SummaryEditTextPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SummaryEditTextPreference.java
new file mode 100644
index 000000000..b3998ab24
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SummaryEditTextPreference.java
@@ -0,0 +1,211 @@
+package org.wordpress.android.ui.prefs;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.EditTextPreference;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AlertDialog;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.util.WPPrefUtils;
+
+/**
+ * Standard EditTextPreference that has attributes to limit summary length.
+ *
+ * Created for and used by {@link SiteSettingsFragment} to style some Preferences.
+ *
+ * When declaring this class in a layout file you can use the following attributes:
+ * - app:summaryLines : sets the number of lines to display in the Summary field
+ * (see {@link TextView#setLines(int)} for details)
+ * - app:maxSummaryLines : sets the maximum number of lines the Summary field can display
+ * (see {@link TextView#setMaxLines(int)} for details)
+ * - app:longClickHint : sets the string to be shown in a Toast when preference is long clicked
+ */
+
+public class SummaryEditTextPreference extends EditTextPreference implements PreferenceHint {
+ private int mLines;
+ private int mMaxLines;
+ private String mHint;
+ private AlertDialog mDialog;
+ private int mWhichButtonClicked;
+
+ public SummaryEditTextPreference(Context context) {
+ super(context);
+ }
+
+ public SummaryEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public SummaryEditTextPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mLines = -1;
+ mMaxLines = -1;
+
+ TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SummaryEditTextPreference);
+
+ for (int i = 0; i < array.getIndexCount(); ++i) {
+ int index = array.getIndex(i);
+ if (index == R.styleable.SummaryEditTextPreference_summaryLines) {
+ mLines = array.getInt(index, -1);
+ } else if (index == R.styleable.SummaryEditTextPreference_maxSummaryLines) {
+ mMaxLines = array.getInt(index, -1);
+ } else if (index == R.styleable.SummaryEditTextPreference_longClickHint) {
+ mHint = array.getString(index);
+ }
+ }
+
+ array.recycle();
+ }
+
+ @Override
+ protected void onBindView(@NonNull View view) {
+ super.onBindView(view);
+
+ TextView titleView = (TextView) view.findViewById(android.R.id.title);
+ TextView summaryView = (TextView) view.findViewById(android.R.id.summary);
+
+ if (titleView != null) WPPrefUtils.layoutAsSubhead(titleView);
+
+ if (summaryView != null) {
+ WPPrefUtils.layoutAsBody1(summaryView);
+ summaryView.setEllipsize(TextUtils.TruncateAt.END);
+ summaryView.setInputType(getEditText().getInputType());
+ if (mLines != -1) summaryView.setLines(mLines);
+ if (mMaxLines != -1) summaryView.setMaxLines(mMaxLines);
+ }
+ }
+
+ @Override
+ public Dialog getDialog() {
+ return mDialog;
+ }
+
+ @Override
+ protected void showDialog(Bundle state) {
+ Context context = getContext();
+ Resources res = context.getResources();
+ AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Calypso_AlertDialog);
+ View titleView = View.inflate(getContext(), R.layout.detail_list_preference_title, null);
+ mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;
+
+ builder.setPositiveButton(R.string.ok, this);
+ builder.setNegativeButton(res.getString(R.string.cancel).toUpperCase(), this);
+ if (titleView != null) {
+ TextView titleText = (TextView) titleView.findViewById(R.id.title);
+ if (titleText != null) {
+ titleText.setText(getTitle());
+ }
+
+ builder.setCustomTitle(titleView);
+ } else {
+ builder.setTitle(getTitle());
+ }
+
+ View view = View.inflate(getContext(), getDialogLayoutResource(), null);
+ if (view != null) {
+ onBindDialogView(view);
+ builder.setView(view);
+ }
+
+ if ((mDialog = builder.create()) == null) return;
+
+ if (state != null) {
+ mDialog.onRestoreInstanceState(state);
+ }
+ mDialog.setOnDismissListener(this);
+ mDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+ mDialog.show();
+
+ Button positive = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ Button negative = mDialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ if (positive != null) WPPrefUtils.layoutAsFlatButton(positive);
+ if (negative != null) WPPrefUtils.layoutAsFlatButton(negative);
+ }
+
+ @Override
+ protected void onBindDialogView(final View view) {
+ super.onBindDialogView(view);
+ if (view == null) return;
+
+ EditText editText = getEditText();
+ ViewParent oldParent = editText.getParent();
+ if (oldParent != view) {
+ if (oldParent != null && oldParent instanceof ViewGroup) {
+ ViewGroup groupParent = (ViewGroup) oldParent;
+ groupParent.removeView(editText);
+ groupParent.setPadding(groupParent.getPaddingLeft(), 0, groupParent.getPaddingRight(), groupParent.getPaddingBottom());
+ }
+ onAddEditTextToDialogView(view, editText);
+ }
+ WPPrefUtils.layoutAsInput(editText);
+ editText.setSelection(editText.getText().length());
+
+ TextView message = (TextView) view.findViewById(android.R.id.message);
+ WPPrefUtils.layoutAsDialogMessage(message);
+
+ // Dialog message has some extra bottom margin we don't want
+ ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) message.getLayoutParams();
+ int leftMargin = 0;
+ int bottomMargin = view.getResources().getDimensionPixelSize(R.dimen.margin_small);
+ // Different versions handle the message view's margin differently
+ // This is a small hack to try to make it align with the input for earlier versions
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
+ leftMargin = view.getResources().getDimensionPixelSize(R.dimen.margin_small);
+ }
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ leftMargin = view.getResources().getDimensionPixelSize(R.dimen.margin_large);
+ }
+ layoutParams.setMargins(leftMargin, layoutParams.topMargin, layoutParams.rightMargin, bottomMargin);
+ message.setLayoutParams(layoutParams);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mWhichButtonClicked = which;
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ mDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+ onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE);
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+ if (positiveResult) {
+ callChangeListener(getEditText().getText());
+ }
+ }
+
+ @Override
+ public boolean hasHint() {
+ return !TextUtils.isEmpty(mHint);
+ }
+
+ @Override
+ public String getHint() {
+ return mHint;
+ }
+
+ @Override
+ public void setHint(String hint) {
+ mHint = hint;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPPreference.java
new file mode 100644
index 000000000..47ca17b6d
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPPreference.java
@@ -0,0 +1,65 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.preference.Preference;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+
+public class WPPreference extends Preference implements PreferenceHint {
+ private String mHint;
+
+ public WPPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DetailListPreference);
+
+ for (int i = 0; i < array.getIndexCount(); ++i) {
+ int index = array.getIndex(i);
+ if (index == R.styleable.DetailListPreference_longClickHint) {
+ mHint = array.getString(index);
+ }
+ }
+
+ array.recycle();
+ }
+
+ @Override
+ protected void onBindView(@NonNull View view) {
+ super.onBindView(view);
+
+ Resources res = getContext().getResources();
+ TextView titleView = (TextView) view.findViewById(android.R.id.title);
+ TextView summaryView = (TextView) view.findViewById(android.R.id.summary);
+ if (titleView != null) {
+ titleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(R.dimen.text_sz_large));
+ titleView.setTextColor(res.getColor(isEnabled() ? R.color.grey_dark : R.color.grey_lighten_10));
+ }
+ if (summaryView != null) {
+ summaryView.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(R.dimen.text_sz_medium));
+ summaryView.setTextColor(res.getColor(isEnabled() ? R.color.grey_darken_10 : R.color.grey_lighten_10));
+ }
+ }
+
+ @Override
+ public boolean hasHint() {
+ return !TextUtils.isEmpty(mHint);
+ }
+
+ @Override
+ public String getHint() {
+ return mHint;
+ }
+
+ @Override
+ public void setHint(String hint) {
+ mHint = hint;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPStartOverPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPStartOverPreference.java
new file mode 100644
index 000000000..0fed50c24
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPStartOverPreference.java
@@ -0,0 +1,82 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.graphics.drawable.VectorDrawableCompat;
+import android.support.v4.content.ContextCompat;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.util.UrlUtils;
+
+/**
+ * Calypso-style Preference that has an icon and a widget in the correct place. If there is a button
+ * with id R.id.button, an onPreferenceClick listener is added.
+ */
+
+public class WPStartOverPreference extends WPPreference {
+ private String mButtonText;
+ private int mButtonTextColor;
+ private boolean mButtonTextAllCaps;
+ private Drawable mPrefIcon;
+
+ public WPStartOverPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.WPStartOverPreference);
+
+ for (int i = 0; i < array.getIndexCount(); ++i) {
+ int index = array.getIndex(i);
+ if (index == R.styleable.WPStartOverPreference_buttonText) {
+ mButtonText = array.getString(index);
+ } else if (index == R.styleable.WPStartOverPreference_buttonTextColor) {
+ mButtonTextColor = array.getColor(index, ContextCompat.getColor(context, R.color.black));
+ } else if (index == R.styleable.WPStartOverPreference_buttonTextAllCaps) {
+ mButtonTextAllCaps = array.getBoolean(index, false);
+ } else if (index == R.styleable.WPStartOverPreference_preficon) {
+ mPrefIcon = VectorDrawableCompat.create(context.getResources(), array.getResourceId(index, 0), null);
+ }
+ }
+
+ array.recycle();
+ }
+
+ @Override
+ protected void onBindView(@NonNull View view) {
+ super.onBindView(view);
+
+ if (view.findViewById(R.id.pref_icon) != null) {
+ ImageView imageView = (ImageView) view.findViewById(R.id.pref_icon);
+ imageView.setImageDrawable(mPrefIcon);
+ }
+
+ if (view.findViewById(R.id.button) != null) {
+ final WPStartOverPreference wpStartOverPreference = this;
+
+ Button button = (Button) view.findViewById(R.id.button);
+ button.setText(mButtonText);
+ button.setTextColor(mButtonTextColor);
+ button.setAllCaps(mButtonTextAllCaps);
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ getOnPreferenceClickListener().onPreferenceClick(wpStartOverPreference);
+ }
+ });
+ }
+
+ if (view.findViewById(R.id.domain) != null) {
+ TextView textView = (TextView) view.findViewById(R.id.domain);
+ Blog blog = WordPress.getCurrentBlog();
+ textView.setText(UrlUtils.getHost(blog.getHomeURL()));
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java
new file mode 100644
index 000000000..f895e21f7
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/WPSwitchPreference.java
@@ -0,0 +1,60 @@
+package org.wordpress.android.ui.prefs;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.preference.SwitchPreference;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+
+public class WPSwitchPreference extends SwitchPreference implements PreferenceHint {
+ private String mHint;
+
+ public WPSwitchPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SummaryEditTextPreference);
+
+ for (int i = 0; i < array.getIndexCount(); ++i) {
+ int index = array.getIndex(i);
+ if (index == R.styleable.SummaryEditTextPreference_longClickHint) {
+ mHint = array.getString(index);
+ }
+ }
+
+ array.recycle();
+ }
+
+ @Override
+ protected void onBindView(@NonNull View view) {
+ super.onBindView(view);
+
+ TextView titleView = (TextView) view.findViewById(android.R.id.title);
+ if (titleView != null) {
+ Resources res = getContext().getResources();
+ titleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimensionPixelSize(R.dimen.text_sz_large));
+ titleView.setTextColor(res.getColor(isEnabled() ? R.color.grey_dark : R.color.grey_lighten_10));
+ }
+ }
+
+ @Override
+ public boolean hasHint() {
+ return !TextUtils.isEmpty(mHint);
+ }
+
+ @Override
+ public String getHint() {
+ return mHint;
+ }
+
+ @Override
+ public void setHint(String hint) {
+ mHint = hint;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.java
new file mode 100644
index 000000000..a86a698bb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.java
@@ -0,0 +1,76 @@
+package org.wordpress.android.ui.prefs.notifications;
+
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.ui.notifications.NotificationEvents;
+
+import de.greenrobot.event.EventBus;
+
+// Simple wrapper activity for NotificationsSettingsFragment
+public class NotificationsSettingsActivity extends AppCompatActivity {
+ private View mMessageContainer;
+ private TextView mMessageTextView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ setContentView(R.layout.notifications_settings_activity);
+
+ setTitle(R.string.notification_settings);
+
+ FragmentManager fragmentManager = getFragmentManager();
+ if (savedInstanceState == null) {
+ fragmentManager.beginTransaction()
+ .add(R.id.fragment_container, new NotificationsSettingsFragment())
+ .commit();
+ }
+
+ mMessageContainer = findViewById(R.id.notifications_settings_message_container);
+ mMessageTextView = (TextView)findViewById(R.id.notifications_settings_message);
+ }
+
+ @Override
+ protected void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(NotificationEvents.NotificationsSettingsStatusChanged event) {
+ if (TextUtils.isEmpty(event.getMessage())) {
+ mMessageContainer.setVisibility(View.GONE);
+ } else {
+ mMessageContainer.setVisibility(View.VISIBLE);
+ mMessageTextView.setText(event.getMessage());
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsDialogPreference.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsDialogPreference.java
new file mode 100644
index 000000000..5a34316b6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsDialogPreference.java
@@ -0,0 +1,171 @@
+package org.wordpress.android.ui.prefs.notifications;
+
+import android.app.ActionBar;
+import android.content.Context;
+import android.preference.DialogPreference;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.models.NotificationsSettings;
+import org.wordpress.android.models.NotificationsSettings.Channel;
+import org.wordpress.android.models.NotificationsSettings.Type;
+import org.wordpress.android.ui.stats.ScrollViewExt;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.JSONUtils;
+
+import java.util.Iterator;
+
+// A dialog preference that displays settings for a NotificationSettings Channel and Type
+public class NotificationsSettingsDialogPreference extends DialogPreference {
+ private static final String SETTING_VALUE_ACHIEVEMENT = "achievement";
+
+ private NotificationsSettings.Channel mChannel;
+ private NotificationsSettings.Type mType;
+ private NotificationsSettings mSettings;
+ private JSONObject mUpdatedJson = new JSONObject();
+ private long mBlogId;
+
+ private OnNotificationsSettingsChangedListener mOnNotificationsSettingsChangedListener;
+
+ public interface OnNotificationsSettingsChangedListener {
+ void onSettingsChanged(Channel channel, Type type, long siteId, JSONObject newValues);
+ }
+
+ public NotificationsSettingsDialogPreference(Context context, AttributeSet attrs, Channel channel,
+ Type type, long blogId, NotificationsSettings settings,
+ OnNotificationsSettingsChangedListener listener) {
+ super(context, attrs);
+
+ mChannel = channel;
+ mType = type;
+ mBlogId = blogId;
+ mSettings = settings;
+ mOnNotificationsSettingsChangedListener = listener;
+ }
+
+ @Override
+ protected void onBindDialogView(@NonNull View view) {
+ super.onBindDialogView(view);
+ }
+
+ @Override
+ protected View onCreateDialogView() {
+
+ ScrollView outerView = new ScrollView(getContext());
+ outerView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
+
+ LinearLayout innerView = new LinearLayout(getContext());
+ innerView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT));
+ innerView.setOrientation(LinearLayout.VERTICAL);
+
+ View spacerView = new View(getContext());
+ int spacerHeight = getContext().getResources().getDimensionPixelSize(R.dimen.margin_medium);
+ spacerView.setLayoutParams(new ViewGroup.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT, spacerHeight));
+ innerView.addView(spacerView);
+
+ outerView.addView(innerView);
+ configureLayoutForView(innerView);
+
+ return outerView;
+ }
+
+ private View configureLayoutForView(LinearLayout view) {
+ JSONObject settingsJson = null;
+
+ String[] settingsArray = new String[0], settingsValues = new String[0], summaryArray = new String[0];
+ String typeString = mType.toString();
+
+ switch (mChannel) {
+ case BLOGS:
+ settingsJson = JSONUtils.queryJSON(mSettings.getBlogSettings().get(mBlogId),
+ typeString, new JSONObject());
+ settingsArray = getContext().getResources().getStringArray(R.array.notifications_blog_settings);
+ settingsValues = getContext().getResources().getStringArray(R.array.notifications_blog_settings_values);
+ break;
+ case OTHER:
+ settingsJson = JSONUtils.queryJSON(mSettings.getOtherSettings(),
+ typeString, new JSONObject());
+ settingsArray = getContext().getResources().getStringArray(R.array.notifications_other_settings);
+ settingsValues = getContext().getResources().getStringArray(R.array.notifications_other_settings_values);
+ break;
+ case DOTCOM:
+ settingsJson = mSettings.getDotcomSettings();
+ settingsArray = getContext().getResources().getStringArray(R.array.notifications_wpcom_settings);
+ settingsValues = getContext().getResources().getStringArray(R.array.notifications_wpcom_settings_values);
+ summaryArray = getContext().getResources().getStringArray(R.array.notifications_wpcom_settings_summaries);
+ break;
+ }
+
+ if (settingsJson != null && settingsArray.length == settingsValues.length) {
+ for (int i = 0; i < settingsArray.length; i++) {
+ String settingName = settingsArray[i];
+ String settingValue = settingsValues[i];
+
+ // Skip a few settings for 'Email' section
+ if (mType == Type.EMAIL && settingValue.equals(SETTING_VALUE_ACHIEVEMENT)) {
+ continue;
+ }
+
+ View commentsSetting = View.inflate(getContext(), R.layout.notifications_settings_switch, null);
+ TextView title = (TextView) commentsSetting.findViewById(R.id.notifications_switch_title);
+ title.setText(settingName);
+
+ // Add special summary text for the DOTCOM section
+ if (mChannel == Channel.DOTCOM && i < summaryArray.length) {
+ String summaryText = summaryArray[i];
+ TextView summary = (TextView) commentsSetting.findViewById(R.id.notifications_switch_summary);
+ summary.setVisibility(View.VISIBLE);
+ summary.setText(summaryText);
+ }
+
+ Switch toggleSwitch = (Switch) commentsSetting.findViewById(R.id.notifications_switch);
+ toggleSwitch.setChecked(JSONUtils.queryJSON(settingsJson, settingValue, true));
+ toggleSwitch.setTag(settingValue);
+ toggleSwitch.setOnCheckedChangeListener(mOnCheckedChangedListener);
+
+ view.addView(commentsSetting);
+ }
+ }
+
+ return view;
+ }
+
+ private CompoundButton.OnCheckedChangeListener mOnCheckedChangedListener = new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
+ try {
+ mUpdatedJson.put(compoundButton.getTag().toString(), isChecked);
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.NOTIFS, "Could not add notification setting change to JSONObject");
+ }
+ }
+ };
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ if (positiveResult && mUpdatedJson.length() > 0 && mOnNotificationsSettingsChangedListener != null) {
+ mOnNotificationsSettingsChangedListener.onSettingsChanged(mChannel, mType, mBlogId, mUpdatedJson);
+
+ // Update the settings json
+ Iterator<?> keys = mUpdatedJson.keys();
+ while( keys.hasNext() ) {
+ String settingName = (String)keys.next();
+ mSettings.updateSettingForChannelAndType(
+ mChannel, mType, settingName,
+ mUpdatedJson.optBoolean(settingName), mBlogId
+ );
+ }
+
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java
new file mode 100644
index 000000000..a0b47e7ac
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsFragment.java
@@ -0,0 +1,451 @@
+package org.wordpress.android.ui.prefs.notifications;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.widget.SearchView;
+import android.text.TextUtils;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.WordPressDB;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.models.NotificationsSettings;
+import org.wordpress.android.models.NotificationsSettings.Channel;
+import org.wordpress.android.models.NotificationsSettings.Type;
+import org.wordpress.android.ui.notifications.NotificationEvents;
+import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.MapUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.util.WPActivityUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+public class NotificationsSettingsFragment extends PreferenceFragment {
+
+ private static final String KEY_SEARCH_QUERY = "search_query";
+ private static final int SITE_SEARCH_VISIBILITY_COUNT = 15;
+ // The number of notification types we support (e.g. timeline, email, mobile)
+ private static final int TYPE_COUNT = 3;
+
+ private NotificationsSettings mNotificationsSettings;
+ private SearchView mSearchView;
+ private MenuItem mSearchMenuItem;
+
+ private String mDeviceId;
+ private String mRestoredQuery;
+ private boolean mNotificationsEnabled;
+ private int mSiteCount;
+
+ private final List<PreferenceCategory> mTypePreferenceCategories = new ArrayList<>();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ addPreferencesFromResource(R.xml.notifications_settings);
+ setHasOptionsMenu(true);
+
+ // Bump Analytics
+ if (savedInstanceState == null) {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_SETTINGS_LIST_OPENED);
+ }
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ mDeviceId = settings.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_SERVER_ID, "");
+
+ if (hasNotificationsSettings()) {
+ loadNotificationsAndUpdateUI(true);
+ }
+
+ if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SEARCH_QUERY)) {
+ mRestoredQuery = savedInstanceState.getString(KEY_SEARCH_QUERY);
+ }
+ }
+
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ mNotificationsEnabled = NotificationsUtils.isNotificationsEnabled(getActivity());
+
+ refreshSettings();
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.notifications_settings, menu);
+
+ mSearchMenuItem = menu.findItem(R.id.menu_notifications_settings_search);
+ mSearchView = (SearchView) MenuItemCompat.getActionView(mSearchMenuItem);
+ mSearchView.setQueryHint(getString(R.string.search_sites));
+
+ mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ configureBlogsSettings();
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ configureBlogsSettings();
+ return true;
+ }
+ });
+
+ updateSearchMenuVisibility();
+
+ // Check for a restored search query (if device was rotated, etc)
+ if (!TextUtils.isEmpty(mRestoredQuery)) {
+ mSearchMenuItem.expandActionView();
+ mSearchView.setQuery(mRestoredQuery, true);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (mSearchView != null && !TextUtils.isEmpty(mSearchView.getQuery())) {
+ outState.putString(KEY_SEARCH_QUERY, mSearchView.getQuery().toString());
+ }
+
+ super.onSaveInstanceState(outState);
+ }
+
+ private void refreshSettings() {
+ if (!hasNotificationsSettings()) {
+ EventBus.getDefault().post(new NotificationEvents.NotificationsSettingsStatusChanged(getString(R.string.loading)));
+ }
+
+ if (hasNotificationsSettings()) {
+ updateUIForNotificationsEnabledState();
+ }
+
+ NotificationsUtils.getPushNotificationSettings(getActivity(), new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ AppLog.d(T.NOTIFS, "Get settings action succeeded");
+ if (!isAdded()) return;
+
+ boolean settingsExisted = hasNotificationsSettings();
+ if (!settingsExisted) {
+ EventBus.getDefault().post(new NotificationEvents.NotificationsSettingsStatusChanged(null));
+ }
+
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ SharedPreferences.Editor editor = settings.edit();
+ editor.putString(NotificationsUtils.WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS, response.toString());
+ editor.apply();
+
+ loadNotificationsAndUpdateUI(!settingsExisted);
+ updateUIForNotificationsEnabledState();
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (!isAdded()) return;
+ AppLog.e(T.NOTIFS, "Get settings action failed", error);
+
+ if (!hasNotificationsSettings()) {
+ EventBus.getDefault().post(new NotificationEvents.NotificationsSettingsStatusChanged(getString(R.string.error_loading_notifications)));
+ }
+ }
+ });
+ }
+
+ private void loadNotificationsAndUpdateUI(boolean shouldUpdateUI) {
+ JSONObject settingsJson;
+ try {
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ settingsJson = new JSONObject(
+ sharedPreferences.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS, "")
+ );
+ } catch (JSONException e) {
+ AppLog.e(T.NOTIFS, "Could not parse notifications settings JSON");
+ return;
+ }
+
+ if (mNotificationsSettings == null) {
+ mNotificationsSettings = new NotificationsSettings(settingsJson);
+ } else {
+ mNotificationsSettings.updateJson(settingsJson);
+ }
+
+ if (shouldUpdateUI) {
+ configureBlogsSettings();
+ configureOtherSettings();
+ configureDotcomSettings();
+ }
+ }
+
+ private boolean hasNotificationsSettings() {
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
+
+ return sharedPreferences.contains(NotificationsUtils.WPCOM_PUSH_DEVICE_NOTIFICATION_SETTINGS);
+ }
+
+ // Updates the UI for preference screens based on if notifications are enabled or not
+ private void updateUIForNotificationsEnabledState() {
+ if (mTypePreferenceCategories == null || mTypePreferenceCategories.size() == 0) {
+ return;
+ }
+
+ for (final PreferenceCategory category : mTypePreferenceCategories) {
+ if (mNotificationsEnabled && category.getPreferenceCount() > TYPE_COUNT) {
+ category.removePreference(category.getPreference(TYPE_COUNT));
+ } else if (!mNotificationsEnabled && category.getPreferenceCount() == TYPE_COUNT) {
+ Preference disabledMessage = new Preference(getActivity());
+ disabledMessage.setSummary(R.string.notifications_disabled);
+ disabledMessage.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Intent intent = new Intent();
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ Uri uri = Uri.fromParts("package", getActivity().getApplicationContext().getPackageName(), null);
+ intent.setData(uri);
+
+ startActivity(intent);
+ return true;
+ }
+ });
+
+ category.addPreference(disabledMessage);
+ }
+
+ if (category.getPreferenceCount() >= TYPE_COUNT &&
+ category.getPreference(TYPE_COUNT - 1) != null) {
+ category.getPreference(TYPE_COUNT - 1).setEnabled(mNotificationsEnabled);
+ }
+ }
+
+ }
+
+ private void configureBlogsSettings() {
+ if (!isAdded()) return;
+ // Retrieve blogs (including jetpack sites) originally retrieved through FetchBlogListWPCom
+ // They will have an empty (but encrypted) password
+ String args = "password='" + WordPressDB.encryptPassword("") + "'";
+
+ // Check if user has typed in a search query
+ String trimmedQuery = null;
+ if (mSearchView != null && !TextUtils.isEmpty(mSearchView.getQuery())) {
+ trimmedQuery = mSearchView.getQuery().toString().trim();
+ args += " AND (url LIKE '%" + trimmedQuery + "%' OR blogName LIKE '%" + trimmedQuery + "%')";
+ }
+
+ List<Map<String, Object>> blogs = WordPress.wpDB.getBlogsBy(args, null, 0, false);
+ mSiteCount = blogs.size();
+
+ Context context = getActivity();
+
+ PreferenceCategory blogsCategory = (PreferenceCategory) findPreference(
+ getString(R.string.pref_notification_blogs));
+ blogsCategory.removeAll();
+
+ for (Map blog : blogs) {
+ if (context == null) return;
+
+ String siteUrl = MapUtils.getMapStr(blog, "url");
+ String title = MapUtils.getMapStr(blog, "blogName");
+ long blogId = MapUtils.getMapLong(blog, "blogId");
+
+ PreferenceScreen prefScreen = getPreferenceManager().createPreferenceScreen(context);
+ prefScreen.setTitle(title);
+ prefScreen.setSummary(UrlUtils.getHost(siteUrl));
+
+ addPreferencesForPreferenceScreen(prefScreen, Channel.BLOGS, blogId);
+ blogsCategory.addPreference(prefScreen);
+ }
+
+ // Add a message in a preference if there are no matching search results
+ if (mSiteCount == 0 && !TextUtils.isEmpty(trimmedQuery)) {
+ Preference searchResultsPref = new Preference(context);
+ searchResultsPref.setSummary(String.format(getString(R.string.notifications_no_search_results), trimmedQuery));
+ blogsCategory.addPreference(searchResultsPref);
+ }
+
+ updateSearchMenuVisibility();
+ }
+
+ private void updateSearchMenuVisibility() {
+ // Show the search menu item in the toolbar if we have enough sites
+ if (mSearchMenuItem != null) {
+ mSearchMenuItem.setVisible(mSiteCount > SITE_SEARCH_VISIBILITY_COUNT);
+ }
+ }
+
+ private void configureOtherSettings() {
+ PreferenceScreen otherBlogsScreen = (PreferenceScreen) findPreference(
+ getString(R.string.pref_notification_other_blogs));
+ addPreferencesForPreferenceScreen(otherBlogsScreen, Channel.OTHER, 0);
+ }
+
+ private void configureDotcomSettings() {
+ PreferenceCategory otherPreferenceCategory = (PreferenceCategory) findPreference(
+ getString(R.string.pref_notification_other_category));
+ NotificationsSettingsDialogPreference devicePreference = new NotificationsSettingsDialogPreference(
+ getActivity(), null, Channel.DOTCOM, NotificationsSettings.Type.DEVICE, 0, mNotificationsSettings, mOnSettingsChangedListener
+ );
+ devicePreference.setTitle(R.string.notifications_account_emails);
+ devicePreference.setDialogTitle(R.string.notifications_account_emails);
+ devicePreference.setSummary(R.string.notifications_account_emails_summary);
+ otherPreferenceCategory.addPreference(devicePreference);
+ }
+
+ private void addPreferencesForPreferenceScreen(PreferenceScreen preferenceScreen, Channel channel, long blogId) {
+ Context context = getActivity();
+ if (context == null) return;
+
+ PreferenceCategory rootCategory = new PreferenceCategory(context);
+ rootCategory.setTitle(R.string.notification_types);
+ preferenceScreen.addPreference(rootCategory);
+
+ NotificationsSettingsDialogPreference timelinePreference = new NotificationsSettingsDialogPreference(
+ context, null, channel, NotificationsSettings.Type.TIMELINE, blogId, mNotificationsSettings, mOnSettingsChangedListener
+ );
+ timelinePreference.setIcon(R.drawable.ic_bell_grey);
+ timelinePreference.setTitle(R.string.notifications_tab);
+ timelinePreference.setDialogTitle(R.string.notifications_tab);
+ timelinePreference.setSummary(R.string.notifications_tab_summary);
+ rootCategory.addPreference(timelinePreference);
+
+ NotificationsSettingsDialogPreference emailPreference = new NotificationsSettingsDialogPreference(
+ context, null, channel, NotificationsSettings.Type.EMAIL, blogId, mNotificationsSettings, mOnSettingsChangedListener
+ );
+ emailPreference.setIcon(R.drawable.ic_email_grey);
+ emailPreference.setTitle(R.string.email);
+ emailPreference.setDialogTitle(R.string.email);
+ emailPreference.setSummary(R.string.notifications_email_summary);
+ rootCategory.addPreference(emailPreference);
+
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
+ String deviceID = settings.getString(NotificationsUtils.WPCOM_PUSH_DEVICE_SERVER_ID, null);
+ if (!TextUtils.isEmpty(deviceID)) {
+ NotificationsSettingsDialogPreference devicePreference = new NotificationsSettingsDialogPreference(
+ context, null, channel, NotificationsSettings.Type.DEVICE, blogId, mNotificationsSettings, mOnSettingsChangedListener
+ );
+ devicePreference.setIcon(R.drawable.ic_phone_grey);
+ devicePreference.setTitle(R.string.app_notifications);
+ devicePreference.setDialogTitle(R.string.app_notifications);
+ devicePreference.setSummary(R.string.notifications_push_summary);
+ devicePreference.setEnabled(mNotificationsEnabled);
+ rootCategory.addPreference(devicePreference);
+ }
+
+ mTypePreferenceCategories.add(rootCategory);
+ }
+
+ private final NotificationsSettingsDialogPreference.OnNotificationsSettingsChangedListener mOnSettingsChangedListener =
+ new NotificationsSettingsDialogPreference.OnNotificationsSettingsChangedListener() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onSettingsChanged(Channel channel, NotificationsSettings.Type type, long blogId, JSONObject newValues) {
+ if (!isAdded()) return;
+
+ // Construct a new settings JSONObject to send back to WP.com
+ JSONObject settingsObject = new JSONObject();
+ switch (channel) {
+ case BLOGS:
+ try {
+ JSONObject blogObject = new JSONObject();
+ blogObject.put(NotificationsSettings.KEY_BLOG_ID, blogId);
+
+ JSONArray blogsArray = new JSONArray();
+ if (type == Type.DEVICE) {
+ newValues.put(NotificationsSettings.KEY_DEVICE_ID, Long.parseLong(mDeviceId));
+ JSONArray devicesArray = new JSONArray();
+ devicesArray.put(newValues);
+ blogObject.put(NotificationsSettings.KEY_DEVICES, devicesArray);
+ blogsArray.put(blogObject);
+ } else {
+ blogObject.put(type.toString(), newValues);
+ blogsArray.put(blogObject);
+ }
+
+ settingsObject.put(NotificationsSettings.KEY_BLOGS, blogsArray);
+ } catch (JSONException e) {
+ AppLog.e(T.NOTIFS, "Could not build notification settings object");
+ }
+ break;
+ case OTHER:
+ try {
+ JSONObject otherObject = new JSONObject();
+ if (type == Type.DEVICE) {
+ newValues.put(NotificationsSettings.KEY_DEVICE_ID, Long.parseLong(mDeviceId));
+ JSONArray devicesArray = new JSONArray();
+ devicesArray.put(newValues);
+ otherObject.put(NotificationsSettings.KEY_DEVICES, devicesArray);
+ } else {
+ otherObject.put(type.toString(), newValues);
+ }
+
+ settingsObject.put(NotificationsSettings.KEY_OTHER, otherObject);
+ } catch (JSONException e) {
+ AppLog.e(T.NOTIFS, "Could not build notification settings object");
+ }
+ break;
+ case DOTCOM:
+ try {
+ settingsObject.put(NotificationsSettings.KEY_DOTCOM, newValues);
+ } catch (JSONException e) {
+ AppLog.e(T.NOTIFS, "Could not build notification settings object");
+ }
+ break;
+ }
+
+ if (settingsObject.length() > 0) {
+ WordPress.getRestClientUtilsV1_1().post("/me/notifications/settings", settingsObject, null, null, null);
+ }
+ }
+ };
+
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, @NonNull Preference preference) {
+ super.onPreferenceTreeClick(preferenceScreen, preference);
+
+ if (preference instanceof PreferenceScreen) {
+ Dialog prefDialog = ((PreferenceScreen) preference).getDialog();
+ if (prefDialog != null) {
+ String title = String.valueOf(preference.getTitle());
+ WPActivityUtils.addToolbarToDialog(this, prefDialog, title);
+ }
+ AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_SETTINGS_STREAMS_OPENED);
+ } else {
+ AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_SETTINGS_DETAILS_OPENED);
+ }
+
+ return false;
+ }
+}