/* * Copyright (c) 2015 Ha Duy Trung * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.github.hidroh.materialistic; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.annotation.StyleRes; import android.support.annotation.VisibleForTesting; import android.support.v7.preference.ListPreference; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceManager; import android.text.TextUtils; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Set; import io.github.hidroh.materialistic.annotation.PublicApi; import io.github.hidroh.materialistic.annotation.Synthetic; import io.github.hidroh.materialistic.data.AlgoliaPopularClient; import io.github.hidroh.materialistic.preference.ThemePreference; @SuppressWarnings("WeakerAccess") @PublicApi public class Preferences { private static final String DRAFT_PREFIX = "draft_%1$s"; private static final String PREFERENCES_DRAFT = "_drafts"; @VisibleForTesting static Boolean sReleaseNotesSeen = null; public enum SwipeAction { None, Vote, Save, Refresh } public enum StoryViewMode { Comment, Article, Readability } private static final BoolToStringPref[] PREF_MIGRATION = new BoolToStringPref[]{ new BoolToStringPref(R.string.pref_item_click, false, R.string.pref_story_display, R.string.pref_story_display_value_comments), new BoolToStringPref(R.string.pref_item_search_recent, true, R.string.pref_search_sort, R.string.pref_search_sort_value_default) }; public static void sync(PreferenceManager preferenceManager) { Map<String, ?> map = preferenceManager.getSharedPreferences().getAll(); for (String key : map.keySet()) { sync(preferenceManager, key); } } private static void sync(PreferenceManager preferenceManager, String key) { Preference pref = preferenceManager.findPreference(key); if (pref instanceof ListPreference) { ListPreference listPref = (ListPreference) pref; pref.setSummary(listPref.getEntry()); } } /** * Migrate from boolean preferences to string preferences. Should be called only once * when application is relaunched. * If boolean preference has been set before, and value is not default, migrate to the new * corresponding string value * If boolean preference has been set before, but value is default, simply remove it * @param context application context * TODO remove once all users migrated */ public static void migrate(Context context) { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences.Editor editor = sp.edit(); for (BoolToStringPref pref : PREF_MIGRATION) { if (pref.isChanged(context, sp)) { editor.putString(context.getString(pref.newKey), context.getString(pref.newValue)); } if (pref.hasOldValue(context, sp)) { editor.remove(context.getString(pref.oldKey)); } } editor.apply(); } public static boolean isListItemCardView(Context context) { return get(context, R.string.pref_list_item_view, false); } public static boolean isSortByRecent(Context context) { return get(context, R.string.pref_search_sort, R.string.pref_search_sort_value_recent) .equals(context.getString(R.string.pref_search_sort_value_recent)); } public static void setSortByRecent(Context context, boolean byRecent) { set(context, R.string.pref_search_sort, context.getString(byRecent ? R.string.pref_search_sort_value_recent : R.string.pref_search_sort_value_default)); } public static StoryViewMode getDefaultStoryView(Context context) { String pref = get(context, R.string.pref_story_display, R.string.pref_story_display_value_article); if (TextUtils.equals(context.getString(R.string.pref_story_display_value_comments), pref)) { return StoryViewMode.Comment; } if (TextUtils.equals(context.getString(R.string.pref_story_display_value_readability), pref)) { return StoryViewMode.Readability; } return StoryViewMode.Article; } public static boolean externalBrowserEnabled(Context context) { return get(context, R.string.pref_external, false); } public static boolean colorCodeEnabled(Context context) { return get(context, R.string.pref_color_code, true); } public static boolean smoothScrollEnabled(Context context) { return get(context, R.string.pref_smooth_scroll, true); } public static boolean threadIndicatorEnabled(Context context) { return get(context, R.string.pref_thread_indicator, true); } public static boolean highlightUpdatedEnabled(Context context) { return get(context, R.string.pref_highlight_updated, true); } public static boolean autoMarkAsViewed(Context context) { return get(context, R.string.pref_auto_viewed, false); } public static boolean navigationEnabled(Context context) { return get(context, R.string.pref_navigation, false); } public static boolean navigationVibrationEnabled(Context context) { return get(context, R.string.pref_navigation_vibrate, true); } public static boolean customChromeTabEnabled(Context context) { return get(context, R.string.pref_custom_tab, true); } public static boolean isSinglePage(Context context, String displayOption) { return !TextUtils.equals(displayOption, context.getString(R.string.pref_comment_display_value_multiple)); } public static boolean isAutoExpand(Context context, String displayOption) { return TextUtils.equals(displayOption, context.getString(R.string.pref_comment_display_value_single)); } public static String getCommentDisplayOption(Context context) { return get(context, R.string.pref_comment_display, R.string.pref_comment_display_value_single); } public static void setPopularRange(Context context, @AlgoliaPopularClient.Range @NonNull String range) { set(context, R.string.pref_popular_range, range); } @NonNull public static String getPopularRange(Context context) { return get(context, R.string.pref_popular_range, AlgoliaPopularClient.LAST_24H); } public static int getCommentMaxLines(Context context) { String maxLinesString = get(context, R.string.pref_max_lines, null); int maxLines = maxLinesString == null ? -1 : Integer.parseInt(maxLinesString); if (maxLines < 0) { maxLines = Integer.MAX_VALUE; } return maxLines; } public static float getLineHeight(Context context) { return getFloatFromString(context, R.string.pref_line_height, 1.0f); } public static float getReadabilityLineHeight(Context context) { return getFloatFromString(context, R.string.pref_readability_line_height, 1.0f); } public static boolean shouldLazyLoad(Context context) { return get(context, R.string.pref_lazy_load, true); } public static String getUsername(Context context) { return get(context, R.string.pref_username, null); } public static void setUsername(Context context, String username) { set(context, R.string.pref_username, username); } @NonNull public static String getLaunchScreen(Context context) { return get(context, R.string.pref_launch_screen, R.string.pref_launch_screen_value_top); } public static boolean isLaunchScreenLast(Context context) { return TextUtils.equals(context.getString(R.string.pref_launch_screen_value_last), getLaunchScreen(context)); } public static boolean adBlockEnabled(Context context) { return get(context, R.string.pref_ad_block, true); } public static void saveDraft(Context context, String parentId, String draft) { context.getSharedPreferences(context.getPackageName() + PREFERENCES_DRAFT, Context.MODE_PRIVATE) .edit() .putString(String.format(Locale.US, DRAFT_PREFIX, parentId), draft) .apply(); } public static String getDraft(Context context, String parentId) { return context .getSharedPreferences(context.getPackageName() + PREFERENCES_DRAFT, Context.MODE_PRIVATE) .getString(String.format(Locale.US, DRAFT_PREFIX, parentId), null); } public static void deleteDraft(Context context, String parentId) { context.getSharedPreferences(context.getPackageName() + PREFERENCES_DRAFT, Context.MODE_PRIVATE) .edit() .remove(String.format(Locale.US, DRAFT_PREFIX, parentId)) .apply(); } public static void clearDrafts(Context context) { context.getSharedPreferences(context.getPackageName() + PREFERENCES_DRAFT, Context.MODE_PRIVATE) .edit() .clear() .apply(); } public static boolean isReleaseNotesSeen(Context context) { if (sReleaseNotesSeen == null) { PackageInfo info = null; try { info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); } catch (PackageManager.NameNotFoundException e) { // no op } // considered seen if first time install or last seen release is up to date if (info != null && info.firstInstallTime == info.lastUpdateTime) { setReleaseNotesSeen(context); } else { sReleaseNotesSeen = getInt(context, R.string.pref_latest_release, 0) >= BuildConfig.LATEST_RELEASE; } } return sReleaseNotesSeen; } public static void setReleaseNotesSeen(Context context) { sReleaseNotesSeen = true; setInt(context, R.string.pref_latest_release, BuildConfig.LATEST_RELEASE); } public static boolean multiWindowEnabled(Context context) { return !TextUtils.equals(context.getString(R.string.pref_multi_window_value_none), get(context, R.string.pref_multi_window, R.string.pref_multi_window_value_none)); } public static SwipeAction[] getListSwipePreferences(Context context) { String left = get(context, R.string.pref_list_swipe_left, R.string.swipe_save), right = get(context, R.string.pref_list_swipe_right, R.string.swipe_vote); return new SwipeAction[]{parseSwipeAction(left), parseSwipeAction(right)}; } public static void reset(Context context) { PreferenceManager.getDefaultSharedPreferences(context) .edit() .clear() .apply(); } private static SwipeAction parseSwipeAction(String value) { try { return SwipeAction.valueOf(value); } catch (IllegalArgumentException | NullPointerException e) { return SwipeAction.None; } } @Synthetic static boolean get(Context context, @StringRes int key, boolean defaultValue) { return PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(context.getString(key), defaultValue); } private static int getInt(Context context, @StringRes int key, int defaultValue) { return PreferenceManager.getDefaultSharedPreferences(context) .getInt(context.getString(key), defaultValue); } private static float getFloatFromString(Context context, @StringRes int key, float defaultValue) { String floatValue = get(context, key, null); try { return Float.parseFloat(floatValue); } catch (NumberFormatException | NullPointerException e) { return defaultValue; } } @Synthetic static String get(Context context, @StringRes int key, String defaultValue) { return get(context, context.getString(key), defaultValue); } private static String get(Context context, @StringRes int key, @StringRes int defaultValue) { return PreferenceManager.getDefaultSharedPreferences(context) .getString(context.getString(key), context.getString(defaultValue)); } private static String get(Context context, String key, String defaultValue) { return PreferenceManager.getDefaultSharedPreferences(context) .getString(key, defaultValue); } private static void setInt(Context context, @StringRes int key, int value) { PreferenceManager.getDefaultSharedPreferences(context) .edit() .putInt(context.getString(key), value) .apply(); } private static void set(Context context, @StringRes int key, String value) { set(context, context.getString(key), value); } private static void set(Context context, String key, String value) { PreferenceManager.getDefaultSharedPreferences(context) .edit() .putString(key, value) .apply(); } public static class BoolToStringPref { @Synthetic final int oldKey; private final boolean oldDefault; @Synthetic final int newKey; @Synthetic final int newValue; @Synthetic BoolToStringPref(@StringRes int oldKey, boolean oldDefault, @StringRes int newKey, @StringRes int newValue) { this.oldKey = oldKey; this.oldDefault = oldDefault; this.newKey = newKey; this.newValue = newValue; } @Synthetic boolean isChanged(Context context, SharedPreferences sp) { return hasOldValue(context, sp) && sp.getBoolean(context.getString(oldKey), oldDefault) != oldDefault; } @Synthetic boolean hasOldValue(Context context, SharedPreferences sp) { return sp.contains(context.getString(oldKey)); } } @PublicApi public static class Theme { public static void apply(Context context, boolean dialogTheme, boolean isTranslucent) { ThemePreference.ThemeSpec themeSpec = getTheme(context, isTranslucent); context.setTheme(themeSpec.theme); if (themeSpec.themeOverrides >= 0) { context.getTheme().applyStyle(themeSpec.themeOverrides, true); } if (dialogTheme) { context.setTheme(AppUtils.getThemedResId(context, R.attr.alertDialogTheme)); } } static @Nullable String getTypeface(Context context) { return get(context, R.string.pref_font, null); } static @Nullable String getReadabilityTypeface(Context context) { String typefaceName = get(context, R.string.pref_readability_font, null); if (TextUtils.isEmpty(typefaceName)) { return getTypeface(context); } return typefaceName; } public static @StyleRes int resolveTextSize(String choice) { switch (Integer.parseInt(choice)) { case -1: return R.style.AppTextSize_XSmall; case 0: default: return R.style.AppTextSize; case 1: return R.style.AppTextSize_Medium; case 2: return R.style.AppTextSize_Large; case 3: return R.style.AppTextSize_XLarge; } } public static @StyleRes int resolvePreferredTextSize(Context context) { return resolveTextSize(getPreferredTextSize(context)); } static @StyleRes int resolvePreferredReadabilityTextSize(Context context) { return resolveTextSize(getPreferredReadabilityTextSize(context)); } private static @NonNull String getPreferredReadabilityTextSize(Context context) { String choice = get(context, R.string.pref_readability_text_size, null); if (TextUtils.isEmpty(choice)) { return getPreferredTextSize(context); } return choice; } private static @NonNull String getPreferredTextSize(Context context) { return get(context, R.string.pref_text_size, String.valueOf(0)); } private static ThemePreference.ThemeSpec getTheme(Context context, boolean isTransulcent) { return ThemePreference.getTheme(get(context, R.string.pref_theme, null), isTransulcent); } } @PublicApi public static class Offline { public static boolean isEnabled(Context context) { return get(context, R.string.pref_saved_item_sync, false); } public static boolean isCommentsEnabled(Context context) { return isEnabled(context) && get(context, R.string.pref_offline_comments, true); } public static boolean isArticleEnabled(Context context) { return isEnabled(context) && get(context, R.string.pref_offline_article, true); } public static boolean isReadabilityEnabled(Context context) { return isEnabled(context) && get(context, R.string.pref_offline_readability, true); } public static boolean currentConnectionEnabled(Context context) { return !isWifiOnly(context) || AppUtils.isOnWiFi(context); } public static boolean isNotificationEnabled(Context context) { return get(context, R.string.pref_offline_notification, false); } public static boolean isWifiOnly(Context context) { String wifiValue = context.getString(R.string.offline_data_wifi); return TextUtils.equals(wifiValue, get(context, R.string.pref_offline_data, wifiValue)); } } public static class Observable { private static Set<String> CONTEXT_KEYS; private final Map<String, Integer> mSubscribedKeys = new HashMap<>(); private final SharedPreferences.OnSharedPreferenceChangeListener mListener = (sharedPreferences, key) -> { if (mSubscribedKeys.containsKey(key)) { notifyChanged(mSubscribedKeys.get(key), CONTEXT_KEYS.contains(key)); } }; private Observer mObserver; public void subscribe(Context context, @NonNull Observer observer, @NonNull int... preferenceKeys) { ensureContextKeys(context); setSubscription(context, preferenceKeys); mObserver = observer; PreferenceManager.getDefaultSharedPreferences(context) .registerOnSharedPreferenceChangeListener(mListener); } public void unsubscribe(Context context) { PreferenceManager.getDefaultSharedPreferences(context) .unregisterOnSharedPreferenceChangeListener(mListener); } private void setSubscription(Context context, int[] preferenceKeys) { mSubscribedKeys.clear(); for (int key : preferenceKeys) { mSubscribedKeys.put(context.getString(key), key); } } private void notifyChanged(int key, boolean contextChanged) { if (mObserver != null) { mObserver.onPreferenceChanged(key, contextChanged); } } @SuppressLint("UseSparseArrays") private void ensureContextKeys(Context context) { if (CONTEXT_KEYS != null) { return; } CONTEXT_KEYS = new HashSet<>(); CONTEXT_KEYS.add(context.getString(R.string.pref_theme)); CONTEXT_KEYS.add(context.getString(R.string.pref_text_size)); CONTEXT_KEYS.add(context.getString(R.string.pref_font)); } } public interface Observer { void onPreferenceChanged(@StringRes int key, boolean contextChanged); } }