mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 09:08:33 +00:00
Add an encrypted key-value store.
SignalStore is backed by SQLCipher and is intended to be used instead of TextSecurePreferences moving forward.
This commit is contained in:
parent
711d22a0ed
commit
4b5b9fbde8
@ -52,7 +52,7 @@ import org.thoughtcrime.securesms.logging.AndroidLogger;
|
|||||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||||
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
|
import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
|
||||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
||||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||||
@ -70,7 +70,6 @@ import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
|||||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||||
import org.thoughtcrime.securesms.stickers.BlessedPacks;
|
import org.thoughtcrime.securesms.stickers.BlessedPacks;
|
||||||
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||||
@ -217,7 +216,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||||||
|
|
||||||
private void initializeCrashHandling() {
|
private void initializeCrashHandling() {
|
||||||
final Thread.UncaughtExceptionHandler originalHandler = Thread.getDefaultUncaughtExceptionHandler();
|
final Thread.UncaughtExceptionHandler originalHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(originalHandler));
|
Thread.setDefaultUncaughtExceptionHandler(new SignalUncaughtExceptionHandler(originalHandler));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeApplicationMigrations() {
|
private void initializeApplicationMigrations() {
|
||||||
|
@ -60,6 +60,7 @@ public class DatabaseFactory {
|
|||||||
private final JobDatabase jobDatabase;
|
private final JobDatabase jobDatabase;
|
||||||
private final StickerDatabase stickerDatabase;
|
private final StickerDatabase stickerDatabase;
|
||||||
private final StorageKeyDatabase storageKeyDatabase;
|
private final StorageKeyDatabase storageKeyDatabase;
|
||||||
|
private final KeyValueDatabase keyValueDatabase;
|
||||||
|
|
||||||
public static DatabaseFactory getInstance(Context context) {
|
public static DatabaseFactory getInstance(Context context) {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
@ -150,6 +151,10 @@ public class DatabaseFactory {
|
|||||||
return getInstance(context).storageKeyDatabase;
|
return getInstance(context).storageKeyDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static KeyValueDatabase getKeyValueDatabase(Context context) {
|
||||||
|
return getInstance(context).keyValueDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
public static SQLiteDatabase getBackupDatabase(Context context) {
|
public static SQLiteDatabase getBackupDatabase(Context context) {
|
||||||
return getInstance(context).databaseHelper.getReadableDatabase();
|
return getInstance(context).databaseHelper.getReadableDatabase();
|
||||||
}
|
}
|
||||||
@ -187,6 +192,7 @@ public class DatabaseFactory {
|
|||||||
this.jobDatabase = new JobDatabase(context, databaseHelper);
|
this.jobDatabase = new JobDatabase(context, databaseHelper);
|
||||||
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
|
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
|
||||||
this.storageKeyDatabase = new StorageKeyDatabase(context, databaseHelper);
|
this.storageKeyDatabase = new StorageKeyDatabase(context, databaseHelper);
|
||||||
|
this.keyValueDatabase = new KeyValueDatabase(context, databaseHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,
|
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,
|
||||||
|
@ -0,0 +1,135 @@
|
|||||||
|
package org.thoughtcrime.securesms.database;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class KeyValueDatabase extends Database {
|
||||||
|
|
||||||
|
private static final String TABLE_NAME = "key_value";
|
||||||
|
|
||||||
|
private static final String ID = "_id";
|
||||||
|
private static final String KEY = "key";
|
||||||
|
private static final String VALUE = "value";
|
||||||
|
private static final String TYPE = "type";
|
||||||
|
|
||||||
|
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||||
|
KEY + " TEXT UNIQUE, " +
|
||||||
|
VALUE + " TEXT, " +
|
||||||
|
TYPE + " INTEGER)";
|
||||||
|
|
||||||
|
KeyValueDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||||
|
super(context, databaseHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull KeyValueDataSet getDataSet() {
|
||||||
|
KeyValueDataSet dataSet = new KeyValueDataSet();
|
||||||
|
|
||||||
|
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null)){
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
Type type = Type.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)));
|
||||||
|
String key = cursor.getString(cursor.getColumnIndexOrThrow(KEY));
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case BLOB:
|
||||||
|
dataSet.putBlob(key, cursor.getBlob(cursor.getColumnIndexOrThrow(VALUE)));
|
||||||
|
break;
|
||||||
|
case BOOLEAN:
|
||||||
|
dataSet.putBoolean(key, cursor.getInt(cursor.getColumnIndexOrThrow(VALUE)) == 1);
|
||||||
|
break;
|
||||||
|
case FLOAT:
|
||||||
|
dataSet.putFloat(key, cursor.getFloat(cursor.getColumnIndexOrThrow(VALUE)));
|
||||||
|
break;
|
||||||
|
case INTEGER:
|
||||||
|
dataSet.putInteger(key, cursor.getInt(cursor.getColumnIndexOrThrow(VALUE)));
|
||||||
|
break;
|
||||||
|
case LONG:
|
||||||
|
dataSet.putLong(key, cursor.getLong(cursor.getColumnIndexOrThrow(VALUE)));
|
||||||
|
break;
|
||||||
|
case STRING:
|
||||||
|
dataSet.putString(key, cursor.getString(cursor.getColumnIndexOrThrow(VALUE)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeDataSet(@NonNull KeyValueDataSet dataSet, @NonNull Collection<String> removes) {
|
||||||
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
|
||||||
|
db.beginTransaction();
|
||||||
|
try {
|
||||||
|
for (Map.Entry<String, Object> entry : dataSet.getValues().entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
Object value = entry.getValue();
|
||||||
|
Class type = dataSet.getType(key);
|
||||||
|
|
||||||
|
ContentValues contentValues = new ContentValues(3);
|
||||||
|
contentValues.put(KEY, key);
|
||||||
|
|
||||||
|
if (type == byte[].class) {
|
||||||
|
contentValues.put(VALUE, (byte[]) value);
|
||||||
|
contentValues.put(TYPE, Type.BLOB.getId());
|
||||||
|
} else if (type == Boolean.class) {
|
||||||
|
contentValues.put(VALUE, (boolean) value);
|
||||||
|
contentValues.put(TYPE, Type.BOOLEAN.getId());
|
||||||
|
} else if (type == Float.class) {
|
||||||
|
contentValues.put(VALUE, (float) value);
|
||||||
|
contentValues.put(TYPE, Type.FLOAT.getId());
|
||||||
|
} else if (type == Integer.class) {
|
||||||
|
contentValues.put(VALUE, (int) value);
|
||||||
|
contentValues.put(TYPE, Type.INTEGER.getId());
|
||||||
|
} else if (type == Long.class) {
|
||||||
|
contentValues.put(VALUE, (long) value);
|
||||||
|
contentValues.put(TYPE, Type.LONG.getId());
|
||||||
|
} else if (type == String.class) {
|
||||||
|
contentValues.put(VALUE, (String) value);
|
||||||
|
contentValues.put(TYPE, Type.STRING.getId());
|
||||||
|
} else {
|
||||||
|
throw new AssertionError("Unknown type: " + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.insertWithOnConflict(TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
String deleteQuery = KEY + " = ?";
|
||||||
|
for (String remove : removes) {
|
||||||
|
db.delete(TABLE_NAME, deleteQuery, new String[] { remove });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
} finally {
|
||||||
|
db.endTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Type {
|
||||||
|
BLOB(0), BOOLEAN(1), FLOAT(2), INTEGER(3), LONG(4), STRING(5);
|
||||||
|
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
Type(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Type fromId(int id) {
|
||||||
|
return values()[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.database.GroupDatabase;
|
|||||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||||
@ -99,8 +100,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
private static final int STORAGE_SERVICE = 38;
|
private static final int STORAGE_SERVICE = 38;
|
||||||
private static final int REACTIONS_UNREAD_INDEX = 39;
|
private static final int REACTIONS_UNREAD_INDEX = 39;
|
||||||
private static final int RESUMABLE_DOWNLOADS = 40;
|
private static final int RESUMABLE_DOWNLOADS = 40;
|
||||||
|
private static final int KEY_VALUE_STORE = 41;
|
||||||
|
|
||||||
private static final int DATABASE_VERSION = 40;
|
private static final int DATABASE_VERSION = 41;
|
||||||
private static final String DATABASE_NAME = "signal.db";
|
private static final String DATABASE_NAME = "signal.db";
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@ -142,6 +144,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.execSQL(SessionDatabase.CREATE_TABLE);
|
db.execSQL(SessionDatabase.CREATE_TABLE);
|
||||||
db.execSQL(StickerDatabase.CREATE_TABLE);
|
db.execSQL(StickerDatabase.CREATE_TABLE);
|
||||||
db.execSQL(StorageKeyDatabase.CREATE_TABLE);
|
db.execSQL(StorageKeyDatabase.CREATE_TABLE);
|
||||||
|
db.execSQL(KeyValueDatabase.CREATE_TABLE);
|
||||||
executeStatements(db, SearchDatabase.CREATE_TABLE);
|
executeStatements(db, SearchDatabase.CREATE_TABLE);
|
||||||
executeStatements(db, JobDatabase.CREATE_TABLE);
|
executeStatements(db, JobDatabase.CREATE_TABLE);
|
||||||
|
|
||||||
@ -684,6 +687,13 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.execSQL("ALTER TABLE part ADD COLUMN transfer_file TEXT DEFAULT NULL");
|
db.execSQL("ALTER TABLE part ADD COLUMN transfer_file TEXT DEFAULT NULL");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < KEY_VALUE_STORE) {
|
||||||
|
db.execSQL("CREATE TABLE key_value (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||||
|
"key TEXT UNIQUE, " +
|
||||||
|
"value TEXT, " +
|
||||||
|
"type INTEGER)");
|
||||||
|
}
|
||||||
|
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
} finally {
|
} finally {
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
|
@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.BuildConfig;
|
|||||||
import org.thoughtcrime.securesms.IncomingMessageProcessor;
|
import org.thoughtcrime.securesms.IncomingMessageProcessor;
|
||||||
import org.thoughtcrime.securesms.gcm.MessageRetriever;
|
import org.thoughtcrime.securesms.gcm.MessageRetriever;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.KeyValueStore;
|
||||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
||||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||||
@ -41,6 +42,7 @@ public class ApplicationDependencies {
|
|||||||
private static LiveRecipientCache recipientCache;
|
private static LiveRecipientCache recipientCache;
|
||||||
private static JobManager jobManager;
|
private static JobManager jobManager;
|
||||||
private static FrameRateTracker frameRateTracker;
|
private static FrameRateTracker frameRateTracker;
|
||||||
|
private static KeyValueStore keyValueStore;
|
||||||
|
|
||||||
public static synchronized void init(@NonNull Application application, @NonNull Provider provider) {
|
public static synchronized void init(@NonNull Application application, @NonNull Provider provider) {
|
||||||
if (ApplicationDependencies.application != null || ApplicationDependencies.provider != null) {
|
if (ApplicationDependencies.application != null || ApplicationDependencies.provider != null) {
|
||||||
@ -157,6 +159,16 @@ public class ApplicationDependencies {
|
|||||||
return frameRateTracker;
|
return frameRateTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static synchronized @NonNull KeyValueStore getKeyValueStore() {
|
||||||
|
assertInitialization();
|
||||||
|
|
||||||
|
if (keyValueStore == null) {
|
||||||
|
keyValueStore = provider.provideKeyValueStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyValueStore;
|
||||||
|
}
|
||||||
|
|
||||||
private static void assertInitialization() {
|
private static void assertInitialization() {
|
||||||
if (application == null || provider == null) {
|
if (application == null || provider == null) {
|
||||||
throw new UninitializedException();
|
throw new UninitializedException();
|
||||||
@ -173,6 +185,7 @@ public class ApplicationDependencies {
|
|||||||
@NonNull LiveRecipientCache provideRecipientCache();
|
@NonNull LiveRecipientCache provideRecipientCache();
|
||||||
@NonNull JobManager provideJobManager();
|
@NonNull JobManager provideJobManager();
|
||||||
@NonNull FrameRateTracker provideFrameRateTracker();
|
@NonNull FrameRateTracker provideFrameRateTracker();
|
||||||
|
@NonNull KeyValueStore provideKeyValueStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class UninitializedException extends IllegalStateException {
|
private static class UninitializedException extends IllegalStateException {
|
||||||
|
@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.jobmanager.JobMigrator;
|
|||||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||||
import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
||||||
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.KeyValueStore;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.push.SecurityEventListener;
|
import org.thoughtcrime.securesms.push.SecurityEventListener;
|
||||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||||
@ -119,6 +120,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
|||||||
return new FrameRateTracker(context);
|
return new FrameRateTracker(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull KeyValueStore provideKeyValueStore() {
|
||||||
|
return new KeyValueStore(context);
|
||||||
|
}
|
||||||
|
|
||||||
private static class DynamicCredentialsProvider implements CredentialsProvider {
|
private static class DynamicCredentialsProvider implements CredentialsProvider {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
package org.thoughtcrime.securesms.keyvalue;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class KeyValueDataSet implements KeyValueReader {
|
||||||
|
private final Map<String, Object> values = new HashMap<>();
|
||||||
|
private final Map<String, Class> types = new HashMap<>();
|
||||||
|
|
||||||
|
public void putBlob(@NonNull String key, byte[] value) {
|
||||||
|
values.put(key, value);
|
||||||
|
types.put(key, byte[].class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putBoolean(@NonNull String key, boolean value) {
|
||||||
|
values.put(key, value);
|
||||||
|
types.put(key, Boolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putFloat(@NonNull String key, float value) {
|
||||||
|
values.put(key, value);
|
||||||
|
types.put(key, Float.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putInteger(@NonNull String key, int value) {
|
||||||
|
values.put(key, value);
|
||||||
|
types.put(key, Integer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putLong(@NonNull String key, long value) {
|
||||||
|
values.put(key, value);
|
||||||
|
types.put(key, Long.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putString(@NonNull String key, String value) {
|
||||||
|
values.put(key, value);
|
||||||
|
types.put(key, String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
void putAll(@NonNull KeyValueDataSet other) {
|
||||||
|
values.putAll(other.values);
|
||||||
|
types.putAll(other.types);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeAll(@NonNull Collection<String> removes) {
|
||||||
|
for (String remove : removes) {
|
||||||
|
values.remove(remove);
|
||||||
|
types.remove(remove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getBlob(@NonNull String key, byte[] defaultValue) {
|
||||||
|
if (containsKey(key)) {
|
||||||
|
return readValueAsType(key, byte[].class, true);
|
||||||
|
} else {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getBoolean(@NonNull String key, boolean defaultValue) {
|
||||||
|
if (containsKey(key)) {
|
||||||
|
return readValueAsType(key, Boolean.class, false);
|
||||||
|
} else {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getFloat(@NonNull String key, float defaultValue) {
|
||||||
|
if (containsKey(key)) {
|
||||||
|
return readValueAsType(key, Float.class, false);
|
||||||
|
} else {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getInteger(@NonNull String key, int defaultValue) {
|
||||||
|
if (containsKey(key)) {
|
||||||
|
return readValueAsType(key, Integer.class, false);
|
||||||
|
} else {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLong(@NonNull String key, long defaultValue) {
|
||||||
|
if (containsKey(key)) {
|
||||||
|
return readValueAsType(key, Long.class, false);
|
||||||
|
} else {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getString(@NonNull String key, String defaultValue) {
|
||||||
|
if (containsKey(key)) {
|
||||||
|
return readValueAsType(key, String.class, true);
|
||||||
|
} else {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull Map<String, Object> getValues() {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Class getType(@NonNull String key) {
|
||||||
|
return types.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean containsKey(@NonNull String key) {
|
||||||
|
return values.containsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <E> E readValueAsType(@NonNull String key, Class<E> type, boolean nullable) {
|
||||||
|
Object value = values.get(key);
|
||||||
|
if ((value == null && nullable) || (value != null && value.getClass() == type)) {
|
||||||
|
return type.cast(value);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Type mismatch!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package org.thoughtcrime.securesms.keyvalue;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
interface KeyValueReader {
|
||||||
|
byte[] getBlob(@NonNull String key, byte[] defaultValue);
|
||||||
|
boolean getBoolean(@NonNull String key, boolean defaultValue);
|
||||||
|
float getFloat(@NonNull String key, float defaultValue);
|
||||||
|
int getInteger(@NonNull String key, int defaultValue);
|
||||||
|
long getLong(@NonNull String key, long defaultValue);
|
||||||
|
String getString(@NonNull String key, String defaultValue);
|
||||||
|
}
|
@ -0,0 +1,199 @@
|
|||||||
|
package org.thoughtcrime.securesms.keyvalue;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.AnyThread;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An replacement for {@link android.content.SharedPreferences} that stores key-value pairs in our
|
||||||
|
* encrypted database.
|
||||||
|
*
|
||||||
|
* Implemented as a write-through cache that is safe to read and write to on the main thread.
|
||||||
|
*
|
||||||
|
* Writes are enqueued on a separate executor, but writes are finished up in
|
||||||
|
* {@link SignalUncaughtExceptionHandler}, meaning all write should finish barring a native crash
|
||||||
|
* or the system killing us unexpectedly (i.e. a force-stop).
|
||||||
|
*/
|
||||||
|
public final class KeyValueStore implements KeyValueReader {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(KeyValueStore.class);
|
||||||
|
|
||||||
|
private final ExecutorService executor;
|
||||||
|
private final KeyValueDatabase database;
|
||||||
|
|
||||||
|
private KeyValueDataSet dataSet;
|
||||||
|
|
||||||
|
public KeyValueStore(@NonNull Context context) {
|
||||||
|
this.executor = SignalExecutors.newCachedSingleThreadExecutor("signal-KeyValueStore");
|
||||||
|
this.database = DatabaseFactory.getKeyValueDatabase(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
@Override
|
||||||
|
public synchronized byte[] getBlob(@NonNull String key, byte[] defaultValue) {
|
||||||
|
initializeIfNecessary();
|
||||||
|
return dataSet.getBlob(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
@Override
|
||||||
|
public synchronized boolean getBoolean(@NonNull String key, boolean defaultValue) {
|
||||||
|
initializeIfNecessary();
|
||||||
|
return dataSet.getBoolean(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
@Override
|
||||||
|
public synchronized float getFloat(@NonNull String key, float defaultValue) {
|
||||||
|
initializeIfNecessary();
|
||||||
|
return dataSet.getFloat(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
@Override
|
||||||
|
public synchronized int getInteger(@NonNull String key, int defaultValue) {
|
||||||
|
initializeIfNecessary();
|
||||||
|
return dataSet.getInteger(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
@Override
|
||||||
|
public synchronized long getLong(@NonNull String key, long defaultValue) {
|
||||||
|
initializeIfNecessary();
|
||||||
|
return dataSet.getLong(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
@Override
|
||||||
|
public synchronized String getString(@NonNull String key, String defaultValue) {
|
||||||
|
initializeIfNecessary();
|
||||||
|
return dataSet.getString(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A writer that allows writing and removing multiple entries in a single atomic
|
||||||
|
* transaction.
|
||||||
|
*/
|
||||||
|
@AnyThread
|
||||||
|
@NonNull Writer beginWrite() {
|
||||||
|
return new Writer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A reader that lets you read from an immutable snapshot of the store, ensuring that data
|
||||||
|
* is consistent between reads. If you're only reading a single value, it is more
|
||||||
|
* efficient to use the various get* methods instead.
|
||||||
|
*/
|
||||||
|
@AnyThread
|
||||||
|
synchronized @NonNull KeyValueReader beginRead() {
|
||||||
|
initializeIfNecessary();
|
||||||
|
|
||||||
|
KeyValueDataSet copy = new KeyValueDataSet();
|
||||||
|
copy.putAll(dataSet);
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that any pending writes (such as those made via {@link Writer#apply()}) are finished.
|
||||||
|
*/
|
||||||
|
@AnyThread
|
||||||
|
synchronized void blockUntilAllWritesFinished() {
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
executor.execute(latch::countDown);
|
||||||
|
|
||||||
|
try {
|
||||||
|
latch.await();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Log.w(TAG, "Failed to wait for all writes.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private synchronized void write(@NonNull KeyValueDataSet newDataSet, @NonNull Collection<String> removes) {
|
||||||
|
initializeIfNecessary();
|
||||||
|
|
||||||
|
dataSet.putAll(newDataSet);
|
||||||
|
dataSet.removeAll(removes);
|
||||||
|
|
||||||
|
executor.execute(() -> database.writeDataSet(newDataSet, removes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeIfNecessary() {
|
||||||
|
if (dataSet != null) return;
|
||||||
|
this.dataSet = database.getDataSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Writer {
|
||||||
|
private final KeyValueDataSet dataSet = new KeyValueDataSet();
|
||||||
|
private final Set<String> removes = new HashSet<>();
|
||||||
|
|
||||||
|
@NonNull Writer putBlob(@NonNull String key, @Nullable byte[] value) {
|
||||||
|
dataSet.putBlob(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Writer putBoolean(@NonNull String key, boolean value) {
|
||||||
|
dataSet.putBoolean(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Writer putFloat(@NonNull String key, float value) {
|
||||||
|
dataSet.putFloat(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Writer putInteger(@NonNull String key, int value) {
|
||||||
|
dataSet.putInteger(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Writer putLong(@NonNull String key, long value) {
|
||||||
|
dataSet.putLong(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Writer putString(@NonNull String key, String value) {
|
||||||
|
dataSet.putString(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Writer remove(@NonNull String key) {
|
||||||
|
removes.add(key);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
void apply() {
|
||||||
|
for (String key : removes) {
|
||||||
|
if (dataSet.containsKey(key)) {
|
||||||
|
throw new IllegalStateException("Tried to remove a key while also setting it!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write(dataSet, removes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
void commit() {
|
||||||
|
apply();
|
||||||
|
blockUntilAllWritesFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package org.thoughtcrime.securesms.keyvalue;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple, encrypted key-value store.
|
||||||
|
*/
|
||||||
|
public final class SignalStore {
|
||||||
|
|
||||||
|
private SignalStore() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures any pending writes are finished. Only intended to be called by
|
||||||
|
* {@link SignalUncaughtExceptionHandler}.
|
||||||
|
*/
|
||||||
|
public static void blockUntilAllWritesFinished() {
|
||||||
|
getStore().blockUntilAllWritesFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NonNull KeyValueStore getStore() {
|
||||||
|
return ApplicationDependencies.getKeyValueStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void putBlob(@NonNull String key, byte[] value) {
|
||||||
|
getStore().beginWrite().putBlob(key, value).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void putBoolean(@NonNull String key, boolean value) {
|
||||||
|
getStore().beginWrite().putBoolean(key, value).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void putFloat(@NonNull String key, float value) {
|
||||||
|
getStore().beginWrite().putFloat(key, value).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void putInteger(@NonNull String key, int value) {
|
||||||
|
getStore().beginWrite().putInteger(key, value).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void putLong(@NonNull String key, long value) {
|
||||||
|
getStore().beginWrite().putLong(key, value).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void putString(@NonNull String key, String value) {
|
||||||
|
getStore().beginWrite().putString(key, value).apply();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package org.thoughtcrime.securesms.logging;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
|
|
||||||
|
public class SignalUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
|
||||||
|
|
||||||
|
private static final String TAG = SignalUncaughtExceptionHandler.class.getSimpleName();
|
||||||
|
|
||||||
|
private final Thread.UncaughtExceptionHandler originalHandler;
|
||||||
|
|
||||||
|
public SignalUncaughtExceptionHandler(@NonNull Thread.UncaughtExceptionHandler originalHandler) {
|
||||||
|
this.originalHandler = originalHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void uncaughtException(Thread t, Throwable e) {
|
||||||
|
Log.e(TAG, "", e);
|
||||||
|
SignalStore.blockUntilAllWritesFinished();
|
||||||
|
Log.blockUntilAllWritesFinished();
|
||||||
|
originalHandler.uncaughtException(t, e);
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.logging;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public class UncaughtExceptionLogger implements Thread.UncaughtExceptionHandler {
|
|
||||||
|
|
||||||
private static final String TAG = UncaughtExceptionLogger.class.getSimpleName();
|
|
||||||
|
|
||||||
private final Thread.UncaughtExceptionHandler originalHandler;
|
|
||||||
|
|
||||||
public UncaughtExceptionLogger(@NonNull Thread.UncaughtExceptionHandler originalHandler) {
|
|
||||||
this.originalHandler = originalHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void uncaughtException(Thread t, Throwable e) {
|
|
||||||
Log.e(TAG, "", e);
|
|
||||||
Log.blockUntilAllWritesFinished();
|
|
||||||
originalHandler.uncaughtException(t, e);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,204 @@
|
|||||||
|
package org.thoughtcrime.securesms.keyvalue;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static junit.framework.TestCase.assertTrue;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
|
public class KeyValueDataSetTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void containsKey_generic() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putBlob("a", new byte[0]);
|
||||||
|
subject.putBoolean("b", true);
|
||||||
|
subject.putFloat("c", 1.1f);
|
||||||
|
subject.putInteger("d", 3);
|
||||||
|
subject.putLong("e", 7L);
|
||||||
|
subject.putString("f", "spiderman");
|
||||||
|
|
||||||
|
assertTrue(subject.containsKey("a"));
|
||||||
|
assertTrue(subject.containsKey("b"));
|
||||||
|
assertTrue(subject.containsKey("c"));
|
||||||
|
assertTrue(subject.containsKey("d"));
|
||||||
|
assertTrue(subject.containsKey("e"));
|
||||||
|
assertTrue(subject.containsKey("f"));
|
||||||
|
assertFalse(subject.containsKey("venom"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getBlob_positive_nonNull() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putBlob("key", new byte[] { 1 });
|
||||||
|
|
||||||
|
assertEquals(1, subject.getBlob("key", null).length);
|
||||||
|
assertEquals(1, subject.getBlob("key", null)[0]);
|
||||||
|
assertEquals(byte[].class, subject.getType("key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getBlob_positive_null() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putBlob("key", null);
|
||||||
|
|
||||||
|
assertNull(subject.getBlob("key", new byte[0]));
|
||||||
|
assertEquals(byte[].class, subject.getType("key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getBlob_default() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
byte[] value = subject.getBlob("key", new byte[]{1});
|
||||||
|
|
||||||
|
assertEquals(1, value.length);
|
||||||
|
assertEquals(1, value[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void getBlob_negative() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putBlob("key", new byte[0]);
|
||||||
|
|
||||||
|
subject.getInteger("key", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getBoolean_positive() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putBoolean("key", true);
|
||||||
|
|
||||||
|
assertTrue(subject.getBoolean("key", false));
|
||||||
|
assertEquals(Boolean.class, subject.getType("key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void getBoolean_negative() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putBoolean("key", true);
|
||||||
|
|
||||||
|
subject.getInteger("key", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getBoolean_default() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
assertTrue(subject.getBoolean("key", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFloat_positive() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putFloat("key", 1.1f);
|
||||||
|
|
||||||
|
assertEquals(1.1f, subject.getFloat("key", 0), 0.1f);
|
||||||
|
assertEquals(Float.class, subject.getType("key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void getFloat_negative() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putFloat("key", 1.1f);
|
||||||
|
|
||||||
|
subject.getInteger("key", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFloat_default() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
assertEquals(1.1f, subject.getFloat("key", 1.1f), 0.1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getInteger_positive() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putInteger("key", 1);
|
||||||
|
|
||||||
|
assertEquals(1, subject.getInteger("key", 0));
|
||||||
|
assertEquals(Integer.class, subject.getType("key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void getInteger_negative() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putInteger("key", 1);
|
||||||
|
|
||||||
|
subject.getLong("key", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getInteger_default() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
assertEquals(1, subject.getInteger("key", 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getLong_positive() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putLong("key", 1);
|
||||||
|
|
||||||
|
assertEquals(1, subject.getLong("key", 0));
|
||||||
|
assertEquals(Long.class, subject.getType("key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void getLong_negative() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putLong("key", 1L);
|
||||||
|
|
||||||
|
subject.getInteger("key", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getLong_default() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
assertEquals(1, subject.getLong("key", 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getString_positive_nonNull() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putString("key", "spiderman");
|
||||||
|
|
||||||
|
assertEquals("spiderman", subject.getString("key", null));
|
||||||
|
assertEquals(String.class, subject.getType("key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getString_positive_null() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putString("key", null);
|
||||||
|
|
||||||
|
assertNull(subject.getString("key", "something"));
|
||||||
|
assertEquals(String.class, subject.getType("key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void getString_negative() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putString("key", "spiderman");
|
||||||
|
|
||||||
|
subject.getInteger("key", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getString_default() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
assertEquals("spiderman", subject.getString("key", "spiderman"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void remove_generic() {
|
||||||
|
KeyValueDataSet subject = new KeyValueDataSet();
|
||||||
|
subject.putInteger("key", 1);
|
||||||
|
|
||||||
|
assertTrue(subject.containsKey("key"));
|
||||||
|
|
||||||
|
subject.removeAll(Collections.singletonList("key"));
|
||||||
|
|
||||||
|
assertFalse(subject.containsKey("key"));
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user