kopia lustrzana https://github.com/ryukoposting/Signal-Android
210 wiersze
5.6 KiB
Java
210 wiersze
5.6 KiB
Java
package org.thoughtcrime.securesms.keyvalue;
|
|
|
|
import androidx.annotation.AnyThread;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.WorkerThread;
|
|
|
|
import org.signal.core.util.concurrent.SignalExecutors;
|
|
import org.signal.core.util.logging.Log;
|
|
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
|
|
|
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 KeyValuePersistentStorage storage;
|
|
|
|
private KeyValueDataSet dataSet;
|
|
|
|
public KeyValueStore(@NonNull KeyValuePersistentStorage storage) {
|
|
this.executor = SignalExecutors.newCachedSingleThreadExecutor("signal-KeyValueStore");
|
|
this.storage = storage;
|
|
}
|
|
|
|
@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);
|
|
}
|
|
|
|
@AnyThread
|
|
@Override
|
|
public synchronized boolean containsKey(@NonNull String key) {
|
|
initializeIfNecessary();
|
|
return dataSet.containsKey(key);
|
|
}
|
|
|
|
/**
|
|
* @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.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Forces the store to re-fetch all of it's data from the database.
|
|
*/
|
|
synchronized void resetCache() {
|
|
dataSet = null;
|
|
initializeIfNecessary();
|
|
}
|
|
|
|
private synchronized void write(@NonNull KeyValueDataSet newDataSet, @NonNull Collection<String> removes) {
|
|
initializeIfNecessary();
|
|
|
|
dataSet.putAll(newDataSet);
|
|
dataSet.removeAll(removes);
|
|
|
|
executor.execute(() -> storage.writeDataSet(newDataSet, removes));
|
|
}
|
|
|
|
private void initializeIfNecessary() {
|
|
if (dataSet != null) return;
|
|
this.dataSet = storage.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();
|
|
}
|
|
}
|
|
}
|