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.XmlRes; 20 import android.content.Context; 21 import android.content.res.TypedArray; 22 import android.content.res.XmlResourceParser; 23 import android.os.Bundle; 24 import android.text.TextUtils; 25 import android.util.AttributeSet; 26 import android.util.Log; 27 import android.util.Xml; 28 29 import androidx.annotation.IntDef; 30 import androidx.annotation.NonNull; 31 32 import com.android.settings.R; 33 34 import org.xmlpull.v1.XmlPullParser; 35 import org.xmlpull.v1.XmlPullParserException; 36 37 import java.io.IOException; 38 import java.lang.annotation.Retention; 39 import java.lang.annotation.RetentionPolicy; 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.List; 43 44 /** 45 * Utility class to parse elements of XML preferences 46 */ 47 public class PreferenceXmlParserUtils { 48 49 private static final String TAG = "PreferenceXmlParserUtil"; 50 public static final String PREF_SCREEN_TAG = "PreferenceScreen"; 51 private static final List<String> SUPPORTED_PREF_TYPES = Arrays.asList( 52 "Preference", "PreferenceCategory", "PreferenceScreen", "SwitchPreferenceCompat", 53 "com.android.settings.widget.WorkOnlyCategory"); 54 public static final int PREPEND_VALUE = 0; 55 public static final int APPEND_VALUE = 1; 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 MetadataFlag.FLAG_NEED_KEYWORDS, 71 MetadataFlag.FLAG_NEED_SEARCHABLE, 72 MetadataFlag.FLAG_NEED_PREF_APPEND, 73 MetadataFlag.FLAG_UNAVAILABLE_SLICE_SUBTITLE, 74 MetadataFlag.FLAG_FOR_WORK, 75 MetadataFlag.FLAG_NEED_HIGHLIGHTABLE_MENU_KEY, 76 MetadataFlag.FLAG_NEED_USER_RESTRICTION}) 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 int FLAG_NEED_HIGHLIGHTABLE_MENU_KEY = 1 << 13; 93 int FLAG_NEED_USER_RESTRICTION = 1 << 14; 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 public static final String METADATA_USER_RESTRICTION = "userRestriction"; 109 110 /** 111 * Extracts metadata from preference xml and put them into a {@link Bundle}. 112 * 113 * @param xmlResId xml res id of a preference screen 114 * @param flags Should be one or more of {@link MetadataFlag}. 115 */ 116 @NonNull extractMetadata(Context context, @XmlRes int xmlResId, int flags)117 public static List<Bundle> extractMetadata(Context context, @XmlRes int xmlResId, int flags) 118 throws IOException, XmlPullParserException { 119 final List<Bundle> metadata = new ArrayList<>(); 120 if (xmlResId <= 0) { 121 Log.d(TAG, xmlResId + " is invalid."); 122 return metadata; 123 } 124 final XmlResourceParser parser = context.getResources().getXml(xmlResId); 125 126 int type; 127 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 128 && type != XmlPullParser.START_TAG) { 129 // Parse next until start tag is found 130 } 131 final int outerDepth = parser.getDepth(); 132 final boolean hasPrefScreenFlag = hasFlag(flags, MetadataFlag.FLAG_INCLUDE_PREF_SCREEN); 133 do { 134 if (type != XmlPullParser.START_TAG) { 135 continue; 136 } 137 final String nodeName = parser.getName(); 138 if (!hasPrefScreenFlag && TextUtils.equals(PREF_SCREEN_TAG, nodeName)) { 139 continue; 140 } 141 if (!SUPPORTED_PREF_TYPES.contains(nodeName) && !nodeName.endsWith("Preference")) { 142 continue; 143 } 144 final Bundle preferenceMetadata = new Bundle(); 145 final AttributeSet attrs = Xml.asAttributeSet(parser); 146 147 final TypedArray preferenceAttributes = context.obtainStyledAttributes(attrs, 148 R.styleable.Preference); 149 TypedArray preferenceScreenAttributes = null; 150 if (hasPrefScreenFlag) { 151 preferenceScreenAttributes = context.obtainStyledAttributes( 152 attrs, R.styleable.PreferenceScreen); 153 } 154 155 if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_TYPE)) { 156 preferenceMetadata.putString(METADATA_PREF_TYPE, nodeName); 157 } 158 if (hasFlag(flags, MetadataFlag.FLAG_NEED_KEY)) { 159 preferenceMetadata.putString(METADATA_KEY, getKey(preferenceAttributes)); 160 } 161 if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_CONTROLLER)) { 162 preferenceMetadata.putString(METADATA_CONTROLLER, 163 getController(preferenceAttributes)); 164 } 165 if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_TITLE)) { 166 preferenceMetadata.putString(METADATA_TITLE, getTitle(preferenceAttributes)); 167 } 168 if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_SUMMARY)) { 169 preferenceMetadata.putString(METADATA_SUMMARY, getSummary(preferenceAttributes)); 170 } 171 if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_ICON)) { 172 preferenceMetadata.putInt(METADATA_ICON, getIcon(preferenceAttributes)); 173 } 174 if (hasFlag(flags, MetadataFlag.FLAG_NEED_KEYWORDS)) { 175 preferenceMetadata.putString(METADATA_KEYWORDS, getKeywords(preferenceAttributes)); 176 } 177 if (hasFlag(flags, MetadataFlag.FLAG_NEED_SEARCHABLE)) { 178 preferenceMetadata.putBoolean(METADATA_SEARCHABLE, 179 isSearchable(preferenceAttributes)); 180 } 181 if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_APPEND) && hasPrefScreenFlag) { 182 preferenceMetadata.putBoolean(METADATA_APPEND, 183 isAppended(preferenceScreenAttributes)); 184 } 185 if (hasFlag(flags, MetadataFlag.FLAG_UNAVAILABLE_SLICE_SUBTITLE)) { 186 preferenceMetadata.putString(METADATA_UNAVAILABLE_SLICE_SUBTITLE, 187 getUnavailableSliceSubtitle(preferenceAttributes)); 188 } 189 if (hasFlag(flags, MetadataFlag.FLAG_FOR_WORK)) { 190 preferenceMetadata.putBoolean(METADATA_FOR_WORK, 191 isForWork(preferenceAttributes)); 192 } 193 if (hasFlag(flags, MetadataFlag.FLAG_NEED_HIGHLIGHTABLE_MENU_KEY)) { 194 preferenceMetadata.putString(METADATA_HIGHLIGHTABLE_MENU_KEY, 195 getHighlightableMenuKey(preferenceAttributes)); 196 } 197 if (hasFlag(flags, MetadataFlag.FLAG_NEED_USER_RESTRICTION)) { 198 preferenceMetadata.putString(METADATA_USER_RESTRICTION, 199 getUserRestriction(context, attrs)); 200 } 201 metadata.add(preferenceMetadata); 202 203 preferenceAttributes.recycle(); 204 if (preferenceScreenAttributes != null) { 205 preferenceScreenAttributes.recycle(); 206 } 207 } while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 208 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)); 209 parser.close(); 210 return metadata; 211 } 212 hasFlag(int flags, @MetadataFlag int flag)213 private static boolean hasFlag(int flags, @MetadataFlag int flag) { 214 return (flags & flag) != 0; 215 } 216 getKey(TypedArray styledAttributes)217 private static String getKey(TypedArray styledAttributes) { 218 return styledAttributes.getString(com.android.internal.R.styleable.Preference_key); 219 } 220 getTitle(TypedArray styledAttributes)221 private static String getTitle(TypedArray styledAttributes) { 222 return styledAttributes.getString(com.android.internal.R.styleable.Preference_title); 223 } 224 getSummary(TypedArray styledAttributes)225 private static String getSummary(TypedArray styledAttributes) { 226 return styledAttributes.getString(com.android.internal.R.styleable.Preference_summary); 227 } 228 getController(TypedArray styledAttributes)229 private static String getController(TypedArray styledAttributes) { 230 return styledAttributes.getString(R.styleable.Preference_controller); 231 } 232 getHighlightableMenuKey(TypedArray styledAttributes)233 private static String getHighlightableMenuKey(TypedArray styledAttributes) { 234 return styledAttributes.getString(R.styleable.Preference_highlightableMenuKey); 235 } 236 getIcon(TypedArray styledAttributes)237 private static int getIcon(TypedArray styledAttributes) { 238 return styledAttributes.getResourceId(com.android.internal.R.styleable.Icon_icon, 0); 239 } 240 isSearchable(TypedArray styledAttributes)241 private static boolean isSearchable(TypedArray styledAttributes) { 242 return styledAttributes.getBoolean(R.styleable.Preference_searchable, true /* default */); 243 } 244 getKeywords(TypedArray styledAttributes)245 private static String getKeywords(TypedArray styledAttributes) { 246 return styledAttributes.getString(R.styleable.Preference_keywords); 247 } 248 isAppended(TypedArray styledAttributes)249 private static boolean isAppended(TypedArray styledAttributes) { 250 return styledAttributes.getInt(R.styleable.PreferenceScreen_staticPreferenceLocation, 251 PREPEND_VALUE) == APPEND_VALUE; 252 } 253 getUnavailableSliceSubtitle(TypedArray styledAttributes)254 private static String getUnavailableSliceSubtitle(TypedArray styledAttributes) { 255 return styledAttributes.getString( 256 R.styleable.Preference_unavailableSliceSubtitle); 257 } 258 isForWork(TypedArray styledAttributes)259 private static boolean isForWork(TypedArray styledAttributes) { 260 return styledAttributes.getBoolean( 261 R.styleable.Preference_forWork, false); 262 } 263 getUserRestriction(Context context, AttributeSet attrs)264 private static String getUserRestriction(Context context, AttributeSet attrs) { 265 TypedArray preferenceAttributes = context.obtainStyledAttributes(attrs, 266 com.android.settingslib.R.styleable.RestrictedPreference); 267 String userRestriction = preferenceAttributes.getString( 268 com.android.settingslib.R.styleable.RestrictedPreference_userRestriction); 269 preferenceAttributes.recycle(); 270 return userRestriction; 271 } 272 }