1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.leanback.preference;
18 
19 import android.content.Context;
20 import android.os.Bundle;
21 import android.text.TextUtils;
22 import android.util.TypedValue;
23 import android.view.ContextThemeWrapper;
24 import android.view.LayoutInflater;
25 import android.view.View;
26 import android.view.ViewGroup;
27 import android.widget.Checkable;
28 import android.widget.TextView;
29 
30 import androidx.collection.ArraySet;
31 import androidx.leanback.widget.VerticalGridView;
32 import androidx.preference.DialogPreference;
33 import androidx.preference.ListPreference;
34 import androidx.preference.MultiSelectListPreference;
35 import androidx.recyclerview.widget.RecyclerView;
36 
37 import org.jspecify.annotations.NonNull;
38 
39 import java.util.Collections;
40 import java.util.HashSet;
41 import java.util.Set;
42 
43 /**
44  * Implemented a dialog to show {@link ListPreference} or {@link MultiSelectListPreference}.
45  */
46 public class LeanbackListPreferenceDialogFragmentCompat extends
47         LeanbackPreferenceDialogFragmentCompat {
48 
49     private static final String SAVE_STATE_IS_MULTI =
50             "LeanbackListPreferenceDialogFragment.isMulti";
51     private static final String SAVE_STATE_ENTRIES = "LeanbackListPreferenceDialogFragment.entries";
52     private static final String SAVE_STATE_ENTRY_VALUES =
53             "LeanbackListPreferenceDialogFragment.entryValues";
54     private static final String SAVE_STATE_TITLE = "LeanbackListPreferenceDialogFragment.title";
55     private static final String SAVE_STATE_MESSAGE = "LeanbackListPreferenceDialogFragment.message";
56     private static final String SAVE_STATE_INITIAL_SELECTIONS =
57             "LeanbackListPreferenceDialogFragment.initialSelections";
58     private static final String SAVE_STATE_INITIAL_SELECTION =
59             "LeanbackListPreferenceDialogFragment.initialSelection";
60 
61     private boolean mMulti;
62     private CharSequence[] mEntries;
63     private CharSequence[] mEntryValues;
64     private CharSequence mDialogTitle;
65     private CharSequence mDialogMessage;
66     Set<String> mInitialSelections;
67     private String mInitialSelection;
68 
69     /**
70      * Create a new LeanbackListPreferenceDialogFragmentCompat.
71      * @param key The key of {@link ListPreference} it will be created from.
72      * @return A new LeanbackListPreferenceDialogFragmentCompat to display.
73      */
newInstanceSingle(String key)74     public static LeanbackListPreferenceDialogFragmentCompat newInstanceSingle(String key) {
75         final Bundle args = new Bundle(1);
76         args.putString(ARG_KEY, key);
77 
78         final LeanbackListPreferenceDialogFragmentCompat
79                 fragment = new LeanbackListPreferenceDialogFragmentCompat();
80         fragment.setArguments(args);
81 
82         return fragment;
83     }
84 
85     /**
86      * Create a new LeanbackListPreferenceDialogFragmentCompat.
87      * @param key The key of {@link MultiSelectListPreference} it will be created from.
88      * @return A new LeanbackListPreferenceDialogFragmentCompat to display.
89      */
newInstanceMulti(String key)90     public static LeanbackListPreferenceDialogFragmentCompat newInstanceMulti(String key) {
91         final Bundle args = new Bundle(1);
92         args.putString(ARG_KEY, key);
93 
94         final LeanbackListPreferenceDialogFragmentCompat
95                 fragment = new LeanbackListPreferenceDialogFragmentCompat();
96         fragment.setArguments(args);
97 
98         return fragment;
99     }
100 
101     @Override
onCreate(Bundle savedInstanceState)102     public void onCreate(Bundle savedInstanceState) {
103         super.onCreate(savedInstanceState);
104 
105         if (savedInstanceState == null) {
106             final DialogPreference preference = getPreference();
107             mDialogTitle = preference.getDialogTitle();
108             mDialogMessage = preference.getDialogMessage();
109 
110             if (preference instanceof ListPreference) {
111                 mMulti = false;
112                 mEntries = ((ListPreference) preference).getEntries();
113                 mEntryValues = ((ListPreference) preference).getEntryValues();
114                 mInitialSelection = ((ListPreference) preference).getValue();
115             } else if (preference instanceof MultiSelectListPreference) {
116                 mMulti = true;
117                 mEntries = ((MultiSelectListPreference) preference).getEntries();
118                 mEntryValues = ((MultiSelectListPreference) preference).getEntryValues();
119                 mInitialSelections = ((MultiSelectListPreference) preference).getValues();
120             } else {
121                 throw new IllegalArgumentException("Preference must be a ListPreference or "
122                         + "MultiSelectListPreference");
123             }
124         } else {
125             mDialogTitle = savedInstanceState.getCharSequence(SAVE_STATE_TITLE);
126             mDialogMessage = savedInstanceState.getCharSequence(SAVE_STATE_MESSAGE);
127             mMulti = savedInstanceState.getBoolean(SAVE_STATE_IS_MULTI);
128             mEntries = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRIES);
129             mEntryValues = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRY_VALUES);
130             if (mMulti) {
131                 final String[] initialSelections = savedInstanceState.getStringArray(
132                         SAVE_STATE_INITIAL_SELECTIONS);
133                 mInitialSelections = new ArraySet<>(
134                         initialSelections != null ? initialSelections.length : 0);
135                 if (initialSelections != null) {
136                     Collections.addAll(mInitialSelections, initialSelections);
137                 }
138             } else {
139                 mInitialSelection = savedInstanceState.getString(SAVE_STATE_INITIAL_SELECTION);
140             }
141         }
142     }
143 
144     @Override
onSaveInstanceState(@onNull Bundle outState)145     public void onSaveInstanceState(@NonNull Bundle outState) {
146         super.onSaveInstanceState(outState);
147         outState.putCharSequence(SAVE_STATE_TITLE, mDialogTitle);
148         outState.putCharSequence(SAVE_STATE_MESSAGE, mDialogMessage);
149         outState.putBoolean(SAVE_STATE_IS_MULTI, mMulti);
150         outState.putCharSequenceArray(SAVE_STATE_ENTRIES, mEntries);
151         outState.putCharSequenceArray(SAVE_STATE_ENTRY_VALUES, mEntryValues);
152         if (mMulti) {
153             outState.putStringArray(SAVE_STATE_INITIAL_SELECTIONS,
154                     mInitialSelections.toArray(new String[mInitialSelections.size()]));
155         } else {
156             outState.putString(SAVE_STATE_INITIAL_SELECTION, mInitialSelection);
157         }
158     }
159 
160     @Override
onCreateView(@onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)161     public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
162             Bundle savedInstanceState) {
163         final TypedValue tv = new TypedValue();
164         getActivity().getTheme().resolveAttribute(
165                 androidx.preference.R.attr.preferenceTheme, tv, true);
166         int theme = tv.resourceId;
167         if (theme == 0) {
168             // Fallback to default theme.
169             theme = R.style.PreferenceThemeOverlayLeanback;
170         }
171         Context styledContext = new ContextThemeWrapper(getActivity(), theme);
172         LayoutInflater styledInflater = inflater.cloneInContext(styledContext);
173         final View view = styledInflater.inflate(R.layout.leanback_list_preference_fragment,
174                 container, false);
175         final VerticalGridView verticalGridView =
176                 (VerticalGridView) view.findViewById(android.R.id.list);
177 
178         verticalGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE);
179         verticalGridView.setFocusScrollStrategy(VerticalGridView.FOCUS_SCROLL_ALIGNED);
180         verticalGridView.setAdapter(onCreateAdapter());
181         verticalGridView.requestFocus();
182 
183         final CharSequence title = mDialogTitle;
184         if (!TextUtils.isEmpty(title)) {
185             final TextView titleView = (TextView) view.findViewById(R.id.decor_title);
186             titleView.setText(title);
187         }
188 
189         final CharSequence message = mDialogMessage;
190         if (!TextUtils.isEmpty(message)) {
191             final TextView messageView = (TextView) view.findViewById(android.R.id.message);
192             messageView.setVisibility(View.VISIBLE);
193             messageView.setText(message);
194         }
195 
196         return view;
197     }
198 
onCreateAdapter()199     RecyclerView.Adapter onCreateAdapter() {
200         //final DialogPreference preference = getPreference();
201         if (mMulti) {
202             return new AdapterMulti(mEntries, mEntryValues, mInitialSelections);
203         } else {
204             return new AdapterSingle(mEntries, mEntryValues, mInitialSelection);
205         }
206     }
207 
208     /**
209      * Modifies or replaces the OnItemClickListener used for each item.
210      *
211      * <p>The default implementation simply returns the listener.
212      */
213     @NonNull
214     @SuppressWarnings("ExecutorRegistration")
decorateOnItemClickListener( @onNull OnItemClickListener onItemClickListener)215     protected OnItemClickListener decorateOnItemClickListener(
216             @NonNull OnItemClickListener onItemClickListener) {
217         return onItemClickListener;
218     }
219 
220     final class AdapterSingle extends RecyclerView.Adapter<ViewHolder>
221             implements OnItemClickListener {
222 
223         private final CharSequence[] mEntries;
224         private final CharSequence[] mEntryValues;
225         private CharSequence mSelectedValue;
226 
AdapterSingle(CharSequence[] entries, CharSequence[] entryValues, CharSequence selectedValue)227         AdapterSingle(CharSequence[] entries, CharSequence[] entryValues,
228                 CharSequence selectedValue) {
229             mEntries = entries;
230             mEntryValues = entryValues;
231             mSelectedValue = selectedValue;
232         }
233 
234         @Override
onCreateViewHolder(ViewGroup parent, int viewType)235         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
236             final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
237             final View view = inflater.inflate(R.layout.leanback_list_preference_item_single,
238                     parent, false);
239             return new ViewHolder(view, decorateOnItemClickListener(this));
240         }
241 
242         @Override
onBindViewHolder(ViewHolder holder, int position)243         public void onBindViewHolder(ViewHolder holder, int position) {
244             holder.getWidgetView().setChecked(
245                     TextUtils.equals(mEntryValues[position].toString(), mSelectedValue));
246             holder.getTitleView().setText(mEntries[position]);
247         }
248 
249         @Override
getItemCount()250         public int getItemCount() {
251             return mEntries.length;
252         }
253 
254         @Override
onItemClick(ViewHolder viewHolder)255         public void onItemClick(ViewHolder viewHolder) {
256             final int index = viewHolder.getAbsoluteAdapterPosition();
257             if (index == RecyclerView.NO_POSITION) {
258                 return;
259             }
260             final CharSequence entry = mEntryValues[index];
261             final ListPreference preference = (ListPreference) getPreference();
262             if (index >= 0) {
263                 String value = mEntryValues[index].toString();
264                 if (preference.callChangeListener(value)) {
265                     preference.setValue(value);
266                     mSelectedValue = entry;
267                 }
268             }
269 
270             getFragmentManager().popBackStack();
271             notifyDataSetChanged();
272         }
273     }
274 
275     final class AdapterMulti extends RecyclerView.Adapter<ViewHolder>
276             implements OnItemClickListener {
277 
278         private final CharSequence[] mEntries;
279         private final CharSequence[] mEntryValues;
280         private final Set<String> mSelections;
281 
AdapterMulti(CharSequence[] entries, CharSequence[] entryValues, Set<String> initialSelections)282         AdapterMulti(CharSequence[] entries, CharSequence[] entryValues,
283                 Set<String> initialSelections) {
284             mEntries = entries;
285             mEntryValues = entryValues;
286             mSelections = new HashSet<>(initialSelections);
287         }
288 
289         @Override
onCreateViewHolder(ViewGroup parent, int viewType)290         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
291             final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
292             final View view = inflater.inflate(R.layout.leanback_list_preference_item_multi, parent,
293                     false);
294             return new ViewHolder(view, decorateOnItemClickListener(this));
295         }
296 
297         @Override
onBindViewHolder(ViewHolder holder, int position)298         public void onBindViewHolder(ViewHolder holder, int position) {
299             holder.getWidgetView().setChecked(
300                     mSelections.contains(mEntryValues[position].toString()));
301             holder.getTitleView().setText(mEntries[position]);
302         }
303 
304         @Override
getItemCount()305         public int getItemCount() {
306             return mEntries.length;
307         }
308 
309         @Override
onItemClick(ViewHolder viewHolder)310         public void onItemClick(ViewHolder viewHolder) {
311             final int index = viewHolder.getAbsoluteAdapterPosition();
312             if (index == RecyclerView.NO_POSITION) {
313                 return;
314             }
315             final String entry = mEntryValues[index].toString();
316             if (mSelections.contains(entry)) {
317                 mSelections.remove(entry);
318             } else {
319                 mSelections.add(entry);
320             }
321             final MultiSelectListPreference multiSelectListPreference =
322                     (MultiSelectListPreference) getPreference();
323             // Pass copies of the set to callChangeListener and setValues to avoid mutations
324             if (multiSelectListPreference.callChangeListener(new HashSet<>(mSelections))) {
325                 multiSelectListPreference.setValues(new HashSet<>(mSelections));
326                 mInitialSelections = mSelections;
327             } else {
328                 // Change refused, back it out
329                 if (mSelections.contains(entry)) {
330                     mSelections.remove(entry);
331                 } else {
332                     mSelections.add(entry);
333                 }
334             }
335 
336             notifyDataSetChanged();
337         }
338     }
339 
340     /**
341      * Click listener for items in the list.
342      */
343     protected interface OnItemClickListener {
onItemClick(@onNull ViewHolder viewHolder)344         void onItemClick(@NonNull ViewHolder viewHolder);
345     }
346 
347     /**
348      * ViewHolder for each Item in the List.
349      */
350     public static final class ViewHolder extends RecyclerView.ViewHolder
351             implements View.OnClickListener {
352 
353         private final Checkable mWidgetView;
354         private final TextView mTitleView;
355         private final ViewGroup mContainer;
356         private final OnItemClickListener mListener;
357 
ViewHolder(@onNull View view, @NonNull OnItemClickListener listener)358         ViewHolder(@NonNull View view, @NonNull OnItemClickListener listener) {
359             super(view);
360             mWidgetView = (Checkable) view.findViewById(R.id.button);
361             mContainer = (ViewGroup) view.findViewById(R.id.container);
362             mTitleView = (TextView) view.findViewById(android.R.id.title);
363             mContainer.setOnClickListener(this);
364             mListener = listener;
365         }
366 
getWidgetView()367         public Checkable getWidgetView() {
368             return mWidgetView;
369         }
370 
getTitleView()371         public TextView getTitleView() {
372             return mTitleView;
373         }
374 
getContainer()375         public ViewGroup getContainer() {
376             return mContainer;
377         }
378 
379         @Override
onClick(View v)380         public void onClick(View v) {
381             mListener.onItemClick(this);
382         }
383     }
384 }
385