From 2681ff3c9a02dd1d23f546254bc85f30fa72e34b Mon Sep 17 00:00:00 2001 From: Jay Date: Sat, 2 Nov 2019 01:45:12 -0700 Subject: [PATCH] Added Local fork of the preferences controller Because it didnt work correctly and I dont wanna upload the changes to git --- app/build.gradle | 3 +- j2k-preference/build.gradle | 29 + j2k-preference/proguard-rules.pro | 25 + j2k-preference/src/main/AndroidManifest.xml | 3 + .../EditTextPreferenceDialogController.java | 107 +++ .../ListPreferenceDialogController.java | 120 +++ ...iSelectListPreferenceDialogController.java | 136 +++ .../preference/PreferenceController.java | 813 ++++++++++++++++++ .../PreferenceDialogController.java | 363 ++++++++ settings.gradle | 2 +- 10 files changed, 1598 insertions(+), 3 deletions(-) create mode 100644 j2k-preference/build.gradle create mode 100644 j2k-preference/proguard-rules.pro create mode 100644 j2k-preference/src/main/AndroidManifest.xml create mode 100644 j2k-preference/src/main/java/androidx/preference/EditTextPreferenceDialogController.java create mode 100644 j2k-preference/src/main/java/androidx/preference/ListPreferenceDialogController.java create mode 100644 j2k-preference/src/main/java/androidx/preference/MultiSelectListPreferenceDialogController.java create mode 100644 j2k-preference/src/main/java/androidx/preference/PreferenceController.java create mode 100644 j2k-preference/src/main/java/androidx/preference/PreferenceDialogController.java diff --git a/app/build.gradle b/app/build.gradle index 555abc895d..c5fd976da1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -112,7 +112,6 @@ dependencies { implementation 'com.github.inorichi:junrar-android:634c1f5' // Android support library - implementation 'com.android.support:preference-v7:28.0.0' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'com.google.android.material:material:1.0.0' @@ -219,7 +218,7 @@ dependencies { implementation ("com.bluelinelabs:conductor-support:2.1.5") { exclude group: "com.android.support" } - implementation 'com.github.inorichi:conductor-support-preference:78e2344' + implementation project(":j2k-preference") // RxBindings final rxbindings_version = '1.0.1' diff --git a/j2k-preference/build.gradle b/j2k-preference/build.gradle new file mode 100644 index 0000000000..dba8bf87e0 --- /dev/null +++ b/j2k-preference/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + buildToolsVersion '29.0.2' + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 29 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + encoding = 'UTF-8' + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation 'androidx.preference:preference:1.1.0' + compileOnly 'com.bluelinelabs:conductor:2.1.5' +} diff --git a/j2k-preference/proguard-rules.pro b/j2k-preference/proguard-rules.pro new file mode 100644 index 0000000000..bb141e2a64 --- /dev/null +++ b/j2k-preference/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\Users\len\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/j2k-preference/src/main/AndroidManifest.xml b/j2k-preference/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..32e1b7866a --- /dev/null +++ b/j2k-preference/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/j2k-preference/src/main/java/androidx/preference/EditTextPreferenceDialogController.java b/j2k-preference/src/main/java/androidx/preference/EditTextPreferenceDialogController.java new file mode 100644 index 0000000000..c4933e2a82 --- /dev/null +++ b/j2k-preference/src/main/java/androidx/preference/EditTextPreferenceDialogController.java @@ -0,0 +1,107 @@ +/* + * 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.os.Bundle; +import android.view.View; +import android.widget.EditText; +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; + +public class EditTextPreferenceDialogController extends PreferenceDialogController { + + private static final String SAVE_STATE_TEXT = "EditTextPreferenceDialogController.text"; + + private EditText mEditText; + + private CharSequence mText; + + public static EditTextPreferenceDialogController newInstance(String key) { + EditTextPreferenceDialogController + controller = new EditTextPreferenceDialogController(); + controller.getArgs().putString(ARG_KEY, key); + return controller; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState == null) { + mText = getEditTextPreference().getText(); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putCharSequence(SAVE_STATE_TEXT, mText); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + mText = savedInstanceState.getCharSequence(SAVE_STATE_TEXT); + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + mEditText = view.findViewById(android.R.id.edit); + mEditText.requestFocus(); + + if (mEditText == null) { + throw new IllegalStateException("Dialog view must contain an EditText with id" + + " @android:id/edit"); + } + + mEditText.setText(mText); + // Place cursor at the end + mEditText.setSelection(mEditText.getText().length()); + } + + @Override + protected void onDestroyView(@NonNull View view) { + super.onDestroyView(view); + mEditText = null; + } + + private EditTextPreference getEditTextPreference() { + return (EditTextPreference) getPreference(); + } + + /** @hide */ + @RestrictTo(LIBRARY_GROUP) + @Override + protected boolean needInputMethod() { + // We want the input method to show, if possible, when dialog is displayed + return true; + } + + @Override + public void onDialogClosed(boolean positiveResult) { + if (positiveResult) { + String value = mEditText.getText().toString(); + if (getEditTextPreference().callChangeListener(value)) { + getEditTextPreference().setText(value); + } + } + } + +} diff --git a/j2k-preference/src/main/java/androidx/preference/ListPreferenceDialogController.java b/j2k-preference/src/main/java/androidx/preference/ListPreferenceDialogController.java new file mode 100644 index 0000000000..92c1d09066 --- /dev/null +++ b/j2k-preference/src/main/java/androidx/preference/ListPreferenceDialogController.java @@ -0,0 +1,120 @@ +/* + * 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.content.DialogInterface; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; + +public class ListPreferenceDialogController extends PreferenceDialogController { + + private static final String SAVE_STATE_INDEX = "ListPreferenceDialogController.index"; + private static final String SAVE_STATE_ENTRIES = "ListPreferenceDialogController.entries"; + private static final String SAVE_STATE_ENTRY_VALUES = + "ListPreferenceDialogController.entryValues"; + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + private int mClickedDialogEntryIndex; + private CharSequence[] mEntries; + private CharSequence[] mEntryValues; + + public static ListPreferenceDialogController newInstance(String key) { + ListPreferenceDialogController controller = + new ListPreferenceDialogController(); + controller.getArgs().putString(ARG_KEY, key); + return controller; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState == null) { + final ListPreference preference = getListPreference(); + + if (preference.getEntries() == null || preference.getEntryValues() == null) { + throw new IllegalStateException( + "ListPreference requires an entries array and an entryValues array."); + } + + mClickedDialogEntryIndex = preference.findIndexOfValue(preference.getValue()); + mEntries = preference.getEntries(); + mEntryValues = preference.getEntryValues(); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(SAVE_STATE_INDEX, mClickedDialogEntryIndex); + outState.putCharSequenceArray(SAVE_STATE_ENTRIES, mEntries); + outState.putCharSequenceArray(SAVE_STATE_ENTRY_VALUES, mEntryValues); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + mClickedDialogEntryIndex = savedInstanceState.getInt(SAVE_STATE_INDEX, 0); + mEntries = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRIES); + mEntryValues = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRY_VALUES); + } + + private ListPreference getListPreference() { + return (ListPreference) getPreference(); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + super.onPrepareDialogBuilder(builder); + + builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mClickedDialogEntryIndex = which; + + /* + * Clicking on an item simulates the positive button + * click, and dismisses the dialog. + */ + ListPreferenceDialogController.this.onClick(dialog, + DialogInterface.BUTTON_POSITIVE); + dialog.dismiss(); + } + }); + + /* + * The typical interaction for list-based dialogs is to have + * click-on-an-item dismiss the dialog instead of the user having to + * press 'Ok'. + */ + builder.setPositiveButton(null, null); + } + + @Override + public void onDialogClosed(boolean positiveResult) { + final ListPreference preference = getListPreference(); + if (positiveResult && mClickedDialogEntryIndex >= 0) { + String value = mEntryValues[mClickedDialogEntryIndex].toString(); + if (preference.callChangeListener(value)) { + preference.setValue(value); + } + } + } + +} diff --git a/j2k-preference/src/main/java/androidx/preference/MultiSelectListPreferenceDialogController.java b/j2k-preference/src/main/java/androidx/preference/MultiSelectListPreferenceDialogController.java new file mode 100644 index 0000000000..ff72e48437 --- /dev/null +++ b/j2k-preference/src/main/java/androidx/preference/MultiSelectListPreferenceDialogController.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2016 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.content.DialogInterface; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.MultiSelectListPreference; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +@SuppressWarnings("RestrictedApi") +public class MultiSelectListPreferenceDialogController extends PreferenceDialogController { + + private static final String SAVE_STATE_VALUES = + "MultiSelectListPreferenceDialogController.values"; + private static final String SAVE_STATE_CHANGED = + "MultiSelectListPreferenceDialogController.changed"; + private static final String SAVE_STATE_ENTRIES = + "MultiSelectListPreferenceDialogController.entries"; + private static final String SAVE_STATE_ENTRY_VALUES = + "MultiSelectListPreferenceDialogController.entryValues"; + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + Set mNewValues = new HashSet<>(); + @SuppressWarnings("WeakerAccess") /* synthetic access */ + boolean mPreferenceChanged; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + CharSequence[] mEntries; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + CharSequence[] mEntryValues; + + public static MultiSelectListPreferenceDialogController newInstance(String key) { + MultiSelectListPreferenceDialogController controller = + new MultiSelectListPreferenceDialogController(); + controller.getArgs().putString(ARG_KEY, key); + return controller; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState == null) { + final MultiSelectListPreference preference = getListPreference(); + + if (preference.getEntries() == null || preference.getEntryValues() == null) { + throw new IllegalStateException( + "MultiSelectListPreference requires an entries array and " + + "an entryValues array."); + } + + mNewValues.clear(); + mNewValues.addAll(preference.getValues()); + mPreferenceChanged = false; + mEntries = preference.getEntries(); + mEntryValues = preference.getEntryValues(); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putStringArrayList(SAVE_STATE_VALUES, new ArrayList<>(mNewValues)); + outState.putBoolean(SAVE_STATE_CHANGED, mPreferenceChanged); + outState.putCharSequenceArray(SAVE_STATE_ENTRIES, mEntries); + outState.putCharSequenceArray(SAVE_STATE_ENTRY_VALUES, mEntryValues); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + mNewValues.clear(); + mNewValues.addAll(savedInstanceState.getStringArrayList(SAVE_STATE_VALUES)); + mPreferenceChanged = savedInstanceState.getBoolean(SAVE_STATE_CHANGED, false); + mEntries = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRIES); + mEntryValues = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRY_VALUES); + } + + private MultiSelectListPreference getListPreference() { + return (MultiSelectListPreference) getPreference(); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + super.onPrepareDialogBuilder(builder); + + final int entryCount = mEntryValues.length; + final boolean[] checkedItems = new boolean[entryCount]; + for (int i = 0; i < entryCount; i++) { + checkedItems[i] = mNewValues.contains(mEntryValues[i].toString()); + } + builder.setMultiChoiceItems(mEntries, checkedItems, + new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + if (isChecked) { + mPreferenceChanged |= mNewValues.add( + mEntryValues[which].toString()); + } else { + mPreferenceChanged |= mNewValues.remove( + mEntryValues[which].toString()); + } + } + }); + } + + @Override + public void onDialogClosed(boolean positiveResult) { + final MultiSelectListPreference preference = getListPreference(); + if (positiveResult && mPreferenceChanged) { + final Set values = mNewValues; + if (preference.callChangeListener(values)) { + preference.setValues(values); + } + } + mPreferenceChanged = false; + } +} diff --git a/j2k-preference/src/main/java/androidx/preference/PreferenceController.java b/j2k-preference/src/main/java/androidx/preference/PreferenceController.java new file mode 100644 index 0000000000..0352ab8cc7 --- /dev/null +++ b/j2k-preference/src/main/java/androidx/preference/PreferenceController.java @@ -0,0 +1,813 @@ +/* + * 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.content.Context; +import android.content.pm.PackageManager; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.TypedValue; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.XmlRes; +import androidx.fragment.app.Fragment; +import androidx.preference.MultiSelectListPreference; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.bluelinelabs.conductor.RestoreViewOnCreateController; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; + +/** + * Shows a hierarchy of {@link Preference} objects as + * lists. These preferences will + * automatically save to {@link android.content.SharedPreferences} as the user interacts with + * them. To retrieve an instance of {@link android.content.SharedPreferences} that the + * preference hierarchy in this fragment will use, call + * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)} + * with a context in the same package as this fragment. + *

+ * 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: + *

  • From an XML file specifying the hierarchy + *
  • From different {@link android.app.Activity Activities} that each specify its own + * preferences in an XML file via {@link android.app.Activity} meta-data + *
  • From an object hierarchy rooted with {@link PreferenceScreen} + *

    + * 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)}. + * + *

    + *

    Developer Guides

    + *

    For information about using {@code PreferenceFragment}, + * read the Settings + * guide.

    + *
    + * + * + *

    Sample Code

    + * + *

    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'