• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }