1 /* 2 * Copyright (C) 2017 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.settings.core; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.XmlRes; 22 import android.content.Context; 23 import android.content.res.TypedArray; 24 import android.content.res.XmlResourceParser; 25 import android.os.Bundle; 26 import android.text.TextUtils; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.util.TypedValue; 30 import android.util.Xml; 31 32 import androidx.annotation.IntDef; 33 import androidx.annotation.VisibleForTesting; 34 35 import com.android.settings.R; 36 37 import org.xmlpull.v1.XmlPullParser; 38 import org.xmlpull.v1.XmlPullParserException; 39 40 import java.io.IOException; 41 import java.lang.annotation.Retention; 42 import java.lang.annotation.RetentionPolicy; 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.List; 46 47 /** 48 * Utility class to parse elements of XML preferences 49 */ 50 public class PreferenceXmlParserUtils { 51 52 private static final String TAG = "PreferenceXmlParserUtil"; 53 @VisibleForTesting 54 static final String PREF_SCREEN_TAG = "PreferenceScreen"; 55 private static final List<String> SUPPORTED_PREF_TYPES = Arrays.asList( 56 "Preference", "PreferenceCategory", "PreferenceScreen", 57 "com.android.settings.widget.WorkOnlyCategory"); 58 public static final int PREPEND_VALUE = 0; 59 public static final int APPEND_VALUE = 1; 60 61 /** 62 * Flag definition to indicate which metadata should be extracted when 63 * {@link #extractMetadata(Context, int, int)} is called. The flags can be combined by using | 64 * (binary or). 65 */ 66 @IntDef(flag = true, value = { 67 MetadataFlag.FLAG_INCLUDE_PREF_SCREEN, 68 MetadataFlag.FLAG_NEED_KEY, 69 MetadataFlag.FLAG_NEED_PREF_TYPE, 70 MetadataFlag.FLAG_NEED_PREF_CONTROLLER, 71 MetadataFlag.FLAG_NEED_PREF_TITLE, 72 MetadataFlag.FLAG_NEED_PREF_SUMMARY, 73 MetadataFlag.FLAG_NEED_PREF_ICON, 74 MetadataFlag.FLAG_NEED_SEARCHABLE, 75 MetadataFlag.FLAG_UNAVAILABLE_SLICE_SUBTITLE, 76 MetadataFlag.FLAG_FOR_WORK, 77 MetadataFlag.FLAG_NEED_HIGHLIGHTABLE_MENU_KEY}) 78 @Retention(RetentionPolicy.SOURCE) 79 public @interface MetadataFlag { 80 81 int FLAG_INCLUDE_PREF_SCREEN = 1; 82 int FLAG_NEED_KEY = 1 << 1; 83 int FLAG_NEED_PREF_TYPE = 1 << 2; 84 int FLAG_NEED_PREF_CONTROLLER = 1 << 3; 85 int FLAG_NEED_PREF_TITLE = 1 << 4; 86 int FLAG_NEED_PREF_SUMMARY = 1 << 5; 87 int FLAG_NEED_PREF_ICON = 1 << 6; 88 int FLAG_NEED_KEYWORDS = 1 << 8; 89 int FLAG_NEED_SEARCHABLE = 1 << 9; 90 int FLAG_NEED_PREF_APPEND = 1 << 10; 91 int FLAG_UNAVAILABLE_SLICE_SUBTITLE = 1 << 11; 92 int FLAG_FOR_WORK = 1 << 12; 93 int FLAG_NEED_HIGHLIGHTABLE_MENU_KEY = 1 << 13; 94 } 95 96 public static final String METADATA_PREF_TYPE = "type"; 97 public static final String METADATA_KEY = "key"; 98 public static final String METADATA_CONTROLLER = "controller"; 99 public static final String METADATA_TITLE = "title"; 100 public static final String METADATA_SUMMARY = "summary"; 101 public static final String METADATA_ICON = "icon"; 102 public static final String METADATA_KEYWORDS = "keywords"; 103 public static final String METADATA_SEARCHABLE = "searchable"; 104 public static final String METADATA_APPEND = "staticPreferenceLocation"; 105 public static final String METADATA_UNAVAILABLE_SLICE_SUBTITLE = "unavailable_slice_subtitle"; 106 public static final String METADATA_FOR_WORK = "for_work"; 107 public static final String METADATA_HIGHLIGHTABLE_MENU_KEY = "highlightable_menu_key"; 108 109 private static final String ENTRIES_SEPARATOR = "|"; 110 111 /** 112 * Call {@link #extractMetadata(Context, int, int)} with {@link #METADATA_KEY} instead. 113 */ 114 @Deprecated getDataKey(Context context, AttributeSet attrs)115 public static String getDataKey(Context context, AttributeSet attrs) { 116 return getStringData(context, attrs, 117 com.android.internal.R.styleable.Preference, 118 com.android.internal.R.styleable.Preference_key); 119 } 120 121 /** 122 * Call {@link #extractMetadata(Context, int, int)} with {@link #METADATA_TITLE} instead. 123 */ 124 @Deprecated getDataTitle(Context context, AttributeSet attrs)125 public static String getDataTitle(Context context, AttributeSet attrs) { 126 return getStringData(context, attrs, 127 com.android.internal.R.styleable.Preference, 128 com.android.internal.R.styleable.Preference_title); 129 } 130 131 /** 132 * Call {@link #extractMetadata(Context, int, int)} with {@link #METADATA_SUMMARY} instead. 133 */ 134 @Deprecated getDataSummary(Context context, AttributeSet attrs)135 public static String getDataSummary(Context context, AttributeSet attrs) { 136 return getStringData(context, attrs, 137 com.android.internal.R.styleable.Preference, 138 com.android.internal.R.styleable.Preference_summary); 139 } 140 getDataSummaryOn(Context context, AttributeSet attrs)141 public static String getDataSummaryOn(Context context, AttributeSet attrs) { 142 return getStringData(context, attrs, 143 com.android.internal.R.styleable.CheckBoxPreference, 144 com.android.internal.R.styleable.CheckBoxPreference_summaryOn); 145 } 146 getDataSummaryOff(Context context, AttributeSet attrs)147 public static String getDataSummaryOff(Context context, AttributeSet attrs) { 148 return getStringData(context, attrs, 149 com.android.internal.R.styleable.CheckBoxPreference, 150 com.android.internal.R.styleable.CheckBoxPreference_summaryOff); 151 } 152 getDataEntries(Context context, AttributeSet attrs)153 public static String getDataEntries(Context context, AttributeSet attrs) { 154 return getDataEntries(context, attrs, 155 com.android.internal.R.styleable.ListPreference, 156 com.android.internal.R.styleable.ListPreference_entries); 157 } 158 getDataKeywords(Context context, AttributeSet attrs)159 public static String getDataKeywords(Context context, AttributeSet attrs) { 160 return getStringData(context, attrs, R.styleable.Preference, 161 R.styleable.Preference_keywords); 162 } 163 164 /** 165 * Call {@link #extractMetadata(Context, int, int)} with {@link #METADATA_CONTROLLER} instead. 166 */ 167 @Deprecated getController(Context context, AttributeSet attrs)168 public static String getController(Context context, AttributeSet attrs) { 169 return getStringData(context, attrs, R.styleable.Preference, 170 R.styleable.Preference_controller); 171 } 172 173 /** 174 * Extracts metadata from preference xml and put them into a {@link Bundle}. 175 * 176 * @param xmlResId xml res id of a preference screen 177 * @param flags Should be one or more of {@link MetadataFlag}. 178 */ 179 @NonNull extractMetadata(Context context, @XmlRes int xmlResId, int flags)180 public static List<Bundle> extractMetadata(Context context, @XmlRes int xmlResId, int flags) 181 throws IOException, XmlPullParserException { 182 final List<Bundle> metadata = new ArrayList<>(); 183 if (xmlResId <= 0) { 184 Log.d(TAG, xmlResId + " is invalid."); 185 return metadata; 186 } 187 final XmlResourceParser parser = context.getResources().getXml(xmlResId); 188 189 int type; 190 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 191 && type != XmlPullParser.START_TAG) { 192 // Parse next until start tag is found 193 } 194 final int outerDepth = parser.getDepth(); 195 final boolean hasPrefScreenFlag = hasFlag(flags, MetadataFlag.FLAG_INCLUDE_PREF_SCREEN); 196 do { 197 if (type != XmlPullParser.START_TAG) { 198 continue; 199 } 200 final String nodeName = parser.getName(); 201 if (!hasPrefScreenFlag && TextUtils.equals(PREF_SCREEN_TAG, nodeName)) { 202 continue; 203 } 204 if (!SUPPORTED_PREF_TYPES.contains(nodeName) && !nodeName.endsWith("Preference")) { 205 continue; 206 } 207 final Bundle preferenceMetadata = new Bundle(); 208 final AttributeSet attrs = Xml.asAttributeSet(parser); 209 210 final TypedArray preferenceAttributes = context.obtainStyledAttributes(attrs, 211 R.styleable.Preference); 212 TypedArray preferenceScreenAttributes = null; 213 if (hasPrefScreenFlag) { 214 preferenceScreenAttributes = context.obtainStyledAttributes( 215 attrs, R.styleable.PreferenceScreen); 216 } 217 218 if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_TYPE)) { 219 preferenceMetadata.putString(METADATA_PREF_TYPE, nodeName); 220 } 221 if (hasFlag(flags, MetadataFlag.FLAG_NEED_KEY)) { 222 preferenceMetadata.putString(METADATA_KEY, getKey(preferenceAttributes)); 223 } 224 if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_CONTROLLER)) { 225 preferenceMetadata.putString(METADATA_CONTROLLER, 226 getController(preferenceAttributes)); 227 } 228 if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_TITLE)) { 229 preferenceMetadata.putString(METADATA_TITLE, getTitle(preferenceAttributes)); 230 } 231 if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_SUMMARY)) { 232 preferenceMetadata.putString(METADATA_SUMMARY, getSummary(preferenceAttributes)); 233 } 234 if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_ICON)) { 235 preferenceMetadata.putInt(METADATA_ICON, getIcon(preferenceAttributes)); 236 } 237 if (hasFlag(flags, MetadataFlag.FLAG_NEED_KEYWORDS)) { 238 preferenceMetadata.putString(METADATA_KEYWORDS, getKeywords(preferenceAttributes)); 239 } 240 if (hasFlag(flags, MetadataFlag.FLAG_NEED_SEARCHABLE)) { 241 preferenceMetadata.putBoolean(METADATA_SEARCHABLE, 242 isSearchable(preferenceAttributes)); 243 } 244 if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_APPEND) && hasPrefScreenFlag) { 245 preferenceMetadata.putBoolean(METADATA_APPEND, 246 isAppended(preferenceScreenAttributes)); 247 } 248 if (hasFlag(flags, MetadataFlag.FLAG_UNAVAILABLE_SLICE_SUBTITLE)) { 249 preferenceMetadata.putString(METADATA_UNAVAILABLE_SLICE_SUBTITLE, 250 getUnavailableSliceSubtitle(preferenceAttributes)); 251 } 252 if (hasFlag(flags, MetadataFlag.FLAG_FOR_WORK)) { 253 preferenceMetadata.putBoolean(METADATA_FOR_WORK, 254 isForWork(preferenceAttributes)); 255 } 256 if (hasFlag(flags, MetadataFlag.FLAG_NEED_HIGHLIGHTABLE_MENU_KEY)) { 257 preferenceMetadata.putString(METADATA_HIGHLIGHTABLE_MENU_KEY, 258 getHighlightableMenuKey(preferenceAttributes)); 259 } 260 metadata.add(preferenceMetadata); 261 262 preferenceAttributes.recycle(); 263 } while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 264 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)); 265 parser.close(); 266 return metadata; 267 } 268 269 /** 270 * Call {@link #extractMetadata(Context, int, int)} with a {@link MetadataFlag} instead. 271 */ 272 @Deprecated 273 @Nullable getStringData(Context context, AttributeSet set, int[] attrs, int resId)274 private static String getStringData(Context context, AttributeSet set, int[] attrs, int resId) { 275 final TypedArray ta = context.obtainStyledAttributes(set, attrs); 276 String data = ta.getString(resId); 277 ta.recycle(); 278 return data; 279 } 280 hasFlag(int flags, @MetadataFlag int flag)281 private static boolean hasFlag(int flags, @MetadataFlag int flag) { 282 return (flags & flag) != 0; 283 } 284 getDataEntries(Context context, AttributeSet set, int[] attrs, int resId)285 private static String getDataEntries(Context context, AttributeSet set, int[] attrs, 286 int resId) { 287 final TypedArray sa = context.obtainStyledAttributes(set, attrs); 288 final TypedValue tv = sa.peekValue(resId); 289 sa.recycle(); 290 String[] data = null; 291 if (tv != null && tv.type == TypedValue.TYPE_REFERENCE) { 292 if (tv.resourceId != 0) { 293 data = context.getResources().getStringArray(tv.resourceId); 294 } 295 } 296 final int count = (data == null) ? 0 : data.length; 297 if (count == 0) { 298 return null; 299 } 300 final StringBuilder result = new StringBuilder(); 301 for (int n = 0; n < count; n++) { 302 result.append(data[n]); 303 result.append(ENTRIES_SEPARATOR); 304 } 305 return result.toString(); 306 } 307 getKey(TypedArray styledAttributes)308 private static String getKey(TypedArray styledAttributes) { 309 return styledAttributes.getString(com.android.internal.R.styleable.Preference_key); 310 } 311 getTitle(TypedArray styledAttributes)312 private static String getTitle(TypedArray styledAttributes) { 313 return styledAttributes.getString(com.android.internal.R.styleable.Preference_title); 314 } 315 getSummary(TypedArray styledAttributes)316 private static String getSummary(TypedArray styledAttributes) { 317 return styledAttributes.getString(com.android.internal.R.styleable.Preference_summary); 318 } 319 getController(TypedArray styledAttributes)320 private static String getController(TypedArray styledAttributes) { 321 return styledAttributes.getString(R.styleable.Preference_controller); 322 } 323 getHighlightableMenuKey(TypedArray styledAttributes)324 private static String getHighlightableMenuKey(TypedArray styledAttributes) { 325 return styledAttributes.getString(R.styleable.Preference_highlightableMenuKey); 326 } 327 getIcon(TypedArray styledAttributes)328 private static int getIcon(TypedArray styledAttributes) { 329 return styledAttributes.getResourceId(com.android.internal.R.styleable.Icon_icon, 0); 330 } 331 isSearchable(TypedArray styledAttributes)332 private static boolean isSearchable(TypedArray styledAttributes) { 333 return styledAttributes.getBoolean(R.styleable.Preference_searchable, true /* default */); 334 } 335 getKeywords(TypedArray styledAttributes)336 private static String getKeywords(TypedArray styledAttributes) { 337 return styledAttributes.getString(R.styleable.Preference_keywords); 338 } 339 isAppended(TypedArray styledAttributes)340 private static boolean isAppended(TypedArray styledAttributes) { 341 return styledAttributes.getInt(R.styleable.PreferenceScreen_staticPreferenceLocation, 342 PREPEND_VALUE) == APPEND_VALUE; 343 } 344 getUnavailableSliceSubtitle(TypedArray styledAttributes)345 private static String getUnavailableSliceSubtitle(TypedArray styledAttributes) { 346 return styledAttributes.getString( 347 R.styleable.Preference_unavailableSliceSubtitle); 348 } 349 isForWork(TypedArray styledAttributes)350 private static boolean isForWork(TypedArray styledAttributes) { 351 return styledAttributes.getBoolean( 352 R.styleable.Preference_forWork, false); 353 } 354 }