1 /* 2 * Copyright (C) 2015 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.providers.settings; 18 19 import android.os.Build; 20 import android.os.Handler; 21 import android.os.Looper; 22 import android.os.Message; 23 import android.os.SystemClock; 24 import android.provider.Settings; 25 import android.text.TextUtils; 26 import android.util.ArrayMap; 27 import android.util.AtomicFile; 28 import android.util.Base64; 29 import android.util.Slog; 30 import android.util.TimeUtils; 31 import android.util.Xml; 32 import com.android.internal.annotations.GuardedBy; 33 import libcore.io.IoUtils; 34 import libcore.util.Objects; 35 import org.xmlpull.v1.XmlPullParser; 36 import org.xmlpull.v1.XmlPullParserException; 37 import org.xmlpull.v1.XmlSerializer; 38 39 import java.io.File; 40 import java.io.FileInputStream; 41 import java.io.FileNotFoundException; 42 import java.io.FileOutputStream; 43 import java.io.IOException; 44 import java.io.PrintWriter; 45 import java.nio.charset.StandardCharsets; 46 import java.util.ArrayList; 47 import java.util.List; 48 49 /** 50 * This class contains the state for one type of settings. It is responsible 51 * for saving the state asynchronously to an XML file after a mutation and 52 * loading the from an XML file on construction. 53 * <p> 54 * This class uses the same lock as the settings provider to ensure that 55 * multiple changes made by the settings provider, e,g, upgrade, bulk insert, 56 * etc, are atomically persisted since the asynchronous persistence is using 57 * the same lock to grab the current state to write to disk. 58 * </p> 59 */ 60 final class SettingsState { 61 private static final boolean DEBUG = false; 62 private static final boolean DEBUG_PERSISTENCE = false; 63 64 private static final String LOG_TAG = "SettingsState"; 65 66 static final int SETTINGS_VERSION_NEW_ENCODING = 121; 67 68 private static final long WRITE_SETTINGS_DELAY_MILLIS = 200; 69 private static final long MAX_WRITE_SETTINGS_DELAY_MILLIS = 2000; 70 71 public static final int MAX_BYTES_PER_APP_PACKAGE_UNLIMITED = -1; 72 public static final int MAX_BYTES_PER_APP_PACKAGE_LIMITED = 20000; 73 74 public static final String SYSTEM_PACKAGE_NAME = "android"; 75 76 public static final int VERSION_UNDEFINED = -1; 77 78 private static final String TAG_SETTINGS = "settings"; 79 private static final String TAG_SETTING = "setting"; 80 private static final String ATTR_PACKAGE = "package"; 81 82 private static final String ATTR_VERSION = "version"; 83 private static final String ATTR_ID = "id"; 84 private static final String ATTR_NAME = "name"; 85 86 /** Non-binary value will be written in this attribute. */ 87 private static final String ATTR_VALUE = "value"; 88 89 /** 90 * KXmlSerializer won't like some characters. We encode such characters in base64 and 91 * store in this attribute. 92 * NOTE: A null value will have NEITHER ATTR_VALUE nor ATTR_VALUE_BASE64. 93 */ 94 private static final String ATTR_VALUE_BASE64 = "valueBase64"; 95 96 // This was used in version 120 and before. 97 private static final String NULL_VALUE_OLD_STYLE = "null"; 98 99 private static final int HISTORICAL_OPERATION_COUNT = 20; 100 private static final String HISTORICAL_OPERATION_UPDATE = "update"; 101 private static final String HISTORICAL_OPERATION_DELETE = "delete"; 102 private static final String HISTORICAL_OPERATION_PERSIST = "persist"; 103 private static final String HISTORICAL_OPERATION_INITIALIZE = "initialize"; 104 105 private final Object mLock; 106 107 private final Handler mHandler; 108 109 @GuardedBy("mLock") 110 private final ArrayMap<String, Setting> mSettings = new ArrayMap<>(); 111 112 @GuardedBy("mLock") 113 private final ArrayMap<String, Integer> mPackageToMemoryUsage; 114 115 @GuardedBy("mLock") 116 private final int mMaxBytesPerAppPackage; 117 118 @GuardedBy("mLock") 119 private final File mStatePersistFile; 120 121 private final Setting mNullSetting = new Setting(null, null, null) { 122 @Override 123 public boolean isNull() { 124 return true; 125 } 126 }; 127 128 @GuardedBy("mLock") 129 private final List<HistoricalOperation> mHistoricalOperations; 130 131 @GuardedBy("mLock") 132 public final int mKey; 133 134 @GuardedBy("mLock") 135 private int mVersion = VERSION_UNDEFINED; 136 137 @GuardedBy("mLock") 138 private long mLastNotWrittenMutationTimeMillis; 139 140 @GuardedBy("mLock") 141 private boolean mDirty; 142 143 @GuardedBy("mLock") 144 private boolean mWriteScheduled; 145 146 @GuardedBy("mLock") 147 private long mNextId; 148 149 @GuardedBy("mLock") 150 private int mNextHistoricalOpIdx; 151 152 public SettingsState(Object lock, File file, int key, int maxBytesPerAppPackage, 153 Looper looper) { 154 // It is important that we use the same lock as the settings provider 155 // to ensure multiple mutations on this state are atomicaly persisted 156 // as the async persistence should be blocked while we make changes. 157 mLock = lock; 158 mStatePersistFile = file; 159 mKey = key; 160 mHandler = new MyHandler(looper); 161 if (maxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_LIMITED) { 162 mMaxBytesPerAppPackage = maxBytesPerAppPackage; 163 mPackageToMemoryUsage = new ArrayMap<>(); 164 } else { 165 mMaxBytesPerAppPackage = maxBytesPerAppPackage; 166 mPackageToMemoryUsage = null; 167 } 168 169 mHistoricalOperations = Build.IS_DEBUGGABLE 170 ? new ArrayList<>(HISTORICAL_OPERATION_COUNT) : null; 171 172 synchronized (mLock) { 173 readStateSyncLocked(); 174 } 175 } 176 177 // The settings provider must hold its lock when calling here. 178 public int getVersionLocked() { 179 return mVersion; 180 } 181 182 public Setting getNullSetting() { 183 return mNullSetting; 184 } 185 186 // The settings provider must hold its lock when calling here. 187 public void setVersionLocked(int version) { 188 if (version == mVersion) { 189 return; 190 } 191 mVersion = version; 192 193 scheduleWriteIfNeededLocked(); 194 } 195 196 // The settings provider must hold its lock when calling here. 197 public void onPackageRemovedLocked(String packageName) { 198 boolean removedSomething = false; 199 200 final int settingCount = mSettings.size(); 201 for (int i = settingCount - 1; i >= 0; i--) { 202 String name = mSettings.keyAt(i); 203 // Settings defined by us are never dropped. 204 if (Settings.System.PUBLIC_SETTINGS.contains(name) 205 || Settings.System.PRIVATE_SETTINGS.contains(name)) { 206 continue; 207 } 208 Setting setting = mSettings.valueAt(i); 209 if (packageName.equals(setting.packageName)) { 210 mSettings.removeAt(i); 211 removedSomething = true; 212 } 213 } 214 215 if (removedSomething) { 216 scheduleWriteIfNeededLocked(); 217 } 218 } 219 220 // The settings provider must hold its lock when calling here. 221 public List<String> getSettingNamesLocked() { 222 ArrayList<String> names = new ArrayList<>(); 223 final int settingsCount = mSettings.size(); 224 for (int i = 0; i < settingsCount; i++) { 225 String name = mSettings.keyAt(i); 226 names.add(name); 227 } 228 return names; 229 } 230 231 // The settings provider must hold its lock when calling here. 232 public Setting getSettingLocked(String name) { 233 if (TextUtils.isEmpty(name)) { 234 return mNullSetting; 235 } 236 Setting setting = mSettings.get(name); 237 if (setting != null) { 238 return new Setting(setting); 239 } 240 return mNullSetting; 241 } 242 243 // The settings provider must hold its lock when calling here. 244 public boolean updateSettingLocked(String name, String value, String packageName) { 245 if (!hasSettingLocked(name)) { 246 return false; 247 } 248 249 return insertSettingLocked(name, value, packageName); 250 } 251 252 // The settings provider must hold its lock when calling here. 253 public boolean insertSettingLocked(String name, String value, String packageName) { 254 if (TextUtils.isEmpty(name)) { 255 return false; 256 } 257 258 Setting oldState = mSettings.get(name); 259 String oldValue = (oldState != null) ? oldState.value : null; 260 Setting newState; 261 262 if (oldState != null) { 263 if (!oldState.update(value, packageName)) { 264 return false; 265 } 266 newState = oldState; 267 } else { 268 newState = new Setting(name, value, packageName); 269 mSettings.put(name, newState); 270 } 271 272 addHistoricalOperationLocked(HISTORICAL_OPERATION_UPDATE, newState); 273 274 updateMemoryUsagePerPackageLocked(packageName, oldValue, value); 275 276 scheduleWriteIfNeededLocked(); 277 278 return true; 279 } 280 281 // The settings provider must hold its lock when calling here. 282 public void persistSyncLocked() { 283 mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS); 284 doWriteState(); 285 } 286 287 // The settings provider must hold its lock when calling here. 288 public boolean deleteSettingLocked(String name) { 289 if (TextUtils.isEmpty(name) || !hasSettingLocked(name)) { 290 return false; 291 } 292 293 Setting oldState = mSettings.remove(name); 294 295 updateMemoryUsagePerPackageLocked(oldState.packageName, oldState.value, null); 296 297 addHistoricalOperationLocked(HISTORICAL_OPERATION_DELETE, oldState); 298 299 scheduleWriteIfNeededLocked(); 300 301 return true; 302 } 303 304 // The settings provider must hold its lock when calling here. 305 public void destroyLocked(Runnable callback) { 306 mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS); 307 if (callback != null) { 308 if (mDirty) { 309 // Do it without a delay. 310 mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS, 311 callback).sendToTarget(); 312 return; 313 } 314 callback.run(); 315 } 316 } 317 318 private void addHistoricalOperationLocked(String type, Setting setting) { 319 if (mHistoricalOperations == null) { 320 return; 321 } 322 HistoricalOperation operation = new HistoricalOperation( 323 SystemClock.elapsedRealtime(), type, 324 setting != null ? new Setting(setting) : null); 325 if (mNextHistoricalOpIdx >= mHistoricalOperations.size()) { 326 mHistoricalOperations.add(operation); 327 } else { 328 mHistoricalOperations.set(mNextHistoricalOpIdx, operation); 329 } 330 mNextHistoricalOpIdx++; 331 if (mNextHistoricalOpIdx >= HISTORICAL_OPERATION_COUNT) { 332 mNextHistoricalOpIdx = 0; 333 } 334 } 335 336 public void dumpHistoricalOperations(PrintWriter pw) { 337 synchronized (mLock) { 338 if (mHistoricalOperations == null) { 339 return; 340 } 341 pw.println("Historical operations"); 342 final int operationCount = mHistoricalOperations.size(); 343 for (int i = 0; i < operationCount; i++) { 344 int index = mNextHistoricalOpIdx - 1 - i; 345 if (index < 0) { 346 index = operationCount + index; 347 } 348 HistoricalOperation operation = mHistoricalOperations.get(index); 349 pw.print(TimeUtils.formatForLogging(operation.mTimestamp)); 350 pw.print(" "); 351 pw.print(operation.mOperation); 352 if (operation.mSetting != null) { 353 pw.print(" "); 354 pw.print(operation.mSetting); 355 } 356 pw.println(); 357 } 358 pw.println(); 359 pw.println(); 360 } 361 } 362 363 private void updateMemoryUsagePerPackageLocked(String packageName, String oldValue, 364 String newValue) { 365 if (mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED) { 366 return; 367 } 368 369 if (SYSTEM_PACKAGE_NAME.equals(packageName)) { 370 return; 371 } 372 373 final int oldValueSize = (oldValue != null) ? oldValue.length() : 0; 374 final int newValueSize = (newValue != null) ? newValue.length() : 0; 375 final int deltaSize = newValueSize - oldValueSize; 376 377 Integer currentSize = mPackageToMemoryUsage.get(packageName); 378 final int newSize = Math.max((currentSize != null) 379 ? currentSize + deltaSize : deltaSize, 0); 380 381 if (newSize > mMaxBytesPerAppPackage) { 382 throw new IllegalStateException("You are adding too many system settings. " 383 + "You should stop using system settings for app specific data" 384 + " package: " + packageName); 385 } 386 387 if (DEBUG) { 388 Slog.i(LOG_TAG, "Settings for package: " + packageName 389 + " size: " + newSize + " bytes."); 390 } 391 392 mPackageToMemoryUsage.put(packageName, newSize); 393 } 394 395 private boolean hasSettingLocked(String name) { 396 return mSettings.indexOfKey(name) >= 0; 397 } 398 399 private void scheduleWriteIfNeededLocked() { 400 // If dirty then we have a write already scheduled. 401 if (!mDirty) { 402 mDirty = true; 403 writeStateAsyncLocked(); 404 } 405 } 406 407 private void writeStateAsyncLocked() { 408 final long currentTimeMillis = SystemClock.uptimeMillis(); 409 410 if (mWriteScheduled) { 411 mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS); 412 413 // If enough time passed, write without holding off anymore. 414 final long timeSinceLastNotWrittenMutationMillis = currentTimeMillis 415 - mLastNotWrittenMutationTimeMillis; 416 if (timeSinceLastNotWrittenMutationMillis >= MAX_WRITE_SETTINGS_DELAY_MILLIS) { 417 mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS).sendToTarget(); 418 return; 419 } 420 421 // Hold off a bit more as settings are frequently changing. 422 final long maxDelayMillis = Math.max(mLastNotWrittenMutationTimeMillis 423 + MAX_WRITE_SETTINGS_DELAY_MILLIS - currentTimeMillis, 0); 424 final long writeDelayMillis = Math.min(WRITE_SETTINGS_DELAY_MILLIS, maxDelayMillis); 425 426 Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS); 427 mHandler.sendMessageDelayed(message, writeDelayMillis); 428 } else { 429 mLastNotWrittenMutationTimeMillis = currentTimeMillis; 430 Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS); 431 mHandler.sendMessageDelayed(message, WRITE_SETTINGS_DELAY_MILLIS); 432 mWriteScheduled = true; 433 } 434 } 435 436 private void doWriteState() { 437 if (DEBUG_PERSISTENCE) { 438 Slog.i(LOG_TAG, "[PERSIST START]"); 439 } 440 441 AtomicFile destination = new AtomicFile(mStatePersistFile); 442 443 final int version; 444 final ArrayMap<String, Setting> settings; 445 446 synchronized (mLock) { 447 version = mVersion; 448 settings = new ArrayMap<>(mSettings); 449 mDirty = false; 450 mWriteScheduled = false; 451 } 452 453 FileOutputStream out = null; 454 try { 455 out = destination.startWrite(); 456 457 XmlSerializer serializer = Xml.newSerializer(); 458 serializer.setOutput(out, StandardCharsets.UTF_8.name()); 459 serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 460 serializer.startDocument(null, true); 461 serializer.startTag(null, TAG_SETTINGS); 462 serializer.attribute(null, ATTR_VERSION, String.valueOf(version)); 463 464 final int settingCount = settings.size(); 465 for (int i = 0; i < settingCount; i++) { 466 Setting setting = settings.valueAt(i); 467 468 writeSingleSetting(mVersion, serializer, setting.getId(), setting.getName(), 469 setting.getValue(), setting.getPackageName()); 470 471 if (DEBUG_PERSISTENCE) { 472 Slog.i(LOG_TAG, "[PERSISTED]" + setting.getName() + "=" + setting.getValue()); 473 } 474 } 475 476 serializer.endTag(null, TAG_SETTINGS); 477 serializer.endDocument(); 478 destination.finishWrite(out); 479 480 synchronized (mLock) { 481 addHistoricalOperationLocked(HISTORICAL_OPERATION_PERSIST, null); 482 } 483 484 if (DEBUG_PERSISTENCE) { 485 Slog.i(LOG_TAG, "[PERSIST END]"); 486 } 487 } catch (Throwable t) { 488 Slog.wtf(LOG_TAG, "Failed to write settings, restoring backup", t); 489 destination.failWrite(out); 490 } finally { 491 IoUtils.closeQuietly(out); 492 } 493 } 494 495 static void writeSingleSetting(int version, XmlSerializer serializer, String id, 496 String name, String value, String packageName) throws IOException { 497 if (id == null || isBinary(id) || name == null || isBinary(name) 498 || packageName == null || isBinary(packageName)) { 499 // This shouldn't happen. 500 return; 501 } 502 serializer.startTag(null, TAG_SETTING); 503 serializer.attribute(null, ATTR_ID, id); 504 serializer.attribute(null, ATTR_NAME, name); 505 setValueAttribute(version, serializer, value); 506 serializer.attribute(null, ATTR_PACKAGE, packageName); 507 serializer.endTag(null, TAG_SETTING); 508 } 509 510 static void setValueAttribute(int version, XmlSerializer serializer, String value) 511 throws IOException { 512 if (version >= SETTINGS_VERSION_NEW_ENCODING) { 513 if (value == null) { 514 // Null value -> No ATTR_VALUE nor ATTR_VALUE_BASE64. 515 } else if (isBinary(value)) { 516 serializer.attribute(null, ATTR_VALUE_BASE64, base64Encode(value)); 517 } else { 518 serializer.attribute(null, ATTR_VALUE, value); 519 } 520 } else { 521 // Old encoding. 522 if (value == null) { 523 serializer.attribute(null, ATTR_VALUE, NULL_VALUE_OLD_STYLE); 524 } else { 525 serializer.attribute(null, ATTR_VALUE, value); 526 } 527 } 528 } 529 530 private String getValueAttribute(XmlPullParser parser) { 531 if (mVersion >= SETTINGS_VERSION_NEW_ENCODING) { 532 final String value = parser.getAttributeValue(null, ATTR_VALUE); 533 if (value != null) { 534 return value; 535 } 536 final String base64 = parser.getAttributeValue(null, ATTR_VALUE_BASE64); 537 if (base64 != null) { 538 return base64Decode(base64); 539 } 540 // null has neither ATTR_VALUE nor ATTR_VALUE_BASE64. 541 return null; 542 } else { 543 // Old encoding. 544 final String stored = parser.getAttributeValue(null, ATTR_VALUE); 545 if (NULL_VALUE_OLD_STYLE.equals(stored)) { 546 return null; 547 } else { 548 return stored; 549 } 550 } 551 } 552 553 private void readStateSyncLocked() { 554 FileInputStream in; 555 if (!mStatePersistFile.exists()) { 556 Slog.i(LOG_TAG, "No settings state " + mStatePersistFile); 557 addHistoricalOperationLocked(HISTORICAL_OPERATION_INITIALIZE, null); 558 return; 559 } 560 try { 561 in = new AtomicFile(mStatePersistFile).openRead(); 562 } catch (FileNotFoundException fnfe) { 563 String message = "No settings state " + mStatePersistFile; 564 Slog.wtf(LOG_TAG, message); 565 Slog.i(LOG_TAG, message); 566 return; 567 } 568 try { 569 XmlPullParser parser = Xml.newPullParser(); 570 parser.setInput(in, StandardCharsets.UTF_8.name()); 571 parseStateLocked(parser); 572 } catch (XmlPullParserException | IOException e) { 573 String message = "Failed parsing settings file: " + mStatePersistFile; 574 Slog.wtf(LOG_TAG, message); 575 throw new IllegalStateException(message , e); 576 } finally { 577 IoUtils.closeQuietly(in); 578 } 579 } 580 581 private void parseStateLocked(XmlPullParser parser) 582 throws IOException, XmlPullParserException { 583 final int outerDepth = parser.getDepth(); 584 int type; 585 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 586 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 587 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 588 continue; 589 } 590 591 String tagName = parser.getName(); 592 if (tagName.equals(TAG_SETTINGS)) { 593 parseSettingsLocked(parser); 594 } 595 } 596 } 597 598 private void parseSettingsLocked(XmlPullParser parser) 599 throws IOException, XmlPullParserException { 600 601 mVersion = Integer.parseInt(parser.getAttributeValue(null, ATTR_VERSION)); 602 603 final int outerDepth = parser.getDepth(); 604 int type; 605 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 606 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 607 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 608 continue; 609 } 610 611 String tagName = parser.getName(); 612 if (tagName.equals(TAG_SETTING)) { 613 String id = parser.getAttributeValue(null, ATTR_ID); 614 String name = parser.getAttributeValue(null, ATTR_NAME); 615 String value = getValueAttribute(parser); 616 String packageName = parser.getAttributeValue(null, ATTR_PACKAGE); 617 mSettings.put(name, new Setting(name, value, packageName, id)); 618 619 if (DEBUG_PERSISTENCE) { 620 Slog.i(LOG_TAG, "[RESTORED] " + name + "=" + value); 621 } 622 } 623 } 624 } 625 626 private final class MyHandler extends Handler { 627 public static final int MSG_PERSIST_SETTINGS = 1; 628 629 public MyHandler(Looper looper) { 630 super(looper); 631 } 632 633 @Override 634 public void handleMessage(Message message) { 635 switch (message.what) { 636 case MSG_PERSIST_SETTINGS: { 637 Runnable callback = (Runnable) message.obj; 638 doWriteState(); 639 if (callback != null) { 640 callback.run(); 641 } 642 } 643 break; 644 } 645 } 646 } 647 648 private class HistoricalOperation { 649 final long mTimestamp; 650 final String mOperation; 651 final Setting mSetting; 652 653 public HistoricalOperation(long timestamp, 654 String operation, Setting setting) { 655 mTimestamp = timestamp; 656 mOperation = operation; 657 mSetting = setting; 658 } 659 } 660 661 class Setting { 662 private String name; 663 private String value; 664 private String packageName; 665 private String id; 666 667 public Setting(Setting other) { 668 name = other.name; 669 value = other.value; 670 packageName = other.packageName; 671 id = other.id; 672 } 673 674 public Setting(String name, String value, String packageName) { 675 init(name, value, packageName, String.valueOf(mNextId++)); 676 } 677 678 public Setting(String name, String value, String packageName, String id) { 679 mNextId = Math.max(mNextId, Long.valueOf(id) + 1); 680 init(name, value, packageName, id); 681 } 682 683 private void init(String name, String value, String packageName, String id) { 684 this.name = name; 685 this.value = value; 686 this.packageName = packageName; 687 this.id = id; 688 } 689 690 public String getName() { 691 return name; 692 } 693 694 public int getkey() { 695 return mKey; 696 } 697 698 public String getValue() { 699 return value; 700 } 701 702 public String getPackageName() { 703 return packageName; 704 } 705 706 public String getId() { 707 return id; 708 } 709 710 public boolean isNull() { 711 return false; 712 } 713 714 public boolean update(String value, String packageName) { 715 if (Objects.equal(value, this.value)) { 716 return false; 717 } 718 this.value = value; 719 this.packageName = packageName; 720 this.id = String.valueOf(mNextId++); 721 return true; 722 } 723 724 public String toString() { 725 return "Setting{name=" + value + " from " + packageName + "}"; 726 } 727 } 728 729 /** 730 * @return TRUE if a string is considered "binary" from KXML's point of view. NOTE DO NOT 731 * pass null. 732 */ 733 public static boolean isBinary(String s) { 734 if (s == null) { 735 throw new NullPointerException(); 736 } 737 // See KXmlSerializer.writeEscaped 738 for (int i = 0; i < s.length(); i++) { 739 char c = s.charAt(i); 740 boolean allowedInXml = (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); 741 if (!allowedInXml) { 742 return true; 743 } 744 } 745 return false; 746 } 747 748 private static String base64Encode(String s) { 749 return Base64.encodeToString(toBytes(s), Base64.NO_WRAP); 750 } 751 752 private static String base64Decode(String s) { 753 return fromBytes(Base64.decode(s, Base64.DEFAULT)); 754 } 755 756 // Note the followings are basically just UTF-16 encode/decode. But we want to preserve 757 // contents as-is, even if it contains broken surrogate pairs, we do it by ourselves, 758 // since I don't know how Charset would treat them. 759 760 private static byte[] toBytes(String s) { 761 final byte[] result = new byte[s.length() * 2]; 762 int resultIndex = 0; 763 for (int i = 0; i < s.length(); ++i) { 764 char ch = s.charAt(i); 765 result[resultIndex++] = (byte) (ch >> 8); 766 result[resultIndex++] = (byte) ch; 767 } 768 return result; 769 } 770 771 private static String fromBytes(byte[] bytes) { 772 final StringBuffer sb = new StringBuffer(bytes.length / 2); 773 774 final int last = bytes.length - 1; 775 776 for (int i = 0; i < last; i += 2) { 777 final char ch = (char) ((bytes[i] & 0xff) << 8 | (bytes[i + 1] & 0xff)); 778 sb.append(ch); 779 } 780 return sb.toString(); 781 } 782 } 783