1 /*
2  * Copyright (C) 2022 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.tv.settings.customization;
18 
19 import android.content.Context;
20 import android.util.Log;
21 
22 import androidx.annotation.Nullable;
23 import androidx.preference.Preference;
24 import androidx.preference.PreferenceGroup;
25 import androidx.preference.PreferenceScreen;
26 
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Iterator;
30 import java.util.List;
31 
32 /**
33  * This is responsible for building the PreferenceScreen according to the
34  * Partner provided ordered preference list.
35  */
36 public final class PartnerPreferencesMerger {
37     private static final String TAG = "PartnerPreferencesMerger";
38 
mergePreferences( Context context, PreferenceScreen preferenceScreen, String settingsScreen)39     public static void mergePreferences(
40             Context context, PreferenceScreen preferenceScreen, String settingsScreen) {
41         /*
42         High level algorithm of adding new preferences in the desired order
43         1. Build partner provided new preferences if any.
44 
45         2. Add the preferences in 1. to the existing TvSettings PreferenceScreen.
46 
47         3. Recursively expand and parse the partner provided ordered string array
48         of preference keys. Each preference key can either be that of a PreferenceGroup
49         or a base preference. For every PreferenceGroup there is an array listing the
50         preferences it contains.
51 
52         4. Recursively clone every preference in current TvSettings PreferenceScreen.
53         The preference can either be a preference or a PreferenceGroup. Remove all the
54         preferences in each PreferenceGroup.
55 
56         5. Iterate through the ordered array in step 3, recursively adding preferences
57         in all PreferenceGroups.
58          */
59         final PartnerResourcesParser partnerResourcesParser = new PartnerResourcesParser(
60                 context, settingsScreen);
61         for (final Preference newPartnerPreference : partnerResourcesParser.buildPreferences()) {
62             preferenceScreen.addPreference(newPartnerPreference);
63         }
64 
65         final String[] orderedPreferenceKeys = partnerResourcesParser.getOrderedPreferences();
66 
67         // Clone the existing tv settings PreferenceScreen. All the preferences
68         // will be removed from this screen to avoid multiple re-orderings as
69         // the ordered preferences are being built
70         final Preference[] combinedSettingsPreferences = clonePreferenceScreen(preferenceScreen);
71         preferenceScreen.removeAll();
72 
73         addPreferences(
74                 Arrays.stream(orderedPreferenceKeys).iterator(),
75                 preferenceScreen,
76                 combinedSettingsPreferences
77         );
78 
79         // PreferenceScreen preferences are re-ordered whenever the notifyHierarchyChanged()
80         // method is invoked. It is package private and thus indirectly triggered by removing
81         // a preference that does not exist. Adding / removing a new preference always invokes
82         // notifyHierarchyChanged()
83         final Preference triggerReorderPreference = new Preference(preferenceScreen.getContext());
84         preferenceScreen.removePreference(triggerReorderPreference);
85     }
86 
87     /**
88      * Recursively iterates through all the preferences in PreferenceScreen and all
89      * PreferenceGroups in it doing a clone by reference.
90      * @param preferenceScreen current Tv Settings screen shown to the user
91      * @return Array of all preferences in present in the preferenceScreen
92      */
clonePreferenceScreen(PreferenceScreen preferenceScreen)93     private static Preference[] clonePreferenceScreen(PreferenceScreen preferenceScreen) {
94         return clonePreferencesInPreferenceGroup(preferenceScreen)
95                 .toArray(Preference[]::new);
96     }
97 
clonePreferencesInPreferenceGroup( PreferenceGroup preferenceGroup)98     private static List<Preference> clonePreferencesInPreferenceGroup(
99             PreferenceGroup preferenceGroup) {
100         final List<Preference> preferences = new ArrayList<>();
101         for (int index = 0; index < preferenceGroup.getPreferenceCount(); index++) {
102             final Preference preference = preferenceGroup.getPreference(index);
103             if (preference instanceof PreferenceGroup) {
104                 final List<Preference> nestedPreferences =
105                         clonePreferencesInPreferenceGroup((PreferenceGroup) preference);
106                 // Remove all preferences in the PreferenceGroup since the logic
107                 // to sort the preferences involves iterating through each preference
108                 // key. Having these preferences in a PreferenceGroup will result
109                 // in these nested preferences being added twice in the final list
110                 // of ordered preferences.
111                 ((PreferenceGroup) preference).removeAll();
112                 preferences.add(preference);
113                 preferences.addAll(nestedPreferences);
114             } else {
115                 preferences.add(preference);
116             }
117         }
118         return preferences;
119     }
120 
addPreferences( Iterator<String> partnerPreferenceKeyIterator, PreferenceGroup preferenceGroup, Preference[] tvSettingsPreferences)121     private static void addPreferences(
122             Iterator<String> partnerPreferenceKeyIterator,
123             PreferenceGroup preferenceGroup,
124             Preference[] tvSettingsPreferences) {
125         int order = 0;
126         while (partnerPreferenceKeyIterator.hasNext()) {
127             final String preferenceKey = partnerPreferenceKeyIterator.next();
128             if (preferenceKey.equals(PartnerResourcesParser.PREFERENCE_GROUP_END_INDICATOR)) {
129                 break;
130             }
131 
132             final Preference preference = findPreference(preferenceKey, tvSettingsPreferences);
133             if (preference == null) {
134                 Log.i(TAG, "Partner provided preference key: "
135                         + preferenceKey + " is not defined anywhere");
136                 continue;
137             }
138             if (preference instanceof PreferenceGroup) {
139                 addPreferences(partnerPreferenceKeyIterator,
140                         (PreferenceGroup) preference, tvSettingsPreferences);
141             }
142             preference.setOrder(++order);
143             preferenceGroup.addPreference(preference);
144         }
145     }
146 
147     @Nullable
findPreference(String key, Preference[] preferences)148     private static Preference findPreference(String key, Preference[] preferences) {
149         for (final Preference preference : preferences) {
150             if (preference.getKey().equals(key)) {
151                 return preference;
152             }
153         }
154         return null;
155     }
156 }
157