diff --git a/app/build.gradle b/app/build.gradle index de71e3209a..f45e8ba3e9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,8 +28,8 @@ configurations.all { exclude module: "commons-logging" } -def canonicalVersionCode = 376 -def canonicalVersionName = "1.18.6" +def canonicalVersionCode = 377 +def canonicalVersionName = "1.19.0" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 1870cf6260..82699f393a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -86,6 +86,7 @@ import org.thoughtcrime.securesms.sskenvironment.ProfileManager; import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager; import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository; import org.thoughtcrime.securesms.util.Broadcaster; +import org.thoughtcrime.securesms.util.VersionDataFetcher; import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper; import org.thoughtcrime.securesms.webrtc.CallMessageProcessor; import org.webrtc.PeerConnectionFactory; @@ -110,7 +111,6 @@ import javax.inject.Inject; import dagger.hilt.EntryPoints; import dagger.hilt.android.HiltAndroidApp; import kotlin.Unit; -import kotlinx.coroutines.Job; import network.loki.messenger.BuildConfig; import network.loki.messenger.libsession_util.ConfigBase; import network.loki.messenger.libsession_util.UserProfile; @@ -151,6 +151,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO @Inject PushRegistry pushRegistry; @Inject ConfigFactory configFactory; @Inject LastSentTimestampCache lastSentTimestampCache; + @Inject VersionDataFetcher versionDataFetcher; CallMessageProcessor callMessageProcessor; MessagingModuleConfiguration messagingModuleConfiguration; @@ -275,6 +276,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO OpenGroupManager.INSTANCE.startPolling(); }); + + // fetch last version data + versionDataFetcher.startTimedVersionCheck(); } @Override @@ -287,12 +291,14 @@ public class ApplicationContext extends Application implements DefaultLifecycleO poller.stopIfNeeded(); } ClosedGroupPollerV2.getShared().stopAll(); + versionDataFetcher.stopTimedVersionCheck(); } @Override public void onTerminate() { stopKovenant(); // Loki OpenGroupManager.INSTANCE.stopPolling(); + versionDataFetcher.stopTimedVersionCheck(); super.onTerminate(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt index f294e387ff..071da43311 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt @@ -18,7 +18,7 @@ fun showMuteDialog( private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) { ONE_HOUR(R.string.arrays__mute_for_one_hour, duration = TimeUnit.HOURS.toMillis(1)), - TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.DAYS.toMillis(2)), + TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.HOURS.toMillis(2)), ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)), SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)), FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt index 1ffc65f592..8dffb1fd9b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt @@ -27,7 +27,11 @@ import org.thoughtcrime.securesms.groups.JoinCommunityFragment @AndroidEntryPoint class StartConversationFragment : BottomSheetDialogFragment(), StartConversationDelegate { - private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * 0.94).toInt() } + companion object{ + const val PEEK_RATIO = 0.94f + } + + private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * PEEK_RATIO).toInt() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt index 97740b2a26..f0a6e21b4c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt @@ -1,10 +1,13 @@ package org.thoughtcrime.securesms.conversation.start.newmessage -import androidx.compose.animation.AnimatedVisibility +import android.graphics.Rect +import android.os.Build +import android.view.ViewTreeObserver import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -15,23 +18,31 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import network.loki.messenger.R -import org.thoughtcrime.securesms.onboarding.ui.ContinuePrimaryOutlineButton +import org.thoughtcrime.securesms.conversation.start.StartConversationFragment.Companion.PEEK_RATIO import org.thoughtcrime.securesms.ui.LoadingArcOr -import org.thoughtcrime.securesms.ui.theme.LocalDimensions -import org.thoughtcrime.securesms.ui.theme.PreviewTheme -import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider -import org.thoughtcrime.securesms.ui.theme.ThemeColors -import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.components.AppBar import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode @@ -39,7 +50,13 @@ import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField import org.thoughtcrime.securesms.ui.components.SessionTabRow import org.thoughtcrime.securesms.ui.contentDescription +import org.thoughtcrime.securesms.ui.theme.LocalColors +import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType +import org.thoughtcrime.securesms.ui.theme.PreviewTheme +import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider +import org.thoughtcrime.securesms.ui.theme.ThemeColors +import kotlin.math.max private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan) @@ -76,63 +93,113 @@ private fun EnterAccountId( callbacks: Callbacks, onHelp: () -> Unit = {} ) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .imePadding() - ) { - Column( - modifier = Modifier.padding(vertical = LocalDimensions.current.spacing), - horizontalAlignment = Alignment.CenterHorizontally, + // the scaffold is required to provide the contentPadding. That contentPadding is needed + // to properly handle the ime padding. + Scaffold() { contentPadding -> + // we need this extra surface to handle nested scrolling properly, + // because this scrollable component is inside a bottomSheet dialog which is itself scrollable + Surface( + modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()), + color = LocalColors.current.backgroundSecondary ) { - SessionOutlinedTextField( - text = state.newMessageIdOrOns, - modifier = Modifier - .padding(horizontal = LocalDimensions.current.spacing), - contentDescription = "Session id input box", - placeholder = stringResource(R.string.accountIdOrOnsEnter), - onChange = callbacks::onChange, - onContinue = callbacks::onContinue, - error = state.error?.string(), - isTextErrorColor = state.isTextErrorColor - ) - Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) + var accountModifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) - BorderlessButtonWithIcon( - text = stringResource(R.string.messageNewDescription), - modifier = Modifier - .contentDescription(R.string.AccessibilityId_help_desk_link) - .padding(horizontal = LocalDimensions.current.mediumSpacing) - .fillMaxWidth(), - style = LocalType.current.small, - color = LocalColors.current.textSecondary, - iconRes = R.drawable.ic_circle_question_mark, - onClick = onHelp - ) - } + // There is a known issue with the ime padding on android versions below 30 + /// So on these older versions we need to resort to some manual padding based on the visible height + // when the keyboard is up + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + val keyboardHeight by keyboardHeight() + accountModifier = accountModifier.padding(bottom = keyboardHeight) + } else { + accountModifier = accountModifier + .consumeWindowInsets(contentPadding) + .imePadding() + } - Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) - Spacer(Modifier.weight(2f)) + Column( + modifier = accountModifier + ) { + Column( + modifier = Modifier.padding(vertical = LocalDimensions.current.spacing), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SessionOutlinedTextField( + text = state.newMessageIdOrOns, + modifier = Modifier + .padding(horizontal = LocalDimensions.current.spacing), + contentDescription = "Session id input box", + placeholder = stringResource(R.string.accountIdOrOnsEnter), + onChange = callbacks::onChange, + onContinue = callbacks::onContinue, + error = state.error?.string(), + isTextErrorColor = state.isTextErrorColor + ) - PrimaryOutlineButton( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(horizontal = LocalDimensions.current.xlargeSpacing) - .padding(bottom = LocalDimensions.current.smallSpacing) - .fillMaxWidth() - .contentDescription(R.string.next), - enabled = state.isNextButtonEnabled, - onClick = callbacks::onContinue - ) { - LoadingArcOr(state.loading) { - Text(stringResource(R.string.next)) + Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) + + BorderlessButtonWithIcon( + text = stringResource(R.string.messageNewDescription), + modifier = Modifier + .contentDescription(R.string.AccessibilityId_help_desk_link) + .padding(horizontal = LocalDimensions.current.mediumSpacing) + .fillMaxWidth(), + style = LocalType.current.small, + color = LocalColors.current.textSecondary, + iconRes = R.drawable.ic_circle_question_mark, + onClick = onHelp + ) + } + + Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) + Spacer(Modifier.weight(2f)) + + PrimaryOutlineButton( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(horizontal = LocalDimensions.current.xlargeSpacing) + .padding(bottom = LocalDimensions.current.smallSpacing) + .fillMaxWidth() + .contentDescription(R.string.next), + enabled = state.isNextButtonEnabled, + onClick = callbacks::onContinue + ) { + LoadingArcOr(state.loading) { + Text(stringResource(R.string.next)) + } + } } } } } +@Composable +fun keyboardHeight(): MutableState { + val view = LocalView.current + var keyboardHeight = remember { mutableStateOf(0.dp) } + val density = LocalDensity.current + + DisposableEffect(view) { + val listener = ViewTreeObserver.OnGlobalLayoutListener { + val rect = Rect() + view.getWindowVisibleDisplayFrame(rect) + val screenHeight = view.rootView.height * PEEK_RATIO + val keypadHeightPx = max( screenHeight - rect.bottom, 0f) + + keyboardHeight.value = with(density) { keypadHeightPx.toDp() } + } + + view.viewTreeObserver.addOnGlobalLayoutListener(listener) + onDispose { + view.viewTreeObserver.removeOnGlobalLayoutListener(listener) + } + } + + return keyboardHeight +} + @Preview @Composable private fun PreviewNewMessage( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt index 65c8dd539a..6ed8a08233 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt @@ -46,7 +46,7 @@ internal class NewMessageViewModel @Inject constructor( } override fun onContinue() { - val idOrONS = state.value.newMessageIdOrOns + val idOrONS = state.value.newMessageIdOrOns.trim() if (PublicKeyValidation.isValid(idOrONS, isPrefixRequired = false)) { onUnvalidatedPublicKey(publicKey = idOrONS) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 98f300dc66..8a09ba542b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -164,7 +164,6 @@ import org.thoughtcrime.securesms.mms.VideoSlide import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment -import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities @@ -177,7 +176,6 @@ import org.thoughtcrime.securesms.util.isScrolledToBottom import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.show -import org.thoughtcrime.securesms.util.start import org.thoughtcrime.securesms.util.toPx import java.lang.ref.WeakReference import java.util.Locale @@ -1589,8 +1587,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val text = getMessageBody() val userPublicKey = textSecurePreferences.getLocalNumber() val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey) - if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) { - start() + if (seed in text && !isNoteToSelf && !hasPermissionToSendSeed) { + showSessionDialog { + title(R.string.dialog_send_seed_title) + text(R.string.dialog_send_seed_explanation) + button(R.string.dialog_send_seed_send_button_title) { sendTextOnlyMessage(true) } + cancelButton() + } + + return null } // Create the message val message = VisibleMessage().applyExpiryMode(viewModel.threadId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt deleted file mode 100644 index 6abb0814d6..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.thoughtcrime.securesms.conversation.v2.dialogs - -import android.app.Dialog -import android.os.Bundle -import androidx.fragment.app.DialogFragment -import network.loki.messenger.R -import org.thoughtcrime.securesms.createSessionDialog - -/** Shown if the user is about to send their recovery phrase to someone. */ -class SendSeedDialog(private val proceed: (() -> Unit)? = null) : DialogFragment() { - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { - title(R.string.dialog_send_seed_title) - text(R.string.dialog_send_seed_explanation) - button(R.string.dialog_send_seed_send_button_title) { send() } - cancelButton() - } - - private fun send() { - proceed?.invoke() - dismiss() - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 74ce5638da..5c411b54a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -80,16 +80,14 @@ import org.thoughtcrime.securesms.util.start import java.io.IOException import javax.inject.Inject +private const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT" +private const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING" + @AndroidEntryPoint class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener, GlobalSearchInputLayout.GlobalSearchInputLayoutListener { - companion object { - const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT" - const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING" - } - private lateinit var binding: ActivityHomeBinding private lateinit var glide: RequestManager @@ -137,7 +135,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } } - private val isNewAccount: Boolean get() = intent.getBooleanExtra(FROM_ONBOARDING, false) + private val isFromOnboarding: Boolean get() = intent.getBooleanExtra(FROM_ONBOARDING, false) + private val isNewAccount: Boolean get() = intent.getBooleanExtra(NEW_ACCOUNT, false) // region Lifecycle override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { @@ -251,7 +250,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } else -> buildList { result.contactAndGroupList.takeUnless { it.isEmpty() }?.let { - add(GlobalSearchAdapter.Model.Header(R.string.contacts)) + add(GlobalSearchAdapter.Model.Header(R.string.conversations)) addAll(it) } result.messageResults.takeUnless { it.isEmpty() }?.let { @@ -264,8 +263,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } } EventBus.getDefault().register(this@HomeActivity) - if (intent.hasExtra(FROM_ONBOARDING) - && intent.getBooleanExtra(FROM_ONBOARDING, false)) { + if (isFromOnboarding) { if (Build.VERSION.SDK_INT >= 33 && (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled().not()) { Permissions.with(this) @@ -637,10 +635,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } } -fun Context.startHomeActivity(isNewAccount: Boolean) { +fun Context.startHomeActivity(isFromOnboarding: Boolean, isNewAccount: Boolean) { Intent(this, HomeActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - putExtra(HomeActivity.NEW_ACCOUNT, true) - putExtra(HomeActivity.FROM_ONBOARDING, true) + putExtra(NEW_ACCOUNT, isNewAccount) + putExtra(FROM_ONBOARDING, isFromOnboarding) }.also(::startActivity) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingActivity.kt index 9c8a1869d4..abf0471598 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingActivity.kt @@ -32,7 +32,7 @@ class LoadingActivity: BaseActionBarActivity() { when { loadFailed -> startPickDisplayNameActivity(loadFailed = true) - else -> startHomeActivity(isNewAccount = false) + else -> startHomeActivity(isNewAccount = false, isFromOnboarding = true) } finish() diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt index 1e0a21d571..98e9c8b20d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt @@ -8,6 +8,7 @@ import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.hexEncodedPublicKey import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.util.VersionDataFetcher import javax.inject.Inject import javax.inject.Singleton @@ -16,6 +17,7 @@ class CreateAccountManager @Inject constructor( private val application: Application, private val prefs: TextSecurePreferences, private val configFactory: ConfigFactory, + private val versionDataFetcher: VersionDataFetcher ) { private val database: LokiAPIDatabaseProtocol get() = SnodeModule.shared.storage @@ -41,5 +43,7 @@ class CreateAccountManager @Inject constructor( prefs.setLocalRegistrationId(registrationID) prefs.setLocalNumber(userHexEncodedPublicKey) prefs.setRestorationTime(0) + + versionDataFetcher.startTimedVersionCheck() } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt index 5a40103830..51d1b24609 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt @@ -12,6 +12,7 @@ import org.session.libsignal.utilities.hexEncodedPublicKey import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.util.VersionDataFetcher import javax.inject.Inject import javax.inject.Singleton @@ -19,7 +20,8 @@ import javax.inject.Singleton class LoadAccountManager @Inject constructor( @dagger.hilt.android.qualifiers.ApplicationContext private val context: Context, private val configFactory: ConfigFactory, - private val prefs: TextSecurePreferences + private val prefs: TextSecurePreferences, + private val versionDataFetcher: VersionDataFetcher ) { private val database: LokiAPIDatabaseProtocol get() = SnodeModule.shared.storage @@ -52,6 +54,8 @@ class LoadAccountManager @Inject constructor( setHasViewedSeed(true) } + versionDataFetcher.startTimedVersionCheck() + ApplicationContext.getInstance(context).retrieveUserProfile() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt index 42cf49bce8..9ea4fd2bd6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt @@ -49,7 +49,7 @@ class MessageNotificationsActivity : BaseActionBarActivity() { viewModel.events.collect { when (it) { Event.Loading -> start() - Event.OnboardingComplete -> startHomeActivity(isNewAccount = true) + Event.OnboardingComplete -> startHomeActivity(isNewAccount = true, isFromOnboarding = true) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt index 97aacf82e6..8ade5b4e8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt @@ -45,7 +45,7 @@ class PickDisplayNameActivity : BaseActionBarActivity() { viewModel.events.collect { when (it) { is Event.CreateAccount -> startMessageNotificationsActivity(it.profileName) - Event.LoadAccountComplete -> startHomeActivity(isNewAccount = false) + Event.LoadAccountComplete -> startHomeActivity(isNewAccount = false, isFromOnboarding = true) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt index afe33400ff..2547e23e22 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt @@ -6,7 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.session.libsession.utilities.TextSecurePreferences -import org.thoughtcrime.securesms.ui.theme.selectedTheme +import org.thoughtcrime.securesms.ui.theme.invalidateComposeThemeColors import org.thoughtcrime.securesms.util.ThemeState import org.thoughtcrime.securesms.util.themeState import javax.inject.Inject @@ -21,6 +21,8 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec prefs.setAccentColorStyle(newAccentColorStyle) // update UI state _uiState.value = prefs.themeState() + + invalidateComposeThemeColors() } fun setNewStyle(newThemeStyle: String) { @@ -28,16 +30,13 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec // update UI state _uiState.value = prefs.themeState() - // force compose to refresh its style reference - selectedTheme = null + invalidateComposeThemeColors() } fun setNewFollowSystemSettings(followSystemSettings: Boolean) { prefs.setFollowSystemSettings(followSystemSettings) _uiState.value = prefs.themeState() - // force compose to refresh its style reference - selectedTheme = null + invalidateComposeThemeColors() } - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt deleted file mode 100644 index 3ff4d55b61..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.thoughtcrime.securesms.ui.theme - -/** - * This class holds two instances of [ThemeColors], [light] representing the [ThemeColors] to use when the system is in a - * light theme, and [dark] representing the [ThemeColors] to use when the system is in a dark theme. - */ -data class ThemeColorSet( - val light: ThemeColors, - val dark: ThemeColors -) \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorsProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorsProvider.kt new file mode 100644 index 0000000000..0713254afc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorsProvider.kt @@ -0,0 +1,20 @@ +package org.thoughtcrime.securesms.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable + +fun interface ThemeColorsProvider { + @Composable + fun get(): ThemeColors +} + +@Suppress("FunctionName") +fun FollowSystemThemeColorsProvider(light: ThemeColors, dark: ThemeColors) = ThemeColorsProvider { + when { + isSystemInDarkTheme() -> dark + else -> light + } +} + +@Suppress("FunctionName") +fun ThemeColorsProvider(colors: ThemeColors) = ThemeColorsProvider { colors } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt index 8988e84ca7..403235ef79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt @@ -1,7 +1,5 @@ package org.thoughtcrime.securesms.ui.theme -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences.Companion.BLUE_ACCENT @@ -17,38 +15,25 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.YELLOW_A * Some behaviour is hardcoded to cater for legacy usage of people with themes already set * But future themes will be picked and set directly from the "Appearance" screen */ -@Composable -fun TextSecurePreferences.getComposeTheme(): ThemeColors { +fun TextSecurePreferences.getColorsProvider(): ThemeColorsProvider { val selectedTheme = getThemeStyle() // get the chosen primary color from the preferences val selectedPrimary = primaryColor() - // create a theme set with the appropriate primary - val colorSet = when(selectedTheme){ - TextSecurePreferences.OCEAN_DARK, - TextSecurePreferences.OCEAN_LIGHT -> ThemeColorSet( - light = OceanLight(selectedPrimary), - dark = OceanDark(selectedPrimary) - ) - - else -> ThemeColorSet( - light = ClassicLight(selectedPrimary), - dark = ClassicDark(selectedPrimary) + val isOcean = "ocean" in selectedTheme + + val createLight = if (isOcean) ::OceanLight else ::ClassicLight + val createDark = if (isOcean) ::OceanDark else ::ClassicDark + + return when { + getFollowSystemSettings() -> FollowSystemThemeColorsProvider( + light = createLight(selectedPrimary), + dark = createDark(selectedPrimary) ) + "light" in selectedTheme -> ThemeColorsProvider(createLight(selectedPrimary)) + else -> ThemeColorsProvider(createDark(selectedPrimary)) } - - // deliver the right set from the light/dark mode chosen - val theme = when{ - getFollowSystemSettings() -> if(isSystemInDarkTheme()) colorSet.dark else colorSet.light - - selectedTheme == TextSecurePreferences.CLASSIC_LIGHT || - selectedTheme == TextSecurePreferences.OCEAN_LIGHT -> colorSet.light - - else -> colorSet.dark - } - - return theme } fun TextSecurePreferences.primaryColor(): Color = when(getSelectedAccentColor()) { @@ -60,6 +45,3 @@ fun TextSecurePreferences.primaryColor(): Color = when(getSelectedAccentColor()) YELLOW_ACCENT -> primaryYellow else -> primaryGreen } - - - diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt index 87e91e0ab0..2f4957565b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt @@ -20,7 +20,12 @@ import org.session.libsession.utilities.AppTextSecurePreferences val LocalColors = compositionLocalOf { ClassicDark() } val LocalType = compositionLocalOf { sessionTypography } -var selectedTheme: ThemeColors? = null +var cachedColorsProvider: ThemeColorsProvider? = null + +fun invalidateComposeThemeColors() { + // invalidate compose theme colors + cachedColorsProvider = null +} /** * Apply a Material2 compose theme based on user selections in SharedPreferences. @@ -29,15 +34,15 @@ var selectedTheme: ThemeColors? = null fun SessionMaterialTheme( content: @Composable () -> Unit ) { - // set the theme data if it hasn't been done yet - if(selectedTheme == null) { - // Some values can be set from the preferences, and if not should fallback to a default value - val context = LocalContext.current - val preferences = AppTextSecurePreferences(context) - selectedTheme = preferences.getComposeTheme() - } + val context = LocalContext.current + val preferences = AppTextSecurePreferences(context) - SessionMaterialTheme(colors = selectedTheme ?: ClassicDark()) { content() } + val cachedColors = cachedColorsProvider ?: preferences.getColorsProvider().also { cachedColorsProvider = it } + + SessionMaterialTheme( + colors = cachedColors.get(), + content = content + ) } /** @@ -58,9 +63,8 @@ fun SessionMaterialTheme( LocalType provides sessionTypography, LocalContentColor provides colors.text, LocalTextSelectionColors provides colors.textSelectionColors, - ) { - content() - } + content = content + ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt b/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt new file mode 100644 index 0000000000..aba814524c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt @@ -0,0 +1,60 @@ +package org.thoughtcrime.securesms.util + +import android.os.Handler +import android.os.Looper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.session.libsession.messaging.file_server.FileServerApi +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.utilities.Log +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.time.Duration.Companion.hours + +private val TAG: String = VersionDataFetcher::class.java.simpleName +private val REFRESH_TIME_MS = 4.hours.inWholeMilliseconds + +@Singleton +class VersionDataFetcher @Inject constructor( + private val prefs: TextSecurePreferences +) { + private val handler = Handler(Looper.getMainLooper()) + private val fetchVersionData = Runnable { + scope.launch { + try { + // Perform the version check + val clientVersion = FileServerApi.getClientVersion() + Log.i(TAG, "Fetched version data: $clientVersion") + prefs.setLastVersionCheck() + startTimedVersionCheck() + } catch (e: Exception) { + // We can silently ignore the error + Log.e(TAG, "Error fetching version data", e) + // Schedule the next check for 4 hours from now, but do not setLastVersionCheck + // so the app will retry when the app is next foregrounded. + startTimedVersionCheck(REFRESH_TIME_MS) + } + } + } + + private val scope = CoroutineScope(Dispatchers.Default) + + /** + * Schedules fetching version data. + * + * @param delayMillis The delay before fetching version data. Default value is 4 hours from the + * last check or 0 if there was no previous check or if it was longer than 4 hours ago. + */ + @JvmOverloads + fun startTimedVersionCheck( + delayMillis: Long = REFRESH_TIME_MS + prefs.getLastVersionCheck() - System.currentTimeMillis() + ) { + stopTimedVersionCheck() + handler.postDelayed(fetchVersionData, delayMillis) + } + + fun stopTimedVersionCheck() { + handler.removeCallbacks(fetchVersionData) + } +} \ No newline at end of file diff --git a/libsession-util/libsession-util b/libsession-util/libsession-util index 20c06674d8..0193c36e0d 160000 --- a/libsession-util/libsession-util +++ b/libsession-util/libsession-util @@ -1 +1 @@ -Subproject commit 20c06674d85369c2d12261582dd36a9f21504233 +Subproject commit 0193c36e0dad461385d6407a00f33b7314e6d740 diff --git a/libsession-util/src/main/cpp/CMakeLists.txt b/libsession-util/src/main/cpp/CMakeLists.txt index 47fee4803c..f65667bcb5 100644 --- a/libsession-util/src/main/cpp/CMakeLists.txt +++ b/libsession-util/src/main/cpp/CMakeLists.txt @@ -30,6 +30,7 @@ set(SOURCES config_base.cpp contacts.cpp conversation.cpp + blinded_key.cpp util.cpp) add_library( # Sets the name of the library. diff --git a/libsession-util/src/main/cpp/blinded_key.cpp b/libsession-util/src/main/cpp/blinded_key.cpp new file mode 100644 index 0000000000..1ed1cfd554 --- /dev/null +++ b/libsession-util/src/main/cpp/blinded_key.cpp @@ -0,0 +1,34 @@ +#include +#include + +#include "util.h" +#include "jni_utils.h" + +// +// Created by Thomas Ruffie on 29/7/2024. +// + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_util_BlindKeyAPI_blindVersionKeyPair(JNIEnv *env, + jobject thiz, + jbyteArray ed25519_secret_key) { + return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { + const auto [pk, sk] = session::blind_version_key_pair(util::ustring_from_bytes(env, ed25519_secret_key)); + + jclass kp_class = env->FindClass("network/loki/messenger/libsession_util/util/KeyPair"); + jmethodID kp_constructor = env->GetMethodID(kp_class, "", "([B[B)V"); + return env->NewObject(kp_class, kp_constructor, util::bytes_from_ustring(env, {pk.data(), pk.size()}), util::bytes_from_ustring(env, {sk.data(), sk.size()})); + }); +} +extern "C" +JNIEXPORT jbyteArray JNICALL +Java_network_loki_messenger_libsession_1util_util_BlindKeyAPI_blindVersionSign(JNIEnv *env, + jobject thiz, + jbyteArray ed25519_secret_key, + jlong timestamp) { + return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { + auto bytes = session::blind_version_sign(util::ustring_from_bytes(env, ed25519_secret_key), session::Platform::android, timestamp); + return util::bytes_from_ustring(env, bytes); + }); +} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt new file mode 100644 index 0000000000..cd3dac3af2 --- /dev/null +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt @@ -0,0 +1,15 @@ +package network.loki.messenger.libsession_util.util + +object BlindKeyAPI { + private val loadLibrary by lazy { + System.loadLibrary("session_util") + } + + init { + // Ensure the library is loaded at initialization + loadLibrary + } + + external fun blindVersionKeyPair(ed25519SecretKey: ByteArray): KeyPair + external fun blindVersionSign(ed25519SecretKey: ByteArray, timestamp: Long): ByteArray +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt index 5bffed57ee..6a9c95aa56 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt @@ -1,18 +1,22 @@ package org.session.libsession.messaging.file_server +import android.util.Base64 +import network.loki.messenger.libsession_util.util.BlindKeyAPI import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.map -import okhttp3.Headers import okhttp3.Headers.Companion.toHeaders import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody +import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.snode.OnionRequestAPI +import org.session.libsession.snode.utilities.await import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.toHexString +import kotlin.time.Duration.Companion.milliseconds object FileServerApi { @@ -23,6 +27,7 @@ object FileServerApi { sealed class Error(message: String) : Exception(message) { object ParsingFailed : Error("Invalid response.") object InvalidURL : Error("Invalid URL.") + object NoEd25519KeyPair : Error("Couldn't find ed25519 key pair.") } data class Request( @@ -105,4 +110,50 @@ object FileServerApi { val request = Request(verb = HTTP.Verb.GET, endpoint = "file/$file") return send(request) } + + /** + * Returns the current version of session + * This is effectively proxying (and caching) the response from the github release + * page. + * + * Note that the value is cached and can be up to 30 minutes out of date normally, and up to 24 + * hours out of date if we cannot reach the Github API for some reason. + * + * https://github.com/oxen-io/session-file-server/blob/dev/doc/api.yaml#L119 + */ + suspend fun getClientVersion(): VersionData { + // Generate the auth signature + val secretKey = MessagingModuleConfiguration.shared.getUserED25519KeyPair()?.secretKey?.asBytes + ?: throw (Error.NoEd25519KeyPair) + + val blindedKeys = BlindKeyAPI.blindVersionKeyPair(secretKey) + val timestamp = System.currentTimeMillis().milliseconds.inWholeSeconds // The current timestamp in seconds + val signature = BlindKeyAPI.blindVersionSign(secretKey, timestamp) + + // The hex encoded version-blinded public key with a 07 prefix + val blindedPkHex = "07" + blindedKeys.pubKey.toHexString() + + val request = Request( + verb = HTTP.Verb.GET, + endpoint = "session_version", + queryParameters = mapOf("platform" to "android"), + headers = mapOf( + "X-FS-Pubkey" to blindedPkHex, + "X-FS-Timestamp" to timestamp.toString(), + "X-FS-Signature" to Base64.encodeToString(signature, Base64.NO_WRAP) + ) + ) + + // transform the promise into a coroutine + val result = send(request).await() + + // map out the result + return JsonUtil.fromJson(result, Map::class.java).let { + VersionData( + statusCode = it["status_code"] as? Int ?: 0, + version = it["result"] as? String ?: "", + updated = it["updated"] as? Double ?: 0.0 + ) + } + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/VersionData.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/VersionData.kt new file mode 100644 index 0000000000..b7c28020e7 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/VersionData.kt @@ -0,0 +1,7 @@ +package org.session.libsession.messaging.file_server + +data class VersionData( + val statusCode: Int, // The value 200. Included for backwards compatibility, and may be removed someday. + val version: String, // The Session version. + val updated: Double // The unix timestamp when this version was retrieved from Github; this can be up to 24 hours ago in case of consistent fetch errors, though normally will be within the last 30 minutes. +) \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt b/libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt new file mode 100644 index 0000000000..9c55a2282a --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt @@ -0,0 +1,13 @@ +package org.session.libsession.snode.utilities + +import nl.komponents.kovenant.Promise +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +suspend fun Promise.await(): T { + return suspendCoroutine { cont -> + success(cont::resume) + fail(cont::resumeWithException) + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index 12fdd4ceaf..5bf109843d 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import org.session.libsession.R +import org.session.libsession.utilities.TextSecurePreferences.Companion import org.session.libsession.utilities.TextSecurePreferences.Companion.AUTOPLAY_AUDIO_MESSAGES import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOTIFICATIONS_ENABLED import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_DARK @@ -20,6 +21,7 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_ import org.session.libsession.utilities.TextSecurePreferences.Companion.FOLLOW_SYSTEM_SETTINGS import org.session.libsession.utilities.TextSecurePreferences.Companion.HIDE_PASSWORD import org.session.libsession.utilities.TextSecurePreferences.Companion.LAST_VACUUM_TIME +import org.session.libsession.utilities.TextSecurePreferences.Companion.LAST_VERSION_CHECK import org.session.libsession.utilities.TextSecurePreferences.Companion.LEGACY_PREF_KEY_SELECTED_UI_MODE import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_DARK import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_LIGHT @@ -186,6 +188,8 @@ interface TextSecurePreferences { fun clearAll() fun getHidePassword(): Boolean fun setHidePassword(value: Boolean) + fun getLastVersionCheck(): Long + fun setLastVersionCheck() companion object { val TAG = TextSecurePreferences::class.simpleName @@ -272,6 +276,7 @@ interface TextSecurePreferences { const val AUTOPLAY_AUDIO_MESSAGES = "pref_autoplay_audio" const val FINGERPRINT_KEY_GENERATED = "fingerprint_key_generated" const val SELECTED_ACCENT_COLOR = "selected_accent_color" + const val LAST_VERSION_CHECK = "pref_last_version_check" const val HAS_RECEIVED_LEGACY_CONFIG = "has_received_legacy_config" const val HAS_FORCED_NEW_CONFIG = "has_forced_new_config" @@ -1541,6 +1546,14 @@ class AppTextSecurePreferences @Inject constructor( setLongPreference(LAST_VACUUM_TIME, System.currentTimeMillis()) } + override fun getLastVersionCheck(): Long { + return getLongPreference(LAST_VERSION_CHECK, 0) + } + + override fun setLastVersionCheck() { + setLongPreference(LAST_VERSION_CHECK, System.currentTimeMillis()) + } + override fun setShownCallNotification(): Boolean { val previousValue = getBooleanPreference(SHOWN_CALL_NOTIFICATION, false) if (previousValue) return false