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