diff --git a/app/src/main/java/org/futo/circles/App.kt b/app/src/main/java/org/futo/circles/App.kt index 7c94023e5e469d37d502c4cf81d8bb90c3ec61c5..644f369d1942c39bc4cb5aaf0251e6a5d3a17183 100644 --- a/app/src/main/java/org/futo/circles/App.kt +++ b/app/src/main/java/org/futo/circles/App.kt @@ -90,6 +90,7 @@ class App : Application() { override fun onResume(owner: LifecycleOwner) { fcmHelper.onEnterForeground() MatrixSessionProvider.currentSession?.syncService()?.stopAnyBackgroundSync() + NetworkObserver.updateConnectionState(applicationContext) } override fun onPause(owner: LifecycleOwner) { diff --git a/app/src/main/java/org/futo/circles/feature/timeline/TimelineDialogFragment.kt b/app/src/main/java/org/futo/circles/feature/timeline/TimelineDialogFragment.kt index 67baafdd534cbeb85c59b6c6419431c88fe1fbf8..6964f2aa995f7f759baebdf3f9d5f54cc1d8a7e3 100644 --- a/app/src/main/java/org/futo/circles/feature/timeline/TimelineDialogFragment.kt +++ b/app/src/main/java/org/futo/circles/feature/timeline/TimelineDialogFragment.kt @@ -16,6 +16,7 @@ import com.google.android.material.badge.ExperimentalBadgeUtils import dagger.hilt.android.AndroidEntryPoint import org.futo.circles.R import org.futo.circles.core.base.fragment.BaseFullscreenDialogFragment +import org.futo.circles.core.extensions.dpToPx import org.futo.circles.core.extensions.getCurrentUserPowerLevel import org.futo.circles.core.extensions.isCurrentUserAbleToPost import org.futo.circles.core.extensions.observeData @@ -40,6 +41,7 @@ import org.futo.circles.model.CreatePostContent import org.futo.circles.model.EndPoll import org.futo.circles.model.IgnoreSender import org.futo.circles.model.RemovePost +import org.futo.circles.model.TextPostContent import org.futo.circles.view.CreatePostViewListener import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent @@ -113,7 +115,11 @@ class TimelineDialogFragment : BaseFullscreenDialogFragment(DialogFragmentTimeli private fun setupViews() { binding.rvTimeline.apply { adapter = listAdapter - getRecyclerView().isNestedScrollingEnabled = false + getRecyclerView().apply { + isNestedScrollingEnabled = false + clipToPadding = false + setPadding(paddingLeft, paddingTop, paddingRight, context.dpToPx(70)) + } addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) addPageEndListener { viewModel.loadMore() } } @@ -223,6 +229,7 @@ class TimelineDialogFragment : BaseFullscreenDialogFragment(DialogFragmentTimeli } override fun onReply(roomId: String, eventId: String) { + if (isThread) return navigator.navigateToThread(roomId, eventId) } diff --git a/app/src/main/java/org/futo/circles/view/CreatePostView.kt b/app/src/main/java/org/futo/circles/view/CreatePostView.kt index 648e41d7eb1754472f6e8beee003e1ec640aa74a..c0b071e343e9d8f4e19167700dd17d4040ec0761 100644 --- a/app/src/main/java/org/futo/circles/view/CreatePostView.kt +++ b/app/src/main/java/org/futo/circles/view/CreatePostView.kt @@ -66,6 +66,13 @@ class CreatePostView( if (isUserAbleToPost) if (dy > 0) gone() else if (dy < 0) visible() } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (!recyclerView.canScrollVertically(1) && newState == RecyclerView.SCROLL_STATE_IDLE) { + if (isUserAbleToPost) visible() + } + } }) } diff --git a/auth/build.gradle b/auth/build.gradle index cb98c75473ba0fcb1e9031a7073febae10229f23..1bd1a82c631f20adea4f4fc8766e6850881b720a 100644 --- a/auth/build.gradle +++ b/auth/build.gradle @@ -70,10 +70,10 @@ dependencies { implementation 'com.nulab-inc:zxcvbn:1.9.0' //Subscriptions - gplayImplementation 'com.android.billingclient:billing-ktx:6.2.1' + gplayImplementation 'com.android.billingclient:billing-ktx:7.0.0' //PasswordManager - def credentials_version = '1.3.0-alpha03' + def credentials_version = '1.3.0-alpha04' gplayImplementation "androidx.credentials:credentials:$credentials_version" gplayImplementation "androidx.credentials:credentials-play-services-auth:$credentials_version" diff --git a/auth/src/main/java/org/futo/circles/auth/feature/pass_phrase/create/CreatePassPhraseDataSource.kt b/auth/src/main/java/org/futo/circles/auth/feature/pass_phrase/create/CreatePassPhraseDataSource.kt index 9dc6c754bad907b60df768099265a5be0d9b2405..56baa3c5186d25bbb88480b831078ce0a5748bd1 100644 --- a/auth/src/main/java/org/futo/circles/auth/feature/pass_phrase/create/CreatePassPhraseDataSource.kt +++ b/auth/src/main/java/org/futo/circles/auth/feature/pass_phrase/create/CreatePassPhraseDataSource.kt @@ -31,8 +31,8 @@ class CreatePassPhraseDataSource @Inject constructor( val backupCreationInfo = keysBackupService.prepareKeysBackupVersion(keyBackupPrivateKey, null) createKeyBackup(backupCreationInfo) - val keyData = ssssDataSource.storeBsSpekeKeyIntoSSSS(keyBackupPrivateKey) - crossSigningDataSource.initCrossSigningIfNeed(keyData.keySpec) + val keySpec = ssssDataSource.storeBsSpekeKeyIntoSSSS(keyBackupPrivateKey) + crossSigningDataSource.initCrossSigningIfNeed(keySpec) loadingLiveData.postValue(LoadingData(isLoading = false)) } diff --git a/auth/src/main/java/org/futo/circles/auth/feature/pass_phrase/restore/RestoreBackupDataSource.kt b/auth/src/main/java/org/futo/circles/auth/feature/pass_phrase/restore/RestoreBackupDataSource.kt index fcdd46dd01766e4535130bacbb3a4c3895d69e41..b51db190d3a9808187d309ccc00c0167e57d0b14 100644 --- a/auth/src/main/java/org/futo/circles/auth/feature/pass_phrase/restore/RestoreBackupDataSource.kt +++ b/auth/src/main/java/org/futo/circles/auth/feature/pass_phrase/restore/RestoreBackupDataSource.kt @@ -7,20 +7,22 @@ import androidx.lifecycle.MutableLiveData import dagger.hilt.android.qualifiers.ApplicationContext import org.futo.circles.auth.R import org.futo.circles.auth.feature.cross_signing.CrossSigningDataSource -import org.futo.circles.auth.model.KeyData +import org.futo.circles.auth.model.SecretKeyData import org.futo.circles.core.model.LoadingData +import org.futo.circles.core.provider.KeyStoreProvider import org.futo.circles.core.provider.MatrixSessionProvider import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.listeners.StepProgressListener -import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupRecoveryKey import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult +import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import javax.inject.Inject class RestoreBackupDataSource @Inject constructor( @ApplicationContext private val context: Context, private val ssssDataSource: SSSSDataSource, - private val crossSigningDataSource: CrossSigningDataSource + private val crossSigningDataSource: CrossSigningDataSource, + private val keyStoreProvider: KeyStoreProvider ) { val loadingLiveData = MutableLiveData(LoadingData(isLoading = false)) @@ -75,8 +77,12 @@ class RestoreBackupDataSource @Inject constructor( suspend fun restoreWithBsSpekeKey() { try { - val keyData = ssssDataSource.getBsSpekeRecoveryKey(progressObserver) + val keyData = ssssDataSource.getBsSpekeSecretKeyData(progressObserver) restoreKeysWithRecoveryKey(keyData) + keyStoreProvider.storeBsSpekePrivateKey( + (keyData.keySpec as RawBytesKeySpec).privateKey, + keyData.keyId + ) } catch (e: Throwable) { loadingLiveData.postValue(LoadingData(isLoading = false)) throw e @@ -87,7 +93,7 @@ class RestoreBackupDataSource @Inject constructor( suspend fun restoreKeysWithPassPhase(passphrase: String) { try { val keyData = - ssssDataSource.getRecoveryKeyFromPassphrase(passphrase, progressObserver) + ssssDataSource.getSecretKeyDataFromPassphrase(passphrase, progressObserver) restoreKeysWithRecoveryKey(keyData) } catch (e: Throwable) { loadingLiveData.postValue(LoadingData(isLoading = false)) @@ -96,18 +102,18 @@ class RestoreBackupDataSource @Inject constructor( loadingLiveData.postValue(LoadingData(isLoading = false)) } - private suspend fun restoreKeysWithRecoveryKey(keyData: KeyData) { + private suspend fun restoreKeysWithRecoveryKey(secretKeyData: SecretKeyData) { val keysBackupService = getKeysBackupService() try { val keyVersion = getKeysVersion(keysBackupService) keysBackupService.restoreKeysWithRecoveryKey( keyVersion, - BackupRecoveryKey.fromBase58(keyData.recoveryKey), + secretKeyData.getBackupRecoveryKey(), null, MatrixSessionProvider.currentSession?.myUserId, progressObserver ) - crossSigningDataSource.configureCrossSigning(keyData.keySpec) + crossSigningDataSource.configureCrossSigning(secretKeyData.keySpec) keysBackupService.trustKeysBackupVersion(keyVersion, true) } catch (e: Throwable) { loadingLiveData.postValue(LoadingData(isLoading = false)) @@ -118,7 +124,7 @@ class RestoreBackupDataSource @Inject constructor( suspend fun restoreKeysWithRawKey(rawKey: String) { try { - val keyData = ssssDataSource.getRecoveryKeyFromFileKey(rawKey, progressObserver) + val keyData = ssssDataSource.getSecretKeyDataKeyFromFileKey(rawKey, progressObserver) restoreKeysWithRecoveryKey(keyData) } catch (e: Throwable) { loadingLiveData.postValue(LoadingData(isLoading = false)) @@ -130,7 +136,7 @@ class RestoreBackupDataSource @Inject constructor( suspend fun restoreKeysWithRecoveryKey(uri: Uri) { try { val key = readRecoveryKeyFile(uri) - val keyData = ssssDataSource.getRecoveryKeyFromFileKey(key, progressObserver) + val keyData = ssssDataSource.getSecretKeyDataKeyFromFileKey(key, progressObserver) restoreKeysWithRecoveryKey(keyData) } catch (e: Throwable) { loadingLiveData.postValue(LoadingData(isLoading = false)) diff --git a/auth/src/main/java/org/futo/circles/auth/feature/pass_phrase/restore/SSSSDataSource.kt b/auth/src/main/java/org/futo/circles/auth/feature/pass_phrase/restore/SSSSDataSource.kt index 142ffd9c2589611e7785cd3fca601bc81297cb3a..3b02df39af3182bfa78853d5536c248705ba3684 100644 --- a/auth/src/main/java/org/futo/circles/auth/feature/pass_phrase/restore/SSSSDataSource.kt +++ b/auth/src/main/java/org/futo/circles/auth/feature/pass_phrase/restore/SSSSDataSource.kt @@ -1,14 +1,13 @@ package org.futo.circles.auth.feature.pass_phrase.restore import org.futo.circles.auth.bsspeke.BSSpekeClientProvider -import org.futo.circles.auth.model.KeyData +import org.futo.circles.auth.model.SecretKeyData +import org.futo.circles.core.provider.KeyStoreProvider import org.futo.circles.core.provider.MatrixSessionProvider import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.listeners.StepProgressListener import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupRecoveryKey -import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner import org.matrix.android.sdk.api.session.securestorage.KeyInfo @@ -16,13 +15,15 @@ import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult import org.matrix.android.sdk.api.session.securestorage.KeyRef import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo -import org.matrix.android.sdk.api.util.fromBase64 +import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec import org.matrix.android.sdk.api.util.toBase64NoPadding import javax.inject.Inject -class SSSSDataSource @Inject constructor() { +class SSSSDataSource @Inject constructor( + private val keyStoreProvider: KeyStoreProvider +) { - suspend fun storeBsSpekeKeyIntoSSSS(keyBackupPrivateKey: ByteArray): KeyData { + suspend fun storeBsSpekeKeyIntoSSSS(keyBackupPrivateKey: ByteArray): SsssKeySpec { val session = MatrixSessionProvider.getSessionOrThrow() val bsSpekeClient = BSSpekeClientProvider.getClientOrThrow() val keyId = bsSpekeClient.generateKeyId() @@ -30,10 +31,10 @@ class SSSSDataSource @Inject constructor() { val keyInfo = session.sharedSecretStorageService() .generateBsSpekeKeyInfo(keyId, key, EmptyKeySigner()) storeSecret(session, keyBackupPrivateKey, keyInfo) - return KeyData(keyInfo.recoveryKey, keyInfo.keySpec) + return keyInfo.keySpec } - suspend fun getBsSpekeRecoveryKey(progressObserver: StepProgressListener): KeyData { + suspend fun getBsSpekeSecretKeyData(progressObserver: StepProgressListener): SecretKeyData { progressObserver.onStepProgress( StepProgressListener.Step.ComputingKey(0, 0) ) @@ -41,10 +42,11 @@ class SSSSDataSource @Inject constructor() { val keySpec = RawBytesKeySpec( BSSpekeClientProvider.getClientOrThrow().generateHashKey() ) + val secret = getSecret(keyInfo, keySpec) ?: throw Exception("Backup could not be decrypted with this passphrase") - return KeyData(computeRecoveryKey(secret.fromBase64()), keySpec) + return SecretKeyData(keyInfo.id, secret, keySpec) } suspend fun replaceBsSpeke4SKey() { @@ -57,10 +59,10 @@ class SSSSDataSource @Inject constructor() { storeBsSpekeKeyIntoSSSS(secret) } - suspend fun getRecoveryKeyFromPassphrase( + suspend fun getSecretKeyDataFromPassphrase( passphrase: String, progressObserver: StepProgressListener - ): KeyData { + ): SecretKeyData { val keyInfo = getKeyInfo() progressObserver.onStepProgress( @@ -82,13 +84,13 @@ class SSSSDataSource @Inject constructor() { val secret = getSecret(keyInfo, keySpec) ?: throw Exception("Backup could not be decrypted with this passphrase") - return KeyData(computeRecoveryKey(secret.fromBase64()), keySpec) + return SecretKeyData(keyInfo.id, secret, keySpec) } - suspend fun getRecoveryKeyFromFileKey( + suspend fun getSecretKeyDataKeyFromFileKey( recoveryKey: String, progressObserver: StepProgressListener - ): KeyData { + ): SecretKeyData { val keyInfo = getKeyInfo() progressObserver.onStepProgress( @@ -100,7 +102,7 @@ class SSSSDataSource @Inject constructor() { val secret = getSecret(keyInfo, keySpec) ?: throw Exception("Backup could not be decrypted with this recovery key") - return KeyData(computeRecoveryKey(secret.fromBase64()), keySpec) + return SecretKeyData(keyInfo.id, secret, keySpec) } private suspend fun storeSecret( @@ -115,6 +117,11 @@ class SSSSDataSource @Inject constructor() { ) session.cryptoService().keysBackupService() .onSecretKeyGossip(keyBackupPrivateKey.toBase64NoPadding()) + + keyStoreProvider.storeBsSpekePrivateKey( + (keyInfo.keySpec as RawBytesKeySpec).privateKey, + keyInfo.keyId + ) session.sharedSecretStorageService().setDefaultKey(keyInfo.keyId) } diff --git a/auth/src/main/java/org/futo/circles/auth/model/KeyData.kt b/auth/src/main/java/org/futo/circles/auth/model/KeyData.kt deleted file mode 100644 index a565dd9d4e711904bf12cac02669656b81034699..0000000000000000000000000000000000000000 --- a/auth/src/main/java/org/futo/circles/auth/model/KeyData.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.futo.circles.auth.model - -import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec - -data class KeyData( - val recoveryKey: String, - val keySpec: SsssKeySpec -) \ No newline at end of file diff --git a/auth/src/main/java/org/futo/circles/auth/model/SecretKeyData.kt b/auth/src/main/java/org/futo/circles/auth/model/SecretKeyData.kt new file mode 100644 index 0000000000000000000000000000000000000000..c1bf051cb10dbe59de05cbf0b3439717cc235c8b --- /dev/null +++ b/auth/src/main/java/org/futo/circles/auth/model/SecretKeyData.kt @@ -0,0 +1,18 @@ +package org.futo.circles.auth.model + +import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupRecoveryKey +import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey +import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec +import org.matrix.android.sdk.api.util.fromBase64 + +data class SecretKeyData( + val keyId: String, + val secretBase64: String, + val keySpec: SsssKeySpec +) { + fun getBackupRecoveryKey(): BackupRecoveryKey { + val recoveryBase58 = computeRecoveryKey(secretBase64.fromBase64()) + return BackupRecoveryKey.fromBase58(recoveryBase58) + } + +} \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index a4ba3eacb2b2283927ee7aaf93acaac58e3b54b0..5f4cb10d735bf1110a725f3b46a82882c44227a5 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -55,7 +55,7 @@ android { dependencies { api 'androidx.appcompat:appcompat:1.6.1' - api 'com.google.android.material:material:1.11.0' + api 'com.google.android.material:material:1.12.0' api 'androidx.recyclerview:recyclerview:1.3.2' api "androidx.autofill:autofill:1.1.0" @@ -63,7 +63,7 @@ dependencies { api 'androidx.core:core-ktx:1.13.1' //androidx lifecycle - def lifecycle_version = "2.7.0" + def lifecycle_version = "2.8.0" api "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" api "androidx.lifecycle:lifecycle-process:$lifecycle_version" @@ -80,12 +80,12 @@ dependencies { kapt "com.google.dagger:hilt-compiler:$rootProject.ext.hilt_version" //Matrix release - api('org.futo.gitlab.circles:matrix-android-sdk:v1.6.10.35@aar') { + api('org.futo.gitlab.circles:matrix-android-sdk:v1.6.10.38@aar') { transitive = true } //Matrix mavenLocal testing - //api "org.futo.gitlab.circles:matrix-android-sdk:0.1" + //api "org.futo.gitlab.circles:matrix-android-sdk:0.1.57" //Retrofit2 def retrofit_version = '2.11.0' @@ -117,7 +117,7 @@ dependencies { api "io.noties.markwon:linkify:$markwon_version" api "io.noties.markwon:ext-strikethrough:$markwon_version" api "io.noties.markwon:ext-tasklist:$markwon_version" - api 'io.element.android:wysiwyg:2.37.0' + api 'io.element.android:wysiwyg:2.37.2' //ExoPlayer def exoplayer_version = '1.3.1' @@ -138,9 +138,10 @@ dependencies { implementation 'androidx.browser:browser:1.8.0' //Firebase - gplayImplementation "com.google.firebase:firebase-crashlytics-ktx:18.6.4" - gplayImplementation "com.google.firebase:firebase-analytics-ktx:21.6.2" - gplayImplementation "com.google.firebase:firebase-messaging-ktx:23.4.1" + gplayImplementation "com.google.firebase:firebase-crashlytics-ktx:19.0.0" + gplayImplementation "com.google.firebase:firebase-analytics-ktx:22.0.0" + gplayImplementation "com.google.firebase:firebase-messaging-ktx:24.0.0" + gplayImplementation 'com.google.android.gms:play-services-base:18.4.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml index 3de53f08d1a3637f60e5f0f887e94cc3d855ba8a..9af6326419d89b2b53a9a7e44760504d81ed99ec 100644 --- a/core/src/main/AndroidManifest.xml +++ b/core/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.VIBRATE" /> </manifest> \ No newline at end of file diff --git a/core/src/main/java/org/futo/circles/core/base/NetworkObserver.kt b/core/src/main/java/org/futo/circles/core/base/NetworkObserver.kt index 175028eafd3cce4ee9b751aa760f079bf4c3ae95..e0d6dde162e6bbfd6476071d80cdfb774a1f502c 100644 --- a/core/src/main/java/org/futo/circles/core/base/NetworkObserver.kt +++ b/core/src/main/java/org/futo/circles/core/base/NetworkObserver.kt @@ -21,10 +21,14 @@ object NetworkObserver { fun isConnected() = internetConnectionFlow.value fun register(context: Context) { - internetConnectionFlow.value = isConnectedToInternet(context) + updateConnectionState(context) setInternetConnectionObserver(context) { internetConnectionFlow.value = it } } + fun updateConnectionState(context: Context) { + internetConnectionFlow.value = isConnectedToInternet(context) + } + fun observe(lifecycleOwner: LifecycleOwner, onConnectionChanged: (Boolean) -> Unit) { lifecycleOwner.lifecycleScope.launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { diff --git a/core/src/main/java/org/futo/circles/core/feature/autocomplete/AutocompletePopup.kt b/core/src/main/java/org/futo/circles/core/feature/autocomplete/AutocompletePopup.kt index cd239fb63d1b3e21d6dbb7b6a53d4c39ae1d25e1..a5b181ac69a42213900d1d9dd40c6283892e2e25 100644 --- a/core/src/main/java/org/futo/circles/core/feature/autocomplete/AutocompletePopup.kt +++ b/core/src/main/java/org/futo/circles/core/feature/autocomplete/AutocompletePopup.kt @@ -3,14 +3,11 @@ package org.futo.circles.core.feature.autocomplete import android.content.Context import android.graphics.Rect import android.graphics.drawable.Drawable -import android.os.Build import android.view.Gravity import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.widget.PopupWindow -import androidx.annotation.StyleRes -import androidx.core.view.ViewCompat import androidx.core.widget.PopupWindowCompat import kotlin.math.min @@ -110,7 +107,7 @@ internal class AutocompletePopup(private val mContext: Context) { } fun setElevation(elevationPx: Float) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) mPopup.elevation = elevationPx + mPopup.elevation = elevationPx } var isOutsideTouchable: Boolean @@ -155,24 +152,6 @@ internal class AutocompletePopup(private val mContext: Context) { mPopup.setBackgroundDrawable(d) } - @get:StyleRes - @get:Suppress("unused") - @set:Suppress("unused") - var animationStyle: Int - /** - * Returns the animation style that will be used when the popup window is - * shown or dismissed. - * @return Animation style that will be used. - */ - get() = mPopup.animationStyle - /** - * Set an animation style to use when the popup window is shown or dismissed. - * @param animationStyle Animation style to use. - */ - set(animationStyle) { - mPopup.animationStyle = animationStyle - } - /** * Sets the popup's anchor view. This popup will always be positioned relative to * the anchor view when shown. @@ -262,7 +241,7 @@ internal class AutocompletePopup(private val mContext: Context) { * will recalculate the popup's size and position. */ fun show() { - if (!ViewCompat.isAttachedToWindow(anchorView!!)) return + if (anchorView!!.isAttachedToWindow.not()) return val height = buildDropDown() val noInputMethod = isInputMethodNotNeeded val mDropDownWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL @@ -285,8 +264,7 @@ internal class AutocompletePopup(private val mContext: Context) { // for any value you do not want to update. // Width. - var widthSpec: Int - widthSpec = if (width == ViewGroup.LayoutParams.MATCH_PARENT) { + var widthSpec: Int = if (width == ViewGroup.LayoutParams.MATCH_PARENT) { -1 } else if (width == ViewGroup.LayoutParams.WRAP_CONTENT) { anchorView!!.width @@ -297,8 +275,7 @@ internal class AutocompletePopup(private val mContext: Context) { widthSpec = if (widthSpec < 0) -1 else widthSpec // Height. - var heightSpec: Int - heightSpec = if (this.height == ViewGroup.LayoutParams.MATCH_PARENT) { + var heightSpec: Int = if (this.height == ViewGroup.LayoutParams.MATCH_PARENT) { if (noInputMethod) height else ViewGroup.LayoutParams.MATCH_PARENT } else if (this.height == ViewGroup.LayoutParams.WRAP_CONTENT) { height @@ -316,8 +293,7 @@ internal class AutocompletePopup(private val mContext: Context) { mPopup.update(anchorView, mHorizontalOffset, mVerticalOffset, widthSpec, heightSpec) } } else { - var widthSpec: Int - widthSpec = if (width == ViewGroup.LayoutParams.MATCH_PARENT) { + var widthSpec: Int = if (width == ViewGroup.LayoutParams.MATCH_PARENT) { ViewGroup.LayoutParams.MATCH_PARENT } else if (width == ViewGroup.LayoutParams.WRAP_CONTENT) { anchorView!!.width @@ -325,8 +301,7 @@ internal class AutocompletePopup(private val mContext: Context) { width } widthSpec = Math.min(widthSpec, mMaxWidth) - var heightSpec: Int - heightSpec = if (this.height == ViewGroup.LayoutParams.MATCH_PARENT) { + var heightSpec: Int = if (this.height == ViewGroup.LayoutParams.MATCH_PARENT) { ViewGroup.LayoutParams.MATCH_PARENT } else if (this.height == ViewGroup.LayoutParams.WRAP_CONTENT) { height @@ -380,7 +355,7 @@ internal class AutocompletePopup(private val mContext: Context) { * @return `true` if the popup is currently showing, `false` otherwise. */ get() = mPopup.isShowing - val isInputMethodNotNeeded: Boolean + private val isInputMethodNotNeeded: Boolean /** * @return `true` if this popup is configured to assume the user does not need * to interact with the IME while it is showing, `false` otherwise. @@ -433,12 +408,9 @@ internal class AutocompletePopup(private val mContext: Context) { // Redefine dimensions taking into account maxWidth and maxHeight. val ignoreBottomDecorations = mPopup.inputMethodMode == PopupWindow.INPUT_METHOD_NOT_NEEDED - val maxContentHeight = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) mPopup.getMaxAvailableHeight( - anchorView!!, mVerticalOffset, ignoreBottomDecorations - ) else mPopup.getMaxAvailableHeight( - anchorView!!, mVerticalOffset - ) + val maxContentHeight = mPopup.getMaxAvailableHeight( + anchorView!!, mVerticalOffset, ignoreBottomDecorations + ) val maxContentWidth = mContext.resources.displayMetrics.widthPixels - paddingHoriz mMaxHeight = min(maxContentHeight + paddingVert, mUserMaxHeight) mMaxWidth = min(maxContentWidth + paddingHoriz, mUserMaxWidth) @@ -471,6 +443,6 @@ internal class AutocompletePopup(private val mContext: Context) { if (viewHeight > 0) { otherHeights += paddingVert + mView!!.paddingTop + mView!!.paddingBottom } - return Math.min(viewHeight + otherHeights, mMaxHeight) + return min(viewHeight + otherHeights, mMaxHeight) } } \ No newline at end of file diff --git a/core/src/main/java/org/futo/circles/core/provider/KeyStoreProvider.kt b/core/src/main/java/org/futo/circles/core/provider/KeyStoreProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..d292d9e640fd6a5d7037bad02f992dc9f2fcf0c3 --- /dev/null +++ b/core/src/main/java/org/futo/circles/core/provider/KeyStoreProvider.kt @@ -0,0 +1,81 @@ +package org.futo.circles.core.provider + +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import org.matrix.android.sdk.api.extensions.tryOrNull +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import javax.inject.Inject + + +class KeyStoreProvider @Inject constructor( + private val preferencesProvider: PreferencesProvider +) { + private val keyStore + get() = KeyStore.getInstance(ANDROID_KEYS_TORE).apply { load(null) } + + fun storeBsSpekePrivateKey(keyBytes: ByteArray, keyId: String) { + tryOrNull { + val alias = "$ORG_FUTO_SSSS_KEY_PREFIX.$keyId" + val masterKey = getOrGenerateMasterKeyIfNotExist(alias) + ?: throw IllegalArgumentException("Failed to get master key") + val encrypted = encryptData(keyBytes, masterKey) + preferencesProvider.storeBSspekeEncryptedPrivateKeyBase64(alias, encrypted) + } + } + + fun getBsSpekePrivateKey(keyId: String): ByteArray? = tryOrNull { + val alias = "$ORG_FUTO_SSSS_KEY_PREFIX.$keyId" + val masterKey = getOrGenerateMasterKeyIfNotExist(alias) + ?: throw IllegalArgumentException("Failed to get master key") + val encrypted = preferencesProvider.getBSspekeEncryptedPrivateKeyBase64(alias) + ?: throw IllegalArgumentException("Not saved in preferences $alias") + decryptData(encrypted, masterKey) + } + + private fun getOrGenerateMasterKeyIfNotExist(alias: String): SecretKey? { + if (!keyStore.containsAlias(alias)) { + KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYS_TORE).apply { + init( + KeyGenParameterSpec.Builder( + alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setKeySize(KEY_SIZE) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build() + ) + generateKey() + } + } + return keyStore.getKey(alias, null) as? SecretKey + } + + private fun encryptData(data: ByteArray, masterKey: SecretKey): String { + val cipher = Cipher.getInstance(AES_NO_PADDING) + cipher.init(Cipher.ENCRYPT_MODE, masterKey) + val ivString = Base64.encodeToString(cipher.iv, Base64.DEFAULT) + val encryptedBytes: ByteArray = cipher.doFinal(data) + return ivString + IV_SEPARATOR + Base64.encodeToString(encryptedBytes, Base64.DEFAULT) + } + + private fun decryptData(encryptedData: String, masterKey: SecretKey): ByteArray { + val (iv64, encrypted64) = encryptedData.split(IV_SEPARATOR) + val cipher = Cipher.getInstance(AES_NO_PADDING) + val spec = GCMParameterSpec(KEY_SIZE, Base64.decode(iv64, Base64.DEFAULT)) + cipher.init(Cipher.DECRYPT_MODE, masterKey, spec) + return cipher.doFinal(Base64.decode(encrypted64, Base64.DEFAULT)) + } + + companion object { + private const val AES_NO_PADDING = "AES/GCM/NoPadding" + private const val IV_SEPARATOR = "]" + private const val KEY_SIZE = 128 + private const val ANDROID_KEYS_TORE = "AndroidKeyStore" + private const val ORG_FUTO_SSSS_KEY_PREFIX = "org.futo.ssss.key" + } +} diff --git a/core/src/main/java/org/futo/circles/core/provider/PreferencesProvider.kt b/core/src/main/java/org/futo/circles/core/provider/PreferencesProvider.kt index 0be15e3a30d79d65e19254235a2eb961e332b530..5ba08a31f400e2f5de1e7ad76f3fecd0ef39e9f5 100644 --- a/core/src/main/java/org/futo/circles/core/provider/PreferencesProvider.kt +++ b/core/src/main/java/org/futo/circles/core/provider/PreferencesProvider.kt @@ -95,6 +95,15 @@ class PreferencesProvider @Inject constructor( getSharedPreferences().edit(true) { putStringSet(NOT_RESTORED_SESSION, set) } } + fun getBSspekeEncryptedPrivateKeyBase64(alias: String): String? { + return getSharedPreferences().getString(alias, null) + } + + fun storeBSspekeEncryptedPrivateKeyBase64(alias: String, key: String) { + getSharedPreferences().edit { putString(alias, key) } + } + + companion object { private const val PREFERENCES_NAME = "circles_preferences" private const val DEV_MODE_KEY = "developer_mode" diff --git a/gallery/build.gradle b/gallery/build.gradle index 445bdc29aa4fda3e756c8c2d7537e92479efeabf..2f1f082c0bd6488183cdbe5900b9ebdcc33392bb 100644 --- a/gallery/build.gradle +++ b/gallery/build.gradle @@ -14,8 +14,6 @@ android { minSdk rootProject.ext.min_sdk_version targetSdk rootProject.ext.sdk_version - missingDimensionStrategy "store", "gplay", "fdroid" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" } @@ -30,6 +28,16 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + def flavor_dimension_name = "store" + flavorDimensions.add(flavor_dimension_name) + productFlavors { + gplay { + dimension flavor_dimension_name + } + fdroid { + dimension flavor_dimension_name + } + } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 @@ -63,25 +71,36 @@ kapt { correctErrorTypes = true } - afterEvaluate { - publishing.publications.create("release", MavenPublication) { - groupId rootProject.ext.modules_groupId - artifactId "gallery" - version rootProject.ext.modules_version + android.libraryVariants.each { variant -> + if (variant.buildType.name != "release") { + return + } + publishing.publications.create(variant.name, MavenPublication) { + groupId rootProject.ext.modules_groupId + artifactId "gallery_${variant.flavorName}" + version rootProject.ext.modules_version - pom.withXml { - def dependenciesNode = asNode().appendNode('dependencies') - ext.addDependency = { Dependency dep, String scope -> - if (dep.group == null || dep.name == null || dep.name == "unspecified" || dep.version == "unspecified") return - final dependencyNode = dependenciesNode.appendNode('dependency') - dependencyNode.appendNode('groupId', dep.group) - dependencyNode.appendNode('artifactId', dep.name) - dependencyNode.appendNode('version', dep.version) - dependencyNode.appendNode('scope', scope) + pom.withXml { + def dependenciesNode = asNode().appendNode('dependencies') + ext.addDependency = { Dependency dep, String scope -> + if (dep.group == null || dep.name == null || dep.name == "unspecified" || dep.version == "unspecified") return + final dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', dep.group) + dependencyNode.appendNode('artifactId', dep.name) + dependencyNode.appendNode('version', dep.version) + dependencyNode.appendNode('scope', scope) + } + configurations.api.getDependencies().each { dep -> addDependency(dep, "compile") } + configurations.implementation.getDependencies().each { dep -> addDependency(dep, "runtime") } + if (variant.flavorName == "gplay") { + configurations.gplayApi.getDependencies().each { dep -> addDependency(dep, "compile") } + configurations.gplayImplementation.getDependencies().each { dep -> addDependency(dep, "runtime") } + } else if (variant.flavorName == "fdroid") { + configurations.fdroidApi.getDependencies().each { dep -> addDependency(dep, "compile") } + configurations.fdroidImplementation.getDependencies().each { dep -> addDependency(dep, "runtime") } + } } - configurations.api.getDependencies().each { dep -> addDependency(dep, "compile") } - configurations.implementation.getDependencies().each { dep -> addDependency(dep, "runtime") } } } } \ No newline at end of file diff --git a/jitpack.yml b/jitpack.yml index 4f0cb798b8152401a753aff2aa502d542416fafc..39f456d05dfd577f223c6d3d4f85428326a56eee 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,15 +1,26 @@ jdk: - openjdk11 install: + - GROUP_ID="-DgroupId=org.futo.gitlab.circles" + - VERSION="-Dversion=1.0.12" + - PACKAGING="-Dpackaging=aar" + - FILE_AUTH_GPLAY="-Dfile=auth-gplay-release.aar" - - mvn install:install-file $FILE_AUTH_GPLAY -DgroupId=org.futo.gitlab.circles -DartifactId=auth_gplay -Dversion=1.0.12 -Dpackaging=aar -DpomFile=pom_auth_gplay.xml + - mvn install:install-file $FILE_AUTH_GPLAY $GROUP_ID -DartifactId=auth_gplay $VERSION $PACKAGING -DpomFile=pom_auth_gplay.xml - FILE_AUTH_FDROID="-Dfile=auth-fdroid-release.aar" - - mvn install:install-file $FILE_AUTH_FDROID -DgroupId=org.futo.gitlab.circles -DartifactId=auth_fdroid -Dversion=1.0.12 -Dpackaging=aar -DpomFile=pom_auth_fdroid.xml + - mvn install:install-file $FILE_AUTH_FDROID $GROUP_ID -DartifactId=auth_fdroid $VERSION $PACKAGING -DpomFile=pom_auth_fdroid.xml + - FILE_CORE_GPLAY="-Dfile=core-gplay-release.aar" - - mvn install:install-file $FILE_CORE_GPLAY -DgroupId=org.futo.gitlab.circles -DartifactId=core_gplay -Dversion=1.0.12 -Dpackaging=aar -DpomFile=pom_core_gplay.xml + - mvn install:install-file $FILE_CORE_GPLAY $GROUP_ID -DartifactId=core_gplay $VERSION $PACKAGING -DpomFile=pom_core_gplay.xml - FILE_CORE_FDROID="-Dfile=core-fdroid-release.aar" - - mvn install:install-file $FILE_CORE_FDROID -DgroupId=org.futo.gitlab.circles -DartifactId=core_fdroid -Dversion=1.0.12 -Dpackaging=aar -DpomFile=pom_core_fdroid.xml - - FILE_GALLERY="-Dfile=gallery-release.aar" - - mvn install:install-file $FILE_GALLERY -DgroupId=org.futo.gitlab.circles -DartifactId=gallery -Dversion=1.0.12 -Dpackaging=aar -DpomFile=pom_gallery.xml - - FILE_SETTINGS="-Dfile=settings-release.aar" - - mvn install:install-file $FILE_SETTINGS -DgroupId=org.futo.gitlab.circles -DartifactId=settings -Dversion=1.0.12 -Dpackaging=aar -DpomFile=pom_settings.xml + - mvn install:install-file $FILE_CORE_FDROID $GROUP_ID -DartifactId=core_fdroid $VERSION $PACKAGING -DpomFile=pom_core_fdroid.xml + + - FILE_GALLERY_GPLAY="-Dfile=gallery-gplay-release.aar" + - mvn install:install-file $FILE_GALLERY_GPLAY $GROUP_ID -DartifactId=gallery_gplay $VERSION $PACKAGING -DpomFile=pom_gallery_gplay.xml + - FILE_GALLERY_FDROID="-Dfile=gallery-fdroid-release.aar" + - mvn install:install-file $FILE_GALLERY_FDROID $GROUP_ID -DartifactId=gallery_fdroid $VERSION $PACKAGING -DpomFile=pom_gallery_fdroid.xml + + - FILE_SETTINGS_GPLAY="-Dfile=settings-gplay-release.aar" + - mvn install:install-file $FILE_SETTINGS_GPLAY $GROUP_ID -DartifactId=settings_gplay $VERSION $PACKAGING -DpomFile=pom_settings_gplay.xml + - FILE_SETTINGS_FDROID="-Dfile=settings-fdroid-release.aar" + - mvn install:install-file $FILE_SETTINGS_FDROID $GROUP_ID -DartifactId=settings_fdroid $VERSION $PACKAGING -DpomFile=pom_settings_fdroid.xml diff --git a/modules_release.sh b/modules_release.sh index 39cb7c9ec2df6b49074c3261a76c1f252462f5c1..46e77f5404d04cd6c40c074ce7799915d188c987 100644 --- a/modules_release.sh +++ b/modules_release.sh @@ -24,24 +24,39 @@ awk -v version="$version_name" '/-Dversion=/ {gsub(/-Dversion=[^ ]+/, "-Dversion # Generate POM file for release publication ./gradlew :core:generatePomFileForGplayReleasePublication ./gradlew :core:generatePomFileForFdroidReleasePublication -./gradlew :gallery:generatePomFileForReleasePublication + +./gradlew :gallery:generatePomFileForGplayReleasePublication +./gradlew :gallery:generatePomFileForFdroidReleasePublication + ./gradlew :auth:generatePomFileForGplayReleasePublication ./gradlew :auth:generatePomFileForFdroidReleasePublication -./gradlew :settings:generatePomFileForReleasePublication + +./gradlew :settings:generatePomFileForGplayReleasePublication +./gradlew :settings:generatePomFileForFdroidReleasePublication # Move .aar to the root directory mv core/build/outputs/aar/core-fdroid-release.aar core-fdroid-release.aar mv core/build/outputs/aar/core-gplay-release.aar core-gplay-release.aar -mv gallery/build/outputs/aar/gallery-release.aar gallery-release.aar + +mv gallery/build/outputs/aar/gallery-fdroid-release.aar gallery-fdroid-release.aar +mv gallery/build/outputs/aar/gallery-gplay-release.aar gallery-gplay-release.aar + mv auth/build/outputs/aar/auth-fdroid-release.aar auth-fdroid-release.aar mv auth/build/outputs/aar/auth-gplay-release.aar auth-gplay-release.aar -mv settings/build/outputs/aar/settings-release.aar settings-release.aar + +mv settings/build/outputs/aar/settings-fdroid-release.aar settings-fdroid-release.aar +mv settings/build/outputs/aar/settings-gplay-release.aar settings-gplay-release.aar # Move poms to the root directory and rename mv core/build/publications/gplayRelease/pom-default.xml pom_core_gplay.xml mv core/build/publications/fdroidRelease/pom-default.xml pom_core_fdroid.xml -mv gallery/build/publications/release/pom-default.xml pom_gallery.xml + +mv gallery/build/publications/gplayRelease/pom-default.xml pom_gallery_gplay.xml +mv gallery/build/publications/fdroidRelease/pom-default.xml pom_gallery_fdroid.xml + mv auth/build/publications/gplayRelease/pom-default.xml pom_auth_gplay.xml mv auth/build/publications/fdroidRelease/pom-default.xml pom_auth_fdroid.xml -mv settings/build/publications/release/pom-default.xml pom_settings.xml \ No newline at end of file + +mv settings/build/publications/gplayRelease/pom-default.xml pom_settings_gplay.xml +mv settings/build/publications/fdroidRelease/pom-default.xml pom_settings_fdroid.xml \ No newline at end of file diff --git a/modules_release_clean.sh b/modules_release_clean.sh index 76e3c949fe71141677ffb6c9a7368bba130677a0..bf9ddd201214c10943f254ee75085f3dbacc27b4 100755 --- a/modules_release_clean.sh +++ b/modules_release_clean.sh @@ -2,14 +2,24 @@ # Remove .aar and pom.xml from the root directory if they exist rm -f auth-gplay-release.aar rm -f auth-fdroid-release.aar + rm -f core-gplay-release.aar rm -f core-fdroid-release.aar -rm -f gallery-release.aar -rm -f settings-release.aar + +rm -f gallery-gplay-release.aar +rm -f gallery-fdroid-release.aar + +rm -f settings-gplay-release.aar +rm -f settings-fdroid-release.aar rm -f pom_auth_gplay.xml rm -f pom_auth_fdroid.xml + rm -f pom_core_gplay.xml rm -f pom_core_fdroid.xml -rm -f pom_gallery.xml -rm -f pom_settings.xml \ No newline at end of file + +rm -f pom_gallery_gplay.xml +rm -f pom_gallery_fdroid.xml + +rm -f pom_settings_gplay.xml +rm -f pom_settings_fdroid.xml \ No newline at end of file diff --git a/settings/build.gradle b/settings/build.gradle index 7aa6fc1865f065f645bdf6b02d2636780c0bfefd..5c1a826dcf14bd24399add263f0444bd23c4f087 100644 --- a/settings/build.gradle +++ b/settings/build.gradle @@ -14,8 +14,6 @@ android { minSdk rootProject.ext.min_sdk_version targetSdk rootProject.ext.sdk_version - missingDimensionStrategy "store", "gplay", "fdroid" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" } @@ -30,6 +28,16 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + def flavor_dimension_name = "store" + flavorDimensions.add(flavor_dimension_name) + productFlavors { + gplay { + dimension flavor_dimension_name + } + fdroid { + dimension flavor_dimension_name + } + } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 @@ -64,25 +72,36 @@ kapt { correctErrorTypes = true } - afterEvaluate { - publishing.publications.create("release", MavenPublication) { - groupId rootProject.ext.modules_groupId - artifactId "settings" - version rootProject.ext.modules_version + android.libraryVariants.each { variant -> + if (variant.buildType.name != "release") { + return + } + publishing.publications.create(variant.name, MavenPublication) { + groupId rootProject.ext.modules_groupId + artifactId "settings_${variant.flavorName}" + version rootProject.ext.modules_version - pom.withXml { - def dependenciesNode = asNode().appendNode('dependencies') - ext.addDependency = { Dependency dep, String scope -> - if (dep.group == null || dep.name == null || dep.name == "unspecified" || dep.version == "unspecified") return - final dependencyNode = dependenciesNode.appendNode('dependency') - dependencyNode.appendNode('groupId', dep.group) - dependencyNode.appendNode('artifactId', dep.name) - dependencyNode.appendNode('version', dep.version) - dependencyNode.appendNode('scope', scope) + pom.withXml { + def dependenciesNode = asNode().appendNode('dependencies') + ext.addDependency = { Dependency dep, String scope -> + if (dep.group == null || dep.name == null || dep.name == "unspecified" || dep.version == "unspecified") return + final dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', dep.group) + dependencyNode.appendNode('artifactId', dep.name) + dependencyNode.appendNode('version', dep.version) + dependencyNode.appendNode('scope', scope) + } + configurations.api.getDependencies().each { dep -> addDependency(dep, "compile") } + configurations.implementation.getDependencies().each { dep -> addDependency(dep, "runtime") } + if (variant.flavorName == "gplay") { + configurations.gplayApi.getDependencies().each { dep -> addDependency(dep, "compile") } + configurations.gplayImplementation.getDependencies().each { dep -> addDependency(dep, "runtime") } + } else if (variant.flavorName == "fdroid") { + configurations.fdroidApi.getDependencies().each { dep -> addDependency(dep, "compile") } + configurations.fdroidImplementation.getDependencies().each { dep -> addDependency(dep, "runtime") } + } } - configurations.api.getDependencies().each { dep -> addDependency(dep, "compile") } - configurations.implementation.getDependencies().each { dep -> addDependency(dep, "runtime") } } } } \ No newline at end of file diff --git a/settings/src/main/java/org/futo/circles/settings/feature/active_sessions/ActiveSessionsDataSource.kt b/settings/src/main/java/org/futo/circles/settings/feature/active_sessions/ActiveSessionsDataSource.kt index 200e7957bacd98a395381d66b13eb088d16d646f..f3f8a057aeac6c75cc27d1bc7b0be625a30e80cc 100644 --- a/settings/src/main/java/org/futo/circles/settings/feature/active_sessions/ActiveSessionsDataSource.kt +++ b/settings/src/main/java/org/futo/circles/settings/feature/active_sessions/ActiveSessionsDataSource.kt @@ -19,8 +19,6 @@ import org.futo.circles.settings.model.ActiveSession import org.futo.circles.settings.model.ActiveSessionListItem import org.futo.circles.settings.model.SessionHeader import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import java.util.concurrent.TimeUnit import javax.inject.Inject class ActiveSessionsDataSource @Inject constructor( @@ -36,56 +34,54 @@ class ActiveSessionsDataSource @Inject constructor( MutableStateFlow(mutableSetOf()) + suspend fun refreshDevicesList() { + session.cryptoService().downloadKeysIfNeeded(listOf(session.myUserId), true) + } + fun getActiveSessionsFlow(): Flow<List<ActiveSessionListItem>> { return combine( - session.cryptoService().getMyDevicesInfoLive().asFlow(), session.cryptoService().getLiveCryptoDeviceInfo(session.myUserId).asFlow(), itemsWithVisibleOptionsFlow - ) { infoList, cryptoList, devicesWithVisibleOptions -> - buildList(infoList, cryptoList, devicesWithVisibleOptions) + ) { cryptoList, devicesWithVisibleOptions -> + buildList(cryptoList, devicesWithVisibleOptions) }.flowOn(Dispatchers.IO).distinctUntilChanged() } private fun buildList( - infoList: List<DeviceInfo>, cryptoList: List<CryptoDeviceInfo>, sessionsWithVisibleOptions: Set<String> ): List<ActiveSessionListItem> { - val devicesList = infoList.mapNotNull { deviceInfo -> - val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId } - cryptoDeviceInfo?.let { deviceInfo to it } - }.sortedByDescending { it.first.lastSeenTs } - - val currentSession = - devicesList.firstOrNull { it.second.deviceId == MatrixSessionProvider.currentSession?.sessionParams?.deviceId } - ?: return emptyList() - val otherSessions = devicesList.toMutableList().apply { remove(currentSession) } - .filter { !isSessionInactive(it.first.lastSeenTs) } + val currentSession = cryptoList.firstOrNull { + it.deviceId == MatrixSessionProvider.currentSession?.sessionParams?.deviceId + } ?: return emptyList() + + val otherSessions = cryptoList.toMutableList().apply { remove(currentSession) } + val isCurrentSessionVerified = - currentSession.second.trustLevel?.isCrossSigningVerified() == true + currentSession.trustLevel?.isCrossSigningVerified() == true val sessionsList = mutableListOf<ActiveSessionListItem>(SessionHeader(context.getString(R.string.current_session))) sessionsList.add( ActiveSession( - deviceInfo = currentSession.first, - cryptoDeviceInfo = currentSession.second, + cryptoDeviceInfo = currentSession, canVerify = !isCurrentSessionVerified && otherSessions.isNotEmpty(), isResetKeysVisible = !isCurrentSessionVerified, - isOptionsVisible = sessionsWithVisibleOptions.contains(currentSession.second.deviceId) + isOptionsVisible = sessionsWithVisibleOptions.contains(currentSession.deviceId) ) ) if (otherSessions.isNotEmpty()) { sessionsList.add(SessionHeader(context.getString(R.string.other_sessions))) - sessionsList.addAll(otherSessions.map { - ActiveSession( - deviceInfo = it.first, - cryptoDeviceInfo = it.second, - canVerify = isCurrentSessionVerified && it.second.trustLevel?.isCrossSigningVerified() != true, - isResetKeysVisible = false, - isOptionsVisible = sessionsWithVisibleOptions.contains(it.second.deviceId) - ) + sessionsList.addAll(otherSessions.mapNotNull { + if (!it.isDehydrated) { + ActiveSession( + cryptoDeviceInfo = it, + canVerify = isCurrentSessionVerified && it.trustLevel?.isCrossSigningVerified() != true, + isResetKeysVisible = false, + isOptionsVisible = sessionsWithVisibleOptions.contains(it.deviceId) + ) + } else null } ) } @@ -101,16 +97,4 @@ class ActiveSessionsDataSource @Inject constructor( .initializeCrossSigning(authConfirmationProvider) } - private fun isSessionInactive(lastSeenTsMillis: Long?): Boolean = - if (lastSeenTsMillis == null || lastSeenTsMillis <= 0) { - false - } else { - val diffMilliseconds = System.currentTimeMillis() - lastSeenTsMillis - diffMilliseconds >= TimeUnit.DAYS.toMillis(SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS) - } - - companion object { - private const val SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS = 30L - } - } \ No newline at end of file diff --git a/settings/src/main/java/org/futo/circles/settings/feature/active_sessions/ActiveSessionsViewModel.kt b/settings/src/main/java/org/futo/circles/settings/feature/active_sessions/ActiveSessionsViewModel.kt index b2f3db9872916ca33d15bbb1b35d96324bdc109d..c2e296fe7b48a36d1ef23ef1392cd90836835198 100644 --- a/settings/src/main/java/org/futo/circles/settings/feature/active_sessions/ActiveSessionsViewModel.kt +++ b/settings/src/main/java/org/futo/circles/settings/feature/active_sessions/ActiveSessionsViewModel.kt @@ -41,6 +41,9 @@ class ActiveSessionsViewModel @Inject constructor( } } + init { + launchBg { dataSource.refreshDevicesList() } + } fun onSessionClicked(deviceId: String) { dataSource.toggleOptionsVisibilityFor(deviceId) diff --git a/settings/src/main/java/org/futo/circles/settings/feature/active_sessions/list/ActiveSessionsViewHolder.kt b/settings/src/main/java/org/futo/circles/settings/feature/active_sessions/list/ActiveSessionsViewHolder.kt index ee21a65a2a440f6759f0044120db81e0a269ac5a..9ca96e7dd45de7bb0d133c7c719f4c0df8198e32 100644 --- a/settings/src/main/java/org/futo/circles/settings/feature/active_sessions/list/ActiveSessionsViewHolder.kt +++ b/settings/src/main/java/org/futo/circles/settings/feature/active_sessions/list/ActiveSessionsViewHolder.kt @@ -31,7 +31,7 @@ class SessionItemViewHolder( with(binding) { lRoot.setOnClickListener { activeSessionClickListener.onItemClicked(data.id) } - tvDeviceName.text = data.deviceInfo.displayName ?: data.id + tvDeviceName.text = data.cryptoDeviceInfo.displayName() ?: data.id tvDeviceId.text = data.cryptoDeviceInfo.deviceId vInfo.setData(data, activeSessionClickListener) vInfo.setIsVisible(data.isOptionsVisible) diff --git a/settings/src/main/java/org/futo/circles/settings/model/ActiveSessionListItem.kt b/settings/src/main/java/org/futo/circles/settings/model/ActiveSessionListItem.kt index f2ae29c621a2a003cab9d2fb785a4ce12783744b..5956757eb90a43be51f6130bd68097ba38621aa5 100644 --- a/settings/src/main/java/org/futo/circles/settings/model/ActiveSessionListItem.kt +++ b/settings/src/main/java/org/futo/circles/settings/model/ActiveSessionListItem.kt @@ -3,7 +3,6 @@ package org.futo.circles.settings.model import org.futo.circles.core.base.list.IdEntity import org.futo.circles.core.provider.MatrixSessionProvider import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo sealed class ActiveSessionListItem : IdEntity<String> @@ -14,7 +13,6 @@ data class SessionHeader( } data class ActiveSession( - val deviceInfo: DeviceInfo, val cryptoDeviceInfo: CryptoDeviceInfo, val canVerify: Boolean, val isResetKeysVisible: Boolean,