diff --git a/app/build.gradle b/app/build.gradle index cfb7bc708..e2cc39276 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -538,6 +538,7 @@ dependencies { force = true } testImplementation testLibs.hamcrest.hamcrest + testImplementation testLibs.mockk testImplementation(testFixtures(project(":libsignal-service"))) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RemoteMegaphoneDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RemoteMegaphoneDatabase.kt index 1a74e9f1b..025fa47a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RemoteMegaphoneDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RemoteMegaphoneDatabase.kt @@ -6,7 +6,10 @@ import android.database.Cursor import android.net.Uri import androidx.core.content.contentValuesOf import androidx.core.net.toUri +import org.json.JSONException +import org.json.JSONObject import org.signal.core.util.delete +import org.signal.core.util.logging.Log import org.signal.core.util.readToList import org.signal.core.util.requireInt import org.signal.core.util.requireLong @@ -24,6 +27,8 @@ import java.util.concurrent.TimeUnit class RemoteMegaphoneDatabase(context: Context, databaseHelper: SignalDatabase) : Database(context, databaseHelper) { companion object { + private val TAG = Log.tag(RemoteMegaphoneDatabase::class.java) + private const val TABLE_NAME = "remote_megaphone" private const val ID = "_id" private const val UUID = "uuid" @@ -44,6 +49,10 @@ class RemoteMegaphoneDatabase(context: Context, databaseHelper: SignalDatabase) private const val SECONDARY_ACTION_TEXT = "secondary_action_text" private const val SHOWN_AT = "shown_at" private const val FINISHED_AT = "finished_at" + private const val PRIMARY_ACTION_DATA = "primary_action_data" + private const val SECONDARY_ACTION_DATA = "secondary_action_data" + private const val SNOOZED_AT = "snoozed_at" + private const val SEEN_COUNT = "seen_count" val CREATE_TABLE = """ CREATE TABLE $TABLE_NAME ( @@ -65,7 +74,11 @@ class RemoteMegaphoneDatabase(context: Context, databaseHelper: SignalDatabase) $PRIMARY_ACTION_TEXT TEXT, $SECONDARY_ACTION_TEXT TEXT, $SHOWN_AT INTEGER DEFAULT 0, - $FINISHED_AT INTEGER DEFAULT 0 + $FINISHED_AT INTEGER DEFAULT 0, + $PRIMARY_ACTION_DATA TEXT DEFAULT NULL, + $SECONDARY_ACTION_DATA TEXT DEFAULT NULL, + $SNOOZED_AT INTEGER DEFAULT 0, + $SEEN_COUNT INTEGER DEFAULT 0 ) """.trimIndent() @@ -99,7 +112,7 @@ class RemoteMegaphoneDatabase(context: Context, databaseHelper: SignalDatabase) .readToList { it.toRemoteMegaphoneRecord() } } - fun getPotentialMegaphonesAndClearOld(now: Long = System.currentTimeMillis()): List { + fun getPotentialMegaphonesAndClearOld(now: Long): List { val records: List = readableDatabase .select() .from(TABLE_NAME) @@ -148,6 +161,17 @@ class RemoteMegaphoneDatabase(context: Context, databaseHelper: SignalDatabase) .run() } + fun snooze(remote: RemoteMegaphoneRecord) { + writableDatabase + .update(TABLE_NAME) + .values( + SEEN_COUNT to remote.seenCount + 1, + SNOOZED_AT to System.currentTimeMillis() + ) + .where("$UUID = ?", remote.uuid) + .run() + } + fun clearImageUrl(uuid: String) { writableDatabase .update(TABLE_NAME) @@ -192,7 +216,11 @@ class RemoteMegaphoneDatabase(context: Context, databaseHelper: SignalDatabase) BODY to body, PRIMARY_ACTION_TEXT to primaryActionText, SECONDARY_ACTION_TEXT to secondaryActionText, - FINISHED_AT to finishedAt + FINISHED_AT to finishedAt, + PRIMARY_ACTION_DATA to primaryActionData?.toString(), + SECONDARY_ACTION_DATA to secondaryActionData?.toString(), + SNOOZED_AT to snoozedAt, + SEEN_COUNT to seenCount ) } @@ -216,7 +244,24 @@ class RemoteMegaphoneDatabase(context: Context, databaseHelper: SignalDatabase) primaryActionText = requireString(PRIMARY_ACTION_TEXT), secondaryActionText = requireString(SECONDARY_ACTION_TEXT), shownAt = requireLong(SHOWN_AT), - finishedAt = requireLong(FINISHED_AT) + finishedAt = requireLong(FINISHED_AT), + primaryActionData = requireString(PRIMARY_ACTION_DATA).parseJsonObject(), + secondaryActionData = requireString(SECONDARY_ACTION_DATA).parseJsonObject(), + snoozedAt = requireLong(SNOOZED_AT), + seenCount = requireInt(SEEN_COUNT) ) } + + private fun String?.parseJsonObject(): JSONObject? { + if (this == null) { + return null + } + + return try { + JSONObject(this) + } catch (e: JSONException) { + Log.w(TAG, "Unable to parse data", e) + null + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index cf0267989..9eaf3a32a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V159_ThreadUnreadSe import org.thoughtcrime.securesms.database.helpers.migration.V160_SmsMmsExportedIndexMigration import org.thoughtcrime.securesms.database.helpers.migration.V161_StorySendMessageIdIndex import org.thoughtcrime.securesms.database.helpers.migration.V162_ThreadUnreadSelfMentionCountFixup +import org.thoughtcrime.securesms.database.helpers.migration.V163_RemoteMegaphoneSnoozeSupportMigration /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -26,7 +27,7 @@ object SignalDatabaseMigrations { val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass) - const val DATABASE_VERSION = 162 + const val DATABASE_VERSION = 163 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -85,6 +86,10 @@ object SignalDatabaseMigrations { if (oldVersion < 162) { V162_ThreadUnreadSelfMentionCountFixup.migrate(context, db, oldVersion, newVersion) } + + if (oldVersion < 163) { + V163_RemoteMegaphoneSnoozeSupportMigration.migrate(context, db, oldVersion, newVersion) + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V163_RemoteMegaphoneSnoozeSupportMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V163_RemoteMegaphoneSnoozeSupportMigration.kt new file mode 100644 index 000000000..7a7cdc714 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V163_RemoteMegaphoneSnoozeSupportMigration.kt @@ -0,0 +1,41 @@ +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import androidx.sqlite.db.SupportSQLiteDatabase +import net.zetetic.database.sqlcipher.SQLiteDatabase + +/** + * Add columns needed to track remote megaphone specific snooze rates. + */ +object V163_RemoteMegaphoneSnoozeSupportMigration : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + if (columnMissing(db, "primary_action_data")) { + db.execSQL("ALTER TABLE remote_megaphone ADD COLUMN primary_action_data TEXT DEFAULT NULL") + } + + if (columnMissing(db, "secondary_action_data")) { + db.execSQL("ALTER TABLE remote_megaphone ADD COLUMN secondary_action_data TEXT DEFAULT NULL") + } + + if (columnMissing(db, "snoozed_at")) { + db.execSQL("ALTER TABLE remote_megaphone ADD COLUMN snoozed_at INTEGER DEFAULT 0") + } + + if (columnMissing(db, "seen_count")) { + db.execSQL("ALTER TABLE remote_megaphone ADD COLUMN seen_count INTEGER DEFAULT 0") + } + } + + private fun columnMissing(db: SupportSQLiteDatabase, column: String): Boolean { + db.query("PRAGMA table_info(remote_megaphone)", null).use { cursor -> + val nameColumnIndex = cursor.getColumnIndexOrThrow("name") + while (cursor.moveToNext()) { + val name = cursor.getString(nameColumnIndex) + if (name == column) { + return false + } + } + } + return true + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/RemoteMegaphoneRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/RemoteMegaphoneRecord.kt index d0b46ec5f..2b9c7d532 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/RemoteMegaphoneRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/RemoteMegaphoneRecord.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.database.model import android.net.Uri +import org.json.JSONObject /** * Represents a Remote Megaphone. @@ -24,7 +25,11 @@ data class RemoteMegaphoneRecord( val primaryActionText: String?, val secondaryActionText: String?, val shownAt: Long = 0, - val finishedAt: Long = 0 + val finishedAt: Long = 0, + val primaryActionData: JSONObject? = null, + val secondaryActionData: JSONObject? = null, + val snoozedAt: Long = 0, + val seenCount: Int = 0 ) { @get:JvmName("hasPrimaryAction") val hasPrimaryAction = primaryActionId != null && primaryActionText != null @@ -32,6 +37,16 @@ data class RemoteMegaphoneRecord( @get:JvmName("hasSecondaryAction") val hasSecondaryAction = secondaryActionId != null && secondaryActionText != null + fun getDataForAction(actionId: ActionId): JSONObject? { + return if (primaryActionId == actionId) { + primaryActionData + } else if (secondaryActionId == actionId) { + secondaryActionData + } else { + null + } + } + enum class ActionId(val id: String, val isDonateAction: Boolean = false) { SNOOZE("snooze"), FINISH("finish"), diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveRemoteAnnouncementsJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveRemoteAnnouncementsJob.kt index 99a2b5eb8..d072cc6f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveRemoteAnnouncementsJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveRemoteAnnouncementsJob.kt @@ -2,6 +2,9 @@ package org.thoughtcrime.securesms.jobs import androidx.core.os.LocaleListCompat import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.ObjectNode +import org.json.JSONObject import org.signal.core.util.Hex import org.signal.core.util.ThreadUtil import org.signal.core.util.logging.Log @@ -275,7 +278,9 @@ class RetrieveRemoteAnnouncementsJob private constructor(private val force: Bool title = megaphone.translation.title, body = megaphone.translation.body, primaryActionText = megaphone.translation.primaryCtaText, - secondaryActionText = megaphone.translation.secondaryCtaText + secondaryActionText = megaphone.translation.secondaryCtaText, + primaryActionData = megaphone.remoteMegaphone.primaryCtaData?.takeIf { it is ObjectNode }?.let { JSONObject(it.toString()) }, + secondaryActionData = megaphone.remoteMegaphone.secondaryCtaData?.takeIf { it is ObjectNode }?.let { JSONObject(it.toString()) } ) SignalDatabase.remoteMegaphones.insert(record) @@ -384,7 +389,9 @@ class RetrieveRemoteAnnouncementsJob private constructor(private val force: Bool @JsonProperty val showForNumberOfDays: Long?, @JsonProperty val conditionalId: String?, @JsonProperty val primaryCtaId: String?, - @JsonProperty val secondaryCtaId: String? + @JsonProperty val secondaryCtaId: String?, + @JsonProperty val primaryCtaData: JsonNode?, + @JsonProperty val secondaryCtaData: JsonNode? ) data class TranslatedReleaseNote( 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 2c75a7d63..348f79e07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -307,7 +307,7 @@ public final class Megaphones { } private static @NonNull Megaphone buildRemoteMegaphone(@NonNull Context context) { - RemoteMegaphoneRecord record = RemoteMegaphoneRepository.getRemoteMegaphoneToShow(); + RemoteMegaphoneRecord record = RemoteMegaphoneRepository.getRemoteMegaphoneToShow(System.currentTimeMillis()); if (record != null) { Megaphone.Builder builder = new Megaphone.Builder(Event.REMOTE_MEGAPHONE, Megaphone.Style.BASIC) diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt index 49bc24d9f..bb26e1d1f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt @@ -4,7 +4,10 @@ import android.app.Application import android.content.Context import androidx.annotation.AnyThread import androidx.annotation.WorkerThread +import org.json.JSONArray +import org.json.JSONException import org.signal.core.util.concurrent.SignalExecutors +import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity import org.thoughtcrime.securesms.database.RemoteMegaphoneDatabase @@ -19,16 +22,23 @@ import org.thoughtcrime.securesms.util.LocaleFeatureFlags import org.thoughtcrime.securesms.util.PlayServicesUtil import org.thoughtcrime.securesms.util.VersionTracker import java.util.Objects +import kotlin.math.min +import kotlin.time.Duration.Companion.days /** * Access point for interacting with Remote Megaphones. */ object RemoteMegaphoneRepository { + private val TAG = Log.tag(RemoteMegaphoneRepository::class.java) + private val db: RemoteMegaphoneDatabase = SignalDatabase.remoteMegaphones private val context: Application = ApplicationDependencies.getApplication() - private val snooze: Action = Action { _, controller, _ -> controller.onMegaphoneSnooze(Megaphones.Event.REMOTE_MEGAPHONE) } + private val snooze: Action = Action { _, controller, remote -> + controller.onMegaphoneSnooze(Megaphones.Event.REMOTE_MEGAPHONE) + db.snooze(remote) + } private val finish: Action = Action { context, controller, remote -> if (remote.imageUri != null) { @@ -40,7 +50,7 @@ object RemoteMegaphoneRepository { private val donate: Action = Action { context, controller, remote -> controller.onMegaphoneNavigationRequested(AppSettingsActivity.manageSubscriptions(context)) - finish.run(context, controller, remote) + snooze.run(context, controller, remote) } private val actions = mapOf( @@ -65,12 +75,13 @@ object RemoteMegaphoneRepository { @WorkerThread @JvmStatic - fun getRemoteMegaphoneToShow(): RemoteMegaphoneRecord? { - return db.getPotentialMegaphonesAndClearOld() + fun getRemoteMegaphoneToShow(now: Long = System.currentTimeMillis()): RemoteMegaphoneRecord? { + return db.getPotentialMegaphonesAndClearOld(now) .asSequence() .filter { it.imageUrl == null || it.imageUri != null } .filter { it.countries == null || LocaleFeatureFlags.shouldShowReleaseNote(it.uuid, it.countries) } .filter { it.conditionalId == null || checkCondition(it.conditionalId) } + .filter { it.snoozedAt == 0L || checkSnooze(it, now) } .firstOrNull() } @@ -95,6 +106,17 @@ object RemoteMegaphoneRepository { } } + private fun checkSnooze(record: RemoteMegaphoneRecord, now: Long): Boolean { + if (record.seenCount == 0) { + return true + } + + val gaps: JSONArray? = record.getDataForAction(ActionId.SNOOZE)?.getJSONArray("snoozeDurationDays")?.takeIf { it.length() > 0 } + val gapDays: Int? = gaps?.getIntOrNull(record.seenCount - 1) + + return gapDays == null || (record.snoozedAt + gapDays.days.inWholeMilliseconds <= now) + } + private fun shouldShowDonateMegaphone(): Boolean { return VersionTracker.getDaysSinceFirstInstalled(context) >= 7 && PlayServicesUtil.getPlayServicesStatus(context) == PlayServicesUtil.PlayServicesStatus.SUCCESS && @@ -108,4 +130,16 @@ object RemoteMegaphoneRepository { fun interface Action { fun run(context: Context, controller: MegaphoneActionController, remoteMegaphone: RemoteMegaphoneRecord) } + + /** + * Gets the int at the specified index, or last index of array if larger then array length, or null if unable to parse json + */ + private fun JSONArray.getIntOrNull(index: Int): Int? { + return try { + getInt(min(index, length() - 1)) + } catch (e: JSONException) { + Log.w(TAG, "Unable to parse", e) + null + } + } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepositoryTest.kt b/app/src/test/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepositoryTest.kt new file mode 100644 index 000000000..d7c77a147 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepositoryTest.kt @@ -0,0 +1,206 @@ +package org.thoughtcrime.securesms.megaphone + +import android.app.Application +import android.net.Uri +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.notNullValue +import org.hamcrest.Matchers.nullValue +import org.json.JSONObject +import org.junit.After +import org.junit.AfterClass +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.thoughtcrime.securesms.SignalStoreRule +import org.thoughtcrime.securesms.database.RemoteMegaphoneDatabase +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord +import org.thoughtcrime.securesms.util.toMillis +import java.time.LocalDateTime +import java.util.UUID + +/** + * [RemoteMegaphoneRepository] is an Kotlin Object, which means it's like a singleton and thus maintains + * state and dependencies across tests. You must be aware of this when mocking/testing. + */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) +class RemoteMegaphoneRepositoryTest { + + @get:Rule + val signalStore: SignalStoreRule = SignalStoreRule() + + @Before + fun setUp() { + } + + @After + fun tearDown() { + clearMocks(remoteMegaphoneDatabase) + } + + /** Should return null if no megaphones in database. */ + @Test + fun getRemoteMegaphoneToShow_noMegaphones() { + // GIVEN + every { remoteMegaphoneDatabase.getPotentialMegaphonesAndClearOld(any()) } returns emptyList() + + // WHEN + val record = RemoteMegaphoneRepository.getRemoteMegaphoneToShow(0) + + // THEN + assertThat(record, nullValue()) + } + + @Test + fun getRemoteMegaphoneToShow_oneMegaphone() { + // GIVEN + every { remoteMegaphoneDatabase.getPotentialMegaphonesAndClearOld(any()) } returns listOf(megaphone(1)) + + // WHEN + val record = RemoteMegaphoneRepository.getRemoteMegaphoneToShow(0) + + // THEN + assertThat(record, notNullValue()) + } + + @Test + fun getRemoteMegaphoneToShow_snoozedMegaphone() { + // GIVEN + val snoozed = megaphone( + id = 1, + seenCount = 1, + snoozedAt = now.minusDays(1).toMillis(), + secondaryActionId = RemoteMegaphoneRecord.ActionId.SNOOZE, + secondaryActionData = JSONObject("{\"snoozeDurationDays\":[3]}") + ) + + every { remoteMegaphoneDatabase.getPotentialMegaphonesAndClearOld(now.toMillis()) } returns listOf(snoozed) + + // WHEN + val record = RemoteMegaphoneRepository.getRemoteMegaphoneToShow(now.toMillis()) + + // THEN + assertThat(record, nullValue()) + } + + @Test + fun getRemoteMegaphoneToShow_oldSnoozedMegaphone() { + // GIVEN + val snoozed = megaphone( + id = 1, + seenCount = 1, + snoozedAt = now.minusDays(5).toMillis(), + secondaryActionId = RemoteMegaphoneRecord.ActionId.SNOOZE, + secondaryActionData = JSONObject("{\"snoozeDurationDays\":[3]}") + ) + + every { remoteMegaphoneDatabase.getPotentialMegaphonesAndClearOld(now.toMillis()) } returns listOf(snoozed) + + // WHEN + val record = RemoteMegaphoneRepository.getRemoteMegaphoneToShow(now.toMillis()) + + // THEN + assertThat(record, notNullValue()) + } + + @Test + fun getRemoteMegaphoneToShow_multipleOldSnoozedMegaphone() { + // GIVEN + val snoozed = megaphone( + id = 1, + seenCount = 5, + snoozedAt = now.minusDays(8).toMillis(), + secondaryActionId = RemoteMegaphoneRecord.ActionId.SNOOZE, + secondaryActionData = JSONObject("{\"snoozeDurationDays\":[3, 5, 7]}") + ) + + every { remoteMegaphoneDatabase.getPotentialMegaphonesAndClearOld(now.toMillis()) } returns listOf(snoozed) + + // WHEN + val record = RemoteMegaphoneRepository.getRemoteMegaphoneToShow(now.toMillis()) + + // THEN + assertThat(record, notNullValue()) + } + + companion object { + private val now = LocalDateTime.of(2021, 11, 5, 12, 0) + + private val remoteMegaphoneDatabase: RemoteMegaphoneDatabase = mockk() + + @BeforeClass + @JvmStatic + fun classSetup() { + mockkObject(SignalDatabase.Companion) + every { SignalDatabase.remoteMegaphones } returns remoteMegaphoneDatabase + } + + @AfterClass + @JvmStatic + fun classCleanup() { + unmockkObject(SignalDatabase.Companion) + } + + fun megaphone( + id: Long, + priority: Long = 100, + uuid: String = UUID.randomUUID().toString(), + countries: String? = null, + minimumVersion: Int = 100, + doNotShowBefore: Long = 0, + doNotShowAfter: Long = Long.MAX_VALUE, + showForNumberOfDays: Long = Long.MAX_VALUE, + conditionalId: String? = null, + primaryActionId: RemoteMegaphoneRecord.ActionId? = null, + secondaryActionId: RemoteMegaphoneRecord.ActionId? = null, + imageUrl: String? = null, + imageUri: Uri? = null, + title: String = "", + body: String = "", + primaryActionText: String? = null, + secondaryActionText: String? = null, + shownAt: Long = 0, + finishedAt: Long = 0, + primaryActionData: JSONObject? = null, + secondaryActionData: JSONObject? = null, + snoozedAt: Long = 0, + seenCount: Int = 0 + ): RemoteMegaphoneRecord { + return RemoteMegaphoneRecord( + id, + priority, + uuid, + countries, + minimumVersion, + doNotShowBefore, + doNotShowAfter, + showForNumberOfDays, + conditionalId, + primaryActionId, + secondaryActionId, + imageUrl, + imageUri, + title, + body, + primaryActionText, + secondaryActionText, + shownAt, + finishedAt, + primaryActionData, + secondaryActionData, + snoozedAt, + seenCount, + ) + } + } +} diff --git a/dependencies.gradle b/dependencies.gradle index 3dc36c738..56de99e25 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -155,6 +155,7 @@ dependencyResolutionManagement { alias('hamcrest-hamcrest').to('org.hamcrest:hamcrest:2.2') alias('assertj-core').to('org.assertj:assertj-core:3.11.1') alias('square-okhttp-mockserver').to('com.squareup.okhttp3:mockwebserver:3.12.13') + alias('mockk').to('io.mockk:mockk:1.13.2') alias('conscrypt-openjdk-uber').to('org.conscrypt:conscrypt-openjdk-uber:2.0.0') } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index ca8300633..8129a2f02 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2569,6 +2569,86 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3304,6 +3384,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3449,6 +3534,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3479,6 +3569,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3546,6 +3641,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -3564,6 +3667,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3596,6 +3704,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -3663,6 +3779,19 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + @@ -3671,6 +3800,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -3679,6 +3816,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + @@ -3687,6 +3840,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -3695,6 +3856,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -4026,6 +4195,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + +