Notify a user when they link a device.

This commit is contained in:
Michelle Tang
2025-01-10 17:27:32 -05:00
committed by Greyson Parrelli
parent 919648b94b
commit d4c8c16df3
22 changed files with 382 additions and 91 deletions

View File

@@ -65,6 +65,7 @@ class JobDatabase(
const val IS_RUNNING = "is_running"
const val GLOBAL_PRIORITY = "global_priority"
const val QUEUE_PRIORITY = "queue_priority"
const val INITIAL_DELAY = "initial_delay"
val CREATE_TABLE =
"""
@@ -83,7 +84,8 @@ class JobDatabase(
$IS_RUNNING INTEGER,
$NEXT_BACKOFF_INTERVAL INTEGER,
$GLOBAL_PRIORITY INTEGER DEFAULT 0,
$QUEUE_PRIORITY INTEGER DEFAULT 0
$QUEUE_PRIORITY INTEGER DEFAULT 0,
$INITIAL_DELAY INTEGER DEFAULT 0
)
""".trimIndent()
}
@@ -147,6 +149,10 @@ class JobDatabase(
db.execSQL("ALTER TABLE job_spec RENAME COLUMN priority TO global_priority")
db.execSQL("ALTER TABLE job_spec ADD COLUMN queue_priority INTEGER DEFAULT 0")
}
if (oldVersion < 5) {
db.execSQL("ALTER TABLE job_spec ADD COLUMN initial_delay INTEGER DEFAULT 0")
}
}
override fun onOpen(db: SQLiteDatabase) {
@@ -232,7 +238,8 @@ class JobDatabase(
Jobs.NEXT_BACKOFF_INTERVAL,
Jobs.IS_RUNNING,
Jobs.GLOBAL_PRIORITY,
Jobs.QUEUE_PRIORITY
Jobs.QUEUE_PRIORITY,
Jobs.INITIAL_DELAY
)
return readableDatabase
.query(Jobs.TABLE_NAME, columns, null, null, null, null, "${Jobs.CREATE_TIME}, ${Jobs.ID} ASC")
@@ -247,7 +254,8 @@ class JobDatabase(
globalPriority = cursor.requireInt(Jobs.GLOBAL_PRIORITY),
queuePriority = cursor.requireInt(Jobs.QUEUE_PRIORITY),
isRunning = cursor.requireBoolean(Jobs.IS_RUNNING),
isMemoryOnly = false
isMemoryOnly = false,
initialDelay = cursor.requireLong(Jobs.INITIAL_DELAY)
)
}
}
@@ -450,7 +458,8 @@ class JobDatabase(
isRunning = this.requireBoolean(Jobs.IS_RUNNING),
isMemoryOnly = false,
globalPriority = this.requireInt(Jobs.GLOBAL_PRIORITY),
queuePriority = this.requireInt(Jobs.QUEUE_PRIORITY)
queuePriority = this.requireInt(Jobs.QUEUE_PRIORITY),
initialDelay = this.requireLong(Jobs.INITIAL_DELAY)
)
}
@@ -494,13 +503,14 @@ class JobDatabase(
Jobs.SERIALIZED_INPUT_DATA to this.serializedInputData,
Jobs.IS_RUNNING to if (this.isRunning) 1 else 0,
Jobs.GLOBAL_PRIORITY to this.globalPriority,
Jobs.QUEUE_PRIORITY to this.queuePriority
Jobs.QUEUE_PRIORITY to this.queuePriority,
Jobs.INITIAL_DELAY to this.initialDelay
)
}
companion object {
private val TAG = Log.tag(JobDatabase::class.java)
private const val DATABASE_VERSION = 4
private const val DATABASE_VERSION = 5
private const val DATABASE_NAME = "signal-jobmanager.db"
@SuppressLint("StaticFieldLeak")

View File

@@ -286,6 +286,7 @@ public abstract class Job {
private final boolean memoryOnly;
private final int globalPriority;
private final int queuePriority;
private final long initialDelay;
private Parameters(@NonNull String id,
long createTime,
@@ -298,7 +299,8 @@ public abstract class Job {
@Nullable byte[] inputData,
boolean memoryOnly,
int globalPriority,
int queuePriority)
int queuePriority,
long initialDelay)
{
this.id = id;
this.createTime = createTime;
@@ -312,6 +314,7 @@ public abstract class Job {
this.memoryOnly = memoryOnly;
this.globalPriority = globalPriority;
this.queuePriority = queuePriority;
this.initialDelay = initialDelay;
}
@NonNull String getId() {
@@ -362,8 +365,12 @@ public abstract class Job {
return queuePriority;
}
long getInitialDelay() {
return initialDelay;
}
public Builder toBuilder() {
return new Builder(id, createTime, lifespan, maxAttempts, maxInstancesForFactory, maxInstancesForQueue, queue, constraintKeys, inputData, memoryOnly, globalPriority, queuePriority);
return new Builder(id, createTime, lifespan, maxAttempts, maxInstancesForFactory, maxInstancesForQueue, queue, constraintKeys, inputData, memoryOnly, globalPriority, queuePriority, initialDelay);
}
@@ -380,13 +387,14 @@ public abstract class Job {
private boolean memoryOnly;
private int globalPriority;
private int queuePriority;
private long initialDelay;
public Builder() {
this(UUID.randomUUID().toString());
}
Builder(@NonNull String id) {
this(id, System.currentTimeMillis(), IMMORTAL, 1, UNLIMITED, UNLIMITED, null, new LinkedList<>(), null, false, Parameters.PRIORITY_DEFAULT, Parameters.PRIORITY_DEFAULT);
this(id, System.currentTimeMillis(), IMMORTAL, 1, UNLIMITED, UNLIMITED, null, new LinkedList<>(), null, false, Parameters.PRIORITY_DEFAULT, Parameters.PRIORITY_DEFAULT, 0);
}
private Builder(@NonNull String id,
@@ -400,7 +408,8 @@ public abstract class Job {
@Nullable byte[] inputData,
boolean memoryOnly,
int globalPriority,
int queuePriority)
int queuePriority,
long initialDelay)
{
this.id = id;
this.createTime = createTime;
@@ -414,6 +423,7 @@ public abstract class Job {
this.memoryOnly = memoryOnly;
this.globalPriority = globalPriority;
this.queuePriority = queuePriority;
this.initialDelay = initialDelay;
}
/** Should only be invoked by {@link JobController} */
@@ -553,8 +563,16 @@ public abstract class Job {
return this;
}
/**
* Specifies a delay (in milliseconds) before the job runs. Default is 0.
*/
public @NonNull Builder setInitialDelay(long initialDelay) {
this.initialDelay = initialDelay;
return this;
}
public @NonNull Parameters build() {
return new Parameters(id, createTime, lifespan, maxAttempts, maxInstancesForFactory, maxInstancesForQueue, queue, constraintKeys, inputData, memoryOnly, globalPriority, queuePriority);
return new Parameters(id, createTime, lifespan, maxAttempts, maxInstancesForFactory, maxInstancesForQueue, queue, constraintKeys, inputData, memoryOnly, globalPriority, queuePriority, initialDelay);
}
}
}

View File

@@ -449,7 +449,8 @@ class JobController {
false,
job.getParameters().isMemoryOnly(),
job.getParameters().getGlobalPriority(),
job.getParameters().getQueuePriority());
job.getParameters().getQueuePriority(),
job.getParameters().getInitialDelay());
List<ConstraintSpec> constraintSpecs = Stream.of(job.getParameters().getConstraintKeys())
.map(key -> new ConstraintSpec(jobSpec.getId(), key, jobSpec.isMemoryOnly()))
@@ -476,7 +477,7 @@ class JobController {
constraints.add(constraintInstantiator.instantiate(key));
}
scheduler.schedule(0, constraints);
scheduler.schedule(job.getParameters().getInitialDelay(), constraints);
}
}
@@ -558,7 +559,8 @@ class JobController {
jobSpec.isRunning(),
jobSpec.isMemoryOnly(),
jobSpec.getGlobalPriority(),
jobSpec.getQueuePriority());
jobSpec.getQueuePriority(),
jobSpec.getInitialDelay());
}
interface Callback {

View File

@@ -73,7 +73,8 @@ public class JobMigrator {
jobSpec.isRunning(),
jobSpec.isMemoryOnly(),
jobSpec.getGlobalPriority(),
jobSpec.getQueuePriority());
jobSpec.getQueuePriority(),
jobSpec.getInitialDelay());
});
}

View File

@@ -15,7 +15,8 @@ data class JobSpec(
val isRunning: Boolean,
val isMemoryOnly: Boolean,
val globalPriority: Int,
val queuePriority: Int
val queuePriority: Int,
val initialDelay: Long
) {
fun withNextBackoffInterval(updated: Long): JobSpec {
@@ -27,7 +28,7 @@ data class JobSpec(
}
override fun toString(): String {
return "id: JOB::$id | factoryKey: $factoryKey | queueKey: $queueKey | createTime: $createTime | lastRunAttemptTime: $lastRunAttemptTime | nextBackoffInterval: $nextBackoffInterval | runAttempt: $runAttempt | maxAttempts: $maxAttempts | lifespan: $lifespan | isRunning: $isRunning | memoryOnly: $isMemoryOnly | globalPriority: $globalPriority | queuePriorty: $queuePriority"
return "id: JOB::$id | factoryKey: $factoryKey | queueKey: $queueKey | createTime: $createTime | lastRunAttemptTime: $lastRunAttemptTime | nextBackoffInterval: $nextBackoffInterval | runAttempt: $runAttempt | maxAttempts: $maxAttempts | lifespan: $lifespan | isRunning: $isRunning | memoryOnly: $isMemoryOnly | globalPriority: $globalPriority | queuePriorty: $queuePriority | initialDelay: $initialDelay"
}
override fun equals(other: Any?): Boolean {

View File

@@ -499,9 +499,18 @@ class FastJobStorage(private val jobDatabase: JobDatabase) : JobStorage {
/**
* Whether or not the job's eligible to be run based off of it's [Job.nextBackoffInterval] and other properties.
* Jobs that were created in the future with respect to the current time are automatically eligible
*/
private fun MinimalJobSpec.hasEligibleRunTime(currentTime: Long): Boolean {
return this.lastRunAttemptTime > currentTime || (this.lastRunAttemptTime + this.nextBackoffInterval) < currentTime
val isTimeTravel = this.createTime > currentTime || this.lastRunAttemptTime > currentTime
if (isTimeTravel) {
return true
}
val initialDelaySatisfied = this.createTime + this.initialDelay < currentTime
val backoffSatisfied = this.lastRunAttemptTime + this.nextBackoffInterval < currentTime
return initialDelaySatisfied && backoffSatisfied
}
private fun getSingleLayerOfDependencySpecsThatDependOnJob(jobSpecId: String): List<DependencySpec> {
@@ -574,6 +583,7 @@ fun JobSpec.toMinimalJobSpec(): MinimalJobSpec {
globalPriority = this.globalPriority,
queuePriority = this.queuePriority,
isRunning = this.isRunning,
isMemoryOnly = this.isMemoryOnly
isMemoryOnly = this.isMemoryOnly,
initialDelay = this.initialDelay
)
}

View File

@@ -146,6 +146,7 @@ public final class JobManagerFactories {
put(CopyAttachmentToArchiveJob.KEY, new CopyAttachmentToArchiveJob.Factory());
put(CreateReleaseChannelJob.KEY, new CreateReleaseChannelJob.Factory());
put(DeleteAbandonedAttachmentsJob.KEY, new DeleteAbandonedAttachmentsJob.Factory());
put(NewLinkedDeviceNotificationJob.KEY, new NewLinkedDeviceNotificationJob.Factory());
put(DeviceNameChangeJob.KEY, new DeviceNameChangeJob.Factory());
put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory());
put(DownloadLatestEmojiDataJob.KEY, new DownloadLatestEmojiDataJob.Factory());

View File

@@ -19,5 +19,6 @@ data class MinimalJobSpec(
val globalPriority: Int,
val queuePriority: Int,
val isRunning: Boolean,
val isMemoryOnly: Boolean
val isMemoryOnly: Boolean,
val initialDelay: Long
)

View File

@@ -0,0 +1,92 @@
package org.thoughtcrime.securesms.jobs
import android.app.PendingIntent
import androidx.core.app.NotificationCompat
import org.signal.core.util.PendingIntentFlags.cancelCurrent
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.protos.NewLinkedDeviceNotificationJobData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.ServiceUtil
import kotlin.random.Random
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.minutes
/**
* Notifies users that a device has been linked to their account at a randomly time 1 - 3 hours after it was created
*/
class NewLinkedDeviceNotificationJob private constructor(
private val data: NewLinkedDeviceNotificationJobData,
parameters: Parameters
) : Job(parameters) {
companion object {
const val KEY: String = "NewLinkedDeviceNotificationJob"
private val TAG = Log.tag(NewLinkedDeviceNotificationJob::class.java)
@JvmStatic
fun enqueue(deviceId: Int, deviceCreatedAt: Long) {
AppDependencies.jobManager.add(NewLinkedDeviceNotificationJob(deviceId, deviceCreatedAt))
}
/**
* Generates a random delay between 1 - 3 hours in milliseconds
*/
private fun getRandomDelay(): Long {
val delay = Random.nextInt(60, 180)
return delay.minutes.inWholeMilliseconds
}
}
constructor(
deviceId: Int,
deviceCreatedAt: Long
) : this(
NewLinkedDeviceNotificationJobData(deviceId, deviceCreatedAt),
Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setQueue("NewLinkedDeviceNotificationJob")
.setInitialDelay(getRandomDelay())
.setLifespan(7.days.inWholeMilliseconds)
.setMaxAttempts(Parameters.UNLIMITED)
.build()
)
override fun serialize(): ByteArray = data.encode()
override fun getFactoryKey(): String = KEY
override fun run(): Result {
if (!Recipient.self().isRegistered) {
Log.w(TAG, "Not registered")
return Result.failure()
}
if (NotificationChannels.getInstance().areNotificationsEnabled()) {
val pendingIntent = PendingIntent.getActivity(context, 0, AppSettingsActivity.linkedDevices(context), cancelCurrent())
val builder = NotificationCompat.Builder(context, NotificationChannels.getInstance().NEW_LINKED_DEVICE)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(context.getString(R.string.NewLinkedDeviceNotification__you_linked_new_device))
.setContentText(context.getString(R.string.NewLinkedDeviceNotification__a_new_device_was_linked, DateUtils.getOnlyTimeString(context, data.deviceCreatedAt)))
.setContentIntent(pendingIntent)
ServiceUtil.getNotificationManager(context).notify(NotificationIds.NEW_LINKED_DEVICE, builder.build())
}
SignalStore.misc.newLinkedDeviceId = data.deviceId
SignalStore.misc.newLinkedDeviceCreatedTime = data.deviceCreatedAt
return Result.success()
}
override fun onFailure() = Unit
class Factory : Job.Factory<NewLinkedDeviceNotificationJob?> {
override fun create(parameters: Parameters, serializedData: ByteArray?): NewLinkedDeviceNotificationJob = NewLinkedDeviceNotificationJob(NewLinkedDeviceNotificationJobData.ADAPTER.decode(serializedData!!), parameters)
}
}

View File

@@ -38,6 +38,8 @@ class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalSto
private const val LAST_NETWORK_RESET_TIME = "misc.last_network_reset_time"
private const val LAST_WEBSOCKET_CONNECT_TIME = "misc.last_websocket_connect_time"
private const val LAST_CONNECTIVITY_WARNING_TIME = "misc.last_connectivity_warning_time"
private const val NEW_LINKED_DEVICE_ID = "misc.new_linked_device_id"
private const val NEW_LINKED_DEVICE_CREATED_TIME = "misc.new_linked_device_created_time"
}
public override fun onFirstEverAppLaunch() {
@@ -257,4 +259,14 @@ class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalSto
* The last time we prompted the user regarding a [org.thoughtcrime.securesms.util.ConnectivityWarning].
*/
var lastConnectivityWarningTime: Long by longValue(LAST_CONNECTIVITY_WARNING_TIME, 0)
/**
* The device id of the device that was recently linked
*/
var newLinkedDeviceId: Int by integerValue(NEW_LINKED_DEVICE_ID, 0)
/**
* The time, in milliseconds, that the device was created at
*/
var newLinkedDeviceCreatedTime: Long by longValue(NEW_LINKED_DEVICE_CREATED_TIME, 0)
}

View File

@@ -137,7 +137,7 @@ class LinkDeviceFragment : ComposeFragment() {
Log.i(TAG, "Acquiring wake lock for linked device")
linkDeviceWakeLock.acquire()
}
DialogState.Unlinking -> Unit
DialogState.Unlinking, is DialogState.DeviceUnlinked -> Unit
}
}
@@ -206,7 +206,8 @@ class LinkDeviceFragment : ComposeFragment() {
onEditDevice = { device ->
viewModel.setDeviceToEdit(device)
navController.safeNavigate(R.id.action_linkDeviceFragment_to_editDeviceNameFragment)
}
},
onDialogDismissed = { viewModel.onDialogDismissed() }
)
}
}
@@ -257,7 +258,8 @@ fun DeviceListScreen(
onSyncFailureRetryRequested: () -> Unit = {},
onSyncFailureIgnored: () -> Unit = {},
onSyncCancelled: () -> Unit = {},
onEditDevice: (Device) -> Unit = {}
onEditDevice: (Device) -> Unit = {},
onDialogDismissed: () -> Unit = {}
) {
// If a bottom sheet is showing, we don't want the spinner underneath
if (!state.bottomSheetVisible) {
@@ -291,6 +293,15 @@ fun DeviceListScreen(
onDeny = onSyncFailureIgnored
)
}
is DialogState.DeviceUnlinked -> {
val createdAt = DateUtils.getOnlyTimeString(LocalContext.current, state.dialogState.deviceCreatedAt)
Dialogs.SimpleMessageDialog(
title = stringResource(id = R.string.LinkDeviceFragment__device_unlinked),
message = stringResource(id = R.string.LinkDeviceFragment__the_device_that_was, createdAt),
dismiss = stringResource(id = R.string.LinkDeviceFragment__ok),
onDismiss = onDialogDismissed
)
}
}
}
@@ -595,3 +606,17 @@ private fun DeviceListScreenSyncingFailedPreview() {
)
}
}
@SignalPreview
@Composable
private fun DeviceListScreenDeviceUnlinkedPreview() {
Previews.Preview {
DeviceListScreen(
state = LinkDeviceSettingsState(
dialogState = DialogState.DeviceUnlinked(1736454440342),
seenBioAuthEducationSheet = true,
seenQrEducationSheet = true
)
)
}
}

View File

@@ -31,6 +31,7 @@ data class LinkDeviceSettingsState(
data class SyncingMessages(val deviceId: Int, val deviceCreatedAt: Long) : DialogState
data object SyncingTimedOut : DialogState
data class SyncingFailed(val deviceId: Int, val deviceCreatedAt: Long) : DialogState
data class DeviceUnlinked(val deviceCreatedAt: Long) : DialogState
}
sealed interface OneTimeEvent {

View File

@@ -9,14 +9,18 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob
import org.thoughtcrime.securesms.jobs.NewLinkedDeviceNotificationJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.linkdevice.LinkDeviceRepository.LinkDeviceResult
import org.thoughtcrime.securesms.linkdevice.LinkDeviceRepository.getPlaintextDeviceName
import org.thoughtcrime.securesms.linkdevice.LinkDeviceSettingsState.DialogState
import org.thoughtcrime.securesms.linkdevice.LinkDeviceSettingsState.OneTimeEvent
import org.thoughtcrime.securesms.linkdevice.LinkDeviceSettingsState.QrCodeState
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.backup.MessageBackupKey
import org.whispersystems.signalservice.api.link.TransferArchiveError
@@ -36,7 +40,32 @@ class LinkDeviceViewModel : ViewModel() {
val state = _state.asStateFlow()
fun initialize() {
loadDevices()
loadDevices(initialLoad = true)
}
/**
* Checks for the existence of a newly linked device and shows a dialog if it has since been unlinked
*/
private fun checkForNewDevice(devices: List<Device>) {
val newLinkedDeviceId = SignalStore.misc.newLinkedDeviceId
val newLinkedDeviceCreatedAt = SignalStore.misc.newLinkedDeviceCreatedTime
val hasNewLinkedDevice = newLinkedDeviceId > 0
if (hasNewLinkedDevice) {
ServiceUtil.getNotificationManager(AppDependencies.application).cancel(NotificationIds.NEW_LINKED_DEVICE)
SignalStore.misc.newLinkedDeviceId = 0
SignalStore.misc.newLinkedDeviceCreatedTime = 0
}
val isMissingNewLinkedDevice = devices.none { device -> device.id == newLinkedDeviceId && device.createdMillis == newLinkedDeviceCreatedAt }
val dialogState = if (hasNewLinkedDevice && isMissingNewLinkedDevice) {
DialogState.DeviceUnlinked(newLinkedDeviceCreatedAt)
} else {
DialogState.None
}
_state.update { it.copy(dialogState = dialogState) }
}
fun setDeviceToRemove(device: Device?) {
@@ -66,7 +95,7 @@ class LinkDeviceViewModel : ViewModel() {
}
}
private fun loadDevices() {
private fun loadDevices(initialLoad: Boolean = false) {
_state.value = _state.value.copy(
deviceListLoading = true,
showFrontCamera = null
@@ -80,6 +109,9 @@ class LinkDeviceViewModel : ViewModel() {
deviceListLoading = false
)
} else {
if (initialLoad) {
checkForNewDevice(devices)
}
_state.update {
it.copy(
oneTimeEvent = OneTimeEvent.None,
@@ -143,6 +175,10 @@ class LinkDeviceViewModel : ViewModel() {
}
}
fun onDialogDismissed() {
_state.update { it.copy(dialogState = DialogState.None) }
}
fun addDevice(shouldSync: Boolean) = viewModelScope.launch(Dispatchers.IO) {
val linkUri: Uri = _state.value.linkUri!!
@@ -243,7 +279,8 @@ class LinkDeviceViewModel : ViewModel() {
return
}
Log.d(TAG, "[addDeviceWithSync] Found a linked device!")
Log.d(TAG, "[addDeviceWithSync] Found a linked device! Creating notification job.")
NewLinkedDeviceNotificationJob.enqueue(waitResult.id, waitResult.created)
_state.update {
it.copy(
@@ -319,6 +356,8 @@ class LinkDeviceViewModel : ViewModel() {
if (waitResult == null) {
Log.i(TAG, "No linked device found!")
} else {
Log.i(TAG, "Found a linked device! Creating notification job.")
NewLinkedDeviceNotificationJob.enqueue(waitResult.id, waitResult.created)
_state.update {
it.copy(oneTimeEvent = OneTimeEvent.ToastLinked(waitResult.getPlaintextDeviceName()))
}

View File

@@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.profiles.manage.EditProfileActivity;
import org.thoughtcrime.securesms.profiles.username.NewWaysToConnectDialogFragment;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
@@ -110,6 +111,7 @@ public final class Megaphones {
return new LinkedHashMap<>() {{
put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? ALWAYS : NEVER);
put(Event.NEW_LINKED_DEVICE, shouldShowNewLinkedDeviceMegaphone() ? ALWAYS: NEVER);
put(Event.NOTIFICATIONS, shouldShowNotificationsMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(30)) : NEVER);
put(Event.GRANT_FULL_SCREEN_INTENT, shouldShowGrantFullScreenIntentPermission(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(3)) : NEVER);
put(Event.BACKUP_SCHEDULE_PERMISSION, shouldShowBackupSchedulePermissionMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(3)) : NEVER);
@@ -166,6 +168,8 @@ public final class Megaphones {
return buildGrantFullScreenIntentPermission(context);
case PNP_LAUNCH:
return buildPnpLaunchMegaphone();
case NEW_LINKED_DEVICE:
return buildNewLinkedDeviceMegaphone(context);
default:
throw new IllegalArgumentException("Event not handled!");
}
@@ -184,7 +188,7 @@ public final class Megaphones {
return new Megaphone.Builder(Event.LINKED_DEVICE_INACTIVE, Megaphone.Style.BASIC)
.setTitle(R.string.LinkedDeviceInactiveMegaphone_title)
.setBody(context.getResources().getQuantityString(R.plurals.LinkedDeviceInactiveMegaphone_body, expiringDays, device.name, expiringDays))
.setImage(R.drawable.ic_inactive_linked_device)
.setImage(R.drawable.symbol_linked_device)
.setActionButton(R.string.LinkedDeviceInactiveMegaphone_got_it_button_label, (megaphone, listener) -> {
listener.onMegaphoneSnooze(Event.LINKED_DEVICE_INACTIVE);
})
@@ -294,6 +298,23 @@ public final class Megaphones {
.build();
}
private static @NonNull Megaphone buildNewLinkedDeviceMegaphone(@NonNull Context context) {
String createdAt = DateUtils.getOnlyTimeString(context, SignalStore.misc().getNewLinkedDeviceCreatedTime());
return new Megaphone.Builder(Event.NEW_LINKED_DEVICE, Megaphone.Style.BASIC)
.setTitle(R.string.NewLinkedDeviceNotification__you_linked_new_device)
.setBody(context.getString(R.string.NewLinkedDeviceMegaphone__a_new_device_was_linked, createdAt))
.setImage(R.drawable.symbol_linked_device)
.setActionButton(R.string.NewLinkedDeviceMegaphone__ok, (megaphone, listener) -> {
SignalStore.misc().setNewLinkedDeviceId(0);
SignalStore.misc().setNewLinkedDeviceCreatedTime(0);
listener.onMegaphoneSnooze(Event.NEW_LINKED_DEVICE);
})
.setSecondaryButton(R.string.NewLinkedDeviceMegaphone__view_device, (megaphone, listener) -> {
listener.onMegaphoneNavigationRequested(AppSettingsActivity.linkedDevices(context));
})
.build();
}
private static @NonNull Megaphone buildTurnOffCircumventionMegaphone(@NonNull Context context) {
return new Megaphone.Builder(Event.TURN_OFF_CENSORSHIP_CIRCUMVENTION, Megaphone.Style.BASIC)
.setTitle(R.string.CensorshipCircumventionMegaphone_turn_off_censorship_circumvention)
@@ -419,6 +440,10 @@ public final class Megaphones {
return SignalStore.onboarding().hasOnboarding(context);
}
private static boolean shouldShowNewLinkedDeviceMegaphone() {
return SignalStore.misc().getNewLinkedDeviceId() > 0 && !NotificationChannels.getInstance().areNotificationsEnabled();
}
private static boolean shouldShowTurnOffCircumventionMegaphone() {
return AppDependencies.getSignalServiceNetworkAccess().isCensored() &&
SignalStore.misc().isServiceReachableWithoutCircumvention();
@@ -525,7 +550,8 @@ public final class Megaphones {
BACKUP_SCHEDULE_PERMISSION("backup_schedule_permission"),
SET_UP_YOUR_USERNAME("set_up_your_username"),
PNP_LAUNCH("pnp_launch"),
GRANT_FULL_SCREEN_INTENT("grant_full_screen_intent");
GRANT_FULL_SCREEN_INTENT("grant_full_screen_intent"),
NEW_LINKED_DEVICE("new_linked_device");
private final String key;

View File

@@ -80,6 +80,7 @@ public class NotificationChannels {
public final String CALL_STATUS = "call_status";
public final String APP_ALERTS = "app_alerts";
public final String ADDITIONAL_MESSAGE_NOTIFICATIONS = "additional_message_notifications";
public final String NEW_LINKED_DEVICE = "new_linked_device";
private static volatile NotificationChannels instance;
@@ -634,6 +635,7 @@ public class NotificationChannels {
NotificationChannel callStatus = new NotificationChannel(CALL_STATUS, context.getString(R.string.NotificationChannel_call_status), NotificationManager.IMPORTANCE_LOW);
NotificationChannel appAlerts = new NotificationChannel(APP_ALERTS, context.getString(R.string.NotificationChannel_critical_app_alerts), NotificationManager.IMPORTANCE_HIGH);
NotificationChannel additionalMessageNotifications = new NotificationChannel(ADDITIONAL_MESSAGE_NOTIFICATIONS, context.getString(R.string.NotificationChannel_additional_message_notifications), NotificationManager.IMPORTANCE_HIGH);
NotificationChannel newLinkedDevice = new NotificationChannel(NEW_LINKED_DEVICE, context.getString(R.string.NotificationChannel_new_linked_device), NotificationManager.IMPORTANCE_HIGH);
messages.setGroup(CATEGORY_MESSAGES);
setVibrationEnabled(messages, SignalStore.settings().isMessageVibrateEnabled());
@@ -651,7 +653,7 @@ public class NotificationChannels {
callStatus.setShowBadge(false);
appAlerts.setShowBadge(false);
notificationManager.createNotificationChannels(Arrays.asList(messages, calls, failures, backups, lockedStatus, other, voiceNotes, joinEvents, background, callStatus, appAlerts, additionalMessageNotifications));
notificationManager.createNotificationChannels(Arrays.asList(messages, calls, failures, backups, lockedStatus, other, voiceNotes, joinEvents, background, callStatus, appAlerts, additionalMessageNotifications, newLinkedDevice));
if (BuildConfig.MANAGES_APP_UPDATES) {
NotificationChannel appUpdates = new NotificationChannel(APP_UPDATES, context.getString(R.string.NotificationChannel_app_updates), NotificationManager.IMPORTANCE_DEFAULT);

View File

@@ -31,6 +31,7 @@ public final class NotificationIds {
public static final int MESSAGE_DELIVERY_FAILURE = 800000;
public static final int STORY_MESSAGE_DELIVERY_FAILURE = 900000;
public static final int UNREGISTERED_NOTIFICATION_ID = 20230102;
public static final int NEW_LINKED_DEVICE = 120400;
private NotificationIds() { }

View File

@@ -151,4 +151,9 @@ message BackupMessagesJobData {
string dataFile = 2;
ResumableUpload uploadSpec = 3;
string resumableUri = 4;
}
message NewLinkedDeviceNotificationJobData {
uint32 deviceId = 1;
uint64 deviceCreatedAt = 2;
}

View File

@@ -1,39 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:fillColor="#FFFCAF68"
android:pathData="M13 19c0-2.2 1.8-4 4-4h28c2.2 0 4 1.8 4 4v24c0 2.2-1.8 4-4 4H17c-2.2 0-4-1.8-4-4V19Z"/>
<path
android:fillColor="#FF617092"
android:fillType="evenOdd"
android:pathData="M12 19c0-2.76 2.24-5 5-5h28c2.76 0 5 2.24 5 5v24c0 2.76-2.24 5-5 5H17c-2.76 0-5-2.24-5-5V19Zm5-3c-1.66 0-3 1.34-3 3v24c0 1.66 1.34 3 3 3h28c1.66 0 3-1.34 3-3V19c0-1.66-1.34-3-3-3H17Z"/>
<path
android:fillColor="#FFFFCF71"
android:fillType="evenOdd"
android:pathData="M47.9 22.07v21.75c0 1.66-1.33 3-3 3H16.83c-1.66 0-3-1.34-3-3v-1.55c4.75-11.98 16.44-20.45 30.11-20.45 1.35 0 2.68 0.09 3.98 0.25Z"/>
<path
android:fillColor="#FFFFE3A5"
android:fillType="evenOdd"
android:pathData="M47.9 28.95v17.87H19.46c3.17-10.52 12.93-18.18 24.48-18.18 1.35 0 2.68 0.1 3.98 0.3Z"/>
<path
android:fillColor="#FFC1C5E1"
android:pathData="M7 44.5C7 43.12 8.12 42 9.5 42h43c1.38 0 2.5 1.12 2.5 2.5S53.88 47 52.5 47h-43C8.12 47 7 45.88 7 44.5Z"/>
<path
android:fillColor="#FF617092"
android:fillType="evenOdd"
android:pathData="M6 44.5C6 42.57 7.57 41 9.5 41h43c1.93 0 3.5 1.57 3.5 3.5S54.43 48 52.5 48h-43C7.57 48 6 46.43 6 44.5ZM9.5 43C8.67 43 8 43.67 8 44.5S8.67 46 9.5 46h43c0.83 0 1.5-0.67 1.5-1.5S53.33 43 52.5 43h-43Z"/>
<path
android:fillColor="#FFC2D5F0"
android:pathData="M43 29c0-1.66 1.34-3 3-3h8c1.66 0 3 1.34 3 3v17c0 1.66-1.34 3-3 3h-8c-1.66 0-3-1.34-3-3V29Z"/>
<path
android:fillColor="#FFAEC8E8"
android:fillType="evenOdd"
android:pathData="M57 37.48v8.9c0 1.66-1.34 3-3 3h-7.64c-1.65 0-3-1.34-3-3v-8.9c1.96-1.28 4.3-2.02 6.82-2.02 2.52 0 4.86 0.74 6.82 2.02Z"/>
<path
android:fillColor="#FF617092"
android:fillType="evenOdd"
android:pathData="M42 29c0-2.2 1.8-4 4-4h8c2.2 0 4 1.8 4 4v17c0 2.2-1.8 4-4 4h-8c-2.2 0-4-1.8-4-4V29Zm4-2c-1.1 0-2 0.9-2 2v17c0 1.1 0.9 2 2 2h8c1.1 0 2-0.9 2-2V29c0-1.1-0.9-2-2-2h-8Z"/>
</vector>

View File

@@ -0,0 +1,39 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:pathData="M13,19C13,16.791 14.791,15 17,15H45C47.209,15 49,16.791 49,19V43C49,45.209 47.209,47 45,47H17C14.791,47 13,45.209 13,43V19Z"
android:fillColor="#FCAF68"/>
<path
android:pathData="M12,19C12,16.239 14.239,14 17,14H45C47.761,14 50,16.239 50,19V43C50,45.761 47.761,48 45,48H17C14.239,48 12,45.761 12,43V19ZM17,16C15.343,16 14,17.343 14,19V43C14,44.657 15.343,46 17,46H45C46.657,46 48,44.657 48,43V19C48,17.343 46.657,16 45,16H17Z"
android:fillColor="#617092"
android:fillType="evenOdd"/>
<path
android:pathData="M47.909,22.066V43.824C47.909,45.481 46.566,46.824 44.909,46.824H16.818C15.161,46.824 13.818,45.481 13.818,43.824V42.268C18.572,30.292 30.263,21.824 43.931,21.824C45.278,21.824 46.606,21.906 47.909,22.066Z"
android:fillColor="#FFCF71"
android:fillType="evenOdd"/>
<path
android:pathData="M47.909,28.949V46.824H19.447C22.616,36.304 32.38,28.642 43.932,28.642C45.285,28.642 46.613,28.747 47.909,28.949Z"
android:fillColor="#FFE3A5"
android:fillType="evenOdd"/>
<path
android:pathData="M7,44.5C7,43.119 8.119,42 9.5,42H52.5C53.881,42 55,43.119 55,44.5C55,45.881 53.881,47 52.5,47H9.5C8.119,47 7,45.881 7,44.5Z"
android:fillColor="#C1C5E1"/>
<path
android:pathData="M6,44.5C6,42.567 7.567,41 9.5,41H52.5C54.433,41 56,42.567 56,44.5C56,46.433 54.433,48 52.5,48H9.5C7.567,48 6,46.433 6,44.5ZM9.5,43C8.672,43 8,43.672 8,44.5C8,45.328 8.672,46 9.5,46H52.5C53.328,46 54,45.328 54,44.5C54,43.672 53.328,43 52.5,43H9.5Z"
android:fillColor="#617092"
android:fillType="evenOdd"/>
<path
android:pathData="M43,29C43,27.343 44.343,26 46,26H54C55.657,26 57,27.343 57,29V46C57,47.657 55.657,49 54,49H46C44.343,49 43,47.657 43,46V29Z"
android:fillColor="#C2D5F0"/>
<path
android:pathData="M57,37.482V46.381C57,48.037 55.657,49.381 54,49.381H46.364C44.707,49.381 43.364,48.037 43.364,46.381V37.482C45.325,36.203 47.666,35.46 50.182,35.46C52.697,35.46 55.039,36.203 57,37.482Z"
android:fillColor="#AEC8E8"
android:fillType="evenOdd"/>
<path
android:pathData="M42,29C42,26.791 43.791,25 46,25H54C56.209,25 58,26.791 58,29V46C58,48.209 56.209,50 54,50H46C43.791,50 42,48.209 42,46V29ZM46,27C44.895,27 44,27.895 44,29V46C44,47.105 44.895,48 46,48H54C55.105,48 56,47.105 56,46V29C56,27.895 55.105,27 54,27H46Z"
android:fillColor="#617092"
android:fillType="evenOdd"/>
</vector>

View File

@@ -1020,6 +1020,12 @@
<string name="LinkDeviceFragment__linking_cancelled">Linking cancelled</string>
<!-- Message shown in progress dialog telling users to avoid closing the app while messages are being synced -->
<string name="LinkDeviceFragment__do_not_close">Do not close app</string>
<!-- Dialog title shown when a device is unlinked -->
<string name="LinkDeviceFragment__device_unlinked">Device unlinked</string>
<!-- Dialog body shown when a device is unlinked where %1$s is when the device was originally linked -->
<string name="LinkDeviceFragment__the_device_that_was">The device that was recently linked at %1$s is no longer linked.</string>
<!-- Button to dismiss dialog -->
<string name="LinkDeviceFragment__ok">OK</string>
<!-- EditDeviceNameFragment -->
<!-- App bar title when editing the name of a device -->
@@ -1065,6 +1071,11 @@
<!-- Description in bottom sheet of what not transferring history will do -->
<string name="LinkDeviceSyncBottomSheet_no_old_messages">No old messages or media will be transferred to your linked device</string>
<!-- Title of notification telling users that a device was linked to their account -->
<string name="NewLinkedDeviceNotification__you_linked_new_device">You linked a new device</string>
<!-- Message body of notification telling users that a device was linked to their account. %1$s is the time it was linked -->
<string name="NewLinkedDeviceNotification__a_new_device_was_linked">A new device was linked to your account at %1$s. Tap to view.</string>
<!-- DeviceListActivity -->
<string name="DeviceListActivity_unlink_s">Unlink \"%s\"?</string>
<string name="DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive">By unlinking this device, it will no longer be able to send or receive messages.</string>
@@ -1650,6 +1661,13 @@
<string name="Megaphones_chat_colors">Chat colors</string>
<string name="Megaphones_add_a_profile_photo">Add a profile photo</string>
<!-- Message body of megaphone telling users that a device was linked to their account. %1$s is the time it was linked -->
<string name="NewLinkedDeviceMegaphone__a_new_device_was_linked">A new device was linked to your account at %1$s.</string>
<!-- Button shown on megaphone that will redirect to the linked devices screen -->
<string name="NewLinkedDeviceMegaphone__view_device">View device</string>
<!-- Button shown on megaphone to acknowledge and dismiss the megaphone -->
<string name="NewLinkedDeviceMegaphone__ok">OK</string>
<!-- Title of a bottom sheet to render messages that all quote a specific message -->
<string name="MessageQuotesBottomSheet_replies">Replies</string>
@@ -2942,6 +2960,8 @@
<string name="NotificationChannel_critical_app_alerts">Critical app alerts</string>
<!-- Notification channel name for other notifications related to messages. Will appear in the system notification settings as the title of this notification channel. -->
<string name="NotificationChannel_additional_message_notifications">Additional message notifications</string>
<!-- Notification channel name for notifications sent when a device has been linked -->
<string name="NotificationChannel_new_linked_device">New linked device</string>
<!-- ProfileEditNameFragment -->

View File

@@ -115,7 +115,8 @@ class JobMigratorTest {
isRunning = false,
isMemoryOnly = false,
globalPriority = 0,
queuePriority = 0
queuePriority = 0,
initialDelay = 0
)
return mockk<JobStorage> {
every { debugGetJobSpecs(any()) } returns listOf(job)

View File

@@ -345,7 +345,7 @@ class FastJobStorageTest {
@Test
fun `getNextEligibleJob - none when next run time is after current time`() {
val currentTime = 0L
val currentTime = 1L
val fullSpec = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q", lastRunAttemptTime = 0, nextBackoffInterval = 10), emptyList(), emptyList())
val subject = FastJobStorage(mockDatabase(listOf(fullSpec)))
@@ -450,40 +450,40 @@ class FastJobStorageTest {
val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2, fullSpec3, fullSpec4, fullSpec5, fullSpec6, fullSpec7, fullSpec8, fullSpec9, fullSpec10, fullSpec11, fullSpec12)))
subject.init()
assertThat(subject.getNextEligibleJob(10, NO_PREDICATE)).isEqualTo(fullSpec2.jobSpec)
assertThat(subject.getNextEligibleJob(15, NO_PREDICATE)).isEqualTo(fullSpec2.jobSpec)
subject.deleteJob(fullSpec2.jobSpec.id)
assertThat(subject.getNextEligibleJob(10, NO_PREDICATE)).isEqualTo(fullSpec6.jobSpec)
assertThat(subject.getNextEligibleJob(15, NO_PREDICATE)).isEqualTo(fullSpec6.jobSpec)
subject.deleteJob(fullSpec6.jobSpec.id)
assertThat(subject.getNextEligibleJob(10, NO_PREDICATE)).isEqualTo(fullSpec11.jobSpec)
assertThat(subject.getNextEligibleJob(15, NO_PREDICATE)).isEqualTo(fullSpec11.jobSpec)
subject.deleteJob(fullSpec11.jobSpec.id)
assertThat(subject.getNextEligibleJob(10, NO_PREDICATE)).isEqualTo(fullSpec10.jobSpec)
assertThat(subject.getNextEligibleJob(15, NO_PREDICATE)).isEqualTo(fullSpec10.jobSpec)
subject.deleteJob(fullSpec10.jobSpec.id)
assertThat(subject.getNextEligibleJob(10, NO_PREDICATE)).isEqualTo(fullSpec12.jobSpec)
assertThat(subject.getNextEligibleJob(15, NO_PREDICATE)).isEqualTo(fullSpec12.jobSpec)
subject.deleteJob(fullSpec12.jobSpec.id)
assertThat(subject.getNextEligibleJob(10, NO_PREDICATE)).isEqualTo(fullSpec3.jobSpec)
assertThat(subject.getNextEligibleJob(15, NO_PREDICATE)).isEqualTo(fullSpec3.jobSpec)
subject.deleteJob(fullSpec3.jobSpec.id)
assertThat(subject.getNextEligibleJob(10, NO_PREDICATE)).isEqualTo(fullSpec5.jobSpec)
assertThat(subject.getNextEligibleJob(15, NO_PREDICATE)).isEqualTo(fullSpec5.jobSpec)
subject.deleteJob(fullSpec5.jobSpec.id)
assertThat(subject.getNextEligibleJob(10, NO_PREDICATE)).isEqualTo(fullSpec9.jobSpec)
assertThat(subject.getNextEligibleJob(15, NO_PREDICATE)).isEqualTo(fullSpec9.jobSpec)
subject.deleteJob(fullSpec9.jobSpec.id)
assertThat(subject.getNextEligibleJob(10, NO_PREDICATE)).isEqualTo(fullSpec1.jobSpec)
assertThat(subject.getNextEligibleJob(15, NO_PREDICATE)).isEqualTo(fullSpec1.jobSpec)
subject.deleteJob(fullSpec1.jobSpec.id)
assertThat(subject.getNextEligibleJob(10, NO_PREDICATE)).isEqualTo(fullSpec4.jobSpec)
assertThat(subject.getNextEligibleJob(15, NO_PREDICATE)).isEqualTo(fullSpec4.jobSpec)
subject.deleteJob(fullSpec4.jobSpec.id)
assertThat(subject.getNextEligibleJob(10, NO_PREDICATE)).isEqualTo(fullSpec7.jobSpec)
assertThat(subject.getNextEligibleJob(15, NO_PREDICATE)).isEqualTo(fullSpec7.jobSpec)
subject.deleteJob(fullSpec7.jobSpec.id)
assertThat(subject.getNextEligibleJob(10, NO_PREDICATE)).isEqualTo(fullSpec8.jobSpec)
assertThat(subject.getNextEligibleJob(15, NO_PREDICATE)).isEqualTo(fullSpec8.jobSpec)
}
@Test
@@ -799,6 +799,20 @@ class FastJobStorageTest {
assertThat(subject.getNextEligibleJob(100, NO_PREDICATE)).isEqualTo(olderJob)
}
@Test
fun `getNextEligibleJob - jobs with initial delay will not run until after the delay`() {
val fullSpec1 = FullSpec(jobSpec(id = "1", factoryKey = "f1", queueKey = "q1", createTime = 1, initialDelay = 10), emptyList(), emptyList())
val fullSpec2 = FullSpec(jobSpec(id = "2", factoryKey = "f2", queueKey = "q2", createTime = 2, initialDelay = 0), emptyList(), emptyList())
val subject = FastJobStorage(mockDatabase(listOf(fullSpec1, fullSpec2)))
subject.init()
assertThat(subject.getNextEligibleJob(10, NO_PREDICATE)).isEqualTo(fullSpec2.jobSpec)
subject.deleteJob(fullSpec2.jobSpec.id)
assertThat(subject.getNextEligibleJob(20, NO_PREDICATE)).isEqualTo(fullSpec1.jobSpec)
}
@Test
fun `deleteJobs - writes to database`() {
val database = mockDatabase(DataSet1.FULL_SPECS)
@@ -1043,7 +1057,8 @@ class FastJobStorageTest {
isRunning: Boolean = false,
isMemoryOnly: Boolean = false,
globalPriority: Int = 0,
queuePriority: Int = 0
queuePriority: Int = 0,
initialDelay: Long = 0
): JobSpec {
return JobSpec(
id = id,
@@ -1060,7 +1075,8 @@ class FastJobStorageTest {
isRunning = isRunning,
isMemoryOnly = isMemoryOnly,
globalPriority = globalPriority,
queuePriority = queuePriority
queuePriority = queuePriority,
initialDelay = initialDelay
)
}
@@ -1080,7 +1096,8 @@ class FastJobStorageTest {
isRunning = false,
isMemoryOnly = false,
globalPriority = 0,
queuePriority = 0
queuePriority = 0,
initialDelay = 0
)
val JOB_2 = JobSpec(
id = "id2",
@@ -1097,7 +1114,8 @@ class FastJobStorageTest {
isRunning = false,
isMemoryOnly = false,
globalPriority = 0,
queuePriority = 0
queuePriority = 0,
initialDelay = 0
)
val JOB_3 = JobSpec(
id = "id3",
@@ -1114,7 +1132,8 @@ class FastJobStorageTest {
isRunning = false,
isMemoryOnly = false,
globalPriority = 0,
queuePriority = 0
queuePriority = 0,
initialDelay = 0
)
val CONSTRAINT_1 = ConstraintSpec(jobSpecId = "id1", factoryKey = "f1", isMemoryOnly = false)
@@ -1163,7 +1182,8 @@ class FastJobStorageTest {
isRunning = false,
isMemoryOnly = true,
globalPriority = 0,
queuePriority = 0
queuePriority = 0,
initialDelay = 0
)
val CONSTRAINT_1 = ConstraintSpec(jobSpecId = "id1", factoryKey = "f1", isMemoryOnly = true)
val FULL_SPEC_1 = FullSpec(JOB_1, listOf(CONSTRAINT_1), emptyList())
@@ -1186,7 +1206,8 @@ class FastJobStorageTest {
isRunning = false,
isMemoryOnly = false,
globalPriority = 0,
queuePriority = 0
queuePriority = 0,
initialDelay = 0
)
val JOB_2 = JobSpec(
id = "id2",
@@ -1203,7 +1224,8 @@ class FastJobStorageTest {
isRunning = false,
isMemoryOnly = false,
globalPriority = 0,
queuePriority = 0
queuePriority = 0,
initialDelay = 0
)
val JOB_3 = JobSpec(
id = "id3",
@@ -1220,7 +1242,8 @@ class FastJobStorageTest {
isRunning = false,
isMemoryOnly = false,
globalPriority = 0,
queuePriority = 0
queuePriority = 0,
initialDelay = 0
)
val DEPENDENCY_1 = DependencySpec(jobId = "id1", dependsOnJobId = "id2", isMemoryOnly = false)