diff options
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/prefs')
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; + } +} |