1 /*
2  * Copyright (C) 2018 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.car.settings.common;
18 
19 import android.annotation.NonNull;
20 import android.annotation.XmlRes;
21 import android.content.Context;
22 import android.content.res.TypedArray;
23 import android.content.res.XmlResourceParser;
24 import android.os.Bundle;
25 import android.util.AttributeSet;
26 import android.util.Xml;
27 
28 import androidx.annotation.IntDef;
29 
30 import com.android.car.settings.R;
31 
32 import org.xmlpull.v1.XmlPullParser;
33 import org.xmlpull.v1.XmlPullParserException;
34 
35 import java.io.IOException;
36 import java.lang.annotation.Retention;
37 import java.lang.annotation.RetentionPolicy;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.List;
41 
42 /**
43  * Utility class to parse elements of XML preferences. This is a reduced version of {@code com
44  * .android.settings.core.PreferenceXmlParserUtils}.
45  */
46 public class PreferenceXmlParser {
47 
48     private static final Logger LOG = new Logger(PreferenceXmlParser.class);
49 
50     private static final String PREF_TAG_ENDS_WITH = "Preference";
51     private static final String PREF_GROUP_TAG_ENDS_WITH = "PreferenceGroup";
52     private static final String PREF_CATEGORY_TAG_ENDS_WITH = "PreferenceCategory";
53     private static final List<String> SUPPORTED_PREF_TYPES = Arrays.asList("Preference",
54             "PreferenceCategory", "PreferenceScreen");
55 
56     /**
57      * Flag definition to indicate which metadata should be extracted when
58      * {@link #extractMetadata(Context, int, int)} is called. The flags can be combined by using |
59      * (binary or).
60      */
61     @IntDef(flag = true, value = {
62             MetadataFlag.FLAG_NEED_KEY,
63             MetadataFlag.FLAG_NEED_PREF_CONTROLLER,
64             MetadataFlag.FLAG_NEED_SEARCHABLE})
65     @Retention(RetentionPolicy.SOURCE)
66     public @interface MetadataFlag {
67         int FLAG_NEED_KEY = 1;
68         int FLAG_NEED_PREF_CONTROLLER = 1 << 1;
69         int FLAG_NEED_SEARCHABLE = 1 << 9;
70     }
71 
72     public static final String METADATA_KEY = "key";
73     public static final String METADATA_SEARCHABLE = "searchable";
74     static final String METADATA_CONTROLLER = "controller";
75 
76     /**
77      * Extracts metadata from each preference XML and puts them into a {@link Bundle}.
78      *
79      * @param xmlResId xml res id of a preference screen
80      * @param flags one or more of {@link MetadataFlag}
81      * @return a list of Bundles containing the extracted metadata
82      */
83     @NonNull
extractMetadata(Context context, @XmlRes int xmlResId, int flags)84     public static List<Bundle> extractMetadata(Context context, @XmlRes int xmlResId, int flags)
85             throws IOException, XmlPullParserException {
86         final List<Bundle> metadata = new ArrayList<>();
87         if (xmlResId <= 0) {
88             LOG.d(xmlResId + " is invalid.");
89             return metadata;
90         }
91         final XmlResourceParser parser = context.getResources().getXml(xmlResId);
92 
93         int type;
94         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
95                 && type != XmlPullParser.START_TAG) {
96             // Parse next until start tag is found
97         }
98         final int outerDepth = parser.getDepth();
99 
100         do {
101             if (type != XmlPullParser.START_TAG) {
102                 continue;
103             }
104             final String nodeName = parser.getName();
105             if (!SUPPORTED_PREF_TYPES.contains(nodeName) && !nodeName.endsWith(PREF_TAG_ENDS_WITH)
106                     && !nodeName.endsWith(PREF_GROUP_TAG_ENDS_WITH)
107                     && !nodeName.endsWith(PREF_CATEGORY_TAG_ENDS_WITH)) {
108                 continue;
109             }
110             final Bundle preferenceMetadata = new Bundle();
111             final AttributeSet attrs = Xml.asAttributeSet(parser);
112             final TypedArray preferenceAttributes = context.obtainStyledAttributes(attrs,
113                     R.styleable.Preference);
114 
115             if (hasFlag(flags, MetadataFlag.FLAG_NEED_KEY)) {
116                 preferenceMetadata.putString(METADATA_KEY, getKey(preferenceAttributes));
117             }
118             if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_CONTROLLER)) {
119                 preferenceMetadata.putString(METADATA_CONTROLLER,
120                         getController(preferenceAttributes));
121             }
122             if (hasFlag(flags, MetadataFlag.FLAG_NEED_SEARCHABLE)) {
123                 preferenceMetadata.putBoolean(METADATA_SEARCHABLE,
124                         isSearchable(preferenceAttributes));
125             }
126             metadata.add(preferenceMetadata);
127 
128             preferenceAttributes.recycle();
129         } while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
130                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth));
131         parser.close();
132 
133         return metadata;
134     }
135 
hasFlag(int flags, @MetadataFlag int flag)136     private static boolean hasFlag(int flags, @MetadataFlag int flag) {
137         return (flags & flag) != 0;
138     }
139 
getKey(TypedArray styledAttributes)140     private static String getKey(TypedArray styledAttributes) {
141         return styledAttributes.getString(com.android.internal.R.styleable.Preference_key);
142     }
143 
getController(TypedArray styledAttributes)144     private static String getController(TypedArray styledAttributes) {
145         return styledAttributes.getString(R.styleable.Preference_controller);
146     }
147 
isSearchable(TypedArray styledAttributes)148     private static boolean isSearchable(TypedArray styledAttributes) {
149         return styledAttributes.getBoolean(R.styleable.Preference_searchable, true);
150     }
151 }
152