diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MegaphoneDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MegaphoneDatabase.java deleted file mode 100644 index 490cf8332c..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MegaphoneDatabase.java +++ /dev/null @@ -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 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 getAllAndDeleteMissing() { - SQLiteDatabase db = getWritableDatabase(); - List records = new ArrayList<>(); - - db.beginTransaction(); - try { - Set 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(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MegaphoneDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MegaphoneDatabase.kt new file mode 100644 index 0000000000..372dfccded --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MegaphoneDatabase.kt @@ -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) { + writableDatabase.withinTransaction { db -> + for (event in events) { + db.insertInto(TABLE_NAME) + .values(EVENT to event.key) + .run(SQLiteDatabase.CONFLICT_IGNORE) + } + } + } + + fun getAllAndDeleteMissing(): MutableList { + val records: MutableList = mutableListOf() + + writableDatabase.withinTransaction { db -> + val missingKeys: MutableSet = 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 + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MegaphoneRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MegaphoneRecord.java deleted file mode 100644 index 4ba070cf20..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MegaphoneRecord.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MegaphoneRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/MegaphoneRecord.kt new file mode 100644 index 0000000000..0ad31ca7e3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MegaphoneRecord.kt @@ -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 +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmSvrPinFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmSvrPinFragment.kt index 0bae4921cb..fbf3ae4d9d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmSvrPinFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmSvrPinFragment.kt @@ -115,6 +115,6 @@ internal class ConfirmSvrPinFragment : BaseSvrPinFragment t > 0 } } + .mapNotNull { it.value.lastInteractionTime.takeIf { t -> t > 0 } } .maxOrNull() ?: 0L if (currentTime - lastSeenAnyBackupUpsell <= MIN_TIME_BETWEEN_BACKUP_UPSELLS) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java deleted file mode 100644 index ea8da25cd9..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java +++ /dev/null @@ -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 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 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 records = database.getAllAndDeleteMissing(); - Set events = records.stream().map(MegaphoneRecord::getEvent).collect(Collectors.toSet()); - Set 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 { - void onResult(E result); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.kt new file mode 100644 index 0000000000..14268cf4fb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.kt @@ -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 = 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) { + 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 = 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 { + fun onResult(result: E?) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java index 3228686999..c5d6b1e22c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -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 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();