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.car.CarOccupantZoneManager;
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.util.AttributeSet;
27 import android.util.Xml;
28 
29 import androidx.annotation.IntDef;
30 
31 import com.android.car.settings.R;
32 
33 import org.xmlpull.v1.XmlPullParser;
34 import org.xmlpull.v1.XmlPullParserException;
35 
36 import java.io.IOException;
37 import java.lang.annotation.Retention;
38 import java.lang.annotation.RetentionPolicy;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.List;
42 
43 /**
44  * Utility class to parse elements of XML preferences. This is a reduced version of {@code com
45  * .android.settings.core.PreferenceXmlParserUtils}.
46  */
47 public class PreferenceXmlParser {
48 
49     private static final Logger LOG = new Logger(PreferenceXmlParser.class);
50 
51     private static final String PREF_TAG_ENDS_WITH = "Preference";
52     private static final String PREF_GROUP_TAG_ENDS_WITH = "PreferenceGroup";
53     private static final String PREF_CATEGORY_TAG_ENDS_WITH = "PreferenceCategory";
54     private static final List<String> SUPPORTED_PREF_TYPES = Arrays.asList("Preference",
55             "PreferenceCategory", "PreferenceScreen");
56 
57     public static final String PREF_AVAILABILITY_STATUS_WRITE = "write";
58     public static final String PREF_AVAILABILITY_STATUS_READ = "read";
59     public static final String PREF_AVAILABILITY_STATUS_HIDDEN = "hidden";
60     public static final List<String> SUPPORTED_AVAILABILITY_STATUS =
61             Arrays.asList(PREF_AVAILABILITY_STATUS_WRITE, PREF_AVAILABILITY_STATUS_READ,
62                     PREF_AVAILABILITY_STATUS_HIDDEN);
63 
64     /**
65      * Flag definition to indicate which metadata should be extracted when
66      * {@link #extractMetadata(Context, int, int)} is called. The flags can be combined by using |
67      * (binary or).
68      */
69     @IntDef(flag = true, value = {
70             MetadataFlag.FLAG_NEED_KEY,
71             MetadataFlag.FLAG_NEED_PREF_CONTROLLER,
72             MetadataFlag.FLAG_NEED_SEARCHABLE,
73             MetadataFlag.FLAG_NEED_PREF_DRIVER,
74             MetadataFlag.FLAG_NEED_PREF_FRONT_PASSENGER,
75             MetadataFlag.FLAG_NEED_PREF_REAR_PASSENGER})
76     @Retention(RetentionPolicy.SOURCE)
77     public @interface MetadataFlag {
78         int FLAG_NEED_KEY = 1;
79         int FLAG_NEED_PREF_CONTROLLER = 1 << 1;
80         int FLAG_NEED_SEARCHABLE = 1 << 9;
81         int FLAG_NEED_PREF_DRIVER = 1 << 10;
82         int FLAG_NEED_PREF_FRONT_PASSENGER = 1 << 11;
83         int FLAG_NEED_PREF_REAR_PASSENGER = 1 << 12;
84     }
85 
86     public static final String METADATA_KEY = "key";
87     public static final String METADATA_SEARCHABLE = "searchable";
88     static final String METADATA_CONTROLLER = "controller";
89     public static final String METADATA_OCCUPANT_ZONE = "occupant_zone";
90 
91     /**
92      * Extracts metadata from each preference XML and puts them into a {@link Bundle}.
93      *
94      * @param xmlResId xml res id of a preference screen
95      * @param flags one or more of {@link MetadataFlag}
96      * @return a list of Bundles containing the extracted metadata
97      */
98     @NonNull
extractMetadata(Context context, @XmlRes int xmlResId, int flags)99     public static List<Bundle> extractMetadata(Context context, @XmlRes int xmlResId, int flags)
100             throws IOException, XmlPullParserException {
101         final List<Bundle> metadata = new ArrayList<>();
102         if (xmlResId <= 0) {
103             LOG.d(xmlResId + " is invalid.");
104             return metadata;
105         }
106         final XmlResourceParser parser = context.getResources().getXml(xmlResId);
107 
108         int type;
109         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
110                 && type != XmlPullParser.START_TAG) {
111             // Parse next until start tag is found
112         }
113         final int outerDepth = parser.getDepth();
114 
115         do {
116             if (type != XmlPullParser.START_TAG) {
117                 continue;
118             }
119             final String nodeName = parser.getName();
120             if (!SUPPORTED_PREF_TYPES.contains(nodeName) && !nodeName.endsWith(PREF_TAG_ENDS_WITH)
121                     && !nodeName.endsWith(PREF_GROUP_TAG_ENDS_WITH)
122                     && !nodeName.endsWith(PREF_CATEGORY_TAG_ENDS_WITH)) {
123                 continue;
124             }
125             final Bundle preferenceMetadata = new Bundle();
126             final AttributeSet attrs = Xml.asAttributeSet(parser);
127             final TypedArray preferenceAttributes = context.obtainStyledAttributes(attrs,
128                     R.styleable.Preference);
129 
130             if (hasFlag(flags, MetadataFlag.FLAG_NEED_KEY)) {
131                 preferenceMetadata.putString(METADATA_KEY, getKey(preferenceAttributes));
132             }
133             if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_CONTROLLER)) {
134                 preferenceMetadata.putString(METADATA_CONTROLLER,
135                         getController(preferenceAttributes));
136             }
137             if (hasFlag(flags, MetadataFlag.FLAG_NEED_SEARCHABLE)) {
138                 preferenceMetadata.putBoolean(METADATA_SEARCHABLE,
139                         isSearchable(preferenceAttributes));
140             }
141             if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_DRIVER)) {
142                 preferenceMetadata.putString(METADATA_OCCUPANT_ZONE,
143                         getDriver(preferenceAttributes));
144             } else if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_FRONT_PASSENGER)) {
145                 preferenceMetadata.putString(METADATA_OCCUPANT_ZONE,
146                         getFrontPassenger(preferenceAttributes));
147             } else if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_REAR_PASSENGER)) {
148                 preferenceMetadata.putString(METADATA_OCCUPANT_ZONE,
149                         getRearPassenger(preferenceAttributes));
150             }
151             metadata.add(preferenceMetadata);
152 
153             preferenceAttributes.recycle();
154         } while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
155                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth));
156         parser.close();
157 
158         return metadata;
159     }
160 
161     /**
162      * Gets metadata flag from current zone type.
163      *
164      * @return a flag {@link MetadataFlag} that determines which zone attribute
165      * should be extracted from xml.
166      */
getMetadataFlagForOccupantZoneType(int zoneType)167     public static int getMetadataFlagForOccupantZoneType(int zoneType) {
168         switch (zoneType) {
169             case CarOccupantZoneManager.OCCUPANT_TYPE_FRONT_PASSENGER:
170                 return PreferenceXmlParser.MetadataFlag.FLAG_NEED_PREF_FRONT_PASSENGER;
171             case CarOccupantZoneManager.OCCUPANT_TYPE_REAR_PASSENGER:
172                 return PreferenceXmlParser.MetadataFlag.FLAG_NEED_PREF_REAR_PASSENGER;
173             case CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER: // fall through
174             default:
175                 return PreferenceXmlParser.MetadataFlag.FLAG_NEED_PREF_DRIVER;
176         }
177     }
178 
hasFlag(int flags, @MetadataFlag int flag)179     private static boolean hasFlag(int flags, @MetadataFlag int flag) {
180         return (flags & flag) != 0;
181     }
182 
getKey(TypedArray styledAttributes)183     private static String getKey(TypedArray styledAttributes) {
184         return styledAttributes.getString(com.android.internal.R.styleable.Preference_key);
185     }
186 
getController(TypedArray styledAttributes)187     private static String getController(TypedArray styledAttributes) {
188         return styledAttributes.getString(R.styleable.Preference_controller);
189     }
190 
isSearchable(TypedArray styledAttributes)191     private static boolean isSearchable(TypedArray styledAttributes) {
192         return styledAttributes.getBoolean(R.styleable.Preference_searchable, true);
193     }
194 
getDriver(TypedArray styledAttributes)195     private static String getDriver(TypedArray styledAttributes) {
196         return styledAttributes.getString(R.styleable.Preference_occupant_driver);
197     }
198 
getFrontPassenger(TypedArray styledAttributes)199     private static String getFrontPassenger(TypedArray styledAttributes) {
200         return styledAttributes.getString(R.styleable.Preference_occupant_front_passenger);
201     }
202 
getRearPassenger(TypedArray styledAttributes)203     private static String getRearPassenger(TypedArray styledAttributes) {
204         return styledAttributes.getString(R.styleable.Preference_occupant_rear_passenger);
205     }
206 }
207