mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-20 07:10:15 +01:00
Convert megaphone database and repository to kotlin.
This commit is contained in:
committed by
Michelle Tang
parent
5bcbbdf339
commit
0cf7705d4f
@@ -1,190 +0,0 @@
|
||||
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.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.database.model.MegaphoneRecord;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones.Event;
|
||||
import org.signal.core.util.CursorUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* IMPORTANT: Writes should only be made through {@link org.thoughtcrime.securesms.megaphone.MegaphoneRepository}.
|
||||
*/
|
||||
public class MegaphoneDatabase extends SQLiteOpenHelper implements SignalDatabaseOpenHelper {
|
||||
|
||||
private static final String TAG = Log.tag(MegaphoneDatabase.class);
|
||||
|
||||
private static final int DATABASE_VERSION = 1;
|
||||
private static final String DATABASE_NAME = "signal-megaphone.db";
|
||||
|
||||
private static final String TABLE_NAME = "megaphone";
|
||||
private static final String ID = "_id";
|
||||
private static final String EVENT = "event";
|
||||
private static final String SEEN_COUNT = "seen_count";
|
||||
private static final String LAST_SEEN = "last_seen";
|
||||
private static final String FIRST_VISIBLE = "first_visible";
|
||||
private static final String FINISHED = "finished";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
EVENT + " TEXT UNIQUE, " +
|
||||
SEEN_COUNT + " INTEGER, " +
|
||||
LAST_SEEN + " INTEGER, " +
|
||||
FIRST_VISIBLE + " INTEGER, " +
|
||||
FINISHED + " INTEGER)";
|
||||
|
||||
private static volatile MegaphoneDatabase instance;
|
||||
|
||||
private final Application application;
|
||||
|
||||
public static @NonNull MegaphoneDatabase getInstance(@NonNull Application context) {
|
||||
if (instance == null) {
|
||||
synchronized (MegaphoneDatabase.class) {
|
||||
if (instance == null) {
|
||||
SqlCipherLibraryLoader.load();
|
||||
instance = new MegaphoneDatabase(context, DatabaseSecretProvider.getOrCreateDatabaseSecret(context));
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public MegaphoneDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) {
|
||||
super(application, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, 0, new SqlCipherErrorHandler(application, DATABASE_NAME), new SqlCipherDatabaseHook(), true);
|
||||
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
Log.i(TAG, "onCreate()");
|
||||
db.execSQL(CREATE_TABLE);
|
||||
}
|
||||
|
||||
@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.setForeignKeyConstraintsEnabled(true);
|
||||
}
|
||||
|
||||
public void insert(@NonNull Collection<Event> events) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
for (Event event : events) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(EVENT, event.getKey());
|
||||
|
||||
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull List<MegaphoneRecord> getAllAndDeleteMissing() {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
List<MegaphoneRecord> records = new ArrayList<>();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
Set<String> missingKeys = new HashSet<>();
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, null, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String event = cursor.getString(cursor.getColumnIndexOrThrow(EVENT));
|
||||
int seenCount = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_COUNT));
|
||||
long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(LAST_SEEN));
|
||||
long firstVisible = cursor.getLong(cursor.getColumnIndexOrThrow(FIRST_VISIBLE));
|
||||
boolean finished = cursor.getInt(cursor.getColumnIndexOrThrow(FINISHED)) == 1;
|
||||
|
||||
if (Event.hasKey(event)) {
|
||||
records.add(new MegaphoneRecord(Event.fromKey(event), seenCount, lastSeen, firstVisible, finished));
|
||||
} else {
|
||||
Log.w(TAG, "No in-app handing for event '" + event + "'! Deleting it from the database.");
|
||||
missingKeys.add(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (String missing : missingKeys) {
|
||||
String query = EVENT + " = ?";
|
||||
String[] args = new String[]{missing};
|
||||
|
||||
db.delete(TABLE_NAME, query, args);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
public void markFirstVisible(@NonNull Event event, long time) {
|
||||
String query = EVENT + " = ?";
|
||||
String[] args = new String[]{event.getKey()};
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(FIRST_VISIBLE, time);
|
||||
|
||||
getWritableDatabase().update(TABLE_NAME, values, query, args);
|
||||
}
|
||||
|
||||
public void markSeen(@NonNull Event event, int seenCount, long lastSeen) {
|
||||
String query = EVENT + " = ?";
|
||||
String[] args = new String[]{event.getKey()};
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(SEEN_COUNT, seenCount);
|
||||
values.put(LAST_SEEN, lastSeen);
|
||||
|
||||
getWritableDatabase().update(TABLE_NAME, values, query, args);
|
||||
}
|
||||
|
||||
public void markFinished(@NonNull Event event) {
|
||||
String query = EVENT + " = ?";
|
||||
String[] args = new String[]{event.getKey()};
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(FINISHED, 1);
|
||||
|
||||
getWritableDatabase().update(TABLE_NAME, values, query, args);
|
||||
}
|
||||
|
||||
public void delete(@NonNull Event event) {
|
||||
String query = EVENT + " = ?";
|
||||
String[] args = new String[]{event.getKey()};
|
||||
|
||||
getWritableDatabase().delete(TABLE_NAME, query, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull SQLiteDatabase getSqlCipherDatabase() {
|
||||
return getWritableDatabase();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.app.Application
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import net.zetetic.database.sqlcipher.SQLiteOpenHelper
|
||||
import org.signal.core.util.delete
|
||||
import org.signal.core.util.forEach
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.logging.Log.tag
|
||||
import org.signal.core.util.requireBoolean
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
|
||||
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader.load
|
||||
import org.thoughtcrime.securesms.database.model.MegaphoneRecord
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
/**
|
||||
* IMPORTANT: Writes should only be made through [org.thoughtcrime.securesms.megaphone.MegaphoneRepository].
|
||||
*/
|
||||
class MegaphoneDatabase(
|
||||
application: Application,
|
||||
databaseSecret: DatabaseSecret
|
||||
) : SQLiteOpenHelper(
|
||||
application,
|
||||
DATABASE_NAME,
|
||||
databaseSecret.asString(),
|
||||
null,
|
||||
DATABASE_VERSION,
|
||||
0,
|
||||
SqlCipherErrorHandler(application, DATABASE_NAME),
|
||||
SqlCipherDatabaseHook(),
|
||||
true
|
||||
),
|
||||
SignalDatabaseOpenHelper {
|
||||
|
||||
companion object {
|
||||
private val TAG = tag(MegaphoneDatabase::class.java)
|
||||
|
||||
private const val DATABASE_VERSION = 1
|
||||
private const val DATABASE_NAME = "signal-megaphone.db"
|
||||
|
||||
private const val TABLE_NAME = "megaphone"
|
||||
private const val ID = "_id"
|
||||
private const val EVENT = "event"
|
||||
private const val INTERACTION_COUNT = "seen_count"
|
||||
private const val LAST_INTERACTION_TIMESTAMP = "last_seen"
|
||||
private const val FIRST_VISIBLE = "first_visible"
|
||||
private const val FINISHED = "finished"
|
||||
|
||||
const val CREATE_TABLE: String = """CREATE TABLE $TABLE_NAME(
|
||||
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$EVENT TEXT UNIQUE,
|
||||
$INTERACTION_COUNT INTEGER,
|
||||
$LAST_INTERACTION_TIMESTAMP INTEGER,
|
||||
$FIRST_VISIBLE INTEGER,
|
||||
$FINISHED INTEGER
|
||||
)"""
|
||||
|
||||
@Volatile
|
||||
private var instance: MegaphoneDatabase? = null
|
||||
|
||||
@JvmStatic
|
||||
fun getInstance(context: Application): MegaphoneDatabase {
|
||||
if (instance == null) {
|
||||
synchronized(MegaphoneDatabase::class.java) {
|
||||
if (instance == null) {
|
||||
load()
|
||||
instance = MegaphoneDatabase(context, DatabaseSecretProvider.getOrCreateDatabaseSecret(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance!!
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
Log.i(TAG, "onCreate()")
|
||||
db.execSQL(CREATE_TABLE)
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
|
||||
Log.i(TAG, "onUpgrade($oldVersion, $newVersion)")
|
||||
}
|
||||
|
||||
override fun onOpen(db: SQLiteDatabase) {
|
||||
Log.i(TAG, "onOpen()")
|
||||
db.setForeignKeyConstraintsEnabled(true)
|
||||
}
|
||||
|
||||
fun insert(events: Collection<Megaphones.Event>) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
for (event in events) {
|
||||
db.insertInto(TABLE_NAME)
|
||||
.values(EVENT to event.key)
|
||||
.run(SQLiteDatabase.CONFLICT_IGNORE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllAndDeleteMissing(): MutableList<MegaphoneRecord> {
|
||||
val records: MutableList<MegaphoneRecord> = mutableListOf()
|
||||
|
||||
writableDatabase.withinTransaction { db ->
|
||||
val missingKeys: MutableSet<String> = mutableSetOf()
|
||||
|
||||
db.select()
|
||||
.from(TABLE_NAME)
|
||||
.run()
|
||||
.forEach { cursor ->
|
||||
val event = cursor.requireNonNullString(EVENT)
|
||||
val interactionCount = cursor.requireInt(INTERACTION_COUNT)
|
||||
val lastInteractionTime = cursor.requireLong(LAST_INTERACTION_TIMESTAMP)
|
||||
val firstVisible = cursor.requireLong(FIRST_VISIBLE)
|
||||
val finished = cursor.requireBoolean(FINISHED)
|
||||
|
||||
if (Megaphones.Event.hasKey(event)) {
|
||||
records += MegaphoneRecord(
|
||||
event = Megaphones.Event.fromKey(event),
|
||||
interactionCount = interactionCount,
|
||||
lastInteractionTime = lastInteractionTime,
|
||||
firstVisible = firstVisible,
|
||||
finished = finished
|
||||
)
|
||||
} else {
|
||||
Log.w(TAG, "No in-app handing for event '$event'! Deleting it from the database.")
|
||||
missingKeys += event
|
||||
}
|
||||
}
|
||||
|
||||
for (missing in missingKeys) {
|
||||
db.delete(TABLE_NAME)
|
||||
.where("$EVENT = ?", missing)
|
||||
.run()
|
||||
}
|
||||
}
|
||||
|
||||
return records
|
||||
}
|
||||
|
||||
fun markFirstVisible(event: Megaphones.Event, time: Long) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(FIRST_VISIBLE to time)
|
||||
.where("$EVENT = ?", event.key)
|
||||
.run()
|
||||
}
|
||||
|
||||
fun markInteractedWith(event: Megaphones.Event, interactionCount: Int, lastInteractionTimestamp: Long) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(
|
||||
INTERACTION_COUNT to interactionCount,
|
||||
LAST_INTERACTION_TIMESTAMP to lastInteractionTimestamp
|
||||
)
|
||||
.where("$EVENT = ?", event.key)
|
||||
.run()
|
||||
}
|
||||
|
||||
fun markFinished(event: Megaphones.Event) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(FINISHED to 1)
|
||||
.where("$EVENT = ?", event.key)
|
||||
.run()
|
||||
}
|
||||
|
||||
fun delete(event: Megaphones.Event) {
|
||||
writableDatabase
|
||||
.delete(TABLE_NAME)
|
||||
.where("$EVENT = ?", event.key)
|
||||
.run()
|
||||
}
|
||||
|
||||
override fun getSqlCipherDatabase(): SQLiteDatabase {
|
||||
return writableDatabase
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
|
||||
public class MegaphoneRecord {
|
||||
|
||||
private final Megaphones.Event event;
|
||||
private final int seenCount;
|
||||
private final long lastSeen;
|
||||
private final long firstVisible;
|
||||
private final boolean finished;
|
||||
|
||||
public MegaphoneRecord(@NonNull Megaphones.Event event, int seenCount, long lastSeen, long firstVisible, boolean finished) {
|
||||
this.event = event;
|
||||
this.seenCount = seenCount;
|
||||
this.lastSeen = lastSeen;
|
||||
this.firstVisible = firstVisible;
|
||||
this.finished = finished;
|
||||
}
|
||||
|
||||
public @NonNull Megaphones.Event getEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
public int getSeenCount() {
|
||||
return seenCount;
|
||||
}
|
||||
|
||||
public long getLastSeen() {
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
public long getFirstVisible() {
|
||||
return firstVisible;
|
||||
}
|
||||
|
||||
public boolean isFinished() {
|
||||
return finished;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
|
||||
data class MegaphoneRecord(
|
||||
val event: Megaphones.Event,
|
||||
val interactionCount: Int,
|
||||
val lastInteractionTime: Long,
|
||||
val firstVisible: Long,
|
||||
val finished: Boolean
|
||||
)
|
||||
@@ -115,6 +115,6 @@ internal class ConfirmSvrPinFragment : BaseSvrPinFragment<ConfirmSvrPinViewModel
|
||||
}
|
||||
|
||||
private fun markMegaphoneSeenIfNecessary() {
|
||||
AppDependencies.megaphoneRepository.markSeen(Megaphones.Event.PINS_FOR_ALL)
|
||||
AppDependencies.megaphoneRepository.markInteractedWith(Megaphones.Event.PINS_FOR_ALL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ class MainNavigationViewModel(
|
||||
}
|
||||
|
||||
fun onMegaphoneSnoozed(event: Megaphones.Event) {
|
||||
megaphoneRepository.markSeen(event)
|
||||
megaphoneRepository.markInteractedWith(event)
|
||||
internalMegaphone.update { Megaphone.NONE }
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class BackupUpsellSchedule(
|
||||
|
||||
val lastSeenAnyBackupUpsell: Long = records.entries
|
||||
.filter { it.key in BACKUP_UPSELL_EVENTS }
|
||||
.mapNotNull { it.value.lastSeen.takeIf { t -> t > 0 } }
|
||||
.mapNotNull { it.value.lastInteractionTime.takeIf { t -> t > 0 } }
|
||||
.maxOrNull() ?: 0L
|
||||
|
||||
if (currentTime - lastSeenAnyBackupUpsell <= MIN_TIME_BETWEEN_BACKUP_UPSELLS) {
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.Discouraged;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.database.MegaphoneDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones.Event;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Synchronization of data structures is done using a serial executor. Do not access or change
|
||||
* data structures or fields on anything except the executor.
|
||||
*/
|
||||
public class MegaphoneRepository {
|
||||
|
||||
private final Application context;
|
||||
private final Executor executor;
|
||||
private final MegaphoneDatabase database;
|
||||
private final Map<Event, MegaphoneRecord> databaseCache;
|
||||
|
||||
private boolean enabled;
|
||||
|
||||
@Discouraged(message = "Instances of MegaphoneRepository should be accessed via ApplicationDependencies.")
|
||||
public MegaphoneRepository(@NonNull Application context) {
|
||||
this.context = context;
|
||||
this.executor = SignalExecutors.SERIAL;
|
||||
this.database = MegaphoneDatabase.getInstance(context);
|
||||
this.databaseCache = new HashMap<>();
|
||||
|
||||
executor.execute(this::init);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks any megaphones a new user shouldn't see as "finished".
|
||||
*/
|
||||
@AnyThread
|
||||
public void onFirstEverAppLaunch() {
|
||||
executor.execute(() -> {
|
||||
database.markFinished(Event.ADD_A_PROFILE_PHOTO);
|
||||
database.markFinished(Event.PNP_LAUNCH);
|
||||
resetDatabaseCache();
|
||||
});
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public void onAppForegrounded() {
|
||||
executor.execute(() -> enabled = true);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public void getNextMegaphone(@NonNull Callback<Megaphone> callback) {
|
||||
executor.execute(() -> {
|
||||
if (enabled) {
|
||||
init();
|
||||
callback.onResult(Megaphones.getNextMegaphone(context, databaseCache));
|
||||
} else {
|
||||
callback.onResult(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public void markVisible(@NonNull Megaphones.Event event) {
|
||||
long time = System.currentTimeMillis();
|
||||
|
||||
executor.execute(() -> {
|
||||
if (getRecord(event).getFirstVisible() == 0) {
|
||||
database.markFirstVisible(event, time);
|
||||
resetDatabaseCache();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public void markSeen(@NonNull Event event) {
|
||||
long lastSeen = System.currentTimeMillis();
|
||||
|
||||
executor.execute(() -> {
|
||||
MegaphoneRecord record = getRecord(event);
|
||||
database.markSeen(event, record.getSeenCount() + 1, lastSeen);
|
||||
enabled = false;
|
||||
resetDatabaseCache();
|
||||
});
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public void markFinished(@NonNull Event event) {
|
||||
markFinished(event, null);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public void markFinished(@NonNull Event event, @Nullable Runnable onComplete) {
|
||||
executor.execute(() -> {
|
||||
MegaphoneRecord record = databaseCache.get(event);
|
||||
if (record != null && record.isFinished()) {
|
||||
return;
|
||||
}
|
||||
|
||||
database.markFinished(event);
|
||||
resetDatabaseCache();
|
||||
|
||||
if (onComplete != null) {
|
||||
onComplete.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void init() {
|
||||
List<MegaphoneRecord> records = database.getAllAndDeleteMissing();
|
||||
Set<Event> events = records.stream().map(MegaphoneRecord::getEvent).collect(Collectors.toSet());
|
||||
Set<Event> missing = Stream.of(Event.values()).filter(o -> !events.contains(o)).collect(Collectors.toSet());
|
||||
|
||||
database.insert(missing);
|
||||
resetDatabaseCache();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull MegaphoneRecord getRecord(@NonNull Event event) {
|
||||
//noinspection ConstantConditions
|
||||
return databaseCache.get(event);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void resetDatabaseCache() {
|
||||
databaseCache.clear();
|
||||
databaseCache.putAll(database.getAllAndDeleteMissing().stream().collect(Collectors.toMap(MegaphoneRecord::getEvent, m -> m)));
|
||||
}
|
||||
|
||||
public interface Callback<E> {
|
||||
void onResult(E result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package org.thoughtcrime.securesms.megaphone
|
||||
|
||||
import android.app.Application
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.database.MegaphoneDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MegaphoneRecord
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* Synchronization of data structures is done using a serial executor. Do not access or change
|
||||
* data structures or fields on anything except the executor.
|
||||
*/
|
||||
class MegaphoneRepository(private val context: Application) {
|
||||
private val executor: Executor = SignalExecutors.SERIAL
|
||||
private val database: MegaphoneDatabase = MegaphoneDatabase.getInstance(context)
|
||||
private val databaseCache: MutableMap<Megaphones.Event?, MegaphoneRecord> = mutableMapOf()
|
||||
|
||||
private var enabled = false
|
||||
|
||||
init {
|
||||
executor.execute {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks any megaphones a new user shouldn't see as "finished".
|
||||
*/
|
||||
@AnyThread
|
||||
fun onFirstEverAppLaunch() {
|
||||
executor.execute {
|
||||
database.markFinished(Megaphones.Event.ADD_A_PROFILE_PHOTO)
|
||||
database.markFinished(Megaphones.Event.PNP_LAUNCH)
|
||||
resetDatabaseCache()
|
||||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
fun onAppForegrounded() {
|
||||
executor.execute {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
fun getNextMegaphone(callback: Callback<Megaphone?>) {
|
||||
executor.execute {
|
||||
if (enabled) {
|
||||
init()
|
||||
callback.onResult(Megaphones.getNextMegaphone(context, databaseCache))
|
||||
} else {
|
||||
callback.onResult(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
fun markVisible(event: Megaphones.Event) {
|
||||
val time = System.currentTimeMillis()
|
||||
|
||||
executor.execute {
|
||||
if (getRecord(event).firstVisible == 0L) {
|
||||
database.markFirstVisible(event, time)
|
||||
resetDatabaseCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
fun markInteractedWith(event: Megaphones.Event) {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
executor.execute {
|
||||
val record = getRecord(event)
|
||||
database.markInteractedWith(event, record.interactionCount + 1, currentTime)
|
||||
enabled = false
|
||||
resetDatabaseCache()
|
||||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
fun markFinished(event: Megaphones.Event) {
|
||||
markFinished(event, null)
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
fun markFinished(event: Megaphones.Event, onComplete: Runnable?) {
|
||||
executor.execute {
|
||||
val record = databaseCache[event]
|
||||
if (record != null && record.finished) {
|
||||
return@execute
|
||||
}
|
||||
|
||||
database.markFinished(event)
|
||||
resetDatabaseCache()
|
||||
onComplete?.run()
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun init() {
|
||||
val records: MutableList<MegaphoneRecord> = database.getAllAndDeleteMissing()
|
||||
val events = records.map { it.event }.toSet()
|
||||
val missing = Megaphones.Event.entries - events
|
||||
|
||||
database.insert(missing)
|
||||
resetDatabaseCache()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun getRecord(event: Megaphones.Event): MegaphoneRecord {
|
||||
return databaseCache.get(event)!!
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun resetDatabaseCache() {
|
||||
databaseCache.clear()
|
||||
databaseCache += database.getAllAndDeleteMissing().associateBy { it.event }
|
||||
}
|
||||
|
||||
fun interface Callback<E> {
|
||||
fun onResult(result: E?)
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ public final class Megaphones {
|
||||
MegaphoneRecord record = Objects.requireNonNull(records.get(e.getKey()));
|
||||
MegaphoneSchedule schedule = e.getValue();
|
||||
|
||||
return !record.isFinished() && schedule.shouldDisplay(record.getSeenCount(), record.getLastSeen(), record.getFirstVisible(), currentTime);
|
||||
return !record.getFinished() && schedule.shouldDisplay(record.getInteractionCount(), record.getLastInteractionTime(), record.getFirstVisible(), currentTime);
|
||||
})
|
||||
.map(Map.Entry::getKey)
|
||||
.map(records::get)
|
||||
@@ -600,7 +600,7 @@ public final class Megaphones {
|
||||
*/
|
||||
private static boolean shouldShowSetUpYourUsernameMegaphone(@NonNull Map<Event, MegaphoneRecord> records) {
|
||||
boolean hasUsername = SignalStore.account().isRegistered() && SignalStore.account().getUsername() != null;
|
||||
boolean hasCompleted = MapUtil.mapOrDefault(records, Event.SET_UP_YOUR_USERNAME, MegaphoneRecord::isFinished, false);
|
||||
boolean hasCompleted = MapUtil.mapOrDefault(records, Event.SET_UP_YOUR_USERNAME, MegaphoneRecord::getFinished, false);
|
||||
long phoneNumberDiscoveryDisabledAt = SignalStore.phoneNumberPrivacy().getPhoneNumberDiscoverabilityModeTimestamp();
|
||||
PhoneNumberDiscoverabilityMode listingMode = SignalStore.phoneNumberPrivacy().getPhoneNumberDiscoverabilityMode();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user