mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 11:20:47 +01:00
Notify a user when they link a device.
This commit is contained in:
committed by
Greyson Parrelli
parent
919648b94b
commit
d4c8c16df3
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -73,7 +73,8 @@ public class JobMigrator {
|
||||
jobSpec.isRunning(),
|
||||
jobSpec.isMemoryOnly(),
|
||||
jobSpec.getGlobalPriority(),
|
||||
jobSpec.getQueuePriority());
|
||||
jobSpec.getQueuePriority(),
|
||||
jobSpec.getInitialDelay());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() { }
|
||||
|
||||
|
||||
@@ -151,4 +151,9 @@ message BackupMessagesJobData {
|
||||
string dataFile = 2;
|
||||
ResumableUpload uploadSpec = 3;
|
||||
string resumableUri = 4;
|
||||
}
|
||||
|
||||
message NewLinkedDeviceNotificationJobData {
|
||||
uint32 deviceId = 1;
|
||||
uint64 deviceCreatedAt = 2;
|
||||
}
|
||||
@@ -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>
|
||||
39
app/src/main/res/drawable/symbol_linked_device.xml
Normal file
39
app/src/main/res/drawable/symbol_linked_device.xml
Normal 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>
|
||||
@@ -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 -->
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user