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