Merge remote-tracking branch 'origin/develop' into feature/eric/new-layout-navigation

# Conflicts:
#	vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
#	vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt
This commit is contained in:
ericdecanini 2022-08-22 14:56:14 +02:00
commit edb2d5d78e
79 changed files with 1147 additions and 267 deletions

View File

@ -98,7 +98,8 @@ jobs:
# Skip in forks
if: >
github.repository == 'vector-im/element-android' &&
(contains(github.event.issue.labels.*.name, 'Team: Delight'))
(contains(github.event.issue.labels.*.name, 'Team: Delight') ||
contains(github.event.issue.labels.*.name, 'Z-AppLayout'))
steps:
- uses: octokit/graphql-action@v2.x
with:

1
changelog.d/6506.wip Normal file
View File

@ -0,0 +1 @@
[App Layout] added dialog to configure app layout

1
changelog.d/6787.wip Normal file
View File

@ -0,0 +1 @@
[App Layout] Dialpad moved from bottom navigation tab to a separate activity accessed via home screen context menu

1
changelog.d/6801.wip Normal file
View File

@ -0,0 +1 @@
Adds new chat bottom sheet as the click action of the main FAB in the new app layout

1
changelog.d/6827.bugfix Normal file
View File

@ -0,0 +1 @@
Fixing sign in/up for homeservers that rely on the SSO fallback url

1
changelog.d/6864.bugfix Normal file
View File

@ -0,0 +1 @@
Fixes server selection being unable to trust certificates

1
changelog.d/6884.bugfix Normal file
View File

@ -0,0 +1 @@
Ensure SyncThread is started when the app is launched after a Push has been received.

1
changelog.d/6884.sdk Normal file
View File

@ -0,0 +1 @@
Rename `DebugService.logDbUsageInfo` (resp. `Session.logDbUsageInfo`) to `DebugService.getDbUsageInfo` (resp. `Session.getDbUsageInfo`) and return a String instead of logging. The caller may want to log the String.

1
changelog.d/6891.bugfix Normal file
View File

@ -0,0 +1 @@
Fixes missing firebase notifications after logging in when UnifiedPush distributor is installed

View File

@ -199,7 +199,7 @@ dependencies {
implementation libs.apache.commonsImaging
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.53'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.54'
testImplementation libs.tests.junit
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281

View File

@ -28,7 +28,7 @@ interface DebugService {
fun getAllRealmConfigurations(): List<RealmConfiguration>
/**
* Prints out info on DB size to logcat.
* Get info on DB size.
*/
fun logDbUsageInfo()
fun getDbUsageInfo(): String
}

View File

@ -93,6 +93,8 @@ fun Throwable.isMissingEmailVerification() = this is Failure.ServerError &&
error.code == MatrixError.M_UNAUTHORIZED &&
error.message == "Unable to get validated threepid"
fun Throwable.isUnrecognisedCertificate() = this is Failure.UnrecognizedCertificateFailure
/**
* Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible
*/

View File

@ -323,9 +323,9 @@ interface Session {
fun getUiaSsoFallbackUrl(authenticationSessionId: String): String
/**
* Debug API, will print out info on DB size to logcat.
* Debug API, will return info about the DB.
*/
fun logDbUsageInfo()
fun getDbUsageInfo(): String
/**
* Debug API, return the list of all RealmConfiguration used by this session.

View File

@ -53,6 +53,11 @@ interface SyncService {
*/
fun getSyncState(): SyncState
/**
* This method returns true if the sync thread is alive, i.e. started.
*/
fun isSyncThreadAlive(): Boolean
/**
* This method allows to listen the sync state.
* @return a [LiveData] of [SyncState].

View File

@ -38,7 +38,7 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r
LiveEntityObserver, RealmChangeListener<RealmResults<T>> {
private companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND")
val BACKGROUND_HANDLER = createBackgroundHandler("Matrix-LIVE_ENTITY_BACKGROUND")
}
protected val observerScope = CoroutineScope(SupervisorJob() + BACKGROUND_HANDLER.asCoroutineDispatcher())

View File

@ -19,16 +19,15 @@ package org.matrix.android.sdk.internal.database.tools
import io.realm.Realm
import io.realm.RealmConfiguration
import org.matrix.android.sdk.BuildConfig
import timber.log.Timber
internal class RealmDebugTools(
private val realmConfiguration: RealmConfiguration
) {
/**
* Log info about the DB.
* Get info about the DB.
*/
fun logInfo(baseName: String) {
buildString {
fun getInfo(baseName: String): String {
return buildString {
append("\n$baseName Realm located at : ${realmConfiguration.realmDirectory}/${realmConfiguration.realmFileName}")
if (BuildConfig.LOG_PRIVATE_DATA) {
@ -54,7 +53,6 @@ internal class RealmDebugTools(
separator()
}
}
.let { Timber.i(it) }
}
private fun StringBuilder.separator() = append("\n==============================================")

View File

@ -36,9 +36,9 @@ internal class DefaultDebugService @Inject constructor(
realmConfigurationGlobal
}
override fun logDbUsageInfo() {
RealmDebugTools(realmConfigurationAuth).logInfo("Auth")
RealmDebugTools(realmConfigurationGlobal).logInfo("Global")
sessionManager.getLastSession()?.logDbUsageInfo()
override fun getDbUsageInfo() = buildString {
append(RealmDebugTools(realmConfigurationAuth).getInfo("Auth"))
append(RealmDebugTools(realmConfigurationGlobal).getInfo("Global"))
append(sessionManager.getLastSession()?.getDbUsageInfo())
}
}

View File

@ -40,7 +40,7 @@ internal object MatrixModule {
io = Dispatchers.IO,
computation = Dispatchers.Default,
main = Dispatchers.Main,
crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(),
crypto = createBackgroundHandler("Matrix-Crypto_Thread").asCoroutineDispatcher(),
dmVerif = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
)
}

View File

@ -263,11 +263,11 @@ internal class DefaultSession @Inject constructor(
}
}
override fun logDbUsageInfo() {
RealmDebugTools(realmConfiguration).logInfo("Session")
RealmDebugTools(realmConfigurationCrypto).logInfo("Crypto")
RealmDebugTools(realmConfigurationIdentity).logInfo("Identity")
RealmDebugTools(realmConfigurationContentScanner).logInfo("ContentScanner")
override fun getDbUsageInfo() = buildString {
append(RealmDebugTools(realmConfiguration).getInfo("Session"))
append(RealmDebugTools(realmConfigurationCrypto).getInfo("Crypto"))
append(RealmDebugTools(realmConfigurationIdentity).getInfo("Identity"))
append(RealmDebugTools(realmConfigurationContentScanner).getInfo("ContentScanner"))
}
override fun getRealmConfigurations(): List<RealmConfiguration> {

View File

@ -55,7 +55,7 @@ internal class EventSenderProcessorThread @Inject constructor(
private val queuedTaskFactory: QueuedTaskFactory,
private val taskExecutor: TaskExecutor,
private val memento: QueueMemento
) : Thread("SENDER_THREAD_SID_${sessionParams.credentials.sessionId()}"), EventSenderProcessor {
) : Thread("Matrix-SENDER_THREAD_SID_${sessionParams.credentials.sessionId()}"), EventSenderProcessor {
private fun markAsManaged(task: QueuedTask) {
memento.track(task)

View File

@ -76,7 +76,7 @@ internal class DefaultTimeline(
) : Timeline {
companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("DefaultTimeline_Thread")
val BACKGROUND_HANDLER = createBackgroundHandler("Matrix-DefaultTimeline_Thread")
}
override val timelineID = UUID.randomUUID().toString()

View File

@ -73,6 +73,8 @@ internal class DefaultSyncService @Inject constructor(
override fun getSyncState() = getSyncThread().currentState()
override fun isSyncThreadAlive() = getSyncThread().isAlive
override fun getSyncRequestStateFlow() = syncRequestStateTracker.syncRequestState
override fun hasAlreadySynced(): Boolean {

View File

@ -62,7 +62,7 @@ internal class SyncThread @Inject constructor(
private val backgroundDetectionObserver: BackgroundDetectionObserver,
private val activeCallHandler: ActiveCallHandler,
private val lightweightSettingsStorage: DefaultLightweightSettingsStorage
) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
) : Thread("Matrix-SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
private var state: SyncState = SyncState.Idle
private var liveState = MutableLiveData(state)

View File

@ -49,13 +49,13 @@ internal class DefaultBackgroundDetectionObserver : BackgroundDetectionObserver
}
override fun onStart(owner: LifecycleOwner) {
Timber.v("App returning to foreground…")
Timber.d("App returning to foreground…")
isInBackground = false
listeners.forEach { it.onMoveToForeground() }
}
override fun onStop(owner: LifecycleOwner) {
Timber.v("App going to background…")
Timber.d("App going to background…")
isInBackground = true
listeners.forEach { it.onMoveToBackground() }
}

View File

@ -413,7 +413,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.53'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.54'
// FlowBinding
implementation libs.github.flowBinding

View File

@ -348,6 +348,7 @@
<activity android:name=".features.location.LocationSharingActivity" />
<activity android:name=".features.location.live.map.LiveLocationMapViewActivity" />
<activity android:name=".features.settings.font.FontScaleSettingActivity"/>
<activity android:name=".features.call.dialpad.PstnDialActivity" />
<!-- Services -->

View File

@ -266,7 +266,7 @@ class VectorApplication :
}
private fun createFontThreadHandler(): Handler {
val handlerThread = HandlerThread("fonts")
val handlerThread = HandlerThread("Vector-fonts")
handlerThread.start()
return Handler(handlerThread.looper)
}

View File

@ -20,6 +20,7 @@ import android.content.Context
import arrow.core.Option
import im.vector.app.ActiveSessionDataSource
import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.services.GuardServiceStarter
import im.vector.app.features.call.webrtc.WebRtcCallManager
@ -100,7 +101,13 @@ class ActiveSessionHolder @Inject constructor(
}
suspend fun getOrInitializeSession(startSync: Boolean): Session? {
return activeSessionReference.get() ?: sessionInitializer.tryInitialize(readCurrentSession = { activeSessionReference.get() }) { session ->
return activeSessionReference.get()
?.also {
if (startSync && !it.syncService().isSyncThreadAlive()) {
it.startSyncing(applicationContext)
}
}
?: sessionInitializer.tryInitialize(readCurrentSession = { activeSessionReference.get() }) { session ->
setActiveSession(session)
session.configureAndStart(applicationContext, startSyncing = startSync)
}

View File

@ -63,6 +63,7 @@ import im.vector.app.features.home.room.detail.TimelineFragment
import im.vector.app.features.home.room.detail.search.SearchFragment
import im.vector.app.features.home.room.list.RoomListFragment
import im.vector.app.features.home.room.list.home.HomeRoomListFragment
import im.vector.app.features.home.room.list.home.NewChatBottomSheet
import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
import im.vector.app.features.location.LocationSharingFragment
import im.vector.app.features.location.preview.LocationPreviewFragment
@ -191,6 +192,11 @@ interface FragmentModule {
@FragmentKey(RoomListFragment::class)
fun bindRoomListFragment(fragment: RoomListFragment): Fragment
@Binds
@IntoMap
@FragmentKey(NewChatBottomSheet::class)
fun bindNewChatBottomSheetFragment(fragment: NewChatBottomSheet): Fragment
@Binds
@IntoMap
@FragmentKey(LocalePickerFragment::class)

View File

@ -28,7 +28,7 @@ import org.matrix.android.sdk.api.session.sync.FilterService
import timber.log.Timber
fun Session.configureAndStart(context: Context, startSyncing: Boolean = true) {
Timber.i("Configure and start session for $myUserId")
Timber.i("Configure and start session for $myUserId. startSyncing: $startSyncing")
open()
filterService().setFilter(FilterService.FilterPreset.ElementFilter)
if (startSyncing) {

View File

@ -92,8 +92,6 @@ class UnifiedPushHelper @Inject constructor(
return@launch
}
// By default, use internal solution (fcm/background sync)
UnifiedPush.saveDistributor(context, context.packageName)
val distributors = UnifiedPush.getDistributors(context)
if (distributors.size == 1 && !force) {
@ -101,7 +99,14 @@ class UnifiedPushHelper @Inject constructor(
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
} else {
openDistributorDialogInternal(activity, pushersManager, onDoneRunnable, distributors, !force, !force)
openDistributorDialogInternal(
activity = activity,
pushersManager = pushersManager,
onDoneRunnable = onDoneRunnable,
distributors = distributors,
unregisterFirst = force,
cancellable = !force
)
}
}
}
@ -165,6 +170,12 @@ class UnifiedPushHelper @Inject constructor(
onDoneRunnable?.run()
}
}
.setOnCancelListener {
// By default, use internal solution (fcm/background sync)
UnifiedPush.saveDistributor(context, context.packageName)
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
}
.setCancelable(cancellable)
.show()
}

View File

@ -0,0 +1,109 @@
/*
* Copyright (c) 2022 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.app.features.call.dialpad
import android.os.Bundle
import androidx.appcompat.app.AppCompatDialog
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper
import im.vector.app.features.settings.VectorLocale
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject
@AndroidEntryPoint
class PstnDialActivity : SimpleFragmentActivity() {
@Inject lateinit var callManager: WebRtcCallManager
@Inject lateinit var directRoomHelper: DirectRoomHelper
@Inject lateinit var session: Session
@Inject lateinit var errorFormatter: ErrorFormatter
private var progress: AppCompatDialog? = null
override fun getTitleRes(): Int = R.string.call
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isFirstCreation()) {
addFragment(
views.container,
createDialPadFragment()
)
}
}
private fun handleStartCallWithPhoneNumber(rawNumber: String) {
lifecycleScope.launch {
try {
showLoadingDialog()
val result = DialPadLookup(session, callManager, directRoomHelper).lookupPhoneNumber(rawNumber)
callManager.startOutgoingCall(result.roomId, result.userId, isVideoCall = false)
dismissLoadingDialog()
finish()
} catch (failure: Throwable) {
dismissLoadingDialog()
displayErrorDialog(failure)
}
}
}
private fun createDialPadFragment(): Fragment {
val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, DialPadFragment::class.java.name)
return (fragment as DialPadFragment).apply {
arguments = Bundle().apply {
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true)
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
}
callback = object : DialPadFragment.Callback {
override fun onOkClicked(formatted: String?, raw: String?) {
if (raw.isNullOrEmpty()) return
handleStartCallWithPhoneNumber(raw)
}
}
}
}
private fun showLoadingDialog() {
progress?.dismiss()
progress = MaterialProgressDialog(this)
.show(getString(R.string.please_wait))
}
private fun dismissLoadingDialog() {
progress?.dismiss()
}
private fun displayErrorDialog(throwable: Throwable) {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
}

View File

@ -59,6 +59,7 @@ import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.disclaimer.showDisclaimerDialog
import im.vector.app.features.home.room.list.actions.RoomListSharedAction
import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel
import im.vector.app.features.home.room.list.home.layout.HomeLayoutSettingBottomDialogFragment
import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.matrixto.OriginOfMatrixTo
import im.vector.app.features.navigation.Navigator
@ -292,6 +293,11 @@ class HomeActivity :
.show(supportFragmentManager, "SPACE_SETTINGS")
}
private fun showLayoutSettings() {
HomeLayoutSettingBottomDialogFragment()
.show(supportFragmentManager, "LAYOUT_SETTINGS")
}
private fun openSpaceInvite(spaceId: String) {
SpaceInviteBottomSheet.newInstance(spaceId)
.show(supportFragmentManager, "SPACE_INVITE")
@ -613,6 +619,10 @@ class HomeActivity :
navigator.openSettings(this)
true
}
R.id.menu_home_layout_settings -> {
showLayoutSettings()
true
}
R.id.menu_home_invite_friends -> {
launchInviteFriends()
true

View File

@ -16,6 +16,7 @@
package im.vector.app.features.home
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
@ -45,12 +46,11 @@ import im.vector.app.core.ui.views.KeysBackupBanner
import im.vector.app.databinding.FragmentNewHomeDetailBinding
import im.vector.app.features.call.SharedKnownCallsViewModel
import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.dialpad.DialPadFragment
import im.vector.app.features.call.dialpad.PstnDialActivity
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.list.home.HomeRoomListFragment
import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.popup.VerificationVectorAlert
import im.vector.app.features.settings.VectorLocale
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
import im.vector.app.features.themes.ThemeUtils
@ -102,6 +102,10 @@ class NewHomeDetailFragment @Inject constructor(
viewModel.handle(HomeDetailAction.MarkAllRoomsRead)
true
}
R.id.menu_home_dialpad -> {
startActivity(Intent(requireContext(), PstnDialActivity::class.java))
true
}
else -> false
}
}
@ -110,6 +114,7 @@ class NewHomeDetailFragment @Inject constructor(
withState(viewModel) { state ->
val isRoomList = state.currentTab is HomeTab.RoomList
menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = isRoomList && hasUnreadRooms
menu.findItem(R.id.menu_home_dialpad).isVisible = state.showDialPadTab
}
}
@ -142,14 +147,10 @@ class NewHomeDetailFragment @Inject constructor(
updateUIForTab(currentTab)
}
viewModel.onEach(HomeDetailViewState::showDialPadTab) { showDialPadTab ->
updateTabVisibilitySafely(R.id.bottom_action_dial_pad, showDialPadTab)
}
viewModel.observeViewEvents { viewEvent ->
when (viewEvent) {
HomeDetailViewEvents.CallStarted -> handleCallStarted()
is HomeDetailViewEvents.FailToCall -> showFailure(viewEvent.failure)
HomeDetailViewEvents.CallStarted -> Unit
is HomeDetailViewEvents.FailToCall -> Unit
HomeDetailViewEvents.Loading -> showLoadingDialog()
}
}
@ -179,6 +180,17 @@ class NewHomeDetailFragment @Inject constructor(
}
}
private fun navigateBack() {
val previousSpaceId = spaceStateHandler.getSpaceBackstack().removeLastOrNull()
val parentSpaceId = spaceStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull()
setCurrentSpace(previousSpaceId ?: parentSpaceId)
}
private fun setCurrentSpace(spaceId: String?) {
spaceStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false)
sharedActionViewModel.post(HomeActivitySharedAction.OnCloseSpace)
}
private fun handleCallStarted() {
dismissLoadingDialog()
val fragmentTag = HomeTab.DialPad.toFragmentTag()
@ -333,30 +345,15 @@ class NewHomeDetailFragment @Inject constructor(
add(R.id.roomListContainer, HomeRoomListFragment::class.java, null, fragmentTag)
}
is HomeTab.DialPad -> {
add(R.id.roomListContainer, createDialPadFragment(), fragmentTag)
throw NotImplementedError("this tab shouldn't exists when app layout is enabled")
}
}
} else {
if (tab is HomeTab.DialPad) {
(fragmentToShow as? DialPadFragment)?.applyCallback()
}
attach(fragmentToShow)
}
}
}
private fun createDialPadFragment(): Fragment {
val fragment = childFragmentManager.fragmentFactory.instantiate(vectorBaseActivity.classLoader, DialPadFragment::class.java.name)
return (fragment as DialPadFragment).apply {
arguments = Bundle().apply {
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true)
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
}
applyCallback()
}
}
private fun updateTabVisibilitySafely(tabId: Int, isVisible: Boolean) {
val wasVisible = views.bottomNavigationView.menu.findItem(tabId).isVisible
views.bottomNavigationView.menu.findItem(tabId).isVisible = isVisible
@ -458,9 +455,8 @@ class NewHomeDetailFragment @Inject constructor(
return this
}
override fun onBackPressed(toolbarButton: Boolean) = try {
val lastSpace = spaceStateHandler.popSpaceBackstack()
spaceStateHandler.setCurrentSpace(lastSpace, isForwardNavigation = false)
override fun onBackPressed(toolbarButton: Boolean) = if (spaceStateHandler.getCurrentSpace() != null) {
navigateBack()
true
} catch (e: NoSuchElementException) {
false

View File

@ -19,7 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.helper
import android.os.Handler
import android.os.HandlerThread
private const val THREAD_NAME = "Timeline_Building_Thread"
private const val THREAD_NAME = "Vector-Timeline_Building_Thread"
object TimelineAsyncHelper {

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2022 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.app.features.home.room.list.home
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import org.matrix.android.sdk.api.extensions.orFalse
import javax.inject.Inject
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "layout_preferences")
class HomeLayoutPreferencesStore @Inject constructor(
private val context: Context
) {
private val areRecentsEnbabled = booleanPreferencesKey("SETTINGS_PREFERENCES_HOME_RECENTS")
private val areFiltersEnabled = booleanPreferencesKey("SETTINGS_PREFERENCES_HOME_FILTERS")
private val isAZOrderingEnabled = booleanPreferencesKey("SETTINGS_PREFERENCES_USE_AZ_ORDER")
val areRecentsEnabledFlow: Flow<Boolean> = context.dataStore.data
.map { preferences -> preferences[areRecentsEnbabled].orFalse() }
.distinctUntilChanged()
val areFiltersEnabledFlow: Flow<Boolean> = context.dataStore.data
.map { preferences -> preferences[areFiltersEnabled].orFalse() }
.distinctUntilChanged()
val isAZOrderingEnabledFlow: Flow<Boolean> = context.dataStore.data
.map { preferences -> preferences[isAZOrderingEnabled].orFalse() }
.distinctUntilChanged()
suspend fun setRecentsEnabled(isEnabled: Boolean) {
context.dataStore.edit { settings ->
settings[areRecentsEnbabled] = isEnabled
}
}
suspend fun setFiltersEnabled(isEnabled: Boolean) {
context.dataStore.edit { settings ->
settings[areFiltersEnabled] = isEnabled
}
}
suspend fun setAZOrderingEnabled(isEnabled: Boolean) {
context.dataStore.edit { settings ->
settings[isAZOrderingEnabled] = isEnabled
}
}
}

View File

@ -74,6 +74,8 @@ class HomeRoomListFragment @Inject constructor(
private lateinit var stateRestorer: LayoutManagerStateRestorer
private val newChatBottomSheet = NewChatBottomSheet()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomListBinding {
return FragmentRoomListBinding.inflate(inflater, container, false)
}
@ -160,13 +162,22 @@ class HomeRoomListFragment @Inject constructor(
}.launchIn(lifecycleScope)
views.roomListView.adapter = concatAdapter
// we need to force scroll when recents/filter tabs are added to make them visible
concatAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == 0) {
layoutManager.scrollToPosition(0)
}
}
})
}
private fun setupFabs() {
showFABs()
views.newLayoutCreateChatButton.setOnClickListener {
// Click action for create chat modal goes here (Issue #6717)
newChatBottomSheet.show(requireActivity().supportFragmentManager, NewChatBottomSheet.TAG)
}
views.newLayoutOpenSpacesButton.setOnClickListener {
@ -203,6 +214,9 @@ class HomeRoomListFragment @Inject constructor(
}
private fun setUpAdapters(sections: Set<HomeRoomSection>) {
concatAdapter.adapters.forEach {
concatAdapter.removeAdapter(it)
}
sections.forEach {
concatAdapter.addAdapter(getAdapterForData(it))
}
@ -232,12 +246,11 @@ class HomeRoomListFragment @Inject constructor(
is HomeRoomSection.RoomSummaryData -> {
HomeFilteredRoomsController(
roomSummaryItemFactory,
showFilters = section.showFilters,
).also { controller ->
controller.listener = this
controller.onFilterChanged = ::onRoomFilterChanged
section.filtersData.onEach {
controller.submitFiltersData(it)
controller.submitFiltersData(it.getOrNull())
}.launchIn(lifecycleScope)
section.list.observe(viewLifecycleOwner) { list ->
controller.submitList(list)

View File

@ -34,6 +34,7 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@ -53,12 +54,14 @@ import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.state.isPublic
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.flow.flow
class HomeRoomListViewModel @AssistedInject constructor(
@Assisted initialState: HomeRoomListViewState,
private val session: Session,
private val spaceStateHandler: SpaceStateHandler,
private val preferencesStore: HomeLayoutPreferencesStore,
) : VectorViewModel<HomeRoomListViewState, HomeRoomListAction, HomeRoomListViewEvents>(initialState) {
@AssistedFactory
@ -82,17 +85,30 @@ class HomeRoomListViewModel @AssistedInject constructor(
init {
configureSections()
observePreferences()
}
private fun configureSections() {
private fun observePreferences() {
preferencesStore.areRecentsEnabledFlow.onEach {
configureSections()
}.launchIn(viewModelScope)
preferencesStore.isAZOrderingEnabledFlow.onEach {
configureSections()
}.launchIn(viewModelScope)
}
private fun configureSections() = viewModelScope.launch {
val newSections = mutableSetOf<HomeRoomSection>()
val areSettingsEnabled = preferencesStore.areRecentsEnabledFlow.first()
if (areSettingsEnabled) {
newSections.add(getRecentRoomsSection())
}
newSections.add(getFilteredRoomsSection())
viewModelScope.launch {
_sections.emit(newSections)
}
setState {
copy(state = StateView.State.Content)
@ -111,13 +127,17 @@ class HomeRoomListViewModel @AssistedInject constructor(
)
}
private fun getFilteredRoomsSection(): HomeRoomSection.RoomSummaryData {
private suspend fun getFilteredRoomsSection(): HomeRoomSection.RoomSummaryData {
val builder = RoomSummaryQueryParams.Builder().also {
it.memberships = listOf(Membership.JOIN)
}
val params = getFilteredQueryParams(HomeRoomFilter.ALL, builder.build())
val sortOrder = RoomSortOrder.ACTIVITY // #6506
val sortOrder = if (preferencesStore.isAZOrderingEnabledFlow.first()) {
RoomSortOrder.NAME
} else {
RoomSortOrder.ACTIVITY
}
val liveResults = session.roomService().getFilteredPagedRoomSummariesLive(
params,
@ -141,13 +161,12 @@ class HomeRoomListViewModel @AssistedInject constructor(
return HomeRoomSection.RoomSummaryData(
list = liveResults.livePagedList,
showFilters = true, // #6506
filtersData = getFiltersDataFlow()
)
}
private fun getFiltersDataFlow(): SharedFlow<List<HomeRoomFilter>> {
val flow = MutableSharedFlow<List<HomeRoomFilter>>(replay = 1)
private fun getFiltersDataFlow(): SharedFlow<Optional<List<HomeRoomFilter>>> {
val flow = MutableSharedFlow<Optional<List<HomeRoomFilter>>>(replay = 1)
val favouritesFlow = session.flow()
.liveRoomSummaries(
@ -168,9 +187,10 @@ class HomeRoomListViewModel @AssistedInject constructor(
.map { it.isNotEmpty() }
.distinctUntilChanged()
favouritesFlow.combine(dmsFLow) { hasFavourite, hasDm ->
hasFavourite to hasDm
}.onEach { (hasFavourite, hasDm) ->
combine(favouritesFlow, dmsFLow, preferencesStore.areFiltersEnabledFlow) { hasFavourite, hasDm, areFiltersEnabled ->
Triple(hasFavourite, hasDm, areFiltersEnabled)
}.onEach { (hasFavourite, hasDm, areFiltersEnabled) ->
if (areFiltersEnabled) {
val filtersData = mutableListOf(
HomeRoomFilter.ALL,
HomeRoomFilter.UNREADS
@ -185,8 +205,10 @@ class HomeRoomListViewModel @AssistedInject constructor(
HomeRoomFilter.PEOPlE
)
}
flow.emit(filtersData)
flow.emit(Optional.from(filtersData))
} else {
flow.emit(Optional.empty())
}
}.launchIn(viewModelScope)
return flow

View File

@ -21,12 +21,12 @@ import androidx.paging.PagedList
import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter
import kotlinx.coroutines.flow.SharedFlow
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.Optional
sealed class HomeRoomSection {
data class RoomSummaryData(
val list: LiveData<PagedList<RoomSummary>>,
val showFilters: Boolean,
val filtersData: SharedFlow<List<HomeRoomFilter>>
val filtersData: SharedFlow<Optional<List<HomeRoomFilter>>>,
) : HomeRoomSection()
data class RecentRoomsData(

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2022 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.app.features.home.room.list.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.databinding.FragmentNewChatBottomSheetBinding
import im.vector.app.features.navigation.Navigator
import javax.inject.Inject
@AndroidEntryPoint
class NewChatBottomSheet @Inject constructor() : BottomSheetDialogFragment() {
@Inject lateinit var navigator: Navigator
private lateinit var binding: FragmentNewChatBottomSheetBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentNewChatBottomSheetBinding.inflate(inflater, container, false)
initFABs()
return binding.root
}
private fun initFABs() {
binding.startChat.setOnClickListener {
navigator.openCreateDirectRoom(requireActivity())
}
binding.createRoom.setOnClickListener {
navigator.openCreateRoom(requireActivity())
}
binding.exploreRooms.setOnClickListener {
navigator.openRoomDirectory(requireContext())
}
}
companion object {
const val TAG = "NewChatBottomSheet"
}
}

View File

@ -28,7 +28,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
class HomeFilteredRoomsController(
private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val showFilters: Boolean,
) : PagedListEpoxyController<RoomSummary>(
// Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler()
@ -48,7 +47,7 @@ class HomeFilteredRoomsController(
override fun addModels(models: List<EpoxyModel<*>>) {
val host = this
if (showFilters) {
if (host.filtersData != null) {
roomFilterHeaderItem {
id("filter_header")
filtersData(host.filtersData)
@ -58,7 +57,7 @@ class HomeFilteredRoomsController(
super.addModels(models)
}
fun submitFiltersData(data: List<HomeRoomFilter>) {
fun submitFiltersData(data: List<HomeRoomFilter>?) {
this.filtersData = data
requestForcedModelBuild()
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (c) 2022 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.app.features.home.room.list.home.layout
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.databinding.BottomSheetHomeLayoutSettingsBinding
import im.vector.app.features.home.room.list.home.HomeLayoutPreferencesStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class HomeLayoutSettingBottomDialogFragment : VectorBaseBottomSheetDialogFragment<BottomSheetHomeLayoutSettingsBinding>() {
@Inject lateinit var preferencesStore: HomeLayoutPreferencesStore
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetHomeLayoutSettingsBinding {
return BottomSheetHomeLayoutSettingsBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
views.homeLayoutSettingsRecents.isChecked = preferencesStore.areRecentsEnabledFlow.first()
views.homeLayoutSettingsFilters.isChecked = preferencesStore.areFiltersEnabledFlow.first()
if (preferencesStore.isAZOrderingEnabledFlow.first()) {
views.homeLayoutSettingsSortName.isChecked = true
} else {
views.homeLayoutSettingsSortActivity.isChecked = true
}
}
views.homeLayoutSettingsRecents.setOnCheckedChangeListener { _, isChecked ->
setRecentsEnabled(isChecked)
}
views.homeLayoutSettingsFilters.setOnCheckedChangeListener { _, isChecked ->
setFiltersEnabled(isChecked)
}
views.homeLayoutSettingsSortGroup.setOnCheckedChangeListener { _, checkedId ->
setAzOrderingEnabled(checkedId == R.id.home_layout_settings_sort_name)
}
}
private fun setRecentsEnabled(isEnabled: Boolean) = lifecycleScope.launch {
preferencesStore.setRecentsEnabled(isEnabled)
}
private fun setFiltersEnabled(isEnabled: Boolean) = lifecycleScope.launch {
preferencesStore.setFiltersEnabled(isEnabled)
}
private fun setAzOrderingEnabled(isEnabled: Boolean) = lifecycleScope.launch {
preferencesStore.setAZOrderingEnabled(isEnabled)
}
}

View File

@ -59,7 +59,13 @@ class RecentRoomCarouselController @Inject constructor(
data?.let { data ->
carousel {
id("recents_carousel")
padding(Carousel.Padding(host.hPadding, host.itemSpacing))
padding(Carousel.Padding(
host.hPadding,
0,
host.hPadding,
0,
host.itemSpacing)
)
withModelsFrom(data) { roomSummary ->
val onClick = host.listener?.let { it::onRoomClicked }
val onLongClick = host.listener?.let { it::onRoomLongClicked }

View File

@ -85,7 +85,7 @@ abstract class AbstractSSOLoginFragment<VB : ViewBinding> : AbstractLoginFragmen
private fun prefetchIfNeeded() {
withState(loginViewModel) { state ->
if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) {
if (state.loginMode.hasSso() && state.loginMode.ssoState().isFallback()) {
// in this case we can prefetch (not other cases for privacy concerns)
loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,

View File

@ -17,12 +17,13 @@
package im.vector.app.features.login
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.network.ssl.Fingerprint
import timber.log.Timber
import javax.inject.Inject
class HomeServerConnectionConfigFactory @Inject constructor() {
fun create(url: String?): HomeServerConnectionConfig? {
fun create(url: String?, fingerprint: Fingerprint? = null): HomeServerConnectionConfig? {
if (url == null) {
return null
}
@ -30,6 +31,13 @@ class HomeServerConnectionConfigFactory @Inject constructor() {
return try {
HomeServerConnectionConfig.Builder()
.withHomeServerUri(url)
.run {
if (fingerprint == null) {
this
} else {
withAllowedFingerPrints(listOf(fingerprint))
}
}
.build()
} catch (t: Throwable) {
Timber.e(t)

View File

@ -37,7 +37,6 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.isInvalidPassword
@ -100,14 +99,12 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
}
}
private fun setupSocialLoginButtons(state: LoginViewState) {
views.loginSocialLoginButtons.mode = when (state.signMode) {
private fun ssoMode(state: LoginViewState) = when (state.signMode) {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> SocialLoginButtonsView.Mode.MODE_SIGN_UP
SignMode.SignIn,
SignMode.SignInWithMatrixId -> SocialLoginButtonsView.Mode.MODE_SIGN_IN
}
}
private fun submit() {
cleanupUi()
@ -201,9 +198,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
if (state.loginMode is LoginMode.SsoAndPassword) {
views.loginSocialLoginContainer.isVisible = true
views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted()
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(provider: SsoIdentityProvider?) {
views.loginSocialLoginButtons.render(state.loginMode.ssoState, ssoMode(state)) { provider ->
loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
@ -211,7 +206,6 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
)
?.let { openInCustomTab(it) }
}
}
} else {
views.loginSocialLoginContainer.isVisible = false
views.loginSocialLoginButtons.ssoIdentityProviders = null
@ -272,7 +266,6 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
setupUi(state)
setupAutoFill(state)
setupSocialLoginButtons(state)
setupButtons(state)
when (state.asyncLoginAction) {

View File

@ -18,22 +18,21 @@ package im.vector.app.features.login
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
sealed class LoginMode : Parcelable { // Parcelable because persist state
@Parcelize object Unknown : LoginMode()
@Parcelize object Password : LoginMode()
@Parcelize data class Sso(val ssoIdentityProviders: List<SsoIdentityProvider>?) : LoginMode()
@Parcelize data class SsoAndPassword(val ssoIdentityProviders: List<SsoIdentityProvider>?) : LoginMode()
@Parcelize data class Sso(val ssoState: SsoState) : LoginMode()
@Parcelize data class SsoAndPassword(val ssoState: SsoState) : LoginMode()
@Parcelize object Unsupported : LoginMode()
}
fun LoginMode.ssoIdentityProviders(): List<SsoIdentityProvider>? {
fun LoginMode.ssoState(): SsoState {
return when (this) {
is LoginMode.Sso -> ssoIdentityProviders
is LoginMode.SsoAndPassword -> ssoIdentityProviders
else -> null
is LoginMode.Sso -> ssoState
is LoginMode.SsoAndPassword -> ssoState
else -> SsoState.Fallback
}
}

View File

@ -25,7 +25,7 @@ import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginSignupSigninSelectionBinding
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import im.vector.app.features.login.SocialLoginButtonsView.Mode
import javax.inject.Inject
/**
@ -73,9 +73,7 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOLogi
when (state.loginMode) {
is LoginMode.SsoAndPassword -> {
views.loginSignupSigninSignInSocialLoginContainer.isVisible = true
views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders()?.sorted()
views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(provider: SsoIdentityProvider?) {
views.loginSignupSigninSocialLoginButtons.render(state.loginMode.ssoState(), Mode.MODE_CONTINUE) { provider ->
loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
@ -84,7 +82,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOLogi
?.let { openInCustomTab(it) }
}
}
}
else -> {
// SSO only is managed without container as well as No sso
views.loginSignupSigninSignInSocialLoginContainer.isVisible = false

View File

@ -223,7 +223,7 @@ class LoginViewModel @AssistedInject constructor(
setState {
copy(
signMode = SignMode.SignIn,
loginMode = LoginMode.Sso(action.ssoIdentityProviders),
loginMode = LoginMode.Sso(action.ssoIdentityProviders.toSsoState()),
homeServerUrlFromUser = action.homeServerUrl,
homeServerUrl = action.homeServerUrl,
deviceId = action.deviceId
@ -816,8 +816,8 @@ class LoginViewModel @AssistedInject constructor(
val loginMode = when {
// SSO login is taken first
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) &&
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders)
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders)
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders.toSsoState())
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState())
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
}

View File

@ -160,8 +160,11 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
}
}
fun SocialLoginButtonsView.render(ssoProviders: List<SsoIdentityProvider>?, mode: SocialLoginButtonsView.Mode, listener: (SsoIdentityProvider?) -> Unit) {
fun SocialLoginButtonsView.render(state: SsoState, mode: SocialLoginButtonsView.Mode, listener: (SsoIdentityProvider?) -> Unit) {
this.mode = mode
this.ssoIdentityProviders = ssoProviders?.sorted()
this.ssoIdentityProviders = when (state) {
SsoState.Fallback -> null
is SsoState.IdentityProviders -> state.providers.sorted()
}
this.listener = SocialLoginButtonsView.InteractionListener { listener(it) }
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2022 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.app.features.login
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
sealed interface SsoState : Parcelable {
@Parcelize
data class IdentityProviders(val providers: List<SsoIdentityProvider>) : SsoState
@Parcelize
object Fallback : SsoState
fun isFallback() = this == Fallback
fun providersOrNull() = when (this) {
Fallback -> null
is IdentityProviders -> providers.takeIf { it.isNotEmpty() }
}
}
fun List<SsoIdentityProvider>?.toSsoState() = this
?.takeIf { it.isNotEmpty() }
?.let { SsoState.IdentityProviders(it) }
?: SsoState.Fallback

View File

@ -82,7 +82,7 @@ sealed interface OnboardingAction : VectorViewModelAction {
data class PostViewEvent(val viewEvent: OnboardingViewEvents) : OnboardingAction
data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction
data class UserAcceptCertificate(val fingerprint: Fingerprint, val retryAction: OnboardingAction) : OnboardingAction
object PersonalizeProfile : OnboardingAction
data class UpdateDisplayName(val displayName: String) : OnboardingAction

View File

@ -21,6 +21,7 @@ import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.features.login.ServerType
import im.vector.app.features.login.SignMode
import org.matrix.android.sdk.api.auth.registration.Stage
import org.matrix.android.sdk.api.failure.Failure as SdkFailure
/**
* Transient events for Login.
@ -29,6 +30,7 @@ sealed class OnboardingViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : OnboardingViewEvents()
data class Failure(val throwable: Throwable) : OnboardingViewEvents()
data class DeeplinkAuthenticationFailure(val retryAction: OnboardingAction) : OnboardingViewEvents()
data class UnrecognisedCertificateFailure(val retryAction: OnboardingAction, val cause: SdkFailure.UnrecognizedCertificateFailure) : OnboardingViewEvents()
object DisplayRegistrationFallback : OnboardingViewEvents()
data class DisplayRegistrationStage(val stage: Stage) : OnboardingViewEvents()

View File

@ -60,7 +60,10 @@ import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.RegistrationAvailability
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
import org.matrix.android.sdk.api.failure.isUnrecognisedCertificate
import org.matrix.android.sdk.api.network.ssl.Fingerprint
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
import timber.log.Timber
@ -113,10 +116,6 @@ class OnboardingViewModel @AssistedInject constructor(
}
}
// Store the last action, to redo it after user has trusted the untrusted certificate
private var lastAction: OnboardingAction? = null
private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null
private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
private val defaultHomeserverUrl = matrixOrgUrl
@ -146,9 +145,9 @@ class OnboardingViewModel @AssistedInject constructor(
is OnboardingAction.UpdateServerType -> handleUpdateServerType(action)
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
is OnboardingAction.InitWith -> handleInitWith(action)
is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) }
is OnboardingAction.HomeServerChange -> handleHomeserverChange(action)
is OnboardingAction.UserNameEnteredAction -> handleUserNameEntered(action)
is AuthenticateAction -> withAction(action) { handleAuthenticateAction(action) }
is AuthenticateAction -> handleAuthenticateAction(action)
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is OnboardingAction.ResetPassword -> handleResetPassword(action)
@ -221,11 +220,6 @@ class OnboardingViewModel @AssistedInject constructor(
)
}
private fun withAction(action: OnboardingAction, block: (OnboardingAction) -> Unit) {
lastAction = action
block(action)
}
private fun handleAuthenticateAction(action: AuthenticateAction) {
when (action) {
is AuthenticateAction.Register -> handleRegisterWith(action.username, action.password, action.initialDeviceName)
@ -276,20 +270,13 @@ class OnboardingViewModel @AssistedInject constructor(
private fun handleUserAcceptCertificate(action: OnboardingAction.UserAcceptCertificate) {
// It happens when we get the login flow, or during direct authentication.
// So alter the homeserver config and retrieve again the login flow
when (val finalLastAction = lastAction) {
is OnboardingAction.HomeServerChange.SelectHomeServer -> {
currentHomeServerConnectionConfig
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
?.let { startAuthenticationFlow(finalLastAction, it, serverTypeOverride = null) }
}
when (action.retryAction) {
is OnboardingAction.HomeServerChange -> handleHomeserverChange(action.retryAction, fingerprint = action.fingerprint)
is AuthenticateAction.LoginDirect ->
handleDirectLogin(
finalLastAction,
HomeServerConnectionConfig.Builder()
action.retryAction,
// Will be replaced by the task
.withHomeServerUri("https://dummy.org")
.withAllowedFingerPrints(listOf(action.fingerprint))
.build()
homeServerConnectionConfigFactory.create("https://dummy.org", action.fingerprint)
)
else -> Unit
}
@ -589,9 +576,19 @@ class OnboardingViewModel @AssistedInject constructor(
currentJob = viewModelScope.launch {
directLoginUseCase.execute(action, homeServerConnectionConfig).fold(
onSuccess = { onSessionCreated(it, authenticationDescription = AuthenticationDescription.Login) },
onFailure = {
onFailure = { error ->
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.Failure(it))
when {
error.isUnrecognisedCertificate() -> {
_viewEvents.post(
OnboardingViewEvents.UnrecognisedCertificateFailure(
retryAction = action,
cause = error as Failure.UnrecognizedCertificateFailure
)
)
}
else -> _viewEvents.post(OnboardingViewEvents.Failure(error))
}
}
)
}
@ -682,8 +679,13 @@ class OnboardingViewModel @AssistedInject constructor(
}
}
private fun handleHomeserverChange(action: OnboardingAction.HomeServerChange, serverTypeOverride: ServerType? = null, postAction: suspend () -> Unit = {}) {
val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl)
private fun handleHomeserverChange(
action: OnboardingAction.HomeServerChange,
serverTypeOverride: ServerType? = null,
fingerprint: Fingerprint? = null,
postAction: suspend () -> Unit = {},
) {
val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl, fingerprint)
if (homeServerConnectionConfig == null) {
// This is invalid
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
@ -698,8 +700,6 @@ class OnboardingViewModel @AssistedInject constructor(
serverTypeOverride: ServerType?,
postAction: suspend () -> Unit = {},
) {
currentHomeServerConnectionConfig = homeServerConnectionConfig
currentJob = viewModelScope.launch {
setState { copy(isLoading = true) }
runCatching { startAuthenticationFlowUseCase.execute(homeServerConnectionConfig) }.fold(
@ -723,9 +723,10 @@ class OnboardingViewModel @AssistedInject constructor(
retryAction = (trigger as OnboardingAction.HomeServerChange.SelectHomeServer).resetToDefaultUrl()
)
)
else -> _viewEvents.post(
OnboardingViewEvents.Failure(error)
)
error.isUnrecognisedCertificate() -> {
_viewEvents.post(OnboardingViewEvents.UnrecognisedCertificateFailure(trigger, error as Failure.UnrecognizedCertificateFailure))
}
else -> _viewEvents.post(OnboardingViewEvents.Failure(error))
}
}

View File

@ -18,6 +18,7 @@ package im.vector.app.features.onboarding
import im.vector.app.core.extensions.containsAllItems
import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.toSsoState
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
@ -50,8 +51,8 @@ class StartAuthenticationFlowUseCase @Inject constructor(
)
private fun LoginFlowResult.findPreferredLoginMode() = when {
supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(ssoIdentityProviders)
supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders)
supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(ssoIdentityProviders.toSsoState())
supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders.toSsoState())
supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
}

View File

@ -33,7 +33,6 @@ import im.vector.app.features.onboarding.OnboardingViewEvents
import im.vector.app.features.onboarding.OnboardingViewModel
import im.vector.app.features.onboarding.OnboardingViewState
import kotlinx.coroutines.CancellationException
import org.matrix.android.sdk.api.failure.Failure
/**
* Parent Fragment for all the login/registration screens.
@ -68,6 +67,7 @@ abstract class AbstractFtueAuthFragment<VB : ViewBinding> : VectorBaseFragment<V
private fun handleOnboardingViewEvents(viewEvents: OnboardingViewEvents) {
when (viewEvents) {
is OnboardingViewEvents.Failure -> showFailure(viewEvents.throwable)
is OnboardingViewEvents.UnrecognisedCertificateFailure -> showUnrecognizedCertificateFailure(viewEvents)
else ->
// This is handled by the Activity
Unit
@ -84,20 +84,20 @@ abstract class AbstractFtueAuthFragment<VB : ViewBinding> : VectorBaseFragment<V
is CancellationException ->
/* Ignore this error, user has cancelled the action */
Unit
is Failure.UnrecognizedCertificateFailure -> showUnrecognizedCertificateFailure(throwable)
else -> onError(throwable)
}
}
private fun showUnrecognizedCertificateFailure(failure: Failure.UnrecognizedCertificateFailure) {
private fun showUnrecognizedCertificateFailure(event: OnboardingViewEvents.UnrecognisedCertificateFailure) {
// Ask the user to accept the certificate
val cause = event.cause
unrecognizedCertificateDialog.show(requireActivity(),
failure.fingerprint,
failure.url,
cause.fingerprint,
cause.url,
object : UnrecognizedCertificateDialog.Callback {
override fun onAccept() {
// User accept the certificate
viewModel.handle(OnboardingAction.UserAcceptCertificate(failure.fingerprint))
viewModel.handle(OnboardingAction.UserAcceptCertificate(cause.fingerprint, event.retryAction))
}
override fun onIgnore() {

View File

@ -26,7 +26,7 @@ import com.airbnb.mvrx.withState
import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.features.login.SSORedirectRouterActivity
import im.vector.app.features.login.hasSso
import im.vector.app.features.login.ssoIdentityProviders
import im.vector.app.features.login.ssoState
abstract class AbstractSSOFtueAuthFragment<VB : ViewBinding> : AbstractFtueAuthFragment<VB>() {
@ -88,7 +88,7 @@ abstract class AbstractSSOFtueAuthFragment<VB : ViewBinding> : AbstractFtueAuthF
private fun prefetchIfNeeded() {
withState(viewModel) { state ->
if (state.selectedHomeserver.preferredLoginMode.hasSso() && state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders().isNullOrEmpty()) {
if (state.selectedHomeserver.preferredLoginMode.hasSso() && state.selectedHomeserver.preferredLoginMode.ssoState().isFallback()) {
// in this case we can prefetch (not other cases for privacy concerns)
viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,

View File

@ -38,13 +38,13 @@ import im.vector.app.databinding.FragmentFtueCombinedLoginBinding
import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SSORedirectRouterActivity
import im.vector.app.features.login.SocialLoginButtonsView
import im.vector.app.features.login.SsoState
import im.vector.app.features.login.render
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewEvents
import im.vector.app.features.onboarding.OnboardingViewState
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import reactivecircus.flowbinding.android.widget.textChanges
import javax.inject.Inject
@ -125,11 +125,11 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
when (state.selectedHomeserver.preferredLoginMode) {
is LoginMode.SsoAndPassword -> {
showUsernamePassword()
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState)
}
is LoginMode.Sso -> {
hideUsernamePassword()
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState)
}
else -> {
showUsernamePassword()
@ -138,10 +138,10 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
}
}
private fun renderSsoProviders(deviceId: String?, ssoProviders: List<SsoIdentityProvider>?) {
views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
views.ssoButtonsHeader.isVisible = views.ssoGroup.isVisible && views.loginEntryGroup.isVisible
views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id ->
private fun renderSsoProviders(deviceId: String?, ssoState: SsoState) {
views.ssoGroup.isVisible = true
views.ssoButtonsHeader.isVisible = isUsernameAndPasswordVisible()
views.ssoButtons.render(ssoState, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id ->
viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = deviceId,
@ -163,6 +163,8 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
views.loginEntryGroup.isVisible = true
}
private fun isUsernameAndPasswordVisible() = views.loginEntryGroup.isVisible
private fun setupAutoFill() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
views.loginInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)

View File

@ -44,6 +44,7 @@ import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding
import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SSORedirectRouterActivity
import im.vector.app.features.login.SocialLoginButtonsView
import im.vector.app.features.login.SsoState
import im.vector.app.features.login.render
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction
@ -51,7 +52,6 @@ import im.vector.app.features.onboarding.OnboardingViewEvents
import im.vector.app.features.onboarding.OnboardingViewState
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
import org.matrix.android.sdk.api.failure.isInvalidPassword
import org.matrix.android.sdk.api.failure.isInvalidUsername
@ -205,14 +205,14 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
}
when (state.selectedHomeserver.preferredLoginMode) {
is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState)
else -> hideSsoProviders()
}
}
private fun renderSsoProviders(deviceId: String?, ssoProviders: List<SsoIdentityProvider>?) {
views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { provider ->
private fun renderSsoProviders(deviceId: String?, ssoState: SsoState) {
views.ssoGroup.isVisible = true
views.ssoButtons.render(ssoState, SocialLoginButtonsView.Mode.MODE_CONTINUE) { provider ->
viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = deviceId,

View File

@ -37,7 +37,8 @@ import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SSORedirectRouterActivity
import im.vector.app.features.login.ServerType
import im.vector.app.features.login.SignMode
import im.vector.app.features.login.SocialLoginButtonsView
import im.vector.app.features.login.SocialLoginButtonsView.Mode
import im.vector.app.features.login.render
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewEvents
import im.vector.app.features.onboarding.OnboardingViewState
@ -45,7 +46,6 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import org.matrix.android.sdk.api.failure.isInvalidPassword
import org.matrix.android.sdk.api.failure.isInvalidUsername
import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
@ -111,13 +111,11 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
}
}
private fun setupSocialLoginButtons(state: OnboardingViewState) {
views.loginSocialLoginButtons.mode = when (state.signMode) {
private fun ssoMode(state: OnboardingViewState) = when (state.signMode) {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> SocialLoginButtonsView.Mode.MODE_SIGN_UP
SignMode.SignUp -> Mode.MODE_SIGN_UP
SignMode.SignIn,
SignMode.SignInWithMatrixId -> SocialLoginButtonsView.Mode.MODE_SIGN_IN
}
SignMode.SignInWithMatrixId -> Mode.MODE_SIGN_IN
}
private fun submit() {
@ -215,9 +213,7 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
if (state.selectedHomeserver.preferredLoginMode is LoginMode.SsoAndPassword) {
views.loginSocialLoginContainer.isVisible = true
views.loginSocialLoginButtons.ssoIdentityProviders = state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders?.sorted()
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(provider: SsoIdentityProvider?) {
views.loginSocialLoginButtons.render(state.selectedHomeserver.preferredLoginMode.ssoState, ssoMode(state)) { provider ->
viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
@ -225,7 +221,6 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
)
?.let { openInCustomTab(it) }
}
}
} else {
views.loginSocialLoginContainer.isVisible = false
views.loginSocialLoginButtons.ssoIdentityProviders = null
@ -305,7 +300,6 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
setupUi(state)
setupAutoFill(state)
setupSocialLoginButtons(state)
setupButtons(state)
if (state.isLoading) {

View File

@ -30,11 +30,10 @@ import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SSORedirectRouterActivity
import im.vector.app.features.login.ServerType
import im.vector.app.features.login.SignMode
import im.vector.app.features.login.SocialLoginButtonsView
import im.vector.app.features.login.ssoIdentityProviders
import im.vector.app.features.login.SocialLoginButtonsView.Mode
import im.vector.app.features.login.render
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewState
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import javax.inject.Inject
/**
@ -80,9 +79,7 @@ class FtueAuthSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOF
when (state.selectedHomeserver.preferredLoginMode) {
is LoginMode.SsoAndPassword -> {
views.loginSignupSigninSignInSocialLoginContainer.isVisible = true
views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders()?.sorted()
views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(provider: SsoIdentityProvider?) {
views.loginSignupSigninSocialLoginButtons.render(state.selectedHomeserver.preferredLoginMode.ssoState, Mode.MODE_CONTINUE) { provider ->
viewModel.fetchSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
@ -91,7 +88,6 @@ class FtueAuthSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOF
?.let { openInCustomTab(it) }
}
}
}
else -> {
// SSO only is managed without container as well as No sso
views.loginSignupSigninSignInSocialLoginContainer.isVisible = false

View File

@ -202,6 +202,7 @@ class FtueAuthVariant(
openMsisdnConfirmation(viewEvents.msisdn)
}
is OnboardingViewEvents.Failure,
is OnboardingViewEvents.UnrecognisedCertificateFailure,
is OnboardingViewEvents.Loading ->
// This is handled by the Fragments
Unit

View File

@ -78,6 +78,7 @@ class BugReporter @Inject constructor(
private val systemLocaleProvider: SystemLocaleProvider,
private val matrix: Matrix,
private val buildMeta: BuildMeta,
private val processInfo: ProcessInfo,
private val sdkIntProvider: BuildVersionSdkIntProvider,
) {
var inMultiWindowMode = false
@ -499,10 +500,26 @@ class BugReporter @Inject constructor(
*/
fun openBugReportScreen(activity: FragmentActivity, reportType: ReportType = ReportType.BUG_REPORT) {
screenshot = takeScreenshot(activity)
matrix.debugService().logDbUsageInfo()
logDbInfo()
logProcessInfo()
logOtherInfo()
activity.startActivity(BugReportActivity.intent(activity, reportType))
}
private fun logOtherInfo() {
Timber.i("SyncThread state: " + activeSessionHolder.getSafeActiveSession()?.syncService()?.getSyncState())
}
private fun logDbInfo() {
val dbInfo = matrix.debugService().getDbUsageInfo()
Timber.i(dbInfo)
}
private fun logProcessInfo() {
val pInfo = processInfo.getInfo()
Timber.i(pInfo)
}
private fun rageShakeAppNameForReport(reportType: ReportType): String {
// As per https://github.com/matrix-org/rageshake
// app: Identifier for the application (eg 'riot-web').

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) 2022 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.app.features.rageshake
import android.annotation.SuppressLint
import android.app.Application
import android.os.Build
import android.os.Process
import java.lang.reflect.Method
import javax.inject.Inject
class ProcessInfo @Inject constructor() {
fun getInfo() = buildString {
append("===========================================\n")
append("* PROCESS INFO *\n")
append("===========================================\n")
val processId = Process.myPid()
append("ProcessId: $processId\n")
append("ProcessName: ${getProcessName()}\n")
append(getThreadInfo())
append("===========================================\n")
}
@SuppressLint("PrivateApi")
private fun getProcessName(): String? {
return if (Build.VERSION.SDK_INT >= 28) {
Application.getProcessName()
} else {
try {
val activityThread = Class.forName("android.app.ActivityThread")
val getProcessName: Method = activityThread.getDeclaredMethod("currentProcessName")
getProcessName.invoke(null) as? String
} catch (t: Throwable) {
null
}
}
}
private fun getThreadInfo() = buildString {
append("Thread activeCount: ${Thread.activeCount()}\n")
Thread.getAllStackTraces().keys
.sortedBy { it.name }
.forEach { thread -> append(thread.getInfo()) }
}
}
private fun Thread.getInfo() = buildString {
append("Thread '$name':")
append(" id: $id")
append(" priority: $priority")
append(" group name: ${threadGroup?.name ?: "null"}")
append(" state: $state")
append(" isAlive: $isAlive")
append(" isDaemon: $isDaemon")
append(" isInterrupted: $isInterrupted")
append("\n")
}

View File

@ -63,7 +63,7 @@ class SoftLogoutFragment @Inject constructor(
LoginAction.SetupSsoForSessionRecovery(
softLogoutViewState.homeServerUrl,
softLogoutViewState.deviceId,
mode.ssoIdentityProviders
mode.ssoState.providersOrNull()
)
)
}
@ -72,7 +72,7 @@ class SoftLogoutFragment @Inject constructor(
LoginAction.SetupSsoForSessionRecovery(
softLogoutViewState.homeServerUrl,
softLogoutViewState.deviceId,
mode.ssoIdentityProviders
mode.ssoState.providersOrNull()
)
)
}

View File

@ -33,6 +33,7 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.hasUnsavedKeys
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.toSsoState
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.LoginType
@ -115,8 +116,8 @@ class SoftLogoutViewModel @AssistedInject constructor(
val loginMode = when {
// SSO login is taken first
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) &&
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders)
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders)
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders.toSsoState())
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState())
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
else -> LoginMode.Unsupported
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.283,21.44C17.649,21.44 22,17.088 22,11.719C22,6.351 17.649,1.999 12.283,1.999C6.916,1.999 2.566,6.351 2.566,11.719C2.566,13.223 2.907,14.648 3.517,15.918L2.045,20.705C1.808,21.474 2.531,22.194 3.299,21.953L8.046,20.47C9.326,21.091 10.764,21.44 12.283,21.44Z"
android:fillColor="#737D8C"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M17.048,4.105C17.107,3.556 16.708,3.064 16.159,3.006C15.61,2.948 15.118,3.346 15.059,3.895L14.668,7.602H9.613L9.983,4.105C10.041,3.556 9.642,3.064 9.093,3.006C8.544,2.948 8.052,3.346 7.994,3.895L7.602,7.602H4.834C4.281,7.602 3.834,8.05 3.834,8.602C3.834,9.154 4.281,9.602 4.834,9.602H7.39L6.872,14.505H4C3.448,14.505 3,14.953 3,15.505C3,16.058 3.448,16.506 4,16.506H6.661L6.331,19.622C6.273,20.171 6.671,20.663 7.221,20.721C7.77,20.779 8.262,20.381 8.32,19.832L8.589,17.288C8.319,16.818 8.165,16.274 8.165,15.693C8.165,14.871 8.474,14.122 8.984,13.555L9.401,9.602H12.959C13.538,8.776 14.497,8.235 15.583,8.235C16.669,8.235 17.628,8.776 18.207,9.602H19.379C19.931,9.602 20.379,9.154 20.379,8.602C20.379,8.05 19.931,7.602 19.379,7.602H16.679L17.048,4.105ZM15.583,10.382C16.135,10.382 16.583,10.83 16.583,11.382V14.693H19.852C20.404,14.693 20.852,15.141 20.852,15.693C20.852,16.246 20.404,16.694 19.852,16.694H16.583V20.004C16.583,20.557 16.135,21.004 15.583,21.004C15.031,21.004 14.583,20.557 14.583,20.004V16.694H11.313C10.76,16.694 10.313,16.246 10.313,15.693C10.313,15.141 10.76,14.693 11.313,14.693H14.583V11.382C14.583,10.83 15.031,10.382 15.583,10.382Z"
android:fillColor="#737D8C"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M16.647,3.006C17.196,3.065 17.593,3.558 17.533,4.108L17.147,7.684H19.998C20.55,7.684 20.998,8.132 20.998,8.684C20.998,9.236 20.55,9.684 19.998,9.684H16.931L16.8,10.891C16.058,10.967 15.356,11.181 14.722,11.508L14.919,9.684H9.582L9.039,14.708H11.898C11.67,15.332 11.545,16.006 11.544,16.708H8.822L8.455,20.111C8.395,20.66 7.902,21.057 7.353,20.997C6.804,20.938 6.407,20.445 6.466,19.896L6.811,16.708H3.999C3.447,16.708 2.999,16.26 2.999,15.708C2.999,15.156 3.447,14.708 3.999,14.708H7.027L7.57,9.684H4.864C4.312,9.684 3.864,9.236 3.864,8.684C3.864,8.132 4.312,7.684 4.864,7.684H7.786L8.196,3.893C8.255,3.344 8.748,2.947 9.298,3.006C9.847,3.065 10.244,3.558 10.184,4.108L9.798,7.684H15.135L15.545,3.893C15.604,3.344 16.097,2.947 16.647,3.006Z"
android:fillColor="#737D8C"
android:fillType="evenOdd"/>
<path
android:pathData="M19.003,16.765C19.003,17.817 18.151,18.669 17.1,18.669C16.048,18.669 15.196,17.817 15.196,16.765C15.196,15.714 16.048,14.862 17.1,14.862C18.151,14.862 19.003,15.714 19.003,16.765ZM20.332,18.698C20.67,18.133 20.865,17.472 20.865,16.765C20.865,14.686 19.179,13 17.1,13C15.02,13 13.334,14.686 13.334,16.765C13.334,18.845 15.02,20.531 17.1,20.531C17.806,20.531 18.467,20.336 19.032,19.998C19.062,20.038 19.094,20.077 19.131,20.114L20.745,21.727C21.108,22.091 21.698,22.091 22.061,21.727C22.425,21.364 22.425,20.774 22.061,20.411L20.448,18.797C20.411,18.76 20.372,18.728 20.332,18.698Z"
android:fillColor="#737D8C"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rootLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorSurface"
android:orientation="vertical">
<TextView
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:text="@string/home_layout_preferences"
android:textAllCaps="true" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/home_layout_settings_recents"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_marginHorizontal="16dp"
android:checked="true"
android:text="@string/home_layout_preferences_recents" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/home_layout_settings_filters"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_marginHorizontal="16dp"
android:checked="true"
android:text="@string/home_layout_preferences_filters" />
<TextView
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
android:text="@string/home_layout_preferences_sort_by"
android:textAllCaps="true" />
<RadioGroup
android:id="@+id/home_layout_settings_sort_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:orientation="vertical">
<RadioButton
android:id="@+id/home_layout_settings_sort_activity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/home_layout_preferences_sort_activity"
android:textColor="?vctr_content_primary" />
<RadioButton
android:id="@+id/home_layout_settings_sort_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/home_layout_preferences_sort_name"
android:textColor="?vctr_content_primary" />
</RadioGroup>
</LinearLayout>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/start_chat"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:drawablePadding="16dp"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/start_chat"
android:textColor="?vctr_content_primary"
android:textSize="16sp"
app:drawableStartCompat="@drawable/ic_chat" />
<TextView
android:id="@+id/create_room"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:drawablePadding="16dp"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/create_room"
android:textColor="?vctr_content_primary"
android:textSize="16sp"
app:drawableStartCompat="@drawable/ic_room_add" />
<TextView
android:id="@+id/explore_rooms"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:drawablePadding="16dp"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/explore_rooms"
android:textColor="?vctr_content_primary"
android:textSize="16sp"
app:drawableStartCompat="@drawable/ic_room_explore" />
</LinearLayout>

View File

@ -3,6 +3,10 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/menu_home_layout_settings"
android:title="@string/home_layout_preferences"/>
<item
android:id="@+id/menu_home_invite_friends"
android:title="@string/invite_friends"
@ -37,6 +41,6 @@
android:icon="@drawable/ic_home_search"
android:title="@string/home_filter_placeholder_home"
app:iconTint="?vctr_content_secondary"
app:showAsAction="always" />
app:showAsAction="ifRoom" />
</menu>

View File

@ -1,9 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/menu_home_mark_all_as_read"
android:icon="@drawable/ic_material_done"
android:title="@string/action_mark_all_as_read" />
<item
android:id="@+id/menu_home_dialpad"
android:title="@string/call_dial_pad_title"
android:visible="false"
app:showAsAction="never"
tools:visible="true" />
</menu>

View File

@ -137,7 +137,10 @@
<!-- Home Screen -->
<string name="all_chats">All Chats</string>
<string name="start_chat">Start Chat</string>
<string name="create_room">Create Room</string>
<string name="change_space">Change Space</string>
<string name="explore_rooms">Explore Rooms</string>
<!-- Last seen time -->
@ -424,6 +427,15 @@
<!-- Home screen -->
<string name="home_filter_placeholder_home">Filter room names</string>
<string name="home_layout_preferences">Layout preferences</string>
<!-- Home screen layout settings -->
<string name="home_layout_preferences_filters">Show filters</string>
<string name="home_layout_preferences_recents">Show recents</string>
<string name="home_layout_preferences_sort_by">Sort by</string>
<string name="home_layout_preferences_sort_activity">Activity</string>
<string name="home_layout_preferences_sort_name">A - Z</string>
<!-- Home fragment -->
<string name="invitations_header">Invites</string>

View File

@ -48,6 +48,7 @@ import im.vector.app.test.fakes.FakeVectorOverrides
import im.vector.app.test.fakes.toTestString
import im.vector.app.test.fixtures.a401ServerError
import im.vector.app.test.fixtures.aHomeServerCapabilities
import im.vector.app.test.fixtures.anUnrecognisedCertificateError
import im.vector.app.test.test
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
@ -58,6 +59,7 @@ import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import org.matrix.android.sdk.api.auth.registration.Stage
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.network.ssl.Fingerprint
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
@ -65,10 +67,12 @@ private const val A_DISPLAY_NAME = "a display name"
private const val A_PICTURE_FILENAME = "a-picture.png"
private val A_SERVER_ERROR = a401ServerError()
private val AN_ERROR = RuntimeException("an error!")
private val AN_UNRECOGNISED_CERTIFICATE_ERROR = anUnrecognisedCertificateError()
private val A_LOADABLE_REGISTER_ACTION = RegisterAction.StartRegistration
private val A_NON_LOADABLE_REGISTER_ACTION = RegisterAction.CheckIfEmailHasBeenValidated(delayMillis = -1L)
private val A_RESULT_IGNORED_REGISTER_ACTION = RegisterAction.SendAgainThreePid
private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canChangeDisplayName = true, canChangeAvatar = true)
private val A_FINGERPRINT = Fingerprint(ByteArray(1), Fingerprint.HashType.SHA1)
private val ANY_CONTINUING_REGISTRATION_RESULT = RegistrationActionHandler.Result.NextStage(Stage.Dummy(mandatory = true))
private val A_DIRECT_LOGIN = OnboardingAction.AuthenticateAction.LoginDirect("@a-user:id.org", "a-password", "a-device-name")
private const val A_HOMESERVER_URL = "https://edited-homeserver.org"
@ -320,6 +324,25 @@ class OnboardingViewModelTest {
.finish()
}
@Test
fun `given has sign in with matrix id sign mode, when handling login or register action fails with certificate error, then emits error`() = runTest {
viewModelWith(initialState.copy(signMode = SignMode.SignInWithMatrixId))
fakeDirectLoginUseCase.givenFailureResult(A_DIRECT_LOGIN, config = null, cause = AN_UNRECOGNISED_CERTIFICATE_ERROR)
givenInitialisesSession(fakeSession)
val test = viewModel.test()
viewModel.handle(A_DIRECT_LOGIN)
test
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(isLoading = false) }
)
.assertEvents(OnboardingViewEvents.UnrecognisedCertificateFailure(A_DIRECT_LOGIN, AN_UNRECOGNISED_CERTIFICATE_ERROR))
.finish()
}
@Test
fun `when handling SignUp then sets sign mode to sign up and starts registration`() = runTest {
givenRegistrationResultFor(RegisterAction.StartRegistration, ANY_CONTINUING_REGISTRATION_RESULT)
@ -406,7 +429,7 @@ class OnboardingViewModelTest {
@Test
fun `given unavailable deeplink, when selecting homeserver, then emits failure with default homeserver as retry action`() = runTest {
fakeContext.givenHasConnection()
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG)
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, fingerprint = null, A_HOMESERVER_CONFIG)
fakeStartAuthenticationFlowUseCase.givenHomeserverUnavailable(A_HOMESERVER_CONFIG)
val test = viewModel.test()
@ -548,6 +571,44 @@ class OnboardingViewModelTest {
.finish()
}
@Test
fun `when editing homeserver errors with certificate error, then emits error`() = runTest {
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, fingerprint = null, A_HOMESERVER_CONFIG)
fakeStartAuthenticationFlowUseCase.givenErrors(A_HOMESERVER_CONFIG, AN_UNRECOGNISED_CERTIFICATE_ERROR)
val editAction = OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL)
val test = viewModel.test()
viewModel.handle(editAction)
test
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(isLoading = false) }
)
.assertEvents(OnboardingViewEvents.UnrecognisedCertificateFailure(editAction, AN_UNRECOGNISED_CERTIFICATE_ERROR))
.finish()
}
@Test
fun `when selecting homeserver errors with certificate error, then emits error`() = runTest {
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, fingerprint = null, A_HOMESERVER_CONFIG)
fakeStartAuthenticationFlowUseCase.givenErrors(A_HOMESERVER_CONFIG, AN_UNRECOGNISED_CERTIFICATE_ERROR)
val selectAction = OnboardingAction.HomeServerChange.SelectHomeServer(A_HOMESERVER_URL)
val test = viewModel.test()
viewModel.handle(selectAction)
test
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(isLoading = false) }
)
.assertEvents(OnboardingViewEvents.UnrecognisedCertificateFailure(selectAction, AN_UNRECOGNISED_CERTIFICATE_ERROR))
.finish()
}
@Test
fun `given unavailable full matrix id, when a register username is entered, then emits availability error`() = runTest {
viewModelWith(initialRegistrationState("ignored-url"))
@ -724,6 +785,76 @@ class OnboardingViewModelTest {
.finish()
}
@Test
fun `given in sign in mode, when accepting user certificate with SelectHomeserver retry action, then emits OnHomeserverEdited`() = runTest {
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignIn))
val test = viewModel.test()
fakeVectorFeatures.givenCombinedLoginEnabled()
givenCanSuccessfullyUpdateHomeserver(
A_HOMESERVER_URL,
SELECTED_HOMESERVER_STATE,
config = A_HOMESERVER_CONFIG.copy(allowedFingerprints = listOf(A_FINGERPRINT)),
fingerprint = A_FINGERPRINT,
)
viewModel.handle(OnboardingAction.UserAcceptCertificate(A_FINGERPRINT, OnboardingAction.HomeServerChange.SelectHomeServer(A_HOMESERVER_URL)))
test
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(selectedHomeserver = SELECTED_HOMESERVER_STATE) },
{ copy(signMode = SignMode.SignIn) },
{ copy(isLoading = false) }
)
.assertEvents(OnboardingViewEvents.OpenCombinedLogin)
.finish()
}
@Test
fun `given in sign up mode, when accepting user certificate with EditHomeserver retry action, then emits OnHomeserverEdited`() = runTest {
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
givenCanSuccessfullyUpdateHomeserver(
A_HOMESERVER_URL,
SELECTED_HOMESERVER_STATE,
config = A_HOMESERVER_CONFIG.copy(allowedFingerprints = listOf(A_FINGERPRINT)),
fingerprint = A_FINGERPRINT,
)
val test = viewModel.test()
viewModel.handle(OnboardingAction.UserAcceptCertificate(A_FINGERPRINT, OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL)))
test
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(selectedHomeserver = SELECTED_HOMESERVER_STATE) },
{ copy(isLoading = false) }
)
.assertEvents(OnboardingViewEvents.OnHomeserverEdited)
.finish()
}
@Test
fun `given DirectLogin retry action, when accepting user certificate, then logs in directly`() = runTest {
fakeHomeServerConnectionConfigFactory.givenConfigFor("https://dummy.org", A_FINGERPRINT, A_HOMESERVER_CONFIG)
fakeDirectLoginUseCase.givenSuccessResult(A_DIRECT_LOGIN, config = A_HOMESERVER_CONFIG, result = fakeSession)
givenInitialisesSession(fakeSession)
val test = viewModel.test()
viewModel.handle(OnboardingAction.UserAcceptCertificate(A_FINGERPRINT, A_DIRECT_LOGIN))
test
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(isLoading = false) }
)
.assertEvents(OnboardingViewEvents.OnAccountSignedIn)
.finish()
}
@Test
fun `given can successfully start password reset, when resetting password, then emits confirmation email sent`() = runTest {
viewModelWith(initialState.copy(selectedHomeserver = SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES))
@ -991,15 +1122,20 @@ class OnboardingViewModelTest {
fakeRegistrationActionHandler.givenResultsFor(results)
}
private fun givenCanSuccessfullyUpdateHomeserver(homeserverUrl: String, resultingState: SelectedHomeserverState) {
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, A_HOMESERVER_CONFIG)
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
private fun givenCanSuccessfullyUpdateHomeserver(
homeserverUrl: String,
resultingState: SelectedHomeserverState,
config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG,
fingerprint: Fingerprint? = null,
) {
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config)
fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration)
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString())
}
private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) {
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, A_HOMESERVER_CONFIG)
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint = null, A_HOMESERVER_CONFIG)
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error))
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())

View File

@ -17,6 +17,7 @@
package im.vector.app.features.onboarding
import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SsoState
import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
import im.vector.app.test.fakes.FakeAuthenticationService
import im.vector.app.test.fakes.FakeUri
@ -32,7 +33,11 @@ import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
private const val A_DECLARED_HOMESERVER_URL = "https://foo.bar"
private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(homeServerUri = FakeUri().instance)
private val SSO_IDENTITY_PROVIDERS = emptyList<SsoIdentityProvider>()
private val FALLBACK_SSO_IDENTITY_PROVIDERS = emptyList<SsoIdentityProvider>()
private val SSO_IDENTITY_PROVIDERS = listOf(SsoIdentityProvider(id = "id", "name", null, "sso-brand"))
private val SSO_LOGIN_TYPE = listOf(LoginFlowTypes.SSO)
private val SSO_AND_PASSWORD_LOGIN_TYPES = listOf(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD)
private val PASSWORD_LOGIN_TYPE = listOf(LoginFlowTypes.PASSWORD)
class StartAuthenticationFlowUseCaseTest {
@ -47,7 +52,7 @@ class StartAuthenticationFlowUseCaseTest {
@Test
fun `given empty login result when starting authentication flow then returns empty result`() = runTest {
val loginResult = aLoginResult()
val loginResult = aLoginResult(supportedLoginTypes = emptyList())
fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
val result = useCase.execute(A_HOMESERVER_CONFIG)
@ -57,55 +62,81 @@ class StartAuthenticationFlowUseCaseTest {
}
@Test
fun `given login supports SSO and Password when starting authentication flow then prefers SsoAndPassword`() = runTest {
val supportedLoginTypes = listOf(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD)
val loginResult = aLoginResult(supportedLoginTypes = supportedLoginTypes)
fun `given empty sso providers and login supports SSO and Password when starting authentication flow then prefers fallback SsoAndPassword`() = runTest {
val loginResult = aLoginResult(supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES, ssoProviders = emptyList())
fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
val result = useCase.execute(A_HOMESERVER_CONFIG)
result shouldBeEqualTo expectedResult(
supportedLoginTypes = supportedLoginTypes,
preferredLoginMode = LoginMode.SsoAndPassword(SSO_IDENTITY_PROVIDERS),
supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES,
preferredLoginMode = LoginMode.SsoAndPassword(SsoState.Fallback),
)
verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
}
@Test
fun `given login supports SSO when starting authentication flow then prefers Sso`() = runTest {
val supportedLoginTypes = listOf(LoginFlowTypes.SSO)
val loginResult = aLoginResult(supportedLoginTypes = supportedLoginTypes)
fun `given sso providers and login supports SSO and Password when starting authentication flow then prefers SsoAndPassword`() = runTest {
val loginResult = aLoginResult(supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES, ssoProviders = SSO_IDENTITY_PROVIDERS)
fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
val result = useCase.execute(A_HOMESERVER_CONFIG)
result shouldBeEqualTo expectedResult(
supportedLoginTypes = supportedLoginTypes,
preferredLoginMode = LoginMode.Sso(SSO_IDENTITY_PROVIDERS),
supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES,
preferredLoginMode = LoginMode.SsoAndPassword(SsoState.IdentityProviders(SSO_IDENTITY_PROVIDERS)),
)
verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
}
@Test
fun `given empty sso providers and login supports SSO when starting authentication flow then prefers fallback Sso`() = runTest {
val loginResult = aLoginResult(supportedLoginTypes = SSO_LOGIN_TYPE, ssoProviders = emptyList())
fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
val result = useCase.execute(A_HOMESERVER_CONFIG)
result shouldBeEqualTo expectedResult(
supportedLoginTypes = SSO_LOGIN_TYPE,
preferredLoginMode = LoginMode.Sso(SsoState.Fallback),
)
verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
}
@Test
fun `given identity providers and login supports SSO when starting authentication flow then prefers Sso`() = runTest {
val loginResult = aLoginResult(supportedLoginTypes = SSO_LOGIN_TYPE, ssoProviders = SSO_IDENTITY_PROVIDERS)
fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
val result = useCase.execute(A_HOMESERVER_CONFIG)
result shouldBeEqualTo expectedResult(
supportedLoginTypes = SSO_LOGIN_TYPE,
preferredLoginMode = LoginMode.Sso(SsoState.IdentityProviders(SSO_IDENTITY_PROVIDERS)),
)
verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
}
@Test
fun `given login supports Password when starting authentication flow then prefers Password`() = runTest {
val supportedLoginTypes = listOf(LoginFlowTypes.PASSWORD)
val loginResult = aLoginResult(supportedLoginTypes = supportedLoginTypes)
val loginResult = aLoginResult(supportedLoginTypes = PASSWORD_LOGIN_TYPE)
fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
val result = useCase.execute(A_HOMESERVER_CONFIG)
result shouldBeEqualTo expectedResult(
supportedLoginTypes = supportedLoginTypes,
supportedLoginTypes = PASSWORD_LOGIN_TYPE,
preferredLoginMode = LoginMode.Password,
)
verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
}
private fun aLoginResult(
supportedLoginTypes: List<String> = emptyList()
supportedLoginTypes: List<String>,
ssoProviders: List<SsoIdentityProvider> = FALLBACK_SSO_IDENTITY_PROVIDERS
) = LoginFlowResult(
supportedLoginTypes = supportedLoginTypes,
ssoIdentityProviders = SSO_IDENTITY_PROVIDERS,
ssoIdentityProviders = ssoProviders,
isLoginAndRegistrationSupported = true,
homeServerUrl = A_DECLARED_HOMESERVER_URL,
isOutdatedHomeserver = false,

View File

@ -20,11 +20,12 @@ import im.vector.app.features.login.HomeServerConnectionConfigFactory
import io.mockk.every
import io.mockk.mockk
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.network.ssl.Fingerprint
class FakeHomeServerConnectionConfigFactory {
val instance: HomeServerConnectionConfigFactory = mockk()
fun givenConfigFor(url: String, config: HomeServerConnectionConfig) {
every { instance.create(url) } returns config
fun givenConfigFor(url: String, fingerprint: Fingerprint? = null, config: HomeServerConnectionConfig) {
every { instance.create(url, fingerprint) } returns config
}
}

View File

@ -31,6 +31,10 @@ class FakeStartAuthenticationFlowUseCase {
coEvery { instance.execute(config) } returns result
}
fun givenErrors(config: HomeServerConnectionConfig, error: Throwable) {
coEvery { instance.execute(config) } throws error
}
fun givenHomeserverUnavailable(config: HomeServerConnectionConfig) {
coEvery { instance.execute(config) } throws aHomeserverUnavailableError()
}

View File

@ -18,6 +18,7 @@ package im.vector.app.test.fixtures
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.network.ssl.Fingerprint
import java.net.UnknownHostException
import javax.net.ssl.HttpsURLConnection
@ -38,3 +39,5 @@ fun aLoginEmailUnknownError() = Failure.ServerError(
)
fun aHomeserverUnavailableError() = Failure.NetworkConnection(UnknownHostException())
fun anUnrecognisedCertificateError() = Failure.UnrecognizedCertificateFailure("a-url", Fingerprint(ByteArray(1), Fingerprint.HashType.SHA1))