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 }