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
17package androidx.preference;
18
19import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.content.res.TypedArray;
22import android.graphics.drawable.Drawable;
23import android.os.Handler;
24import android.text.TextUtils;
25import android.view.LayoutInflater;
26import android.view.View;
27import android.view.ViewGroup;
28
29import androidx.annotation.RestrictTo;
30import androidx.annotation.VisibleForTesting;
31import androidx.core.content.ContextCompat;
32import androidx.core.view.ViewCompat;
33import androidx.recyclerview.widget.DiffUtil;
34import androidx.recyclerview.widget.RecyclerView;
35
36import java.util.ArrayList;
37import java.util.List;
38
39/**
40 * An adapter that connects a RecyclerView to the {@link Preference} objects contained in the
41 * associated {@link PreferenceGroup}.
42 *
43 * @hide
44 */
45@RestrictTo(LIBRARY_GROUP)
46public class PreferenceGroupAdapter extends RecyclerView.Adapter<PreferenceViewHolder>
47        implements Preference.OnPreferenceChangeInternalListener,
48        PreferenceGroup.PreferencePositionCallback {
49
50    /**
51     * The group that we are providing data from.
52     */
53    private PreferenceGroup mPreferenceGroup;
54
55    /**
56     * Maps a position into this adapter -> {@link Preference}. These
57     * {@link Preference}s don't have to be direct children of this
58     * {@link PreferenceGroup}, they can be grand children or younger)
59     */
60    private List<Preference> mPreferenceList;
61
62    /**
63     * Contains a sorted list of all preferences in this adapter regardless of visibility. This is
64     * used to construct {@link #mPreferenceList}
65     */
66    private List<Preference> mPreferenceListInternal;
67
68    /**
69     * List of unique Preference and its subclasses' names and layouts.
70     */
71    private List<PreferenceLayout> mPreferenceLayouts;
72
73
74    private PreferenceLayout mTempPreferenceLayout = new PreferenceLayout();
75
76    private Handler mHandler;
77
78    private CollapsiblePreferenceGroupController mPreferenceGroupController;
79
80    private Runnable mSyncRunnable = new Runnable() {
81        @Override
82        public void run() {
83            syncMyPreferences();
84        }
85    };
86
87    private static class PreferenceLayout {
88        private int mResId;
89        private int mWidgetResId;
90        private String mName;
91
92        PreferenceLayout() {}
93
94        PreferenceLayout(PreferenceLayout other) {
95            mResId = other.mResId;
96            mWidgetResId = other.mWidgetResId;
97            mName = other.mName;
98        }
99
100        @Override
101        public boolean equals(Object o) {
102            if (!(o instanceof PreferenceLayout)) {
103                return false;
104            }
105            final PreferenceLayout other = (PreferenceLayout) o;
106            return mResId == other.mResId
107                    && mWidgetResId == other.mWidgetResId
108                    && TextUtils.equals(mName, other.mName);
109        }
110
111        @Override
112        public int hashCode() {
113            int result = 17;
114            result = 31 * result + mResId;
115            result = 31 * result + mWidgetResId;
116            result = 31 * result + mName.hashCode();
117            return result;
118        }
119    }
120
121    public PreferenceGroupAdapter(PreferenceGroup preferenceGroup) {
122        this(preferenceGroup, new Handler());
123    }
124
125    private PreferenceGroupAdapter(PreferenceGroup preferenceGroup, Handler handler) {
126        mPreferenceGroup = preferenceGroup;
127        mHandler = handler;
128        mPreferenceGroupController =
129                new CollapsiblePreferenceGroupController(preferenceGroup, this);
130        // If this group gets or loses any children, let us know
131        mPreferenceGroup.setOnPreferenceChangeInternalListener(this);
132
133        mPreferenceList = new ArrayList<>();
134        mPreferenceListInternal = new ArrayList<>();
135        mPreferenceLayouts = new ArrayList<>();
136
137        if (mPreferenceGroup instanceof PreferenceScreen) {
138            setHasStableIds(((PreferenceScreen) mPreferenceGroup).shouldUseGeneratedIds());
139        } else {
140            setHasStableIds(true);
141        }
142
143        syncMyPreferences();
144    }
145
146    @VisibleForTesting
147    static PreferenceGroupAdapter createInstanceWithCustomHandler(PreferenceGroup preferenceGroup,
148            Handler handler) {
149        return new PreferenceGroupAdapter(preferenceGroup, handler);
150    }
151
152    private void syncMyPreferences() {
153        for (final Preference preference : mPreferenceListInternal) {
154            // Clear out the listeners in anticipation of some items being removed. This listener
155            // will be (re-)added to the remaining prefs when we flatten.
156            preference.setOnPreferenceChangeInternalListener(null);
157        }
158        final List<Preference> fullPreferenceList = new ArrayList<>(mPreferenceListInternal.size());
159        flattenPreferenceGroup(fullPreferenceList, mPreferenceGroup);
160
161        final List<Preference> visiblePreferenceList =
162                mPreferenceGroupController.createVisiblePreferencesList(mPreferenceGroup);
163
164        final List<Preference> oldVisibleList = mPreferenceList;
165        mPreferenceList = visiblePreferenceList;
166        mPreferenceListInternal = fullPreferenceList;
167
168        final PreferenceManager preferenceManager = mPreferenceGroup.getPreferenceManager();
169        if (preferenceManager != null
170                && preferenceManager.getPreferenceComparisonCallback() != null) {
171            final PreferenceManager.PreferenceComparisonCallback comparisonCallback =
172                    preferenceManager.getPreferenceComparisonCallback();
173            final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
174                @Override
175                public int getOldListSize() {
176                    return oldVisibleList.size();
177                }
178
179                @Override
180                public int getNewListSize() {
181                    return visiblePreferenceList.size();
182                }
183
184                @Override
185                public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
186                    return comparisonCallback.arePreferenceItemsTheSame(
187                            oldVisibleList.get(oldItemPosition),
188                            visiblePreferenceList.get(newItemPosition));
189                }
190
191                @Override
192                public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
193                    return comparisonCallback.arePreferenceContentsTheSame(
194                            oldVisibleList.get(oldItemPosition),
195                            visiblePreferenceList.get(newItemPosition));
196                }
197            });
198
199            result.dispatchUpdatesTo(this);
200        } else {
201            notifyDataSetChanged();
202        }
203
204        for (final Preference preference : fullPreferenceList) {
205            preference.clearWasDetached();
206        }
207    }
208
209    private void flattenPreferenceGroup(List<Preference> preferences, PreferenceGroup group) {
210        group.sortPreferences();
211
212        final int groupSize = group.getPreferenceCount();
213        for (int i = 0; i < groupSize; i++) {
214            final Preference preference = group.getPreference(i);
215
216            preferences.add(preference);
217
218            addPreferenceClassName(preference);
219
220            if (preference instanceof PreferenceGroup) {
221                final PreferenceGroup preferenceAsGroup = (PreferenceGroup) preference;
222                if (preferenceAsGroup.isOnSameScreenAsChildren()) {
223                    flattenPreferenceGroup(preferences, preferenceAsGroup);
224                }
225            }
226
227            preference.setOnPreferenceChangeInternalListener(this);
228        }
229    }
230
231    /**
232     * Creates a string that includes the preference name, layout id and widget layout id.
233     * If a particular preference type uses 2 different resources, they will be treated as
234     * different view types.
235     */
236    private PreferenceLayout createPreferenceLayout(Preference preference, PreferenceLayout in) {
237        PreferenceLayout pl = in != null ? in : new PreferenceLayout();
238        pl.mName = preference.getClass().getName();
239        pl.mResId = preference.getLayoutResource();
240        pl.mWidgetResId = preference.getWidgetLayoutResource();
241        return pl;
242    }
243
244    private void addPreferenceClassName(Preference preference) {
245        final PreferenceLayout pl = createPreferenceLayout(preference, null);
246        if (!mPreferenceLayouts.contains(pl)) {
247            mPreferenceLayouts.add(pl);
248        }
249    }
250
251    @Override
252    public int getItemCount() {
253        return mPreferenceList.size();
254    }
255
256    public Preference getItem(int position) {
257        if (position < 0 || position >= getItemCount()) return null;
258        return mPreferenceList.get(position);
259    }
260
261    @Override
262    public long getItemId(int position) {
263        if (!hasStableIds()) {
264            return RecyclerView.NO_ID;
265        }
266        return this.getItem(position).getId();
267    }
268
269    @Override
270    public void onPreferenceChange(Preference preference) {
271        final int index = mPreferenceList.indexOf(preference);
272        // If we don't find the preference, we don't need to notify anyone
273        if (index != -1) {
274            // Send the pref object as a placeholder to ensure the view holder is recycled in place
275            notifyItemChanged(index, preference);
276        }
277    }
278
279    @Override
280    public void onPreferenceHierarchyChange(Preference preference) {
281        mHandler.removeCallbacks(mSyncRunnable);
282        mHandler.post(mSyncRunnable);
283    }
284
285    @Override
286    public void onPreferenceVisibilityChange(Preference preference) {
287        if (!mPreferenceListInternal.contains(preference)) {
288            return;
289        }
290        if (mPreferenceGroupController.onPreferenceVisibilityChange(preference)) {
291            return;
292        }
293        if (preference.isVisible()) {
294            // The preference has become visible, we need to add it in the correct location.
295
296            // Index (inferred) in mPreferenceList of the item preceding the newly visible pref
297            int previousVisibleIndex = -1;
298            for (final Preference pref : mPreferenceListInternal) {
299                if (preference.equals(pref)) {
300                    break;
301                }
302                if (pref.isVisible()) {
303                    previousVisibleIndex++;
304                }
305            }
306            // Insert this preference into the active list just after the previous visible entry
307            mPreferenceList.add(previousVisibleIndex + 1, preference);
308
309            notifyItemInserted(previousVisibleIndex + 1);
310        } else {
311            // The preference has become invisible. Find it in the list and remove it.
312
313            int removalIndex;
314            final int listSize = mPreferenceList.size();
315            for (removalIndex = 0; removalIndex < listSize; removalIndex++) {
316                if (preference.equals(mPreferenceList.get(removalIndex))) {
317                    break;
318                }
319            }
320            mPreferenceList.remove(removalIndex);
321            notifyItemRemoved(removalIndex);
322        }
323    }
324
325    @Override
326    public int getItemViewType(int position) {
327        final Preference preference = this.getItem(position);
328
329        mTempPreferenceLayout = createPreferenceLayout(preference, mTempPreferenceLayout);
330
331        int viewType = mPreferenceLayouts.indexOf(mTempPreferenceLayout);
332        if (viewType != -1) {
333            return viewType;
334        } else {
335            viewType = mPreferenceLayouts.size();
336            mPreferenceLayouts.add(new PreferenceLayout(mTempPreferenceLayout));
337            return viewType;
338        }
339    }
340
341    @Override
342    public PreferenceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
343        final PreferenceLayout pl = mPreferenceLayouts.get(viewType);
344        final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
345        TypedArray a
346                = parent.getContext().obtainStyledAttributes(null, R.styleable.BackgroundStyle);
347        Drawable background
348                = a.getDrawable(R.styleable.BackgroundStyle_android_selectableItemBackground);
349        if (background == null) {
350            background = ContextCompat.getDrawable(parent.getContext(),
351                    android.R.drawable.list_selector_background);
352        }
353        a.recycle();
354
355        final View view = inflater.inflate(pl.mResId, parent, false);
356        if (view.getBackground() == null) {
357            ViewCompat.setBackground(view, background);
358        }
359
360        final ViewGroup widgetFrame = (ViewGroup) view.findViewById(android.R.id.widget_frame);
361        if (widgetFrame != null) {
362            if (pl.mWidgetResId != 0) {
363                inflater.inflate(pl.mWidgetResId, widgetFrame);
364            } else {
365                widgetFrame.setVisibility(View.GONE);
366            }
367        }
368
369        return new PreferenceViewHolder(view);
370    }
371
372    @Override
373    public void onBindViewHolder(PreferenceViewHolder holder, int position) {
374        final Preference preference = getItem(position);
375        preference.onBindViewHolder(holder);
376    }
377
378    @Override
379    public int getPreferenceAdapterPosition(String key) {
380        final int size = mPreferenceList.size();
381        for (int i = 0; i < size; i++) {
382            final Preference candidate = mPreferenceList.get(i);
383            if (TextUtils.equals(key, candidate.getKey())) {
384                return i;
385            }
386        }
387        return RecyclerView.NO_POSITION;
388    }
389
390    @Override
391    public int getPreferenceAdapterPosition(Preference preference) {
392        final int size = mPreferenceList.size();
393        for (int i = 0; i < size; i++) {
394            final Preference candidate = mPreferenceList.get(i);
395            if (candidate != null && candidate.equals(preference)) {
396                return i;
397            }
398        }
399        return RecyclerView.NO_POSITION;
400    }
401}
402