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