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/contacts/ContactsDataSource.kt b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
new file mode 100644
index 0000000000..fd23e495b9
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.contacts
+
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.provider.ContactsContract
+import androidx.annotation.WorkerThread
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.system.measureTimeMillis
+
+class ContactsDataSource @Inject constructor(
+ private val context: Context
+) {
+
+ /**
+ * Will return a list of contact from the contacts book of the device, with at least one email or phone.
+ * If both param are false, you will get en empty list.
+ * Note: The return list does not contain any matrixId.
+ */
+ @WorkerThread
+ fun getContacts(
+ withEmails: Boolean,
+ withMsisdn: Boolean
+ ): List {
+ val map = mutableMapOf()
+ val contentResolver = context.contentResolver
+
+ measureTimeMillis {
+ contentResolver.query(
+ ContactsContract.Contacts.CONTENT_URI,
+ arrayOf(
+ ContactsContract.Contacts._ID,
+ ContactsContract.Data.DISPLAY_NAME,
+ ContactsContract.Data.PHOTO_URI
+ ),
+ null,
+ null,
+ // Sort by Display name
+ ContactsContract.Data.DISPLAY_NAME
+ )
+ ?.use { cursor ->
+ if (cursor.count > 0) {
+ while (cursor.moveToNext()) {
+ val id = cursor.getLong(ContactsContract.Contacts._ID) ?: continue
+ val displayName = cursor.getString(ContactsContract.Contacts.DISPLAY_NAME) ?: continue
+
+ val mappedContactBuilder = MappedContactBuilder(
+ id = id,
+ displayName = displayName
+ )
+
+ cursor.getString(ContactsContract.Data.PHOTO_URI)
+ ?.let { Uri.parse(it) }
+ ?.let { mappedContactBuilder.photoURI = it }
+
+ map[id] = mappedContactBuilder
+ }
+ }
+ }
+
+ // Get the phone numbers
+ if (withMsisdn) {
+ contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
+ arrayOf(
+ ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
+ ContactsContract.CommonDataKinds.Phone.NUMBER
+ ),
+ null,
+ null,
+ null)
+ ?.use { innerCursor ->
+ while (innerCursor.moveToNext()) {
+ val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)
+ ?.let { map[it] }
+ ?: continue
+ innerCursor.getString(ContactsContract.CommonDataKinds.Phone.NUMBER)
+ ?.let {
+ mappedContactBuilder.msisdns.add(
+ MappedMsisdn(
+ phoneNumber = it,
+ matrixId = null
+ )
+ )
+ }
+ }
+ }
+ }
+
+ // Get Emails
+ if (withEmails) {
+ contentResolver.query(
+ ContactsContract.CommonDataKinds.Email.CONTENT_URI,
+ arrayOf(
+ ContactsContract.CommonDataKinds.Email.CONTACT_ID,
+ ContactsContract.CommonDataKinds.Email.DATA
+ ),
+ null,
+ null,
+ null)
+ ?.use { innerCursor ->
+ while (innerCursor.moveToNext()) {
+ // This would allow you get several email addresses
+ // if the email addresses were stored in an array
+ val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Email.CONTACT_ID)
+ ?.let { map[it] }
+ ?: continue
+ innerCursor.getString(ContactsContract.CommonDataKinds.Email.DATA)
+ ?.let {
+ mappedContactBuilder.emails.add(
+ MappedEmail(
+ email = it,
+ matrixId = null
+ )
+ )
+ }
+ }
+ }
+ }
+ }.also { Timber.d("Took ${it}ms to fetch ${map.size} contact(s)") }
+
+ return map
+ .values
+ .filter { it.emails.isNotEmpty() || it.msisdns.isNotEmpty() }
+ .map { it.build() }
+ }
+
+ private fun Cursor.getString(column: String): String? {
+ return getColumnIndex(column)
+ .takeIf { it != -1 }
+ ?.let { getString(it) }
+ }
+
+ private fun Cursor.getLong(column: String): Long? {
+ return getColumnIndex(column)
+ .takeIf { it != -1 }
+ ?.let { getLong(it) }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/contacts/MappedContact.kt b/vector/src/main/java/im/vector/riotx/core/contacts/MappedContact.kt
new file mode 100644
index 0000000000..c89a3d4b01
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/contacts/MappedContact.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.contacts
+
+import android.net.Uri
+
+class MappedContactBuilder(
+ val id: Long,
+ val displayName: String
+) {
+ var photoURI: Uri? = null
+ val msisdns = mutableListOf()
+ val emails = mutableListOf()
+
+ fun build(): MappedContact {
+ return MappedContact(
+ id = id,
+ displayName = displayName,
+ photoURI = photoURI,
+ msisdns = msisdns,
+ emails = emails
+ )
+ }
+}
+
+data class MappedContact(
+ val id: Long,
+ val displayName: String,
+ val photoURI: Uri? = null,
+ val msisdns: List = emptyList(),
+ val emails: List = emptyList()
+)
+
+data class MappedEmail(
+ val email: String,
+ val matrixId: String?
+)
+
+data class MappedMsisdn(
+ val phoneNumber: String,
+ val matrixId: String?
+)
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/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
index 21cff188d0..8e4f95ed54 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
@@ -23,6 +23,7 @@ import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
+import im.vector.riotx.features.contactsbook.ContactsBookFragment
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment
import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment
@@ -528,4 +529,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(WidgetFragment::class)
fun bindWidgetFragment(fragment: WidgetFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(ContactsBookFragment::class)
+ fun bindPhoneBookFragment(fragment: ContactsBookFragment): Fragment
}
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/epoxy/profiles/ProfileMatrixItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
index e9f4dba7a5..b89da07984 100644
--- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
+++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
@@ -20,6 +20,7 @@ package im.vector.riotx.core.epoxy.profiles
import android.view.View
import android.widget.ImageView
import android.widget.TextView
+import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
@@ -36,16 +37,21 @@ abstract class ProfileMatrixItem : VectorEpoxyModel()
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem
+ @EpoxyAttribute var editable: Boolean = true
@EpoxyAttribute var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
val bestName = matrixItem.getBestName()
- val matrixId = matrixItem.id.takeIf { it != bestName }
- holder.view.setOnClickListener(clickListener)
+ val matrixId = matrixItem.id
+ .takeIf { it != bestName }
+ // Special case for ThreePid fake matrix item
+ .takeIf { it != "@" }
+ holder.view.setOnClickListener(clickListener?.takeIf { editable })
holder.titleView.text = bestName
holder.subtitleView.setTextOrHide(matrixId)
+ holder.editableView.isVisible = editable
avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.avatarDecorationImageView.setImageResource(userEncryptionTrustLevel.toImageRes())
}
@@ -55,5 +61,6 @@ abstract class ProfileMatrixItem : VectorEpoxyModel()
val subtitleView by bind(R.id.matrixItemSubtitle)
val avatarImageView by bind(R.id.matrixItemAvatar)
val avatarDecorationImageView by bind(R.id.matrixItemAvatarDecoration)
+ val editableView by bind(R.id.matrixItemEditable)
}
}
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt
index b74f143e17..cc6eb54154 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt
@@ -23,21 +23,21 @@ import androidx.fragment.app.FragmentTransaction
import im.vector.riotx.core.platform.VectorBaseActivity
fun VectorBaseActivity.addFragment(frameId: Int, fragment: Fragment) {
- supportFragmentManager.commitTransactionNow { add(frameId, fragment) }
+ supportFragmentManager.commitTransaction { add(frameId, fragment) }
}
fun VectorBaseActivity.addFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) {
- supportFragmentManager.commitTransactionNow {
+ supportFragmentManager.commitTransaction {
add(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
fun VectorBaseActivity.replaceFragment(frameId: Int, fragment: Fragment, tag: String? = null) {
- supportFragmentManager.commitTransactionNow { replace(frameId, fragment, tag) }
+ supportFragmentManager.commitTransaction { replace(frameId, fragment, tag) }
}
fun VectorBaseActivity.replaceFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) {
- supportFragmentManager.commitTransactionNow {
+ supportFragmentManager.commitTransaction {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt b/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
index 5bd6852e8a..99a5cb5a1a 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
@@ -19,6 +19,9 @@ package im.vector.riotx.core.extensions
import android.os.Bundle
import android.util.Patterns
import androidx.fragment.app.Fragment
+import com.google.i18n.phonenumbers.NumberParseException
+import com.google.i18n.phonenumbers.PhoneNumberUtil
+import im.vector.matrix.android.api.extensions.ensurePrefix
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
@@ -33,3 +36,15 @@ fun T.withArgs(block: Bundle.() -> Unit) = apply { arguments = Bu
* Check if a CharSequence is an email
*/
fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches()
+
+/**
+ * Check if a CharSequence is a phone number
+ */
+fun CharSequence.isMsisdn(): Boolean {
+ return try {
+ PhoneNumberUtil.getInstance().parse(ensurePrefix("+"), null)
+ true
+ } catch (e: NumberParseException) {
+ false
+ }
+}
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 c28dcf12d3..2f07c2ade3 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,26 +16,32 @@
package im.vector.riotx.core.extensions
+import android.app.Activity
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.selectTxtFileToWrite
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) {
- parentFragmentManager.commitTransactionNow { add(frameId, fragment) }
+ parentFragmentManager.commitTransaction { add(frameId, fragment) }
}
fun VectorBaseFragment.addFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) {
- parentFragmentManager.commitTransactionNow {
+ parentFragmentManager.commitTransaction {
add(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
fun VectorBaseFragment.replaceFragment(frameId: Int, fragment: Fragment) {
- parentFragmentManager.commitTransactionNow { replace(frameId, fragment) }
+ parentFragmentManager.commitTransaction { replace(frameId, fragment) }
}
fun VectorBaseFragment.replaceFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) {
- parentFragmentManager.commitTransactionNow {
+ parentFragmentManager.commitTransaction {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
@@ -51,21 +57,21 @@ fun VectorBaseFragment.addFragmentToBackstack(frameId: Int, fragm
}
fun VectorBaseFragment.addChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) {
- childFragmentManager.commitTransactionNow { add(frameId, fragment, tag) }
+ childFragmentManager.commitTransaction { add(frameId, fragment, tag) }
}
fun VectorBaseFragment.addChildFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) {
- childFragmentManager.commitTransactionNow {
+ childFragmentManager.commitTransaction {
add(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
fun VectorBaseFragment.replaceChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) {
- childFragmentManager.commitTransactionNow { replace(frameId, fragment, tag) }
+ childFragmentManager.commitTransaction { replace(frameId, fragment, tag) }
}
fun VectorBaseFragment.replaceChildFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) {
- childFragmentManager.commitTransactionNow {
+ childFragmentManager.commitTransaction {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
@@ -89,3 +95,27 @@ fun Fragment.getAllChildFragments(): List {
// Define a missing constant
const val POP_BACK_STACK_EXCLUSIVE = 0
+
+fun Fragment.queryExportKeys(userId: String, requestCode: Int) {
+ val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
+
+ selectTxtFileToWrite(
+ activity = requireActivity(),
+ fragment = this,
+ defaultFileName = "riot-megolm-export-$userId-$timestamp.txt",
+ chooserHint = getString(R.string.keys_backup_setup_step1_manual_export),
+ requestCode = requestCode
+ )
+}
+
+fun Activity.queryExportKeys(userId: String, requestCode: Int) {
+ val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
+
+ selectTxtFileToWrite(
+ activity = this,
+ fragment = null,
+ defaultFileName = "riot-megolm-export-$userId-$timestamp.txt",
+ chooserHint = getString(R.string.keys_backup_setup_step1_manual_export),
+ requestCode = requestCode
+ )
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Iterable.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Iterable.kt
index 987194ea2f..b9907f8789 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/Iterable.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/Iterable.kt
@@ -38,13 +38,13 @@ inline fun > Iterable.lastMinBy(selector: (T) -> R): T?
/**
* Call each for each item, and between between each items
*/
-inline fun Collection.join(each: (T) -> Unit, between: (T) -> Unit) {
+inline fun Collection.join(each: (Int, T) -> Unit, between: (Int, T) -> Unit) {
val lastIndex = size - 1
forEachIndexed { idx, t ->
- each(t)
+ each(idx, t)
if (idx != lastIndex) {
- between(t)
+ between(idx, t)
}
}
}
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
deleted file mode 100644
index f451308c36..0000000000
--- a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt
+++ /dev/null
@@ -1,419 +0,0 @@
-/*
- * Copyright (C) 2011 Micah Hainline
- * Copyright (C) 2012 Triposo
- * Copyright (C) 2013 Paul Imhoff
- * Copyright (C) 2014 Shahin Yousefi
- * 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.
- * 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.platform
-
-import android.content.Context
-import android.graphics.Canvas
-import android.graphics.Color
-import android.text.Layout
-import android.text.Spannable
-import android.text.SpannableString
-import android.text.SpannableStringBuilder
-import android.text.Spanned
-import android.text.StaticLayout
-import android.text.TextUtils.TruncateAt
-import android.text.TextUtils.concat
-import android.text.TextUtils.copySpansFrom
-import android.text.TextUtils.indexOf
-import android.text.TextUtils.lastIndexOf
-import android.text.TextUtils.substring
-import android.text.style.ForegroundColorSpan
-import android.util.AttributeSet
-import androidx.appcompat.widget.AppCompatTextView
-import timber.log.Timber
-import java.util.ArrayList
-import java.util.regex.Pattern
-
-/*
- * Imported from https://gist.github.com/hateum/d2095575b441007d62b8
- *
- * Use it in your layout to avoid this issue: https://issuetracker.google.com/issues/121092510
- */
-
-/**
- * A [android.widget.TextView] that ellipsizes more intelligently.
- * This class supports ellipsizing multiline text through setting `android:ellipsize`
- * and `android:maxLines`.
- *
- *
- * Note: [TruncateAt.MARQUEE] ellipsizing type is not supported.
- * This as to be used to get rid of the StaticLayout issue with maxLines and ellipsize causing some performance issues.
- */
-class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = android.R.attr.textViewStyle)
- : AppCompatTextView(context, attrs, defStyle) {
-
- private val ELLIPSIS = SpannableString("\u2026")
- private val ellipsizeListeners: MutableList = ArrayList()
- private var ellipsizeStrategy: EllipsizeStrategy? = null
- var isEllipsized = false
- private set
- private var isStale = false
- private var programmaticChange = false
- private var fullText: CharSequence? = null
- private var maxLines = 0
- private var lineSpacingMult = 1.0f
- private var lineAddVertPad = 0.0f
- /**
- * The end punctuation which will be removed when appending [.ELLIPSIS].
- */
- private var mEndPunctPattern: Pattern? = null
-
- fun setEndPunctuationPattern(pattern: Pattern?) {
- mEndPunctPattern = pattern
- }
-
- fun addEllipsizeListener(listener: EllipsizeListener) {
- ellipsizeListeners.add(listener)
- }
-
- fun removeEllipsizeListener(listener: EllipsizeListener) {
- ellipsizeListeners.remove(listener)
- }
-
- /**
- * @return The maximum number of lines displayed in this [android.widget.TextView].
- */
- override fun getMaxLines(): Int {
- return maxLines
- }
-
- override fun setMaxLines(maxLines: Int) {
- super.setMaxLines(maxLines)
- this.maxLines = maxLines
- isStale = true
- }
-
- /**
- * Determines if the last fully visible line is being ellipsized.
- *
- * @return `true` if the last fully visible line is being ellipsized;
- * otherwise, returns `false`.
- */
- fun ellipsizingLastFullyVisibleLine(): Boolean {
- return maxLines == Int.MAX_VALUE
- }
-
- override fun setLineSpacing(add: Float, mult: Float) {
- lineAddVertPad = add
- lineSpacingMult = mult
- super.setLineSpacing(add, mult)
- }
-
- override fun setText(text: CharSequence?, type: BufferType) {
- if (!programmaticChange) {
- fullText = if (text is Spanned) text else text
- isStale = true
- }
- super.setText(text, type)
- }
-
- override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
- super.onSizeChanged(w, h, oldw, oldh)
- if (ellipsizingLastFullyVisibleLine()) {
- isStale = true
- }
- }
-
- override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
- super.setPadding(left, top, right, bottom)
- if (ellipsizingLastFullyVisibleLine()) {
- isStale = true
- }
- }
-
- override fun onDraw(canvas: Canvas) {
- if (isStale) {
- resetText()
- }
- super.onDraw(canvas)
- }
-
- /**
- * Sets the ellipsized text if appropriate.
- */
- private fun resetText() {
- val maxLines = maxLines
- var workingText = fullText
- var ellipsized = false
- if (maxLines != -1) {
- if (ellipsizeStrategy == null) setEllipsize(null)
- workingText = ellipsizeStrategy!!.processText(fullText)
- ellipsized = !ellipsizeStrategy!!.isInLayout(fullText)
- }
- if (workingText != text) {
- programmaticChange = true
- text = try {
- workingText
- } finally {
- programmaticChange = false
- }
- }
- isStale = false
- if (ellipsized != isEllipsized) {
- isEllipsized = ellipsized
- for (listener in ellipsizeListeners) {
- listener.ellipsizeStateChanged(ellipsized)
- }
- }
- }
-
- /**
- * Causes words in the text that are longer than the view is wide to be ellipsized
- * instead of broken in the middle. Use `null` to turn off ellipsizing.
- *
- *
- * Note: Method does nothing for [TruncateAt.MARQUEE]
- * ellipsizing type.
- *
- * @param where part of text to ellipsize
- */
- override fun setEllipsize(where: TruncateAt?) {
- if (where == null) {
- ellipsizeStrategy = EllipsizeNoneStrategy()
- return
- }
- ellipsizeStrategy = when (where) {
- TruncateAt.END -> EllipsizeEndStrategy()
- TruncateAt.START -> EllipsizeStartStrategy()
- TruncateAt.MIDDLE -> EllipsizeMiddleStrategy()
- TruncateAt.MARQUEE -> EllipsizeNoneStrategy()
- else -> EllipsizeNoneStrategy()
- }
- }
-
- /**
- * A listener that notifies when the ellipsize state has changed.
- */
- interface EllipsizeListener {
- fun ellipsizeStateChanged(ellipsized: Boolean)
- }
-
- /**
- * A base class for an ellipsize strategy.
- */
- private abstract inner class EllipsizeStrategy {
- /**
- * Returns ellipsized text if the text does not fit inside of the layout;
- * otherwise, returns the full text.
- *
- * @param text text to process
- * @return Ellipsized text if the text does not fit inside of the layout;
- * otherwise, returns the full text.
- */
- fun processText(text: CharSequence?): CharSequence? {
- return if (!isInLayout(text)) createEllipsizedText(text) else text
- }
-
- /**
- * Determines if the text fits inside of the layout.
- *
- * @param text text to fit
- * @return `true` if the text fits inside of the layout;
- * otherwise, returns `false`.
- */
- fun isInLayout(text: CharSequence?): Boolean {
- val layout = createWorkingLayout(text)
- return layout.lineCount <= linesCount
- }
-
- /**
- * Creates a working layout with the given text.
- *
- * @param workingText text to create layout with
- * @return [android.text.Layout] with the given text.
- */
- @Suppress("DEPRECATION")
- protected fun createWorkingLayout(workingText: CharSequence?): Layout {
- return StaticLayout(
- workingText ?: "",
- paint,
- width - compoundPaddingLeft - compoundPaddingRight,
- Layout.Alignment.ALIGN_NORMAL,
- lineSpacingMult,
- lineAddVertPad,
- false
- )
- }
-
- /**
- * Get how many lines of text we are allowed to display.
- */
- protected val linesCount: Int
- get() = if (ellipsizingLastFullyVisibleLine()) {
- val fullyVisibleLinesCount = fullyVisibleLinesCount
- if (fullyVisibleLinesCount == -1) 1 else fullyVisibleLinesCount
- } else {
- maxLines
- }
-
- /**
- * Get how many lines of text we can display so their full height is visible.
- */
- protected val fullyVisibleLinesCount: Int
- get() {
- val layout = createWorkingLayout("")
- val height = height - compoundPaddingTop - compoundPaddingBottom
- val lineHeight = layout.getLineBottom(0)
- return height / lineHeight
- }
-
- /**
- * Creates ellipsized text from the given text.
- *
- * @param fullText text to ellipsize
- * @return Ellipsized text
- */
- protected abstract fun createEllipsizedText(fullText: CharSequence?): CharSequence?
- }
-
- /**
- * An [EllipsizingTextView.EllipsizeStrategy] that
- * does not ellipsize text.
- */
- private inner class EllipsizeNoneStrategy : EllipsizeStrategy() {
- override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
- return fullText
- }
- }
-
- /**
- * An [EllipsizingTextView.EllipsizeStrategy] that
- * ellipsizes text at the end.
- */
- private inner class EllipsizeEndStrategy : EllipsizeStrategy() {
- override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
- val layout = createWorkingLayout(fullText)
- val cutOffIndex = try {
- layout.getLineEnd(maxLines - 1)
- } catch (exception: IndexOutOfBoundsException) {
- // Not sure to understand why this is happening
- Timber.e(exception, "IndexOutOfBoundsException, maxLine: $maxLines")
- 0
- }
- val textLength = fullText!!.length
- var cutOffLength = textLength - cutOffIndex
- if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
- var workingText: CharSequence = substring(fullText, 0, textLength - cutOffLength).trim()
- while (!isInLayout(concat(stripEndPunctuation(workingText), ELLIPSIS))) {
- val lastSpace = lastIndexOf(workingText, ' ')
- if (lastSpace == -1) {
- break
- }
- workingText = substring(workingText, 0, lastSpace).trim()
- }
- workingText = concat(stripEndPunctuation(workingText), ELLIPSIS)
- val dest = SpannableStringBuilder(workingText)
- if (fullText is Spanned) {
- copySpansFrom(fullText as Spanned?, 0, workingText.length, null, dest, 0)
- }
- return dest
- }
-
- /**
- * Strips the end punctuation from a given text according to [.mEndPunctPattern].
- *
- * @param workingText text to strip end punctuation from
- * @return Text without end punctuation.
- */
- fun stripEndPunctuation(workingText: CharSequence): String {
- return mEndPunctPattern!!.matcher(workingText).replaceFirst("")
- }
- }
-
- /**
- * An [EllipsizingTextView.EllipsizeStrategy] that
- * ellipsizes text at the start.
- */
- private inner class EllipsizeStartStrategy : EllipsizeStrategy() {
- override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
- val layout = createWorkingLayout(fullText)
- val cutOffIndex = layout.getLineEnd(maxLines - 1)
- val textLength = fullText!!.length
- var cutOffLength = textLength - cutOffIndex
- if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
- var workingText: CharSequence = substring(fullText, cutOffLength, textLength).trim()
- while (!isInLayout(concat(ELLIPSIS, workingText))) {
- val firstSpace = indexOf(workingText, ' ')
- if (firstSpace == -1) {
- break
- }
- workingText = substring(workingText, firstSpace, workingText.length).trim()
- }
- workingText = concat(ELLIPSIS, workingText)
- val dest = SpannableStringBuilder(workingText)
- if (fullText is Spanned) {
- copySpansFrom(fullText as Spanned?, textLength - workingText.length,
- textLength, null, dest, 0)
- }
- return dest
- }
- }
-
- /**
- * An [EllipsizingTextView.EllipsizeStrategy] that
- * ellipsizes text in the middle.
- */
- private inner class EllipsizeMiddleStrategy : EllipsizeStrategy() {
- override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
- val layout = createWorkingLayout(fullText)
- val cutOffIndex = layout.getLineEnd(maxLines - 1)
- val textLength = fullText!!.length
- var cutOffLength = textLength - cutOffIndex
- if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
- cutOffLength += cutOffIndex % 2 // Make it even.
- var firstPart = substring(
- fullText, 0, textLength / 2 - cutOffLength / 2).trim()
- var secondPart = substring(
- fullText, textLength / 2 + cutOffLength / 2, textLength).trim()
- while (!isInLayout(concat(firstPart, ELLIPSIS, secondPart))) {
- val lastSpaceFirstPart = firstPart.lastIndexOf(' ')
- val firstSpaceSecondPart = secondPart.indexOf(' ')
- if (lastSpaceFirstPart == -1 || firstSpaceSecondPart == -1) break
- firstPart = firstPart.substring(0, lastSpaceFirstPart).trim()
- secondPart = secondPart.substring(firstSpaceSecondPart, secondPart.length).trim()
- }
- val firstDest = SpannableStringBuilder(firstPart)
- val secondDest = SpannableStringBuilder(secondPart)
- if (fullText is Spanned) {
- copySpansFrom(fullText as Spanned?, 0, firstPart.length,
- null, firstDest, 0)
- copySpansFrom(fullText as Spanned?, textLength - secondPart.length,
- textLength, null, secondDest, 0)
- }
- return concat(firstDest, ELLIPSIS, secondDest)
- }
- }
-
- companion object {
- const val ELLIPSIZE_ALPHA = 0x88
- private val DEFAULT_END_PUNCTUATION = Pattern.compile("[.!?,;:\u2026]*$", Pattern.DOTALL)
- }
-
- 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()
- setEndPunctuationPattern(DEFAULT_END_PUNCTUATION)
- val currentTextColor = currentTextColor
- val ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor))
- ELLIPSIS.setSpan(ForegroundColorSpan(ellipsizeColor), 0, ELLIPSIS.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- }
-}
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/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
index bdd873d0cd..59bf7a8aeb 100644
--- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
@@ -162,9 +162,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
return this
}
- protected fun Disposable.disposeOnDestroy(): Disposable {
+ protected fun Disposable.disposeOnDestroy() {
uiDisposables.add(this)
- return this
}
override fun onCreate(savedInstanceState: Bundle?) {
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
index c0b1b54c09..f4343a3e58 100644
--- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
@@ -234,9 +234,8 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
private val uiDisposables = CompositeDisposable()
- protected fun Disposable.disposeOnDestroyView(): Disposable {
+ protected fun Disposable.disposeOnDestroyView() {
uiDisposables.add(this)
- return this
}
/* ==========================================================================================
diff --git a/vector/src/main/java/im/vector/riotx/core/preference/VectorListPreference.kt b/vector/src/main/java/im/vector/riotx/core/preference/VectorListPreference.kt
index d85d343155..174c52d831 100644
--- a/vector/src/main/java/im/vector/riotx/core/preference/VectorListPreference.kt
+++ b/vector/src/main/java/im/vector/riotx/core/preference/VectorListPreference.kt
@@ -90,8 +90,6 @@ class VectorListPreference : ListPreference {
fun setWarningIconVisible(isVisible: Boolean) {
mIsWarningIconVisible = isVisible
- if (null != mWarningIconView) {
- mWarningIconView!!.visibility = if (mIsWarningIconVisible) View.VISIBLE else View.GONE
- }
+ mWarningIconView?.visibility = if (mIsWarningIconVisible) View.VISIBLE else View.GONE
}
}
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..0152f7c2a8 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,16 +17,13 @@
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.transition.TransitionManager
+import androidx.preference.PreferenceManager
import butterknife.BindView
import butterknife.ButterKnife
import butterknife.OnClick
@@ -58,22 +55,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 +78,6 @@ class KeysBackupBanner @JvmOverloads constructor(
state = newState
hideAll()
-
when (newState) {
State.Initial -> renderInitial()
State.Hidden -> renderHidden()
@@ -102,22 +88,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 +136,8 @@ class KeysBackupBanner @JvmOverloads constructor(
ButterKnife.bind(this)
setOnClickListener(this)
+ textView1.setOnClickListener(this)
+ textView2.setOnClickListener(this)
}
private fun renderInitial() {
@@ -184,9 +156,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 +190,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/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt
index 2520f44f50..9c2d12514a 100644
--- a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt
+++ b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt
@@ -424,6 +424,33 @@ fun openPlayStore(activity: Activity, appId: String = BuildConfig.APPLICATION_ID
}
}
+/**
+ * Ask the user to select a location and a file name to write in
+ */
+fun selectTxtFileToWrite(
+ activity: Activity,
+ fragment: Fragment?,
+ defaultFileName: String,
+ chooserHint: String,
+ requestCode: Int
+) {
+ val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+ intent.type = "text/plain"
+ intent.putExtra(Intent.EXTRA_TITLE, defaultFileName)
+
+ try {
+ val chooserIntent = Intent.createChooser(intent, chooserHint)
+ if (fragment != null) {
+ fragment.startActivityForResult(chooserIntent, requestCode)
+ } else {
+ activity.startActivityForResult(chooserIntent, requestCode)
+ }
+ } catch (activityNotFoundException: ActivityNotFoundException) {
+ activity.toast(R.string.error_no_external_application_found)
+ }
+}
+
// ==============================================================================================================
// Media utils
// ==============================================================================================================
diff --git a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
index 4790b26ad0..6f081d52de 100644
--- a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
+++ b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
@@ -63,12 +63,12 @@ const val PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA = 569
const val PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA = 570
const val PERMISSION_REQUEST_CODE_AUDIO_CALL = 571
const val PERMISSION_REQUEST_CODE_VIDEO_CALL = 572
-const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573
const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577
const val PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT = 578
+const val PERMISSION_REQUEST_CODE_READ_CONTACTS = 579
/**
* Log the used permissions statuses.
diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt
index 9e5af038ef..900d5565dc 100644
--- a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt
+++ b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt
@@ -162,7 +162,7 @@ fun startImportTextFromFileIntent(fragment: Fragment, requestCode: Int) {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
type = "text/plain"
}
- if (intent.resolveActivity(fragment.activity!!.packageManager) != null) {
+ if (intent.resolveActivity(fragment.requireActivity().packageManager) != null) {
fragment.startActivityForResult(intent, requestCode)
} else {
fragment.activity?.toast(R.string.error_no_external_application_found)
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/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
index 7c32a34aff..2b38a1ac25 100644
--- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
+++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
@@ -17,6 +17,9 @@
package im.vector.riotx.features.command
import im.vector.matrix.android.api.MatrixPatterns
+import im.vector.matrix.android.api.session.identity.ThreePid
+import im.vector.riotx.core.extensions.isEmail
+import im.vector.riotx.core.extensions.isMsisdn
import timber.log.Timber
object CommandParser {
@@ -139,15 +142,24 @@ object CommandParser {
if (messageParts.size >= 2) {
val userId = messageParts[1]
- if (MatrixPatterns.isUserId(userId)) {
- ParsedCommand.Invite(
- userId,
- textMessage.substring(Command.INVITE.length + userId.length)
- .trim()
- .takeIf { it.isNotBlank() }
- )
- } else {
- ParsedCommand.ErrorSyntax(Command.INVITE)
+ when {
+ MatrixPatterns.isUserId(userId) -> {
+ ParsedCommand.Invite(
+ userId,
+ textMessage.substring(Command.INVITE.length + userId.length)
+ .trim()
+ .takeIf { it.isNotBlank() }
+ )
+ }
+ userId.isEmail() -> {
+ ParsedCommand.Invite3Pid(ThreePid.Email(userId))
+ }
+ userId.isMsisdn() -> {
+ ParsedCommand.Invite3Pid(ThreePid.Msisdn(userId))
+ }
+ else -> {
+ ParsedCommand.ErrorSyntax(Command.INVITE)
+ }
}
} else {
ParsedCommand.ErrorSyntax(Command.INVITE)
diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
index 44ad2265e1..041da3dcac 100644
--- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
+++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
@@ -16,6 +16,8 @@
package im.vector.riotx.features.command
+import im.vector.matrix.android.api.session.identity.ThreePid
+
/**
* Represent a parsed command
*/
@@ -41,6 +43,7 @@ sealed class ParsedCommand {
class UnbanUser(val userId: String, val reason: String?) : ParsedCommand()
class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand()
class Invite(val userId: String, val reason: String?) : ParsedCommand()
+ class Invite3Pid(val threePid: ThreePid) : ParsedCommand()
class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
class PartRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
class ChangeTopic(val topic: String) : ParsedCommand()
diff --git a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactDetailItem.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactDetailItem.kt
new file mode 100644
index 0000000000..8615838571
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactDetailItem.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.features.contactsbook
+
+import android.widget.TextView
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.riotx.R
+import im.vector.riotx.core.epoxy.ClickListener
+import im.vector.riotx.core.epoxy.VectorEpoxyHolder
+import im.vector.riotx.core.epoxy.VectorEpoxyModel
+import im.vector.riotx.core.epoxy.onClick
+import im.vector.riotx.core.extensions.setTextOrHide
+
+@EpoxyModelClass(layout = R.layout.item_contact_detail)
+abstract class ContactDetailItem : VectorEpoxyModel() {
+
+ @EpoxyAttribute lateinit var threePid: String
+ @EpoxyAttribute var matrixId: String? = null
+ @EpoxyAttribute var clickListener: ClickListener? = null
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ holder.view.onClick(clickListener)
+ holder.nameView.text = threePid
+ holder.matrixIdView.setTextOrHide(matrixId)
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val nameView by bind(R.id.contactDetailName)
+ val matrixIdView by bind(R.id.contactDetailMatrixId)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactItem.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactItem.kt
new file mode 100644
index 0000000000..9a6bf8f144
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactItem.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.contactsbook
+
+import android.widget.ImageView
+import android.widget.TextView
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.riotx.R
+import im.vector.riotx.core.contacts.MappedContact
+import im.vector.riotx.core.epoxy.VectorEpoxyHolder
+import im.vector.riotx.core.epoxy.VectorEpoxyModel
+import im.vector.riotx.features.home.AvatarRenderer
+
+@EpoxyModelClass(layout = R.layout.item_contact_main)
+abstract class ContactItem : VectorEpoxyModel() {
+
+ @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
+ @EpoxyAttribute lateinit var mappedContact: MappedContact
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ // If name is empty, use userId as name and force it being centered
+ holder.nameView.text = mappedContact.displayName
+ avatarRenderer.render(mappedContact, holder.avatarImageView)
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val nameView by bind(R.id.contactDisplayName)
+ val avatarImageView by bind(R.id.contactAvatar)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookAction.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookAction.kt
new file mode 100644
index 0000000000..001630d398
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookAction.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.contactsbook
+
+import im.vector.riotx.core.platform.VectorViewModelAction
+
+sealed class ContactsBookAction : VectorViewModelAction {
+ data class FilterWith(val filter: String) : ContactsBookAction()
+ data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction()
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookController.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookController.kt
new file mode 100644
index 0000000000..796ed0d80c
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookController.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.contactsbook
+
+import com.airbnb.epoxy.EpoxyController
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
+import im.vector.matrix.android.api.session.identity.ThreePid
+import im.vector.riotx.R
+import im.vector.riotx.core.contacts.MappedContact
+import im.vector.riotx.core.epoxy.errorWithRetryItem
+import im.vector.riotx.core.epoxy.loadingItem
+import im.vector.riotx.core.epoxy.noResultItem
+import im.vector.riotx.core.error.ErrorFormatter
+import im.vector.riotx.core.resources.StringProvider
+import im.vector.riotx.features.home.AvatarRenderer
+import javax.inject.Inject
+
+class ContactsBookController @Inject constructor(
+ private val stringProvider: StringProvider,
+ private val avatarRenderer: AvatarRenderer,
+ private val errorFormatter: ErrorFormatter) : EpoxyController() {
+
+ private var state: ContactsBookViewState? = null
+
+ var callback: Callback? = null
+
+ init {
+ requestModelBuild()
+ }
+
+ fun setData(state: ContactsBookViewState) {
+ this.state = state
+ requestModelBuild()
+ }
+
+ override fun buildModels() {
+ val currentState = state ?: return
+ val hasSearch = currentState.searchTerm.isNotEmpty()
+ when (val asyncMappedContacts = currentState.mappedContacts) {
+ is Uninitialized -> renderEmptyState(false)
+ is Loading -> renderLoading()
+ is Success -> renderSuccess(currentState.filteredMappedContacts, hasSearch, currentState.onlyBoundContacts)
+ is Fail -> renderFailure(asyncMappedContacts.error)
+ }
+ }
+
+ private fun renderLoading() {
+ loadingItem {
+ id("loading")
+ loadingText(stringProvider.getString(R.string.loading_contact_book))
+ }
+ }
+
+ private fun renderFailure(failure: Throwable) {
+ errorWithRetryItem {
+ id("error")
+ text(errorFormatter.toHumanReadable(failure))
+ }
+ }
+
+ private fun renderSuccess(mappedContacts: List,
+ hasSearch: Boolean,
+ onlyBoundContacts: Boolean) {
+ if (mappedContacts.isEmpty()) {
+ renderEmptyState(hasSearch)
+ } else {
+ renderContacts(mappedContacts, onlyBoundContacts)
+ }
+ }
+
+ private fun renderContacts(mappedContacts: List, onlyBoundContacts: Boolean) {
+ for (mappedContact in mappedContacts) {
+ contactItem {
+ id(mappedContact.id)
+ mappedContact(mappedContact)
+ avatarRenderer(avatarRenderer)
+ }
+ mappedContact.emails
+ .forEachIndexed { index, it ->
+ if (onlyBoundContacts && it.matrixId == null) return@forEachIndexed
+
+ contactDetailItem {
+ id("${mappedContact.id}-e-$index-${it.email}")
+ threePid(it.email)
+ matrixId(it.matrixId)
+ clickListener {
+ if (it.matrixId != null) {
+ callback?.onMatrixIdClick(it.matrixId)
+ } else {
+ callback?.onThreePidClick(ThreePid.Email(it.email))
+ }
+ }
+ }
+ }
+ mappedContact.msisdns
+ .forEachIndexed { index, it ->
+ if (onlyBoundContacts && it.matrixId == null) return@forEachIndexed
+
+ contactDetailItem {
+ id("${mappedContact.id}-m-$index-${it.phoneNumber}")
+ threePid(it.phoneNumber)
+ matrixId(it.matrixId)
+ clickListener {
+ if (it.matrixId != null) {
+ callback?.onMatrixIdClick(it.matrixId)
+ } else {
+ callback?.onThreePidClick(ThreePid.Msisdn(it.phoneNumber))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun renderEmptyState(hasSearch: Boolean) {
+ val noResultRes = if (hasSearch) {
+ R.string.no_result_placeholder
+ } else {
+ R.string.empty_contact_book
+ }
+ noResultItem {
+ id("noResult")
+ text(stringProvider.getString(noResultRes))
+ }
+ }
+
+ interface Callback {
+ fun onMatrixIdClick(matrixId: String)
+ fun onThreePidClick(threePid: ThreePid)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookFragment.kt
new file mode 100644
index 0000000000..2a2fd9fb5d
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookFragment.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.contactsbook
+
+import android.os.Bundle
+import android.view.View
+import androidx.core.view.isVisible
+import com.airbnb.mvrx.activityViewModel
+import com.airbnb.mvrx.withState
+import com.jakewharton.rxbinding3.widget.checkedChanges
+import com.jakewharton.rxbinding3.widget.textChanges
+import im.vector.matrix.android.api.session.identity.ThreePid
+import im.vector.matrix.android.api.session.user.model.User
+import im.vector.riotx.R
+import im.vector.riotx.core.extensions.cleanup
+import im.vector.riotx.core.extensions.configureWith
+import im.vector.riotx.core.extensions.hideKeyboard
+import im.vector.riotx.core.platform.VectorBaseFragment
+import im.vector.riotx.features.userdirectory.PendingInvitee
+import im.vector.riotx.features.userdirectory.UserDirectoryAction
+import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
+import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
+import im.vector.riotx.features.userdirectory.UserDirectoryViewModel
+import kotlinx.android.synthetic.main.fragment_contacts_book.*
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+class ContactsBookFragment @Inject constructor(
+ val contactsBookViewModelFactory: ContactsBookViewModel.Factory,
+ private val contactsBookController: ContactsBookController
+) : VectorBaseFragment(), ContactsBookController.Callback {
+
+ override fun getLayoutResId() = R.layout.fragment_contacts_book
+ private val viewModel: UserDirectoryViewModel by activityViewModel()
+
+ // Use activityViewModel to avoid loading several times the data
+ private val contactsBookViewModel: ContactsBookViewModel by activityViewModel()
+
+ private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
+ setupRecyclerView()
+ setupFilterView()
+ setupOnlyBoundContactsView()
+ setupCloseView()
+ }
+
+ private fun setupOnlyBoundContactsView() {
+ phoneBookOnlyBoundContacts.checkedChanges()
+ .subscribe {
+ contactsBookViewModel.handle(ContactsBookAction.OnlyBoundContacts(it))
+ }
+ .disposeOnDestroyView()
+ }
+
+ private fun setupFilterView() {
+ phoneBookFilter
+ .textChanges()
+ .skipInitialValue()
+ .debounce(300, TimeUnit.MILLISECONDS)
+ .subscribe {
+ contactsBookViewModel.handle(ContactsBookAction.FilterWith(it.toString()))
+ }
+ .disposeOnDestroyView()
+ }
+
+ override fun onDestroyView() {
+ phoneBookRecyclerView.cleanup()
+ contactsBookController.callback = null
+ super.onDestroyView()
+ }
+
+ private fun setupRecyclerView() {
+ contactsBookController.callback = this
+ phoneBookRecyclerView.configureWith(contactsBookController)
+ }
+
+ private fun setupCloseView() {
+ phoneBookClose.debouncedClicks {
+ sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
+ }
+ }
+
+ override fun invalidate() = withState(contactsBookViewModel) { state ->
+ phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved
+ contactsBookController.setData(state)
+ }
+
+ override fun onMatrixIdClick(matrixId: String) {
+ view?.hideKeyboard()
+ viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
+ sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
+ }
+
+ override fun onThreePidClick(threePid: ThreePid) {
+ view?.hideKeyboard()
+ viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
+ sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewModel.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewModel.kt
new file mode 100644
index 0000000000..3eb6b165b8
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewModel.kt
@@ -0,0 +1,192 @@
+/*
+ * 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.contactsbook
+
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.viewModelScope
+import com.airbnb.mvrx.ActivityViewModelContext
+import com.airbnb.mvrx.FragmentViewModelContext
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.MvRxViewModelFactory
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.ViewModelContext
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.identity.FoundThreePid
+import im.vector.matrix.android.api.session.identity.ThreePid
+import im.vector.riotx.core.contacts.ContactsDataSource
+import im.vector.riotx.core.contacts.MappedContact
+import im.vector.riotx.core.extensions.exhaustive
+import im.vector.riotx.core.platform.EmptyViewEvents
+import im.vector.riotx.core.platform.VectorViewModel
+import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
+import im.vector.riotx.features.invite.InviteUsersToRoomActivity
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import timber.log.Timber
+
+private typealias PhoneBookSearch = String
+
+class ContactsBookViewModel @AssistedInject constructor(@Assisted
+ initialState: ContactsBookViewState,
+ private val contactsDataSource: ContactsDataSource,
+ private val session: Session)
+ : VectorViewModel(initialState) {
+
+ @AssistedInject.Factory
+ interface Factory {
+ fun create(initialState: ContactsBookViewState): ContactsBookViewModel
+ }
+
+ companion object : MvRxViewModelFactory {
+
+ override fun create(viewModelContext: ViewModelContext, state: ContactsBookViewState): ContactsBookViewModel? {
+ return when (viewModelContext) {
+ is FragmentViewModelContext -> (viewModelContext.fragment() as ContactsBookFragment).contactsBookViewModelFactory.create(state)
+ is ActivityViewModelContext -> {
+ when (viewModelContext.activity()) {
+ is CreateDirectRoomActivity -> viewModelContext.activity().contactsBookViewModelFactory.create(state)
+ is InviteUsersToRoomActivity -> viewModelContext.activity().contactsBookViewModelFactory.create(state)
+ else -> error("Wrong activity or fragment")
+ }
+ }
+ else -> error("Wrong activity or fragment")
+ }
+ }
+ }
+
+ private var allContacts: List = emptyList()
+ private var mappedContacts: List = emptyList()
+
+ init {
+ loadContacts()
+
+ selectSubscribe(ContactsBookViewState::searchTerm, ContactsBookViewState::onlyBoundContacts) { _, _ ->
+ updateFilteredMappedContacts()
+ }
+ }
+
+ private fun loadContacts() {
+ setState {
+ copy(
+ mappedContacts = Loading()
+ )
+ }
+
+ viewModelScope.launch(Dispatchers.IO) {
+ allContacts = contactsDataSource.getContacts(
+ withEmails = true,
+ // Do not handle phone numbers for the moment
+ withMsisdn = false
+ )
+ mappedContacts = allContacts
+
+ setState {
+ copy(
+ mappedContacts = Success(allContacts)
+ )
+ }
+
+ performLookup(allContacts)
+ updateFilteredMappedContacts()
+ }
+ }
+
+ private fun performLookup(data: List) {
+ viewModelScope.launch {
+ val threePids = data.flatMap { contact ->
+ contact.emails.map { ThreePid.Email(it.email) } +
+ contact.msisdns.map { ThreePid.Msisdn(it.phoneNumber) }
+ }
+ session.identityService().lookUp(threePids, object : MatrixCallback> {
+ override fun onFailure(failure: Throwable) {
+ // Ignore
+ Timber.w(failure, "Unable to perform the lookup")
+ }
+
+ override fun onSuccess(data: List