+ * Furthermore, the preferences shown will follow the visual style of system + * preferences. It is easy to create a hierarchy of preferences (that can be + * shown on multiple screens) via XML. For these reasons, it is recommended to + * use this fragment (as a superclass) to deal with preferences in applications. + *
+ * A {@link PreferenceScreen} object should be at the top of the preference + * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy + * denote a screen break--that is the preferences contained within subsequent + * {@link PreferenceScreen} should be shown on another screen. The preference + * framework handles this by calling {@link #onNavigateToScreen(PreferenceScreen)}. + *
+ * The preference hierarchy can be formed in multiple ways: + *
+ * To inflate from XML, use the {@link #addPreferencesFromResource(int)}. The + * root element should be a {@link PreferenceScreen}. Subsequent elements can point + * to actual {@link Preference} subclasses. As mentioned above, subsequent + * {@link PreferenceScreen} in the hierarchy will result in the screen break. + *
+ * To specify an object hierarchy rooted with {@link PreferenceScreen}, use + * {@link #setPreferenceScreen(PreferenceScreen)}. + *
+ * As a convenience, this fragment implements a click listener for any + * preference in the current hierarchy, see + * {@link #onPreferenceTreeClick(Preference)}. + * + *
For information about using {@code PreferenceFragment}, + * read the Settings + * guide.
+ *The following sample code shows a simple preference fragment that is + * populated from a resource. The resource it loads is:
+ * + * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml preferences} + * + *The fragment implementation itself simply populates the preferences + * when created. Note that the preferences framework takes care of loading + * the current values out of the app preferences and writing them when changed:
+ * + * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/java/com/example/android/supportpreference/FragmentSupportPreferencesCompat.java + * support_fragment_compat} + * + * @see Preference + * @see PreferenceScreen + */ +@SuppressWarnings({"WeakerAccess", "unused", "HandlerLeak", "JavaDoc", "RestrictedApi"}) +public abstract class PreferenceController extends RestoreViewOnCreateController implements + PreferenceManager.OnDisplayPreferenceDialogListener, + DialogPreference.TargetFragment { + + /** + * Fragment argument used to specify the tag of the desired root + * {@link androidx.preference.PreferenceScreen} object. + */ + public static final String ARG_PREFERENCE_ROOT = + "androidx.preference.PreferenceFragmentCompat.PREFERENCE_ROOT"; + + private static final String PREFERENCES_TAG = "android:preferences"; + + private static final String DIALOG_FRAGMENT_TAG = + "androidx.preference.PreferenceFragment.DIALOG"; + + private PreferenceManager mPreferenceManager; + RecyclerView mList; + private boolean mHavePrefs; + private boolean mInitDone; + + private Context mStyledContext; + + private int mLayoutResId = R.layout.preference_list_fragment; + + private DividerDecoration mDividerDecoration = null; + + private static final int MSG_BIND_PREFERENCES = 1; + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + + case MSG_BIND_PREFERENCES: + bindPreferences(); + break; + } + } + }; + + final private Runnable mRequestFocus = new Runnable() { + @Override + public void run() { + mList.focusableViewAvailable(mList); + } + }; + + private Runnable mSelectPreferenceRunnable; + + /** + * Interface that PreferenceFragment's containing activity should + * implement to be able to process preference items that wish to + * switch to a specified fragment. + */ + public interface OnPreferenceStartFragmentCallback { + /** + * Called when the user has clicked on a Preference that has + * a fragment class name associated with it. The implementation + * should instantiate and switch to an instance of the given + * fragment. + * @param caller The fragment requesting navigation. + * @param pref The preference requesting the fragment. + * @return true if the fragment creation has been handled + */ + boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref); + } + + /** + * Interface that PreferenceFragment's containing activity should + * implement to be able to process preference items that wish to + * switch to a new screen of preferences. + */ + public interface OnPreferenceStartScreenCallback { + /** + * Called when the user has clicked on a PreferenceScreen item in order to navigate to a new + * screen of preferences. + * @param caller The fragment requesting navigation. + * @param pref The preference screen to navigate to. + * @return true if the screen navigation has been handled + */ + boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref); + } + + public interface OnPreferenceDisplayDialogCallback { + + /** + * @param caller The fragment containing the preference requesting the dialog. + * @param pref The preference requesting the dialog. + * @return true if the dialog creation has been handled. + */ + boolean onPreferenceDisplayDialog(PreferenceController caller, Preference pref); + } + + /** + * Convenience constructor for use when no arguments are needed. + */ + public PreferenceController() { + super(null); + } + + /** + * Constructor that takes arguments that need to be retained across restarts. + * + * @param args Any arguments that need to be retained. + */ + public PreferenceController(@Nullable Bundle args) { + super(args); + } + + /** + * Called during {@link #onCreate(Bundle)} to supply the preferences for this fragment. + * Subclasses are expected to call {@link #setPreferenceScreen(PreferenceScreen)} either + * directly or via helper methods such as {@link #addPreferencesFromResource(int)}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + * @param rootKey If non-null, this preference fragment should be rooted at the + * {@link androidx.preference.PreferenceScreen} with this key. + */ + public abstract void onCreatePreferences(Bundle savedInstanceState, String rootKey); + + @Override + @NonNull + public View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, + @Nullable Bundle savedInstanceState) { + mInitDone = false; + mHavePrefs = false; + + final TypedValue tv = new TypedValue(); + getActivity().getTheme().resolveAttribute(R.attr.preferenceTheme, tv, true); + int theme = tv.resourceId; + if (theme == 0) { + // Fallback to default theme. + theme = R.style.PreferenceThemeOverlay; + } + mStyledContext = new ContextThemeWrapper(getActivity(), theme); + + mPreferenceManager = new PreferenceManager(mStyledContext); + final String rootKey = getArgs().getString(ARG_PREFERENCE_ROOT); + onCreatePreferences(savedInstanceState, rootKey); + + TypedArray a = mStyledContext.obtainStyledAttributes(null, + R.styleable.PreferenceFragmentCompat, + R.attr.preferenceFragmentCompatStyle, + 0); + + mLayoutResId = a.getResourceId(R.styleable.PreferenceFragmentCompat_android_layout, + mLayoutResId); + + mDividerDecoration = new DividerDecoration(); + final Drawable divider = a.getDrawable( + R.styleable.PreferenceFragmentCompat_android_divider); + final int dividerHeight = a.getDimensionPixelSize( + R.styleable.PreferenceFragmentCompat_android_dividerHeight, -1); + final boolean allowDividerAfterLastItem = a.getBoolean( + R.styleable.PreferenceFragmentCompat_allowDividerAfterLastItem, true); + + a.recycle(); + + final LayoutInflater themedInflater = inflater.cloneInContext(mStyledContext); + + final View view = themedInflater.inflate(mLayoutResId, container, false); + + final View rawListContainer = view.findViewById(AndroidResources.ANDROID_R_LIST_CONTAINER); + if (!(rawListContainer instanceof ViewGroup)) { + throw new RuntimeException("Content has view with id attribute " + + "'android.R.id.list_container' that is not a ViewGroup class"); + } + + final ViewGroup listContainer = (ViewGroup) rawListContainer; + + final RecyclerView listView = onCreateRecyclerView(themedInflater, listContainer, + savedInstanceState); + if (listView == null) { + throw new RuntimeException("Could not create RecyclerView"); + } + + mList = listView; + + listView.addItemDecoration(mDividerDecoration); + setDivider(divider); + if (dividerHeight != -1) { + setDividerHeight(dividerHeight); + } + mDividerDecoration.setAllowDividerAfterLastItem(allowDividerAfterLastItem); + + // If mList isn't present in the view hierarchy, add it. mList is automatically inflated + // on an Auto device so don't need to add it. + if (mList.getParent() == null) { + listContainer.addView(mList); + } + mHandler.post(mRequestFocus); + + onViewCreated(view, savedInstanceState); + + return view; + } + + /** + * Sets the drawable that will be drawn between each item in the list. + *+ * Note: If the drawable does not have an intrinsic + * height, you should also call {@link #setDividerHeight(int)}. + * + * @param divider the drawable to use + * @attr ref R.styleable#PreferenceFragmentCompat_android_divider + */ + public void setDivider(Drawable divider) { + if (mDividerDecoration != null) { + mDividerDecoration.setDivider(divider); + } + } + + /** + * Sets the height of the divider that will be drawn between each item in the list. Calling + * this will override the intrinsic height as set by {@link #setDivider(Drawable)} + * + * @param height The new height of the divider in pixels. + * @attr ref R.styleable#PreferenceFragmentCompat_android_dividerHeight + */ + public void setDividerHeight(int height) { + if (mDividerDecoration != null) { + mDividerDecoration.setDividerHeight(height); + } + } + + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + if (savedInstanceState != null) { + Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG); + if (container != null) { + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + preferenceScreen.restoreHierarchyState(container); + } + } + } + + if (mHavePrefs) { + bindPreferences(); + if (mSelectPreferenceRunnable != null) { + mSelectPreferenceRunnable.run(); + mSelectPreferenceRunnable = null; + } + } + + mInitDone = true; + } + + @Override + public void onAttach(@NonNull View view) { + super.onAttach(view); + mPreferenceManager.setOnDisplayPreferenceDialogListener(this); + } + + @Override + public void onDetach(@NonNull View view) { + super.onDetach(view); + mPreferenceManager.setOnDisplayPreferenceDialogListener(null); + } + + @Override + public void onDestroyView(@NonNull View view) { + mHandler.removeCallbacks(mRequestFocus); + mHandler.removeMessages(MSG_BIND_PREFERENCES); + if (mHavePrefs) { + unbindPreferences(); + } + mList = null; + mPreferenceManager = null; + mStyledContext = null; + mDividerDecoration = null; + + super.onDestroyView(view); + } + + @Override + protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) { + super.onSaveViewState(view, outState); + + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + Bundle container = new Bundle(); + preferenceScreen.saveHierarchyState(container); + outState.putBundle(PREFERENCES_TAG, container); + } + } + + /** + * Returns the {@link PreferenceManager} used by this fragment. + * @return The {@link PreferenceManager}. + */ + public PreferenceManager getPreferenceManager() { + return mPreferenceManager; + } + + /** + * Sets the root of the preference hierarchy that this fragment is showing. + * + * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. + */ + public void setPreferenceScreen(PreferenceScreen preferenceScreen) { + if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) { + onUnbindPreferences(); + mHavePrefs = true; + if (mInitDone) { + postBindPreferences(); + } + } + } + + /** + * Gets the root of the preference hierarchy that this fragment is showing. + * + * @return The {@link PreferenceScreen} that is the root of the preference + * hierarchy. + */ + public PreferenceScreen getPreferenceScreen() { + return mPreferenceManager.getPreferenceScreen(); + } + + /** + * Inflates the given XML resource and adds the preference hierarchy to the current + * preference hierarchy. + * + * @param preferencesResId The XML resource ID to inflate. + */ + public void addPreferencesFromResource(@XmlRes int preferencesResId) { + requirePreferenceManager(); + + setPreferenceScreen(mPreferenceManager.inflateFromResource(mStyledContext, + preferencesResId, getPreferenceScreen())); + } + + /** + * Inflates the given XML resource and replaces the current preference hierarchy (if any) with + * the preference hierarchy rooted at {@code key}. + * + * @param preferencesResId The XML resource ID to inflate. + * @param key The preference key of the {@link androidx.preference.PreferenceScreen} + * to use as the root of the preference hierarchy, or null to use the root + * {@link androidx.preference.PreferenceScreen}. + */ + public void setPreferencesFromResource(@XmlRes int preferencesResId, @Nullable String key) { + requirePreferenceManager(); + + final PreferenceScreen xmlRoot = mPreferenceManager.inflateFromResource(mStyledContext, + preferencesResId, null); + + final Preference root; + if (key != null) { + root = xmlRoot.findPreference(key); + if (!(root instanceof PreferenceScreen)) { + throw new IllegalArgumentException("Preference object with key " + key + + " is not a PreferenceScreen"); + } + } else { + root = xmlRoot; + } + + setPreferenceScreen((PreferenceScreen) root); + } + + /** + * Finds a {@link Preference} based on its key. + * + * @param key The key of the preference to retrieve. + * @return The {@link Preference} with the key, or null. + * @see androidx.preference.PreferenceGroup#findPreference(CharSequence) + */ + @Override + public Preference findPreference(CharSequence key) { + if (mPreferenceManager == null) { + return null; + } + return mPreferenceManager.findPreference(key); + } + + private void requirePreferenceManager() { + if (mPreferenceManager == null) { + throw new RuntimeException("This should be called after super.onCreate."); + } + } + + private void postBindPreferences() { + if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return; + mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); + } + + private void bindPreferences() { + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + getListView().setAdapter(onCreateAdapter(preferenceScreen)); + preferenceScreen.onAttached(); + } + onBindPreferences(); + } + + private void unbindPreferences() { + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + preferenceScreen.onDetached(); + } + onUnbindPreferences(); + } + + /** @hide */ + @RestrictTo(LIBRARY_GROUP) + protected void onBindPreferences() { + } + + /** @hide */ + @RestrictTo(LIBRARY_GROUP) + protected void onUnbindPreferences() { + } + + public final RecyclerView getListView() { + return mList; + } + + /** + * Creates the {@link RecyclerView} used to display the preferences. + * Subclasses may override this to return a customized + * {@link RecyclerView}. + * @param inflater The LayoutInflater object that can be used to inflate the + * {@link RecyclerView}. + * @param parent The parent {@link android.view.View} that the RecyclerView will be attached to. + * This method should not add the view itself, but this can be used to generate + * the LayoutParams of the view. + * @param savedInstanceState If non-null, this view is being re-constructed from a previous + * saved state as given here + * @return A new RecyclerView object to be placed into the view hierarchy + */ + public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, + Bundle savedInstanceState) { + // If device detected is Auto, use Auto's custom layout that contains a custom ViewGroup + // wrapping a RecyclerView + if (mStyledContext.getPackageManager().hasSystemFeature(PackageManager + .FEATURE_AUTOMOTIVE)) { + RecyclerView recyclerView = parent.findViewById(R.id.recycler_view); + if (recyclerView != null) { + return recyclerView; + } + } + RecyclerView recyclerView = (RecyclerView) inflater + .inflate(R.layout.preference_recyclerview, parent, false); + + recyclerView.setLayoutManager(onCreateLayoutManager()); + recyclerView.setAccessibilityDelegateCompat( + new PreferenceRecyclerViewAccessibilityDelegate(recyclerView)); + + return recyclerView; + } + + /** + * Called from {@link #onCreateRecyclerView} to create the + * {@link RecyclerView.LayoutManager} for the created + * {@link RecyclerView}. + * @return A new {@link RecyclerView.LayoutManager} instance. + */ + public RecyclerView.LayoutManager onCreateLayoutManager() { + return new LinearLayoutManager(getActivity()); + } + + /** + * Creates the root adapter. + * + * @param preferenceScreen Preference screen object to create the adapter for. + * @return An adapter that contains the preferences contained in this {@link PreferenceScreen}. + */ + protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) { + return new PreferenceGroupAdapter(preferenceScreen); + } + + /** + * Called when a preference in the tree requests to display a dialog. Subclasses should + * override this method to display custom dialogs or to handle dialogs for custom preference + * classes. + * + * @param preference The Preference object requesting the dialog. + */ + @Override + public void onDisplayPreferenceDialog(Preference preference) { + boolean handled = false; + if (getCallbackFragment() instanceof OnPreferenceDisplayDialogCallback) { + handled = ((OnPreferenceDisplayDialogCallback) getCallbackFragment()) + .onPreferenceDisplayDialog(this, preference); + } + if (!handled && getActivity() instanceof OnPreferenceDisplayDialogCallback) { + handled = ((OnPreferenceDisplayDialogCallback) getActivity()) + .onPreferenceDisplayDialog(this, preference); + } + + if (handled) { + return; + } + + // check if dialog is already showing + if (getRouter().getControllerWithTag(DIALOG_FRAGMENT_TAG) != null) { + return; + } + + final PreferenceDialogController f; + if (preference instanceof EditTextPreference) { + f = EditTextPreferenceDialogController.newInstance(preference.getKey()); + } else if (preference instanceof ListPreference) { + f = ListPreferenceDialogController.newInstance(preference.getKey()); + } else if (preference instanceof MultiSelectListPreference) { + f = MultiSelectListPreferenceDialogController.newInstance(preference.getKey()); + } else { + throw new IllegalArgumentException("Tried to display dialog for unknown " + + "preference type. Did you forget to override onDisplayPreferenceDialog()?"); + } + f.setTargetController(this); + f.showDialog(getRouter(), DIALOG_FRAGMENT_TAG); + } + + /** + * Basically a wrapper for getParentFragment which is v17+. Used by the leanback preference lib. + * @return Fragment to possibly use as a callback + * @hide + */ + @RestrictTo(LIBRARY_GROUP) + public Fragment getCallbackFragment() { + return null; + } + + public void scrollToPreference(final String key) { + scrollToPreferenceInternal(null, key); + } + + public void scrollToPreference(final Preference preference) { + scrollToPreferenceInternal(preference, null); + } + + private void scrollToPreferenceInternal(final Preference preference, final String key) { + final Runnable r = new Runnable() { + @Override + public void run() { + final RecyclerView.Adapter adapter = mList.getAdapter(); + if (!(adapter instanceof + PreferenceGroup.PreferencePositionCallback)) { + if (adapter != null) { + throw new IllegalStateException("Adapter must implement " + + "PreferencePositionCallback"); + } else { + // Adapter was set to null, so don't scroll I guess? + return; + } + } + final int position; + if (preference != null) { + position = ((PreferenceGroup.PreferencePositionCallback) adapter) + .getPreferenceAdapterPosition(preference); + } else { + position = ((PreferenceGroup.PreferencePositionCallback) adapter) + .getPreferenceAdapterPosition(key); + } + if (position != RecyclerView.NO_POSITION) { + mList.scrollToPosition(position); + } else { + // Item not found, wait for an update and try again + adapter.registerAdapterDataObserver( + new ScrollToPreferenceObserver(adapter, mList, preference, key)); + } + } + }; + if (mList == null) { + mSelectPreferenceRunnable = r; + } else { + r.run(); + } + } + + private static class ScrollToPreferenceObserver extends RecyclerView.AdapterDataObserver { + private final RecyclerView.Adapter mAdapter; + private final RecyclerView mList; + private final Preference mPreference; + private final String mKey; + + public ScrollToPreferenceObserver(RecyclerView.Adapter adapter, RecyclerView list, + Preference preference, String key) { + mAdapter = adapter; + mList = list; + mPreference = preference; + mKey = key; + } + + private void scrollToPreference() { + mAdapter.unregisterAdapterDataObserver(this); + final int position; + if (mPreference != null) { + position = ((PreferenceGroup.PreferencePositionCallback) mAdapter) + .getPreferenceAdapterPosition(mPreference); + } else { + position = ((PreferenceGroup.PreferencePositionCallback) mAdapter) + .getPreferenceAdapterPosition(mKey); + } + if (position != RecyclerView.NO_POSITION) { + mList.scrollToPosition(position); + } + } + + @Override + public void onChanged() { + scrollToPreference(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + scrollToPreference(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + scrollToPreference(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + scrollToPreference(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + scrollToPreference(); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + scrollToPreference(); + } + } + + private class DividerDecoration extends RecyclerView.ItemDecoration { + + private Drawable mDivider; + private int mDividerHeight; + private boolean mAllowDividerAfterLastItem = true; + + DividerDecoration() { + } + + @Override + public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { + if (mDivider == null) { + return; + } + final int childCount = parent.getChildCount(); + final int width = parent.getWidth(); + for (int childViewIndex = 0; childViewIndex < childCount; childViewIndex++) { + final View view = parent.getChildAt(childViewIndex); + if (shouldDrawDividerBelow(view, parent)) { + int top = (int) view.getY() + view.getHeight(); + mDivider.setBounds(0, top, width, top + mDividerHeight); + mDivider.draw(c); + } + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + if (shouldDrawDividerBelow(view, parent)) { + outRect.bottom = mDividerHeight; + } + } + + private boolean shouldDrawDividerBelow(View view, RecyclerView parent) { + final RecyclerView.ViewHolder holder = parent.getChildViewHolder(view); + final boolean dividerAllowedBelow = holder instanceof PreferenceViewHolder + && ((PreferenceViewHolder) holder).isDividerAllowedBelow(); + if (!dividerAllowedBelow) { + return false; + } + boolean nextAllowed = mAllowDividerAfterLastItem; + int index = parent.indexOfChild(view); + if (index < parent.getChildCount() - 1) { + final View nextView = parent.getChildAt(index + 1); + final RecyclerView.ViewHolder nextHolder = parent.getChildViewHolder(nextView); + nextAllowed = nextHolder instanceof PreferenceViewHolder + && ((PreferenceViewHolder) nextHolder).isDividerAllowedAbove(); + } + return nextAllowed; + } + + public void setDivider(Drawable divider) { + if (divider != null) { + mDividerHeight = divider.getIntrinsicHeight(); + } else { + mDividerHeight = 0; + } + mDivider = divider; + mList.invalidateItemDecorations(); + } + + public void setDividerHeight(int dividerHeight) { + mDividerHeight = dividerHeight; + mList.invalidateItemDecorations(); + } + + public void setAllowDividerAfterLastItem(boolean allowDividerAfterLastItem) { + mAllowDividerAfterLastItem = allowDividerAfterLastItem; + } + } +} diff --git a/j2k-preference/src/main/java/androidx/preference/PreferenceDialogController.java b/j2k-preference/src/main/java/androidx/preference/PreferenceDialogController.java new file mode 100644 index 0000000000..c50b606d91 --- /dev/null +++ b/j2k-preference/src/main/java/androidx/preference/PreferenceDialogController.java @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package androidx.preference; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.*; +import android.widget.TextView; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.appcompat.app.AlertDialog; +import com.bluelinelabs.conductor.Controller; +import com.bluelinelabs.conductor.RestoreViewOnCreateController; +import com.bluelinelabs.conductor.Router; +import com.bluelinelabs.conductor.RouterTransaction; +import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; + +/** + * Abstract base class which presents a dialog associated with a + * {@link androidx.preference.DialogPreference}. Since the preference object may + * not be available during fragment re-creation, the necessary information for displaying the dialog + * is read once during the initial call to {@link #onCreate(Bundle)} and saved/restored in the saved + * instance state. Custom subclasses should also follow this pattern. + */ +public abstract class PreferenceDialogController extends RestoreViewOnCreateController implements + DialogInterface.OnClickListener { + + protected static final String ARG_KEY = "key"; + + private static final String SAVE_DIALOG_STATE_TAG = "android:savedDialogState"; + private static final String SAVE_STATE_TITLE = "PreferenceDialogController.title"; + private static final String SAVE_STATE_POSITIVE_TEXT = "PreferenceDialogController.positiveText"; + private static final String SAVE_STATE_NEGATIVE_TEXT = "PreferenceDialogController.negativeText"; + private static final String SAVE_STATE_MESSAGE = "PreferenceDialogController.message"; + private static final String SAVE_STATE_LAYOUT = "PreferenceDialogController.layout"; + private static final String SAVE_STATE_ICON = "PreferenceDialogController.icon"; + + private DialogPreference mPreference; + + private CharSequence mDialogTitle; + private CharSequence mPositiveButtonText; + private CharSequence mNegativeButtonText; + private CharSequence mDialogMessage; + private @LayoutRes int mDialogLayoutRes; + + private BitmapDrawable mDialogIcon; + + /** Which button was clicked. */ + private int mWhichButtonClicked; + + private Dialog dialog; + private boolean dismissed; + + @NonNull + @Override + final protected View onCreateView(@NonNull LayoutInflater inflater, + @NonNull ViewGroup container, + @Nullable Bundle savedViewState) { + + onCreate(savedViewState); + + dialog = onCreateDialog(savedViewState); + //noinspection ConstantConditions + dialog.setOwnerActivity(getActivity()); + dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + PreferenceDialogController.this.dismissDialog(); + } + }); + if (savedViewState != null) { + Bundle dialogState = savedViewState.getBundle(SAVE_DIALOG_STATE_TAG); + if (dialogState != null) { + dialog.onRestoreInstanceState(dialogState); + } + } + return new View(getActivity());//stub view + } + + public void onCreate(Bundle savedInstanceState) { + final Controller rawController = getTargetController(); + if (!(rawController instanceof DialogPreference.TargetFragment)) { + throw new IllegalStateException("Target controller must implement TargetFragment" + + " interface"); + } + + final DialogPreference.TargetFragment controller = + (DialogPreference.TargetFragment) rawController; + + final String key = getArgs().getString(ARG_KEY); + if (savedInstanceState == null) { + mPreference = (DialogPreference) controller.findPreference(key); + mDialogTitle = mPreference.getDialogTitle(); + mPositiveButtonText = mPreference.getPositiveButtonText(); + mNegativeButtonText = mPreference.getNegativeButtonText(); + mDialogMessage = mPreference.getDialogMessage(); + mDialogLayoutRes = mPreference.getDialogLayoutResource(); + + final Drawable icon = mPreference.getDialogIcon(); + if (icon == null || icon instanceof BitmapDrawable) { + mDialogIcon = (BitmapDrawable) icon; + } else { + final Bitmap bitmap = Bitmap.createBitmap(icon.getIntrinsicWidth(), + icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + icon.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + icon.draw(canvas); + mDialogIcon = new BitmapDrawable(getResources(), bitmap); + } + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putCharSequence(SAVE_STATE_TITLE, mDialogTitle); + outState.putCharSequence(SAVE_STATE_POSITIVE_TEXT, mPositiveButtonText); + outState.putCharSequence(SAVE_STATE_NEGATIVE_TEXT, mNegativeButtonText); + outState.putCharSequence(SAVE_STATE_MESSAGE, mDialogMessage); + outState.putInt(SAVE_STATE_LAYOUT, mDialogLayoutRes); + if (mDialogIcon != null) { + outState.putParcelable(SAVE_STATE_ICON, mDialogIcon.getBitmap()); + } + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + mDialogTitle = savedInstanceState.getCharSequence(SAVE_STATE_TITLE); + mPositiveButtonText = savedInstanceState.getCharSequence(SAVE_STATE_POSITIVE_TEXT); + mNegativeButtonText = savedInstanceState.getCharSequence(SAVE_STATE_NEGATIVE_TEXT); + mDialogMessage = savedInstanceState.getCharSequence(SAVE_STATE_MESSAGE); + mDialogLayoutRes = savedInstanceState.getInt(SAVE_STATE_LAYOUT, 0); + final Bitmap bitmap = savedInstanceState.getParcelable(SAVE_STATE_ICON); + if (bitmap != null) { + mDialogIcon = new BitmapDrawable(getResources(), bitmap); + } + } + + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE; + + final AlertDialog.Builder builder = new AlertDialog.Builder(context) + .setTitle(mDialogTitle) + .setIcon(mDialogIcon) + .setPositiveButton(mPositiveButtonText, this) + .setNegativeButton(mNegativeButtonText, this); + + View contentView = onCreateDialogView(context); + if (contentView != null) { + onBindDialogView(contentView); + builder.setView(contentView); + } else { + builder.setMessage(mDialogMessage); + } + + onPrepareDialogBuilder(builder); + + // Create the dialog + final Dialog dialog = builder.create(); + if (needInputMethod()) { + requestInputMethod(dialog); + } + + return dialog; + } + + @Override + protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) { + super.onSaveViewState(view, outState); + Bundle dialogState = dialog.onSaveInstanceState(); + outState.putBundle(SAVE_DIALOG_STATE_TAG, dialogState); + } + + @Override + protected void onAttach(@NonNull View view) { + super.onAttach(view); + dialog.show(); + } + + @Override + protected void onDetach(@NonNull View view) { + super.onDetach(view); + dialog.hide(); + } + + @Override + protected void onDestroyView(@NonNull View view) { + super.onDestroyView(view); + dialog.setOnDismissListener(null); + dialog.dismiss(); + dialog = null; + mPreference = null; + } + + /** + * Display the dialog, create a transaction and pushing the controller. + * + * @param router The router on which the transaction will be applied + */ + public void showDialog(@NonNull Router router) { + showDialog(router, null); + } + + /** + * Display the dialog, create a transaction and pushing the controller. + * + * @param router The router on which the transaction will be applied + * @param tag The tag for this controller + */ + public void showDialog(@NonNull Router router, @Nullable String tag) { + dismissed = false; + router.pushController(RouterTransaction.with(this) + .pushChangeHandler(new SimpleSwapChangeHandler(false)) + .popChangeHandler(new SimpleSwapChangeHandler(false)) + .tag(tag)); + } + + /** + * Dismiss the dialog and pop this controller + */ + public void dismissDialog() { + if (dismissed) { + return; + } + onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE); + getRouter().popController(this); + dismissed = true; + } + + @Nullable + protected Dialog getDialog() { + return dialog; + } + + /** + * Get the preference that requested this dialog. Available after {@link #onCreate(Bundle)} has + * been called on the {@link PreferenceFragmentCompat} which launched this dialog. + * + * @return The {@link DialogPreference} associated with this + * dialog. + */ + public DialogPreference getPreference() { + if (mPreference == null) { + final String key = getArgs().getString(ARG_KEY); + final DialogPreference.TargetFragment controller = + (DialogPreference.TargetFragment) getTargetController(); + mPreference = (DialogPreference) controller.findPreference(key); + } + return mPreference; + } + + /** + * Prepares the dialog builder to be shown when the preference is clicked. + * Use this to set custom properties on the dialog. + *
+ * Do not {@link AlertDialog.Builder#create()} or + * {@link AlertDialog.Builder#show()}. + */ + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + } + + /** + * Returns whether the preference needs to display a soft input method when the dialog + * is displayed. Default is false. Subclasses should override this method if they need + * the soft input method brought up automatically. + * + * @hide + */ + @RestrictTo(LIBRARY_GROUP) + protected boolean needInputMethod() { + return false; + } + + /** + * Sets the required flags on the dialog window to enable input method window to show up. + */ + private void requestInputMethod(Dialog dialog) { + Window window = dialog.getWindow(); + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + } + + /** + * Creates the content view for the dialog (if a custom content view is + * required). By default, it inflates the dialog layout resource if it is + * set. + * + * @return The content View for the dialog. + * @see DialogPreference#setLayoutResource(int) + */ + protected View onCreateDialogView(Context context) { + final int resId = mDialogLayoutRes; + if (resId == 0) { + return null; + } + + LayoutInflater inflater = LayoutInflater.from(context); + return inflater.inflate(resId, null); + } + + /** + * Binds views in the content View of the dialog to data. + *
+ * Make sure to call through to the superclass implementation. + * + * @param view The content View of the dialog, if it is custom. + */ + protected void onBindDialogView(View view) { + View dialogMessageView = view.findViewById(android.R.id.message); + + if (dialogMessageView != null) { + final CharSequence message = mDialogMessage; + int newVisibility = View.GONE; + + if (!TextUtils.isEmpty(message)) { + if (dialogMessageView instanceof TextView) { + ((TextView) dialogMessageView).setText(message); + } + + newVisibility = View.VISIBLE; + } + + if (dialogMessageView.getVisibility() != newVisibility) { + dialogMessageView.setVisibility(newVisibility); + } + } + } + + @Override + public void onClick(DialogInterface dialog, int which) { + mWhichButtonClicked = which; + } + + public abstract void onDialogClosed(boolean positiveResult); +} diff --git a/settings.gradle b/settings.gradle index e7b4def49c..f340a35e17 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app' +include ':app', ':j2k-preference'