kopia lustrzana https://github.com/ryukoposting/Signal-Android
242 wiersze
8.3 KiB
Java
242 wiersze
8.3 KiB
Java
package org.thoughtcrime.securesms.database;
|
|
|
|
import android.app.Application;
|
|
import android.content.ContentValues;
|
|
import android.database.Cursor;
|
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
|
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook;
|
|
import net.zetetic.database.sqlcipher.SQLiteOpenHelper;
|
|
|
|
import org.signal.core.util.concurrent.SignalExecutors;
|
|
import org.signal.core.util.logging.Log;
|
|
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
|
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
|
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
|
import org.thoughtcrime.securesms.util.CursorUtil;
|
|
import org.thoughtcrime.securesms.util.SqlUtil;
|
|
|
|
import java.util.Collection;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* Persists data for the {@link org.thoughtcrime.securesms.keyvalue.KeyValueStore}.
|
|
*
|
|
* This is it's own separate physical database, so it cannot do joins or queries with any other
|
|
* tables.
|
|
*/
|
|
public class KeyValueDatabase extends SQLiteOpenHelper implements SignalDatabase {
|
|
|
|
private static final String TAG = Log.tag(KeyValueDatabase.class);
|
|
|
|
private static final int DATABASE_VERSION = 1;
|
|
private static final String DATABASE_NAME = "signal-key-value.db";
|
|
|
|
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";
|
|
|
|
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
KEY + " TEXT UNIQUE, " +
|
|
VALUE + " TEXT, " +
|
|
TYPE + " INTEGER)";
|
|
|
|
private static volatile KeyValueDatabase instance;
|
|
|
|
private final Application application;
|
|
|
|
public static @NonNull KeyValueDatabase getInstance(@NonNull Application context) {
|
|
if (instance == null) {
|
|
synchronized (KeyValueDatabase.class) {
|
|
if (instance == null) {
|
|
SqlCipherLibraryLoader.load();
|
|
instance = new KeyValueDatabase(context, DatabaseSecretProvider.getOrCreateDatabaseSecret(context));
|
|
}
|
|
}
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
private KeyValueDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) {
|
|
super(application, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, 0,new SqlCipherErrorHandler(DATABASE_NAME), new SqlCipherDatabaseHook());
|
|
|
|
this.application = application;
|
|
}
|
|
|
|
@Override
|
|
public void onCreate(SQLiteDatabase db) {
|
|
Log.i(TAG, "onCreate()");
|
|
|
|
db.execSQL(CREATE_TABLE);
|
|
|
|
if (DatabaseFactory.getInstance(application).hasTable("key_value")) {
|
|
Log.i(TAG, "Found old key_value table. Migrating data.");
|
|
migrateDataFromPreviousDatabase(DatabaseFactory.getInstance(application).getRawDatabase(), db);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
|
Log.i(TAG, "onUpgrade(" + oldVersion + ", " + newVersion + ")");
|
|
}
|
|
|
|
@Override
|
|
public void onOpen(SQLiteDatabase db) {
|
|
Log.i(TAG, "onOpen()");
|
|
|
|
db.enableWriteAheadLogging();
|
|
db.setForeignKeyConstraintsEnabled(true);
|
|
|
|
SignalExecutors.BOUNDED.execute(() -> {
|
|
if (DatabaseFactory.getInstance(application).hasTable("key_value")) {
|
|
Log.i(TAG, "Dropping original key_value table from the main database.");
|
|
DatabaseFactory.getInstance(application).getRawDatabase().execSQL("DROP TABLE key_value");
|
|
}
|
|
});
|
|
}
|
|
|
|
public @NonNull KeyValueDataSet getDataSet() {
|
|
KeyValueDataSet dataSet = new KeyValueDataSet();
|
|
|
|
try (Cursor cursor = 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 = 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();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public @NonNull SQLiteDatabase getSqlCipherDatabase() {
|
|
return getWritableDatabase();
|
|
}
|
|
|
|
private static void migrateDataFromPreviousDatabase(@NonNull SQLiteDatabase oldDb, @NonNull SQLiteDatabase newDb) {
|
|
try (Cursor cursor = oldDb.rawQuery("SELECT * FROM key_value", null)) {
|
|
while (cursor.moveToNext()) {
|
|
int type = CursorUtil.requireInt(cursor, "type");
|
|
ContentValues values = new ContentValues();
|
|
values.put(KEY, CursorUtil.requireString(cursor, "key"));
|
|
values.put(TYPE, type);
|
|
|
|
switch (type) {
|
|
case 0:
|
|
values.put(VALUE, CursorUtil.requireBlob(cursor, "value"));
|
|
break;
|
|
case 1:
|
|
values.put(VALUE, CursorUtil.requireBoolean(cursor, "value"));
|
|
break;
|
|
case 2:
|
|
values.put(VALUE, CursorUtil.requireFloat(cursor, "value"));
|
|
break;
|
|
case 3:
|
|
values.put(VALUE, CursorUtil.requireInt(cursor, "value"));
|
|
break;
|
|
case 4:
|
|
values.put(VALUE, CursorUtil.requireLong(cursor, "value"));
|
|
break;
|
|
case 5:
|
|
values.put(VALUE, CursorUtil.requireString(cursor, "value"));
|
|
break;
|
|
}
|
|
|
|
newDb.insert(TABLE_NAME, null, values);
|
|
}
|
|
}
|
|
}
|
|
|
|
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];
|
|
}
|
|
}
|
|
}
|