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