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         final List<Preference> partnerPreferences = partnerResourcesParser.buildPreferences();
62         final String[] orderedPreferenceKeys = partnerResourcesParser.getOrderedPreferences();
63 
64         // Don't touch this screen if our partner hasn't asked to.
65         if (partnerPreferences.isEmpty() && orderedPreferenceKeys.length == 0) {
66             return;
67         }
68 
69         for (final Preference newPartnerPreference : partnerPreferences) {
70             preferenceScreen.addPreference(newPartnerPreference);
71         }
72 
73 
74         // Clone the existing tv settings PreferenceScreen. All the preferences
75         // will be removed from this screen to avoid multiple re-orderings as
76         // the ordered preferences are being built
77         final Preference[] combinedSettingsPreferences = clonePreferenceScreen(preferenceScreen);
78         preferenceScreen.removeAll();
79 
80         addPreferences(
81                 Arrays.stream(orderedPreferenceKeys).iterator(),
82                 preferenceScreen,
83                 combinedSettingsPreferences
84         );
85 
86         // PreferenceScreen preferences are re-ordered whenever the notifyHierarchyChanged()
87         // method is invoked. It is package private and thus indirectly triggered by removing
88         // a preference that does not exist. Adding / removing a new preference always invokes
89         // notifyHierarchyChanged()
90         final Preference triggerReorderPreference = new Preference(preferenceScreen.getContext());
91         preferenceScreen.removePreference(triggerReorderPreference);
92     }
93 
94     /**
95      * Recursively iterates through all the preferences in PreferenceScreen and all
96      * PreferenceGroups in it doing a clone by reference.
97      * @param preferenceScreen current Tv Settings screen shown to the user
98      * @return Array of all preferences in present in the preferenceScreen
99      */
clonePreferenceScreen(PreferenceScreen preferenceScreen)100     private static Preference[] clonePreferenceScreen(PreferenceScreen preferenceScreen) {
101         return clonePreferencesInPreferenceGroup(preferenceScreen)
102                 .toArray(Preference[]::new);
103     }
104 
clonePreferencesInPreferenceGroup( PreferenceGroup preferenceGroup)105     private static List<Preference> clonePreferencesInPreferenceGroup(
106             PreferenceGroup preferenceGroup) {
107         final List<Preference> preferences = new ArrayList<>();
108         for (int index = 0; index < preferenceGroup.getPreferenceCount(); index++) {
109             final Preference preference = preferenceGroup.getPreference(index);
110             if (preference instanceof PreferenceGroup) {
111                 final List<Preference> nestedPreferences =
112                         clonePreferencesInPreferenceGroup((PreferenceGroup) preference);
113                 // Remove all preferences in the PreferenceGroup since the logic
114                 // to sort the preferences involves iterating through each preference
115                 // key. Having these preferences in a PreferenceGroup will result
116                 // in these nested preferences being added twice in the final list
117                 // of ordered preferences.
118                 ((PreferenceGroup) preference).removeAll();
119                 preferences.add(preference);
120                 preferences.addAll(nestedPreferences);
121             } else {
122                 preferences.add(preference);
123             }
124         }
125         return preferences;
126     }
127 
addPreferences( Iterator<String> partnerPreferenceKeyIterator, PreferenceGroup preferenceGroup, Preference[] tvSettingsPreferences)128     private static void addPreferences(
129             Iterator<String> partnerPreferenceKeyIterator,
130             PreferenceGroup preferenceGroup,
131             Preference[] tvSettingsPreferences) {
132         int order = 0;
133         while (partnerPreferenceKeyIterator.hasNext()) {
134             final String preferenceKey = partnerPreferenceKeyIterator.next();
135             if (preferenceKey.equals(PartnerResourcesParser.PREFERENCE_GROUP_END_INDICATOR)) {
136                 break;
137             }
138 
139             final Preference preference = findPreference(preferenceKey, tvSettingsPreferences);
140             if (preference == null) {
141                 Log.i(TAG, "Partner provided preference key: "
142                         + preferenceKey + " is not defined anywhere");
143                 continue;
144             }
145             if (preference instanceof PreferenceGroup) {
146                 addPreferences(partnerPreferenceKeyIterator,
147                         (PreferenceGroup) preference, tvSettingsPreferences);
148             }
149             preference.setOrder(++order);
150             preferenceGroup.addPreference(preference);
151         }
152     }
153 
154     @Nullable
findPreference(String key, Preference[] preferences)155     private static Preference findPreference(String key, Preference[] preferences) {
156         for (final Preference preference : preferences) {
157             if (preference.getKey().equals(key)) {
158                 return preference;
159             }
160         }
161         return null;
162     }
163 }
164