Allow binary value in SettingsProvider
Now a text value will be written to "value" but a binary value will be encoded
in base64 and stored in "valueBase64".
A null value will have neither value nor valueBase64.
Bug 20202004
Change-Id: I1eae936ff38e3460dc76ca20cc38f8d7e5ec6215
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 5137e1b..aff6ad8 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -342,7 +342,7 @@
}
String name = values.getAsString(Settings.Secure.NAME);
- if (TextUtils.isEmpty(name)) {
+ if (!isKeyValid(name)) {
return null;
}
@@ -406,11 +406,10 @@
return 0;
}
- if (TextUtils.isEmpty(args.name)) {
+ if (!isKeyValid(args.name)) {
return 0;
}
-
switch (args.table) {
case TABLE_GLOBAL: {
final int userId = UserHandle.getCallingUserId();
@@ -446,10 +445,11 @@
return 0;
}
- String value = values.getAsString(Settings.Secure.VALUE);
- if (TextUtils.isEmpty(value)) {
+ String name = values.getAsString(Settings.Secure.NAME);
+ if (!isKeyValid(name)) {
return 0;
}
+ String value = values.getAsString(Settings.Secure.VALUE);
switch (args.table) {
case TABLE_GLOBAL: {
@@ -525,13 +525,20 @@
final int valueColumnIdx = cursor.getColumnIndex(Settings.NameValueTable.VALUE);
do {
- pw.append("_id:").append(cursor.getString(idColumnIdx));
- pw.append(" name:").append(cursor.getString(nameColumnIdx));
- pw.append(" value:").append(cursor.getString(valueColumnIdx));
+ pw.append("_id:").append(toDumpString(cursor.getString(idColumnIdx)));
+ pw.append(" name:").append(toDumpString(cursor.getString(nameColumnIdx)));
+ pw.append(" value:").append(toDumpString(cursor.getString(valueColumnIdx)));
pw.println();
} while (cursor.moveToNext());
}
+ private static final String toDumpString(String s) {
+ if (s != null) {
+ return s;
+ }
+ return "{null}";
+ }
+
private void registerBroadcastReceivers() {
IntentFilter userFilter = new IntentFilter();
userFilter.addAction(Intent.ACTION_USER_REMOVED);
@@ -1280,6 +1287,10 @@
cursor.addRow(values);
}
+ private static boolean isKeyValid(String key) {
+ return !(TextUtils.isEmpty(key) || SettingsState.isBinary(key));
+ }
+
private static final class Arguments {
private static final Pattern WHERE_PATTERN_WITH_PARAM_NO_BRACKETS =
Pattern.compile("[\\s]*name[\\s]*=[\\s]*\\?[\\s]*");
@@ -1812,7 +1823,7 @@
}
private final class UpgradeController {
- private static final int SETTINGS_VERSION = 120;
+ private static final int SETTINGS_VERSION = 121;
private final int mUserId;
@@ -1940,6 +1951,10 @@
currentVersion = 120;
}
+ // Before 121, we used a different string encoding logic. We just bump the version
+ // here; SettingsState knows how to handle pre-version 120 files.
+ currentVersion = 121;
+
// vXXX: Add new settings above this point.
// Return the current version.
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index a2adb15..95d7772 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -23,6 +23,7 @@
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.AtomicFile;
+import android.util.Base64;
import android.util.Slog;
import android.util.Xml;
import com.android.internal.annotations.GuardedBy;
@@ -59,6 +60,8 @@
private static final String LOG_TAG = "SettingsState";
+ static final int SETTINGS_VERSOIN_NEW_ENCODING = 121;
+
private static final long WRITE_SETTINGS_DELAY_MILLIS = 200;
private static final long MAX_WRITE_SETTINGS_DELAY_MILLIS = 2000;
@@ -76,9 +79,19 @@
private static final String ATTR_VERSION = "version";
private static final String ATTR_ID = "id";
private static final String ATTR_NAME = "name";
+
+ /** Non-binary value will be written in this attribute. */
private static final String ATTR_VALUE = "value";
- private static final String NULL_VALUE = "null";
+ /**
+ * KXmlSerializer won't like some characters. We encode such characters in base64 and
+ * store in this attribute.
+ * NOTE: A null value will have NEITHER ATTR_VALUE nor ATTR_VALUE_BASE64.
+ */
+ private static final String ATTR_VALUE_BASE64 = "valueBase64";
+
+ // This was used in version 120 and before.
+ private static final String NULL_VALUE_OLD_STYLE = "null";
private final Object mLock;
@@ -364,12 +377,8 @@
for (int i = 0; i < settingCount; i++) {
Setting setting = settings.valueAt(i);
- serializer.startTag(null, TAG_SETTING);
- serializer.attribute(null, ATTR_ID, setting.getId());
- serializer.attribute(null, ATTR_NAME, setting.getName());
- serializer.attribute(null, ATTR_VALUE, packValue(setting.getValue()));
- serializer.attribute(null, ATTR_PACKAGE, packValue(setting.getPackageName()));
- serializer.endTag(null, TAG_SETTING);
+ writeSingleSetting(mVersion, serializer, setting.getId(), setting.getName(),
+ setting.getValue(), setting.getPackageName());
if (DEBUG_PERSISTENCE) {
Slog.i(LOG_TAG, "[PERSISTED]" + setting.getName() + "=" + setting.getValue());
@@ -394,6 +403,64 @@
}
}
+ static void writeSingleSetting(int version, XmlSerializer serializer, String id,
+ String name, String value, String packageName) throws IOException {
+ if (id == null || isBinary(id) || name == null || isBinary(name)
+ || packageName == null || isBinary(packageName)) {
+ // This shouldn't happen.
+ return;
+ }
+ serializer.startTag(null, TAG_SETTING);
+ serializer.attribute(null, ATTR_ID, id);
+ serializer.attribute(null, ATTR_NAME, name);
+ setValueAttribute(version, serializer, value);
+ serializer.attribute(null, ATTR_PACKAGE, packageName);
+ serializer.endTag(null, TAG_SETTING);
+ }
+
+ static void setValueAttribute(int version, XmlSerializer serializer, String value)
+ throws IOException {
+ if (version >= SETTINGS_VERSOIN_NEW_ENCODING) {
+ if (value == null) {
+ // Null value -> No ATTR_VALUE nor ATTR_VALUE_BASE64.
+ } else if (isBinary(value)) {
+ serializer.attribute(null, ATTR_VALUE_BASE64, base64Encode(value));
+ } else {
+ serializer.attribute(null, ATTR_VALUE, value);
+ }
+ } else {
+ // Old encoding.
+ if (value == null) {
+ serializer.attribute(null, ATTR_VALUE, NULL_VALUE_OLD_STYLE);
+ } else {
+ serializer.attribute(null, ATTR_VALUE, value);
+ }
+ }
+ }
+
+ private String getValueAttribute(XmlPullParser parser) {
+ if (mVersion >= SETTINGS_VERSOIN_NEW_ENCODING) {
+ final String value = parser.getAttributeValue(null, ATTR_VALUE);
+ if (value != null) {
+ return value;
+ }
+ final String base64 = parser.getAttributeValue(null, ATTR_VALUE_BASE64);
+ if (base64 != null) {
+ return base64Decode(base64);
+ }
+ // null has neither ATTR_VALUE nor ATTR_VALUE_BASE64.
+ return null;
+ } else {
+ // Old encoding.
+ final String stored = parser.getAttributeValue(null, ATTR_VALUE);
+ if (NULL_VALUE_OLD_STYLE.equals(stored)) {
+ return null;
+ } else {
+ return stored;
+ }
+ }
+ }
+
private void readStateSyncLocked() {
FileInputStream in;
if (!mStatePersistFile.exists()) {
@@ -452,10 +519,9 @@
if (tagName.equals(TAG_SETTING)) {
String id = parser.getAttributeValue(null, ATTR_ID);
String name = parser.getAttributeValue(null, ATTR_NAME);
- String value = parser.getAttributeValue(null, ATTR_VALUE);
+ String value = getValueAttribute(parser);
String packageName = parser.getAttributeValue(null, ATTR_PACKAGE);
- mSettings.put(name, new Setting(name, unpackValue(value),
- unpackValue(packageName), id));
+ mSettings.put(name, new Setting(name, value, packageName, id));
if (DEBUG_PERSISTENCE) {
Slog.i(LOG_TAG, "[RESTORED] " + name + "=" + value);
@@ -486,20 +552,6 @@
}
}
- private static String packValue(String value) {
- if (value == null) {
- return NULL_VALUE;
- }
- return value;
- }
-
- private static String unpackValue(String value) {
- if (NULL_VALUE.equals(value)) {
- return null;
- }
- return value;
- }
-
public final class Setting {
private String name;
private String value;
@@ -548,4 +600,58 @@
return true;
}
}
+
+ /**
+ * @return TRUE if a string is considered "binary" from KXML's point of view. NOTE DO NOT
+ * pass null.
+ */
+ public static boolean isBinary(String s) {
+ if (s == null) {
+ throw new NullPointerException();
+ }
+ // See KXmlSerializer.writeEscaped
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ boolean allowedInXml = (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd);
+ if (!allowedInXml) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String base64Encode(String s) {
+ return Base64.encodeToString(toBytes(s), Base64.NO_WRAP);
+ }
+
+ private static String base64Decode(String s) {
+ return fromBytes(Base64.decode(s, Base64.DEFAULT));
+ }
+
+ // Note the followings are basically just UTF-16 encode/decode. But we want to preserve
+ // contents as-is, even if it contains broken surrogate pairs, we do it by ourselves,
+ // since I don't know how Charset would treat them.
+
+ private static byte[] toBytes(String s) {
+ final byte[] result = new byte[s.length() * 2];
+ int resultIndex = 0;
+ for (int i = 0; i < s.length(); ++i) {
+ char ch = s.charAt(i);
+ result[resultIndex++] = (byte) (ch >> 8);
+ result[resultIndex++] = (byte) ch;
+ }
+ return result;
+ }
+
+ private static String fromBytes(byte[] bytes) {
+ final StringBuffer sb = new StringBuffer(bytes.length / 2);
+
+ final int last = bytes.length - 1;
+
+ for (int i = 0; i < last; i += 2) {
+ final char ch = (char) ((bytes[i] & 0xff) << 8 | (bytes[i + 1] & 0xff));
+ sb.append(ch);
+ }
+ return sb.toString();
+ }
}