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