BillCarsonFr/JsonViewer
+
+ Copyright (C) 2018 stfalcon.com
+
Apache License
diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
index ab7c3e1bf7..db14dba93d 100644
--- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
+++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
@@ -32,8 +32,6 @@ import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController
import com.facebook.stetho.Stetho
import com.gabrielittner.threetenbp.LazyThreeTen
-import com.github.piasy.biv.BigImageViewer
-import com.github.piasy.biv.loader.glide.GlideImageLoader
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.matrix.android.api.auth.AuthenticationService
@@ -44,15 +42,12 @@ import im.vector.riotx.core.di.HasVectorInjector
import im.vector.riotx.core.di.VectorComponent
import im.vector.riotx.core.extensions.configureAndStart
import im.vector.riotx.core.rx.RxConfig
-import im.vector.riotx.features.call.WebRtcPeerConnectionManager
import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.notifications.NotificationUtils
-import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
-import im.vector.riotx.features.session.SessionListener
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.version.VersionProvider
import im.vector.riotx.push.fcm.FcmHelper
@@ -79,16 +74,13 @@ class VectorApplication :
@Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
- @Inject lateinit var sessionListener: SessionListener
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
- @Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
@Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var versionProvider: VersionProvider
@Inject lateinit var notificationUtils: NotificationUtils
@Inject lateinit var appStateHandler: AppStateHandler
@Inject lateinit var rxConfig: RxConfig
@Inject lateinit var popupAlertManager: PopupAlertManager
- @Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
lateinit var vectorComponent: VectorComponent
@@ -114,7 +106,6 @@ class VectorApplication :
logInfo()
LazyThreeTen.init(this)
- BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager))
@@ -137,8 +128,7 @@ class VectorApplication :
if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
- lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
- lastAuthenticatedSession.callSignalingService().addCallListener(webRtcPeerConnectionManager)
+ lastAuthenticatedSession.configureAndStart(applicationContext)
}
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
diff --git a/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt b/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt
index 967d7d638d..37c07b8293 100644
--- a/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt
+++ b/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 New Vector Ltd
+ * Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@ import android.graphics.drawable.ColorDrawable
import android.util.AttributeSet
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.content.withStyledAttributes
import im.vector.riotx.R
import kotlin.math.abs
@@ -67,19 +68,19 @@ class PercentViewBehavior(context: Context, attrs: AttributeSet) : Coo
private var isPrepared: Boolean = false
init {
- val a = context.obtainStyledAttributes(attrs, R.styleable.PercentViewBehavior)
- dependViewId = a.getResourceId(R.styleable.PercentViewBehavior_behavior_dependsOn, 0)
- dependType = a.getInt(R.styleable.PercentViewBehavior_behavior_dependType, DEPEND_TYPE_WIDTH)
- dependTarget = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_dependTarget, UNSPECIFIED_INT)
- targetX = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetX, UNSPECIFIED_INT)
- targetY = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetY, UNSPECIFIED_INT)
- targetWidth = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetWidth, UNSPECIFIED_INT)
- targetHeight = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetHeight, UNSPECIFIED_INT)
- targetBackgroundColor = a.getColor(R.styleable.PercentViewBehavior_behavior_targetBackgroundColor, UNSPECIFIED_INT)
- targetAlpha = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetAlpha, UNSPECIFIED_FLOAT)
- targetRotateX = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateX, UNSPECIFIED_FLOAT)
- targetRotateY = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateY, UNSPECIFIED_FLOAT)
- a.recycle()
+ context.withStyledAttributes(attrs, R.styleable.PercentViewBehavior) {
+ dependViewId = getResourceId(R.styleable.PercentViewBehavior_behavior_dependsOn, 0)
+ dependType = getInt(R.styleable.PercentViewBehavior_behavior_dependType, DEPEND_TYPE_WIDTH)
+ dependTarget = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_dependTarget, UNSPECIFIED_INT)
+ targetX = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetX, UNSPECIFIED_INT)
+ targetY = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetY, UNSPECIFIED_INT)
+ targetWidth = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetWidth, UNSPECIFIED_INT)
+ targetHeight = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetHeight, UNSPECIFIED_INT)
+ targetBackgroundColor = getColor(R.styleable.PercentViewBehavior_behavior_targetBackgroundColor, UNSPECIFIED_INT)
+ targetAlpha = getFloat(R.styleable.PercentViewBehavior_behavior_targetAlpha, UNSPECIFIED_FLOAT)
+ targetRotateX = getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateX, UNSPECIFIED_FLOAT)
+ targetRotateY = getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateY, UNSPECIFIED_FLOAT)
+ }
}
private fun prepare(parent: CoordinatorLayout, child: View, dependency: View) {
diff --git a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
index ff9865c3ea..2dc7b24ebf 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
@@ -20,8 +20,12 @@ import arrow.core.Option
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.ActiveSessionDataSource
+import im.vector.riotx.features.call.WebRtcPeerConnectionManager
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
+import im.vector.riotx.features.notifications.PushRuleTriggerListener
+import im.vector.riotx.features.session.SessionListener
+import timber.log.Timber
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
import javax.inject.Singleton
@@ -30,23 +34,42 @@ import javax.inject.Singleton
class ActiveSessionHolder @Inject constructor(private val authenticationService: AuthenticationService,
private val sessionObservableStore: ActiveSessionDataSource,
private val keyRequestHandler: KeyRequestHandler,
- private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler
+ private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler,
+ private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
+ private val pushRuleTriggerListener: PushRuleTriggerListener,
+ private val sessionListener: SessionListener,
+ private val imageManager: ImageManager
) {
private var activeSession: AtomicReference = AtomicReference()
fun setActiveSession(session: Session) {
+ Timber.w("setActiveSession of ${session.myUserId}")
activeSession.set(session)
sessionObservableStore.post(Option.just(session))
+
keyRequestHandler.start(session)
incomingVerificationRequestHandler.start(session)
+ session.addListener(sessionListener)
+ pushRuleTriggerListener.startWithSession(session)
+ session.callSignalingService().addCallListener(webRtcPeerConnectionManager)
+ imageManager.onSessionStarted(session)
}
fun clearActiveSession() {
+ // Do some cleanup first
+ getSafeActiveSession()?.let {
+ Timber.w("clearActiveSession of ${it.myUserId}")
+ it.callSignalingService().removeCallListener(webRtcPeerConnectionManager)
+ it.removeListener(sessionListener)
+ }
+
activeSession.set(null)
sessionObservableStore.post(Option.empty())
+
keyRequestHandler.stop()
incomingVerificationRequestHandler.stop()
+ pushRuleTriggerListener.stop()
}
fun hasActiveSession(): Boolean {
diff --git a/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt b/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
new file mode 100644
index 0000000000..74a01e76ec
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.core.di
+
+import android.content.Context
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.model.GlideUrl
+import com.github.piasy.biv.BigImageViewer
+import com.github.piasy.biv.loader.glide.GlideImageLoader
+import im.vector.matrix.android.api.session.Session
+import im.vector.riotx.ActiveSessionDataSource
+import im.vector.riotx.core.glide.FactoryUrl
+import java.io.InputStream
+import javax.inject.Inject
+
+/**
+ * This class is used to configure the library we use for images
+ */
+class ImageManager @Inject constructor(
+ private val context: Context,
+ private val activeSessionDataSource: ActiveSessionDataSource
+) {
+
+ fun onSessionStarted(session: Session) {
+ // Do this call first
+ BigImageViewer.initialize(GlideImageLoader.with(context, session.getOkHttpClient()))
+
+ val glide = Glide.get(context)
+
+ // And this one. FIXME But are losing what BigImageViewer has done to add a Progress listener
+ glide.registry.replace(GlideUrl::class.java, InputStream::class.java, FactoryUrl(activeSessionDataSource))
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
index ceb276614a..2838a42169 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
@@ -48,6 +48,7 @@ import im.vector.riotx.features.invite.InviteUsersToRoomActivity
import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.link.LinkHandlerActivity
import im.vector.riotx.features.login.LoginActivity
+import im.vector.riotx.features.media.VectorAttachmentViewerActivity
import im.vector.riotx.features.media.BigImageViewerActivity
import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoMediaViewerActivity
@@ -72,6 +73,7 @@ import im.vector.riotx.features.terms.ReviewTermsActivity
import im.vector.riotx.features.ui.UiStateRepository
import im.vector.riotx.features.widgets.WidgetActivity
import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionBottomSheet
+import im.vector.riotx.features.workers.signout.SignOutBottomSheetDialogFragment
@Component(
dependencies = [
@@ -135,6 +137,7 @@ interface ScreenComponent {
fun inject(activity: ReviewTermsActivity)
fun inject(activity: WidgetActivity)
fun inject(activity: VectorCallActivity)
+ fun inject(activity: VectorAttachmentViewerActivity)
/* ==========================================================================================
* BottomSheets
@@ -152,6 +155,7 @@ interface ScreenComponent {
fun inject(bottomSheet: RoomWidgetPermissionBottomSheet)
fun inject(bottomSheet: RoomWidgetsBottomSheet)
fun inject(bottomSheet: CallControlsBottomSheet)
+ fun inject(bottomSheet: SignOutBottomSheetDialogFragment)
/* ==========================================================================================
* Others
diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
index badfdd96c1..6ac6fa03da 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
@@ -36,7 +36,6 @@ import im.vector.riotx.features.reactions.EmojiChooserViewModel
import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel
import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
-import im.vector.riotx.features.workers.signout.SignOutViewModel
@Module
interface ViewModelModule {
@@ -51,11 +50,6 @@ interface ViewModelModule {
* Below are bindings for the androidx view models (which extend ViewModel). Will be converted to MvRx ViewModel in the future.
*/
- @Binds
- @IntoMap
- @ViewModelKey(SignOutViewModel::class)
- fun bindSignOutViewModel(viewModel: SignOutViewModel): ViewModel
-
@Binds
@IntoMap
@ViewModelKey(EmojiChooserViewModel::class)
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt
index 88c96578ae..a1c4ddd038 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt
@@ -16,9 +16,16 @@
package im.vector.riotx.core.extensions
+import android.content.ActivityNotFoundException
+import android.content.Intent
import android.os.Parcelable
import androidx.fragment.app.Fragment
+import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
+import im.vector.riotx.core.utils.toast
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) {
parentFragmentManager.commitTransaction { add(frameId, fragment) }
@@ -89,3 +96,29 @@ fun Fragment.getAllChildFragments(): List {
// Define a missing constant
const val POP_BACK_STACK_EXCLUSIVE = 0
+
+fun Fragment.queryExportKeys(userId: String, requestCode: Int) {
+ // We need WRITE_EXTERNAL permission
+// if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
+// this,
+// PERMISSION_REQUEST_CODE_EXPORT_KEYS,
+// R.string.permissions_rationale_msg_keys_backup_export)) {
+ // WRITE permissions are not needed
+ val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).let {
+ it.format(Date())
+ }
+ val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+ intent.type = "text/plain"
+ intent.putExtra(
+ Intent.EXTRA_TITLE,
+ "riot-megolm-export-$userId-$timestamp.txt"
+ )
+
+ try {
+ startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), requestCode)
+ } catch (activityNotFoundException: ActivityNotFoundException) {
+ activity?.toast(R.string.error_no_external_application_found)
+ }
+// }
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
index 29b169ffd4..9d49319896 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
@@ -24,20 +24,14 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.riotx.core.services.VectorSyncService
-import im.vector.riotx.features.notifications.PushRuleTriggerListener
-import im.vector.riotx.features.session.SessionListener
import timber.log.Timber
-fun Session.configureAndStart(context: Context,
- pushRuleTriggerListener: PushRuleTriggerListener,
- sessionListener: SessionListener) {
+fun Session.configureAndStart(context: Context) {
+ Timber.i("Configure and start session for $myUserId")
open()
- addListener(sessionListener)
setFilter(FilterService.FilterPreset.RiotFilter)
- Timber.i("Configure and start session for ${this.myUserId}")
startSyncing(context)
refreshPushers()
- pushRuleTriggerListener.startWithSession(this)
}
fun Session.startSyncing(context: Context) {
@@ -65,3 +59,12 @@ fun Session.hasUnsavedKeys(): Boolean {
return cryptoService().inboundGroupSessionsCount(false) > 0
&& cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp
}
+
+fun Session.cannotLogoutSafely(): Boolean {
+ // has some encrypted chat
+ return hasUnsavedKeys()
+ // has local cross signing keys
+ || (cryptoService().crossSigningService().allPrivateKeysKnown()
+ // That are not backed up
+ && !sharedSecretStorageService.isRecoverySetup())
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/glide/FactoryUrl.kt b/vector/src/main/java/im/vector/riotx/core/glide/FactoryUrl.kt
new file mode 100644
index 0000000000..fc037894db
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/glide/FactoryUrl.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.core.glide
+
+import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
+import com.bumptech.glide.load.model.GlideUrl
+import com.bumptech.glide.load.model.ModelLoader
+import com.bumptech.glide.load.model.ModelLoaderFactory
+import com.bumptech.glide.load.model.MultiModelLoaderFactory
+import im.vector.riotx.ActiveSessionDataSource
+import okhttp3.OkHttpClient
+import java.io.InputStream
+
+class FactoryUrl(private val activeSessionDataSource: ActiveSessionDataSource) : ModelLoaderFactory {
+
+ override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader {
+ val client = activeSessionDataSource.currentValue?.orNull()?.getOkHttpClient() ?: OkHttpClient()
+ return OkHttpUrlLoader(client)
+ }
+
+ override fun teardown() {
+ // Do nothing, this instance doesn't own the client.
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt
index 191ab6d972..510eef71e1 100644
--- a/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt
+++ b/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt
@@ -65,7 +65,7 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde
private val height: Int)
: DataFetcher {
- val client = OkHttpClient()
+ private val client = activeSessionHolder.getSafeActiveSession()?.getOkHttpClient() ?: OkHttpClient()
override fun getDataClass(): Class {
return InputStream::class.java
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt
index f451308c36..f54776fc40 100644
--- a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt
+++ b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt
@@ -38,6 +38,7 @@ import android.text.TextUtils.substring
import android.text.style.ForegroundColorSpan
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.content.withStyledAttributes
import timber.log.Timber
import java.util.ArrayList
import java.util.regex.Pattern
@@ -71,6 +72,7 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att
private var maxLines = 0
private var lineSpacingMult = 1.0f
private var lineAddVertPad = 0.0f
+
/**
* The end punctuation which will be removed when appending [.ELLIPSIS].
*/
@@ -408,9 +410,9 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att
}
init {
- val a = context.obtainStyledAttributes(attrs, intArrayOf(android.R.attr.maxLines, android.R.attr.ellipsize), defStyle, 0)
- maxLines = a.getInt(0, Int.MAX_VALUE)
- a.recycle()
+ context.withStyledAttributes(attrs, intArrayOf(android.R.attr.maxLines, android.R.attr.ellipsize), defStyle) {
+ maxLines = getInt(0, Int.MAX_VALUE)
+ }
setEndPunctuationPattern(DEFAULT_END_PUNCTUATION)
val currentTextColor = currentTextColor
val ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor))
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt
index b8587750a3..99c158252f 100644
--- a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt
+++ b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 New Vector Ltd
+ * Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package im.vector.riotx.core.platform
import android.content.Context
import android.util.AttributeSet
+import androidx.core.content.withStyledAttributes
import androidx.core.widget.NestedScrollView
import im.vector.riotx.R
@@ -34,9 +35,9 @@ class MaxHeightScrollView @JvmOverloads constructor(context: Context, attrs: Att
init {
if (attrs != null) {
- val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView)
- maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT)
- styledAttrs.recycle()
+ context.withStyledAttributes(attrs, R.styleable.MaxHeightScrollView) {
+ maxHeight = getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT)
+ }
}
}
diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt
index d29982c9e4..455e856833 100644
--- a/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt
+++ b/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt
@@ -25,6 +25,7 @@ import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
+import androidx.core.content.withStyledAttributes
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
@@ -117,16 +118,15 @@ class BottomSheetActionButton @JvmOverloads constructor(
inflate(context, R.layout.item_verification_action, this)
ButterKnife.bind(this)
- val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BottomSheetActionButton, 0, 0)
- title = typedArray.getString(R.styleable.BottomSheetActionButton_actionTitle) ?: ""
- subTitle = typedArray.getString(R.styleable.BottomSheetActionButton_actionDescription) ?: ""
- forceStartPadding = typedArray.getBoolean(R.styleable.BottomSheetActionButton_forceStartPadding, false)
- leftIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_leftIcon)
+ context.withStyledAttributes(attrs, R.styleable.BottomSheetActionButton) {
+ title = getString(R.styleable.BottomSheetActionButton_actionTitle) ?: ""
+ subTitle = getString(R.styleable.BottomSheetActionButton_actionDescription) ?: ""
+ forceStartPadding = getBoolean(R.styleable.BottomSheetActionButton_forceStartPadding, false)
+ leftIcon = getDrawable(R.styleable.BottomSheetActionButton_leftIcon)
- rightIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_rightIcon)
+ rightIcon = getDrawable(R.styleable.BottomSheetActionButton_rightIcon)
- tint = typedArray.getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor))
-
- typedArray.recycle()
+ tint = getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor))
+ }
}
}
diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
index 817575d91a..d0cea6194b 100755
--- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
+++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
@@ -17,15 +17,14 @@
package im.vector.riotx.core.ui.views
import android.content.Context
-import androidx.preference.PreferenceManager
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
-import android.widget.AbsListView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.edit
import androidx.core.view.isVisible
+import androidx.preference.PreferenceManager
import androidx.transition.TransitionManager
import butterknife.BindView
import butterknife.ButterKnife
@@ -58,22 +57,12 @@ class KeysBackupBanner @JvmOverloads constructor(
var delegate: Delegate? = null
private var state: State = State.Initial
- private var scrollState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE
- set(value) {
- field = value
-
- val pendingV = pendingVisibility
-
- if (pendingV != null) {
- pendingVisibility = null
- visibility = pendingV
- }
- }
-
- private var pendingVisibility: Int? = null
-
init {
setupView()
+ PreferenceManager.getDefaultSharedPreferences(context).edit {
+ putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)
+ putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "")
+ }
}
/**
@@ -91,7 +80,8 @@ class KeysBackupBanner @JvmOverloads constructor(
state = newState
hideAll()
-
+ val parent = parent as ViewGroup
+ TransitionManager.beginDelayedTransition(parent)
when (newState) {
State.Initial -> renderInitial()
State.Hidden -> renderHidden()
@@ -102,22 +92,6 @@ class KeysBackupBanner @JvmOverloads constructor(
}
}
- override fun setVisibility(visibility: Int) {
- if (scrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
- // Wait for scroll state to be idle
- pendingVisibility = visibility
- return
- }
-
- if (visibility != getVisibility()) {
- // Schedule animation
- val parent = parent as ViewGroup
- TransitionManager.beginDelayedTransition(parent)
- }
-
- super.setVisibility(visibility)
- }
-
override fun onClick(v: View?) {
when (state) {
is State.Setup -> {
@@ -166,6 +140,8 @@ class KeysBackupBanner @JvmOverloads constructor(
ButterKnife.bind(this)
setOnClickListener(this)
+ textView1.setOnClickListener(this)
+ textView2.setOnClickListener(this)
}
private fun renderInitial() {
@@ -184,9 +160,9 @@ class KeysBackupBanner @JvmOverloads constructor(
} else {
isVisible = true
- textView1.setText(R.string.keys_backup_banner_setup_line1)
+ textView1.setText(R.string.secure_backup_banner_setup_line1)
textView2.isVisible = true
- textView2.setText(R.string.keys_backup_banner_setup_line2)
+ textView2.setText(R.string.secure_backup_banner_setup_line2)
close.isVisible = true
}
}
@@ -218,10 +194,10 @@ class KeysBackupBanner @JvmOverloads constructor(
}
private fun renderBackingUp() {
- // Do not render when backing up anymore
- isVisible = false
-
- textView1.setText(R.string.keys_backup_banner_in_progress)
+ isVisible = true
+ textView1.setText(R.string.secure_backup_banner_setup_line1)
+ textView2.isVisible = true
+ textView2.setText(R.string.keys_backup_banner_in_progress)
loading.isVisible = true
}
diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt b/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt
index 4c4a553e5c..6f6057cb43 100644
--- a/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt
+++ b/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt
@@ -36,6 +36,9 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD
private val behaviorRelay = createRelay()
+ val currentValue: T?
+ get() = behaviorRelay.value
+
override fun observe(): Observable {
return behaviorRelay.hide().observeOn(AndroidSchedulers.mainThread())
}
diff --git a/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt b/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
index 05f14ae4f2..070375d201 100644
--- a/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
+++ b/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
@@ -22,6 +22,7 @@ import android.os.Build
import androidx.annotation.RequiresApi
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.extensions.tryThis
+import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.call.CallState
import im.vector.matrix.android.api.session.call.CallsListener
import im.vector.matrix.android.api.session.call.EglUtils
@@ -31,7 +32,7 @@ import im.vector.matrix.android.api.session.room.model.call.CallAnswerContent
import im.vector.matrix.android.api.session.room.model.call.CallCandidatesContent
import im.vector.matrix.android.api.session.room.model.call.CallHangupContent
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
-import im.vector.riotx.core.di.ActiveSessionHolder
+import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.core.services.BluetoothHeadsetReceiver
import im.vector.riotx.core.services.CallService
import im.vector.riotx.core.services.WiredHeadsetStateReceiver
@@ -71,9 +72,12 @@ import javax.inject.Singleton
@Singleton
class WebRtcPeerConnectionManager @Inject constructor(
private val context: Context,
- private val sessionHolder: ActiveSessionHolder
+ private val activeSessionDataSource: ActiveSessionDataSource
) : CallsListener {
+ private val currentSession: Session?
+ get() = activeSessionDataSource.currentValue?.orNull()
+
interface CurrentCallListener {
fun onCurrentCallChange(call: MxCall?)
fun onCaptureStateChanged(mgr: WebRtcPeerConnectionManager) {}
@@ -288,15 +292,16 @@ class WebRtcPeerConnectionManager @Inject constructor(
}
private fun getTurnServer(callback: ((TurnServerResponse?) -> Unit)) {
- sessionHolder.getActiveSession().callSignalingService().getTurnServer(object : MatrixCallback {
- override fun onSuccess(data: TurnServerResponse?) {
- callback(data)
- }
+ currentSession?.callSignalingService()
+ ?.getTurnServer(object : MatrixCallback {
+ override fun onSuccess(data: TurnServerResponse?) {
+ callback(data)
+ }
- override fun onFailure(failure: Throwable) {
- callback(null)
- }
- })
+ override fun onFailure(failure: Throwable) {
+ callback(null)
+ }
+ })
}
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
@@ -310,7 +315,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
currentCall?.mxCall
?.takeIf { it.state is CallState.Connected }
?.let { mxCall ->
- val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
+ val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.roomId
// Start background service with notification
CallService.onPendingCall(
@@ -318,7 +323,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
isVideo = mxCall.isVideoCall,
roomName = name,
roomId = mxCall.roomId,
- matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
+ matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId)
}
@@ -373,14 +378,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
val mxCall = callContext.mxCall
// Update service state
- val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
+ val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.roomId
CallService.onPendingCall(
context = context,
isVideo = mxCall.isVideoCall,
roomName = name,
roomId = mxCall.roomId,
- matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
+ matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId
)
executor.execute {
@@ -563,14 +568,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
?.let { mxCall ->
// Start background service with notification
- val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
+ val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.otherUserId
CallService.onOnGoingCallBackground(
context = context,
isVideo = mxCall.isVideoCall,
roomName = name,
roomId = mxCall.roomId,
- matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
+ matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId
)
}
@@ -631,20 +636,20 @@ class WebRtcPeerConnectionManager @Inject constructor(
}
Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall")
- val createdCall = sessionHolder.getSafeActiveSession()?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
+ val createdCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
val callContext = CallContext(createdCall)
audioManager.startForCall(createdCall)
currentCall = callContext
- val name = sessionHolder.getSafeActiveSession()?.getUser(createdCall.otherUserId)?.getBestName()
+ val name = currentSession?.getUser(createdCall.otherUserId)?.getBestName()
?: createdCall.otherUserId
CallService.onOutgoingCallRinging(
context = context.applicationContext,
isVideo = createdCall.isVideoCall,
roomName = name,
roomId = createdCall.roomId,
- matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
+ matrixId = currentSession?.myUserId ?: "",
callId = createdCall.callId)
executor.execute {
@@ -693,14 +698,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
}
// Start background service with notification
- val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
+ val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.otherUserId
CallService.onIncomingCallRinging(
context = context,
isVideo = mxCall.isVideoCall,
roomName = name,
roomId = mxCall.roomId,
- matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
+ matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId
)
@@ -818,14 +823,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
}
val mxCall = call.mxCall
// Update service state
- val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
+ val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.otherUserId
CallService.onPendingCall(
context = context,
isVideo = mxCall.isVideoCall,
roomName = name,
roomId = mxCall.roomId,
- matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
+ matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId
)
executor.execute {
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt
index b9b75588f1..2467334f69 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt
@@ -17,37 +17,34 @@
package im.vector.riotx.features.crypto.keys
import android.content.Context
-import android.os.Environment
+import android.net.Uri
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.awaitCallback
-import im.vector.riotx.core.files.addEntryToDownloadManager
-import im.vector.riotx.core.files.writeToFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import java.io.File
class KeysExporter(private val session: Session) {
/**
* Export keys and return the file path with the callback
*/
- fun export(context: Context, password: String, callback: MatrixCallback) {
+ fun export(context: Context, password: String, uri: Uri, callback: MatrixCallback) {
GlobalScope.launch(Dispatchers.Main) {
runCatching {
- val data = awaitCallback { session.cryptoService().exportRoomKeys(password, it) }
withContext(Dispatchers.IO) {
- val parentDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
- val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt")
-
- writeToFile(data, file)
-
- addEntryToDownloadManager(context, file, "text/plain")
-
- file.absolutePath
+ val data = awaitCallback { session.cryptoService().exportRoomKeys(password, it) }
+ val os = context.contentResolver?.openOutputStream(uri)
+ if (os == null) {
+ false
+ } else {
+ os.write(data)
+ os.flush()
+ true
+ }
}
}.foldToCallback(callback)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
index c7d3da30ea..b99c0e4330 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
@@ -15,6 +15,8 @@
*/
package im.vector.riotx.features.crypto.keysbackup.setup
+import android.app.Activity
+import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AlertDialog
@@ -132,36 +134,22 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
this,
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
R.string.permissions_rationale_msg_keys_backup_export)) {
- ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
- override fun onPassphrase(passphrase: String) {
- showWaitingView()
+ try {
+ val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+ intent.type = "text/plain"
+ intent.putExtra(Intent.EXTRA_TITLE, "riot-megolm-export-${session.myUserId}-${System.currentTimeMillis()}.txt")
- KeysExporter(session)
- .export(this@KeysBackupSetupActivity,
- passphrase,
- object : MatrixCallback {
- override fun onSuccess(data: String) {
- hideWaitingView()
-
- AlertDialog.Builder(this@KeysBackupSetupActivity)
- .setMessage(getString(R.string.encryption_export_saved_as, data))
- .setCancelable(false)
- .setPositiveButton(R.string.ok) { _, _ ->
- val resultIntent = Intent()
- resultIntent.putExtra(MANUAL_EXPORT, true)
- setResult(RESULT_OK, resultIntent)
- finish()
- }
- .show()
- }
-
- override fun onFailure(failure: Throwable) {
- toast(failure.localizedMessage ?: getString(R.string.unexpected_error))
- hideWaitingView()
- }
- })
- }
- })
+ startActivityForResult(
+ Intent.createChooser(
+ intent,
+ getString(R.string.keys_backup_setup_step1_manual_export)
+ ),
+ REQUEST_CODE_SAVE_MEGOLM_EXPORT
+ )
+ } catch (activityNotFoundException: ActivityNotFoundException) {
+ toast(R.string.error_no_external_application_found)
+ }
}
}
@@ -173,6 +161,47 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
}
}
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {
+ val uri = data?.data
+ if (resultCode == Activity.RESULT_OK && uri != null) {
+ ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
+ override fun onPassphrase(passphrase: String) {
+ showWaitingView()
+
+ KeysExporter(session)
+ .export(this@KeysBackupSetupActivity,
+ passphrase,
+ uri,
+ object : MatrixCallback {
+ override fun onSuccess(data: Boolean) {
+ if (data) {
+ toast(getString(R.string.encryption_exported_successfully))
+ Intent().apply {
+ putExtra(MANUAL_EXPORT, true)
+ }.let {
+ setResult(Activity.RESULT_OK, it)
+ finish()
+ }
+ }
+ hideWaitingView()
+ }
+
+ override fun onFailure(failure: Throwable) {
+ toast(failure.localizedMessage ?: getString(R.string.unexpected_error))
+ hideWaitingView()
+ }
+ })
+ }
+ })
+ } else {
+ toast(getString(R.string.unexpected_error))
+ hideWaitingView()
+ }
+ }
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+
override fun onBackPressed() {
if (viewModel.shouldPromptOnBack) {
if (waitingView?.isVisible == true) {
@@ -205,6 +234,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
const val KEYS_VERSION = "KEYS_VERSION"
const val MANUAL_EXPORT = "MANUAL_EXPORT"
const val EXTRA_SHOW_MANUAL_EXPORT = "SHOW_MANUAL_EXPORT"
+ const val REQUEST_CODE_SAVE_MEGOLM_EXPORT = 101
fun intent(context: Context, showManualExport: Boolean): Intent {
val intent = Intent(context, KeysBackupSetupActivity::class.java)
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt
index a3306677fe..40ea79eb6d 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt
@@ -15,13 +15,13 @@
*/
package im.vector.riotx.features.crypto.keysbackup.setup
-import android.os.AsyncTask
import android.os.Bundle
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.ImageView
import androidx.lifecycle.Observer
+import androidx.lifecycle.viewModelScope
import androidx.transition.TransitionManager
import butterknife.BindView
import butterknife.OnClick
@@ -33,6 +33,8 @@ import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.ui.views.PasswordStrengthBar
import im.vector.riotx.features.settings.VectorLocale
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import javax.inject.Inject
class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() {
@@ -117,9 +119,9 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment()
if (newValue.isEmpty()) {
viewModel.passwordStrength.value = null
} else {
- AsyncTask.execute {
+ viewModel.viewModelScope.launch(Dispatchers.IO) {
val strength = zxcvbn.measure(newValue)
- activity?.runOnUiThread {
+ launch(Dispatchers.Main) {
viewModel.passwordStrength.value = strength
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt
index 6a3fadbcb3..9f68e09444 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt
@@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
+import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.util.awaitCallback
@@ -84,8 +85,10 @@ class BootstrapCrossSigningTask @Inject constructor(
override suspend fun execute(params: Params): BootstrapResult {
val crossSigningService = session.cryptoService().crossSigningService()
+ Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Starting...")
// Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized
if (!crossSigningService.isCrossSigningInitialized()) {
+ Timber.d("## BootstrapCrossSigningTask: Cross signing not enabled, so initialize")
params.progressListener?.onProgress(
WaitingViewData(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
@@ -104,8 +107,9 @@ class BootstrapCrossSigningTask @Inject constructor(
return handleInitializeXSigningError(failure)
}
} else {
- // not sure how this can happen??
+ Timber.d("## BootstrapCrossSigningTask: Cross signing already setup, go to 4S setup")
if (params.initOnlyCrossSigning) {
+ // not sure how this can happen??
return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup"))
}
}
@@ -119,6 +123,8 @@ class BootstrapCrossSigningTask @Inject constructor(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2),
isIndeterminate = true)
)
+
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S key with pass: ${params.passphrase != null}")
try {
keyInfo = awaitCallback {
params.passphrase?.let { passphrase ->
@@ -141,6 +147,7 @@ class BootstrapCrossSigningTask @Inject constructor(
}
}
} catch (failure: Failure) {
+ Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to generate key <${failure.localizedMessage}>")
return BootstrapResult.FailedToCreateSSSSKey(failure)
}
@@ -149,19 +156,24 @@ class BootstrapCrossSigningTask @Inject constructor(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key),
isIndeterminate = true)
)
+
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Set default key")
try {
awaitCallback {
ssssService.setDefaultKey(keyInfo.keyId, it)
}
} catch (failure: Failure) {
// Maybe we could just ignore this error?
+ Timber.e("## BootstrapCrossSigningTask: Creating 4S - Set default key error <${failure.localizedMessage}>")
return BootstrapResult.FailedToSetDefaultSSSSKey(failure)
}
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys")
val xKeys = crossSigningService.getCrossSigningPrivateKeys()
val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey
val sskPrivateKey = xKeys.selfSigned ?: return BootstrapResult.MissingPrivateKey
val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys success")
try {
params.progressListener?.onProgress(
@@ -170,6 +182,7 @@ class BootstrapCrossSigningTask @Inject constructor(
isIndeterminate = true
)
)
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing MSK...")
awaitCallback {
ssssService.storeSecret(
MASTER_KEY_SSSS_NAME,
@@ -183,6 +196,7 @@ class BootstrapCrossSigningTask @Inject constructor(
isIndeterminate = true
)
)
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing USK...")
awaitCallback {
ssssService.storeSecret(
USER_SIGNING_KEY_SSSS_NAME,
@@ -196,6 +210,7 @@ class BootstrapCrossSigningTask @Inject constructor(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true
)
)
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing SSK...")
awaitCallback {
ssssService.storeSecret(
SELF_SIGNING_KEY_SSSS_NAME,
@@ -204,6 +219,7 @@ class BootstrapCrossSigningTask @Inject constructor(
)
}
} catch (failure: Failure) {
+ Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to store keys <${failure.localizedMessage}>")
// Maybe we could just ignore this error?
return BootstrapResult.FailedToStorePrivateKeyInSSSS(failure)
}
@@ -215,7 +231,14 @@ class BootstrapCrossSigningTask @Inject constructor(
)
)
try {
- if (session.cryptoService().keysBackupService().keysBackupVersion == null) {
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Checking megolm backup")
+
+ // First ensure that in sync
+ val serverVersion = awaitCallback {
+ session.cryptoService().keysBackupService().getCurrentVersion(it)
+ }
+ if (serverVersion == null) {
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Create megolm backup")
val creationInfo = awaitCallback {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
@@ -223,6 +246,7 @@ class BootstrapCrossSigningTask @Inject constructor(
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
}
// Save it for gossiping
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Save megolm backup key for gossiping")
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
awaitCallback {
@@ -239,6 +263,7 @@ class BootstrapCrossSigningTask @Inject constructor(
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
}
+ Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Finished")
return BootstrapResult.Success(keyInfo)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt
index 3a95a575f4..22dcab217e 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt
@@ -406,7 +406,10 @@ class BootstrapSharedViewModel @AssistedInject constructor(
setState {
copy(
recoveryKeyCreationInfo = bootstrapResult.keyInfo,
- step = BootstrapStep.SaveRecoveryKey(false)
+ step = BootstrapStep.SaveRecoveryKey(
+ // If a passphrase was used, saving key is optional
+ state.passphrase != null
+ )
)
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
index 7a3d38f649..cd9fed108b 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
@@ -250,7 +250,10 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
is VerificationTxState.Started,
is VerificationTxState.WaitingOtherReciprocateConfirm -> {
showFragment(VerificationQRWaitingFragment::class, Bundle().apply {
- putParcelable(MvRx.KEY_ARG, VerificationQRWaitingFragment.Args(state.isMe, state.otherUserMxItem?.getBestName() ?: ""))
+ putParcelable(MvRx.KEY_ARG, VerificationQRWaitingFragment.Args(
+ isMe = state.isMe,
+ otherUserName = state.otherUserMxItem?.getBestName() ?: ""
+ ))
})
return@withState
}
@@ -353,6 +356,17 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
}
}
+ fun forSelfVerification(session: Session, outgoingRequest: String): VerificationBottomSheet {
+ return VerificationBottomSheet().apply {
+ arguments = Bundle().apply {
+ putParcelable(MvRx.KEY_ARG, VerificationArgs(
+ otherUserId = session.myUserId,
+ selfVerificationMode = true,
+ verificationId = outgoingRequest
+ ))
+ }
+ }
+ }
const val WAITING_SELF_VERIF_TAG: String = "WAITING_SELF_VERIF_TAG"
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
index 687c280910..f917b5a9f9 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
@@ -65,19 +65,19 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
@UiThread
fun render(context: Context,
- glideRequest: GlideRequests,
+ glideRequests: GlideRequests,
matrixItem: MatrixItem,
target: Target) {
val placeholder = getPlaceholderDrawable(context, matrixItem)
- buildGlideRequest(glideRequest, matrixItem.avatarUrl)
+ buildGlideRequest(glideRequests, matrixItem.avatarUrl)
.placeholder(placeholder)
.into(target)
}
@AnyThread
@Throws
- fun shortcutDrawable(context: Context, glideRequest: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap {
- return glideRequest
+ fun shortcutDrawable(context: Context, glideRequests: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap {
+ return glideRequests
.asBitmap()
.apply {
val resolvedUrl = resolvedUrl(matrixItem.avatarUrl)
@@ -98,8 +98,8 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
}
@AnyThread
- fun getCachedDrawable(glideRequest: GlideRequests, matrixItem: MatrixItem): Drawable {
- return buildGlideRequest(glideRequest, matrixItem.avatarUrl)
+ fun getCachedDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem): Drawable {
+ return buildGlideRequest(glideRequests, matrixItem.avatarUrl)
.onlyRetrieveFromCache(true)
.submit()
.get()
@@ -117,9 +117,9 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
// PRIVATE API *********************************************************************************
- private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest {
+ private fun buildGlideRequest(glideRequests: GlideRequests, avatarUrl: String?): GlideRequest {
val resolvedUrl = resolvedUrl(avatarUrl)
- return glideRequest
+ return glideRequests
.load(resolvedUrl)
.apply(RequestOptions.circleCropTransform())
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
index 8d5fc5f564..5bed5b1f78 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
@@ -46,7 +46,8 @@ import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.popup.VerificationVectorAlert
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences
-import im.vector.riotx.features.workers.signout.SignOutViewModel
+import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel
+import im.vector.riotx.features.workers.signout.ServerBackupStatusViewState
import im.vector.riotx.push.fcm.FcmHelper
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_home.*
@@ -60,13 +61,16 @@ data class HomeActivityArgs(
val accountCreation: Boolean
) : Parcelable
-class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory {
+class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory {
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
@Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory
+ private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
+ @Inject lateinit var serverBackupviewModelFactory: ServerBackupStatusViewModel.Factory
+
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var pushManager: PushersManager
@@ -92,6 +96,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
return unknownDeviceViewModelFactory.create(initialState)
}
+ override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel {
+ return serverBackupviewModelFactory.create(initialState)
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice())
@@ -177,7 +185,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
R.string.crosssigning_verify_this_session,
R.string.confirm_your_identity
) {
- it.navigator.waitSessionVerification(it)
+ if (event.waitForIncomingRequest) {
+ it.navigator.waitSessionVerification(it)
+ } else {
+ it.navigator.requestSelfSessionVerification(it)
+ }
}
}
@@ -230,7 +242,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
}
// Force remote backup state update to update the banner if needed
- viewModelProvider.get(SignOutViewModel::class.java).refreshRemoteStateIfNeeded()
+ serverBackupStatusViewModel.refreshRemoteStateIfNeeded()
}
override fun configure(toolbar: Toolbar) {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewEvents.kt
index 2f1d8b2705..1cdabe824c 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewEvents.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewEvents.kt
@@ -21,5 +21,5 @@ import im.vector.riotx.core.platform.VectorViewEvents
sealed class HomeActivityViewEvents : VectorViewEvents {
data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents()
- data class OnNewSession(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents()
+ data class OnNewSession(val userItem: MatrixItem.UserItem?, val waitForIncomingRequest: Boolean = true) : HomeActivityViewEvents()
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt
index fdf0936d58..f89bb5a547 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt
@@ -130,7 +130,14 @@ class HomeActivityViewModel @AssistedInject constructor(
// Cross-signing is already set up for this user, is it trusted?
if (!mxCrossSigningInfo.isTrusted()) {
// New session
- _viewEvents.post(HomeActivityViewEvents.OnNewSession(session.getUser(session.myUserId)?.toMatrixItem()))
+ _viewEvents.post(
+ HomeActivityViewEvents.OnNewSession(
+ session.getUser(session.myUserId)?.toMatrixItem(),
+ // If it's an old unverified, we should send requests
+ // instead of waiting for an incoming one
+ reAuthHelper.data != null
+ )
+ )
}
} else {
// Initialize cross-signing
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
index 435ff7a9ab..e7ee8ca577 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
@@ -27,7 +27,6 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView
-import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
@@ -50,13 +49,10 @@ import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.popup.VerificationVectorAlert
import im.vector.riotx.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
-import im.vector.riotx.features.workers.signout.SignOutViewModel
+import im.vector.riotx.features.workers.signout.BannerState
+import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel
+import im.vector.riotx.features.workers.signout.ServerBackupStatusViewState
import kotlinx.android.synthetic.main.fragment_home_detail.*
-import kotlinx.android.synthetic.main.fragment_home_detail.activeCallPiP
-import kotlinx.android.synthetic.main.fragment_home_detail.activeCallPiPWrap
-import kotlinx.android.synthetic.main.fragment_home_detail.activeCallView
-import kotlinx.android.synthetic.main.fragment_home_detail.syncStateView
-import kotlinx.android.synthetic.main.fragment_room_detail.*
import timber.log.Timber
import javax.inject.Inject
@@ -66,15 +62,17 @@ private const val INDEX_ROOMS = 2
class HomeDetailFragment @Inject constructor(
val homeDetailViewModelFactory: HomeDetailViewModel.Factory,
+ private val serverBackupStatusViewModelFactory: ServerBackupStatusViewModel.Factory,
private val avatarRenderer: AvatarRenderer,
private val alertManager: PopupAlertManager,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager
-) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback {
+) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback, ServerBackupStatusViewModel.Factory {
private val unreadCounterBadgeViews = arrayListOf()
private val viewModel: HomeDetailViewModel by fragmentViewModel()
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
+ private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel()
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel
@@ -196,34 +194,14 @@ class HomeDetailFragment @Inject constructor(
}
private fun setupKeysBackupBanner() {
- // Keys backup banner
- // Use the SignOutViewModel, it observe the keys backup state and this is what we need here
- val model = fragmentViewModelProvider.get(SignOutViewModel::class.java)
-
- model.keysBackupState.observe(viewLifecycleOwner, Observer { keysBackupState ->
- when (keysBackupState) {
- null ->
- homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
- KeysBackupState.Disabled ->
- homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(model.getNumberOfKeysToBackup()), false)
- KeysBackupState.NotTrusted,
- KeysBackupState.WrongBackUpVersion ->
- // In this case, getCurrentBackupVersion() should not return ""
- homeKeysBackupBanner.render(KeysBackupBanner.State.Recover(model.getCurrentBackupVersion()), false)
- KeysBackupState.WillBackUp,
- KeysBackupState.BackingUp ->
- homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
- KeysBackupState.ReadyToBackUp ->
- if (model.canRestoreKeys()) {
- homeKeysBackupBanner.render(KeysBackupBanner.State.Update(model.getCurrentBackupVersion()), false)
- } else {
- homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
- }
- else ->
- homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
+ serverBackupStatusViewModel.subscribe(this) {
+ when (val banState = it.bannerState.invoke()) {
+ is BannerState.Setup -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
+ BannerState.BackingUp -> homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
+ null,
+ BannerState.Hidden -> homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
}
- })
-
+ }.disposeOnDestroyView()
homeKeysBackupBanner.delegate = this
}
@@ -332,4 +310,8 @@ class HomeDetailFragment @Inject constructor(
}
}
}
+
+ override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel {
+ return serverBackupStatusViewModelFactory.create(initialState)
+ }
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
index 2f8c72a996..ba7e356545 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
@@ -1174,14 +1174,27 @@ class RoomDetailFragment @Inject constructor(
}
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) {
- navigator.openImageViewer(requireActivity(), mediaData, view) { pairs ->
+ navigator.openMediaViewer(
+ activity = requireActivity(),
+ roomId = roomDetailArgs.roomId,
+ mediaData = mediaData,
+ view = view
+ ) { pairs ->
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
}
}
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
- navigator.openVideoViewer(requireActivity(), mediaData)
+ navigator.openMediaViewer(
+ activity = requireActivity(),
+ roomId = roomDetailArgs.roomId,
+ mediaData = mediaData,
+ view = view
+ ) { pairs ->
+ pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
+ pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
+ }
}
// override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {
@@ -1199,7 +1212,7 @@ class RoomDetailFragment @Inject constructor(
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
if (allGranted(grantResults)) {
when (requestCode) {
- SAVE_ATTACHEMENT_REQUEST_CODE -> {
+ SAVE_ATTACHEMENT_REQUEST_CODE -> {
sharedActionViewModel.pendingAction?.let {
handleActions(it)
sharedActionViewModel.pendingAction = null
@@ -1340,13 +1353,13 @@ class RoomDetailFragment @Inject constructor(
private fun onShareActionClicked(action: EventSharedAction.Share) {
session.fileService().downloadFile(
- FileService.DownloadMode.FOR_EXTERNAL_SHARE,
- action.eventId,
- action.messageContent.body,
- action.messageContent.getFileUrl(),
- action.messageContent.mimeType,
- action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
- object : MatrixCallback {
+ downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
+ id = action.eventId,
+ fileName = action.messageContent.body,
+ mimeType = action.messageContent.mimeType,
+ url = action.messageContent.getFileUrl(),
+ elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
+ callback = object : MatrixCallback {
override fun onSuccess(data: File) {
if (isAdded) {
shareMedia(requireContext(), data, getMimeTypeFromUri(requireContext(), data.toUri()))
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
index fb1cb8e666..982448d1c1 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
@@ -877,13 +877,13 @@ class RoomDetailViewModel @AssistedInject constructor(
}
} else {
session.fileService().downloadFile(
- FileService.DownloadMode.FOR_INTERNAL_USE,
- action.eventId,
- action.messageFileContent.getFileName(),
- action.messageFileContent.mimeType,
- mxcUrl,
- action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(),
- object : MatrixCallback {
+ downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
+ id = action.eventId,
+ fileName = action.messageFileContent.getFileName(),
+ mimeType = action.messageFileContent.mimeType,
+ url = mxcUrl,
+ elementToDecrypt = action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(),
+ callback = object : MatrixCallback {
override fun onSuccess(data: File) {
_viewEvents.post(RoomDetailViewEvents.DownloadFileState(
action.messageFileContent.mimeType,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 2174556098..4f5f34cbf0 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -337,7 +337,7 @@ class MessageItemFactory @Inject constructor(
.playable(true)
.highlighted(highlight)
.mediaData(thumbnailData)
- .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
+ .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) }
}
private fun buildItemForTextContent(messageContent: MessageTextContent,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
index fff74e0328..c2f683d5a5 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
@@ -40,17 +40,20 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.widgets.model.WidgetContent
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
+import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.R
-import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.resources.StringProvider
import timber.log.Timber
import javax.inject.Inject
-class NoticeEventFormatter @Inject constructor(private val sessionHolder: ActiveSessionHolder,
+class NoticeEventFormatter @Inject constructor(private val activeSessionDataSource: ActiveSessionDataSource,
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
private val sp: StringProvider) {
- private fun Event.isSentByCurrentUser() = senderId != null && senderId == sessionHolder.getSafeActiveSession()?.myUserId
+ private val currentUserId: String?
+ get() = activeSessionDataSource.currentValue?.orNull()?.myUserId
+
+ private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
fun format(timelineEvent: TimelineEvent): CharSequence? {
return when (val type = timelineEvent.root.getClearType()) {
@@ -449,7 +452,6 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
return when (eventContent?.membership) {
Membership.INVITE -> {
- val selfUserId = sessionHolder.getSafeActiveSession()?.myUserId
when {
eventContent.thirdPartyInvite != null -> {
val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid ?: event.stateKey
@@ -466,7 +468,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
sp.getString(R.string.notice_room_third_party_registered_invite, userWhoHasAccepted, threePidDisplayName)
}
}
- event.stateKey == selfUserId ->
+ event.stateKey == currentUserId ->
eventContent.safeReason?.let { reason ->
sp.getString(R.string.notice_room_invite_you_with_reason, senderDisplayName, reason)
} ?: sp.getString(R.string.notice_room_invite_you, senderDisplayName)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt
index c52b863658..bee3ca6c5b 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt
@@ -22,6 +22,7 @@ import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
+import androidx.core.content.withStyledAttributes
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.riotx.R
@@ -73,11 +74,11 @@ class PollResultLineView @JvmOverloads constructor(
orientation = HORIZONTAL
ButterKnife.bind(this)
- val typedArray = context.obtainStyledAttributes(attrs, R.styleable.PollResultLineView, 0, 0)
- label = typedArray.getString(R.styleable.PollResultLineView_optionName) ?: ""
- percent = typedArray.getString(R.styleable.PollResultLineView_optionCount) ?: ""
- optionSelected = typedArray.getBoolean(R.styleable.PollResultLineView_optionSelected, false)
- isWinner = typedArray.getBoolean(R.styleable.PollResultLineView_optionIsWinner, false)
- typedArray.recycle()
+ context.withStyledAttributes(attrs, R.styleable.PollResultLineView) {
+ label = getString(R.styleable.PollResultLineView_optionName) ?: ""
+ percent = getString(R.styleable.PollResultLineView_optionCount) ?: ""
+ optionSelected = getBoolean(R.styleable.PollResultLineView_optionSelected, false)
+ isWinner = getBoolean(R.styleable.PollResultLineView_optionIsWinner, false)
+ }
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
index 7edc674b11..071e23c252 100644
--- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
@@ -49,9 +49,6 @@ import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.ensureTrailingSlash
-import im.vector.riotx.features.call.WebRtcPeerConnectionManager
-import im.vector.riotx.features.notifications.PushRuleTriggerListener
-import im.vector.riotx.features.session.SessionListener
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
import timber.log.Timber
import java.util.concurrent.CancellationException
@@ -64,13 +61,10 @@ class LoginViewModel @AssistedInject constructor(
private val applicationContext: Context,
private val authenticationService: AuthenticationService,
private val activeSessionHolder: ActiveSessionHolder,
- private val pushRuleTriggerListener: PushRuleTriggerListener,
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
- private val sessionListener: SessionListener,
private val reAuthHelper: ReAuthHelper,
- private val stringProvider: StringProvider,
- private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager)
- : VectorViewModel(initialState) {
+ private val stringProvider: StringProvider
+) : VectorViewModel(initialState) {
@AssistedInject.Factory
interface Factory {
@@ -667,8 +661,7 @@ class LoginViewModel @AssistedInject constructor(
private fun onSessionCreated(session: Session) {
activeSessionHolder.setActiveSession(session)
- session.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
- session.callSignalingService().addCallListener(webRtcPeerConnectionManager)
+ session.configureAndStart(applicationContext)
setState {
copy(
asyncLoginAction = Success(Unit)
diff --git a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt
new file mode 100644
index 0000000000..2812b011f9
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.media
+
+import android.content.Context
+import android.graphics.Color
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ImageView
+import android.widget.SeekBar
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.Group
+import im.vector.riotx.R
+import im.vector.riotx.attachmentviewer.AttachmentEventListener
+import im.vector.riotx.attachmentviewer.AttachmentEvents
+
+class AttachmentOverlayView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr), AttachmentEventListener {
+
+ var onShareCallback: (() -> Unit)? = null
+ var onBack: (() -> Unit)? = null
+ var onPlayPause: ((play: Boolean) -> Unit)? = null
+ var videoSeekTo: ((progress: Int) -> Unit)? = null
+
+ private val counterTextView: TextView
+ private val infoTextView: TextView
+ private val shareImage: ImageView
+ private val overlayPlayPauseButton: ImageView
+ private val overlaySeekBar: SeekBar
+
+ var isPlaying = false
+
+ val videoControlsGroup: Group
+
+ var suspendSeekBarUpdate = false
+
+ init {
+ View.inflate(context, R.layout.merge_image_attachment_overlay, this)
+ setBackgroundColor(Color.TRANSPARENT)
+ counterTextView = findViewById(R.id.overlayCounterText)
+ infoTextView = findViewById(R.id.overlayInfoText)
+ shareImage = findViewById(R.id.overlayShareButton)
+ videoControlsGroup = findViewById(R.id.overlayVideoControlsGroup)
+ overlayPlayPauseButton = findViewById(R.id.overlayPlayPauseButton)
+ overlaySeekBar = findViewById(R.id.overlaySeekBar)
+ findViewById(R.id.overlayBackButton).setOnClickListener {
+ onBack?.invoke()
+ }
+ findViewById(R.id.overlayShareButton).setOnClickListener {
+ onShareCallback?.invoke()
+ }
+ findViewById(R.id.overlayPlayPauseButton).setOnClickListener {
+ onPlayPause?.invoke(!isPlaying)
+ }
+
+ overlaySeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
+ if (fromUser) {
+ videoSeekTo?.invoke(progress)
+ }
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar?) {
+ suspendSeekBarUpdate = true
+ }
+
+ override fun onStopTrackingTouch(seekBar: SeekBar?) {
+ suspendSeekBarUpdate = false
+ }
+ })
+ }
+
+ fun updateWith(counter: String, senderInfo: String) {
+ counterTextView.text = counter
+ infoTextView.text = senderInfo
+ }
+
+ override fun onEvent(event: AttachmentEvents) {
+ when (event) {
+ is AttachmentEvents.VideoEvent -> {
+ overlayPlayPauseButton.setImageResource(if (!event.isPlaying) R.drawable.ic_play_arrow else R.drawable.ic_pause)
+ if (!suspendSeekBarUpdate) {
+ val safeDuration = (if (event.duration == 0) 100 else event.duration).toFloat()
+ val percent = ((event.progress / safeDuration) * 100f).toInt().coerceAtMost(100)
+ isPlaying = event.isPlaying
+ overlaySeekBar.progress = percent
+ }
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt
new file mode 100644
index 0000000000..d4c41c7cb3
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.media
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.ImageView
+import com.bumptech.glide.request.target.CustomViewTarget
+import com.bumptech.glide.request.transition.Transition
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.session.file.FileService
+import im.vector.riotx.attachmentviewer.AttachmentInfo
+import im.vector.riotx.attachmentviewer.AttachmentSourceProvider
+import im.vector.riotx.attachmentviewer.ImageLoaderTarget
+import im.vector.riotx.attachmentviewer.VideoLoaderTarget
+import java.io.File
+
+abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRenderer, val fileService: FileService) : AttachmentSourceProvider {
+
+ interface InteractionListener {
+ fun onDismissTapped()
+ fun onShareTapped()
+ fun onPlayPause(play: Boolean)
+ fun videoSeekTo(percent: Int)
+ }
+
+ var interactionListener: InteractionListener? = null
+
+ protected var overlayView: AttachmentOverlayView? = null
+
+ override fun overlayViewAtPosition(context: Context, position: Int): View? {
+ if (position == -1) return null
+ if (overlayView == null) {
+ overlayView = AttachmentOverlayView(context)
+ overlayView?.onBack = {
+ interactionListener?.onDismissTapped()
+ }
+ overlayView?.onShareCallback = {
+ interactionListener?.onShareTapped()
+ }
+ overlayView?.onPlayPause = { play ->
+ interactionListener?.onPlayPause(play)
+ }
+ overlayView?.videoSeekTo = { percent ->
+ interactionListener?.videoSeekTo(percent)
+ }
+ }
+ return overlayView
+ }
+
+ override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image) {
+ (info.data as? ImageContentRenderer.Data)?.let {
+ imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) {
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ target.onLoadFailed(info.uid, errorDrawable)
+ }
+
+ override fun onResourceCleared(placeholder: Drawable?) {
+ target.onResourceCleared(info.uid, placeholder)
+ }
+
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ target.onResourceReady(info.uid, resource)
+ }
+ })
+ }
+ }
+
+ override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage) {
+ (info.data as? ImageContentRenderer.Data)?.let {
+ imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) {
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ target.onLoadFailed(info.uid, errorDrawable)
+ }
+
+ override fun onResourceCleared(placeholder: Drawable?) {
+ target.onResourceCleared(info.uid, placeholder)
+ }
+
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ target.onResourceReady(info.uid, resource)
+ }
+ })
+ }
+ }
+
+ override fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video) {
+ val data = info.data as? VideoContentRenderer.Data ?: return
+// videoContentRenderer.render(data,
+// holder.thumbnailImage,
+// holder.loaderProgressBar,
+// holder.videoView,
+// holder.errorTextView)
+ imageContentRenderer.render(data.thumbnailMediaData, target.contextView(), object : CustomViewTarget(target.contextView()) {
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ target.onThumbnailLoadFailed(info.uid, errorDrawable)
+ }
+
+ override fun onResourceCleared(placeholder: Drawable?) {
+ target.onThumbnailResourceCleared(info.uid, placeholder)
+ }
+
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ target.onThumbnailResourceReady(info.uid, resource)
+ }
+ })
+
+ target.onVideoFileLoading(info.uid)
+ fileService.downloadFile(
+ downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
+ id = data.eventId,
+ mimeType = data.mimeType,
+ elementToDecrypt = data.elementToDecrypt,
+ fileName = data.filename,
+ url = data.url,
+ callback = object : MatrixCallback {
+ override fun onSuccess(data: File) {
+ target.onVideoFileReady(info.uid, data)
+ }
+
+ override fun onFailure(failure: Throwable) {
+ target.onVideoFileLoadFailed(info.uid)
+ }
+ }
+ )
+ }
+
+ override fun clear(id: String) {
+ // TODO("Not yet implemented")
+ }
+
+ abstract fun getFileForSharing(position: Int, callback: ((File?) -> Unit))
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt
new file mode 100644
index 0000000000..cb0039fc7e
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.media
+
+import android.content.Context
+import android.view.View
+import androidx.core.view.isVisible
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.session.events.model.isVideoMessage
+import im.vector.matrix.android.api.session.file.FileService
+import im.vector.matrix.android.api.session.room.Room
+import im.vector.riotx.attachmentviewer.AttachmentInfo
+import im.vector.riotx.core.date.VectorDateFormatter
+import im.vector.riotx.core.extensions.localDateTime
+import java.io.File
+
+class DataAttachmentRoomProvider(
+ private val attachments: List,
+ private val room: Room?,
+ private val initialIndex: Int,
+ imageContentRenderer: ImageContentRenderer,
+ private val dateFormatter: VectorDateFormatter,
+ fileService: FileService) : BaseAttachmentProvider(imageContentRenderer, fileService) {
+
+ override fun getItemCount(): Int = attachments.size
+
+ override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
+ return attachments[position].let {
+ when (it) {
+ is ImageContentRenderer.Data -> {
+ if (it.mimeType == "image/gif") {
+ AttachmentInfo.AnimatedImage(
+ uid = it.eventId,
+ url = it.url ?: "",
+ data = it
+ )
+ } else {
+ AttachmentInfo.Image(
+ uid = it.eventId,
+ url = it.url ?: "",
+ data = it
+ )
+ }
+ }
+ is VideoContentRenderer.Data -> {
+ AttachmentInfo.Video(
+ uid = it.eventId,
+ url = it.url ?: "",
+ data = it,
+ thumbnail = AttachmentInfo.Image(
+ uid = it.eventId,
+ url = it.thumbnailMediaData.url ?: "",
+ data = it.thumbnailMediaData
+ )
+ )
+ }
+ else -> throw IllegalArgumentException()
+ }
+ }
+ }
+
+ override fun overlayViewAtPosition(context: Context, position: Int): View? {
+ super.overlayViewAtPosition(context, position)
+ val item = attachments[position]
+ val timeLineEvent = room?.getTimeLineEvent(item.eventId)
+ if (timeLineEvent != null) {
+ val dateString = timeLineEvent.root.localDateTime().let {
+ "${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} "
+ }
+ overlayView?.updateWith("${position + 1} of ${attachments.size}", "${timeLineEvent.senderInfo.displayName} $dateString")
+ overlayView?.videoControlsGroup?.isVisible = timeLineEvent.root.isVideoMessage()
+ } else {
+ overlayView?.updateWith("", "")
+ }
+ return overlayView
+ }
+
+ override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
+ val item = attachments[position]
+ fileService.downloadFile(
+ downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
+ id = item.eventId,
+ fileName = item.filename,
+ mimeType = item.mimeType,
+ url = item.url ?: "",
+ elementToDecrypt = item.elementToDecrypt,
+ callback = object : MatrixCallback {
+ override fun onSuccess(data: File) {
+ callback(data)
+ }
+
+ override fun onFailure(failure: Throwable) {
+ callback(null)
+ }
+ }
+ )
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
index eeeb55ed15..f7613855c5 100644
--- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
+++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
@@ -19,11 +19,13 @@ package im.vector.riotx.features.media
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Parcelable
+import android.view.View
import android.widget.ImageView
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestListener
+import com.bumptech.glide.request.target.CustomViewTarget
import com.bumptech.glide.request.target.Target
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION_USE_EXIF
import com.github.piasy.biv.view.BigImageView
@@ -42,21 +44,29 @@ import java.io.File
import javax.inject.Inject
import kotlin.math.min
+interface AttachmentData : Parcelable {
+ val eventId: String
+ val filename: String
+ val mimeType: String?
+ val url: String?
+ val elementToDecrypt: ElementToDecrypt?
+}
+
class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val dimensionConverter: DimensionConverter) {
@Parcelize
data class Data(
- val eventId: String,
- val filename: String,
- val mimeType: String?,
- val url: String?,
- val elementToDecrypt: ElementToDecrypt?,
+ override val eventId: String,
+ override val filename: String,
+ override val mimeType: String?,
+ override val url: String?,
+ override val elementToDecrypt: ElementToDecrypt?,
val height: Int?,
val maxHeight: Int,
val width: Int?,
val maxWidth: Int
- ) : Parcelable {
+ ) : AttachmentData {
fun isLocalFile() = url.isLocalFile()
}
@@ -93,6 +103,25 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.into(imageView)
}
+ fun render(data: Data, contextView: View, target: CustomViewTarget<*, Drawable>) {
+ val req = if (data.elementToDecrypt != null) {
+ // Encrypted image
+ GlideApp
+ .with(contextView)
+ .load(data)
+ } else {
+ // Clear image
+ val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url)
+ GlideApp
+ .with(contextView)
+ .load(resolvedUrl)
+ }
+
+ req.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
+ .fitCenter()
+ .into(target)
+ }
+
fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
val size = processSize(data, mode)
@@ -122,6 +151,45 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.into(imageView)
}
+ fun renderThumbnailDontTransform(data: Data, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
+ // a11y
+ imageView.contentDescription = data.filename
+
+ val req = if (data.elementToDecrypt != null) {
+ // Encrypted image
+ GlideApp
+ .with(imageView)
+ .load(data)
+ } else {
+ // Clear image
+ val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url)
+ GlideApp
+ .with(imageView)
+ .load(resolvedUrl)
+ }
+
+ req.listener(object : RequestListener {
+ override fun onLoadFailed(e: GlideException?,
+ model: Any?,
+ target: Target?,
+ isFirstResource: Boolean): Boolean {
+ callback?.invoke(false)
+ return false
+ }
+
+ override fun onResourceReady(resource: Drawable?,
+ model: Any?,
+ target: Target?,
+ dataSource: DataSource?,
+ isFirstResource: Boolean): Boolean {
+ callback?.invoke(true)
+ return false
+ }
+ })
+ .dontTransform()
+ .into(imageView)
+ }
+
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest {
return if (data.elementToDecrypt != null) {
// Encrypted image
diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
index 2be940d0c1..8a6c2f7545 100644
--- a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
@@ -91,6 +91,8 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
encryptedImageView.isVisible = false
// Postpone transaction a bit until thumbnail is loaded
supportPostponeEnterTransition()
+
+ // We are not passing the exact same image that in the
imageContentRenderer.renderFitTarget(mediaData, ImageContentRenderer.Mode.THUMBNAIL, imageTransitionView) {
// Proceed with transaction
scheduleStartPostponedTransition(imageTransitionView)
@@ -134,13 +136,13 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
private fun onShareActionClicked() {
session.fileService().downloadFile(
- FileService.DownloadMode.FOR_EXTERNAL_SHARE,
- mediaData.eventId,
- mediaData.filename,
- mediaData.mimeType,
- mediaData.url,
- mediaData.elementToDecrypt,
- object : MatrixCallback {
+ downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
+ id = mediaData.eventId,
+ fileName = mediaData.filename,
+ mimeType = mediaData.mimeType,
+ url = mediaData.url,
+ elementToDecrypt = mediaData.elementToDecrypt,
+ callback = object : MatrixCallback {
override fun onSuccess(data: File) {
shareMedia(this@ImageMediaViewerActivity, data, getMimeTypeFromUri(this@ImageMediaViewerActivity, data.toUri()))
}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt
new file mode 100644
index 0000000000..7a7fea6dc4
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.media
+
+import android.content.Context
+import android.view.View
+import androidx.core.view.isVisible
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.events.model.isVideoMessage
+import im.vector.matrix.android.api.session.events.model.toModel
+import im.vector.matrix.android.api.session.file.FileService
+import im.vector.matrix.android.api.session.room.Room
+import im.vector.matrix.android.api.session.room.model.message.MessageContent
+import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
+import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
+import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
+import im.vector.matrix.android.api.session.room.model.message.getFileUrl
+import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
+import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
+import im.vector.riotx.attachmentviewer.AttachmentInfo
+import im.vector.riotx.core.date.VectorDateFormatter
+import im.vector.riotx.core.extensions.localDateTime
+import java.io.File
+import javax.inject.Inject
+
+class RoomEventsAttachmentProvider(
+ private val attachments: List,
+ private val initialIndex: Int,
+ imageContentRenderer: ImageContentRenderer,
+ private val dateFormatter: VectorDateFormatter,
+ fileService: FileService
+) : BaseAttachmentProvider(imageContentRenderer, fileService) {
+
+ override fun getItemCount(): Int {
+ return attachments.size
+ }
+
+ override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
+ return attachments[position].let {
+ val content = it.root.getClearContent().toModel() as? MessageWithAttachmentContent
+ if (content is MessageImageContent) {
+ val data = ImageContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.getFileUrl(),
+ elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
+ maxHeight = -1,
+ maxWidth = -1,
+ width = null,
+ height = null
+ )
+ if (content.mimeType == "image/gif") {
+ AttachmentInfo.AnimatedImage(
+ uid = it.eventId,
+ url = content.url ?: "",
+ data = data
+ )
+ } else {
+ AttachmentInfo.Image(
+ uid = it.eventId,
+ url = content.url ?: "",
+ data = data
+ )
+ }
+ } else if (content is MessageVideoContent) {
+ val thumbnailData = ImageContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.videoInfo?.thumbnailFile?.url
+ ?: content.videoInfo?.thumbnailUrl,
+ elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(),
+ height = content.videoInfo?.height,
+ maxHeight = -1,
+ width = content.videoInfo?.width,
+ maxWidth = -1
+ )
+ val data = VideoContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.getFileUrl(),
+ elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
+ thumbnailMediaData = thumbnailData
+ )
+ AttachmentInfo.Video(
+ uid = it.eventId,
+ url = content.getFileUrl() ?: "",
+ data = data,
+ thumbnail = AttachmentInfo.Image(
+ uid = it.eventId,
+ url = content.videoInfo?.thumbnailFile?.url
+ ?: content.videoInfo?.thumbnailUrl ?: "",
+ data = thumbnailData
+
+ )
+ )
+ } else {
+ AttachmentInfo.Image(
+ uid = it.eventId,
+ url = "",
+ data = null
+ )
+ }
+ }
+ }
+
+ override fun overlayViewAtPosition(context: Context, position: Int): View? {
+ super.overlayViewAtPosition(context, position)
+ val item = attachments[position]
+ val dateString = item.root.localDateTime().let {
+ "${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} "
+ }
+ overlayView?.updateWith("${position + 1} of ${attachments.size}", "${item.senderInfo.displayName} $dateString")
+ overlayView?.videoControlsGroup?.isVisible = item.root.isVideoMessage()
+ return overlayView
+ }
+
+ override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
+ attachments[position].let { timelineEvent ->
+
+ val messageContent = timelineEvent.root.getClearContent().toModel()
+ as? MessageWithAttachmentContent
+ ?: return@let
+ fileService.downloadFile(
+ downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
+ id = timelineEvent.eventId,
+ fileName = messageContent.body,
+ mimeType = messageContent.mimeType,
+ url = messageContent.getFileUrl(),
+ elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
+ callback = object : MatrixCallback {
+ override fun onSuccess(data: File) {
+ callback(data)
+ }
+
+ override fun onFailure(failure: Throwable) {
+ callback(null)
+ }
+ }
+ )
+ }
+ }
+}
+
+class AttachmentProviderFactory @Inject constructor(
+ private val imageContentRenderer: ImageContentRenderer,
+ private val vectorDateFormatter: VectorDateFormatter,
+ private val session: Session
+) {
+
+ fun createProvider(attachments: List, initialIndex: Int): RoomEventsAttachmentProvider {
+ return RoomEventsAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService())
+ }
+
+ fun createProvider(attachments: List, room: Room?, initialIndex: Int): DataAttachmentRoomProvider {
+ return DataAttachmentRoomProvider(attachments, room, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService())
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt
new file mode 100644
index 0000000000..38e3ccc69c
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt
@@ -0,0 +1,277 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package im.vector.riotx.features.media
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.View
+import android.view.ViewTreeObserver
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.core.net.toUri
+import androidx.core.transition.addListener
+import androidx.core.view.ViewCompat
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.transition.Transition
+import im.vector.riotx.R
+import im.vector.riotx.attachmentviewer.AttachmentCommands
+import im.vector.riotx.attachmentviewer.AttachmentViewerActivity
+import im.vector.riotx.core.di.ActiveSessionHolder
+import im.vector.riotx.core.di.DaggerScreenComponent
+import im.vector.riotx.core.di.HasVectorInjector
+import im.vector.riotx.core.di.ScreenComponent
+import im.vector.riotx.core.di.VectorComponent
+import im.vector.riotx.core.intent.getMimeTypeFromUri
+import im.vector.riotx.core.utils.shareMedia
+import im.vector.riotx.features.themes.ActivityOtherThemes
+import im.vector.riotx.features.themes.ThemeUtils
+import kotlinx.android.parcel.Parcelize
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.system.measureTimeMillis
+
+class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmentProvider.InteractionListener {
+
+ @Parcelize
+ data class Args(
+ val roomId: String?,
+ val eventId: String,
+ val sharedTransitionName: String?
+ ) : Parcelable
+
+ @Inject
+ lateinit var sessionHolder: ActiveSessionHolder
+
+ @Inject
+ lateinit var dataSourceFactory: AttachmentProviderFactory
+
+ @Inject
+ lateinit var imageContentRenderer: ImageContentRenderer
+
+ private lateinit var screenComponent: ScreenComponent
+
+ private var initialIndex = 0
+ private var isAnimatingOut = false
+
+ var currentSourceProvider: BaseAttachmentProvider? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Timber.i("onCreate Activity ${this.javaClass.simpleName}")
+ val vectorComponent = getVectorComponent()
+ screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
+ val timeForInjection = measureTimeMillis {
+ screenComponent.inject(this)
+ }
+ Timber.v("Injecting dependencies into ${javaClass.simpleName} took $timeForInjection ms")
+ ThemeUtils.setActivityTheme(this, getOtherThemes())
+
+ val args = args() ?: throw IllegalArgumentException("Missing arguments")
+
+ if (savedInstanceState == null && addTransitionListener()) {
+ args.sharedTransitionName?.let {
+ ViewCompat.setTransitionName(imageTransitionView, it)
+ transitionImageContainer.isVisible = true
+
+ // Postpone transaction a bit until thumbnail is loaded
+ val mediaData: Parcelable? = intent.getParcelableExtra(EXTRA_IMAGE_DATA)
+ if (mediaData is ImageContentRenderer.Data) {
+ // will be shown at end of transition
+ pager2.isInvisible = true
+ supportPostponeEnterTransition()
+ imageContentRenderer.renderThumbnailDontTransform(mediaData, imageTransitionView) {
+ // Proceed with transaction
+ scheduleStartPostponedTransition(imageTransitionView)
+ }
+ } else if (mediaData is VideoContentRenderer.Data) {
+ // will be shown at end of transition
+ pager2.isInvisible = true
+ supportPostponeEnterTransition()
+ imageContentRenderer.renderThumbnailDontTransform(mediaData.thumbnailMediaData, imageTransitionView) {
+ // Proceed with transaction
+ scheduleStartPostponedTransition(imageTransitionView)
+ }
+ }
+ }
+ }
+
+ val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() }
+
+ val room = args.roomId?.let { session.getRoom(it) }
+
+ val inMemoryData = intent.getParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA)
+ if (inMemoryData != null) {
+ val sourceProvider = dataSourceFactory.createProvider(inMemoryData, room, initialIndex)
+ val index = inMemoryData.indexOfFirst { it.eventId == args.eventId }
+ initialIndex = index
+ sourceProvider.interactionListener = this
+ setSourceProvider(sourceProvider)
+ this.currentSourceProvider = sourceProvider
+ if (savedInstanceState == null) {
+ pager2.setCurrentItem(index, false)
+ // The page change listener is not notified of the change...
+ pager2.post {
+ onSelectedPositionChanged(index)
+ }
+ }
+ } else {
+ val events = room?.getAttachmentMessages()
+ ?: emptyList()
+ val index = events.indexOfFirst { it.eventId == args.eventId }
+ initialIndex = index
+
+ val sourceProvider = dataSourceFactory.createProvider(events, index)
+ sourceProvider.interactionListener = this
+ setSourceProvider(sourceProvider)
+ this.currentSourceProvider = sourceProvider
+ if (savedInstanceState == null) {
+ pager2.setCurrentItem(index, false)
+ // The page change listener is not notified of the change...
+ pager2.post {
+ onSelectedPositionChanged(index)
+ }
+ }
+ }
+
+ window.statusBarColor = ContextCompat.getColor(this, R.color.black_alpha)
+ window.navigationBarColor = ContextCompat.getColor(this, R.color.black_alpha)
+ }
+
+ private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview
+
+ override fun shouldAnimateDismiss(): Boolean {
+ return currentPosition != initialIndex
+ }
+
+ override fun onBackPressed() {
+ if (currentPosition == initialIndex) {
+ // show back the transition view
+ // TODO, we should track and update the mapping
+ transitionImageContainer.isVisible = true
+ }
+ isAnimatingOut = true
+ super.onBackPressed()
+ }
+
+ override fun animateClose() {
+ if (currentPosition == initialIndex) {
+ // show back the transition view
+ // TODO, we should track and update the mapping
+ transitionImageContainer.isVisible = true
+ }
+ isAnimatingOut = true
+ ActivityCompat.finishAfterTransition(this)
+ }
+
+ // ==========================================================================================
+ // PRIVATE METHODS
+ // ==========================================================================================
+
+ /**
+ * Try and add a [Transition.TransitionListener] to the entering shared element
+ * [Transition]. We do this so that we can load the full-size image after the transition
+ * has completed.
+ *
+ * @return true if we were successful in adding a listener to the enter transition
+ */
+ private fun addTransitionListener(): Boolean {
+ val transition = window.sharedElementEnterTransition
+
+ if (transition != null) {
+ // There is an entering shared element transition so add a listener to it
+ transition.addListener(
+ onEnd = {
+ // The listener is also called when we are exiting
+ // so we use a boolean to avoid reshowing pager at end of dismiss transition
+ if (!isAnimatingOut) {
+ transitionImageContainer.isVisible = false
+ pager2.isInvisible = false
+ }
+ },
+ onCancel = {
+ if (!isAnimatingOut) {
+ transitionImageContainer.isVisible = false
+ pager2.isInvisible = false
+ }
+ }
+ )
+ return true
+ }
+
+ // If we reach here then we have not added a listener
+ return false
+ }
+
+ private fun args() = intent.getParcelableExtra(EXTRA_ARGS)
+
+ private fun getVectorComponent(): VectorComponent {
+ return (application as HasVectorInjector).injector()
+ }
+
+ private fun scheduleStartPostponedTransition(sharedElement: View) {
+ sharedElement.viewTreeObserver.addOnPreDrawListener(
+ object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ sharedElement.viewTreeObserver.removeOnPreDrawListener(this)
+ supportStartPostponedEnterTransition()
+ return true
+ }
+ })
+ }
+
+ companion object {
+ const val EXTRA_ARGS = "EXTRA_ARGS"
+ const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA"
+ const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA"
+
+ fun newIntent(context: Context,
+ mediaData: AttachmentData,
+ roomId: String?,
+ eventId: String,
+ inMemoryData: List,
+ sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also {
+ it.putExtra(EXTRA_ARGS, Args(roomId, eventId, sharedTransitionName))
+ it.putExtra(EXTRA_IMAGE_DATA, mediaData)
+ if (inMemoryData.isNotEmpty()) {
+ it.putParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA, ArrayList(inMemoryData))
+ }
+ }
+ }
+
+ override fun onDismissTapped() {
+ animateClose()
+ }
+
+ override fun onPlayPause(play: Boolean) {
+ handle(if (play) AttachmentCommands.StartVideo else AttachmentCommands.PauseVideo)
+ }
+
+ override fun videoSeekTo(percent: Int) {
+ handle(AttachmentCommands.SeekTo(percent))
+ }
+
+ override fun onShareTapped() {
+ this.currentSourceProvider?.getFileForSharing(currentPosition) { data ->
+ if (data != null && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
+ shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri()))
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt
index eb9105f792..e6dec88349 100644
--- a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt
+++ b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt
@@ -16,7 +16,6 @@
package im.vector.riotx.features.media
-import android.os.Parcelable
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
@@ -38,13 +37,13 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
@Parcelize
data class Data(
- val eventId: String,
- val filename: String,
- val mimeType: String?,
- val url: String?,
- val elementToDecrypt: ElementToDecrypt?,
+ override val eventId: String,
+ override val filename: String,
+ override val mimeType: String?,
+ override val url: String?,
+ override val elementToDecrypt: ElementToDecrypt?,
val thumbnailMediaData: ImageContentRenderer.Data
- ) : Parcelable
+ ) : AttachmentData
fun render(data: Data,
thumbnailView: ImageView,
@@ -70,7 +69,7 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
id = data.eventId,
fileName = data.filename,
- mimeType = null,
+ mimeType = data.mimeType,
url = data.url,
elementToDecrypt = data.elementToDecrypt,
callback = object : MatrixCallback {
diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt
index 6ef8927f00..d9df861a25 100644
--- a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt
@@ -79,13 +79,13 @@ class VideoMediaViewerActivity : VectorBaseActivity() {
private fun onShareActionClicked() {
session.fileService().downloadFile(
- FileService.DownloadMode.FOR_EXTERNAL_SHARE,
- mediaData.eventId,
- mediaData.filename,
- mediaData.mimeType,
- mediaData.url,
- mediaData.elementToDecrypt,
- object : MatrixCallback {
+ downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
+ id = mediaData.eventId,
+ fileName = mediaData.filename,
+ mimeType = mediaData.mimeType,
+ url = mediaData.url,
+ elementToDecrypt = mediaData.elementToDecrypt,
+ callback = object : MatrixCallback {
override fun onSuccess(data: File) {
shareMedia(this@VideoMediaViewerActivity, data, getMimeTypeFromUri(this@VideoMediaViewerActivity, data.toUri()))
}
diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
index 7b3eedc71f..3df94ba674 100644
--- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
+++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
@@ -19,7 +19,6 @@ package im.vector.riotx.features.navigation
import android.app.Activity
import android.content.Context
import android.content.Intent
-import android.os.Build
import android.view.View
import android.view.Window
import androidx.core.app.ActivityOptionsCompat
@@ -50,11 +49,9 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.invite.InviteUsersToRoomActivity
+import im.vector.riotx.features.media.AttachmentData
import im.vector.riotx.features.media.BigImageViewerActivity
-import im.vector.riotx.features.media.ImageContentRenderer
-import im.vector.riotx.features.media.ImageMediaViewerActivity
-import im.vector.riotx.features.media.VideoContentRenderer
-import im.vector.riotx.features.media.VideoMediaViewerActivity
+import im.vector.riotx.features.media.VectorAttachmentViewerActivity
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
@@ -90,7 +87,8 @@ class DefaultNavigator @Inject constructor(
override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) {
val session = sessionHolder.getSafeActiveSession() ?: return
- val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) ?: return
+ val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId)
+ ?: return
(tx as? IncomingSasVerificationTransaction)?.performAccept()
if (context is VectorBaseActivity) {
VerificationBottomSheet.withArgs(
@@ -117,6 +115,27 @@ class DefaultNavigator @Inject constructor(
}
}
+ override fun requestSelfSessionVerification(context: Context) {
+ val session = sessionHolder.getSafeActiveSession() ?: return
+ val otherSessions = session.cryptoService()
+ .getCryptoDeviceInfo(session.myUserId)
+ .filter { it.deviceId != session.sessionParams.deviceId }
+ .map { it.deviceId }
+ if (context is VectorBaseActivity) {
+ if (otherSessions.isNotEmpty()) {
+ val pr = session.cryptoService().verificationService().requestKeyVerification(
+ supportedVerificationMethodsProvider.provide(),
+ session.myUserId,
+ otherSessions)
+ VerificationBottomSheet.forSelfVerification(session, pr.transactionId ?: pr.localId)
+ .show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
+ } else {
+ VerificationBottomSheet.forSelfVerification(session)
+ .show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
+ }
+ }
+ }
+
override fun waitSessionVerification(context: Context) {
val session = sessionHolder.getSafeActiveSession() ?: return
if (context is VectorBaseActivity) {
@@ -200,7 +219,14 @@ class DefaultNavigator @Inject constructor(
}
override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) {
- context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
+ // if cross signing is enabled we should propose full 4S
+ sessionHolder.getSafeActiveSession()?.let { session ->
+ if (session.cryptoService().crossSigningService().canCrossSign() && context is VectorBaseActivity) {
+ BootstrapBottomSheet.show(context.supportFragmentManager, false)
+ } else {
+ context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
+ }
+ }
}
override fun openKeysBackupManager(context: Context) {
@@ -217,7 +243,8 @@ class DefaultNavigator @Inject constructor(
?.let { avatarUrl ->
val intent = BigImageViewerActivity.newIntent(activity, matrixItem.getBestName(), avatarUrl)
val options = sharedElement?.let {
- ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it) ?: "")
+ ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it)
+ ?: "")
}
activity.startActivity(intent, options?.toBundle())
}
@@ -245,27 +272,32 @@ class DefaultNavigator @Inject constructor(
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
}
- override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) {
- val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view))
- val pairs = ArrayList>()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ override fun openMediaViewer(activity: Activity,
+ roomId: String,
+ mediaData: AttachmentData,
+ view: View,
+ inMemory: List,
+ options: ((MutableList>) -> Unit)?) {
+ VectorAttachmentViewerActivity.newIntent(activity,
+ mediaData,
+ roomId,
+ mediaData.eventId,
+ inMemory,
+ ViewCompat.getTransitionName(view)).let { intent ->
+ val pairs = ArrayList>()
activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let {
pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
}
activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let {
pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME))
}
+
+ pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
+ options?.invoke(pairs)
+
+ val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle()
+ activity.startActivity(intent, bundle)
}
- pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
- options?.invoke(pairs)
-
- val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle()
- activity.startActivity(intent, bundle)
- }
-
- override fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) {
- val intent = VideoMediaViewerActivity.newIntent(activity, mediaData)
- activity.startActivity(intent)
}
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
index 08fd63b93c..2d817183be 100644
--- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
+++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
@@ -24,11 +24,10 @@ import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.matrix.android.api.session.terms.TermsService
-import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.session.widgets.model.Widget
+import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
-import im.vector.riotx.features.media.ImageContentRenderer
-import im.vector.riotx.features.media.VideoContentRenderer
+import im.vector.riotx.features.media.AttachmentData
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.terms.ReviewTermsActivity
@@ -41,6 +40,8 @@ interface Navigator {
fun requestSessionVerification(context: Context, otherSessionId: String)
+ fun requestSelfSessionVerification(context: Context)
+
fun waitSessionVerification(context: Context)
fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean)
@@ -92,7 +93,10 @@ interface Navigator {
fun openRoomWidget(context: Context, roomId: String, widget: Widget)
- fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?)
-
- fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data)
+ fun openMediaViewer(activity: Activity,
+ roomId: String,
+ mediaData: AttachmentData,
+ view: View,
+ inMemory: List = emptyList(),
+ options: ((MutableList>) -> Unit)?)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
index 6fc396b264..d0839795dd 100644
--- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
+++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
@@ -22,10 +22,11 @@ import android.os.HandlerThread
import androidx.annotation.WorkerThread
import androidx.core.app.NotificationCompat
import androidx.core.app.Person
+import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentUrlResolver
+import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.BuildConfig
import im.vector.riotx.R
-import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.settings.VectorPreferences
import me.gujun.android.span.span
@@ -46,7 +47,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
private val notificationUtils: NotificationUtils,
private val vectorPreferences: VectorPreferences,
private val stringProvider: StringProvider,
- private val activeSessionHolder: ActiveSessionHolder,
+ private val activeSessionDataSource: ActiveSessionDataSource,
private val iconLoader: IconLoader,
private val bitmapLoader: BitmapLoader,
private val outdatedDetector: OutdatedEventDetector?) {
@@ -68,6 +69,10 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
private var currentRoomId: String? = null
+ // TODO Multi-session: this will have to be improved
+ private val currentSession: Session?
+ get() = activeSessionDataSource.currentValue?.orNull()
+
/**
Should be called as soon as a new event is ready to be displayed.
The notification corresponding to this event will not be displayed until
@@ -204,7 +209,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
private fun refreshNotificationDrawerBg() {
Timber.v("refreshNotificationDrawerBg()")
- val session = activeSessionHolder.getSafeActiveSession() ?: return
+ val session = currentSession ?: return
val user = session.getUser(session.myUserId)
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
@@ -474,7 +479,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (!file.exists()) file.createNewFile()
FileOutputStream(file).use {
- activeSessionHolder.getSafeActiveSession()?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it)
+ currentSession?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it)
}
} catch (e: Throwable) {
Timber.e(e, "## Failed to save cached notification info")
@@ -487,7 +492,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (file.exists()) {
FileInputStream(file).use {
- val events: ArrayList? = activeSessionHolder.getSafeActiveSession()?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
+ val events: ArrayList? = currentSession?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
if (events != null) {
return events.toMutableList()
}
diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/OutdatedEventDetector.kt b/vector/src/main/java/im/vector/riotx/features/notifications/OutdatedEventDetector.kt
index 6b8d3dae49..d2b939bc99 100644
--- a/vector/src/main/java/im/vector/riotx/features/notifications/OutdatedEventDetector.kt
+++ b/vector/src/main/java/im/vector/riotx/features/notifications/OutdatedEventDetector.kt
@@ -15,10 +15,12 @@
*/
package im.vector.riotx.features.notifications
-import im.vector.riotx.core.di.ActiveSessionHolder
+import im.vector.riotx.ActiveSessionDataSource
import javax.inject.Inject
-class OutdatedEventDetector @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) {
+class OutdatedEventDetector @Inject constructor(
+ private val activeSessionDataSource: ActiveSessionDataSource
+) {
/**
* Returns true if the given event is outdated.
@@ -26,10 +28,12 @@ class OutdatedEventDetector @Inject constructor(private val activeSessionHolder:
* other device.
*/
fun isMessageOutdated(notifiableEvent: NotifiableEvent): Boolean {
+ val session = activeSessionDataSource.currentValue?.orNull() ?: return false
+
if (notifiableEvent is NotifiableMessageEvent) {
val eventID = notifiableEvent.eventId
val roomID = notifiableEvent.roomId
- val room = activeSessionHolder.getSafeActiveSession()?.getRoom(roomID) ?: return false
+ val room = session.getRoom(roomID) ?: return false
return room.isEventRead(eventID)
}
return false
diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt
index 4ba89c02e2..adef246151 100644
--- a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt
+++ b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt
@@ -30,17 +30,17 @@ class PushRuleTriggerListener @Inject constructor(
private val notificationDrawerManager: NotificationDrawerManager
) : PushRuleService.PushRuleListener {
- var session: Session? = null
+ private var session: Session? = null
override fun onMatchRule(event: Event, actions: List) {
Timber.v("Push rule match for event ${event.eventId}")
- if (session == null) {
+ val safeSession = session ?: return Unit.also {
Timber.e("Called without active session")
- return
}
+
val notificationAction = actions.toNotificationAction()
if (notificationAction.shouldNotify) {
- val notifiableEvent = resolver.resolveEvent(event, session!!)
+ val notifiableEvent = resolver.resolveEvent(event, safeSession)
if (notifiableEvent == null) {
Timber.v("## Failed to resolve event")
// TODO
diff --git a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
index 78a0cece41..e5b2f34f61 100644
--- a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
+++ b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
@@ -26,6 +26,7 @@ import com.tapadoo.alerter.Alerter
import com.tapadoo.alerter.OnHideAlertListener
import dagger.Lazy
import im.vector.riotx.R
+import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.themes.ThemeUtils
import timber.log.Timber
@@ -83,7 +84,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy {
session.fileService().downloadFile(
- FileService.DownloadMode.FOR_EXTERNAL_SHARE,
- action.uploadEvent.eventId,
- action.uploadEvent.contentWithAttachmentContent.body,
- action.uploadEvent.contentWithAttachmentContent.getFileUrl(),
- action.uploadEvent.contentWithAttachmentContent.mimeType,
- action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(),
- it)
+ downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
+ id = action.uploadEvent.eventId,
+ fileName = action.uploadEvent.contentWithAttachmentContent.body,
+ mimeType = action.uploadEvent.contentWithAttachmentContent.mimeType,
+ url = action.uploadEvent.contentWithAttachmentContent.getFileUrl(),
+ elementToDecrypt = action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(),
+ callback = it)
}
_viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body))
} catch (failure: Throwable) {
diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt
index a4e6c61238..dda070bf48 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt
@@ -20,23 +20,34 @@ import android.os.Bundle
import android.util.DisplayMetrics
import android.view.View
import androidx.core.content.ContextCompat
+import androidx.core.util.Pair
+import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.GridLayoutManager
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
+import com.google.android.material.appbar.AppBarLayout
+import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
+import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
+import im.vector.matrix.android.api.session.room.model.message.getFileUrl
+import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.trackItemsVisibilityChange
import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.DimensionConverter
+import im.vector.riotx.features.media.AttachmentData
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsAction
+import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel
+import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewState
import kotlinx.android.synthetic.main.fragment_generic_state_view_recycler.*
+import kotlinx.android.synthetic.main.fragment_room_uploads.*
import javax.inject.Inject
class RoomUploadsMediaFragment @Inject constructor(
@@ -76,12 +87,86 @@ class RoomUploadsMediaFragment @Inject constructor(
controller.listener = null
}
- override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) {
- navigator.openImageViewer(requireActivity(), mediaData, view, null)
+ // It's very strange i can't just access
+ // the app bar using find by id...
+ private fun trickFindAppBar(): AppBarLayout? {
+ return activity?.supportFragmentManager?.fragments
+ ?.filterIsInstance()
+ ?.firstOrNull()
+ ?.roomUploadsAppBar
}
- override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) {
- navigator.openVideoViewer(requireActivity(), mediaData)
+ override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) = withState(uploadsViewModel) { state ->
+ val inMemory = getItemsArgs(state)
+ navigator.openMediaViewer(
+ activity = requireActivity(),
+ roomId = state.roomId,
+ mediaData = mediaData,
+ view = view,
+ inMemory = inMemory
+ ) { pairs ->
+ trickFindAppBar()?.let {
+ pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: ""))
+ }
+ }
+ }
+
+ private fun getItemsArgs(state: RoomUploadsViewState): List {
+ return state.mediaEvents.mapNotNull {
+ when (val content = it.contentWithAttachmentContent) {
+ is MessageImageContent -> {
+ ImageContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.getFileUrl(),
+ elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
+ maxHeight = -1,
+ maxWidth = -1,
+ width = null,
+ height = null
+ )
+ }
+ is MessageVideoContent -> {
+ val thumbnailData = ImageContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.videoInfo?.thumbnailFile?.url
+ ?: content.videoInfo?.thumbnailUrl,
+ elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(),
+ height = content.videoInfo?.height,
+ maxHeight = -1,
+ width = content.videoInfo?.width,
+ maxWidth = -1
+ )
+ VideoContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.getFileUrl(),
+ elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
+ thumbnailMediaData = thumbnailData
+ )
+ }
+ else -> null
+ }
+ }
+ }
+
+ override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) = withState(uploadsViewModel) { state ->
+ val inMemory = getItemsArgs(state)
+ navigator.openMediaViewer(
+ activity = requireActivity(),
+ roomId = state.roomId,
+ mediaData = mediaData,
+ view = view,
+ inMemory = inMemory
+ ) { pairs ->
+ trickFindAppBar()?.let {
+ pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: ""))
+ }
+ }
}
override fun loadMore() {
diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt
index 98026901cc..3b83e99656 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt
@@ -18,11 +18,13 @@ package im.vector.riotx.features.roomprofile.uploads.media
import android.view.View
import android.widget.ImageView
+import androidx.core.view.ViewCompat
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
+import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.media.ImageContentRenderer
@EpoxyModelClass(layout = R.layout.item_uploads_image)
@@ -35,8 +37,13 @@ abstract class UploadsImageItem : VectorEpoxyModel() {
override fun bind(holder: Holder) {
super.bind(holder)
- holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) }
+ holder.view.setOnClickListener(
+ DebouncedClickListener(View.OnClickListener { _ ->
+ listener?.onItemClicked(holder.imageView, data)
+ })
+ )
imageContentRenderer.render(data, holder.imageView, IMAGE_SIZE_DP)
+ ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}")
}
class Holder : VectorEpoxyHolder() {
diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt
index 82e33b76da..f20f6ed5b1 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt
@@ -18,11 +18,13 @@ package im.vector.riotx.features.roomprofile.uploads.media
import android.view.View
import android.widget.ImageView
+import androidx.core.view.ViewCompat
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
+import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
@@ -36,8 +38,13 @@ abstract class UploadsVideoItem : VectorEpoxyModel() {
override fun bind(holder: Holder) {
super.bind(holder)
- holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) }
+ holder.view.setOnClickListener(
+ DebouncedClickListener(View.OnClickListener { _ ->
+ listener?.onItemClicked(holder.imageView, data)
+ })
+ )
imageContentRenderer.render(data.thumbnailMediaData, holder.imageView, IMAGE_SIZE_DP)
+ ViewCompat.setTransitionName(holder.imageView, "videoPreview_${id()}")
}
class Holder : VectorEpoxyHolder() {
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
index 2b9338ccc8..3c2acb1693 100644
--- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
@@ -34,16 +34,16 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.R
+import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.dialogs.ExportKeysDialog
+import im.vector.riotx.core.extensions.queryExportKeys
import im.vector.riotx.core.intent.ExternalIntentData
import im.vector.riotx.core.intent.analyseIntent
import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.SimpleTextWatcher
import im.vector.riotx.core.preference.VectorPreference
-import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
import im.vector.riotx.core.utils.allGranted
-import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.openFileSelection
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keys.KeysExporter
@@ -52,7 +52,8 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActiv
import javax.inject.Inject
class VectorSettingsSecurityPrivacyFragment @Inject constructor(
- private val vectorPreferences: VectorPreferences
+ private val vectorPreferences: VectorPreferences,
+ private val activeSessionHolder: ActiveSessionHolder
) : VectorSettingsBaseFragment() {
override var titleRes = R.string.settings_security_and_privacy
@@ -119,38 +120,69 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
}
private fun refreshXSigningStatus() {
- val xSigningIsEnableInAccount = session.cryptoService().crossSigningService().isCrossSigningInitialized()
- val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
- val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
+ val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
+ val xSigningIsEnableInAccount = crossSigningKeys != null
+ val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
+ val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
- if (xSigningKeyCanSign) {
- mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
- mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
- } else if (xSigningKeysAreTrusted) {
- mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom)
- mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
- } else if (xSigningIsEnableInAccount) {
- mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black)
- mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted)
- } else {
- mCrossSigningStatePreference.setIcon(android.R.color.transparent)
- mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled)
- }
+ if (xSigningKeyCanSign) {
+ mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
+ mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
+ } else if (xSigningKeysAreTrusted) {
+ mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom)
+ mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
+ } else if (xSigningIsEnableInAccount) {
+ mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black)
+ mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted)
+ } else {
+ mCrossSigningStatePreference.setIcon(android.R.color.transparent)
+ mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled)
+ }
- mCrossSigningStatePreference.isVisible = true
+ mCrossSigningStatePreference.isVisible = true
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
- exportKeys()
+ queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
+ if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {
+ val uri = data?.data
+ if (resultCode == Activity.RESULT_OK && uri != null) {
+ activity?.let { activity ->
+ ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
+ override fun onPassphrase(passphrase: String) {
+ displayLoadingView()
+ KeysExporter(session)
+ .export(requireContext(),
+ passphrase,
+ uri,
+ object : MatrixCallback {
+ override fun onSuccess(data: Boolean) {
+ if (data) {
+ requireActivity().toast(getString(R.string.encryption_exported_successfully))
+ } else {
+ requireActivity().toast(getString(R.string.unexpected_error))
+ }
+ hideLoadingView()
+ }
+
+ override fun onFailure(failure: Throwable) {
+ onCommonDone(failure.localizedMessage)
+ }
+ })
+ }
+ })
+ }
+ }
+ }
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
REQUEST_E2E_FILE_REQUEST_CODE -> importKeys(data)
@@ -169,7 +201,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
}
exportPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
- exportKeys()
+ queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT)
true
}
@@ -179,46 +211,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
}
}
- /**
- * Manage the e2e keys export.
- */
- private fun exportKeys() {
- // We need WRITE_EXTERNAL permission
- if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
- this,
- PERMISSION_REQUEST_CODE_EXPORT_KEYS,
- R.string.permissions_rationale_msg_keys_backup_export)) {
- activity?.let { activity ->
- ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
- override fun onPassphrase(passphrase: String) {
- displayLoadingView()
-
- KeysExporter(session)
- .export(requireContext(),
- passphrase,
- object : MatrixCallback {
- override fun onSuccess(data: String) {
- if (isAdded) {
- hideLoadingView()
-
- AlertDialog.Builder(activity)
- .setMessage(getString(R.string.encryption_export_saved_as, data))
- .setCancelable(false)
- .setPositiveButton(R.string.ok, null)
- .show()
- }
- }
-
- override fun onFailure(failure: Throwable) {
- onCommonDone(failure.localizedMessage)
- }
- })
- }
- })
- }
- }
- }
-
/**
* Manage the e2e keys import.
*/
@@ -515,6 +507,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
companion object {
private const val REQUEST_E2E_FILE_REQUEST_CODE = 123
+ private const val REQUEST_CODE_SAVE_MEGOLM_EXPORT = 124
private const val PUSHER_PREFERENCE_KEY_BASE = "PUSHER_PREFERENCE_KEY_BASE"
private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE"
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt
index 37d9677f7f..5778d05d1c 100644
--- a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt
@@ -51,7 +51,7 @@ class CrossSigningSettingsFragment @Inject constructor(
Unit
}
CrossSigningSettingsViewEvents.VerifySession -> {
- navigator.waitSessionVerification(requireActivity())
+ navigator.requestSelfSessionVerification(requireActivity())
}
CrossSigningSettingsViewEvents.SetUpRecovery -> {
navigator.upgradeSessionSecurity(requireActivity(), false)
diff --git a/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt b/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt
index b37c1a4818..b29e60784e 100644
--- a/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt
+++ b/vector/src/main/java/im/vector/riotx/features/themes/ActivityOtherThemes.kt
@@ -38,4 +38,10 @@ sealed class ActivityOtherThemes(@StyleRes val dark: Int,
R.style.AppTheme_AttachmentsPreview,
R.style.AppTheme_AttachmentsPreview
)
+
+ object VectorAttachmentsPreview : ActivityOtherThemes(
+ R.style.AppTheme_Transparent,
+ R.style.AppTheme_Transparent,
+ R.style.AppTheme_Transparent
+ )
}
diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt
new file mode 100644
index 0000000000..dca98c16b2
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.workers.signout
+
+import androidx.lifecycle.MutableLiveData
+import com.airbnb.mvrx.ActivityViewModelContext
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.FragmentViewModelContext
+import com.airbnb.mvrx.MvRxState
+import com.airbnb.mvrx.MvRxViewModelFactory
+import com.airbnb.mvrx.Uninitialized
+import com.airbnb.mvrx.ViewModelContext
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
+import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
+import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
+import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
+import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
+import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
+import im.vector.matrix.android.api.util.Optional
+import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
+import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
+import im.vector.matrix.rx.rx
+import im.vector.riotx.core.platform.EmptyAction
+import im.vector.riotx.core.platform.EmptyViewEvents
+import im.vector.riotx.core.platform.VectorViewModel
+import io.reactivex.Observable
+import io.reactivex.functions.Function4
+import io.reactivex.subjects.PublishSubject
+import java.util.concurrent.TimeUnit
+
+data class ServerBackupStatusViewState(
+ val bannerState: Async = Uninitialized
+) : MvRxState
+
+/**
+ * The state representing the view
+ * It can take one state at a time
+ */
+sealed class BannerState {
+
+ object Hidden : BannerState()
+
+ // Keys backup is not setup, numberOfKeys is the number of locally stored keys
+ data class Setup(val numberOfKeys: Int) : BannerState()
+
+ // Keys are backing up
+ object BackingUp : BannerState()
+}
+
+class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialState: ServerBackupStatusViewState,
+ private val session: Session)
+ : VectorViewModel(initialState), KeysBackupStateListener {
+
+ @AssistedInject.Factory
+ interface Factory {
+ fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel
+ }
+
+ companion object : MvRxViewModelFactory {
+
+ @JvmStatic
+ override fun create(viewModelContext: ViewModelContext, state: ServerBackupStatusViewState): ServerBackupStatusViewModel? {
+ val factory = when (viewModelContext) {
+ is FragmentViewModelContext -> viewModelContext.fragment as? Factory
+ is ActivityViewModelContext -> viewModelContext.activity as? Factory
+ }
+ return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
+ }
+ }
+
+ // Keys exported manually
+ val keysExportedToFile = MutableLiveData()
+ val keysBackupState = MutableLiveData()
+
+ private val keyBackupPublishSubject: PublishSubject = PublishSubject.create()
+
+ init {
+ session.cryptoService().keysBackupService().addListener(this)
+
+ keysBackupState.value = session.cryptoService().keysBackupService().state
+
+ Observable.combineLatest, Optional, KeysBackupState, Optional, BannerState>(
+ session.rx().liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)),
+ session.rx().liveCrossSigningInfo(session.myUserId),
+ keyBackupPublishSubject,
+ session.rx().liveCrossSigningPrivateKeys(),
+ Function4 { _, crossSigningInfo, keyBackupState, pInfo ->
+ // first check if 4S is already setup
+ if (session.sharedSecretStorageService.isRecoverySetup()) {
+ // 4S is already setup sp we should not display anything
+ return@Function4 when (keyBackupState) {
+ KeysBackupState.BackingUp -> BannerState.BackingUp
+ else -> BannerState.Hidden
+ }
+ }
+
+ // So recovery is not setup
+ // Check if cross signing is enabled and local secrets known
+ if (crossSigningInfo.getOrNull()?.isTrusted() == true
+ && pInfo.getOrNull()?.master != null
+ && pInfo.getOrNull()?.selfSigned != null
+ && pInfo.getOrNull()?.user != null
+ ) {
+ // So 4S is not setup and we have local secrets,
+ return@Function4 BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup())
+ }
+
+ BannerState.Hidden
+ }
+ )
+ .throttleLast(1000, TimeUnit.MILLISECONDS) // we don't want to flicker or catch transient states
+ .distinctUntilChanged()
+ .execute { async ->
+ copy(
+ bannerState = async
+ )
+ }
+
+ keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state)
+ }
+
+ /**
+ * Safe way to get the current KeysBackup version
+ */
+ fun getCurrentBackupVersion(): String {
+ return session.cryptoService().keysBackupService().currentBackupVersion ?: ""
+ }
+
+ /**
+ * Safe way to get the number of keys to backup
+ */
+ fun getNumberOfKeysToBackup(): Int {
+ return session.cryptoService().inboundGroupSessionsCount(false)
+ }
+
+ /**
+ * Safe way to tell if there are more keys on the server
+ */
+ fun canRestoreKeys(): Boolean {
+ return session.cryptoService().keysBackupService().canRestoreKeys()
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ session.cryptoService().keysBackupService().removeListener(this)
+ }
+
+ override fun onStateChange(newState: KeysBackupState) {
+ keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state)
+ keysBackupState.value = newState
+ }
+
+ fun refreshRemoteStateIfNeeded() {
+ if (keysBackupState.value == KeysBackupState.Disabled) {
+ session.cryptoService().keysBackupService().checkAndStartKeysBackup()
+ }
+ }
+
+ override fun handle(action: EmptyAction) {}
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt
index e1ef7bc07b..16be661f06 100644
--- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt
@@ -28,19 +28,27 @@ import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
-import androidx.lifecycle.Observer
-import androidx.transition.TransitionManager
import butterknife.BindView
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.fragmentViewModel
+import com.airbnb.mvrx.withState
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
+import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.riotx.R
+import im.vector.riotx.core.di.ScreenComponent
+import im.vector.riotx.core.dialogs.ExportKeysDialog
+import im.vector.riotx.core.extensions.queryExportKeys
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
-import im.vector.riotx.core.utils.toast
-import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity
+import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet
+import timber.log.Timber
+import javax.inject.Inject
-class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
+// TODO this needs to be refactored to current standard and remove legacy
+class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), SignoutCheckViewModel.Factory {
@BindView(R.id.bottom_sheet_signout_warning_text)
lateinit var sheetTitle: TextView
@@ -48,14 +56,20 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
@BindView(R.id.bottom_sheet_signout_backingup_status_group)
lateinit var backingUpStatusGroup: ViewGroup
- @BindView(R.id.keys_backup_setup)
- lateinit var setupClickableView: View
+ @BindView(R.id.setupRecoveryButton)
+ lateinit var setupRecoveryButton: SignoutBottomSheetActionButton
- @BindView(R.id.keys_backup_activate)
- lateinit var activateClickableView: View
+ @BindView(R.id.setupMegolmBackupButton)
+ lateinit var setupMegolmBackupButton: SignoutBottomSheetActionButton
- @BindView(R.id.keys_backup_dont_want)
- lateinit var dontWantClickableView: View
+ @BindView(R.id.exportManuallyButton)
+ lateinit var exportManuallyButton: SignoutBottomSheetActionButton
+
+ @BindView(R.id.exitAnywayButton)
+ lateinit var exitAnywayButton: SignoutBottomSheetActionButton
+
+ @BindView(R.id.signOutButton)
+ lateinit var signOutButton: SignoutBottomSheetActionButton
@BindView(R.id.bottom_sheet_signout_icon_progress_bar)
lateinit var backupProgress: ProgressBar
@@ -66,8 +80,8 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
@BindView(R.id.bottom_sheet_backup_status_text)
lateinit var backupStatusTex: TextView
- @BindView(R.id.bottom_sheet_signout_button)
- lateinit var signoutClickableView: View
+ @BindView(R.id.signoutExportingLoading)
+ lateinit var signoutExportingLoading: View
@BindView(R.id.root_layout)
lateinit var rootLayout: ViewGroup
@@ -78,62 +92,44 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
fun newInstance() = SignOutBottomSheetDialogFragment()
private const val EXPORT_REQ = 0
+ private const val QUERY_EXPORT_KEYS = 1
}
init {
isCancelable = true
}
- private lateinit var viewModel: SignOutViewModel
+ @Inject
+ lateinit var viewModelFactory: SignoutCheckViewModel.Factory
+
+ override fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel {
+ return viewModelFactory.create(initialState)
+ }
+
+ private val viewModel: SignoutCheckViewModel by fragmentViewModel(SignoutCheckViewModel::class)
+
+ override fun injectWith(injector: ScreenComponent) {
+ injector.inject(this)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ viewModel.refreshRemoteStateIfNeeded()
+ }
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
- viewModel = fragmentViewModelProvider.get(SignOutViewModel::class.java)
-
- setupClickableView.setOnClickListener {
- context?.let { context ->
- startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ)
- }
+ setupRecoveryButton.action = {
+ BootstrapBottomSheet.show(parentFragmentManager, false)
}
- activateClickableView.setOnClickListener {
- context?.let { context ->
- startActivity(KeysBackupManageActivity.intent(context))
- }
- }
-
- signoutClickableView.setOnClickListener {
- this.onSignOut?.run()
- }
-
- dontWantClickableView.setOnClickListener { _ ->
+ exitAnywayButton.action = {
context?.let {
AlertDialog.Builder(it)
.setTitle(R.string.are_you_sure)
.setMessage(R.string.sign_out_bottom_sheet_will_lose_secure_messages)
- .setPositiveButton(R.string.backup) { _, _ ->
- when (viewModel.keysBackupState.value) {
- KeysBackupState.NotTrusted -> {
- context?.let { context ->
- startActivity(KeysBackupManageActivity.intent(context))
- }
- }
- KeysBackupState.Disabled -> {
- context?.let { context ->
- startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ)
- }
- }
- KeysBackupState.BackingUp,
- KeysBackupState.WillBackUp -> {
- // keys are already backing up please wait
- context?.toast(R.string.keys_backup_is_not_finished_please_wait)
- }
- else -> {
- // nop
- }
- }
- }
+ .setPositiveButton(R.string.backup, null)
.setNegativeButton(R.string.action_sign_out) { _, _ ->
onSignOut?.run()
}
@@ -141,71 +137,143 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
}
}
- viewModel.keysExportedToFile.observe(viewLifecycleOwner, Observer {
- val hasExportedToFile = it ?: false
- if (hasExportedToFile) {
- // We can allow to sign out
-
- sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
-
- signoutClickableView.isVisible = true
- dontWantClickableView.isVisible = false
- setupClickableView.isVisible = false
- activateClickableView.isVisible = false
- backingUpStatusGroup.isVisible = false
+ exportManuallyButton.action = {
+ withState(viewModel) { state ->
+ queryExportKeys(state.userId, QUERY_EXPORT_KEYS)
}
- })
+ }
- viewModel.keysBackupState.observe(viewLifecycleOwner, Observer {
- if (viewModel.keysExportedToFile.value == true) {
- // ignore this
- return@Observer
- }
- TransitionManager.beginDelayedTransition(rootLayout)
+ setupMegolmBackupButton.action = {
+ startActivityForResult(KeysBackupSetupActivity.intent(requireContext(), true), EXPORT_REQ)
+ }
+
+ viewModel.observeViewEvents {
when (it) {
- KeysBackupState.ReadyToBackUp -> {
- signoutClickableView.isVisible = true
- dontWantClickableView.isVisible = false
- setupClickableView.isVisible = false
- activateClickableView.isVisible = false
- backingUpStatusGroup.isVisible = true
+ is SignoutCheckViewModel.ViewEvents.ExportKeys -> {
+ it.exporter
+ .export(requireContext(),
+ it.passphrase,
+ it.uri,
+ object : MatrixCallback {
+ override fun onSuccess(data: Boolean) {
+ if (data) {
+ viewModel.handle(SignoutCheckViewModel.Actions.KeySuccessfullyManuallyExported)
+ } else {
+ viewModel.handle(SignoutCheckViewModel.Actions.KeyExportFailed)
+ }
+ }
+ override fun onFailure(failure: Throwable) {
+ Timber.e("## Failed to export manually keys ${failure.localizedMessage}")
+ viewModel.handle(SignoutCheckViewModel.Actions.KeyExportFailed)
+ }
+ })
+ }
+ }
+ }
+ }
+
+ override fun invalidate() = withState(viewModel) { state ->
+ signoutExportingLoading.isVisible = false
+ if (state.crossSigningSetupAllKeysKnown && !state.backupIsSetup) {
+ sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup)
+ backingUpStatusGroup.isVisible = false
+ // we should show option to setup 4S
+ setupRecoveryButton.isVisible = true
+ setupMegolmBackupButton.isVisible = false
+ signOutButton.isVisible = false
+ // We let the option to ignore and quit
+ exportManuallyButton.isVisible = true
+ exitAnywayButton.isVisible = true
+ } else if (state.keysBackupState == KeysBackupState.Unknown || state.keysBackupState == KeysBackupState.Disabled) {
+ sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup)
+ backingUpStatusGroup.isVisible = false
+ // no key backup and cannot setup full 4S
+ // we propose to setup
+ // we should show option to setup 4S
+ setupRecoveryButton.isVisible = false
+ setupMegolmBackupButton.isVisible = true
+ signOutButton.isVisible = false
+ // We let the option to ignore and quit
+ exportManuallyButton.isVisible = true
+ exitAnywayButton.isVisible = true
+ } else {
+ // so keybackup is setup
+ // You should wait until all are uploaded
+ setupRecoveryButton.isVisible = false
+
+ when (state.keysBackupState) {
+ KeysBackupState.ReadyToBackUp -> {
+ sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
+
+ // Ok all keys are backedUp
+ backingUpStatusGroup.isVisible = true
backupProgress.isVisible = false
backupCompleteImage.isVisible = true
backupStatusTex.text = getString(R.string.keys_backup_info_keys_all_backup_up)
- sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
+ hideViews(setupMegolmBackupButton, exportManuallyButton, exitAnywayButton)
+ // You can signout
+ signOutButton.isVisible = true
}
- KeysBackupState.BackingUp,
- KeysBackupState.WillBackUp -> {
- backingUpStatusGroup.isVisible = true
- sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backing_up)
- dontWantClickableView.isVisible = true
- setupClickableView.isVisible = false
- activateClickableView.isVisible = false
+ KeysBackupState.WillBackUp,
+ KeysBackupState.BackingUp -> {
+ sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backing_up)
+
+ // save in progress
+ backingUpStatusGroup.isVisible = true
backupProgress.isVisible = true
backupCompleteImage.isVisible = false
backupStatusTex.text = getString(R.string.sign_out_bottom_sheet_backing_up_keys)
+
+ hideViews(setupMegolmBackupButton, setupMegolmBackupButton, signOutButton, exportManuallyButton)
+ exitAnywayButton.isVisible = true
}
KeysBackupState.NotTrusted -> {
- backingUpStatusGroup.isVisible = false
- dontWantClickableView.isVisible = true
- setupClickableView.isVisible = false
- activateClickableView.isVisible = true
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backup_not_active)
+ // It's not trusted and we know there are unsaved keys..
+ backingUpStatusGroup.isVisible = false
+
+ exportManuallyButton.isVisible = true
+ // option to enter pass/key
+ setupMegolmBackupButton.isVisible = true
+ exitAnywayButton.isVisible = true
}
else -> {
- backingUpStatusGroup.isVisible = false
- dontWantClickableView.isVisible = true
- setupClickableView.isVisible = true
- activateClickableView.isVisible = false
- sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup)
+ // mmm.. strange state
+
+ exitAnywayButton.isVisible = true
}
}
+ }
- // updateSignOutSection()
- })
+ // final call if keys have been exported
+ when (state.hasBeenExportedToFile) {
+ is Loading -> {
+ signoutExportingLoading.isVisible = true
+ hideViews(setupRecoveryButton,
+ setupMegolmBackupButton,
+ exportManuallyButton,
+ backingUpStatusGroup,
+ signOutButton)
+ exitAnywayButton.isVisible = true
+ }
+ is Success -> {
+ if (state.hasBeenExportedToFile.invoke()) {
+ sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
+ hideViews(setupRecoveryButton,
+ setupMegolmBackupButton,
+ exportManuallyButton,
+ backingUpStatusGroup,
+ exitAnywayButton)
+ signOutButton.isVisible = true
+ }
+ }
+ else -> {
+ }
+ }
+ super.invalidate()
}
override fun getLayoutResId() = R.layout.bottom_sheet_logout_and_backup
@@ -228,10 +296,26 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
- if (requestCode == EXPORT_REQ) {
- val manualExportDone = data?.getBooleanExtra(KeysBackupSetupActivity.MANUAL_EXPORT, false)
- viewModel.keysExportedToFile.value = manualExportDone
+ if (requestCode == QUERY_EXPORT_KEYS) {
+ val uri = data?.data
+ if (resultCode == Activity.RESULT_OK && uri != null) {
+ activity?.let { activity ->
+ ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
+ override fun onPassphrase(passphrase: String) {
+ viewModel.handle(SignoutCheckViewModel.Actions.ExportKeys(passphrase, uri))
+ }
+ })
+ }
+ }
+ } else if (requestCode == EXPORT_REQ) {
+ if (data?.getBooleanExtra(KeysBackupSetupActivity.MANUAL_EXPORT, false) == true) {
+ viewModel.handle(SignoutCheckViewModel.Actions.KeySuccessfullyManuallyExported)
+ }
}
}
}
+
+ private fun hideViews(vararg views: View) {
+ views.forEach { it.isVisible = false }
+ }
}
diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutUiWorker.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutUiWorker.kt
index e51fda2be5..e06a47d3d4 100644
--- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutUiWorker.kt
+++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutUiWorker.kt
@@ -21,7 +21,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
-import im.vector.riotx.core.extensions.hasUnsavedKeys
+import im.vector.riotx.core.extensions.cannotLogoutSafely
import im.vector.riotx.core.extensions.vectorComponent
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs
@@ -33,7 +33,7 @@ class SignOutUiWorker(private val activity: FragmentActivity) {
fun perform(context: Context) {
activeSessionHolder = context.vectorComponent().activeSessionHolder()
val session = activeSessionHolder.getActiveSession()
- if (session.hasUnsavedKeys()) {
+ if (session.cannotLogoutSafely()) {
// The backup check on logout flow has to be displayed if there are keys in the store, and the keys backup state is not Ready
val signOutDialog = SignOutBottomSheetDialogFragment.newInstance()
signOutDialog.onSignOut = Runnable {
diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt
deleted file mode 100644
index 2f26fdf377..0000000000
--- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2019 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package im.vector.riotx.features.workers.signout
-
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import im.vector.matrix.android.api.session.Session
-import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
-import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
-import javax.inject.Inject
-
-class SignOutViewModel @Inject constructor(private val session: Session) : ViewModel(), KeysBackupStateListener {
- // Keys exported manually
- var keysExportedToFile = MutableLiveData()
-
- var keysBackupState = MutableLiveData()
-
- init {
- session.cryptoService().keysBackupService().addListener(this)
-
- keysBackupState.value = session.cryptoService().keysBackupService().state
- }
-
- /**
- * Safe way to get the current KeysBackup version
- */
- fun getCurrentBackupVersion(): String {
- return session.cryptoService().keysBackupService().currentBackupVersion ?: ""
- }
-
- /**
- * Safe way to get the number of keys to backup
- */
- fun getNumberOfKeysToBackup(): Int {
- return session.cryptoService().inboundGroupSessionsCount(false)
- }
-
- /**
- * Safe way to tell if there are more keys on the server
- */
- fun canRestoreKeys(): Boolean {
- return session.cryptoService().keysBackupService().canRestoreKeys()
- }
-
- override fun onCleared() {
- super.onCleared()
-
- session.cryptoService().keysBackupService().removeListener(this)
- }
-
- override fun onStateChange(newState: KeysBackupState) {
- keysBackupState.value = newState
- }
-
- fun refreshRemoteStateIfNeeded() {
- if (keysBackupState.value == KeysBackupState.Disabled) {
- session.cryptoService().keysBackupService().checkAndStartKeysBackup()
- }
- }
-}
diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutBottomSheetActionButton.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutBottomSheetActionButton.kt
new file mode 100644
index 0000000000..cd5e4ed9da
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutBottomSheetActionButton.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.workers.signout
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.view.isVisible
+import butterknife.BindView
+import butterknife.ButterKnife
+import im.vector.riotx.R
+import im.vector.riotx.core.extensions.setTextOrHide
+import im.vector.riotx.features.themes.ThemeUtils
+
+class SignoutBottomSheetActionButton @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : LinearLayout(context, attrs, defStyleAttr) {
+
+ @BindView(R.id.actionTitleText)
+ lateinit var actionTextView: TextView
+
+ @BindView(R.id.actionIconImageView)
+ lateinit var iconImageView: ImageView
+
+ @BindView(R.id.signedOutActionClickable)
+ lateinit var clickableZone: View
+
+ var action: (() -> Unit)? = null
+
+ var title: String? = null
+ set(value) {
+ field = value
+ actionTextView.setTextOrHide(value)
+ }
+
+ var leftIcon: Drawable? = null
+ set(value) {
+ field = value
+ if (value == null) {
+ iconImageView.isVisible = false
+ iconImageView.setImageDrawable(null)
+ } else {
+ iconImageView.isVisible = true
+ iconImageView.setImageDrawable(value)
+ }
+ }
+
+ var tint: Int? = null
+ set(value) {
+ field = value
+ iconImageView.imageTintList = value?.let { ColorStateList.valueOf(value) }
+ }
+
+ var textColor: Int? = null
+ set(value) {
+ field = value
+ textColor?.let { actionTextView.setTextColor(it) }
+ }
+
+ init {
+ inflate(context, R.layout.item_signout_action, this)
+ ButterKnife.bind(this)
+
+ val typedArray = context.obtainStyledAttributes(attrs, R.styleable.SignoutBottomSheetActionButton, 0, 0)
+ title = typedArray.getString(R.styleable.SignoutBottomSheetActionButton_actionTitle) ?: ""
+ leftIcon = typedArray.getDrawable(R.styleable.SignoutBottomSheetActionButton_leftIcon)
+ tint = typedArray.getColor(R.styleable.SignoutBottomSheetActionButton_iconTint, ThemeUtils.getColor(context, android.R.attr.textColor))
+ textColor = typedArray.getColor(R.styleable.SignoutBottomSheetActionButton_textColor, ThemeUtils.getColor(context, android.R.attr.textColor))
+
+ typedArray.recycle()
+
+ clickableZone.setOnClickListener {
+ action?.invoke()
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutCheckViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutCheckViewModel.kt
new file mode 100644
index 0000000000..47da7d4edc
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutCheckViewModel.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.workers.signout
+
+import android.net.Uri
+import com.airbnb.mvrx.ActivityViewModelContext
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.FragmentViewModelContext
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.MvRxState
+import com.airbnb.mvrx.MvRxViewModelFactory
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
+import com.airbnb.mvrx.ViewModelContext
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
+import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
+import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
+import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
+import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
+import im.vector.matrix.rx.rx
+import im.vector.riotx.core.extensions.exhaustive
+import im.vector.riotx.core.platform.VectorViewEvents
+import im.vector.riotx.core.platform.VectorViewModel
+import im.vector.riotx.core.platform.VectorViewModelAction
+import im.vector.riotx.features.crypto.keys.KeysExporter
+
+data class SignoutCheckViewState(
+ val userId: String = "",
+ val backupIsSetup: Boolean = false,
+ val crossSigningSetupAllKeysKnown: Boolean = false,
+ val keysBackupState: KeysBackupState = KeysBackupState.Unknown,
+ val hasBeenExportedToFile: Async = Uninitialized
+) : MvRxState
+
+class SignoutCheckViewModel @AssistedInject constructor(@Assisted initialState: SignoutCheckViewState,
+ private val session: Session)
+ : VectorViewModel(initialState), KeysBackupStateListener {
+
+ sealed class Actions : VectorViewModelAction {
+ data class ExportKeys(val passphrase: String, val uri: Uri) : Actions()
+ object KeySuccessfullyManuallyExported : Actions()
+ object KeyExportFailed : Actions()
+ }
+
+ sealed class ViewEvents : VectorViewEvents {
+ data class ExportKeys(val exporter: KeysExporter, val passphrase: String, val uri: Uri) : ViewEvents()
+ }
+
+ @AssistedInject.Factory
+ interface Factory {
+ fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel
+ }
+
+ companion object : MvRxViewModelFactory {
+
+ @JvmStatic
+ override fun create(viewModelContext: ViewModelContext, state: SignoutCheckViewState): SignoutCheckViewModel? {
+ val factory = when (viewModelContext) {
+ is FragmentViewModelContext -> viewModelContext.fragment as? Factory
+ is ActivityViewModelContext -> viewModelContext.activity as? Factory
+ }
+ return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
+ }
+ }
+
+ init {
+ session.cryptoService().keysBackupService().addListener(this)
+ session.cryptoService().keysBackupService().checkAndStartKeysBackup()
+
+ val quad4SIsSetup = session.sharedSecretStorageService.isRecoverySetup()
+ val allKeysKnown = session.cryptoService().crossSigningService().allPrivateKeysKnown()
+ val backupState = session.cryptoService().keysBackupService().state
+ setState {
+ copy(
+ userId = session.myUserId,
+ crossSigningSetupAllKeysKnown = allKeysKnown,
+ backupIsSetup = quad4SIsSetup,
+ keysBackupState = backupState
+ )
+ }
+
+ session.rx().liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME))
+ .map {
+ session.sharedSecretStorageService.isRecoverySetup()
+ }
+ .distinctUntilChanged()
+ .execute {
+ copy(backupIsSetup = it.invoke() == true)
+ }
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ session.cryptoService().keysBackupService().removeListener(this)
+ }
+
+ override fun onStateChange(newState: KeysBackupState) {
+ setState {
+ copy(
+ keysBackupState = newState
+ )
+ }
+ }
+
+ fun refreshRemoteStateIfNeeded() = withState { state ->
+ if (state.keysBackupState == KeysBackupState.Disabled) {
+ session.cryptoService().keysBackupService().checkAndStartKeysBackup()
+ }
+ }
+
+ override fun handle(action: Actions) {
+ when (action) {
+ is Actions.ExportKeys -> {
+ setState {
+ copy(hasBeenExportedToFile = Loading())
+ }
+ _viewEvents.post(ViewEvents.ExportKeys(KeysExporter(session), action.passphrase, action.uri))
+ }
+ Actions.KeySuccessfullyManuallyExported -> {
+ setState {
+ copy(hasBeenExportedToFile = Success(true))
+ }
+ }
+ Actions.KeyExportFailed -> {
+ setState {
+ copy(hasBeenExportedToFile = Uninitialized)
+ }
+ }
+ }.exhaustive
+ }
+}
diff --git a/vector/src/main/res/drawable/ic_pause.xml b/vector/src/main/res/drawable/ic_pause.xml
new file mode 100644
index 0000000000..13d6d2ec00
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_pause.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/vector/src/main/res/drawable/ic_play_arrow.xml b/vector/src/main/res/drawable/ic_play_arrow.xml
new file mode 100644
index 0000000000..13c137a921
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_play_arrow.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/vector/src/main/res/drawable/ic_secure_backup.xml b/vector/src/main/res/drawable/ic_secure_backup.xml
new file mode 100644
index 0000000000..899bb8d2ae
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_secure_backup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml b/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml
index feaa79e1dc..c6605dfc05 100644
--- a/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml
+++ b/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml
@@ -70,137 +70,60 @@
+ android:layout_height="44dp"
+ android:gravity="center">
-
-
-
-
+ android:layout_height="wrap_content" />
-
-
-
-
-
-
-
+ app:actionTitle="@string/secure_backup_setup"
+ app:iconTint="?riotx_text_primary"
+ app:leftIcon="@drawable/ic_secure_backup"
+ app:textColor="?riotx_text_secondary" />
-
+ app:actionTitle="@string/keys_backup_setup"
+ app:iconTint="?riotx_text_primary"
+ app:leftIcon="@drawable/backup_keys"
+ app:textColor="?riotx_text_secondary" />
-
-
-
-
-
-
+ app:actionTitle="@string/keys_backup_setup_step1_manual_export"
+ app:iconTint="?riotx_text_primary"
+ app:leftIcon="@drawable/ic_download"
+ app:textColor="?riotx_text_secondary" />
-
-
-
-
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/fragment_home_detail.xml b/vector/src/main/res/layout/fragment_home_detail.xml
index f90422dff9..aa7a76cf16 100644
--- a/vector/src/main/res/layout/fragment_home_detail.xml
+++ b/vector/src/main/res/layout/fragment_home_detail.xml
@@ -59,6 +59,8 @@
android:layout_height="wrap_content"
android:background="?riotx_keys_backup_banner_accent_color"
android:minHeight="67dp"
+ android:visibility="gone"
+ tools:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/syncStateView" />
diff --git a/vector/src/main/res/layout/fragment_room_uploads.xml b/vector/src/main/res/layout/fragment_room_uploads.xml
index 5e289d4724..f5d3658ee5 100644
--- a/vector/src/main/res/layout/fragment_room_uploads.xml
+++ b/vector/src/main/res/layout/fragment_room_uploads.xml
@@ -8,6 +8,8 @@
diff --git a/vector/src/main/res/layout/item_signout_action.xml b/vector/src/main/res/layout/item_signout_action.xml
new file mode 100644
index 0000000000..c5acc09e56
--- /dev/null
+++ b/vector/src/main/res/layout/item_signout_action.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/merge_image_attachment_overlay.xml b/vector/src/main/res/layout/merge_image_attachment_overlay.xml
new file mode 100644
index 0000000000..b0e769579c
--- /dev/null
+++ b/vector/src/main/res/layout/merge_image_attachment_overlay.xml
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/view_keys_backup_banner.xml b/vector/src/main/res/layout/view_keys_backup_banner.xml
index 87c92cf8b4..6c8fc2b5a1 100644
--- a/vector/src/main/res/layout/view_keys_backup_banner.xml
+++ b/vector/src/main/res/layout/view_keys_backup_banner.xml
@@ -10,11 +10,11 @@
+
+
+
+
+
+
diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors_riotx.xml
index a9cb32c3fd..c9d1c2a223 100644
--- a/vector/src/main/res/values/colors_riotx.xml
+++ b/vector/src/main/res/values/colors_riotx.xml
@@ -40,6 +40,7 @@
#FF000000
#FFFFFFFF
+ #55000000
Ongoing conference call.\nJoin as %1$s or %2$s
Voice
@@ -1048,6 +1052,7 @@
Export
Please create a passphrase to encrypt the exported keys. You will need to enter the same passphrase to be able to import the keys.
The E2E room keys have been saved to \'%s\'.\n\nWarning: this file may be deleted if the application is uninstalled.
+ Keys successfully exported
Encrypted Messages Recovery
Manage Key Backup
@@ -1493,17 +1498,24 @@ Why choose Riot.im?
New Key Backup
A new secure message key backup has been detected.\n\nIf you didnβt set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.
It was me
+
Never lose encrypted messages
Start using Key Backup
+ Secure Backup
+ Safeguard against losing access to encrypted messages & data
+
Never lose encrypted messages
Use Key Backup
New secure message keys
Manage in Key Backup
- Backing up keysβ¦
+ Backing up your keys. This may take several minutesβ¦
+
+
+ Set Up Secure Backup
All keys backed up
diff --git a/vector/src/main/res/values/theme_common.xml b/vector/src/main/res/values/theme_common.xml
index 151d97c097..414d562ff0 100644
--- a/vector/src/main/res/values/theme_common.xml
+++ b/vector/src/main/res/values/theme_common.xml
@@ -10,4 +10,15 @@
+
+
\ No newline at end of file