1 /* 2 * Copyright (C) 2020 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.server.pm; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.util.Slog; 22 import android.util.TypedXmlPullParser; 23 import android.util.TypedXmlSerializer; 24 import android.util.Xml; 25 26 import org.xmlpull.v1.XmlPullParser; 27 import org.xmlpull.v1.XmlPullParserException; 28 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.nio.charset.StandardCharsets; 32 import java.util.Stack; 33 34 /** 35 * A very specialized serialization/parsing wrapper around {@link TypedXmlSerializer} and {@link 36 * TypedXmlPullParser} intended for use with PackageManager related settings files. 37 * Assumptions/chosen behaviors: 38 * <ul> 39 * <li>No namespace support</li> 40 * <li>Data for a parent object is stored as attributes</li> 41 * <li>All attribute read methods return a default false, -1, or null</li> 42 * <li>Default values will not be written</li> 43 * <li>Children are sub-elements</li> 44 * <li>Collections are repeated sub-elements, no attribute support for collections</li> 45 * </ul> 46 */ 47 public class SettingsXml { 48 49 private static final String TAG = "SettingsXml"; 50 51 private static final boolean DEBUG_THROW_EXCEPTIONS = false; 52 53 private static final String FEATURE_INDENT = 54 "http://xmlpull.org/v1/doc/features.html#indent-output"; 55 56 private static final int DEFAULT_NUMBER = -1; 57 serializer(TypedXmlSerializer serializer)58 public static Serializer serializer(TypedXmlSerializer serializer) { 59 return new Serializer(serializer); 60 } 61 parser(TypedXmlPullParser parser)62 public static ReadSection parser(TypedXmlPullParser parser) 63 throws IOException, XmlPullParserException { 64 return new ReadSectionImpl(parser); 65 } 66 67 public static class Serializer implements AutoCloseable { 68 69 @NonNull 70 private final TypedXmlSerializer mXmlSerializer; 71 72 private final WriteSectionImpl mWriteSection; 73 Serializer(TypedXmlSerializer serializer)74 private Serializer(TypedXmlSerializer serializer) { 75 mXmlSerializer = serializer; 76 mWriteSection = new WriteSectionImpl(mXmlSerializer); 77 } 78 startSection(@onNull String sectionName)79 public WriteSection startSection(@NonNull String sectionName) throws IOException { 80 return mWriteSection.startSection(sectionName); 81 } 82 83 @Override close()84 public void close() throws IOException { 85 mWriteSection.closeCompletely(); 86 mXmlSerializer.flush(); 87 } 88 } 89 90 public interface ReadSection extends AutoCloseable { 91 92 @NonNull getName()93 String getName(); 94 95 @NonNull getDescription()96 String getDescription(); 97 has(String attrName)98 boolean has(String attrName); 99 100 @Nullable getString(String attrName)101 String getString(String attrName); 102 103 /** 104 * @return value as String or {@param defaultValue} if doesn't exist 105 */ 106 @NonNull getString(String attrName, @NonNull String defaultValue)107 String getString(String attrName, @NonNull String defaultValue); 108 109 /** 110 * @return value as boolean or false if doesn't exist 111 */ getBoolean(String attrName)112 boolean getBoolean(String attrName); 113 114 /** 115 * @return value as boolean or {@param defaultValue} if doesn't exist 116 */ getBoolean(String attrName, boolean defaultValue)117 boolean getBoolean(String attrName, boolean defaultValue); 118 119 /** 120 * @return value as int or {@link #DEFAULT_NUMBER} if doesn't exist 121 */ getInt(String attrName)122 int getInt(String attrName); 123 124 /** 125 * @return value as int or {@param defaultValue} if doesn't exist 126 */ getInt(String attrName, int defaultValue)127 int getInt(String attrName, int defaultValue); 128 129 /** 130 * @return value as long or {@link #DEFAULT_NUMBER} if doesn't exist 131 */ getLong(String attrName)132 long getLong(String attrName); 133 134 /** 135 * @return value as long or {@param defaultValue} if doesn't exist 136 */ getLong(String attrName, int defaultValue)137 long getLong(String attrName, int defaultValue); 138 children()139 ChildSection children(); 140 } 141 142 /** 143 * <pre><code> 144 * ChildSection child = parentSection.children(); 145 * while (child.moveToNext(TAG_CHILD)) { 146 * String readValue = child.getString(...); 147 * ... 148 * } 149 * </code></pre> 150 */ 151 public interface ChildSection extends ReadSection { moveToNext()152 boolean moveToNext(); 153 moveToNext(@onNull String expectedChildTagName)154 boolean moveToNext(@NonNull String expectedChildTagName); 155 } 156 157 public static class ReadSectionImpl implements ChildSection { 158 159 @Nullable 160 private final InputStream mInput; 161 162 @NonNull 163 private final TypedXmlPullParser mParser; 164 165 @NonNull 166 private final Stack<Integer> mDepthStack = new Stack<>(); 167 ReadSectionImpl(@onNull InputStream input)168 public ReadSectionImpl(@NonNull InputStream input) 169 throws IOException, XmlPullParserException { 170 mInput = input; 171 mParser = Xml.newFastPullParser(); 172 mParser.setInput(mInput, StandardCharsets.UTF_8.name()); 173 moveToFirstTag(); 174 } 175 ReadSectionImpl(@onNull TypedXmlPullParser parser)176 public ReadSectionImpl(@NonNull TypedXmlPullParser parser) 177 throws IOException, XmlPullParserException { 178 mInput = null; 179 mParser = parser; 180 moveToFirstTag(); 181 } 182 moveToFirstTag()183 private void moveToFirstTag() throws IOException, XmlPullParserException { 184 if (mParser.getEventType() == XmlPullParser.START_TAG) { 185 return; 186 } 187 188 int type; 189 //noinspection StatementWithEmptyBody 190 while ((type = mParser.next()) != XmlPullParser.START_TAG 191 && type != XmlPullParser.END_DOCUMENT) { 192 } 193 } 194 195 @NonNull 196 @Override getName()197 public String getName() { 198 return mParser.getName(); 199 } 200 201 @NonNull 202 @Override getDescription()203 public String getDescription() { 204 return mParser.getPositionDescription(); 205 } 206 207 @Override has(String attrName)208 public boolean has(String attrName) { 209 return mParser.getAttributeValue(null, attrName) != null; 210 } 211 212 @Nullable 213 @Override getString(String attrName)214 public String getString(String attrName) { 215 return mParser.getAttributeValue(null, attrName); 216 } 217 218 @NonNull 219 @Override getString(String attrName, @NonNull String defaultValue)220 public String getString(String attrName, @NonNull String defaultValue) { 221 String value = mParser.getAttributeValue(null, attrName); 222 if (value == null) { 223 return defaultValue; 224 } 225 return value; 226 } 227 228 @Override getBoolean(String attrName)229 public boolean getBoolean(String attrName) { 230 return getBoolean(attrName, false); 231 } 232 233 @Override getBoolean(String attrName, boolean defaultValue)234 public boolean getBoolean(String attrName, boolean defaultValue) { 235 return mParser.getAttributeBoolean(null, attrName, defaultValue); 236 } 237 238 @Override getInt(String attrName)239 public int getInt(String attrName) { 240 return getInt(attrName, DEFAULT_NUMBER); 241 } 242 243 @Override getInt(String attrName, int defaultValue)244 public int getInt(String attrName, int defaultValue) { 245 return mParser.getAttributeInt(null, attrName, defaultValue); 246 } 247 248 @Override getLong(String attrName)249 public long getLong(String attrName) { 250 return getLong(attrName, DEFAULT_NUMBER); 251 } 252 253 @Override getLong(String attrName, int defaultValue)254 public long getLong(String attrName, int defaultValue) { 255 return mParser.getAttributeLong(null, attrName, defaultValue); 256 } 257 258 @Override children()259 public ChildSection children() { 260 mDepthStack.push(mParser.getDepth()); 261 return this; 262 } 263 264 @Override moveToNext()265 public boolean moveToNext() { 266 return moveToNextInternal(null); 267 } 268 269 @Override moveToNext(@onNull String expectedChildTagName)270 public boolean moveToNext(@NonNull String expectedChildTagName) { 271 return moveToNextInternal(expectedChildTagName); 272 } 273 moveToNextInternal(@ullable String expectedChildTagName)274 private boolean moveToNextInternal(@Nullable String expectedChildTagName) { 275 try { 276 int depth = mDepthStack.peek(); 277 boolean hasTag = false; 278 int type; 279 while (!hasTag 280 && (type = mParser.next()) != XmlPullParser.END_DOCUMENT 281 && (type != XmlPullParser.END_TAG || mParser.getDepth() > depth)) { 282 if (type != XmlPullParser.START_TAG) { 283 continue; 284 } 285 286 if (expectedChildTagName != null 287 && !expectedChildTagName.equals(mParser.getName())) { 288 continue; 289 } 290 291 hasTag = true; 292 } 293 294 if (!hasTag) { 295 mDepthStack.pop(); 296 } 297 298 return hasTag; 299 } catch (Exception ignored) { 300 return false; 301 } 302 } 303 304 @Override close()305 public void close() throws Exception { 306 if (mDepthStack.isEmpty()) { 307 Slog.wtf(TAG, "Children depth stack was not empty, data may have been lost", 308 new Exception()); 309 } 310 if (mInput != null) { 311 mInput.close(); 312 } 313 } 314 } 315 316 public interface WriteSection extends AutoCloseable { 317 startSection(@onNull String sectionName)318 WriteSection startSection(@NonNull String sectionName) throws IOException; 319 attribute(String attrName, @Nullable String value)320 WriteSection attribute(String attrName, @Nullable String value) throws IOException; 321 attribute(String attrName, int value)322 WriteSection attribute(String attrName, int value) throws IOException; 323 attribute(String attrName, long value)324 WriteSection attribute(String attrName, long value) throws IOException; 325 attribute(String attrName, boolean value)326 WriteSection attribute(String attrName, boolean value) throws IOException; 327 328 @Override close()329 void close() throws IOException; 330 finish()331 void finish() throws IOException; 332 } 333 334 private static class WriteSectionImpl implements WriteSection { 335 336 @NonNull 337 private final TypedXmlSerializer mXmlSerializer; 338 339 @NonNull 340 private final Stack<String> mTagStack = new Stack<>(); 341 WriteSectionImpl(@onNull TypedXmlSerializer xmlSerializer)342 private WriteSectionImpl(@NonNull TypedXmlSerializer xmlSerializer) { 343 mXmlSerializer = xmlSerializer; 344 } 345 346 @Override startSection(@onNull String sectionName)347 public WriteSection startSection(@NonNull String sectionName) throws IOException { 348 // Try to start the tag first before we push it to the stack 349 mXmlSerializer.startTag(null, sectionName); 350 mTagStack.push(sectionName); 351 return this; 352 } 353 354 @Override attribute(String attrName, String value)355 public WriteSection attribute(String attrName, String value) throws IOException { 356 if (value != null) { 357 mXmlSerializer.attribute(null, attrName, value); 358 } 359 return this; 360 } 361 362 @Override attribute(String attrName, int value)363 public WriteSection attribute(String attrName, int value) throws IOException { 364 if (value != DEFAULT_NUMBER) { 365 mXmlSerializer.attributeInt(null, attrName, value); 366 } 367 return this; 368 } 369 370 @Override attribute(String attrName, long value)371 public WriteSection attribute(String attrName, long value) throws IOException { 372 if (value != DEFAULT_NUMBER) { 373 mXmlSerializer.attributeLong(null, attrName, value); 374 } 375 return this; 376 } 377 378 @Override attribute(String attrName, boolean value)379 public WriteSection attribute(String attrName, boolean value) throws IOException { 380 if (value) { 381 mXmlSerializer.attributeBoolean(null, attrName, value); 382 } 383 return this; 384 } 385 386 @Override finish()387 public void finish() throws IOException { 388 close(); 389 } 390 391 @Override close()392 public void close() throws IOException { 393 mXmlSerializer.endTag(null, mTagStack.pop()); 394 } 395 closeCompletely()396 private void closeCompletely() throws IOException { 397 if (DEBUG_THROW_EXCEPTIONS && mTagStack != null && !mTagStack.isEmpty()) { 398 throw new IllegalStateException( 399 "tag stack is not empty when closing, contains " + mTagStack); 400 } else if (mTagStack != null) { 401 while (!mTagStack.isEmpty()) { 402 close(); 403 } 404 } 405 } 406 } 407 } 408