mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-27 05:03:28 +00:00
Update linked devices screen.
This commit is contained in:
@@ -160,7 +160,11 @@ class AppSettingsFragment : DSLSettingsFragment(
|
||||
title = DSLSettingsText.from(R.string.preferences__linked_devices),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_devices_24),
|
||||
onClick = {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_deviceActivity)
|
||||
if (FeatureFlags.linkedDevicesV2()) {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_linkDeviceFragment)
|
||||
} else {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_deviceActivity)
|
||||
}
|
||||
},
|
||||
isEnabled = state.isRegisteredAndUpToDate()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.thoughtcrime.securesms.linkdevice
|
||||
|
||||
/**
|
||||
* Class that represents a linked device
|
||||
*/
|
||||
data class Device(val id: Long, val name: String, val createdMillis: Long, val lastSeenMillis: Long)
|
||||
@@ -0,0 +1,242 @@
|
||||
package org.thoughtcrime.securesms.linkdevice
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dialogs
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.thoughtcrime.securesms.DeviceActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Fragment that shows current linked devices
|
||||
*/
|
||||
class LinkDeviceFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel: LinkDeviceViewModel by viewModels()
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state
|
||||
|
||||
LaunchedEffect(state.toastDialog) {
|
||||
if (state.toastDialog.isNotEmpty()) {
|
||||
Toast.makeText(requireContext(), state.toastDialog, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(id = R.string.preferences__linked_devices),
|
||||
onNavigationClick = { findNavController().popBackStack() },
|
||||
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24),
|
||||
navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close)
|
||||
) { contentPadding: PaddingValues ->
|
||||
DeviceDescriptionScreen(
|
||||
state = state,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
onLearnMore = this::openLearnMore,
|
||||
onLinkDevice = this::openLinkNewDevice,
|
||||
setDeviceToRemove = this::setDeviceToRemove,
|
||||
onRemoveDevice = this::onRemoveDevice
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.loadDevices(requireContext())
|
||||
}
|
||||
|
||||
private fun openLearnMore() {
|
||||
LinkDeviceLearnMoreBottomSheetFragment().show(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
|
||||
private fun openLinkNewDevice() {
|
||||
// TODO(Michelle): Use linkDeviceAddFragment
|
||||
startActivity(DeviceActivity.getIntentForScanner(requireContext()))
|
||||
// findNavController().safeNavigate(R.id.action_linkDeviceFragment_to_linkDeviceAddFragment)
|
||||
}
|
||||
|
||||
private fun setDeviceToRemove(device: Device?) {
|
||||
viewModel.setDeviceToRemove(device)
|
||||
}
|
||||
|
||||
private fun onRemoveDevice(device: Device) {
|
||||
viewModel.removeDevice(requireContext(), device)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeviceDescriptionScreen(
|
||||
state: LinkDeviceSettingsState,
|
||||
modifier: Modifier = Modifier,
|
||||
onLearnMore: () -> Unit = {},
|
||||
onLinkDevice: () -> Unit = {},
|
||||
setDeviceToRemove: (Device?) -> Unit = {},
|
||||
onRemoveDevice: (Device) -> Unit = {}
|
||||
) {
|
||||
if (state.progressDialogMessage != -1) {
|
||||
Dialogs.IndeterminateProgressDialog(stringResource(id = state.progressDialogMessage))
|
||||
}
|
||||
if (state.deviceToRemove != null) {
|
||||
val device: Device = state.deviceToRemove
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(id = R.string.DeviceListActivity_unlink_s, device.name),
|
||||
body = stringResource(id = R.string.DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive),
|
||||
confirm = stringResource(R.string.LinkDeviceFragment__unlink),
|
||||
dismiss = stringResource(android.R.string.cancel),
|
||||
onConfirm = { onRemoveDevice(device) },
|
||||
onDismiss = { setDeviceToRemove(null) }
|
||||
)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.verticalScroll(rememberScrollState())) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_devices_intro),
|
||||
contentDescription = stringResource(R.string.preferences__linked_devices),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.LinkDeviceFragment__use_signal_on_desktop_ipad),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(start = 20.dp, end = 20.dp, top = 12.dp, bottom = 4.dp)
|
||||
)
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(id = R.string.LearnMoreTextView_learn_more)),
|
||||
style = TextStyle(color = MaterialTheme.colorScheme.primary)
|
||||
) {
|
||||
onLearnMore()
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(20.dp))
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onLinkDevice,
|
||||
modifier = Modifier.width(300.dp)
|
||||
) {
|
||||
Text(stringResource(id = R.string.LinkDeviceFragment__link_a_new_device))
|
||||
}
|
||||
|
||||
if (state.devices.isNotEmpty()) {
|
||||
Dividers.Default()
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.LinkDeviceFragment__my_linked_devices),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(start = 24.dp, top = 12.dp, bottom = 24.dp)
|
||||
)
|
||||
state.devices.forEach { device ->
|
||||
DeviceRow(device, setDeviceToRemove)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 40.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_lock_24),
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResource(id = R.string.LinkDeviceFragment__messages_and_chat_info_are_protected)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeviceRow(device: Device, setDeviceToRemove: (Device) -> Unit) {
|
||||
val titleString = device.name.ifEmpty { stringResource(R.string.DeviceListItem_unnamed_device) }
|
||||
val linkedDate = DateUtils.getDayPrecisionTimeSpanString(LocalContext.current, Locale.getDefault(), device.createdMillis)
|
||||
val lastActive = DateUtils.getDayPrecisionTimeSpanString(LocalContext.current, Locale.getDefault(), device.lastSeenMillis)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { setDeviceToRemove(device) },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.symbol_devices_24),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
|
||||
contentScale = ContentScale.Inside,
|
||||
modifier = Modifier
|
||||
.padding(start = 24.dp)
|
||||
.size(40.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = CircleShape
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(20.dp))
|
||||
Column {
|
||||
Text(text = titleString, style = MaterialTheme.typography.bodyLarge)
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
Text(stringResource(R.string.DeviceListItem_linked_s, linkedDate), style = MaterialTheme.typography.bodyMedium)
|
||||
Text(stringResource(R.string.DeviceListItem_last_active_s, lastActive), style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun DeviceScreenPreview() {
|
||||
val previewDevices = listOf(
|
||||
Device(1, "Sam's Macbook Pro", 1715793982000, 1716053182000),
|
||||
Device(1, "Sam's iPad", 1715793182000, 1716053122000)
|
||||
)
|
||||
val previewState = LinkDeviceSettingsState(devices = previewDevices)
|
||||
|
||||
Previews.Preview {
|
||||
DeviceDescriptionScreen(previewState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package org.thoughtcrime.securesms.linkdevice
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.ui.Texts
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
|
||||
/**
|
||||
* Bottom sheet dialog displayed when users click 'Learn more' when linking a device
|
||||
*/
|
||||
class LinkDeviceLearnMoreBottomSheetFragment : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
override val peekHeightPercentage: Float = 0.8f
|
||||
|
||||
companion object {
|
||||
const val SIGNAL_DOWNLOAD_URL = "https://signal.org/download"
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
LearnMoreSheet()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LearnMoreSheet() {
|
||||
val context = LocalContext.current
|
||||
val downloadUrl = stringResource(id = R.string.LinkDeviceFragment__signal_download_url)
|
||||
val fullString = stringResource(id = R.string.LinkDeviceFragment__on_other_device_visit_signal, downloadUrl)
|
||||
val spanned = SpanUtil.urlSubsequence(fullString, downloadUrl, LinkDeviceLearnMoreBottomSheetFragment.SIGNAL_DOWNLOAD_URL)
|
||||
|
||||
return Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentSize(Alignment.Center)
|
||||
.padding(bottom = 48.dp)
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_all_devices),
|
||||
contentDescription = null,
|
||||
tint = Color.Unspecified,
|
||||
modifier = Modifier.size(110.dp)
|
||||
)
|
||||
Text(
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
text = stringResource(R.string.LinkDeviceFragment__signal_on_desktop_ipad),
|
||||
modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp)
|
||||
)
|
||||
LinkedDeviceInformationRow(
|
||||
painterResource(R.drawable.symbol_lock_24),
|
||||
stringResource(R.string.LinkDeviceFragment__all_messaging_is_private)
|
||||
)
|
||||
LinkedDeviceInformationRow(
|
||||
painterResource(R.drawable.ic_replies_outline_20),
|
||||
stringResource(R.string.LinkDeviceFragment__signal_messages_are_synchronized)
|
||||
)
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp, start = 40.dp, end = 32.dp)) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_save_android_24),
|
||||
contentDescription = stringResource(R.string.preferences__linked_devices),
|
||||
modifier = Modifier.size(24.dp).padding(top = 4.dp)
|
||||
)
|
||||
Texts.LinkifiedText(
|
||||
textWithUrlSpans = spanned,
|
||||
onUrlClick = { CommunicationActions.openBrowserLink(context, it) },
|
||||
style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
modifier = Modifier.padding(start = 20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LinkedDeviceInformationRow(
|
||||
iconPainter: Painter,
|
||||
text: String
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp, start = 40.dp, end = 32.dp)) {
|
||||
Icon(
|
||||
painter = iconPainter,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp).padding(top = 4.dp)
|
||||
)
|
||||
Text(
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
text = text,
|
||||
modifier = Modifier.padding(start = 20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun LearnMorePreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
LearnMoreSheet()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.thoughtcrime.securesms.linkdevice
|
||||
|
||||
import org.signal.core.util.Base64.decode
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.devicelist.protos.DeviceName
|
||||
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Repository for linked devices and its various actions (linking, unlinking, listing).
|
||||
*/
|
||||
object LinkDeviceRepository {
|
||||
|
||||
private val TAG = Log.tag(LinkDeviceRepository::class)
|
||||
|
||||
fun removeDevice(deviceId: Long): Boolean {
|
||||
return try {
|
||||
val accountManager = AppDependencies.signalServiceAccountManager
|
||||
accountManager.removeDevice(deviceId)
|
||||
LinkedDeviceInactiveCheckJob.enqueue()
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDevices(): List<Device>? {
|
||||
val accountManager = AppDependencies.signalServiceAccountManager
|
||||
return try {
|
||||
val devices: List<Device> = accountManager.getDevices()
|
||||
.filter { d: DeviceInfo -> d.getId() != SignalServiceAddress.DEFAULT_DEVICE_ID }
|
||||
.map { deviceInfo: DeviceInfo -> deviceInfo.toDevice() }
|
||||
.sortedBy { it.createdMillis }
|
||||
.toList()
|
||||
devices
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun DeviceInfo.toDevice(): Device {
|
||||
val defaultDevice = Device(getId().toLong(), getName(), getCreated(), getLastSeen())
|
||||
try {
|
||||
if (getName().isNullOrEmpty() || getName().length < 4) {
|
||||
Log.w(TAG, "Invalid DeviceInfo name.")
|
||||
return defaultDevice
|
||||
}
|
||||
|
||||
val deviceName = DeviceName.ADAPTER.decode(decode(getName()))
|
||||
if (deviceName.ciphertext == null || deviceName.ephemeralPublic == null || deviceName.syntheticIv == null) {
|
||||
Log.w(TAG, "Got a DeviceName that wasn't properly populated.")
|
||||
return defaultDevice
|
||||
}
|
||||
|
||||
val plaintext = DeviceNameCipher.decryptDeviceName(deviceName, SignalStore.account().aciIdentityKey)
|
||||
if (plaintext == null) {
|
||||
Log.w(TAG, "Failed to decrypt device name.")
|
||||
return defaultDevice
|
||||
}
|
||||
|
||||
return Device(getId().toLong(), String(plaintext), getCreated(), getLastSeen())
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed while reading the protobuf.", e)
|
||||
}
|
||||
return defaultDevice
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.linkdevice
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
/**
|
||||
* Information about linked devices. Used in [LinkDeviceViewModel].
|
||||
*/
|
||||
data class LinkDeviceSettingsState(
|
||||
val devices: List<Device> = emptyList(),
|
||||
val deviceToRemove: Device? = null,
|
||||
@StringRes val progressDialogMessage: Int = -1,
|
||||
val toastDialog: String = ""
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.thoughtcrime.securesms.linkdevice
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Maintains the state of the [LinkDeviceFragment]
|
||||
*/
|
||||
class LinkDeviceViewModel : ViewModel() {
|
||||
|
||||
private val _state = mutableStateOf(LinkDeviceSettingsState())
|
||||
val state: State<LinkDeviceSettingsState> = _state
|
||||
|
||||
fun onResume() {
|
||||
_state.value = _state.value.copy()
|
||||
}
|
||||
|
||||
fun setDeviceToRemove(device: Device?) {
|
||||
_state.value = _state.value.copy(deviceToRemove = device)
|
||||
}
|
||||
|
||||
fun removeDevice(context: Context, device: Device) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_state.value = _state.value.copy(
|
||||
progressDialogMessage = R.string.DeviceListActivity_unlinking_device
|
||||
)
|
||||
val success = LinkDeviceRepository.removeDevice(device.id)
|
||||
if (success) {
|
||||
loadDevices(context)
|
||||
_state.value = _state.value.copy(
|
||||
toastDialog = context.getString(R.string.LinkDeviceFragment__s_unlinked, device.name),
|
||||
progressDialogMessage = -1
|
||||
)
|
||||
} else {
|
||||
_state.value = _state.value.copy(
|
||||
progressDialogMessage = -1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDevices(context: Context) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val devices = LinkDeviceRepository.loadDevices()
|
||||
if (devices == null) {
|
||||
_state.value = _state.value.copy(
|
||||
toastDialog = context.getString(R.string.DeviceListActivity_network_failed),
|
||||
progressDialogMessage = -1
|
||||
)
|
||||
} else {
|
||||
_state.value = _state.value.copy(
|
||||
devices = devices,
|
||||
progressDialogMessage = -1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,13 +210,7 @@ object DateUtils : android.text.format.DateUtils() {
|
||||
return if (isSameDay(System.currentTimeMillis(), timestamp)) {
|
||||
context.getString(R.string.DeviceListItem_today)
|
||||
} else {
|
||||
val format: String = when {
|
||||
timestamp.isWithin(6.days) -> "EEE "
|
||||
timestamp.isWithin(365.days) -> "MMM d"
|
||||
else -> "MMM d, yyy"
|
||||
}
|
||||
|
||||
timestamp.toDateString(format, locale)
|
||||
timestamp.toDateString("dd/MM/yy", locale)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ public final class FeatureFlags {
|
||||
private static final String RESTORE_POST_REGISTRATION = "android.registration.restorePostRegistration";
|
||||
private static final String LIBSIGNAL_WEB_SOCKET_SHADOW_PCT = "android.libsignalWebSocketShadowingPercentage";
|
||||
private static final String DELETE_SYNC_SEND_RECEIVE = "android.deleteSyncSendReceive";
|
||||
private static final String LINKED_DEVICES_V2 = "android.linkedDevices.v2";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
@@ -211,7 +212,7 @@ public final class FeatureFlags {
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS, REGISTRATION_V2, RESTORE_POST_REGISTRATION);
|
||||
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS, REGISTRATION_V2, RESTORE_POST_REGISTRATION, LINKED_DEVICES_V2);
|
||||
|
||||
/**
|
||||
* Values in this map will take precedence over any value. This should only be used for local
|
||||
@@ -750,6 +751,11 @@ public final class FeatureFlags {
|
||||
return getBoolean(DELETE_SYNC_SEND_RECEIVE, false);
|
||||
}
|
||||
|
||||
/** Whether or not to use V2 of linked devices. */
|
||||
public static boolean linkedDevicesV2() {
|
||||
return getBoolean(LINKED_DEVICES_V2, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
||||
Reference in New Issue
Block a user