1 /* 2 * Copyright 2019 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 com.android.car.ui.preference; 18 19 import static com.android.car.ui.core.CarUi.MIN_TARGET_API; 20 import static com.android.car.ui.preference.PreferenceDialogFragment.ARG_KEY; 21 22 import android.annotation.TargetApi; 23 import android.os.Bundle; 24 import android.view.LayoutInflater; 25 import android.view.View; 26 import android.view.ViewGroup; 27 28 import androidx.annotation.NonNull; 29 import androidx.annotation.Nullable; 30 import androidx.fragment.app.Fragment; 31 import androidx.preference.DialogPreference; 32 import androidx.preference.ListPreference; 33 import androidx.preference.Preference; 34 import androidx.recyclerview.widget.RecyclerView; 35 36 import com.android.car.ui.FocusArea; 37 import com.android.car.ui.R; 38 import com.android.car.ui.baselayout.Insets; 39 import com.android.car.ui.baselayout.InsetsChangedListener; 40 import com.android.car.ui.recyclerview.CarUiContentListItem; 41 import com.android.car.ui.recyclerview.CarUiListItem; 42 import com.android.car.ui.recyclerview.CarUiListItemAdapter; 43 import com.android.car.ui.recyclerview.CarUiRecyclerView; 44 import com.android.car.ui.toolbar.NavButtonMode; 45 import com.android.car.ui.toolbar.Toolbar; 46 import com.android.car.ui.toolbar.ToolbarController; 47 import com.android.car.ui.utils.CarUiUtils; 48 49 import java.util.ArrayList; 50 import java.util.Collections; 51 import java.util.List; 52 53 /** 54 * A fragment that provides a layout with a list of options associated with a {@link 55 * ListPreference}. 56 */ 57 @TargetApi(MIN_TARGET_API) 58 public class ListPreferenceFragment extends Fragment implements InsetsChangedListener { 59 60 private ListPreference mPreference; 61 private CarUiContentListItem mSelectedItem; 62 private int mSelectedIndex = -1; 63 private boolean mUseInstantPreferenceChangeCallback; 64 65 /** 66 * Returns a new instance of {@link ListPreferenceFragment} for the {@link ListPreference} with 67 * the given {@code key}. 68 */ 69 @NonNull newInstance(String key)70 static ListPreferenceFragment newInstance(String key) { 71 ListPreferenceFragment fragment = new ListPreferenceFragment(); 72 Bundle b = new Bundle(/* capacity= */ 1); 73 b.putString(ARG_KEY, key); 74 fragment.setArguments(b); 75 return fragment; 76 } 77 78 @Nullable 79 @Override onCreateView( @onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)80 public View onCreateView( 81 @NonNull LayoutInflater inflater, @Nullable ViewGroup container, 82 @Nullable Bundle savedInstanceState) { 83 return inflater.inflate(R.layout.car_ui_list_preference, container, false); 84 } 85 86 @Override onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)87 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 88 super.onViewCreated(view, savedInstanceState); 89 final CarUiRecyclerView carUiRecyclerView = CarUiUtils.requireViewByRefId(view, R.id.list); 90 mUseInstantPreferenceChangeCallback = 91 getResources().getBoolean(R.bool.car_ui_preference_list_instant_change_callback); 92 ToolbarController toolbar = null; 93 if (getTargetFragment() instanceof PreferenceFragment) { 94 toolbar = ((PreferenceFragment) getTargetFragment()).getPreferenceToolbar(this); 95 } 96 97 carUiRecyclerView.setClipToPadding(false); 98 mPreference = getListPreference(); 99 if (toolbar != null) { 100 toolbar.setTitle(mPreference.getTitle()); 101 toolbar.setSubtitle(""); 102 if (toolbar.isStateSet()) { 103 toolbar.setState(Toolbar.State.SUBPAGE); 104 } else { 105 toolbar.setNavButtonMode(NavButtonMode.BACK); 106 } 107 toolbar.setLogo(null); 108 toolbar.setMenuItems(null); 109 toolbar.setTabs(Collections.emptyList()); 110 } 111 112 CharSequence[] entries = mPreference.getEntries(); 113 CharSequence[] entryValues = mPreference.getEntryValues(); 114 115 if (entries == null || entryValues == null) { 116 throw new IllegalStateException( 117 "ListPreference requires an entries array and an entryValues array."); 118 } 119 120 if (entries.length != entryValues.length) { 121 throw new IllegalStateException( 122 "ListPreference entries array length does not match entryValues array length."); 123 } 124 125 mSelectedIndex = mPreference.findIndexOfValue(mPreference.getValue()); 126 List<CarUiListItem> listItems = new ArrayList<>(); 127 CarUiListItemAdapter adapter = new CarUiListItemAdapter(listItems); 128 129 for (int i = 0; i < entries.length; i++) { 130 String entry = entries[i].toString(); 131 CarUiContentListItem item = new CarUiContentListItem( 132 CarUiContentListItem.Action.RADIO_BUTTON); 133 item.setTitle(entry); 134 135 if (i == mSelectedIndex) { 136 item.setChecked(true); 137 mSelectedItem = item; 138 } 139 140 item.setOnCheckedChangeListener((listItem, isChecked) -> { 141 if (!isChecked) { 142 // Previously selected item is unchecked now, no further processing is needed. 143 return; 144 } 145 146 if (mSelectedItem != null) { 147 mSelectedItem.setChecked(false); 148 adapter.notifyItemChanged(listItems.indexOf(mSelectedItem)); 149 } 150 mSelectedItem = listItem; 151 mSelectedIndex = listItems.indexOf(mSelectedItem); 152 153 if (mUseInstantPreferenceChangeCallback) { 154 updatePreference(); 155 } 156 }); 157 158 listItems.add(item); 159 } 160 161 carUiRecyclerView.setAdapter(adapter); 162 carUiRecyclerView.scrollToPosition(mSelectedIndex); 163 carUiRecyclerView.post( 164 () -> { 165 RecyclerView.ViewHolder viewHolder = 166 carUiRecyclerView.findViewHolderForAdapterPosition(mSelectedIndex); 167 if (viewHolder != null) { 168 viewHolder.itemView.requestFocus(); 169 } 170 }); 171 } 172 173 @Override onStart()174 public void onStart() { 175 super.onStart(); 176 if (getTargetFragment() instanceof PreferenceFragment) { 177 Insets insets = ((PreferenceFragment) getTargetFragment()).getPreferenceInsets(this); 178 if (insets != null) { 179 onCarUiInsetsChanged(insets); 180 } 181 } 182 } 183 184 @Override onStop()185 public void onStop() { 186 super.onStop(); 187 188 if (!mUseInstantPreferenceChangeCallback) { 189 updatePreference(); 190 } 191 } 192 updatePreference()193 private void updatePreference() { 194 if (mSelectedIndex >= 0 && mPreference != null) { 195 String entryValue = mPreference.getEntryValues()[mSelectedIndex].toString(); 196 197 if (mPreference.callChangeListener(entryValue)) { 198 mPreference.setValue(entryValue); 199 } 200 } 201 } 202 getListPreference()203 private ListPreference getListPreference() { 204 String key = requireArguments().getString(ARG_KEY); 205 DialogPreference.TargetFragment fragment = 206 (DialogPreference.TargetFragment) getTargetFragment(); 207 208 if (key == null) { 209 throw new IllegalStateException( 210 "ListPreference key not found in Fragment arguments"); 211 } 212 213 if (fragment == null) { 214 throw new IllegalStateException( 215 "Target fragment must be registered before displaying ListPreference " 216 + "screen."); 217 } 218 219 Preference preference = fragment.findPreference(key); 220 221 if (!(preference instanceof ListPreference)) { 222 throw new IllegalStateException( 223 "Cannot use ListPreferenceFragment with a preference that is not of type " 224 + "ListPreference"); 225 } 226 227 return (ListPreference) preference; 228 } 229 230 @Override onCarUiInsetsChanged(@onNull Insets insets)231 public void onCarUiInsetsChanged(@NonNull Insets insets) { 232 View view = requireView(); 233 CarUiUtils.requireViewByRefId(view, R.id.list) 234 .setPadding(0, insets.getTop(), 0, insets.getBottom()); 235 view.setPadding(insets.getLeft(), 0, insets.getRight(), 0); 236 FocusArea focusArea = view.findViewById(R.id.car_ui_focus_area); 237 if (focusArea != null) { 238 focusArea.setHighlightPadding(0, insets.getTop(), 0, insets.getBottom()); 239 focusArea.setBoundsOffset(0, insets.getTop(), 0, insets.getBottom()); 240 } 241 } 242 } 243