From 618e9a4f523c43bab9d257479e85e50aca1d06ac Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Sep 2019 16:05:04 +0200 Subject: [PATCH 01/44] Fix issue with bad versionCode generated by Buildkite (#553) --- CHANGES.md | 3 ++- vector/build.gradle | 13 ++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 109352d34a..8769b11ea7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,13 +13,14 @@ Other changes: Bugfix: - Fix crash due to missing informationData (#535) - Progress in initial sync dialog is decreasing for a step and should not (#532) + - Fix rendering issue of accepted third party invitation event Translations: - Build: - Fix issue with version name (#533) - - Fix rendering issue of accepted third party invitation event + - Fix issue with bad versionCode generated by Buildkite (#553) Changes in RiotX 0.4.0 (2019-XX-XX) =================================================== diff --git a/vector/build.gradle b/vector/build.gradle index c69b5e5538..de47937676 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -51,8 +51,15 @@ static def gitRevisionDate() { } static def gitBranchName() { - def cmd = "git rev-parse --abbrev-ref HEAD" - return cmd.execute().text.trim() + def fromEnv = System.env.BUILDKITE_BRANCH as String ?: "" + + if (!fromEnv.isEmpty()) { + return fromEnv + } else { + // Note: this command return "HEAD" on Buildkite, so use the system env 'BUILDKITE_BRANCH' content first + def cmd = "git rev-parse --abbrev-ref HEAD" + return cmd.execute().text.trim() + } } static def getVersionSuffix() { @@ -75,7 +82,7 @@ project.android.buildTypes.all { buildType -> // 64 bits have greater value than 32 bits ext.abiVersionCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86": 3, "x86_64": 4].withDefault { 0 } -def buildNumber = System.getenv("BUILDKITE_BUILD_NUMBER") as Integer ?: 0 +def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0 android { compileSdkVersion 28 From 05b2092ffcc96205382884f300b50056fcfd6f44 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Sep 2019 10:07:55 +0200 Subject: [PATCH 02/44] Login: move existing code to a Fragment, MvRx style --- .../vector/riotx/core/di/ScreenComponent.kt | 17 +- .../riotx/features/login/LoginActivity.kt | 122 +----------- .../riotx/features/login/LoginFragment.kt | 177 ++++++++++++++++++ .../riotx/features/login/LoginViewModel.kt | 47 +++++ .../riotx/features/login/LoginViewState.kt | 30 +++ 5 files changed, 266 insertions(+), 127 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 6bfbddbabb..66281aad37 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -42,15 +42,12 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFrag import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet -import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet -import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuFragment -import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionFragment -import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet -import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.action.* import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.login.LoginActivity +import im.vector.riotx.features.login.LoginFragment import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.navigation.Navigator @@ -65,13 +62,7 @@ import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment -import im.vector.riotx.features.settings.VectorSettingsActivity -import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment -import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment -import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment -import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment -import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment -import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment +import im.vector.riotx.features.settings.* import im.vector.riotx.features.settings.push.PushGatewaysFragment @Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class]) @@ -134,6 +125,8 @@ interface ScreenComponent { fun inject(publicRoomsFragment: PublicRoomsFragment) + fun inject(loginFragment: LoginFragment) + fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment) fun inject(quickReactionFragment: QuickReactionFragment) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 41eed536e3..75fcbb7479 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -18,146 +18,38 @@ package im.vector.riotx.features.login import android.content.Context import android.content.Intent -import android.os.Bundle -import android.widget.Toast -import androidx.core.view.isVisible -import arrow.core.Try -import com.jakewharton.rxbinding3.widget.textChanges -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.auth.Authenticator -import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig -import im.vector.matrix.android.api.session.Session import im.vector.riotx.R -import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.core.extensions.configureAndStart -import im.vector.riotx.core.extensions.setTextWithColoredPart -import im.vector.riotx.core.extensions.showPassword +import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.platform.VectorBaseActivity -import im.vector.riotx.core.utils.openUrlInExternalBrowser import im.vector.riotx.features.disclaimer.showDisclaimerDialog -import im.vector.riotx.features.home.HomeActivity -import im.vector.riotx.features.homeserver.ServerUrlsRepository -import im.vector.riotx.features.notifications.PushRuleTriggerListener -import io.reactivex.Observable -import io.reactivex.functions.Function3 -import io.reactivex.rxkotlin.subscribeBy -import kotlinx.android.synthetic.main.activity_login.* import javax.inject.Inject class LoginActivity : VectorBaseActivity() { - @Inject lateinit var authenticator: Authenticator - @Inject lateinit var activeSessionHolder: ActiveSessionHolder - @Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener + @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory - private var passwordShown = false override fun injectWith(injector: ScreenComponent) { injector.inject(this) } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_login) - setupNotice() - setupAuthButton() - setupPasswordReveal() - homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(this)) - } + override fun getLayoutRes() = R.layout.activity_simple - private fun setupNotice() { - riotx_no_registration_notice.setTextWithColoredPart(R.string.riotx_no_registration_notice, R.string.riotx_no_registration_notice_colored_part) - - riotx_no_registration_notice.setOnClickListener { - openUrlInExternalBrowser(this@LoginActivity, "https://about.riot.im/downloads") + override fun initUiAndData() { + if (isFirstCreation()) { + addFragment(LoginFragment(), R.id.simpleFragmentContainer) } } + override fun onResume() { super.onResume() showDisclaimerDialog(this) } - private fun authenticate() { - passwordShown = false - renderPasswordField() - - val login = loginField.text?.trim().toString() - val password = passwordField.text?.trim().toString() - buildHomeServerConnectionConfig().fold( - { Toast.makeText(this@LoginActivity, "Authenticate failure: $it", Toast.LENGTH_LONG).show() }, - { authenticateWith(it, login, password) } - ) - } - - private fun authenticateWith(homeServerConnectionConfig: HomeServerConnectionConfig, login: String, password: String) { - progressBar.isVisible = true - touchArea.isVisible = true - authenticator.authenticate(homeServerConnectionConfig, login, password, object : MatrixCallback { - override fun onSuccess(data: Session) { - activeSessionHolder.setActiveSession(data) - data.configureAndStart(pushRuleTriggerListener) - goToHome() - } - - override fun onFailure(failure: Throwable) { - progressBar.isVisible = false - touchArea.isVisible = false - Toast.makeText(this@LoginActivity, "Authenticate failure: $failure", Toast.LENGTH_LONG).show() - } - }) - } - - private fun buildHomeServerConnectionConfig(): Try { - return Try { - val homeServerUri = homeServerField.text?.trim().toString() - HomeServerConnectionConfig.Builder() - .withHomeServerUri(homeServerUri) - .build() - } - } - - private fun setupAuthButton() { - Observable - .combineLatest( - loginField.textChanges().map { it.trim().isNotEmpty() }, - passwordField.textChanges().map { it.trim().isNotEmpty() }, - homeServerField.textChanges().map { it.trim().isNotEmpty() }, - Function3 { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty -> - isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty - } - ) - .subscribeBy { authenticateButton.isEnabled = it } - .disposeOnDestroy() - authenticateButton.setOnClickListener { authenticate() } - } - - private fun setupPasswordReveal() { - passwordShown = false - - passwordReveal.setOnClickListener { - passwordShown = !passwordShown - - renderPasswordField() - } - - renderPasswordField() - } - - private fun renderPasswordField() { - passwordField.showPassword(passwordShown) - - passwordReveal.setImageResource(if (passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) - } - - private fun goToHome() { - val intent = HomeActivity.newIntent(this) - startActivity(intent) - finish() - } companion object { fun newIntent(context: Context): Intent { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt new file mode 100644 index 0000000000..0471ab6a8a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.core.view.isVisible +import arrow.core.Try +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.Authenticator +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.session.Session +import im.vector.riotx.R +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.configureAndStart +import im.vector.riotx.core.extensions.setTextWithColoredPart +import im.vector.riotx.core.extensions.showPassword +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.openUrlInExternalBrowser +import im.vector.riotx.features.home.HomeActivity +import im.vector.riotx.features.homeserver.ServerUrlsRepository +import im.vector.riotx.features.notifications.PushRuleTriggerListener +import io.reactivex.Observable +import io.reactivex.functions.Function3 +import io.reactivex.rxkotlin.subscribeBy +import kotlinx.android.synthetic.main.activity_login.* +import javax.inject.Inject + + +/** + * What can be improved: + * - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect + */ +class LoginFragment : VectorBaseFragment() { + + private val viewModel: LoginViewModel by activityViewModel() + + @Inject lateinit var authenticator: Authenticator + @Inject lateinit var activeSessionHolder: ActiveSessionHolder + @Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener + private var passwordShown = false + + @Inject lateinit var errorFormatter: ErrorFormatter + + override fun getLayoutResId() = R.layout.activity_login + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupNotice() + setupAuthButton() + setupPasswordReveal() + homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(requireContext())) + + +// viewModel.joinRoomErrorLiveData.observeEvent(this) { throwable -> +// Snackbar.make(publicRoomsCoordinator, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT) +// .show() +// } + } + + private fun setupNotice() { + riotx_no_registration_notice.setTextWithColoredPart(R.string.riotx_no_registration_notice, R.string.riotx_no_registration_notice_colored_part) + + riotx_no_registration_notice.setOnClickListener { + openUrlInExternalBrowser(requireActivity(), "https://about.riot.im/downloads") + } + } + + private fun authenticate() { + passwordShown = false + renderPasswordField() + + val login = loginField.text?.trim().toString() + val password = passwordField.text?.trim().toString() + buildHomeServerConnectionConfig().fold( + { Toast.makeText(requireActivity(), "Authenticate failure: $it", Toast.LENGTH_LONG).show() }, + { authenticateWith(it, login, password) } + ) + } + + private fun authenticateWith(homeServerConnectionConfig: HomeServerConnectionConfig, login: String, password: String) { + progressBar.isVisible = true + touchArea.isVisible = true + authenticator.authenticate(homeServerConnectionConfig, login, password, object : MatrixCallback { + override fun onSuccess(data: Session) { + activeSessionHolder.setActiveSession(data) + data.configureAndStart(pushRuleTriggerListener) + goToHome() + } + + override fun onFailure(failure: Throwable) { + progressBar.isVisible = false + touchArea.isVisible = false + Toast.makeText(requireActivity(), "Authenticate failure: $failure", Toast.LENGTH_LONG).show() + } + }) + } + + private fun buildHomeServerConnectionConfig(): Try { + return Try { + val homeServerUri = homeServerField.text?.trim().toString() + HomeServerConnectionConfig.Builder() + .withHomeServerUri(homeServerUri) + .build() + } + } + + private fun setupAuthButton() { + Observable + .combineLatest( + loginField.textChanges().map { it.trim().isNotEmpty() }, + passwordField.textChanges().map { it.trim().isNotEmpty() }, + homeServerField.textChanges().map { it.trim().isNotEmpty() }, + Function3 { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty -> + isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty + } + ) + .subscribeBy { authenticateButton.isEnabled = it } + .disposeOnDestroy() + authenticateButton.setOnClickListener { authenticate() } + } + + private fun setupPasswordReveal() { + passwordShown = false + + passwordReveal.setOnClickListener { + passwordShown = !passwordShown + + renderPasswordField() + } + + renderPasswordField() + } + + private fun renderPasswordField() { + passwordField.showPassword(passwordShown) + + passwordReveal.setImageResource(if (passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + } + + private fun goToHome() { + val intent = HomeActivity.newIntent(requireActivity()) + startActivity(intent) + requireActivity().finish() + } + + + override fun invalidate() = withState(viewModel) { state -> + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt new file mode 100644 index 0000000000..a34e112e57 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.riotx.core.platform.VectorViewModel + +class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState) : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: LoginViewState): LoginViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: LoginViewState): LoginViewModel? { + val activity: LoginActivity = (viewModelContext as ActivityViewModelContext).activity() + return activity.loginViewModelFactory.create(state) + } + } + + init { + + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt new file mode 100644 index 0000000000..ea63f777b7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized + +data class LoginViewState( + // Current pagination request + val asyncHomeServerLoginFlowRequest: Async = Uninitialized +) : MvRxState + + +// TODO Remove +data class LoginFlowResult(val remover: Boolean) \ No newline at end of file From a47a3ead1fa00a266f0d5152c9fff4de883fc91d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Sep 2019 10:39:22 +0200 Subject: [PATCH 03/44] Login: move login code to the ViewModel --- .../riotx/core/platform/VectorViewModel.kt | 3 - .../riotx/features/login/LoginActions.kt | 24 +++++ .../riotx/features/login/LoginFragment.kt | 92 ++++++---------- .../riotx/features/login/LoginViewModel.kt | 100 +++++++++++++++++- .../riotx/features/login/LoginViewState.kt | 2 +- 5 files changed, 154 insertions(+), 67 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt index 1c2f1d53f0..b0c2dc546a 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt @@ -17,11 +17,8 @@ package im.vector.riotx.core.platform import com.airbnb.mvrx.* -import im.vector.matrix.android.api.util.CancelableBag -import im.vector.riotx.BuildConfig import io.reactivex.Observable import io.reactivex.Single -import io.reactivex.disposables.Disposable abstract class VectorViewModel(initialState: S) : BaseMvRxViewModel(initialState, false) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt new file mode 100644 index 0000000000..be04e6c029 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +sealed class LoginActions { + + data class UpdateHomeServer(val homeServerUrl: String) : LoginActions() + data class Login(val login: String, val password: String) : LoginActions() + +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 0471ab6a8a..d803b451ac 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -20,26 +20,18 @@ import android.os.Bundle import android.view.View import android.widget.Toast import androidx.core.view.isVisible -import arrow.core.Try -import com.airbnb.mvrx.activityViewModel -import com.airbnb.mvrx.withState +import com.airbnb.mvrx.* +import com.jakewharton.rxbinding3.view.focusChanges import com.jakewharton.rxbinding3.widget.textChanges -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.auth.Authenticator -import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig -import im.vector.matrix.android.api.session.Session import im.vector.riotx.R -import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.error.ErrorFormatter -import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.extensions.setTextWithColoredPart import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.openUrlInExternalBrowser import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.homeserver.ServerUrlsRepository -import im.vector.riotx.features.notifications.PushRuleTriggerListener import io.reactivex.Observable import io.reactivex.functions.Function3 import io.reactivex.rxkotlin.subscribeBy @@ -55,9 +47,6 @@ class LoginFragment : VectorBaseFragment() { private val viewModel: LoginViewModel by activityViewModel() - @Inject lateinit var authenticator: Authenticator - @Inject lateinit var activeSessionHolder: ActiveSessionHolder - @Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener private var passwordShown = false @Inject lateinit var errorFormatter: ErrorFormatter @@ -75,13 +64,19 @@ class LoginFragment : VectorBaseFragment() { setupNotice() setupAuthButton() setupPasswordReveal() + + homeServerField.focusChanges() + .subscribe { + if (!it) { + // TODO Also when clicking on button? + viewModel.handle(LoginActions.UpdateHomeServer(homeServerField.text.toString())) + } + } + .disposeOnDestroy() + + homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(requireContext())) - - -// viewModel.joinRoomErrorLiveData.observeEvent(this) { throwable -> -// Snackbar.make(publicRoomsCoordinator, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT) -// .show() -// } + viewModel.handle(LoginActions.UpdateHomeServer(homeServerField.text.toString())) } private fun setupNotice() { @@ -93,42 +88,10 @@ class LoginFragment : VectorBaseFragment() { } private fun authenticate() { - passwordShown = false - renderPasswordField() - val login = loginField.text?.trim().toString() val password = passwordField.text?.trim().toString() - buildHomeServerConnectionConfig().fold( - { Toast.makeText(requireActivity(), "Authenticate failure: $it", Toast.LENGTH_LONG).show() }, - { authenticateWith(it, login, password) } - ) - } - private fun authenticateWith(homeServerConnectionConfig: HomeServerConnectionConfig, login: String, password: String) { - progressBar.isVisible = true - touchArea.isVisible = true - authenticator.authenticate(homeServerConnectionConfig, login, password, object : MatrixCallback { - override fun onSuccess(data: Session) { - activeSessionHolder.setActiveSession(data) - data.configureAndStart(pushRuleTriggerListener) - goToHome() - } - - override fun onFailure(failure: Throwable) { - progressBar.isVisible = false - touchArea.isVisible = false - Toast.makeText(requireActivity(), "Authenticate failure: $failure", Toast.LENGTH_LONG).show() - } - }) - } - - private fun buildHomeServerConnectionConfig(): Try { - return Try { - val homeServerUri = homeServerField.text?.trim().toString() - HomeServerConnectionConfig.Builder() - .withHomeServerUri(homeServerUri) - .build() - } + viewModel.handle(LoginActions.Login(login, password)) } private fun setupAuthButton() { @@ -164,14 +127,25 @@ class LoginFragment : VectorBaseFragment() { passwordReveal.setImageResource(if (passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) } - private fun goToHome() { - val intent = HomeActivity.newIntent(requireActivity()) - startActivity(intent) - requireActivity().finish() - } - - override fun invalidate() = withState(viewModel) { state -> + when (state.asyncLoginAction) { + is Loading -> { + progressBar.isVisible = true + touchArea.isVisible = true + passwordShown = false + renderPasswordField() + } + is Fail -> { + progressBar.isVisible = false + touchArea.isVisible = false + Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncLoginAction.error}", Toast.LENGTH_LONG).show() + } + is Success -> { + val intent = HomeActivity.newIntent(requireActivity()) + startActivity(intent) + requireActivity().finish() + } + } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index a34e112e57..cf73f2517a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -16,14 +16,24 @@ package im.vector.riotx.features.login -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext +import arrow.core.Try +import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.Authenticator +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.Cancelable +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.features.notifications.PushRuleTriggerListener -class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState) : VectorViewModel(initialState) { +class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, + val authenticator: Authenticator, + val activeSessionHolder: ActiveSessionHolder, + val pushRuleTriggerListener: PushRuleTriggerListener) : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -43,5 +53,87 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } + var homeServerConnectionConfig: HomeServerConnectionConfig? = null + private var currentTask: Cancelable? = null + + + fun handle(action: LoginActions) { + when (action) { + is LoginActions.UpdateHomeServer -> handleUpdateHomeserver(action) + is LoginActions.Login -> handleLogin(action) + } + } + + private fun handleLogin(action: LoginActions.Login) { + val homeServerConnectionConfigFinal = homeServerConnectionConfig + + if (homeServerConnectionConfigFinal == null) { + setState { + copy( + asyncLoginAction = Fail(Throwable("Bad configuration")) + ) + } + } else { + setState { + copy( + asyncLoginAction = Loading() + ) + } + + authenticator.authenticate(homeServerConnectionConfigFinal, action.login, action.password, object : MatrixCallback { + override fun onSuccess(data: Session) { + activeSessionHolder.setActiveSession(data) + data.configureAndStart(pushRuleTriggerListener) + + setState { + copy( + asyncLoginAction = Success(Unit) + ) + } + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + }) + } + } + + private fun handleUpdateHomeserver(action: LoginActions.UpdateHomeServer) { + Try { + val homeServerUri = action.homeServerUrl + homeServerConnectionConfig = HomeServerConnectionConfig.Builder() + .withHomeServerUri(homeServerUri) + .build() + } + + + // TODO Do request + + /* + currentTask?.cancel() + + setState { + copy( + asyncHomeServerLoginFlowRequest = Loading() + ) + } + + + // TODO currentTask = + + */ + } + + + override fun onCleared() { + super.onCleared() + + currentTask?.cancel() + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index ea63f777b7..f296e5e064 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -21,7 +21,7 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized data class LoginViewState( - // Current pagination request + val asyncLoginAction: Async = Uninitialized, val asyncHomeServerLoginFlowRequest: Async = Uninitialized ) : MvRxState From db8ea0f5e88034a22170e81067c28a5342fa843b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Sep 2019 11:08:54 +0200 Subject: [PATCH 04/44] Login: check login flow - step 1 --- .../matrix/android/api/auth/Authenticator.kt | 6 +++ .../matrix/android/internal/auth/AuthAPI.kt | 9 ++++ .../internal/auth/DefaultAuthenticator.kt | 19 ++++++- .../internal/auth/data/LoginFlowResponse.kt | 2 +- .../android/internal/di/NetworkModule.kt | 2 +- .../riotx/features/login/LoginFragment.kt | 22 ++++++++ .../riotx/features/login/LoginViewModel.kt | 50 +++++++++++++++---- .../riotx/features/login/LoginViewState.kt | 7 +-- 8 files changed, 99 insertions(+), 18 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt index 932ffbead9..e7cb72544b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt @@ -21,12 +21,18 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.auth.data.LoginFlowResponse /** * This interface defines methods to authenticate to a matrix server. */ interface Authenticator { + /** + * Request the supported login flows for this homeserver + */ + fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable + /** * @param homeServerConnectionConfig this param is used to configure the Homeserver * @param login the login field diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt index d795a3c413..d42962c53e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt @@ -17,10 +17,12 @@ package im.vector.matrix.android.internal.auth import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.PasswordLoginParams import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call import retrofit2.http.Body +import retrofit2.http.GET import retrofit2.http.Headers import retrofit2.http.POST @@ -29,6 +31,13 @@ import retrofit2.http.POST */ internal interface AuthAPI { + /** + * Get the supported login flow + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") + fun getLoginFlows(): Call + /** * Pass params to the server for the current login phase. * Set all the timeouts to 1 minute diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt index adea7c894b..765f410d3f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.SessionManager +import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.PasswordLoginParams import im.vector.matrix.android.internal.auth.data.ThreePidMedium import im.vector.matrix.android.internal.di.Unauthenticated @@ -62,11 +63,20 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated return sessionManager.getOrCreateSession(sessionParams) } + override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable { + val job = GlobalScope.launch(coroutineDispatchers.main) { + val result = runCatching { + getLoginFlowInternal(homeServerConnectionConfig) + } + result.foldToCallback(callback) + } + return CancelableCoroutine(job) + } + override fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig, login: String, password: String, callback: MatrixCallback): Cancelable { - val job = GlobalScope.launch(coroutineDispatchers.main) { val sessionOrFailure = runCatching { authenticate(homeServerConnectionConfig, login, password) @@ -74,7 +84,14 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated sessionOrFailure.foldToCallback(callback) } return CancelableCoroutine(job) + } + private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) { + val authAPI = buildAuthAPI(homeServerConnectionConfig) + + executeRequest { + apiCall = authAPI.getLoginFlows() + } } private suspend fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt index 834b0aee16..78fd372beb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt @@ -20,7 +20,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class LoginFlowResponse( +data class LoginFlowResponse( @Json(name = "flows") val flows: List ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt index 4f3130c5ce..2061d03bed 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt @@ -35,7 +35,7 @@ internal object NetworkModule { @Provides @JvmStatic - fun providesHttpLogingInterceptor(): HttpLoggingInterceptor { + fun providesHttpLoggingInterceptor(): HttpLoggingInterceptor { val logger = FormattedJsonHttpLogger() val interceptor = HttpLoggingInterceptor(logger) interceptor.level = BuildConfig.OKHTTP_LOGGING_LEVEL diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index d803b451ac..5bd7152cbb 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -128,6 +128,28 @@ class LoginFragment : VectorBaseFragment() { } override fun invalidate() = withState(viewModel) { state -> + when (state.asyncHomeServerLoginFlowRequest) { + is Loading -> { + progressBar.isVisible = true + touchArea.isVisible = true + + passwordShown = false + renderPasswordField() + } + is Fail -> { + progressBar.isVisible = false + touchArea.isVisible = false + Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncHomeServerLoginFlowRequest.error}", Toast.LENGTH_LONG).show() + } + is Success -> { + progressBar.isVisible = false + touchArea.isVisible = false + + // Check login flow + // TODO + } + } + when (state.asyncLoginAction) { is Loading -> { progressBar.isVisible = true diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index cf73f2517a..9eee9d2074 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.platform.VectorViewModel @@ -104,6 +105,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } private fun handleUpdateHomeserver(action: LoginActions.UpdateHomeServer) { + currentTask?.cancel() + Try { val homeServerUri = action.homeServerUrl homeServerConnectionConfig = HomeServerConnectionConfig.Builder() @@ -111,22 +114,49 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi .build() } + val homeServerConnectionConfigFinal = homeServerConnectionConfig - // TODO Do request + if (homeServerConnectionConfigFinal == null) { + // This is invalid + setState { + copy( + asyncHomeServerLoginFlowRequest = Fail(Throwable("Baf format")) + ) + } + } else { - /* - currentTask?.cancel() + setState { + copy( + asyncHomeServerLoginFlowRequest = Loading() + ) + } + + + currentTask = authenticator.getLoginFlow(homeServerConnectionConfigFinal, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + setState { + copy( + asyncHomeServerLoginFlowRequest = Fail(failure) + ) + } + } + + override fun onSuccess(data: LoginFlowResponse) { + setState { + copy( + asyncHomeServerLoginFlowRequest = Success(data) + ) + } + + handleLoginFlowResponse(data) + } + }) - setState { - copy( - asyncHomeServerLoginFlowRequest = Loading() - ) } + } + private fun handleLoginFlowResponse(loginFlowResponse: LoginFlowResponse) { - // TODO currentTask = - - */ } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index f296e5e064..32c5c43bed 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -19,12 +19,9 @@ package im.vector.riotx.features.login import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.internal.auth.data.LoginFlowResponse data class LoginViewState( val asyncLoginAction: Async = Uninitialized, - val asyncHomeServerLoginFlowRequest: Async = Uninitialized + val asyncHomeServerLoginFlowRequest: Async = Uninitialized ) : MvRxState - - -// TODO Remove -data class LoginFlowResult(val remover: Boolean) \ No newline at end of file From 5fbd271b1caca553cd3aaea95eba3bd5ecf55c08 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Sep 2019 14:41:30 +0200 Subject: [PATCH 05/44] Login: add SSO support --- .../matrix/android/api/auth/Authenticator.kt | 6 + .../internal/auth/DefaultAuthenticator.kt | 6 + .../data/InteractiveAuthenticationFlow.kt | 10 +- .../vector/riotx/core/di/ScreenComponent.kt | 3 + .../riotx/features/login/LoginActions.kt | 3 + .../riotx/features/login/LoginActivity.kt | 29 ++ .../riotx/features/login/LoginFragment.kt | 59 +++- .../login/LoginSsoFallbackFragment.kt | 303 ++++++++++++++++++ .../riotx/features/login/LoginViewModel.kt | 75 +++-- .../riotx/features/login/LoginViewState.kt | 10 +- ...{activity_login.xml => fragment_login.xml} | 12 + .../layout/fragment_login_sso_fallback.xml | 22 ++ 12 files changed, 497 insertions(+), 41 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt rename vector/src/main/res/layout/{activity_login.xml => fragment_login.xml} (91%) create mode 100644 vector/src/main/res/layout/fragment_login_sso_fallback.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt index e7cb72544b..2dc2d0ef5f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.auth import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.Session @@ -62,4 +63,9 @@ interface Authenticator { * @return the associated session if any, or null */ fun getSession(sessionParams: SessionParams): Session? + + /** + * Create a session after a SSO successful login + */ + fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt index 765f410d3f..949aa6611e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt @@ -112,6 +112,12 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated sessionManager.getOrCreateSession(sessionParams) } + override fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session { + val sessionParams = SessionParams(credentials, homeServerConnectionConfig) + sessionParamsStore.save(sessionParams) + return sessionManager.getOrCreateSession(sessionParams) + } + private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) return retrofit.create(AuthAPI::class.java) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/InteractiveAuthenticationFlow.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/InteractiveAuthenticationFlow.kt index ae75b2737d..e1f963ff3d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/InteractiveAuthenticationFlow.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/InteractiveAuthenticationFlow.kt @@ -30,4 +30,12 @@ data class InteractiveAuthenticationFlow( @Json(name = "stages") val stages: List? = null -) \ No newline at end of file +) { + + companion object { + // Possible values for type + const val TYPE_LOGIN_SSO = "m.login.sso" + const val TYPE_LOGIN_TOKEN = "m.login.token" + const val TYPE_LOGIN_PASSWORD = "m.login.password" + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 66281aad37..ccf3a19202 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -48,6 +48,7 @@ import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.login.LoginFragment +import im.vector.riotx.features.login.LoginSsoFallbackFragment import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.navigation.Navigator @@ -127,6 +128,8 @@ interface ScreenComponent { fun inject(loginFragment: LoginFragment) + fun inject(loginSsoFallbackFragment: LoginSsoFallbackFragment) + fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment) fun inject(quickReactionFragment: QuickReactionFragment) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt index be04e6c029..6f4757246b 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt @@ -16,9 +16,12 @@ package im.vector.riotx.features.login +import im.vector.matrix.android.api.auth.data.Credentials + sealed class LoginActions { data class UpdateHomeServer(val homeServerUrl: String) : LoginActions() data class Login(val login: String, val password: String) : LoginActions() + data class SsoLoginSuccess(val credentials: Credentials) : LoginActions() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 75fcbb7479..591e4ed474 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -18,16 +18,30 @@ package im.vector.riotx.features.login import android.content.Context import android.content.Intent +import androidx.fragment.app.FragmentManager +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.viewModel import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment +import im.vector.riotx.core.extensions.addFragmentToBackstack +import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.disclaimer.showDisclaimerDialog +import im.vector.riotx.features.home.HomeActivity import javax.inject.Inject class LoginActivity : VectorBaseActivity() { + // Supported navigation actions for this Activity + sealed class Navigation { + object OpenSsoLoginFallback : Navigation() + object GoBack : Navigation() + } + + private val loginViewModel: LoginViewModel by viewModel() + @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory @@ -41,6 +55,21 @@ class LoginActivity : VectorBaseActivity() { if (isFirstCreation()) { addFragment(LoginFragment(), R.id.simpleFragmentContainer) } + + loginViewModel.navigationLiveData.observeEvent(this) { + when (it) { + is Navigation.OpenSsoLoginFallback -> addFragmentToBackstack(LoginSsoFallbackFragment(), R.id.simpleFragmentContainer) + is Navigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } + } + + loginViewModel.selectSubscribe(this, LoginViewState::asyncLoginAction) { + if (it is Success) { + val intent = HomeActivity.newIntent(this) + startActivity(intent) + finish() + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 5bd7152cbb..7f2f44db1e 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.view.View import android.widget.Toast import androidx.core.view.isVisible +import androidx.transition.TransitionManager import com.airbnb.mvrx.* import com.jakewharton.rxbinding3.view.focusChanges import com.jakewharton.rxbinding3.widget.textChanges @@ -30,12 +31,11 @@ import im.vector.riotx.core.extensions.setTextWithColoredPart import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.openUrlInExternalBrowser -import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.homeserver.ServerUrlsRepository import io.reactivex.Observable import io.reactivex.functions.Function3 import io.reactivex.rxkotlin.subscribeBy -import kotlinx.android.synthetic.main.activity_login.* +import kotlinx.android.synthetic.main.fragment_login.* import javax.inject.Inject @@ -51,7 +51,7 @@ class LoginFragment : VectorBaseFragment() { @Inject lateinit var errorFormatter: ErrorFormatter - override fun getLayoutResId() = R.layout.activity_login + override fun getLayoutResId() = R.layout.fragment_login override fun injectWith(injector: ScreenComponent) { injector.inject(this) @@ -107,6 +107,12 @@ class LoginFragment : VectorBaseFragment() { .subscribeBy { authenticateButton.isEnabled = it } .disposeOnDestroy() authenticateButton.setOnClickListener { authenticate() } + + authenticateButtonSso.setOnClickListener { openSso() } + } + + private fun openSso() { + viewModel.openSso() } private fun setupPasswordReveal() { @@ -128,25 +134,53 @@ class LoginFragment : VectorBaseFragment() { } override fun invalidate() = withState(viewModel) { state -> + TransitionManager.beginDelayedTransition(login_fragment) + when (state.asyncHomeServerLoginFlowRequest) { - is Loading -> { + is Incomplete -> { progressBar.isVisible = true touchArea.isVisible = true - + loginField.isVisible = false + passwordContainer.isVisible = false + authenticateButton.isVisible = false + authenticateButtonSso.isVisible = false passwordShown = false renderPasswordField() } - is Fail -> { + is Fail -> { progressBar.isVisible = false touchArea.isVisible = false + loginField.isVisible = false + passwordContainer.isVisible = false + authenticateButton.isVisible = false + authenticateButtonSso.isVisible = false Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncHomeServerLoginFlowRequest.error}", Toast.LENGTH_LONG).show() } - is Success -> { + is Success -> { progressBar.isVisible = false touchArea.isVisible = false - // Check login flow - // TODO + when (state.asyncHomeServerLoginFlowRequest()) { + LoginMode.Password -> { + loginField.isVisible = true + passwordContainer.isVisible = true + authenticateButton.isVisible = true + authenticateButtonSso.isVisible = false + } + LoginMode.Sso -> { + loginField.isVisible = false + passwordContainer.isVisible = false + authenticateButton.isVisible = false + authenticateButtonSso.isVisible = true + } + LoginMode.Unsupported -> { + loginField.isVisible = false + passwordContainer.isVisible = false + authenticateButton.isVisible = false + authenticateButtonSso.isVisible = false + Toast.makeText(requireActivity(), "None of the homeserver login mode is supported by RiotX", Toast.LENGTH_LONG).show() + } + } } } @@ -163,11 +197,8 @@ class LoginFragment : VectorBaseFragment() { touchArea.isVisible = false Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncLoginAction.error}", Toast.LENGTH_LONG).show() } - is Success -> { - val intent = HomeActivity.newIntent(requireActivity()) - startActivity(intent) - requireActivity().finish() - } + // Success is handled by the LoginActivity + is Success -> Unit } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt new file mode 100644 index 0000000000..d7637cc2cf --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt @@ -0,0 +1,303 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import android.annotation.SuppressLint +import android.content.DialogInterface +import android.graphics.Bitmap +import android.net.http.SslError +import android.os.Build +import android.os.Bundle +import android.view.KeyEvent +import android.view.View +import android.webkit.SslErrorHandler +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.activityViewModel +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.platform.VectorBaseFragment +import kotlinx.android.synthetic.main.fragment_login_sso_fallback.* +import timber.log.Timber +import java.net.URLDecoder + + +/** + * Only login is supported for the moment + */ +class LoginSsoFallbackFragment : VectorBaseFragment() { + + private val viewModel: LoginViewModel by activityViewModel() + + var homeServerUrl: String = "" + + enum class Mode { + MODE_LOGIN, + // Not supported in RiotX for the moment + MODE_REGISTER + } + + // Mode (MODE_LOGIN or MODE_REGISTER) + private var mMode = Mode.MODE_LOGIN + + override fun getLayoutResId() = R.layout.fragment_login_sso_fallback + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupToolbar(login_sso_fallback_toolbar) + requireActivity().setTitle(R.string.login) + + setupWebview() + } + + @SuppressLint("SetJavaScriptEnabled") + private fun setupWebview() { + login_sso_fallback_webview.settings.javaScriptEnabled = true + + // Due to https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html, we hack + // the user agent to bypass the limitation of Google, as a quick fix (a proper solution will be to use the SSO SDK) + login_sso_fallback_webview.settings.userAgentString = "Mozilla/5.0 Google" + + homeServerUrl = viewModel.getHomeServerUrl() + + if (!homeServerUrl.endsWith("/")) { + homeServerUrl += "/" + } + + // AppRTC requires third party cookies to work + val cookieManager = android.webkit.CookieManager.getInstance() + + // clear the cookies must be cleared + if (cookieManager == null) { + launchWebView() + } else { + if (!cookieManager.hasCookies()) { + launchWebView() + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + try { + cookieManager.removeAllCookie() + } catch (e: Exception) { + Timber.e(e, " cookieManager.removeAllCookie() fails") + } + + launchWebView() + } else { + try { + cookieManager.removeAllCookies { launchWebView() } + } catch (e: Exception) { + Timber.e(e, " cookieManager.removeAllCookie() fails") + launchWebView() + } + } + } + } + + private fun launchWebView() { + if (mMode == Mode.MODE_LOGIN) { + login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/login/") + } else { + // MODE_REGISTER + login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/register/") + } + + login_sso_fallback_webview.webViewClient = object : WebViewClient() { + override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, + error: SslError) { + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.ssl_could_not_verify) + .setPositiveButton(R.string.ssl_trust) { dialog, which -> handler.proceed() } + .setNegativeButton(R.string.ssl_do_not_trust) { dialog, which -> handler.cancel() } + .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + handler.cancel() + dialog.dismiss() + return@OnKeyListener true + } + false + }) + .show() + } + + override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { + super.onReceivedError(view, errorCode, description, failingUrl) + + // on error case, close this activity + viewModel.goBack() + } + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + login_sso_fallback_toolbar.subtitle = url + } + + override fun onPageFinished(view: WebView, url: String) { + // avoid infinite onPageFinished call + if (url.startsWith("http")) { + // Generic method to make a bridge between JS and the UIWebView + val mxcJavascriptSendObjectMessage = "javascript:window.sendObjectMessage = function(parameters) {" + + " var iframe = document.createElement('iframe');" + + " iframe.setAttribute('src', 'js:' + JSON.stringify(parameters));" + + " document.documentElement.appendChild(iframe);" + + " iframe.parentNode.removeChild(iframe); iframe = null;" + + " };" + + view.loadUrl(mxcJavascriptSendObjectMessage) + + if (mMode == Mode.MODE_LOGIN) { + // The function the fallback page calls when the login is complete + val mxcJavascriptOnRegistered = "javascript:window.matrixLogin.onLogin = function(response) {" + + " sendObjectMessage({ 'action': 'onLogin', 'credentials': response });" + + " };" + + view.loadUrl(mxcJavascriptOnRegistered) + } else { + // MODE_REGISTER + // The function the fallback page calls when the registration is complete + val mxcJavascriptOnRegistered = "javascript:window.matrixRegistration.onRegistered" + + " = function(homeserverUrl, userId, accessToken) {" + + " sendObjectMessage({ 'action': 'onRegistered'," + + " 'homeServer': homeserverUrl," + + " 'userId': userId," + + " 'accessToken': accessToken });" + + " };" + + view.loadUrl(mxcJavascriptOnRegistered) + } + } + } + + /** + * Example of (formatted) url for MODE_LOGIN: + * + *
+             * js:{
+             *     "action":"onLogin",
+             *     "credentials":{
+             *         "user_id":"@user:matrix.org",
+             *         "access_token":"[ACCESS_TOKEN]",
+             *         "home_server":"matrix.org",
+             *         "device_id":"[DEVICE_ID]",
+             *         "well_known":{
+             *             "m.homeserver":{
+             *                 "base_url":"https://matrix.org/"
+             *                 }
+             *             }
+             *         }
+             *    }
+             * 
+ * @param view + * @param url + * @return + */ + override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean { + if (null != url && url.startsWith("js:")) { + var json = url.substring(3) + var parameters: Map? = null + + try { + // URL decode + json = URLDecoder.decode(json, "UTF-8") + + val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java) + + parameters = adapter.fromJson(json) as Map? + } catch (e: Exception) { + Timber.e(e, "## shouldOverrideUrlLoading() : fromJson failed") + } + + // succeeds to parse parameters + if (parameters != null) { + val action = parameters["action"] as String + + if (mMode == Mode.MODE_LOGIN) { + try { + if (action == "onLogin") { + val credentials = parameters["credentials"] as Map + + val userId = credentials["user_id"] + val accessToken = credentials["access_token"] + val homeServer = credentials["home_server"] + val deviceId = credentials["device_id"] + + // check if the parameters are defined + if (null != homeServer && null != userId && null != accessToken) { + val credentials = Credentials( + userId = userId, + accessToken = accessToken, + homeServer = homeServer, + deviceId = deviceId, + refreshToken = null + ) + + viewModel.handle(LoginActions.SsoLoginSuccess(credentials)) + } + } + } catch (e: Exception) { + Timber.e(e, "## shouldOverrideUrlLoading() : failed") + } + + } else { + // MODE_REGISTER + // check the required parameters + if (action == "onRegistered") { + // TODO The keys are very strange, this code comes from Riot-Android... + if (parameters.containsKey("homeServer") + && parameters.containsKey("userId") + && parameters.containsKey("accessToken")) { + + // We cannot parse Credentials here because of https://github.com/matrix-org/synapse/issues/4756 + // Build on object manually + val credentials = Credentials( + userId = parameters["userId"] as String, + accessToken = parameters["accessToken"] as String, + homeServer = parameters["homeServer"] as String, + // TODO We need deviceId on RiotX... + deviceId = "TODO", + refreshToken = null + ) + + viewModel.handle(LoginActions.SsoLoginSuccess(credentials)) + } + } + } + } + return true + } + + return super.shouldOverrideUrlLoading(view, url) + } + } + } + + override fun onBackPressed(): Boolean { + return if (login_sso_fallback_webview.canGoBack()) { + login_sso_fallback_webview.goBack() + true + } else { + false + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 9eee9d2074..627bf90177 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -16,6 +16,8 @@ package im.vector.riotx.features.login +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import arrow.core.Try import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted @@ -25,10 +27,12 @@ import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.features.notifications.PushRuleTriggerListener class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, @@ -50,9 +54,9 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } - init { - - } + private val _navigationLiveData = MutableLiveData>() + val navigationLiveData: LiveData> + get() = _navigationLiveData var homeServerConnectionConfig: HomeServerConnectionConfig? = null private var currentTask: Cancelable? = null @@ -62,6 +66,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi when (action) { is LoginActions.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginActions.Login -> handleLogin(action) + is LoginActions.SsoLoginSuccess -> handleSsoLoginSuccess(action) } } @@ -83,14 +88,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi authenticator.authenticate(homeServerConnectionConfigFinal, action.login, action.password, object : MatrixCallback { override fun onSuccess(data: Session) { - activeSessionHolder.setActiveSession(data) - data.configureAndStart(pushRuleTriggerListener) - - setState { - copy( - asyncLoginAction = Success(Unit) - ) - } + onSessionCreated(data) } override fun onFailure(failure: Throwable) { @@ -104,6 +102,24 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + private fun onSessionCreated(session: Session) { + activeSessionHolder.setActiveSession(session) + session.configureAndStart(pushRuleTriggerListener) + + setState { + copy( + asyncLoginAction = Success(Unit) + ) + } + } + + private fun handleSsoLoginSuccess(action: LoginActions.SsoLoginSuccess) { + val session = authenticator.createSessionFromSso(action.credentials, homeServerConnectionConfig!!) + + onSessionCreated(session) + } + + private fun handleUpdateHomeserver(action: LoginActions.UpdateHomeServer) { currentTask?.cancel() @@ -120,18 +136,16 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi // This is invalid setState { copy( - asyncHomeServerLoginFlowRequest = Fail(Throwable("Baf format")) + asyncHomeServerLoginFlowRequest = Fail(Throwable("Bad format")) ) } } else { - setState { copy( asyncHomeServerLoginFlowRequest = Loading() ) } - currentTask = authenticator.getLoginFlow(homeServerConnectionConfigFinal, object : MatrixCallback { override fun onFailure(failure: Throwable) { setState { @@ -142,28 +156,41 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } override fun onSuccess(data: LoginFlowResponse) { - setState { - copy( - asyncHomeServerLoginFlowRequest = Success(data) - ) + val loginMode = when { + // SSO login is taken first + data.flows.any { it.type == InteractiveAuthenticationFlow.TYPE_LOGIN_SSO } -> LoginMode.Sso + data.flows.any { it.type == InteractiveAuthenticationFlow.TYPE_LOGIN_PASSWORD } -> LoginMode.Password + else -> LoginMode.Unsupported } - handleLoginFlowResponse(data) + setState { + copy( + asyncHomeServerLoginFlowRequest = Success(loginMode) + ) + } } }) } } - private fun handleLoginFlowResponse(loginFlowResponse: LoginFlowResponse) { - - } - - override fun onCleared() { super.onCleared() currentTask?.cancel() } + fun openSso() { + // Navigate to SSO + _navigationLiveData.postValue(LiveEvent(LoginActivity.Navigation.OpenSsoLoginFallback)) + } + + fun goBack() { + // Navigate back + _navigationLiveData.postValue(LiveEvent(LoginActivity.Navigation.GoBack)) + } + + fun getHomeServerUrl(): String { + return homeServerConnectionConfig?.homeServerUri?.toString() ?: "" + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 32c5c43bed..837c2c7056 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -19,9 +19,15 @@ package im.vector.riotx.features.login import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized -import im.vector.matrix.android.internal.auth.data.LoginFlowResponse data class LoginViewState( val asyncLoginAction: Async = Uninitialized, - val asyncHomeServerLoginFlowRequest: Async = Uninitialized + val asyncHomeServerLoginFlowRequest: Async = Uninitialized ) : MvRxState + + +enum class LoginMode { + Password, + Sso, + Unsupported +} \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_login.xml b/vector/src/main/res/layout/fragment_login.xml similarity index 91% rename from vector/src/main/res/layout/activity_login.xml rename to vector/src/main/res/layout/fragment_login.xml index 0b76b461df..ed3eeffb9c 100644 --- a/vector/src/main/res/layout/activity_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -1,6 +1,7 @@ @@ -54,6 +55,7 @@ @@ -114,6 +116,16 @@ android:layout_marginTop="22dp" android:text="@string/auth_login" /> + + diff --git a/vector/src/main/res/layout/fragment_login_sso_fallback.xml b/vector/src/main/res/layout/fragment_login_sso_fallback.xml new file mode 100644 index 0000000000..30ac1bc9a3 --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_sso_fallback.xml @@ -0,0 +1,22 @@ + + + + + + + + + From 3ccdf4a24431701159d7c9a11609a3651dd0b30c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Sep 2019 15:27:08 +0200 Subject: [PATCH 06/44] Login: some cleanup --- .../riotx/features/login/LoginActions.kt | 1 + .../riotx/features/login/LoginFragment.kt | 2 +- .../login/LoginSsoFallbackFragment.kt | 6 ++--- .../riotx/features/login/LoginViewModel.kt | 24 ++++++++----------- .../layout/fragment_login_sso_fallback.xml | 1 + 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt index 6f4757246b..42a7320152 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt @@ -23,5 +23,6 @@ sealed class LoginActions { data class UpdateHomeServer(val homeServerUrl: String) : LoginActions() data class Login(val login: String, val password: String) : LoginActions() data class SsoLoginSuccess(val credentials: Credentials) : LoginActions() + data class NavigateTo(val target: LoginActivity.Navigation) : LoginActions() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 7f2f44db1e..5c9bf9e2aa 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -112,7 +112,7 @@ class LoginFragment : VectorBaseFragment() { } private fun openSso() { - viewModel.openSso() + viewModel.handle(LoginActions.NavigateTo(LoginActivity.Navigation.OpenSsoLoginFallback)) } private fun setupPasswordReveal() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt index d7637cc2cf..945bb86f4c 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt @@ -67,7 +67,7 @@ class LoginSsoFallbackFragment : VectorBaseFragment() { super.onViewCreated(view, savedInstanceState) setupToolbar(login_sso_fallback_toolbar) - requireActivity().setTitle(R.string.login) + login_sso_fallback_toolbar.title = getString(R.string.login) setupWebview() } @@ -143,8 +143,8 @@ class LoginSsoFallbackFragment : VectorBaseFragment() { override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { super.onReceivedError(view, errorCode, description, failingUrl) - // on error case, close this activity - viewModel.goBack() + // on error case, close this fragment + viewModel.handle(LoginActions.NavigateTo(LoginActivity.Navigation.GoBack)) } override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 627bf90177..ec4e9e05e7 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -36,9 +36,10 @@ import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.features.notifications.PushRuleTriggerListener class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, - val authenticator: Authenticator, - val activeSessionHolder: ActiveSessionHolder, - val pushRuleTriggerListener: PushRuleTriggerListener) : VectorViewModel(initialState) { + private val authenticator: Authenticator, + private val activeSessionHolder: ActiveSessionHolder, + private val pushRuleTriggerListener: PushRuleTriggerListener) + : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -58,7 +59,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi val navigationLiveData: LiveData> get() = _navigationLiveData - var homeServerConnectionConfig: HomeServerConnectionConfig? = null + private var homeServerConnectionConfig: HomeServerConnectionConfig? = null private var currentTask: Cancelable? = null @@ -67,6 +68,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginActions.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginActions.Login -> handleLogin(action) is LoginActions.SsoLoginSuccess -> handleSsoLoginSuccess(action) + is LoginActions.NavigateTo -> handleNavigation(action) } } @@ -174,22 +176,16 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + private fun handleNavigation(action: LoginActions.NavigateTo) { + _navigationLiveData.postValue(LiveEvent(action.target)) + } + override fun onCleared() { super.onCleared() currentTask?.cancel() } - fun openSso() { - // Navigate to SSO - _navigationLiveData.postValue(LiveEvent(LoginActivity.Navigation.OpenSsoLoginFallback)) - } - - fun goBack() { - // Navigate back - _navigationLiveData.postValue(LiveEvent(LoginActivity.Navigation.GoBack)) - } - fun getHomeServerUrl(): String { return homeServerConnectionConfig?.homeServerUri?.toString() ?: "" } diff --git a/vector/src/main/res/layout/fragment_login_sso_fallback.xml b/vector/src/main/res/layout/fragment_login_sso_fallback.xml index 30ac1bc9a3..e83680d2cd 100644 --- a/vector/src/main/res/layout/fragment_login_sso_fallback.xml +++ b/vector/src/main/res/layout/fragment_login_sso_fallback.xml @@ -10,6 +10,7 @@ style="@style/VectorToolbarStyle" android:layout_width="match_parent" android:layout_height="wrap_content" + android:elevation="4dp" tools:subtitle="https://www.example.org" tools:title="@string/auth_login" /> From b22b8fba021317c161c063b648a40a3c4ddaac48 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Sep 2019 15:47:48 +0200 Subject: [PATCH 07/44] Fix the mess up with OnBackPress support on Fragment --- .../riotx/core/platform/VectorBaseActivity.kt | 15 +++++++++------ .../riotx/core/platform/VectorBaseFragment.kt | 12 ++---------- .../im/vector/riotx/features/home/HomeActivity.kt | 5 +---- .../features/home/room/list/RoomListFragment.kt | 2 +- .../vector/riotx/features/login/LoginActivity.kt | 1 - .../features/login/LoginSsoFallbackFragment.kt | 3 ++- 6 files changed, 15 insertions(+), 23 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 1214bfa045..fad2c0ed87 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -265,18 +265,21 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { return super.onOptionsItemSelected(item) } - protected fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean { - // if (fm.backStackEntryCount == 0) - // return false + override fun onBackPressed() { + val handled = recursivelyDispatchOnBackPressed(supportFragmentManager) + if (!handled) { + super.onBackPressed() + } + } - val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed() + private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean { + val reverseOrder = fm.fragments.filter { it is VectorBaseFragment }.reversed() for (f in reverseOrder) { val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager) if (handledByChildFragments) { return true } - val backPressable = f as OnBackPressed - if (backPressable.onBackPressed()) { + if (f is OnBackPressed && f.onBackPressed()) { return true } } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index aac19d8097..52cd85f249 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -19,11 +19,7 @@ package im.vector.riotx.core.platform import android.content.Context import android.os.Bundle import android.os.Parcelable -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.View -import android.view.ViewGroup +import android.view.* import androidx.annotation.CallSuper import androidx.annotation.LayoutRes import androidx.annotation.MainThread @@ -42,7 +38,7 @@ import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable import timber.log.Timber -abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed, HasScreenInjector { +abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { // Butterknife unbinder private var mUnBinder: Unbinder? = null @@ -132,10 +128,6 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed, HasScreen super.onViewStateRestored(savedInstanceState) } - override fun onBackPressed(): Boolean { - return false - } - override fun invalidate() { //no-ops by default Timber.w("invalidate() method has not been implemented") diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 247c0c1981..7ef015a9b9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -202,10 +202,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { if (drawerLayout.isDrawerOpen(GravityCompat.START)) { drawerLayout.closeDrawer(GravityCompat.START) } else { - val handled = recursivelyDispatchOnBackPressed(supportFragmentManager) - if (!handled) { - super.onBackPressed() - } + super.onBackPressed() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index c0df7a8f43..afe3579d76 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -250,7 +250,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O return true } - return super.onBackPressed() + return false } // RoomSummaryController.Callback ************************************************************** diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 591e4ed474..543131f593 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -79,7 +79,6 @@ class LoginActivity : VectorBaseActivity() { showDisclaimerDialog(this) } - companion object { fun newIntent(context: Context): Intent { return Intent(context, LoginActivity::class.java) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt index 945bb86f4c..1ce282ad77 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt @@ -33,6 +33,7 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.VectorBaseFragment import kotlinx.android.synthetic.main.fragment_login_sso_fallback.* import timber.log.Timber @@ -42,7 +43,7 @@ import java.net.URLDecoder /** * Only login is supported for the moment */ -class LoginSsoFallbackFragment : VectorBaseFragment() { +class LoginSsoFallbackFragment : VectorBaseFragment(), OnBackPressed { private val viewModel: LoginViewModel by activityViewModel() From 137dcab734f99cace6cd2e22646a63380b4458b0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Sep 2019 16:20:19 +0200 Subject: [PATCH 08/44] Curl login interceptor now log the AT (on debug mode) --- .../android/internal/session/SessionModule.kt | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index ab44a4aa93..4cde0f688d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -35,16 +35,16 @@ import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater -import im.vector.matrix.android.internal.session.room.DefaultRoomFactory import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater -import im.vector.matrix.android.internal.session.room.RoomFactory import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver import im.vector.matrix.android.internal.session.room.prune.EventsPruner import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver import im.vector.matrix.android.internal.util.md5 import io.realm.RealmConfiguration import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import java.io.File @@ -94,14 +94,29 @@ internal abstract class SessionModule { .build() } + // TODO Check with Ganfra, I do not want to add this, I want to use the CurlLoggingInterceptor of @MatrixScope + @Provides + @JvmStatic + fun providesCurlLoggingInterceptor(): CurlLoggingInterceptor { + return CurlLoggingInterceptor(HttpLoggingInterceptor.Logger.DEFAULT) + } + + @JvmStatic @Provides @SessionScope @Authenticated fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, - accessTokenInterceptor: AccessTokenInterceptor): OkHttpClient { + accessTokenInterceptor: AccessTokenInterceptor, + curlLoggingInterceptor: CurlLoggingInterceptor): OkHttpClient { return okHttpClient.newBuilder() + .apply { + // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor + val existingCurlInterceptors = interceptors().filterIsInstance() + interceptors().removeAll(existingCurlInterceptors) + } .addInterceptor(accessTokenInterceptor) + .addInterceptor(curlLoggingInterceptor) .build() } From 3e6b65e174bb282df0c76c658c8adec622028e67 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Sep 2019 18:21:56 +0200 Subject: [PATCH 09/44] Handle M_CONSENT_NOT_GIVEN error (#64) --- CHANGES.md | 2 +- matrix-sdk-android/build.gradle | 3 + .../api/failure/ConsentNotGivenError.kt | 22 +++ .../android/internal/network/Request.kt | 7 + vector/build.gradle | 3 + vector/src/main/AndroidManifest.xml | 1 + .../src/main/assets/open_source_licenses.html | 5 + .../vector/riotx/core/dialogs/DialogLocker.kt | 4 +- .../vector/riotx/core/error/ErrorFormatter.kt | 10 +- .../riotx/core/platform/VectorBaseActivity.kt | 32 ++++- .../riotx/core/utils/WeakReferenceDelegate.kt | 32 +++++ .../features/consent/ConsentNotGivenHelper.kt | 57 ++++++++ .../webview/ConsentWebViewEventListener.kt | 88 ++++++++++++ .../webview/DefaultWebViewEventListener.kt | 48 +++++++ .../features/webview/VectorWebViewActivity.kt | 133 ++++++++++++++++++ .../features/webview/VectorWebViewClient.kt | 83 +++++++++++ .../features/webview/WebViewEventListener.kt | 59 ++++++++ .../webview/WebViewEventListenerFactory.kt | 29 ++++ .../riotx/features/webview/WebViewMode.kt | 38 +++++ .../res/layout/activity_vector_web_view.xml | 36 +++++ vector/src/main/res/values/strings_riotX.xml | 3 + 21 files changed, 689 insertions(+), 6 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/ConsentNotGivenError.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/WeakReferenceDelegate.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/consent/ConsentNotGivenHelper.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/webview/ConsentWebViewEventListener.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/webview/DefaultWebViewEventListener.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewActivity.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewClient.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListener.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListenerFactory.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/webview/WebViewMode.kt create mode 100644 vector/src/main/res/layout/activity_vector_web_view.xml diff --git a/CHANGES.md b/CHANGES.md index 8769b11ea7..2b07b6d769 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ Changes in RiotX 0.5.0 (2019-XX-XX) =================================================== Features: - - + - Handle M_CONSENT_NOT_GIVEN error (#64) Improvements: - Reduce default release build log level, and lab option to enable more logs. diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index fbe0969159..8002625e12 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -139,6 +139,9 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.facebook.stetho:stetho-okhttp3:1.5.0' + // Bus + implementation 'org.greenrobot:eventbus:3.1.1' + debugImplementation 'com.airbnb.okreplay:okreplay:1.4.0' releaseImplementation 'com.airbnb.okreplay:noop:1.4.0' androidTestImplementation 'com.airbnb.okreplay:espresso:1.4.0' diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/ConsentNotGivenError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/ConsentNotGivenError.kt new file mode 100644 index 0000000000..c780720a18 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/ConsentNotGivenError.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.failure + +// This data class will be sent to the bus +data class ConsentNotGivenError( + val consentUri: String +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt index 3d1e433b1b..4be2d4a27f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt @@ -18,10 +18,12 @@ package im.vector.matrix.android.internal.network import com.squareup.moshi.JsonDataException import com.squareup.moshi.Moshi +import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.internal.di.MoshiProvider import okhttp3.ResponseBody +import org.greenrobot.eventbus.EventBus import retrofit2.Call import timber.log.Timber import java.io.IOException @@ -65,6 +67,11 @@ internal class Request { val matrixError = matrixErrorAdapter.fromJson(errorBodyStr) if (matrixError != null) { + if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) { + // Also send this error to the bus, for a global management + EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri)) + } + return Failure.ServerError(matrixError, httpCode) } } catch (ex: JsonDataException) { diff --git a/vector/build.gradle b/vector/build.gradle index de47937676..866a95b740 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -280,6 +280,9 @@ dependencies { implementation "ru.noties.markwon:html:$markwon_version" implementation 'me.saket:better-link-movement-method:2.2.0' + // Bus + implementation 'org.greenrobot:eventbus:3.1.1' + // Passphrase strength helper implementation 'com.nulab-inc:zxcvbn:1.2.5' diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index e4cdaee2e4..949da5132f 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -65,6 +65,7 @@ + diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 9e87d466af..1195d38b90 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -349,6 +349,11 @@ SOFTWARE.
Copyright 2018 The diff-match-patch Authors. https://github.com/google/diff-match-patch +
  • + EventBus +
    + Copyright (C) 2012-2017 Markus Junginger, greenrobot (http://greenrobot.org) +
  • diff --git a/vector/src/main/java/im/vector/riotx/core/dialogs/DialogLocker.kt b/vector/src/main/java/im/vector/riotx/core/dialogs/DialogLocker.kt index 5418e2270a..60397fb6b6 100644 --- a/vector/src/main/java/im/vector/riotx/core/dialogs/DialogLocker.kt +++ b/vector/src/main/java/im/vector/riotx/core/dialogs/DialogLocker.kt @@ -26,9 +26,9 @@ private const val KEY_DIALOG_IS_DISPLAYED = "DialogLocker.KEY_DIALOG_IS_DISPLAYE /** * Class to avoid displaying twice the same dialog */ -class DialogLocker() : Restorable { +class DialogLocker(savedInstanceState: Bundle?) : Restorable { - private var isDialogDisplayed: Boolean = false + private var isDialogDisplayed = savedInstanceState?.getBoolean(KEY_DIALOG_IS_DISPLAYED, false) == true private fun unlock() { isDialogDisplayed = false diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index d42bce64c5..bb7892e109 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -17,6 +17,7 @@ package im.vector.riotx.core.error import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider import javax.inject.Inject @@ -34,8 +35,13 @@ class ErrorFormatter @Inject constructor(val stringProvider: StringProvider) { null -> null is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network) is Failure.ServerError -> { - throwable.error.message.takeIf { it.isNotEmpty() } - ?: throwable.error.code.takeIf { it.isNotEmpty() } + if (throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN) { + // Special case for terms and conditions + stringProvider.getString(R.string.error_terms_not_accepted) + } else { + throwable.error.message.takeIf { it.isNotEmpty() } + ?: throwable.error.code.takeIf { it.isNotEmpty() } + } } else -> throwable.localizedMessage } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index fad2c0ed87..a0e67d51ca 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -36,11 +36,14 @@ import butterknife.Unbinder import com.airbnb.mvrx.BaseMvRxActivity import com.bumptech.glide.util.Util import com.google.android.material.snackbar.Snackbar +import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.di.* +import im.vector.riotx.core.dialogs.DialogLocker import im.vector.riotx.core.utils.toast import im.vector.riotx.features.configuration.VectorConfiguration +import im.vector.riotx.features.consent.ConsentNotGivenHelper import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.rageshake.BugReportActivity import im.vector.riotx.features.rageshake.BugReporter @@ -50,6 +53,9 @@ import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.receivers.DebugReceiver import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import timber.log.Timber import kotlin.system.measureTimeMillis @@ -391,6 +397,31 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { } } + /* ========================================================================================== + * User Consent + * ========================================================================================== */ + + private val consentNotGivenHelper by lazy { + ConsentNotGivenHelper(this, DialogLocker(savedInstanceState)) + .apply { restorables.add(this) } + } + + override fun onStart() { + super.onStart() + EventBus.getDefault().register(this) + } + + override fun onStop() { + super.onStop() + EventBus.getDefault().unregister(this) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError) { + consentNotGivenHelper.displayDialog(consentNotGivenError.consentUri, + screenComponent.session().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "") + } + /* ========================================================================================== * Temporary method * ========================================================================================== */ @@ -402,5 +433,4 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { toast(getString(R.string.not_implemented)) } } - } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/utils/WeakReferenceDelegate.kt b/vector/src/main/java/im/vector/riotx/core/utils/WeakReferenceDelegate.kt new file mode 100644 index 0000000000..d892d64327 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/WeakReferenceDelegate.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.utils + +import java.lang.ref.WeakReference +import kotlin.reflect.KProperty + +fun weak(value: T) = WeakReferenceDelegate(value) + +class WeakReferenceDelegate(value: T) { + + private var weakReference: WeakReference = WeakReference(value) + + operator fun getValue(thisRef: Any, property: KProperty<*>): T? = weakReference.get() + operator fun setValue(thisRef: Any, property: KProperty<*>, value: T) { + weakReference = WeakReference(value) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/consent/ConsentNotGivenHelper.kt b/vector/src/main/java/im/vector/riotx/features/consent/ConsentNotGivenHelper.kt new file mode 100644 index 0000000000..0108e0cd88 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/consent/ConsentNotGivenHelper.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.consent + +import android.app.Activity +import androidx.appcompat.app.AlertDialog +import im.vector.riotx.R +import im.vector.riotx.core.dialogs.DialogLocker +import im.vector.riotx.core.platform.Restorable +import im.vector.riotx.features.webview.VectorWebViewActivity +import im.vector.riotx.features.webview.WebViewMode + +class ConsentNotGivenHelper(private val activity: Activity, + private val dialogLocker: DialogLocker) : + Restorable by dialogLocker { + + /* ========================================================================================== + * Public methods + * ========================================================================================== */ + + /** + * Display the consent dialog, if not already displayed + */ + fun displayDialog(consentUri: String, homeServerHost: String) { + dialogLocker.displayDialog { + AlertDialog.Builder(activity) + .setTitle(R.string.settings_app_term_conditions) + .setMessage(activity.getString(R.string.dialog_user_consent_content, homeServerHost)) + .setPositiveButton(R.string.dialog_user_consent_submit) { _, _ -> + openWebViewActivity(consentUri) + } + } + } + + /* ========================================================================================== + * Private + * ========================================================================================== */ + + private fun openWebViewActivity(consentUri: String) { + val intent = VectorWebViewActivity.getIntent(activity, consentUri, activity.getString(R.string.settings_app_term_conditions), WebViewMode.CONSENT) + activity.startActivity(intent) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/webview/ConsentWebViewEventListener.kt b/vector/src/main/java/im/vector/riotx/features/webview/ConsentWebViewEventListener.kt new file mode 100644 index 0000000000..c09639c29f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/webview/ConsentWebViewEventListener.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.webview + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.Session +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.utils.weak +import timber.log.Timber + +private const val SUCCESS_URL_SUFFIX = "/_matrix/consent" +private const val RIOT_BOT_ID = "@riot-bot:matrix.org" + +/** + * This class is the Consent implementation of WebViewEventListener. + * It is used to manage the consent agreement flow. + */ +class ConsentWebViewEventListener(activity: VectorBaseActivity, + private val session: Session, + private val delegate: WebViewEventListener) + : WebViewEventListener by delegate { + + private val safeActivity: VectorBaseActivity? by weak(activity) + + override fun onPageFinished(url: String) { + delegate.onPageFinished(url) + if (url.endsWith(SUCCESS_URL_SUFFIX)) { + createRiotBotRoomIfNeeded() + } + } + + /** + * This methods try to create the RiotBot room when the user gives his agreement + */ + private fun createRiotBotRoomIfNeeded() { + safeActivity?.let { + /* We do not create a Room with RiotBot in RiotX for the moment + val joinedRooms = session.dataHandler.store.rooms.filter { + it.isJoined + } + if (joinedRooms.isEmpty()) { + it.showWaitingView() + // Ensure we can create a Room with riot-bot. Error can be a MatrixError: "Federation denied with matrix.org.", or any other error. + session.profileApiClient + .displayname(RIOT_BOT_ID, object : MatrixCallback(createRiotBotRoomCallback) { + override fun onSuccess(info: String?) { + // Ok, the Home Server knows riot-Bot, so create a Room with him + session.createDirectMessageRoom(RIOT_BOT_ID, createRiotBotRoomCallback) + } + }) + } else { + */ + it.finish() + /* + } + */ + } + } + + /** + * APICallback instance + */ + private val createRiotBotRoomCallback = object : MatrixCallback { + override fun onSuccess(data: String) { + Timber.d("## On success : succeed to invite riot-bot") + safeActivity?.finish() + } + + override fun onFailure(failure: Throwable) { + Timber.e("## On error : failed to invite riot-bot $failure") + safeActivity?.finish() + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/webview/DefaultWebViewEventListener.kt b/vector/src/main/java/im/vector/riotx/features/webview/DefaultWebViewEventListener.kt new file mode 100644 index 0000000000..f72d719872 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/webview/DefaultWebViewEventListener.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.webview + +import timber.log.Timber + +/** + * This class is the default implementation of WebViewEventListener. + * It can be used with delegation pattern + */ + +class DefaultWebViewEventListener : WebViewEventListener { + + override fun pageWillStart(url: String) { + Timber.v("On page will start: $url") + } + + override fun onPageStarted(url: String) { + Timber.d("On page started: $url") + } + + override fun onPageFinished(url: String) { + Timber.d("On page finished: $url") + } + + override fun onPageError(url: String, errorCode: Int, description: String) { + Timber.e("On received error: $url - errorCode: $errorCode - message: $description") + } + + override fun shouldOverrideUrlLoading(url: String): Boolean { + Timber.v("Should override url: $url") + return false + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewActivity.kt b/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewActivity.kt new file mode 100644 index 0000000000..914de2261f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewActivity.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.webview + +import android.content.Context +import android.content.Intent +import android.os.Build +import android.webkit.WebChromeClient +import android.webkit.WebView +import androidx.annotation.CallSuper +import im.vector.matrix.android.api.session.Session +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.platform.VectorBaseActivity +import kotlinx.android.synthetic.main.activity_vector_web_view.* +import javax.inject.Inject + +/** + * This class is responsible for managing a WebView + * It does also have a loading view and a toolbar + * It relies on the VectorWebViewClient + * This class shouldn't be extended. To add new behaviors, you might create a new WebViewMode and a new WebViewEventListener + */ +class VectorWebViewActivity : VectorBaseActivity() { + + override fun getLayoutRes() = R.layout.activity_vector_web_view + + @Inject lateinit var session: Session + + @CallSuper + override fun injectWith(injector: ScreenComponent) { + session = injector.session() + } + + override fun initUiAndData() { + configureToolbar(webview_toolbar) + waitingView = findViewById(R.id.simple_webview_loader) + + simple_webview.settings.apply { + // Enable Javascript + javaScriptEnabled = true + + // Use WideViewport and Zoom out if there is no viewport defined + useWideViewPort = true + loadWithOverviewMode = true + + // Enable pinch to zoom without the zoom buttons + builtInZoomControls = true + + // Allow use of Local Storage + domStorageEnabled = true + + allowFileAccessFromFileURLs = true + allowUniversalAccessFromFileURLs = true + + displayZoomControls = false + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val cookieManager = android.webkit.CookieManager.getInstance() + cookieManager.setAcceptThirdPartyCookies(simple_webview, true) + } + + val url = intent.extras.getString(EXTRA_URL) + val title = intent.extras.getString(EXTRA_TITLE, USE_TITLE_FROM_WEB_PAGE) + if (title != USE_TITLE_FROM_WEB_PAGE) { + setTitle(title) + } + + val webViewMode = intent.extras.getSerializable(EXTRA_MODE) as WebViewMode + val eventListener = webViewMode.eventListener(this, session) + simple_webview.webViewClient = VectorWebViewClient(eventListener) + simple_webview.webChromeClient = object : WebChromeClient() { + override fun onReceivedTitle(view: WebView, title: String) { + if (title == USE_TITLE_FROM_WEB_PAGE) { + setTitle(title) + } + } + } + simple_webview.loadUrl(url) + } + + /* ========================================================================================== + * UI event + * ========================================================================================== */ + + override fun onBackPressed() { + if (simple_webview.canGoBack()) { + simple_webview.goBack() + } else { + super.onBackPressed() + } + } + + /* ========================================================================================== + * Companion + * ========================================================================================== */ + + companion object { + private const val EXTRA_URL = "EXTRA_URL" + private const val EXTRA_TITLE = "EXTRA_TITLE" + private const val EXTRA_MODE = "EXTRA_MODE" + + private const val USE_TITLE_FROM_WEB_PAGE = "" + + fun getIntent(context: Context, + url: String, + title: String = USE_TITLE_FROM_WEB_PAGE, + mode: WebViewMode = WebViewMode.DEFAULT): Intent { + return Intent(context, VectorWebViewActivity::class.java) + .apply { + putExtra(EXTRA_URL, url) + putExtra(EXTRA_TITLE, title) + putExtra(EXTRA_MODE, mode) + } + } + } +} + diff --git a/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewClient.kt b/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewClient.kt new file mode 100644 index 0000000000..080bbacae8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewClient.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.webview + +import android.annotation.TargetApi +import android.graphics.Bitmap +import android.os.Build +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient + +/** + * This class inherits from WebViewClient. It has to be used with a WebView. + * It's responsible for dispatching events to the WebViewEventListener + */ +class VectorWebViewClient(private val eventListener: WebViewEventListener) : WebViewClient() { + + private var mInError: Boolean = false + + @TargetApi(Build.VERSION_CODES.N) + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { + return shouldOverrideUrl(request.url.toString()) + } + + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + return shouldOverrideUrl(url) + } + + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + mInError = false + eventListener.onPageStarted(url) + } + + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + if (!mInError) { + eventListener.onPageFinished(url) + } + } + + override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { + super.onReceivedError(view, errorCode, description, failingUrl) + if (!mInError) { + mInError = true + eventListener.onPageError(failingUrl, errorCode, description) + } + } + + @TargetApi(Build.VERSION_CODES.N) + override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) { + super.onReceivedError(view, request, error) + if (!mInError) { + mInError = true + eventListener.onPageError(request.url.toString(), error.errorCode, error.description.toString()) + } + } + + private fun shouldOverrideUrl(url: String): Boolean { + mInError = false + val shouldOverrideUrlLoading = eventListener.shouldOverrideUrlLoading(url) + if (!shouldOverrideUrlLoading) { + eventListener.pageWillStart(url) + } + return shouldOverrideUrlLoading + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListener.kt b/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListener.kt new file mode 100644 index 0000000000..571eeff4fe --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListener.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.webview + +interface WebViewEventListener { + + /** + * Triggered when a webview page is about to be started. + * + * @param url The url about to be rendered. + */ + fun pageWillStart(url: String) + + /** + * Triggered when a loading webview page has started. + * + * @param url The rendering url. + */ + fun onPageStarted(url: String) + + /** + * Triggered when a loading webview page has finished loading but has not been rendered yet. + * + * @param url The finished url. + */ + fun onPageFinished(url: String) + + /** + * Triggered when an error occurred while loading a page. + * + * @param url The url that failed. + * @param errorCode The error code. + * @param description The error description. + */ + fun onPageError(url: String, errorCode: Int, description: String) + + /** + * Triggered when a webview load an url + * + * @param url The url about to be rendered. + * @return true if the method needs to manage some custom handling + */ + fun shouldOverrideUrlLoading(url: String): Boolean + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListenerFactory.kt b/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListenerFactory.kt new file mode 100644 index 0000000000..d84b72a49d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListenerFactory.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.webview + +import im.vector.matrix.android.api.session.Session +import im.vector.riotx.core.platform.VectorBaseActivity + +interface WebViewEventListenerFactory { + + /** + * @return an instance of WebViewEventListener + */ + fun eventListener(activity: VectorBaseActivity, session: Session): WebViewEventListener + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/webview/WebViewMode.kt b/vector/src/main/java/im/vector/riotx/features/webview/WebViewMode.kt new file mode 100644 index 0000000000..86e9a2f18b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/webview/WebViewMode.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.webview + +import im.vector.matrix.android.api.session.Session +import im.vector.riotx.core.platform.VectorBaseActivity + +/** + * This enum indicates the WebView mode. It's responsible for creating a WebViewEventListener + */ +enum class WebViewMode : WebViewEventListenerFactory { + + DEFAULT { + override fun eventListener(activity: VectorBaseActivity, session: Session): WebViewEventListener { + return DefaultWebViewEventListener() + } + }, + CONSENT { + override fun eventListener(activity: VectorBaseActivity, session: Session): WebViewEventListener { + return ConsentWebViewEventListener(activity, session, DefaultWebViewEventListener()) + } + }; + +} \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_vector_web_view.xml b/vector/src/main/res/layout/activity_vector_web_view.xml new file mode 100644 index 0000000000..8a5633acbc --- /dev/null +++ b/vector/src/main/res/layout/activity_vector_web_view.xml @@ -0,0 +1,36 @@ + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 3dd8c6b36a..0ef32154b7 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -5,4 +5,7 @@ Enable verbose logs. Verbose logs will help developers by providing more logs when you send a RageShake. Even when enabled, the application does not log message contents or any other private data. + + Please retry once you have accepted the terms and conditions of your homeserver. + \ No newline at end of file From 6bda437f5d926a4026c3c278cdaa4126c3ac7a13 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Sep 2019 10:58:51 +0200 Subject: [PATCH 10/44] Auto configure homeserver and identity server URLs of LoginActivity with a magic link --- CHANGES.md | 1 + tools/tests/test_configuration_link.sh | 3 + vector/src/main/AndroidManifest.xml | 12 ++ .../vector/riotx/core/di/ScreenComponent.kt | 3 + .../im/vector/riotx/features/MainActivity.kt | 2 +- .../features/link/LinkHandlerActivity.kt | 120 ++++++++++++++++++ .../riotx/features/login/LoginActions.kt | 1 + .../riotx/features/login/LoginActivity.kt | 14 +- .../riotx/features/login/LoginConfig.kt | 43 +++++++ .../riotx/features/login/LoginFragment.kt | 8 +- .../riotx/features/login/LoginViewModel.kt | 11 ++ .../src/main/res/layout/activity_progress.xml | 15 +++ vector/src/main/res/values/strings_riotX.xml | 4 + 13 files changed, 232 insertions(+), 5 deletions(-) create mode 100755 tools/tests/test_configuration_link.sh create mode 100644 vector/src/main/java/im/vector/riotx/features/link/LinkHandlerActivity.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/LoginConfig.kt create mode 100644 vector/src/main/res/layout/activity_progress.xml diff --git a/CHANGES.md b/CHANGES.md index 2b07b6d769..a7772bc9f2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ Changes in RiotX 0.5.0 (2019-XX-XX) Features: - Handle M_CONSENT_NOT_GIVEN error (#64) + - Auto configure homeserver and identity server URLs of LoginActivity with a magic link Improvements: - Reduce default release build log level, and lab option to enable more logs. diff --git a/tools/tests/test_configuration_link.sh b/tools/tests/test_configuration_link.sh new file mode 100755 index 0000000000..33b1699e70 --- /dev/null +++ b/tools/tests/test_configuration_link.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +adb shell am start -a android.intent.action.VIEW -d "https://riot.im/config/config?hs_url=https%3A%2F%2Fmozilla-test.modular.im" diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 949da5132f..01d8db467e 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -66,6 +66,18 @@ + + + + + + + + + + + + diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index ccf3a19202..1395d6d433 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -46,6 +46,7 @@ import im.vector.riotx.features.home.room.detail.timeline.action.* import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.invite.VectorInviteView +import im.vector.riotx.features.link.LinkHandlerActivity import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.login.LoginFragment import im.vector.riotx.features.login.LoginSsoFallbackFragment @@ -138,6 +139,8 @@ interface ScreenComponent { fun inject(loginActivity: LoginActivity) + fun inject(linkHandlerActivity: LinkHandlerActivity) + fun inject(mainActivity: MainActivity) fun inject(roomDirectoryActivity: RoomDirectoryActivity) diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index 8ef3f0adcb..c32c35c2f1 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -104,7 +104,7 @@ class MainActivity : VectorBaseActivity() { val intent = if (sessionHolder.hasActiveSession()) { HomeActivity.newIntent(this) } else { - LoginActivity.newIntent(this) + LoginActivity.newIntent(this, null) } startActivity(intent) finish() diff --git a/vector/src/main/java/im/vector/riotx/features/link/LinkHandlerActivity.kt b/vector/src/main/java/im/vector/riotx/features/link/LinkHandlerActivity.kt new file mode 100644 index 0000000000..b114e51607 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/link/LinkHandlerActivity.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.link + +import android.content.Intent +import android.net.Uri +import androidx.appcompat.app.AlertDialog +import im.vector.matrix.android.api.MatrixCallback +import im.vector.riotx.R +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.login.LoginActivity +import im.vector.riotx.features.login.LoginConfig +import timber.log.Timber +import javax.inject.Inject + + +/** + * Dummy activity used to dispatch the vector URL links. + */ +class LinkHandlerActivity : VectorBaseActivity() { + + @Inject lateinit var sessionHolder: ActiveSessionHolder + @Inject lateinit var errorFormatter: ErrorFormatter + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getLayoutRes() = R.layout.activity_progress + + override fun initUiAndData() { + val uri = intent.data + + if (uri == null) { + // Should not happen + Timber.w("Uri is null") + finish() + return + } + + if (uri.path == PATH_CONFIG) { + if (sessionHolder.hasActiveSession()) { + displayAlreadyLoginPopup(uri) + } else { + // user is not yet logged in, this is the nominal case + startLoginActivity(uri) + } + } else { + // Other link are not yet handled, but should not comes here (manifest configuration error?) + Timber.w("Unable to handle this uir: $uri") + finish() + } + } + + /** + * Start the login screen with identity server and home server pre-filled + */ + private fun startLoginActivity(uri: Uri) { + val intent = LoginActivity.newIntent(this, LoginConfig.parse(uri)) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + finish() + } + + /** + * Propose to disconnect from a previous HS, when clicking on an auto config link + */ + private fun displayAlreadyLoginPopup(uri: Uri) { + AlertDialog.Builder(this) + .setTitle(R.string.dialog_title_warning) + .setMessage(R.string.error_user_already_logged_in) + .setCancelable(false) + .setPositiveButton(R.string.logout) { _, _ -> + sessionHolder.getSafeActiveSession()?.signOut(object : MatrixCallback { + override fun onFailure(failure: Throwable) { + displayError(failure) + } + + override fun onSuccess(data: Unit) { + Timber.d("## displayAlreadyLoginPopup(): logout succeeded") + sessionHolder.clearActiveSession() + startLoginActivity(uri) + } + }) ?: finish() + } + .setNegativeButton(R.string.cancel) { _, _ -> finish() } + .show() + } + + private fun displayError(failure: Throwable) { + AlertDialog.Builder(this) + .setTitle(R.string.dialog_title_error) + .setMessage(errorFormatter.toHumanReadable(failure)) + .setCancelable(false) + .setPositiveButton(R.string.ok) { _, _ -> finish() } + .show() + } + + companion object { + private const val PATH_CONFIG = "/config/config" + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt index 42a7320152..0691d41fcd 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt @@ -24,5 +24,6 @@ sealed class LoginActions { data class Login(val login: String, val password: String) : LoginActions() data class SsoLoginSuccess(val credentials: Credentials) : LoginActions() data class NavigateTo(val target: LoginActivity.Navigation) : LoginActions() + data class InitWith(val loginConfig: LoginConfig) : LoginActions() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 543131f593..89497d4bea 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -56,6 +56,12 @@ class LoginActivity : VectorBaseActivity() { addFragment(LoginFragment(), R.id.simpleFragmentContainer) } + // Get config extra + val loginConfig = intent.getParcelableExtra(EXTRA_CONFIG) + if (loginConfig != null && isFirstCreation()) { + loginViewModel.handle(LoginActions.InitWith(loginConfig)) + } + loginViewModel.navigationLiveData.observeEvent(this) { when (it) { is Navigation.OpenSsoLoginFallback -> addFragmentToBackstack(LoginSsoFallbackFragment(), R.id.simpleFragmentContainer) @@ -80,8 +86,12 @@ class LoginActivity : VectorBaseActivity() { } companion object { - fun newIntent(context: Context): Intent { - return Intent(context, LoginActivity::class.java) + private val EXTRA_CONFIG = "EXTRA_CONFIG" + + fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { + return Intent(context, LoginActivity::class.java).apply { + putExtra(EXTRA_CONFIG, loginConfig) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginConfig.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginConfig.kt new file mode 100644 index 0000000000..1613d1b041 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginConfig.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import android.net.Uri +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +/** + * Parameters extracted from a configuration url + * Ex: https://riot.im/config/config?hs_url=https%3A%2F%2Fexample.modular.im&is_url=https%3A%2F%2Fcustom.identity.org + * + * Note: On RiotX, identityServerUrl will never be used, so is declared private. Keep it for compatibility reason. + */ +@Parcelize +data class LoginConfig( + val homeServerUrl: String?, + private val identityServerUrl: String? +) : Parcelable { + + companion object { + fun parse(from: Uri): LoginConfig { + return LoginConfig( + homeServerUrl = from.getQueryParameter("hs_url"), + identityServerUrl = from.getQueryParameter("is_url") + ) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 5c9bf9e2aa..6e559bcbe0 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -74,8 +74,12 @@ class LoginFragment : VectorBaseFragment() { } .disposeOnDestroy() - - homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(requireContext())) + val initHsUrl = viewModel.getInitialHomeServerUrl() + if (initHsUrl != null) { + homeServerField.setText(initHsUrl) + } else { + homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(requireContext())) + } viewModel.handle(LoginActions.UpdateHomeServer(homeServerField.text.toString())) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index ec4e9e05e7..96de7cd0df 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -55,6 +55,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + private var loginConfig: LoginConfig? = null + private val _navigationLiveData = MutableLiveData>() val navigationLiveData: LiveData> get() = _navigationLiveData @@ -65,6 +67,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi fun handle(action: LoginActions) { when (action) { + is LoginActions.InitWith -> handleInitWith(action) is LoginActions.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginActions.Login -> handleLogin(action) is LoginActions.SsoLoginSuccess -> handleSsoLoginSuccess(action) @@ -72,6 +75,10 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } + private fun handleInitWith(action: LoginActions.InitWith) { + loginConfig = action.loginConfig + } + private fun handleLogin(action: LoginActions.Login) { val homeServerConnectionConfigFinal = homeServerConnectionConfig @@ -186,6 +193,10 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi currentTask?.cancel() } + fun getInitialHomeServerUrl(): String? { + return loginConfig?.homeServerUrl + } + fun getHomeServerUrl(): String { return homeServerConnectionConfig?.homeServerUri?.toString() ?: "" } diff --git a/vector/src/main/res/layout/activity_progress.xml b/vector/src/main/res/layout/activity_progress.xml new file mode 100644 index 0000000000..ae7b87b61e --- /dev/null +++ b/vector/src/main/res/layout/activity_progress.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 0ef32154b7..e02de69806 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -8,4 +8,8 @@ Please retry once you have accepted the terms and conditions of your homeserver. + + It looks like you’re trying to connect to another homeserver. Do you want to sign out? + + \ No newline at end of file From ca6bcde82d2159e9a3af100fbb276e8ce67e29a3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Sep 2019 14:43:08 +0200 Subject: [PATCH 11/44] Re add the remove CurlLoggingInterceptor --- .../android/internal/session/SessionModule.kt | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 4cde0f688d..7570d50b9c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -44,7 +44,6 @@ import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEve import im.vector.matrix.android.internal.util.md5 import io.realm.RealmConfiguration import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import java.io.File @@ -94,29 +93,25 @@ internal abstract class SessionModule { .build() } - // TODO Check with Ganfra, I do not want to add this, I want to use the CurlLoggingInterceptor of @MatrixScope - @Provides - @JvmStatic - fun providesCurlLoggingInterceptor(): CurlLoggingInterceptor { - return CurlLoggingInterceptor(HttpLoggingInterceptor.Logger.DEFAULT) - } - - @JvmStatic @Provides @SessionScope @Authenticated fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, - accessTokenInterceptor: AccessTokenInterceptor, - curlLoggingInterceptor: CurlLoggingInterceptor): OkHttpClient { + accessTokenInterceptor: AccessTokenInterceptor): OkHttpClient { return okHttpClient.newBuilder() .apply { // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor val existingCurlInterceptors = interceptors().filterIsInstance() interceptors().removeAll(existingCurlInterceptors) + + addInterceptor(accessTokenInterceptor) + + // Re add eventually the curl logging interceptors + existingCurlInterceptors.forEach { + addInterceptor(it) + } } - .addInterceptor(accessTokenInterceptor) - .addInterceptor(curlLoggingInterceptor) .build() } From 138a210a73c2b1645784863a38a46e5003eb005a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Sep 2019 14:43:39 +0200 Subject: [PATCH 12/44] Dagger: Screen component now exposes ActiveSessionHolder instead of Session --- .../im/vector/matrix/android/internal/di/MatrixScope.kt | 3 +++ .../main/java/im/vector/riotx/core/di/ScreenComponent.kt | 3 +-- .../vector/riotx/core/platform/SimpleFragmentActivity.kt | 2 +- .../im/vector/riotx/core/platform/VectorBaseActivity.kt | 8 +++++--- .../java/im/vector/riotx/features/login/LoginActivity.kt | 2 +- .../riotx/features/settings/VectorSettingsBaseFragment.kt | 2 +- .../riotx/features/settings/push/PushRulesViewModel.kt | 2 +- .../riotx/features/webview/VectorWebViewActivity.kt | 2 +- .../workers/signout/SignOutBottomSheetDialogFragment.kt | 2 +- 9 files changed, 15 insertions(+), 11 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt index 9c9327df55..fadcdacf21 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt @@ -18,6 +18,9 @@ package im.vector.matrix.android.internal.di import javax.inject.Scope +/** + * Use the annotation @MatrixScope to annotate classes we want the SDK to instantiate only once + */ @Scope @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 1395d6d433..01db4b4a01 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -21,7 +21,6 @@ import androidx.lifecycle.ViewModelProvider import dagger.BindsInstance import dagger.Component import im.vector.fragments.keysbackup.restore.KeysBackupRestoreFromPassphraseFragment -import im.vector.matrix.android.api.session.Session import im.vector.riotx.core.preference.UserAvatarPreference import im.vector.riotx.features.MainActivity import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyFragment @@ -71,7 +70,7 @@ import im.vector.riotx.features.settings.push.PushGatewaysFragment @ScreenScope interface ScreenComponent { - fun session(): Session + fun activeSessionHolder(): ActiveSessionHolder fun viewModelFactory(): ViewModelProvider.Factory diff --git a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt index ff30138990..85244a26db 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt @@ -49,7 +49,7 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() { @CallSuper override fun injectWith(injector: ScreenComponent) { - session = injector.session() + session = injector.activeSessionHolder().getActiveSession() } override fun initUiAndData() { diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index a0e67d51ca..cb65280907 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -79,6 +79,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { protected lateinit var bugReporter: BugReporter private lateinit var rageShake: RageShake protected lateinit var navigator: Navigator + private lateinit var activeSessionHolder: ActiveSessionHolder private var unBinder: Unbinder? = null @@ -133,6 +134,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { bugReporter = screenComponent.bugReporter() rageShake = screenComponent.rageShake() navigator = screenComponent.navigator() + activeSessionHolder = screenComponent.activeSessionHolder() configurationViewModel.activityRestarter.observe(this, Observer { if (!it.hasBeenHandled) { // Recreate the Activity because configuration has changed @@ -181,7 +183,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { configurationViewModel.onActivityResumed() if (this !is BugReportActivity) { - rageShake?.start() + rageShake.start() } DebugReceiver @@ -196,7 +198,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { override fun onPause() { super.onPause() - rageShake?.stop() + rageShake.stop() debugReceiver?.let { unregisterReceiver(debugReceiver) @@ -419,7 +421,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { @Subscribe(threadMode = ThreadMode.MAIN) fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError) { consentNotGivenHelper.displayDialog(consentNotGivenError.consentUri, - screenComponent.session().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "") + activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "") } /* ========================================================================================== diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 89497d4bea..2debebfd32 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -86,7 +86,7 @@ class LoginActivity : VectorBaseActivity() { } companion object { - private val EXTRA_CONFIG = "EXTRA_CONFIG" + private const val EXTRA_CONFIG = "EXTRA_CONFIG" fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { return Intent(context, LoginActivity::class.java).apply { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt index b78921b5b9..52c332cfb3 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt @@ -54,7 +54,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), HasScree override fun onAttach(context: Context) { screenComponent = DaggerScreenComponent.factory().create(vectorActivity.getVectorComponent(), vectorActivity) super.onAttach(context) - session = screenComponent.session() + session = screenComponent.activeSessionHolder().getActiveSession() injectWith(injector()) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesViewModel.kt index 9de839b364..a8ea087727 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesViewModel.kt @@ -31,7 +31,7 @@ class PushRulesViewModel(initialState: PushRulesViewState) : VectorViewModel { override fun initialState(viewModelContext: ViewModelContext): PushRulesViewState? { - val session = (viewModelContext.activity as HasScreenInjector).injector().session() + val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() val rules = session.getPushRules() return PushRulesViewState(rules) } diff --git a/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewActivity.kt b/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewActivity.kt index 914de2261f..99c9d94336 100644 --- a/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewActivity.kt @@ -43,7 +43,7 @@ class VectorWebViewActivity : VectorBaseActivity() { @CallSuper override fun injectWith(injector: ScreenComponent) { - session = injector.session() + session = injector.activeSessionHolder().getActiveSession() } override fun initUiAndData() { diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt index c06a40e849..641a1ec8d0 100644 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt @@ -106,7 +106,7 @@ class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() { val vectorBaseActivity = activity as VectorBaseActivity val screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) viewModelFactory = screenComponent.viewModelFactory() - session = screenComponent.session() + session = screenComponent.activeSessionHolder().getActiveSession() } override fun onActivityCreated(savedInstanceState: Bundle?) { From 1f127335bc3d322522927a6d13f179b880ec3bbc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Sep 2019 15:50:56 +0200 Subject: [PATCH 13/44] Daggerization of RealmKeysUtils --- .../android/internal/auth/AuthModule.kt | 9 ++-- .../android/internal/crypto/CryptoModule.kt | 8 +-- .../internal/database/RealmKeysUtils.kt | 51 ++++++++++--------- .../android/internal/session/SessionModule.kt | 8 +-- 4 files changed, 42 insertions(+), 34 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt index 2459c3546a..51367ab3f3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt @@ -23,7 +23,7 @@ import dagger.Provides import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.internal.auth.db.AuthRealmModule import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore -import im.vector.matrix.android.internal.database.configureEncryption +import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.AuthDatabase import io.realm.RealmConfiguration import java.io.File @@ -36,13 +36,16 @@ internal abstract class AuthModule { @JvmStatic @Provides @AuthDatabase - fun providesRealmConfiguration(context: Context): RealmConfiguration { + fun providesRealmConfiguration(context: Context, realmKeysUtils: RealmKeysUtils): RealmConfiguration { val old = File(context.filesDir, "matrix-sdk-auth") if (old.exists()) { old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm")) } + return RealmConfiguration.Builder() - .configureEncryption("matrix-sdk-auth", context) + .apply { + realmKeysUtils.configureEncryption(this, "matrix-sdk-auth") + } .name("matrix-sdk-auth.realm") .modules(AuthRealmModule()) .deleteRealmIfMigrationNeeded() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index 4272dbd340..bf82cc6131 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -30,7 +30,7 @@ import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreMigration import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule import im.vector.matrix.android.internal.crypto.tasks.* -import im.vector.matrix.android.internal.database.configureEncryption +import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.CryptoDatabase import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.cache.ClearCacheTask @@ -50,12 +50,14 @@ internal abstract class CryptoModule { @Provides @CryptoDatabase @SessionScope - fun providesRealmConfiguration(context: Context, credentials: Credentials): RealmConfiguration { + fun providesRealmConfiguration(context: Context, credentials: Credentials, realmKeysUtils: RealmKeysUtils): RealmConfiguration { val userIDHash = credentials.userId.md5() return RealmConfiguration.Builder() .directory(File(context.filesDir, userIDHash)) - .configureEncryption("crypto_module_$userIDHash", context) + .apply { + realmKeysUtils.configureEncryption(this, "crypto_module_$userIDHash") + } .name("crypto_store.realm") .modules(RealmCryptoStoreModule()) .schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt index ee8ee41821..03ff641cf7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt @@ -17,10 +17,12 @@ package im.vector.matrix.android.internal.database import android.content.Context import android.util.Base64 +import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.util.SecretStoringUtils import io.realm.RealmConfiguration import timber.log.Timber import java.security.SecureRandom +import javax.inject.Inject /** * On creation a random key is generated, this key is then encrypted using the system KeyStore. @@ -34,12 +36,12 @@ import java.security.SecureRandom * then we generate a random secret key. The database key is encrypted with the secret key; The secret * key is encrypted with the public RSA key and stored with the encrypted key in the shared pref */ -private object RealmKeysUtils { - - private const val ENCRYPTED_KEY_PREFIX = "REALM_ENCRYPTED_KEY" +internal class RealmKeysUtils @Inject constructor(private val context: Context) { private val rng = SecureRandom() + private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.keys", Context.MODE_PRIVATE) + private fun generateKeyForRealm(): ByteArray { val keyForRealm = ByteArray(RealmConfiguration.KEY_LENGTH) rng.nextBytes(keyForRealm) @@ -49,8 +51,7 @@ private object RealmKeysUtils { /** * Check if there is already a key for this alias */ - fun hasKeyForDatabase(alias: String, context: Context): Boolean { - val sharedPreferences = getSharedPreferences(context) + private fun hasKeyForDatabase(alias: String): Boolean { return sharedPreferences.contains("${ENCRYPTED_KEY_PREFIX}_$alias") } @@ -59,13 +60,12 @@ private object RealmKeysUtils { * The random key is then encrypted by the keystore, and the encrypted key is stored * in shared preferences. * - * @return the generate key (can be passed to Realm Configuration) + * @return the generated key (can be passed to Realm Configuration) */ - fun createAndSaveKeyForDatabase(alias: String, context: Context): ByteArray { + private fun createAndSaveKeyForDatabase(alias: String): ByteArray { val key = generateKeyForRealm() val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING) val toStore = SecretStoringUtils.securelyStoreString(encodedKey, alias, context) - val sharedPreferences = getSharedPreferences(context) sharedPreferences .edit() .putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING)) @@ -77,30 +77,31 @@ private object RealmKeysUtils { * Retrieves the key for this database * throws if something goes wrong */ - fun extractKeyForDatabase(alias: String, context: Context): ByteArray { - val sharedPreferences = getSharedPreferences(context) + private fun extractKeyForDatabase(alias: String): ByteArray { val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null) val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING) val b64 = SecretStoringUtils.loadSecureSecret(encryptedKey, alias, context) return Base64.decode(b64!!, Base64.NO_PADDING) } - private fun getSharedPreferences(context: Context) = - context.getSharedPreferences("im.vector.matrix.android.keys", Context.MODE_PRIVATE) -} - - -fun RealmConfiguration.Builder.configureEncryption(alias: String, context: Context): RealmConfiguration.Builder { - if (RealmKeysUtils.hasKeyForDatabase(alias, context)) { - Timber.i("Found key for alias:$alias") - RealmKeysUtils.extractKeyForDatabase(alias, context).also { - this.encryptionKey(it) + fun configureEncryption(realmConfigurationBuilder: RealmConfiguration.Builder, alias: String) { + val key = if (hasKeyForDatabase(alias)) { + Timber.i("Found key for alias:$alias") + extractKeyForDatabase(alias) + } else { + Timber.i("Create key for DB alias:$alias") + createAndSaveKeyForDatabase(alias) } - } else { - Timber.i("Create key for DB alias:$alias") - RealmKeysUtils.createAndSaveKeyForDatabase(alias, context).also { - this.encryptionKey(it) + + if (BuildConfig.LOG_PRIVATE_DATA) { + val log = key.joinToString("") { "%02x".format(it) } + Timber.w("Database key for alias `$alias`: $log") } + + realmConfigurationBuilder.encryptionKey(key) + } + + companion object { + private const val ENCRYPTED_KEY_PREFIX = "REALM_ENCRYPTED_KEY" } - return this } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 7570d50b9c..9aa0365e28 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -28,7 +28,7 @@ import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.internal.database.LiveEntityObserver -import im.vector.matrix.android.internal.database.configureEncryption +import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.database.model.SessionRealmModule import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.di.SessionDatabase @@ -70,14 +70,16 @@ internal abstract class SessionModule { @Provides @SessionDatabase @SessionScope - fun providesRealmConfiguration(sessionParams: SessionParams, context: Context): RealmConfiguration { + fun providesRealmConfiguration(sessionParams: SessionParams, realmKeysUtils: RealmKeysUtils, context: Context): RealmConfiguration { val childPath = sessionParams.credentials.userId.md5() val directory = File(context.filesDir, childPath) return RealmConfiguration.Builder() .directory(directory) .name("disk_store.realm") - .configureEncryption("session_db_$childPath", context) + .apply { + realmKeysUtils.configureEncryption(this, "session_db_$childPath") + } .modules(SessionRealmModule()) .deleteRealmIfMigrationNeeded() .build() From c8010561fca99ac3a3d3bcdacbe30f5e74447b7c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Sep 2019 17:45:26 +0200 Subject: [PATCH 14/44] Rework on sign out task --- .../api/session/crypto/CryptoService.kt | 2 - .../android/internal/crypto/CryptoModule.kt | 14 +++---- .../internal/crypto/DefaultCryptoService.kt | 11 ----- .../android/internal/di/FileQualifiers.kt | 23 +++++++++++ .../android/internal/di/StringQualifiers.kt | 23 +++++++++++ .../internal/session/DefaultSession.kt | 40 ------------------- .../android/internal/session/SessionModule.kt | 27 +++++++++---- .../internal/session/signout/SignOutTask.kt | 35 ++++++++++++++-- .../internal/worker/WorkManagerUtil.kt | 1 + .../im/vector/riotx/features/MainActivity.kt | 22 +++++++++- 10 files changed, 126 insertions(+), 72 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index 0397b51439..43c783a13d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -109,8 +109,6 @@ interface CryptoService { fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>) - fun clearCryptoCache(callback: MatrixCallback) - fun addNewSessionListener(newSessionListener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index bf82cc6131..f5690123a4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto -import android.content.Context import dagger.Binds import dagger.Module import dagger.Provides @@ -32,10 +31,11 @@ import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule import im.vector.matrix.android.internal.crypto.tasks.* import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.CryptoDatabase +import im.vector.matrix.android.internal.di.UserCacheDirectory +import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.cache.ClearCacheTask import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask -import im.vector.matrix.android.internal.util.md5 import io.realm.RealmConfiguration import retrofit2.Retrofit import java.io.File @@ -50,13 +50,13 @@ internal abstract class CryptoModule { @Provides @CryptoDatabase @SessionScope - fun providesRealmConfiguration(context: Context, credentials: Credentials, realmKeysUtils: RealmKeysUtils): RealmConfiguration { - val userIDHash = credentials.userId.md5() - + fun providesRealmConfiguration(@UserCacheDirectory directory: File, + @UserMd5 userMd5: String, + realmKeysUtils: RealmKeysUtils): RealmConfiguration { return RealmConfiguration.Builder() - .directory(File(context.filesDir, userIDHash)) + .directory(directory) .apply { - realmKeysUtils.configureEncryption(this, "crypto_module_$userIDHash") + realmKeysUtils.configureEncryption(this, "crypto_module_$userMd5") } .name("crypto_store.realm") .modules(RealmCryptoStoreModule()) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index a3786d481d..1a94bebde4 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -62,11 +62,9 @@ import im.vector.matrix.android.internal.crypto.tasks.* import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.di.CryptoDatabase import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.session.SessionScope -import im.vector.matrix.android.internal.session.cache.ClearCacheTask import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.sync.model.SyncResponse @@ -135,7 +133,6 @@ internal class DefaultCryptoService @Inject constructor( private val setDeviceNameTask: SetDeviceNameTask, private val uploadKeysTask: UploadKeysTask, private val loadRoomMembersTask: LoadRoomMembersTask, - @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask, private val monarchy: Monarchy, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val taskExecutor: TaskExecutor @@ -1047,14 +1044,6 @@ internal class DefaultCryptoService @Inject constructor( } } - override fun clearCryptoCache(callback: MatrixCallback) { - clearCryptoDataTask - .configureWith { - this.callback = callback - } - .executeBy(taskExecutor) - } - override fun addNewSessionListener(newSessionListener: NewSessionListener) { roomDecryptorProvider.addNewSessionListener(newSessionListener) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt new file mode 100644 index 0000000000..ad4991d5ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class UserCacheDirectory diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt new file mode 100644 index 0000000000..8ca81a1dab --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class UserMd5 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index a18c339b47..8ee73084e7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -39,12 +39,10 @@ import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.user.UserService -import im.vector.matrix.android.api.util.MatrixCallbackDelegate import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.session.sync.job.SyncThread import im.vector.matrix.android.internal.session.sync.job.SyncWorker -import im.vector.matrix.android.internal.worker.WorkManagerUtil import timber.log.Timber import javax.inject.Inject import javax.inject.Provider @@ -75,7 +73,6 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se GroupService by groupService.get(), UserService by userService.get(), CryptoService by cryptoService.get(), - CacheService by cacheService.get(), SignOutService by signOutService.get(), FilterService by filterService.get(), PushRuleService by pushRuleService.get(), @@ -144,43 +141,6 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se } } - @MainThread - override fun signOut(callback: MatrixCallback) { - Timber.w("SIGN_OUT: start") - - assert(isOpen) - - Timber.w("SIGN_OUT: call webservice") - return signOutService.get().signOut(object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.w("SIGN_OUT: call webservice -> SUCCESS: clear cache") - stopSync() - stopAnyBackgroundSync() - // Clear the cache - cacheService.get().clearCache(object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.w("SIGN_OUT: clear cache -> SUCCESS: clear crypto cache") - cryptoService.get().clearCryptoCache(MatrixCallbackDelegate(callback)) - WorkManagerUtil.cancelAllWorks(context) - callback.onSuccess(Unit) - } - - override fun onFailure(failure: Throwable) { - // ignore error - Timber.e("SIGN_OUT: clear cache -> ERROR: ignoring") - onSuccess(Unit) - } - }) - } - - override fun onFailure(failure: Throwable) { - // Ignore failure - Timber.e("SIGN_OUT: call webservice -> ERROR: ignoring") - onSuccess(Unit) - } - }) - } - override fun clearCache(callback: MatrixCallback) { stopSync() stopAnyBackgroundSync() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 9aa0365e28..0a304ef1f0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -30,9 +30,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.database.model.SessionRealmModule -import im.vector.matrix.android.internal.di.Authenticated -import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.di.* import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor @@ -66,19 +64,32 @@ internal abstract class SessionModule { return sessionParams.credentials } + @JvmStatic + @UserMd5 + @Provides + fun providesUserMd5(sessionParams: SessionParams): String { + return sessionParams.credentials.userId.md5() + } + + @JvmStatic + @Provides + @UserCacheDirectory + fun providesFilesDir(@UserMd5 userMd5: String, context: Context): File { + return File(context.filesDir, userMd5) + } + @JvmStatic @Provides @SessionDatabase @SessionScope - fun providesRealmConfiguration(sessionParams: SessionParams, realmKeysUtils: RealmKeysUtils, context: Context): RealmConfiguration { - val childPath = sessionParams.credentials.userId.md5() - val directory = File(context.filesDir, childPath) - + fun providesRealmConfiguration(realmKeysUtils: RealmKeysUtils, + @UserCacheDirectory directory: File, + @UserMd5 userMd5: String): RealmConfiguration { return RealmConfiguration.Builder() .directory(directory) .name("disk_store.realm") .apply { - realmKeysUtils.configureEncryption(this, "session_db_$childPath") + realmKeysUtils.configureEncryption(this, "session_db_$userMd5") } .modules(SessionRealmModule()) .deleteRealmIfMigrationNeeded() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt index 6f4441b189..f6837786cb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt @@ -16,25 +16,54 @@ package im.vector.matrix.android.internal.session.signout +import android.content.Context import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.di.CryptoDatabase +import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.di.UserCacheDirectory import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.cache.ClearCacheTask import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.worker.WorkManagerUtil +import timber.log.Timber +import java.io.File import javax.inject.Inject internal interface SignOutTask : Task -internal class DefaultSignOutTask @Inject constructor(private val credentials: Credentials, +internal class DefaultSignOutTask @Inject constructor(private val context: Context, + private val credentials: Credentials, private val signOutAPI: SignOutAPI, private val sessionManager: SessionManager, - private val sessionParamsStore: SessionParamsStore) : SignOutTask { + private val sessionParamsStore: SessionParamsStore, + @SessionDatabase private val clearSessionDataTask: ClearCacheTask, + @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask, + @UserCacheDirectory private val userFile: File) : SignOutTask { override suspend fun execute(params: Unit) { + Timber.d("SignOut: send request...") executeRequest { apiCall = signOutAPI.signOut() } - sessionParamsStore.delete(credentials.userId) + + Timber.d("SignOut: release session...") sessionManager.releaseSession(credentials.userId) + + Timber.d("SignOut: cancel pending works...") + WorkManagerUtil.cancelAllWorks(context) + + Timber.d("SignOut: delete session params...") + sessionParamsStore.delete(credentials.userId) + + Timber.d("SignOut: clear session data...") + clearSessionDataTask.execute(Unit) + + Timber.d("SignOut: clear crypto data...") + clearCryptoDataTask.execute(Unit) + + Timber.d("SignOut: clear file system") + userFile.deleteRecursively() } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/WorkManagerUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/WorkManagerUtil.kt index ea0ff28293..56516dbda0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/WorkManagerUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/WorkManagerUtil.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.worker import android.content.Context import androidx.work.* +// TODO Multiaccount internal object WorkManagerUtil { private const val MATRIX_SDK_TAG = "MatrixSDK" diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index c32c35c2f1..fcd938216d 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -17,14 +17,17 @@ package im.vector.riotx.features import android.app.Activity +import android.app.AlertDialog import android.content.Intent import android.os.Bundle import com.bumptech.glide.Glide import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.Authenticator +import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.utils.deleteAllFiles import im.vector.riotx.features.home.HomeActivity @@ -57,6 +60,7 @@ class MainActivity : VectorBaseActivity() { @Inject lateinit var matrix: Matrix @Inject lateinit var authenticator: Authenticator @Inject lateinit var sessionHolder: ActiveSessionHolder + @Inject lateinit var errorFormatter: ErrorFormatter override fun injectWith(injector: ScreenComponent) { injector.inject(this) @@ -89,17 +93,33 @@ class MainActivity : VectorBaseActivity() { sessionHolder.clearActiveSession() start() } + + override fun onFailure(failure: Throwable) { + displayErrorAndStart(failure) + } }) clearCache -> sessionHolder.getActiveSession().clearCache(object : MatrixCallback { override fun onSuccess(data: Unit) { start() } + + override fun onFailure(failure: Throwable) { + displayErrorAndStart(failure) + } }) else -> start() - } } + private fun displayErrorAndStart(failure: Throwable) { + AlertDialog.Builder(this) + .setTitle(R.string.dialog_title_error) + .setMessage(errorFormatter.toHumanReadable(failure)) + .setPositiveButton(R.string.ok) { _, _ -> start() } + .setCancelable(false) + .show() + } + private fun start() { val intent = if (sessionHolder.hasActiveSession()) { HomeActivity.newIntent(this) From 1ba8a582196c5c0f73b1d34e540471a4ae56e971 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Sep 2019 18:29:06 +0200 Subject: [PATCH 15/44] Cleanup SecretStoringUtils, and delete keys when user signs out --- .../android/api/util/SecretStoringUtils.kt | 80 ++++++++++--------- .../android/internal/auth/AuthModule.kt | 4 +- .../android/internal/crypto/CryptoModule.kt | 3 +- .../internal/database/RealmKeysUtils.kt | 12 +++ .../android/internal/session/SessionModule.kt | 3 +- .../internal/session/signout/SignOutTask.kt | 12 ++- 6 files changed, 72 insertions(+), 42 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt index f225dc8b4f..c411760f38 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt @@ -22,12 +22,14 @@ import android.security.KeyPairGeneratorSpec import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import androidx.annotation.RequiresApi +import timber.log.Timber import java.io.* import java.math.BigInteger import java.security.KeyPairGenerator import java.security.KeyStore +import java.security.KeyStoreException import java.security.SecureRandom -import java.util.Calendar +import java.util.* import javax.crypto.* import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.IvParameterSpec @@ -65,7 +67,7 @@ import javax.security.auth.x500.X500Principal * val kDecripted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context) * * - * You can also just use this utility to store a secret key, and use any encryption algorthim that you want. + * You can also just use this utility to store a secret key, and use any encryption algorithm that you want. * * Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you * add a pin or change the schema); So you might and with a useless pile of bytes. @@ -76,11 +78,11 @@ object SecretStoringUtils { private const val AES_MODE = "AES/GCM/NoPadding"; private const val RSA_MODE = "RSA/ECB/PKCS1Padding" - const val FORMAT_API_M: Byte = 0 - const val FORMAT_1: Byte = 1 - const val FORMAT_2: Byte = 2 + private const val FORMAT_API_M: Byte = 0 + private const val FORMAT_1: Byte = 1 + private const val FORMAT_2: Byte = 2 - val keyStore: KeyStore by lazy { + private val keyStore: KeyStore by lazy { KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) } @@ -88,11 +90,19 @@ object SecretStoringUtils { private val secureRandom = SecureRandom() + fun safeDeleteKey(keyAlias: String) { + try { + keyStore.deleteEntry(keyAlias) + } catch (e: KeyStoreException) { + Timber.e(e) + } + } + /** * Encrypt the given secret using the android Keystore. - * On android >= M, will directly use the keystore to generate a symetric key - * On KitKat >= KitKat and = M, will directly use the keystore to generate a symmetric key + * On android >= KitKat and = Build.VERSION_CODES.M) { return encryptStringM(secret, keyAlias) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return encryptStringJ(secret, keyAlias, context) + return encryptStringK(secret, keyAlias, context) } else { return encryptForOldDevicesNotGood(secret, keyAlias) } @@ -117,7 +127,7 @@ object SecretStoringUtils { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return decryptStringM(encrypted, keyAlias) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return decryptStringJ(encrypted, keyAlias, context) + return decryptStringK(encrypted, keyAlias, context) } else { return decryptForOldDevicesNotGood(encrypted, keyAlias) } @@ -145,7 +155,7 @@ object SecretStoringUtils { @RequiresApi(Build.VERSION_CODES.M) - fun getOrGenerateSymmetricKeyForAlias(alias: String): SecretKey { + private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey { val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) ?.secretKey if (secretKeyEntry == null) { @@ -163,7 +173,6 @@ object SecretStoringUtils { return secretKeyEntry } - /* Symetric Key Generation is only available in M, so before M the idea is to: - Generate a pair of RSA keys; @@ -172,7 +181,7 @@ object SecretStoringUtils { - Store the encrypted AES Generate a key pair for encryption */ - @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + @RequiresApi(Build.VERSION_CODES.KITKAT) fun getOrGenerateKeyPairForAlias(alias: String, context: Context): KeyStore.PrivateKeyEntry { val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry) @@ -201,7 +210,7 @@ object SecretStoringUtils { @RequiresApi(Build.VERSION_CODES.M) fun encryptStringM(text: String, keyAlias: String): ByteArray? { - val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias) + val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) val cipher = Cipher.getInstance(AES_MODE) cipher.init(Cipher.ENCRYPT_MODE, secretKey) @@ -212,10 +221,10 @@ object SecretStoringUtils { } @RequiresApi(Build.VERSION_CODES.M) - fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String { + private fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String { val (iv, encryptedText) = formatMExtract(ByteArrayInputStream(encryptedChunk)) - val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias) + val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) val cipher = Cipher.getInstance(AES_MODE) val spec = GCMParameterSpec(128, iv) @@ -224,8 +233,8 @@ object SecretStoringUtils { return String(cipher.doFinal(encryptedText), Charsets.UTF_8) } - @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) - fun encryptStringJ(text: String, keyAlias: String, context: Context): ByteArray? { + @RequiresApi(Build.VERSION_CODES.KITKAT) + private fun encryptStringK(text: String, keyAlias: String, context: Context): ByteArray? { //we generate a random symetric key val key = ByteArray(16) secureRandom.nextBytes(key) @@ -242,7 +251,7 @@ object SecretStoringUtils { return format1Make(encryptedKey, iv, encryptedBytes) } - fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray { + private fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray { val salt = ByteArray(8) secureRandom.nextBytes(salt) val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") @@ -258,11 +267,11 @@ object SecretStoringUtils { return format2Make(salt, iv, encryptedBytes) } - fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? { + private fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? { val (salt, iv, encrypted) = format2Extract(ByteArrayInputStream(data)) val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") - val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128) + val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10_000, 128) val tmp = factory.generateSecret(spec) val sKey = SecretKeySpec(tmp.encoded, "AES") @@ -277,7 +286,7 @@ object SecretStoringUtils { } @RequiresApi(Build.VERSION_CODES.KITKAT) - fun decryptStringJ(data: ByteArray, keyAlias: String, context: Context): String? { + private fun decryptStringK(data: ByteArray, keyAlias: String, context: Context): String? { val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data)) @@ -288,14 +297,12 @@ object SecretStoringUtils { cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec) return String(cipher.doFinal(encrypted), Charsets.UTF_8) - } - @RequiresApi(Build.VERSION_CODES.M) @Throws(IOException::class) - fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) { - val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias) + private fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) { + val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) val cipher = Cipher.getInstance(AES_MODE) cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/) @@ -314,7 +321,7 @@ object SecretStoringUtils { } @RequiresApi(Build.VERSION_CODES.KITKAT) - fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any, context: Context) { + private fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any, context: Context) { //we generate a random symetric key val key = ByteArray(16) secureRandom.nextBytes(key) @@ -342,7 +349,7 @@ object SecretStoringUtils { output.write(bos1.toByteArray()) } - fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) { + private fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) { val salt = ByteArray(8) secureRandom.nextBytes(salt) val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") @@ -387,8 +394,8 @@ object SecretStoringUtils { @RequiresApi(Build.VERSION_CODES.M) @Throws(IOException::class) - fun loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? { - val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias) + private fun loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? { + val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) val format = inputStream.read() assert(format.toByte() == FORMAT_API_M) @@ -411,7 +418,7 @@ object SecretStoringUtils { @RequiresApi(Build.VERSION_CODES.KITKAT) @Throws(IOException::class) - fun loadSecureObjectK(keyAlias: String, inputStream: InputStream, context: Context): T? { + private fun loadSecureObjectK(keyAlias: String, inputStream: InputStream, context: Context): T? { val (encryptedKey, iv, encrypted) = format1Extract(inputStream) @@ -432,8 +439,7 @@ object SecretStoringUtils { } @Throws(Exception::class) - fun loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? { - + private fun loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? { val (salt, iv, encrypted) = format2Extract(inputStream) val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") @@ -456,7 +462,7 @@ object SecretStoringUtils { } - @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + @RequiresApi(Build.VERSION_CODES.KITKAT) @Throws(Exception::class) private fun rsaEncrypt(alias: String, secret: ByteArray, context: Context): ByteArray { val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context) @@ -472,7 +478,7 @@ object SecretStoringUtils { return outputStream.toByteArray() } - @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + @RequiresApi(Build.VERSION_CODES.KITKAT) @Throws(Exception::class) private fun rsaDecrypt(alias: String, encrypted: InputStream, context: Context): ByteArray { val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context) @@ -504,7 +510,6 @@ object SecretStoringUtils { } private fun format1Extract(bis: InputStream): Triple { - val format = bis.read() assert(format.toByte() == FORMAT_1) @@ -548,7 +553,6 @@ object SecretStoringUtils { } private fun format2Extract(bis: InputStream): Triple { - val format = bis.read() assert(format.toByte() == FORMAT_2) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt index 51367ab3f3..399605469d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt @@ -33,6 +33,8 @@ internal abstract class AuthModule { @Module companion object { + private const val DB_ALIAS = "matrix-sdk-auth" + @JvmStatic @Provides @AuthDatabase @@ -44,7 +46,7 @@ internal abstract class AuthModule { return RealmConfiguration.Builder() .apply { - realmKeysUtils.configureEncryption(this, "matrix-sdk-auth") + realmKeysUtils.configureEncryption(this, DB_ALIAS) } .name("matrix-sdk-auth.realm") .modules(AuthRealmModule()) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index f5690123a4..742e3ff21a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -45,6 +45,7 @@ internal abstract class CryptoModule { @Module companion object { + internal const val DB_ALIAS_PREFIX = "crypto_module_" @JvmStatic @Provides @@ -56,7 +57,7 @@ internal abstract class CryptoModule { return RealmConfiguration.Builder() .directory(directory) .apply { - realmKeysUtils.configureEncryption(this, "crypto_module_$userMd5") + realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5") } .name("crypto_store.realm") .modules(RealmCryptoStoreModule()) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt index 03ff641cf7..60d5e8a3ae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt @@ -101,6 +101,18 @@ internal class RealmKeysUtils @Inject constructor(private val context: Context) realmConfigurationBuilder.encryptionKey(key) } + // Delete elements related to the alias + fun clear(alias: String) { + if (hasKeyForDatabase(alias)) { + SecretStoringUtils.safeDeleteKey(alias) + + sharedPreferences + .edit() + .remove("${ENCRYPTED_KEY_PREFIX}_$alias") + .apply() + } + } + companion object { private const val ENCRYPTED_KEY_PREFIX = "REALM_ENCRYPTED_KEY" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 0a304ef1f0..f8f94f0321 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -50,6 +50,7 @@ internal abstract class SessionModule { @Module companion object { + internal const val DB_ALIAS_PREFIX = "session_db_" @JvmStatic @Provides @@ -89,7 +90,7 @@ internal abstract class SessionModule { .directory(directory) .name("disk_store.realm") .apply { - realmKeysUtils.configureEncryption(this, "session_db_$userMd5") + realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5") } .modules(SessionRealmModule()) .deleteRealmIfMigrationNeeded() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt index f6837786cb..18c2de71d3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt @@ -20,10 +20,14 @@ import android.content.Context import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.crypto.CryptoModule +import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.CryptoDatabase import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.UserCacheDirectory +import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.SessionModule import im.vector.matrix.android.internal.session.cache.ClearCacheTask import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.worker.WorkManagerUtil @@ -40,7 +44,9 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte private val sessionParamsStore: SessionParamsStore, @SessionDatabase private val clearSessionDataTask: ClearCacheTask, @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask, - @UserCacheDirectory private val userFile: File) : SignOutTask { + @UserCacheDirectory private val userFile: File, + private val realmKeysUtils: RealmKeysUtils, + @UserMd5 private val userMd5: String) : SignOutTask { override suspend fun execute(params: Unit) { Timber.d("SignOut: send request...") @@ -65,5 +71,9 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte Timber.d("SignOut: clear file system") userFile.deleteRecursively() + + Timber.d("SignOut: clear the database keys") + realmKeysUtils.clear(SessionModule.DB_ALIAS_PREFIX + userMd5) + realmKeysUtils.clear(CryptoModule.DB_ALIAS_PREFIX + userMd5) } } \ No newline at end of file From 384dd100e9b49d24fea6647ea864fba9de7c3b4c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Sep 2019 19:19:14 +0200 Subject: [PATCH 16/44] Daggerization and Kotlinification of SecretStoringUtils --- .../matrix/android/api/session/Session.kt | 6 +- .../securestorage/SecureStorageService.kt | 28 ++++++ .../android/api/util/SecretStoringUtils.kt | 91 +++++++++---------- .../internal/database/RealmKeysUtils.kt | 9 +- .../internal/session/DefaultSession.kt | 5 +- .../android/internal/session/SessionModule.kt | 5 + .../DefaultSecureStorageService.kt | 36 ++++++++ .../NotificationDrawerManager.kt | 7 +- 8 files changed, 129 insertions(+), 58 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SecureStorageService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 2dde175bed..53dc8e77a0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService +import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.SyncState @@ -50,7 +51,8 @@ interface Session : FileService, PushRuleService, PushersService, - InitialSyncProgressService { + InitialSyncProgressService, + SecureStorageService { /** * The params associated to the session @@ -87,7 +89,7 @@ interface Session : /** * This method start the sync thread. */ - fun startSync(fromForeground : Boolean) + fun startSync(fromForeground: Boolean) /** * This method stop the sync thread. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SecureStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SecureStorageService.kt new file mode 100644 index 0000000000..d56b6150ee --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SecureStorageService.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.securestorage + +import java.io.InputStream +import java.io.OutputStream + +interface SecureStorageService { + + fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream) + + fun loadSecureSecret(inputStream: InputStream, keyAlias: String): T? + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt index c411760f38..5d45160b03 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt @@ -35,6 +35,7 @@ import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.SecretKeySpec +import javax.inject.Inject import javax.security.auth.x500.X500Principal @@ -72,15 +73,17 @@ import javax.security.auth.x500.X500Principal * Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you * add a pin or change the schema); So you might and with a useless pile of bytes. */ -object SecretStoringUtils { +internal class SecretStoringUtils @Inject constructor(private val context: Context) { - private const val ANDROID_KEY_STORE = "AndroidKeyStore" - private const val AES_MODE = "AES/GCM/NoPadding"; - private const val RSA_MODE = "RSA/ECB/PKCS1Padding" + companion object { + private const val ANDROID_KEY_STORE = "AndroidKeyStore" + private const val AES_MODE = "AES/GCM/NoPadding" + private const val RSA_MODE = "RSA/ECB/PKCS1Padding" - private const val FORMAT_API_M: Byte = 0 - private const val FORMAT_1: Byte = 1 - private const val FORMAT_2: Byte = 2 + private const val FORMAT_API_M: Byte = 0 + private const val FORMAT_1: Byte = 1 + private const val FORMAT_2: Byte = 2 + } private val keyStore: KeyStore by lazy { KeyStore.getInstance(ANDROID_KEY_STORE).apply { @@ -109,13 +112,11 @@ object SecretStoringUtils { * The secret is encrypted using the following method: AES/GCM/NoPadding */ @Throws(Exception::class) - fun securelyStoreString(secret: String, keyAlias: String, context: Context): ByteArray? { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return encryptStringM(secret, keyAlias) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return encryptStringK(secret, keyAlias, context) - } else { - return encryptForOldDevicesNotGood(secret, keyAlias) + fun securelyStoreString(secret: String, keyAlias: String): ByteArray? { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> encryptStringK(secret, keyAlias) + else -> encryptForOldDevicesNotGood(secret, keyAlias) } } @@ -123,33 +124,27 @@ object SecretStoringUtils { * Decrypt a secret that was encrypted by #securelyStoreString() */ @Throws(Exception::class) - fun loadSecureSecret(encrypted: ByteArray, keyAlias: String, context: Context): String? { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return decryptStringM(encrypted, keyAlias) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return decryptStringK(encrypted, keyAlias, context) - } else { - return decryptForOldDevicesNotGood(encrypted, keyAlias) + fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String? { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> decryptStringM(encrypted, keyAlias) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> decryptStringK(encrypted, keyAlias) + else -> decryptForOldDevicesNotGood(encrypted, keyAlias) } } - fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream, context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - saveSecureObjectM(keyAlias, output, any) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return saveSecureObjectK(keyAlias, output, any, context) - } else { - return saveSecureObjectOldNotGood(keyAlias, output, any) + fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) { + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> saveSecureObjectK(keyAlias, output, any) + else -> saveSecureObjectOldNotGood(keyAlias, output, any) } } - fun loadSecureSecret(inputStream: InputStream, keyAlias: String, context: Context): T? { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return loadSecureObjectM(keyAlias, inputStream) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return loadSecureObjectK(keyAlias, inputStream, context) - } else { - return loadSecureObjectOldNotGood(keyAlias, inputStream) + fun loadSecureSecret(inputStream: InputStream, keyAlias: String): T? { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> loadSecureObjectM(keyAlias, inputStream) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> loadSecureObjectK(keyAlias, inputStream) + else -> loadSecureObjectOldNotGood(keyAlias, inputStream) } } @@ -182,7 +177,7 @@ object SecretStoringUtils { Generate a key pair for encryption */ @RequiresApi(Build.VERSION_CODES.KITKAT) - fun getOrGenerateKeyPairForAlias(alias: String, context: Context): KeyStore.PrivateKeyEntry { + fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry { val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry) if (privateKeyEntry != null) return privateKeyEntry @@ -234,14 +229,14 @@ object SecretStoringUtils { } @RequiresApi(Build.VERSION_CODES.KITKAT) - private fun encryptStringK(text: String, keyAlias: String, context: Context): ByteArray? { + private fun encryptStringK(text: String, keyAlias: String): ByteArray? { //we generate a random symetric key val key = ByteArray(16) secureRandom.nextBytes(key) val sKey = SecretKeySpec(key, "AES") //we encrypt this key thanks to the key store - val encryptedKey = rsaEncrypt(keyAlias, key, context) + val encryptedKey = rsaEncrypt(keyAlias, key) val cipher = Cipher.getInstance(AES_MODE) cipher.init(Cipher.ENCRYPT_MODE, sKey) @@ -286,12 +281,12 @@ object SecretStoringUtils { } @RequiresApi(Build.VERSION_CODES.KITKAT) - private fun decryptStringK(data: ByteArray, keyAlias: String, context: Context): String? { + private fun decryptStringK(data: ByteArray, keyAlias: String): String? { val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data)) //we need to decrypt the key - val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context) + val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey)) val cipher = Cipher.getInstance(AES_MODE) val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec) @@ -321,14 +316,14 @@ object SecretStoringUtils { } @RequiresApi(Build.VERSION_CODES.KITKAT) - private fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any, context: Context) { + private fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any) { //we generate a random symetric key val key = ByteArray(16) secureRandom.nextBytes(key) val sKey = SecretKeySpec(key, "AES") //we encrypt this key thanks to the key store - val encryptedKey = rsaEncrypt(keyAlias, key, context) + val encryptedKey = rsaEncrypt(keyAlias, key) val cipher = Cipher.getInstance(AES_MODE) cipher.init(Cipher.ENCRYPT_MODE, sKey) @@ -418,12 +413,12 @@ object SecretStoringUtils { @RequiresApi(Build.VERSION_CODES.KITKAT) @Throws(IOException::class) - private fun loadSecureObjectK(keyAlias: String, inputStream: InputStream, context: Context): T? { + private fun loadSecureObjectK(keyAlias: String, inputStream: InputStream): T? { val (encryptedKey, iv, encrypted) = format1Extract(inputStream) //we need to decrypt the key - val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context) + val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey)) val cipher = Cipher.getInstance(AES_MODE) val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec) @@ -464,8 +459,8 @@ object SecretStoringUtils { @RequiresApi(Build.VERSION_CODES.KITKAT) @Throws(Exception::class) - private fun rsaEncrypt(alias: String, secret: ByteArray, context: Context): ByteArray { - val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context) + private fun rsaEncrypt(alias: String, secret: ByteArray): ByteArray { + val privateKeyEntry = getOrGenerateKeyPairForAlias(alias) // Encrypt the text val inputCipher = Cipher.getInstance(RSA_MODE) inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey) @@ -480,8 +475,8 @@ object SecretStoringUtils { @RequiresApi(Build.VERSION_CODES.KITKAT) @Throws(Exception::class) - private fun rsaDecrypt(alias: String, encrypted: InputStream, context: Context): ByteArray { - val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context) + private fun rsaDecrypt(alias: String, encrypted: InputStream): ByteArray { + val privateKeyEntry = getOrGenerateKeyPairForAlias(alias) val output = Cipher.getInstance(RSA_MODE) output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt index 60d5e8a3ae..c7b4e67883 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt @@ -36,7 +36,8 @@ import javax.inject.Inject * then we generate a random secret key. The database key is encrypted with the secret key; The secret * key is encrypted with the public RSA key and stored with the encrypted key in the shared pref */ -internal class RealmKeysUtils @Inject constructor(private val context: Context) { +internal class RealmKeysUtils @Inject constructor(context: Context, + private val secretStoringUtils: SecretStoringUtils) { private val rng = SecureRandom() @@ -65,7 +66,7 @@ internal class RealmKeysUtils @Inject constructor(private val context: Context) private fun createAndSaveKeyForDatabase(alias: String): ByteArray { val key = generateKeyForRealm() val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING) - val toStore = SecretStoringUtils.securelyStoreString(encodedKey, alias, context) + val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias) sharedPreferences .edit() .putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING)) @@ -80,7 +81,7 @@ internal class RealmKeysUtils @Inject constructor(private val context: Context) private fun extractKeyForDatabase(alias: String): ByteArray { val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null) val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING) - val b64 = SecretStoringUtils.loadSecureSecret(encryptedKey, alias, context) + val b64 = secretStoringUtils.loadSecureSecret(encryptedKey, alias) return Base64.decode(b64!!, Base64.NO_PADDING) } @@ -104,7 +105,7 @@ internal class RealmKeysUtils @Inject constructor(private val context: Context) // Delete elements related to the alias fun clear(alias: String) { if (hasKeyForDatabase(alias)) { - SecretStoringUtils.safeDeleteKey(alias) + secretStoringUtils.safeDeleteKey(alias) sharedPreferences .edit() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 8ee73084e7..02addaceab 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -35,6 +35,7 @@ import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService +import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.SyncState @@ -63,6 +64,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se private val pushersService: Lazy, private val cryptoService: Lazy, private val fileService: Lazy, + private val secureStorageService: Lazy, private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, private val contentUploadProgressTracker: ContentUploadStateTracker, @@ -78,7 +80,8 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se PushRuleService by pushRuleService.get(), PushersService by pushersService.get(), FileService by fileService.get(), - InitialSyncProgressService by initialSyncProgressService.get() { + InitialSyncProgressService by initialSyncProgressService.get(), + SecureStorageService by secureStorageService.get() { private var isOpen = false diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index f8f94f0321..180cdb6ea2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -27,6 +27,7 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.database.model.SessionRealmModule @@ -39,6 +40,7 @@ import im.vector.matrix.android.internal.session.room.EventRelationsAggregationU import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver import im.vector.matrix.android.internal.session.room.prune.EventsPruner import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver +import im.vector.matrix.android.internal.session.securestorage.DefaultSecureStorageService import im.vector.matrix.android.internal.util.md5 import io.realm.RealmConfiguration import okhttp3.OkHttpClient @@ -166,4 +168,7 @@ internal abstract class SessionModule { @Binds abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService + @Binds + abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt new file mode 100644 index 0000000000..bd052f962c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.securestorage + +import im.vector.matrix.android.api.session.securestorage.SecureStorageService +import im.vector.matrix.android.api.util.SecretStoringUtils +import im.vector.matrix.android.internal.di.UserMd5 +import java.io.InputStream +import java.io.OutputStream +import javax.inject.Inject + +internal class DefaultSecureStorageService @Inject constructor(@UserMd5 private val userMd5: String, + private val secretStoringUtils: SecretStoringUtils) : SecureStorageService { + + override fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream) { + secretStoringUtils.securelyStoreObject(any, "${userMd5}_$keyAlias", outputStream) + } + + override fun loadSecureSecret(inputStream: InputStream, keyAlias: String): T? { + return secretStoringUtils.loadSecureSecret(inputStream, "${userMd5}_$keyAlias") + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index 6695c10955..01fa3210bd 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -24,7 +24,6 @@ import androidx.annotation.WorkerThread import androidx.core.app.NotificationCompat import androidx.core.app.Person import im.vector.matrix.android.api.session.content.ContentUrlResolver -import im.vector.matrix.android.api.util.SecretStoringUtils import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder @@ -448,7 +447,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) if (!file.exists()) file.createNewFile() FileOutputStream(file).use { - SecretStoringUtils.securelyStoreObject(eventList, "notificationMgr", it, this.context) + activeSessionHolder.getSafeActiveSession()?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it) } } catch (e: Throwable) { Timber.e(e, "## Failed to save cached notification info") @@ -461,7 +460,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) if (file.exists()) { FileInputStream(file).use { - val events: ArrayList? = SecretStoringUtils.loadSecureSecret(it, "notificationMgr", this.context) + val events: ArrayList? = activeSessionHolder.getSafeActiveSession()?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE) if (events != null) { return ArrayList(events.mapNotNull { it as? NotifiableEvent }) } @@ -486,5 +485,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context private const val ROOM_EVENT_NOTIFICATION_ID = 2 private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache" + + private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr" } } From 3485f023b098a1c87ce08ee995f1b864157f0880 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Sep 2019 19:24:52 +0200 Subject: [PATCH 17/44] All current notifications were dismissed by mistake when the app is launched from the launcher --- CHANGES.md | 1 + .../src/main/java/im/vector/riotx/features/home/HomeActivity.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a7772bc9f2..477746fc7e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ Bugfix: - Fix crash due to missing informationData (#535) - Progress in initial sync dialog is decreasing for a step and should not (#532) - Fix rendering issue of accepted third party invitation event + - All current notifications were dismissed by mistake when the app is launched from the launcher Translations: - diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 7ef015a9b9..2a43ca705a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -111,7 +111,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { } } - if (intent.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)) { + if (intent.getBooleanExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, false)) { notificationDrawerManager.clearAllEvents() intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) } From fdaaca49c2e405445342d203b932d34bd9990470 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Sep 2019 19:27:13 +0200 Subject: [PATCH 18/44] Code quality (bad import) --- vector/src/main/java/im/vector/riotx/features/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index fcd938216d..5189e0959e 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -17,9 +17,9 @@ package im.vector.riotx.features import android.app.Activity -import android.app.AlertDialog import android.content.Intent import android.os.Bundle +import androidx.appcompat.app.AlertDialog import com.bumptech.glide.Glide import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.MatrixCallback From ced72aff4f2a37c82af41a9d6a22a9df1fd67213 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Sep 2019 10:32:09 +0200 Subject: [PATCH 19/44] Revert change done to save alias for the client --- .../session/securestorage/DefaultSecureStorageService.kt | 8 +++----- .../features/notifications/NotificationDrawerManager.kt | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt index bd052f962c..7640acfa49 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt @@ -18,19 +18,17 @@ package im.vector.matrix.android.internal.session.securestorage import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.api.util.SecretStoringUtils -import im.vector.matrix.android.internal.di.UserMd5 import java.io.InputStream import java.io.OutputStream import javax.inject.Inject -internal class DefaultSecureStorageService @Inject constructor(@UserMd5 private val userMd5: String, - private val secretStoringUtils: SecretStoringUtils) : SecureStorageService { +internal class DefaultSecureStorageService @Inject constructor(private val secretStoringUtils: SecretStoringUtils) : SecureStorageService { override fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream) { - secretStoringUtils.securelyStoreObject(any, "${userMd5}_$keyAlias", outputStream) + secretStoringUtils.securelyStoreObject(any, keyAlias, outputStream) } override fun loadSecureSecret(inputStream: InputStream, keyAlias: String): T? { - return secretStoringUtils.loadSecureSecret(inputStream, "${userMd5}_$keyAlias") + return secretStoringUtils.loadSecureSecret(inputStream, keyAlias) } } diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index 01fa3210bd..551bbb808b 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -484,6 +484,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context private const val ROOM_MESSAGES_NOTIFICATION_ID = 1 private const val ROOM_EVENT_NOTIFICATION_ID = 2 + // TODO Mutliaccount private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache" private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr" From 20f53e9a586aaecb398863ad14867283ff63e96b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Sep 2019 10:33:27 +0200 Subject: [PATCH 20/44] Signout: propose the user to retry in case of error --- .../im/vector/riotx/features/MainActivity.kt | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index 5189e0959e..868a094c0a 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -73,49 +73,59 @@ class MainActivity : VectorBaseActivity() { // Handle some wanted cleanup if (clearCache || clearCredentials) { - GlobalScope.launch(Dispatchers.Main) { - // On UI Thread - Glide.get(this@MainActivity).clearMemory() - withContext(Dispatchers.IO) { - // On BG thread - Glide.get(this@MainActivity).clearDiskCache() - - // Also clear cache (Logs, etc...) - deleteAllFiles(this@MainActivity.cacheDir) - } - } + doCleanUp(clearCache, clearCredentials) + } else { + start() } + } + private fun doCleanUp(clearCache: Boolean, clearCredentials: Boolean) { when { clearCredentials -> sessionHolder.getActiveSession().signOut(object : MatrixCallback { override fun onSuccess(data: Unit) { Timber.w("SIGN_OUT: success, start app") sessionHolder.clearActiveSession() - start() + doLocalCleanupAndStart() } override fun onFailure(failure: Throwable) { - displayErrorAndStart(failure) + displayError(failure, clearCache, clearCredentials) } }) clearCache -> sessionHolder.getActiveSession().clearCache(object : MatrixCallback { override fun onSuccess(data: Unit) { - start() + doLocalCleanupAndStart() } override fun onFailure(failure: Throwable) { - displayErrorAndStart(failure) + displayError(failure, clearCache, clearCredentials) } }) - else -> start() } } - private fun displayErrorAndStart(failure: Throwable) { + private fun doLocalCleanupAndStart() { + GlobalScope.launch(Dispatchers.Main) { + // On UI Thread + Glide.get(this@MainActivity).clearMemory() + withContext(Dispatchers.IO) { + // On BG thread + Glide.get(this@MainActivity).clearDiskCache() + + // Also clear cache (Logs, etc...) + deleteAllFiles(this@MainActivity.cacheDir) + } + } + + start() + } + + private fun displayError(failure: Throwable, clearCache: Boolean, clearCredentials: Boolean) { AlertDialog.Builder(this) .setTitle(R.string.dialog_title_error) .setMessage(errorFormatter.toHumanReadable(failure)) - .setPositiveButton(R.string.ok) { _, _ -> start() } + .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp(clearCache, clearCredentials) } + .setNegativeButton(R.string.cancel) { _, _ -> start() } .setCancelable(false) .show() } From 07e99901e1ec18b44380c236672c933b1037ecbc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Sep 2019 10:38:37 +0200 Subject: [PATCH 21/44] SecretStoringUtils -> move to internal package --- .../vector/matrix/android/internal/database/RealmKeysUtils.kt | 2 +- .../session/securestorage/DefaultSecureStorageService.kt | 1 - .../session/securestorage}/SecretStoringUtils.kt | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/{api/util => internal/session/securestorage}/SecretStoringUtils.kt (99%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt index c7b4e67883..d2ab764087 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.database import android.content.Context import android.util.Base64 import im.vector.matrix.android.BuildConfig -import im.vector.matrix.android.api.util.SecretStoringUtils +import im.vector.matrix.android.internal.session.securestorage.SecretStoringUtils import io.realm.RealmConfiguration import timber.log.Timber import java.security.SecureRandom diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt index 7640acfa49..2a3c88e84e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.session.securestorage import im.vector.matrix.android.api.session.securestorage.SecureStorageService -import im.vector.matrix.android.api.util.SecretStoringUtils import java.io.InputStream import java.io.OutputStream import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt similarity index 99% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt index 5d45160b03..6192aaf148 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.util +package im.vector.matrix.android.internal.session.securestorage import android.content.Context import android.os.Build From 695d8cce00a6e06f5ca5fd2add6a95bdf311112a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Sep 2019 10:59:58 +0200 Subject: [PATCH 22/44] Display a no network indicator when there is no network (#559) --- CHANGES.md | 1 + .../android/api/session/sync/SyncState.kt | 1 + .../internal/session/sync/job/SyncThread.kt | 2 ++ .../src/main/res/values/strings_RiotX.xml | 3 +++ .../riotx/features/home/HomeDetailFragment.kt | 6 +++-- .../home/room/detail/RoomDetailFragment.kt | 8 ++++--- .../main/res/layout/fragment_home_detail.xml | 22 ++++++++++++----- .../main/res/layout/fragment_room_detail.xml | 24 ++++++++++++++----- 8 files changed, 50 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8769b11ea7..37fa25cba4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Features: Improvements: - Reduce default release build log level, and lab option to enable more logs. + - Display a no network indicator when there is no network (#559) Other changes: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/sync/SyncState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/sync/SyncState.kt index cc1c3f1a32..8c34e392b5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/sync/SyncState.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/sync/SyncState.kt @@ -22,4 +22,5 @@ sealed class SyncState { object PAUSED : SyncState() object KILLING : SyncState() object KILLED : SyncState() + object NO_NETWORK : SyncState() } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt index f6ff11c1fe..7433a70a50 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt @@ -97,6 +97,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) { Timber.v("No network or sync is Paused. Waiting...") + updateStateTo(SyncState.NO_NETWORK) + synchronized(lock) { lock.wait() } diff --git a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml index 134b699a7a..a588bb36fd 100644 --- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml +++ b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml @@ -3,4 +3,7 @@ + + + There is no network connection right now \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index bd4b2ca4df..15a2cf3afe 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -21,6 +21,7 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import androidx.core.view.forEachIndexed +import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import com.airbnb.mvrx.args @@ -208,11 +209,12 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup)) unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople)) unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms)) - syncProgressBar.visibility = when (it.syncState) { + syncProgressBarWrap.visibility = when (it.syncState) { is SyncState.RUNNING -> if (it.syncState.afterPause) View.VISIBLE else View.GONE else -> View.GONE } - syncProgressBarWrap.visibility = syncProgressBar.visibility + // TODO Create a View + noNetworkBanner.isVisible = it.syncState is SyncState.NO_NETWORK } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 19262fad49..77e47cc468 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -39,6 +39,7 @@ import androidx.core.content.ContextCompat import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach +import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -77,8 +78,8 @@ import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp -import im.vector.riotx.core.ui.views.NotificationAreaView import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.ui.views.NotificationAreaView import im.vector.riotx.core.utils.* import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy @@ -251,11 +252,12 @@ class RoomDetailFragment : } roomDetailViewModel.selectSubscribe(RoomDetailViewState::syncState) { syncState -> - syncProgressBar.visibility = when (syncState) { + syncProgressBarWrap.visibility = when (syncState) { is SyncState.RUNNING -> if (syncState.afterPause) View.VISIBLE else View.GONE else -> View.GONE } - syncProgressBarWrap.visibility = syncProgressBar.visibility + // TODO Create a View + noNetworkBanner.isVisible = syncState is SyncState.NO_NETWORK } } diff --git a/vector/src/main/res/layout/fragment_home_detail.xml b/vector/src/main/res/layout/fragment_home_detail.xml index 13d3fea803..7541829cca 100644 --- a/vector/src/main/res/layout/fragment_home_detail.xml +++ b/vector/src/main/res/layout/fragment_home_detail.xml @@ -44,7 +44,7 @@ - + + android:indeterminate="true" /> + + app:layout_constraintTop_toBottomOf="@id/noNetworkBanner" /> - + + android:indeterminate="true" /> + + + Date: Tue, 17 Sep 2019 11:13:00 +0200 Subject: [PATCH 23/44] Display a no network indicator when there is no network: Create a dedicated View --- .../riotx/features/home/HomeDetailFragment.kt | 10 +---- .../home/room/detail/RoomDetailFragment.kt | 9 +--- .../features/sync/widget/SyncStateView.kt | 43 +++++++++++++++++++ .../main/res/layout/fragment_home_detail.xml | 34 ++------------- .../main/res/layout/fragment_room_detail.xml | 35 ++------------- .../src/main/res/layout/view_sync_state.xml | 37 ++++++++++++++++ 6 files changed, 90 insertions(+), 78 deletions(-) create mode 100755 vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt create mode 100644 vector/src/main/res/layout/view_sync_state.xml diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index 15a2cf3afe..acfac104d4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -19,9 +19,7 @@ package im.vector.riotx.features.home import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater -import android.view.View import androidx.core.view.forEachIndexed -import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import com.airbnb.mvrx.args @@ -31,7 +29,6 @@ import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationMenuView import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState -import im.vector.matrix.android.api.session.sync.SyncState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.ToolbarConfigurable @@ -209,12 +206,7 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup)) unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople)) unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms)) - syncProgressBarWrap.visibility = when (it.syncState) { - is SyncState.RUNNING -> if (it.syncState.afterPause) View.VISIBLE else View.GONE - else -> View.GONE - } - // TODO Create a View - noNetworkBanner.isVisible = it.syncState is SyncState.NO_NETWORK + syncStateView.render(it.syncState) } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 77e47cc468..cd1ccb01d2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -39,7 +39,6 @@ import androidx.core.content.ContextCompat import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach -import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -66,7 +65,6 @@ import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent -import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent @@ -252,12 +250,7 @@ class RoomDetailFragment : } roomDetailViewModel.selectSubscribe(RoomDetailViewState::syncState) { syncState -> - syncProgressBarWrap.visibility = when (syncState) { - is SyncState.RUNNING -> if (syncState.afterPause) View.VISIBLE else View.GONE - else -> View.GONE - } - // TODO Create a View - noNetworkBanner.isVisible = syncState is SyncState.NO_NETWORK + syncStateView.render(syncState) } } diff --git a/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt b/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt new file mode 100755 index 0000000000..2d474a13dd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.sync.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import androidx.core.view.isVisible +import im.vector.matrix.android.api.session.sync.SyncState +import im.vector.riotx.R +import kotlinx.android.synthetic.main.view_sync_state.view.* + +class SyncStateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) + : FrameLayout(context, attrs, defStyle) { + + init { + View.inflate(context, R.layout.view_sync_state, this) + } + + + fun render(newState: SyncState) { + syncStateProgressBar.visibility = when (newState) { + is SyncState.RUNNING -> if (newState.afterPause) View.VISIBLE else View.GONE + else -> View.GONE + } + syncStateNoNetwork.isVisible = newState is SyncState.NO_NETWORK + } +} diff --git a/vector/src/main/res/layout/fragment_home_detail.xml b/vector/src/main/res/layout/fragment_home_detail.xml index 7541829cca..8bc0603013 100644 --- a/vector/src/main/res/layout/fragment_home_detail.xml +++ b/vector/src/main/res/layout/fragment_home_detail.xml @@ -44,39 +44,13 @@ - - - - - - - + app:layout_constraintTop_toBottomOf="@id/groupToolbar" /> + app:layout_constraintTop_toBottomOf="@id/syncStateView" /> - - - - - - - - + app:layout_constraintTop_toBottomOf="@id/roomToolbar" /> + + + + + + + + + + + \ No newline at end of file From 993fa742528b60f69a667885118aaaaa6426d5c0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Sep 2019 11:24:37 +0200 Subject: [PATCH 24/44] Cleanup after BillCarsonFr's review --- .../im/vector/riotx/features/login/LoginViewModel.kt | 12 ++++++++++-- vector/src/main/res/layout/activity_progress.xml | 5 +---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 96de7cd0df..7231089379 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -34,6 +34,7 @@ import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.features.notifications.PushRuleTriggerListener +import timber.log.Timber class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, private val authenticator: Authenticator, @@ -123,9 +124,16 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } private fun handleSsoLoginSuccess(action: LoginActions.SsoLoginSuccess) { - val session = authenticator.createSessionFromSso(action.credentials, homeServerConnectionConfig!!) + val homeServerConnectionConfigFinal = homeServerConnectionConfig - onSessionCreated(session) + if (homeServerConnectionConfigFinal == null) { + // Should not happen + Timber.w("homeServerConnectionConfig is null") + } else { + val session = authenticator.createSessionFromSso(action.credentials, homeServerConnectionConfigFinal) + + onSessionCreated(session) + } } diff --git a/vector/src/main/res/layout/activity_progress.xml b/vector/src/main/res/layout/activity_progress.xml index ae7b87b61e..5942160525 100644 --- a/vector/src/main/res/layout/activity_progress.xml +++ b/vector/src/main/res/layout/activity_progress.xml @@ -1,15 +1,12 @@ + android:layout_gravity="center" /> From 73ec0f5a83f5ef98c69ab875fc686a8bc454206e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Sep 2019 14:22:08 +0200 Subject: [PATCH 25/44] NetworkConnectivityChecker: filter onConnected callbacks (several callback if Wifi and LTE is connected) Also do not use merlinsBeard.isConnected, which return trus even if there is no internet access (ex: with Wifi hotspot) --- .../network/NetworkConnectivityChecker.kt | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt index 412cb73c6e..f89b737eff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt @@ -18,12 +18,10 @@ package im.vector.matrix.android.internal.network import android.content.Context import com.novoda.merlin.Merlin -import com.novoda.merlin.MerlinsBeard import im.vector.matrix.android.internal.di.MatrixScope import timber.log.Timber import java.util.* import javax.inject.Inject -import kotlin.collections.ArrayList import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -35,23 +33,31 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context) .withDisconnectableCallbacks() .build(context) - private val merlinsBeard = MerlinsBeard.Builder().build(context) - private val listeners = Collections.synchronizedList(ArrayList()) + private val listeners = Collections.synchronizedSet(LinkedHashSet()) + + // True when internet is available + private var hasInternetAccess = false init { merlin.bind() merlin.registerDisconnectable { - Timber.v("On Disconnect") - val localListeners = listeners.toList() - localListeners.forEach { - it.onDisconnect() + if (hasInternetAccess) { + Timber.v("On Disconnect") + hasInternetAccess = false + val localListeners = listeners.toList() + localListeners.forEach { + it.onDisconnect() + } } } merlin.registerConnectable { - Timber.v("On Connect") - val localListeners = listeners.toList() - localListeners.forEach { - it.onConnect() + if (!hasInternetAccess) { + Timber.v("On Connect") + hasInternetAccess = true + val localListeners = listeners.toList() + localListeners.forEach { + it.onConnect() + } } } } @@ -80,7 +86,7 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context) } fun isConnected(): Boolean { - return merlinsBeard.isConnected + return hasInternetAccess } interface Listener { From 25e9a179d2b7539a9e4b862bdeff13d85f3164df Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Sep 2019 14:26:30 +0200 Subject: [PATCH 26/44] SyncThread: Fix issue when network is back and the app was in background: do not restart the thread --- .../network/NetworkConnectivityChecker.kt | 9 ++--- .../internal/session/sync/job/SyncService.kt | 4 +- .../internal/session/sync/job/SyncThread.kt | 40 +++++++++++-------- .../util/BackgroundDetectionObserver.kt | 2 +- .../features/sync/widget/SyncStateView.kt | 2 +- 5 files changed, 30 insertions(+), 27 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt index f89b737eff..ed1702ec07 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt @@ -36,7 +36,8 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context) private val listeners = Collections.synchronizedSet(LinkedHashSet()) // True when internet is available - private var hasInternetAccess = false + var hasInternetAccess = false + private set init { merlin.bind() @@ -63,7 +64,7 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context) } suspend fun waitUntilConnected() { - if (isConnected()) { + if (hasInternetAccess) { return } else { suspendCoroutine { continuation -> @@ -85,10 +86,6 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context) listeners.remove(listener) } - fun isConnected(): Boolean { - return hasInternetAccess - } - interface Listener { fun onConnect() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt index 148e25b3a7..b5d83607b2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt @@ -93,8 +93,8 @@ open class SyncService : Service() { } fun doSync(once: Boolean = false) { - if (!networkConnectivityChecker.isConnected()) { - Timber.v("Sync is Paused. Waiting...") + if (!networkConnectivityChecker.hasInternetAccess) { + Timber.v("No internet access. Waiting...") //TODO Retry in ? timer.schedule(object : TimerTask() { override fun run() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt index 7433a70a50..1cb6575161 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt @@ -50,6 +50,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private val lock = Object() private var cancelableTask: Cancelable? = null + private var isStarted = false + init { updateStateTo(SyncState.IDLE) } @@ -60,19 +62,18 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, } fun restart() = synchronized(lock) { - if (state is SyncState.PAUSED) { + if (!isStarted) { Timber.v("Resume sync...") - updateStateTo(SyncState.RUNNING(afterPause = true)) + isStarted = true lock.notify() } } fun pause() = synchronized(lock) { - if (state is SyncState.RUNNING) { + if (isStarted) { Timber.v("Pause sync...") - updateStateTo(SyncState.PAUSED) + isStarted = false cancelableTask?.cancel() - lock.notify() } } @@ -87,21 +88,31 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, return liveState } + override fun onConnect() { + Timber.v("Network is back") + synchronized(lock) { + lock.notify() + } + } + override fun run() { Timber.v("Start syncing...") + isStarted = true networkConnectivityChecker.register(this) backgroundDetectionObserver.register(this) while (state != SyncState.KILLING) { Timber.v("Entering loop, state: $state") - if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) { - Timber.v("No network or sync is Paused. Waiting...") + if (!networkConnectivityChecker.hasInternetAccess) { + Timber.v("No network. Waiting...") updateStateTo(SyncState.NO_NETWORK) - - synchronized(lock) { - lock.wait() - } + synchronized(lock) { lock.wait() } + Timber.v("...unlocked") + } else if (!isStarted) { + Timber.v("Sync is Paused. Waiting...") + updateStateTo(SyncState.PAUSED) + synchronized(lock) { lock.wait() } Timber.v("...unlocked") } else { if (state !is SyncState.RUNNING) { @@ -169,16 +180,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, } private fun updateStateTo(newState: SyncState) { - Timber.v("Update state to $newState") + Timber.v("Update state from $state to $newState") state = newState liveState.postValue(newState) } - override fun onConnect() { - synchronized(lock) { - lock.notify() - } - } override fun onMoveToForeground() { restart() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/BackgroundDetectionObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/BackgroundDetectionObserver.kt index a426bdd084..9318b10717 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/BackgroundDetectionObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/BackgroundDetectionObserver.kt @@ -33,7 +33,7 @@ internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObse private set private - val listeners = ArrayList() + val listeners = LinkedHashSet() fun register(listener: Listener) { listeners.add(listener) diff --git a/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt b/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt index 2d474a13dd..43158f4e86 100755 --- a/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt +++ b/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt @@ -38,6 +38,6 @@ class SyncStateView @JvmOverloads constructor(context: Context, attrs: Attribute is SyncState.RUNNING -> if (newState.afterPause) View.VISIBLE else View.GONE else -> View.GONE } - syncStateNoNetwork.isVisible = newState is SyncState.NO_NETWORK + syncStateNoNetwork.isVisible = newState == SyncState.NO_NETWORK } } From ba9d119892c9b0f031268bb32c33eb4bbf4276e6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Sep 2019 14:50:43 +0200 Subject: [PATCH 27/44] Prepare release 0.5.0 --- CHANGES.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0f56b08aca..8dd858783c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -Changes in RiotX 0.5.0 (2019-XX-XX) +Changes in RiotX 0.5.0 (2019-09-17) =================================================== Features: @@ -9,23 +9,17 @@ Improvements: - Reduce default release build log level, and lab option to enable more logs. - Display a no network indicator when there is no network (#559) -Other changes: - - - Bugfix: - Fix crash due to missing informationData (#535) - Progress in initial sync dialog is decreasing for a step and should not (#532) - Fix rendering issue of accepted third party invitation event - All current notifications were dismissed by mistake when the app is launched from the launcher -Translations: - - - Build: - Fix issue with version name (#533) - Fix issue with bad versionCode generated by Buildkite (#553) -Changes in RiotX 0.4.0 (2019-XX-XX) +Changes in RiotX 0.4.0 (2019-08-30) =================================================== Features: From 6c2faff1f00e28fb896e9b045e9c9f4b0155eea7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Sep 2019 14:53:50 +0200 Subject: [PATCH 28/44] Version++ (0.6.0) --- CHANGES.md | 22 ++++++++++++++++++++++ vector/build.gradle | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 8dd858783c..f8561a0fe4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,29 @@ +Changes in RiotX 0.6.0 (2019-XX-XX) +=================================================== + +Features: + - + +Improvements: + - + +Other changes: + - + +Bugfix: + - + +Translations: + - + +Build: + - + Changes in RiotX 0.5.0 (2019-09-17) =================================================== Features: + - Implementation of login to homeerver with SSO - Handle M_CONSENT_NOT_GIVEN error (#64) - Auto configure homeserver and identity server URLs of LoginActivity with a magic link diff --git a/vector/build.gradle b/vector/build.gradle index 866a95b740..349bf7cf88 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 5 +ext.versionMinor = 6 ext.versionPatch = 0 static def getGitTimestamp() { From 9bf484cf1e8f3ab1379e5814248500b7d6818b21 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Sep 2019 16:38:45 +0200 Subject: [PATCH 29/44] Create a Failure to handle cancellation, and use it to ignore cancellation on room search --- .../java/im/vector/matrix/android/api/failure/Failure.kt | 1 + .../im/vector/matrix/android/internal/network/Request.kt | 2 ++ .../riotx/features/roomdirectory/RoomDirectoryViewModel.kt | 6 ++++++ 3 files changed, 9 insertions(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt index 152adb0665..26170a288f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt @@ -31,6 +31,7 @@ import java.io.IOException */ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) { data class Unknown(val throwable: Throwable? = null) : Failure(throwable) + data class Cancelled(val throwable: Throwable? = null) : Failure(throwable) data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException) data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString())) // When server send an error, but it cannot be interpreted as a MatrixError diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt index 4be2d4a27f..ede9e823bf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.internal.di.MoshiProvider +import kotlinx.coroutines.CancellationException import okhttp3.ResponseBody import org.greenrobot.eventbus.EventBus import retrofit2.Call @@ -49,6 +50,7 @@ internal class Request { is IOException -> Failure.NetworkConnection(exception) is Failure.ServerError, is Failure.OtherServerError -> exception + is CancellationException -> Failure.Cancelled(exception) else -> Failure.Unknown(exception) } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt index bf36fb265d..89af5330ca 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom @@ -176,6 +177,11 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: } override fun onFailure(failure: Throwable) { + if (failure is Failure.Cancelled) { + // Ignore, another request should be already started + return + } + currentTask = null setState { From 3739e50d466d8e38ced20b64d2dfb8385ee4e253 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Sep 2019 16:59:00 +0200 Subject: [PATCH 30/44] Better error message for timeout --- .../java/im/vector/riotx/core/error/ErrorFormatter.kt | 9 ++++++++- vector/src/main/res/values/strings_riotX.xml | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index bb7892e109..8987b0260a 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider +import java.net.SocketTimeoutException import javax.inject.Inject class ErrorFormatter @Inject constructor(val stringProvider: StringProvider) { @@ -33,7 +34,13 @@ class ErrorFormatter @Inject constructor(val stringProvider: StringProvider) { fun toHumanReadable(throwable: Throwable?): String { return when (throwable) { null -> null - is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network) + is Failure.NetworkConnection -> { + if (throwable.ioException is SocketTimeoutException) { + stringProvider.getString(R.string.error_network_timeout) + } else { + stringProvider.getString(R.string.error_no_network) + } + } is Failure.ServerError -> { if (throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN) { // Special case for terms and conditions diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index e02de69806..f2ac7817be 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -11,5 +11,6 @@ It looks like you’re trying to connect to another homeserver. Do you want to sign out? + Looks like the server is taking to long to respond, this can be caused by either poor connectivity or an error with our servers. Please try again in a while. \ No newline at end of file From b3d649a4d9a63ca3e08b4fde520f13599bca6293 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Sep 2019 14:55:19 +0200 Subject: [PATCH 31/44] Fix characters erased from the Search field when the result are coming (#545) --- CHANGES.md | 2 +- .../features/roomdirectory/PublicRoomsFragment.kt | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f8561a0fe4..de38ef4cef 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,7 +11,7 @@ Other changes: - Bugfix: - - + - Fix characters erased from the Search field when the result are coming (#545) Translations: - diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt index 32cd673273..2dfef5536f 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt @@ -143,10 +143,15 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback viewModel.loadMore() } + var initialValueSet = false + override fun invalidate() = withState(viewModel) { state -> - if (publicRoomsFilter.query.toString() != state.currentFilter) { - // For initial filter - publicRoomsFilter.setQuery(state.currentFilter, false) + if (!initialValueSet) { + initialValueSet = true + if (publicRoomsFilter.query.toString() != state.currentFilter) { + // For initial filter + publicRoomsFilter.setQuery(state.currentFilter, false) + } } // Populate list with Epoxy From ed93f4a6c16ab5c173738e94b0b682b2ef50221a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Sep 2019 17:17:54 +0200 Subject: [PATCH 32/44] Cancel any request properly --- .../riotx/features/roomdirectory/RoomDirectoryViewModel.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt index 89af5330ca..456ea23834 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt @@ -226,4 +226,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: }) } + override fun onCleared() { + super.onCleared() + + currentTask?.cancel() + } } \ No newline at end of file From 3ffe2f7d40e147a4737fec5bf4c0f26b27407400 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Sep 2019 10:27:44 +0200 Subject: [PATCH 33/44] Fix (again) issue with bad versionCode generated by Buildkite (#553) --- .buildkite/pipeline.yml | 9 +++++++++ CHANGES.md | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 8fcb69b6c0..355fa0e6b2 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -11,6 +11,9 @@ steps: # gradle build is long queue: "medium" commands: + # Workaround to set env variables from Buildkite. If we do not do that, the gradle script does not see the Buildkite env variables + - "export BUILDKITE_BRANCH=$BUILDKITE_BRANCH" + - "export BUILDKITE_BUILD_NUMBER=$BUILDKITE_BUILD_NUMBER" - "./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace" artifact_paths: - "vector/build/outputs/apk/gplay/debug/*.apk" @@ -25,6 +28,9 @@ steps: # gradle build is long queue: "medium" commands: + # Workaround to set env variables from Buildkite. If we do not do that, the gradle script does not see the Buildkite env variables + - "export BUILDKITE_BRANCH=$BUILDKITE_BRANCH" + - "export BUILDKITE_BUILD_NUMBER=$BUILDKITE_BUILD_NUMBER" - "./gradlew clean lintFdroidRelease assembleFdroidDebug --stacktrace" artifact_paths: - "vector/build/outputs/apk/fdroid/debug/*.apk" @@ -39,6 +45,9 @@ steps: # gradle build is long queue: "medium" commands: + # Workaround to set env variables from Buildkite. If we do not do that, the gradle script does not see the Buildkite env variables + - "export BUILDKITE_BRANCH=$BUILDKITE_BRANCH" + - "export BUILDKITE_BUILD_NUMBER=$BUILDKITE_BUILD_NUMBER" - "./gradlew clean assembleGplayRelease --stacktrace" artifact_paths: - "vector/build/outputs/apk/gplay/release/*.apk" diff --git a/CHANGES.md b/CHANGES.md index de38ef4cef..cd6db863c5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,7 @@ Translations: - Build: - - + - Fix (again) issue with bad versionCode generated by Buildkite (#553) Changes in RiotX 0.5.0 (2019-09-17) =================================================== From 528958b3de76717fb8c784b6bdc6c8c23fd58416 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Sep 2019 10:58:03 +0200 Subject: [PATCH 34/44] Avoid export on env variable --- .buildkite/pipeline.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 355fa0e6b2..c9fe56348b 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -12,9 +12,7 @@ steps: queue: "medium" commands: # Workaround to set env variables from Buildkite. If we do not do that, the gradle script does not see the Buildkite env variables - - "export BUILDKITE_BRANCH=$BUILDKITE_BRANCH" - - "export BUILDKITE_BUILD_NUMBER=$BUILDKITE_BUILD_NUMBER" - - "./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace" + - "BUILDKITE_BRANCH=\"$BUILDKITE_BRANCH\" BUILDKITE_BUILD_NUMBER=\"$BUILDKITE_BUILD_NUMBER\" ./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace" artifact_paths: - "vector/build/outputs/apk/gplay/debug/*.apk" branches: "!master" @@ -29,9 +27,7 @@ steps: queue: "medium" commands: # Workaround to set env variables from Buildkite. If we do not do that, the gradle script does not see the Buildkite env variables - - "export BUILDKITE_BRANCH=$BUILDKITE_BRANCH" - - "export BUILDKITE_BUILD_NUMBER=$BUILDKITE_BUILD_NUMBER" - - "./gradlew clean lintFdroidRelease assembleFdroidDebug --stacktrace" + - "BUILDKITE_BRANCH=\"$BUILDKITE_BRANCH\" BUILDKITE_BUILD_NUMBER=\"$BUILDKITE_BUILD_NUMBER\" ./gradlew clean lintFdroidRelease assembleFdroidDebug --stacktrace" artifact_paths: - "vector/build/outputs/apk/fdroid/debug/*.apk" branches: "!master" @@ -46,9 +42,7 @@ steps: queue: "medium" commands: # Workaround to set env variables from Buildkite. If we do not do that, the gradle script does not see the Buildkite env variables - - "export BUILDKITE_BRANCH=$BUILDKITE_BRANCH" - - "export BUILDKITE_BUILD_NUMBER=$BUILDKITE_BUILD_NUMBER" - - "./gradlew clean assembleGplayRelease --stacktrace" + - "BUILDKITE_BRANCH=\"$BUILDKITE_BRANCH\" BUILDKITE_BUILD_NUMBER=\"$BUILDKITE_BUILD_NUMBER\" ./gradlew clean assembleGplayRelease --stacktrace" artifact_paths: - "vector/build/outputs/apk/gplay/release/*.apk" branches: "master" From ffa8b7e73ad1d649df55fe3da561b8f88953d08f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Sep 2019 11:24:29 +0200 Subject: [PATCH 35/44] Better fix --- .buildkite/pipeline.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index c9fe56348b..e60595e8b9 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -1,6 +1,7 @@ # Use Docker file from https://hub.docker.com/r/runmymind/docker-android-sdk # Last docker plugin version can be found here: # https://github.com/buildkite-plugins/docker-buildkite-plugin/releases +# We propagate the environment to the container (sse https://github.com/buildkite-plugins/docker-buildkite-plugin#propagate-environment-optional-boolean) # Build debug version of the RiotX application, from the develop branch and the features branches @@ -11,14 +12,14 @@ steps: # gradle build is long queue: "medium" commands: - # Workaround to set env variables from Buildkite. If we do not do that, the gradle script does not see the Buildkite env variables - - "BUILDKITE_BRANCH=\"$BUILDKITE_BRANCH\" BUILDKITE_BUILD_NUMBER=\"$BUILDKITE_BUILD_NUMBER\" ./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace" + - "./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace" artifact_paths: - "vector/build/outputs/apk/gplay/debug/*.apk" branches: "!master" plugins: - docker#v3.1.0: image: "runmymind/docker-android-sdk" + propagate-environment: true - label: "Assemble FDroid Debug version" agents: @@ -26,14 +27,14 @@ steps: # gradle build is long queue: "medium" commands: - # Workaround to set env variables from Buildkite. If we do not do that, the gradle script does not see the Buildkite env variables - - "BUILDKITE_BRANCH=\"$BUILDKITE_BRANCH\" BUILDKITE_BUILD_NUMBER=\"$BUILDKITE_BUILD_NUMBER\" ./gradlew clean lintFdroidRelease assembleFdroidDebug --stacktrace" + - "./gradlew clean lintFdroidRelease assembleFdroidDebug --stacktrace" artifact_paths: - "vector/build/outputs/apk/fdroid/debug/*.apk" branches: "!master" plugins: - docker#v3.1.0: image: "runmymind/docker-android-sdk" + propagate-environment: true - label: "Build Google Play unsigned APK" agents: @@ -41,14 +42,14 @@ steps: # gradle build is long queue: "medium" commands: - # Workaround to set env variables from Buildkite. If we do not do that, the gradle script does not see the Buildkite env variables - - "BUILDKITE_BRANCH=\"$BUILDKITE_BRANCH\" BUILDKITE_BUILD_NUMBER=\"$BUILDKITE_BUILD_NUMBER\" ./gradlew clean assembleGplayRelease --stacktrace" + - "./gradlew clean assembleGplayRelease --stacktrace" artifact_paths: - "vector/build/outputs/apk/gplay/release/*.apk" branches: "master" plugins: - docker#v3.1.0: image: "runmymind/docker-android-sdk" + propagate-environment: true # Code quality From b4a13f95049ccf2d252da0b81f8b3ebf43c8c68d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 19 Sep 2019 12:42:01 +0200 Subject: [PATCH 36/44] Add unread indent on room list --- CHANGES.md | 2 +- .../api/session/room/model/RoomSummary.kt | 3 +- .../database/mapper/RoomSummaryMapper.kt | 18 ++++---- .../database/model/RoomSummaryEntity.kt | 3 +- .../internal/database/query/ReadQueries.kt | 42 +++++++++++++++++++ .../session/room/RoomSummaryUpdater.kt | 17 +++++--- .../session/room/read/DefaultReadService.kt | 19 +-------- .../session/room/read/SetReadMarkersTask.kt | 1 + .../internal/session/sync/RoomSyncHandler.kt | 4 +- .../home/room/detail/RoomDetailViewModel.kt | 2 +- .../room/list/ChronologicalRoomComparator.kt | 8 ++-- .../home/room/list/RoomCategoryItem.kt | 4 +- .../home/room/list/RoomSummaryController.kt | 2 +- .../home/room/list/RoomSummaryItem.kt | 9 +++- .../home/room/list/RoomSummaryItemFactory.kt | 13 +++--- vector/src/main/res/layout/item_room.xml | 12 +++++- 16 files changed, 107 insertions(+), 52 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt diff --git a/CHANGES.md b/CHANGES.md index cd6db863c5..a5f2a24e12 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Features: - Improvements: - - + - Add unread indent on room list (#485) Other changes: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index aae72dd41f..e4bf1bd32b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -29,10 +29,11 @@ data class RoomSummary( val topic: String = "", val avatarUrl: String = "", val isDirect: Boolean = false, - val latestEvent: TimelineEvent? = null, + val latestPreviewableEvent: TimelineEvent? = null, val otherMemberIds: List = emptyList(), val notificationCount: Int = 0, val highlightCount: Int = 0, + val hasUnreadMessages: Boolean = false, val tags: List = emptyList(), val membership: Membership = Membership.NONE, val versioningState: VersioningState = VersioningState.NONE diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 3ec51226af..654b5f8595 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -35,7 +35,7 @@ internal class RoomSummaryMapper @Inject constructor( RoomTag(it.tagName, it.tagOrder) } - val latestEvent = roomSummaryEntity.latestEvent?.let { + val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let { timelineEventMapper.map(it) } if (latestEvent?.root?.isEncrypted() == true && latestEvent.root.mxDecryptionResult == null) { @@ -43,26 +43,28 @@ internal class RoomSummaryMapper @Inject constructor( //for now decrypt sync try { val result = cryptoService.decryptEvent(latestEvent.root, latestEvent.root.roomId + UUID.randomUUID().toString()) - latestEvent.root.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) + latestEvent.root.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) } catch (e: MXCryptoError) { } } + return RoomSummary( roomId = roomSummaryEntity.roomId, displayName = roomSummaryEntity.displayName ?: "", topic = roomSummaryEntity.topic ?: "", avatarUrl = roomSummaryEntity.avatarUrl ?: "", isDirect = roomSummaryEntity.isDirect, - latestEvent = latestEvent, + latestPreviewableEvent = latestEvent, otherMemberIds = roomSummaryEntity.otherMemberIds.toList(), highlightCount = roomSummaryEntity.highlightCount, notificationCount = roomSummaryEntity.notificationCount, + hasUnreadMessages = roomSummaryEntity.hasUnreadMessages, tags = tags, membership = roomSummaryEntity.membership, versioningState = roomSummaryEntity.versioningState diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 6fe81f4cdd..95308d367e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -26,7 +26,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var displayName: String? = "", var avatarUrl: String? = "", var topic: String? = "", - var latestEvent: TimelineEventEntity? = null, + var latestPreviewableEvent: TimelineEventEntity? = null, var heroes: RealmList = RealmList(), var joinedMembersCount: Int? = 0, var invitedMembersCount: Int? = 0, @@ -35,6 +35,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var otherMemberIds: RealmList = RealmList(), var notificationCount: Int = 0, var highlightCount: Int = 0, + var hasUnreadMessages: Boolean = false, var tags: RealmList = RealmList() ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt new file mode 100644 index 0000000000..82ab72db26 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.database.query + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptEntity + +internal fun isEventRead(monarchy: Monarchy, + userId: String?, + roomId: String?, + eventId: String?): Boolean { + if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) { + return false + } + + var isEventRead = false + + monarchy.doWithRealm { realm -> + val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@doWithRealm + val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm + val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex ?: Int.MIN_VALUE + val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex ?: Int.MAX_VALUE + + isEventRead = eventToCheckIndex <= readReceiptIndex + } + + return isEventRead +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index dda8b9322f..a8814771cf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.session.room +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel @@ -26,6 +27,7 @@ import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.query.isEventRead import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where @@ -39,7 +41,8 @@ import javax.inject.Inject internal class RoomSummaryUpdater @Inject constructor(private val credentials: Credentials, private val roomDisplayNameResolver: RoomDisplayNameResolver, - private val roomAvatarResolver: RoomAvatarResolver) { + private val roomAvatarResolver: RoomAvatarResolver, + private val monarchy: Monarchy) { // TODO: maybe allow user of SDK to give that list private val PREVIEWABLE_TYPES = listOf( @@ -63,8 +66,7 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C membership: Membership? = null, roomSummary: RoomSyncSummary? = null, unreadNotifications: RoomSyncUnreadNotifications? = null) { - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) if (roomSummary != null) { if (roomSummary.heroes.isNotEmpty()) { @@ -85,9 +87,13 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C roomSummaryEntity.membership = membership } - val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, includedTypes = PREVIEWABLE_TYPES) + val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, includedTypes = PREVIEWABLE_TYPES) val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain() + roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 + //avoid this call if we are sure there are unread events + || !isEventRead(monarchy, credentials.userId, roomId, latestPreviewableEvent?.eventId) + val otherRoomMembers = RoomMembers(realm, roomId) .queryRoomMembersEvent() .notEqualTo(EventEntityFields.STATE_KEY, credentials.userId) @@ -98,9 +104,8 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) roomSummaryEntity.topic = lastTopicEvent?.content.toModel()?.topic - roomSummaryEntity.latestEvent = latestEvent + roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) - } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index 505b958911..11dfee5f81 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -27,11 +27,8 @@ import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity -import im.vector.matrix.android.internal.database.query.find -import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.isEventRead import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -78,19 +75,7 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private override fun isEventRead(eventId: String): Boolean { - var isEventRead = false - monarchy.doWithRealm { - val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst() - ?: return@doWithRealm - val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) - ?: return@doWithRealm - val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE - val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex - ?: Int.MAX_VALUE - isEventRead = eventToCheckIndex <= readReceiptIndex - } - return isEventRead + return isEventRead(monarchy, credentials.userId, roomId, eventId) } override fun getEventReadReceiptsLive(eventId: String): LiveData> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 41c9cca507..4fa365bb66 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -101,6 +101,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI ?: return@writeAsync roomSummary.notificationCount = 0 roomSummary.highlightCount = 0 + roomSummary.hasUnreadMessages = false } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index fccaa9b9b2..8586e6fa0b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -113,12 +113,12 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch private fun handleJoinedRoom(realm: Realm, roomId: String, roomSync: RoomSync, - isInitalSync: Boolean): RoomEntity { + isInitialSync: Boolean): RoomEntity { Timber.v("Handle join sync for room $roomId") if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) { - handleEphemeral(realm, roomId, roomSync.ephemeral, isInitalSync) + handleEphemeral(realm, roomId, roomSync.ephemeral, isInitialSync) } if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 1cd8cc4a41..8ec133c642 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -658,7 +658,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun observeSummaryState() { asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> if (summary.membership == Membership.INVITE) { - summary.latestEvent?.root?.senderId?.let { senderId -> + summary.latestPreviewableEvent?.root?.senderId?.let { senderId -> session.getUser(senderId) }?.also { setState { copy(asyncInviter = Success(it)) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/ChronologicalRoomComparator.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/ChronologicalRoomComparator.kt index c4a1633bed..d25198f56a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/ChronologicalRoomComparator.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/ChronologicalRoomComparator.kt @@ -25,14 +25,14 @@ class ChronologicalRoomComparator @Inject constructor() : Comparator() { @EpoxyAttribute lateinit var title: CharSequence @EpoxyAttribute var expanded: Boolean = false - @EpoxyAttribute var unreadCount: Int = 0 + @EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var listener: (() -> Unit)? = null @@ -42,7 +42,7 @@ abstract class RoomCategoryItem : VectorEpoxyModel() { val expandedArrowDrawable = ContextCompat.getDrawable(holder.rootView.context, expandedArrowDrawableRes)?.also { DrawableCompat.setTint(it, tintColor) } - holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadCount, showHighlighted)) + holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted)) holder.titleView.setCompoundDrawablesWithIntrinsicBounds(expandedArrowDrawable, null, null, null) holder.titleView.text = title holder.rootView.setOnClickListener { listener?.invoke() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt index 03bedbc7b5..42e3a3db85 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt @@ -101,7 +101,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri id(titleRes) title(stringProvider.getString(titleRes).toUpperCase()) expanded(isExpanded) - unreadCount(unreadCount) + unreadNotificationCount(unreadCount) showHighlighted(showHighlighted) listener { mutateExpandedState() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt index 72f5b973fb..2ee1f30645 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt @@ -16,9 +16,11 @@ package im.vector.riotx.features.home.room.list +import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -36,7 +38,8 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var lastFormattedEvent: CharSequence @EpoxyAttribute lateinit var lastEventTime: CharSequence @EpoxyAttribute var avatarUrl: String? = null - @EpoxyAttribute var unreadCount: Int = 0 + @EpoxyAttribute var unreadNotificationCount: Int = 0 + @EpoxyAttribute var hasUnreadMessage: Boolean = false @EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var listener: (() -> Unit)? = null @@ -47,13 +50,15 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { holder.titleView.text = roomName holder.lastEventTimeView.text = lastEventTime holder.lastEventView.text = lastFormattedEvent - holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadCount, showHighlighted)) + holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted)) + holder.unreadIndentIndicator.isVisible = hasUnreadMessage avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView) } class Holder : VectorEpoxyHolder() { val titleView by bind(R.id.roomNameView) val unreadCounterBadgeView by bind(R.id.roomUnreadCounterBadgeView) + val unreadIndentIndicator by bind(R.id.roomUnreadIndicator) val lastEventView by bind(R.id.roomLastEventView) val lastEventTimeView by bind(R.id.roomLastEventTimeView) val avatarImageView by bind(R.id.roomAvatarImageView) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt index 38f15974f3..e1d5f2a14e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.home.room.list +import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership @@ -38,7 +39,8 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte private val dateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider, private val stringProvider: StringProvider, - private val avatarRenderer: AvatarRenderer) { + private val avatarRenderer: AvatarRenderer, + private val session: Session) { fun create(roomSummary: RoomSummary, joiningRoomsIds: Set, @@ -59,9 +61,9 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte rejectingErrorRoomsIds: Set, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { val secondLine = if (roomSummary.isDirect) { - roomSummary.latestEvent?.root?.senderId + roomSummary.latestPreviewableEvent?.root?.senderId } else { - roomSummary.latestEvent?.root?.senderId?.let { + roomSummary.latestPreviewableEvent?.root?.senderId?.let { stringProvider.getString(R.string.invited_by, it) } } @@ -88,7 +90,7 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte var latestFormattedEvent: CharSequence = "" var latestEventTime: CharSequence = "" - val latestEvent = roomSummary.latestEvent + val latestEvent = roomSummary.latestPreviewableEvent if (latestEvent != null) { val date = latestEvent.root.localDateTime() val currentDate = DateProvider.currentLocalDateTime() @@ -131,7 +133,8 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte .roomName(roomSummary.displayName) .avatarUrl(roomSummary.avatarUrl) .showHighlighted(showHighlighted) - .unreadCount(unreadCount) + .unreadNotificationCount(unreadCount) + .hasUnreadMessage(roomSummary.hasUnreadMessages) .listener { listener?.onRoomSelected(roomSummary) } } diff --git a/vector/src/main/res/layout/item_room.xml b/vector/src/main/res/layout/item_room.xml index b110e20082..7eb2083ecb 100644 --- a/vector/src/main/res/layout/item_room.xml +++ b/vector/src/main/res/layout/item_room.xml @@ -10,12 +10,22 @@ android:focusable="true" android:foreground="?attr/selectableItemBackground"> + + Date: Thu, 19 Sep 2019 10:21:28 +0200 Subject: [PATCH 37/44] Cleanup injected constructors --- .../android/internal/crypto/ObjectSigner.kt | 2 +- .../actions/EnsureOlmSessionsForUsersAction.kt | 6 +++--- .../actions/SetDeviceVerificationAction.kt | 4 ++-- .../megolm/MXMegolmDecryptionFactory.kt | 16 ++++++++-------- .../database/mapper/RoomSummaryMapper.kt | 4 ++-- .../android/internal/network/UserAgentHolder.kt | 2 +- .../timeline/DefaultGetContextOfEventTask.kt | 4 ++-- .../im/vector/riotx/core/error/ErrorFormatter.kt | 2 +- .../KeysBackupSettingsRecyclerViewController.kt | 4 ++-- .../crypto/keysrequest/KeyRequestHandler.kt | 2 +- .../IncomingVerificationRequestHandler.kt | 2 +- .../home/room/list/RoomSummaryItemFactory.kt | 8 +++----- .../riotx/features/html/EventHtmlRenderer.kt | 2 +- .../riotx/features/notifications/BitmapLoader.kt | 2 +- .../riotx/features/notifications/IconLoader.kt | 2 +- .../roomdirectory/PublicRoomsController.kt | 2 +- .../createroom/CreateRoomController.kt | 2 +- 17 files changed, 32 insertions(+), 34 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ObjectSigner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ObjectSigner.kt index 38d3e69589..625f81de86 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ObjectSigner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ObjectSigner.kt @@ -20,7 +20,7 @@ import im.vector.matrix.android.api.auth.data.Credentials import javax.inject.Inject internal class ObjectSigner @Inject constructor(private val credentials: Credentials, - private val olmDevice: MXOlmDevice) { + private val olmDevice: MXOlmDevice) { /** * Sign Object diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt index 840a66c543..f929859d76 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt @@ -27,14 +27,14 @@ import java.util.* import javax.inject.Inject internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val olmDevice: MXOlmDevice, - private val cryptoStore: IMXCryptoStore, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction) { + private val cryptoStore: IMXCryptoStore, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction) { /** * Try to make sure we have established olm sessions for the given users. * @param users a list of user ids. */ - suspend fun handle(users: List) : MXUsersDevicesMap { + suspend fun handle(users: List): MXUsersDevicesMap { Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users") val devicesByUser = HashMap>() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt index 7fc39312e1..d5c41769ef 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt @@ -23,8 +23,8 @@ import timber.log.Timber import javax.inject.Inject internal class SetDeviceVerificationAction @Inject constructor(private val cryptoStore: IMXCryptoStore, - private val credentials: Credentials, - private val keysBackup: KeysBackup) { + private val credentials: Credentials, + private val keysBackup: KeysBackup) { fun handle(verificationStatus: Int, deviceId: String, userId: String) { val device = cryptoStore.getUserDevice(deviceId, userId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index eb8df7b9f8..11c43649e9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -28,14 +28,14 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import javax.inject.Inject internal class MXMegolmDecryptionFactory @Inject constructor(private val credentials: Credentials, - private val olmDevice: MXOlmDevice, - private val deviceListManager: DeviceListManager, - private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, - private val messageEncrypter: MessageEncrypter, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers) { + private val olmDevice: MXOlmDevice, + private val deviceListManager: DeviceListManager, + private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, + private val messageEncrypter: MessageEncrypter, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val cryptoStore: IMXCryptoStore, + private val sendToDeviceTask: SendToDeviceTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers) { fun create(): MXMegolmDecryption { return MXMegolmDecryption( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 654b5f8595..8cb738807f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -26,8 +26,8 @@ import java.util.UUID import javax.inject.Inject internal class RoomSummaryMapper @Inject constructor( - val cryptoService: CryptoService, - val timelineEventMapper: TimelineEventMapper + private val cryptoService: CryptoService, + private val timelineEventMapper: TimelineEventMapper ) { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt index 306b8a91e6..5c344a0b11 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt @@ -24,7 +24,7 @@ import timber.log.Timber import javax.inject.Inject @MatrixScope -internal class UserAgentHolder @Inject constructor(val context: Context) { +internal class UserAgentHolder @Inject constructor(private val context: Context) { var userAgent: String = "" private set diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt index e4f48d35a4..efd099e9f9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt @@ -32,8 +32,8 @@ internal interface GetContextOfEventTask : Task() { +class KeysBackupSettingsRecyclerViewController @Inject constructor(private val stringProvider: StringProvider, + private val session: Session) : TypedEpoxyController() { var listener: Listener? = null diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt index 83e6361252..fa10850590 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt @@ -54,7 +54,7 @@ import kotlin.collections.HashMap */ @Singleton -class KeyRequestHandler @Inject constructor(val context: Context) +class KeyRequestHandler @Inject constructor(private val context: Context) : RoomKeysRequestListener, SasVerificationService.SasVerificationListener { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt index e138eb9ea4..98bd8d34ed 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -29,7 +29,7 @@ import javax.inject.Singleton * Listens to the VerificationManager and add a new notification when an incoming request is detected. */ @Singleton -class IncomingVerificationRequestHandler @Inject constructor(val context: Context) : SasVerificationService.SasVerificationListener { +class IncomingVerificationRequestHandler @Inject constructor(private val context: Context) : SasVerificationService.SasVerificationListener { private var session: Session? = null diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt index e1d5f2a14e..015e54b368 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt @@ -16,13 +16,13 @@ package im.vector.riotx.features.home.room.list -import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.riotx.R +import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.ColorProvider @@ -30,7 +30,6 @@ import im.vector.riotx.core.resources.DateProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter -import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.features.home.room.detail.timeline.helper.senderName import me.gujun.android.span.span import javax.inject.Inject @@ -39,8 +38,7 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte private val dateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider, private val stringProvider: StringProvider, - private val avatarRenderer: AvatarRenderer, - private val session: Session) { + private val avatarRenderer: AvatarRenderer) { fun create(roomSummary: RoomSummary, joiningRoomsIds: Set, @@ -96,7 +94,7 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte val currentDate = DateProvider.currentLocalDateTime() val isSameDay = date.toLocalDate() == currentDate.toLocalDate() latestFormattedEvent = if (latestEvent.root.isEncrypted() - && latestEvent.root.mxDecryptionResult == null) { + && latestEvent.root.mxDecryptionResult == null) { stringProvider.getString(R.string.encrypted_message) } else if (latestEvent.root.getClearType() == EventType.MESSAGE) { val senderName = latestEvent.senderName() ?: latestEvent.root.senderId diff --git a/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt index 476a70e7a4..fcd2c011ee 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt @@ -40,7 +40,7 @@ import javax.inject.Singleton @Singleton class EventHtmlRenderer @Inject constructor(context: Context, - val avatarRenderer: AvatarRenderer, + avatarRenderer: AvatarRenderer, sessionHolder: ActiveSessionHolder) { private val markwon = Markwon.builder(context) .usePlugin(MatrixPlugin.create(GlideApp.with(context), context, avatarRenderer, sessionHolder)) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/BitmapLoader.kt b/vector/src/main/java/im/vector/riotx/features/notifications/BitmapLoader.kt index c3a74bd9c3..fbef196c53 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/BitmapLoader.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/BitmapLoader.kt @@ -26,7 +26,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class BitmapLoader @Inject constructor(val context: Context) { +class BitmapLoader @Inject constructor(private val context: Context) { /** * Avatar Url -> Bitmap diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/IconLoader.kt b/vector/src/main/java/im/vector/riotx/features/notifications/IconLoader.kt index 0c4477f41f..dccae42279 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/IconLoader.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/IconLoader.kt @@ -28,7 +28,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class IconLoader @Inject constructor(val context: Context) { +class IconLoader @Inject constructor(private val context: Context) { /** * Avatar Url -> IconCompat diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt index 47a7722e9d..9502370219 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt @@ -33,7 +33,7 @@ import javax.inject.Inject class PublicRoomsController @Inject constructor(private val stringProvider: StringProvider, private val avatarRenderer: AvatarRenderer, - private val errorFormatter: ErrorFormatter) : TypedEpoxyController() { + private val errorFormatter: ErrorFormatter) : TypedEpoxyController() { var callback: Callback? = null diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomController.kt index ad58c0ce26..7559606315 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomController.kt @@ -31,7 +31,7 @@ import im.vector.riotx.features.form.formSwitchItem import javax.inject.Inject class CreateRoomController @Inject constructor(private val stringProvider: StringProvider, - private val errorFormatter: ErrorFormatter + private val errorFormatter: ErrorFormatter ) : TypedEpoxyController() { var listener: Listener? = null From 3169093c50534c302ba7bdf94e9bb616af4ecdc4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 19 Sep 2019 12:55:39 +0200 Subject: [PATCH 38/44] Quick fix on the no connection banner displayed when internet is available --- CHANGES.md | 3 ++- .../android/internal/network/NetworkConnectivityChecker.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a5f2a24e12..ef90baca8d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ Other changes: Bugfix: - Fix characters erased from the Search field when the result are coming (#545) + - "No connection" banner was displayed by mistake Translations: - @@ -23,7 +24,7 @@ Changes in RiotX 0.5.0 (2019-09-17) =================================================== Features: - - Implementation of login to homeerver with SSO + - Implementation of login to homeserver with SSO - Handle M_CONSENT_NOT_GIVEN error (#64) - Auto configure homeserver and identity server URLs of LoginActivity with a magic link diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt index ed1702ec07..88229db325 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.network import android.content.Context import com.novoda.merlin.Merlin +import com.novoda.merlin.MerlinsBeard import im.vector.matrix.android.internal.di.MatrixScope import timber.log.Timber import java.util.* @@ -36,7 +37,7 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context) private val listeners = Collections.synchronizedSet(LinkedHashSet()) // True when internet is available - var hasInternetAccess = false + var hasInternetAccess = MerlinsBeard.Builder().build(context).isConnected private set init { From 468bd5bcc9e3a557396ebf9ccc2e9d8586442087 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Sep 2019 09:37:12 +0200 Subject: [PATCH 39/44] Message Editing: Update notifications (#128) --- CHANGES.md | 1 + .../session/room/timeline/TimelineEvent.kt | 22 ++++++++++++++++++ .../fcm/VectorFirebaseMessagingService.kt | 1 + .../notifications/InviteNotifiableEvent.kt | 1 + .../features/notifications/NotifiableEvent.kt | 1 + .../notifications/NotifiableEventResolver.kt | 14 ++++++----- .../notifications/NotifiableMessageEvent.kt | 1 + .../NotificationBroadcastReceiver.kt | 1 + .../NotificationDrawerManager.kt | 23 ++++++++++++++++++- .../notifications/SimpleNotifiableEvent.kt | 1 + 10 files changed, 59 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ef90baca8d..412d32a0a1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Features: Improvements: - Add unread indent on room list (#485) + - Message Editing: Update notifications (#128) Other changes: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index 36ca360e08..73ef7b779a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.timeline import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.ReadReceipt @@ -92,6 +93,13 @@ data class TimelineEvent( */ fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null +/** + * Get the eventId which was edited by this event if any + */ +fun TimelineEvent.getEditedEventId(): String? { + return root.getClearContent().toModel()?.relatesTo?.takeIf { it.type == RelationType.REPLACE }?.eventId +} + /** * Get last MessageContent, after a possible edition */ @@ -99,6 +107,20 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSu ?: root.getClearContent().toModel() +/** + * Get last Message body, after a possible edition + */ +fun TimelineEvent.getLastMessageBody(): String? { + val lastMessageContent = getLastMessageContent() + + if (lastMessageContent != null) { + return lastMessageContent.newContent?.toModel()?.body ?: lastMessageContent.body + } + + return null +} + + fun TimelineEvent.getTextEditableContent(): String? { val originalContent = root.getClearContent().toModel() ?: return null val isReply = originalContent.isReply() || root.content.toModel()?.relatesTo?.inReplyTo?.eventId != null diff --git a/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt index 5ad064683c..dcb228cfbd 100755 --- a/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt @@ -203,6 +203,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { val simpleNotifiableEvent = SimpleNotifiableEvent( session.myUserId, eventId, + null, true, //It's an issue in this case, all event will bing even if expected to be silent. title = getString(R.string.notification_unknown_new_event), description = "", diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/riotx/features/notifications/InviteNotifiableEvent.kt index d6f3655f4a..10add07a66 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/InviteNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/InviteNotifiableEvent.kt @@ -21,6 +21,7 @@ import androidx.core.app.NotificationCompat data class InviteNotifiableEvent( override var matrixID: String?, override val eventId: String, + override val editedEventId: String?, var roomId: String, override var noisy: Boolean, override val title: String, diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt index ea2e21ab02..bc9c711d67 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt @@ -20,6 +20,7 @@ import java.io.Serializable interface NotifiableEvent : Serializable { var matrixID: String? val eventId: String + val editedEventId: String? var noisy: Boolean val title: String val description: String? diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt index 6650bf0d0d..297e0b31e4 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt @@ -25,7 +25,8 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent +import im.vector.matrix.android.api.session.room.timeline.getEditedEventId +import im.vector.matrix.android.api.session.room.timeline.getLastMessageBody import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.riotx.BuildConfig import im.vector.riotx.R @@ -72,6 +73,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St return SimpleNotifiableEvent( session.myUserId, eventId = event.eventId!!, + editedEventId = timelineEvent.getEditedEventId(), noisy = false,//will be updated timestamp = event.originServerTs ?: System.currentTimeMillis(), description = bodyPreview, @@ -82,7 +84,6 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St } } - private fun resolveMessageEvent(event: TimelineEvent, session: Session): NotifiableEvent? { //The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) @@ -93,14 +94,14 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St Timber.e("## Unable to resolve room for eventId [${event}]") // Ok room is not known in store, but we can still display something val body = - event.getLastMessageContent() - ?.body + event.getLastMessageBody() ?: stringProvider.getString(R.string.notification_unknown_new_event) val roomName = stringProvider.getString(R.string.notification_unknown_room_name) val senderDisplayName = event.senderName ?: event.root.senderId val notifiableEvent = NotifiableMessageEvent( eventId = event.root.eventId!!, + editedEventId = event.getEditedEventId(), timestamp = event.root.originServerTs ?: 0, noisy = false,//will be updated senderName = senderDisplayName, @@ -128,14 +129,14 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St } } - val body = event.getLastMessageContent() - ?.body + val body = event.getLastMessageBody() ?: stringProvider.getString(R.string.notification_unknown_new_event) val roomName = room.roomSummary()?.displayName ?: "" val senderDisplayName = event.senderName ?: event.root.senderId val notifiableEvent = NotifiableMessageEvent( eventId = event.root.eventId!!, + editedEventId = event.getEditedEventId(), timestamp = event.root.originServerTs ?: 0, noisy = false,//will be updated senderName = senderDisplayName, @@ -177,6 +178,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St return InviteNotifiableEvent( session.myUserId, eventId = event.eventId!!, + editedEventId = null, roomId = roomId, timestamp = event.originServerTs ?: 0, noisy = false,//will be set later diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableMessageEvent.kt index 11c2d744f6..36af131130 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableMessageEvent.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableMessageEvent.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.events.model.EventType data class NotifiableMessageEvent( override val eventId: String, + override val editedEventId: String?, override var noisy: Boolean, override val timestamp: Long, var senderName: String?, diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt index 638cf7d161..cb68d0101d 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt @@ -119,6 +119,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { val notifiableMessageEvent = NotifiableMessageEvent( // Generate a Fake event id UUID.randomUUID().toString(), + null, false, System.currentTimeMillis(), session.getUser(session.myUserId)?.displayName diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index 551bbb808b..3ea4c970a0 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -101,7 +101,28 @@ class NotificationDrawerManager @Inject constructor(private val context: Context //keep the existing one, do not replace } } else { - eventList.add(notifiableEvent) + // Check if this is an edit + if (notifiableEvent.editedEventId != null) { + // This is an edition + val eventBeforeEdition = eventList.firstOrNull { + // Edition of an event + it.eventId == notifiableEvent.editedEventId + // or edition of an edition + || it.editedEventId == notifiableEvent.editedEventId + } + + if (eventBeforeEdition != null) { + // Replace the existing notification with the new content + eventList.remove(eventBeforeEdition) + + eventList.add(notifiableEvent) + } else { + // Ignore an edit of a not displayed event in the notification drawer + } + } else { + // Not an edit + eventList.add(notifiableEvent) + } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/riotx/features/notifications/SimpleNotifiableEvent.kt index 8ec5ff9580..abd7b706bc 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/SimpleNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/SimpleNotifiableEvent.kt @@ -20,6 +20,7 @@ import androidx.core.app.NotificationCompat data class SimpleNotifiableEvent( override var matrixID: String?, override val eventId: String, + override val editedEventId: String?, override var noisy: Boolean, override val title: String, override val description: String, From 7da9cafcc2cd5773e5dea13b31dcefbdf353cf40 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Sep 2019 16:26:25 +0200 Subject: [PATCH 40/44] Remove any notification of a redacted event (#563) Also do some cleanup and kotlinification on the code --- CHANGES.md | 1 + .../android/api/pushrules/PushRuleService.kt | 1 + .../notification/DefaultPushRuleService.kt | 10 ++++ .../notification/ProcessEventForPushTask.kt | 19 ++++++++ .../fcm/VectorFirebaseMessagingService.kt | 7 --- .../notifications/InviteNotifiableEvent.kt | 1 + .../features/notifications/NotifiableEvent.kt | 1 + .../notifications/NotifiableMessageEvent.kt | 1 + .../NotificationDrawerManager.kt | 47 ++++++++++--------- .../notifications/PushRuleTriggerListener.kt | 4 ++ .../notifications/SimpleNotifiableEvent.kt | 1 + 11 files changed, 63 insertions(+), 30 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 412d32a0a1..75b11bdbd4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ Features: Improvements: - Add unread indent on room list (#485) - Message Editing: Update notifications (#128) + - Remove any notification of a redacted event (#563) Other changes: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt index 28fcff0c76..05396cb219 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt @@ -43,6 +43,7 @@ interface PushRuleService { interface PushRuleListener { fun onMatchRule(event: Event, actions: List) fun onRoomLeft(roomId: String) + fun onEventRedacted(redactedEventId: String) fun batchFinish() } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt index 83b89701b3..7cccd0dc3e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt @@ -132,6 +132,16 @@ internal class DefaultPushRuleService @Inject constructor( } } + fun dispatchRedactedEventId(redactedEventId: String) { + try { + listeners.forEach { + it.onEventRedacted(redactedEventId) + } + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching room left") + } + } + fun dispatchFinish() { try { listeners.forEach { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt index a434c6e950..064f1e3f46 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt @@ -78,6 +78,25 @@ internal class DefaultProcessEventForPushTask @Inject constructor( defaultPushRuleService.dispatchBing(event, it) } } + + val allRedactedEvents = params.syncResponse.join + .map { entries -> + entries.value.timeline?.events?.filter { + it.type == EventType.REDACTION + } + .orEmpty() + .mapNotNull { it.redacts } + } + .fold(emptyList(), { acc, next -> + acc + next + }) + + Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events") + + allRedactedEvents.forEach { redactedEventId -> + defaultPushRuleService.dispatchRedactedEventId(redactedEventId) + } + defaultPushRuleService.dispatchFinish() } diff --git a/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt index dcb228cfbd..cde848ea41 100755 --- a/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt @@ -196,7 +196,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { // This ID can and should be used to detect duplicate notification requests. val eventId = data["event_id"] ?: return //Just ignore - val eventType = data["type"] if (eventType == null) { //Just add a generic unknown event @@ -214,10 +213,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { ) notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent) notificationDrawerManager.refreshNotificationDrawer() - - return } else { - val event = parseEvent(data) ?: return val notifiableEvent = notifiableEventResolver.resolveEvent(event, session) @@ -228,8 +224,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { Timber.e("--> ${event}") } } else { - - if (notifiableEvent is NotifiableMessageEvent) { if (TextUtils.isEmpty(notifiableEvent.senderName)) { notifiableEvent.senderName = data["sender_display_name"] @@ -246,7 +240,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { notificationDrawerManager.refreshNotificationDrawer() } } - } private fun findRoomNameBestEffort(data: Map, session: Session?): String? { diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/riotx/features/notifications/InviteNotifiableEvent.kt index 10add07a66..5ecc66ffd4 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/InviteNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/InviteNotifiableEvent.kt @@ -32,6 +32,7 @@ data class InviteNotifiableEvent( override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { override var hasBeenDisplayed: Boolean = false + override var isRedacted: Boolean = false override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt index bc9c711d67..1603ea5f00 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt @@ -31,6 +31,7 @@ interface NotifiableEvent : Serializable { // Compat: Only for android <7, for newer version the sound is defined in the channel var soundName: String? var hasBeenDisplayed: Boolean + var isRedacted: Boolean //Used to know if event should be replaced with the one coming from eventstream var isPushGatewayEvent: Boolean } diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableMessageEvent.kt index 36af131130..5a3e989f18 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableMessageEvent.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableMessageEvent.kt @@ -36,6 +36,7 @@ data class NotifiableMessageEvent( override var soundName: String? = null override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC override var hasBeenDisplayed: Boolean = false + override var isRedacted: Boolean = false var roomAvatarPath: String? = null var senderAvatarPath: String? = null diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index 3ea4c970a0..da2fd5c91a 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -15,7 +15,6 @@ */ package im.vector.riotx.features.notifications -import android.app.Notification import android.content.Context import android.graphics.Bitmap import android.os.Handler @@ -96,7 +95,6 @@ class NotificationDrawerManager @Inject constructor(private val context: Context notifiableEvent.noisy = false eventList.remove(existing) eventList.add(notifiableEvent) - } else { //keep the existing one, do not replace } @@ -127,6 +125,15 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } } + fun onEventRedacted(eventId: String) { + synchronized(eventList) { + eventList.filter { it.eventId == eventId }.map { notifiableEvent -> + notifiableEvent.isRedacted = true + notifiableEvent.hasBeenDisplayed = false + } + } + } + /** Clear all known events and refresh the notification drawer */ @@ -215,20 +222,15 @@ class NotificationDrawerManager @Inject constructor(private val context: Context val summaryInboxStyle = NotificationCompat.InboxStyle() //group events by room to create a single MessagingStyle notif - val roomIdToEventMap: MutableMap> = HashMap() - val simpleEvents: ArrayList = ArrayList() - val notifications: ArrayList = ArrayList() + val roomIdToEventMap: MutableMap> = LinkedHashMap() + val simpleEvents: MutableList = ArrayList() val eventIterator = eventList.listIterator() while (eventIterator.hasNext()) { val event = eventIterator.next() if (event is NotifiableMessageEvent) { val roomId = event.roomId - var roomEvents = roomIdToEventMap[roomId] - if (roomEvents == null) { - roomEvents = ArrayList() - roomIdToEventMap[roomId] = roomEvents - } + val roomEvents = roomIdToEventMap.getOrPut(roomId) { ArrayList() } if (shouldIgnoreMessageEventInRoom(roomId) || outdatedDetector?.isMessageOutdated(event) == true) { //forget this event @@ -246,10 +248,10 @@ class NotificationDrawerManager @Inject constructor(private val context: Context var globalLastMessageTimestamp = 0L - //events have been grouped + //events have been grouped by roomId for ((roomId, events) in roomIdToEventMap) { - - if (events.isEmpty()) { + // Build the notification for the room + if (events.isEmpty() || events.all { it.isRedacted }) { //Just clear this notification Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId has no more events") NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID) @@ -280,7 +282,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context for (event in events) { //if all events in this room have already been displayed there is no need to update it - if (!event.hasBeenDisplayed) { + if (!event.hasBeenDisplayed && !event.isRedacted) { roomEventGroupInfo.shouldBing = roomEventGroupInfo.shouldBing || event.noisy roomEventGroupInfo.customSound = event.soundName } @@ -296,7 +298,9 @@ class NotificationDrawerManager @Inject constructor(private val context: Context style.addMessage(context.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) roomEventGroupInfo.hasSmartReplyError = true } else { - style.addMessage(event.body, event.timestamp, senderPerson) + if (!event.isRedacted) { + style.addMessage(event.body, event.timestamp, senderPerson) + } } event.hasBeenDisplayed = true //we can consider it as displayed @@ -356,7 +360,6 @@ class NotificationDrawerManager @Inject constructor(private val context: Context myUserDisplayName) ?.let { //is there an id for this room? - notifications.add(it) NotificationUtils.showNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID, it) } hasNewEvent = true @@ -372,7 +375,6 @@ class NotificationDrawerManager @Inject constructor(private val context: Context //We build a simple event if (firstTime || !event.hasBeenDisplayed) { NotificationUtils.buildSimpleEventNotification(context, vectorPreferences, event, null, session.myUserId)?.let { - notifications.add(it) NotificationUtils.showNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID, it) event.hasBeenDisplayed = true //we can consider it as displayed hasNewEvent = true @@ -396,7 +398,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context // To ensure the best experience on all devices and versions, always include a group summary when you create a group // https://developer.android.com/training/notify-user/group - if (eventList.isEmpty()) { + if (eventList.isEmpty() || eventList.all { it.isRedacted }) { NotificationUtils.cancelNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID) } else { val nbEvents = roomIdToEventMap.size + simpleEvents.size @@ -443,12 +445,11 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } } - private fun getRoomBitmap(events: ArrayList): Bitmap? { + private fun getRoomBitmap(events: List): Bitmap? { if (events.isEmpty()) return null //Use the last event (most recent?) - val roomAvatarPath = events.last().roomAvatarPath - ?: events.last().senderAvatarPath + val roomAvatarPath = events.last().roomAvatarPath ?: events.last().senderAvatarPath return bitmapLoader.getRoomBitmap(roomAvatarPath) } @@ -476,14 +477,14 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } } - private fun loadEventInfo(): ArrayList { + private fun loadEventInfo(): MutableList { try { val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) if (file.exists()) { FileInputStream(file).use { val events: ArrayList? = activeSessionHolder.getSafeActiveSession()?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE) if (events != null) { - return ArrayList(events.mapNotNull { it as? NotifiableEvent }) + return events.toMutableList() } } } diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt index 555c8737c5..6711d57a22 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt @@ -60,6 +60,10 @@ class PushRuleTriggerListener @Inject constructor( notificationDrawerManager.clearMessageEventOfRoom(roomId) } + override fun onEventRedacted(redactedEventId: String) { + notificationDrawerManager.onEventRedacted(redactedEventId) + } + override fun batchFinish() { notificationDrawerManager.refreshNotificationDrawer() } diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/riotx/features/notifications/SimpleNotifiableEvent.kt index abd7b706bc..66779cfe9a 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/SimpleNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/SimpleNotifiableEvent.kt @@ -30,6 +30,7 @@ data class SimpleNotifiableEvent( override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { override var hasBeenDisplayed: Boolean = false + override var isRedacted: Boolean = false override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC } From f5020d0f637bc9d7c7c0ad91d919d6d4b7ccf9ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Sep 2019 17:20:45 +0200 Subject: [PATCH 41/44] Daggerization and cleanup of NotificationUtils --- .../java/im/vector/riotx/VectorApplication.kt | 4 +- .../vector/riotx/core/di/VectorComponent.kt | 9 +- .../vector/riotx/core/services/CallService.kt | 12 +- .../im/vector/riotx/core/utils/SystemUtils.kt | 10 +- .../NotificationDrawerManager.kt | 61 ++--- .../notifications/NotificationUtils.kt | 247 ++++++++---------- ...sAdvancedNotificationPreferenceFragment.kt | 9 +- 7 files changed, 167 insertions(+), 185 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index cd4a97327e..bf56e71915 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -48,7 +48,6 @@ import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.notifications.PushRuleTriggerListener -import im.vector.riotx.features.rageshake.VectorFileLogger import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.version.VersionProvider @@ -73,6 +72,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. @Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var versionProvider: VersionProvider + @Inject lateinit var notificationUtils: NotificationUtils lateinit var vectorComponent: VectorComponent private var fontThreadHandler: Handler? = null @@ -112,7 +112,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. emojiCompatWrapper.init(fontRequest) - NotificationUtils.createNotificationChannels(applicationContext) + notificationUtils.createNotificationChannels() if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!! activeSessionHolder.setActiveSession(lastAuthenticatedSession) diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 0549222428..d755815595 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -36,10 +36,7 @@ import im.vector.riotx.features.home.HomeRoomListObservableStore import im.vector.riotx.features.home.group.SelectedGroupStore import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.navigation.Navigator -import im.vector.riotx.features.notifications.NotifiableEventResolver -import im.vector.riotx.features.notifications.NotificationBroadcastReceiver -import im.vector.riotx.features.notifications.NotificationDrawerManager -import im.vector.riotx.features.notifications.PushRuleTriggerListener +import im.vector.riotx.features.notifications.* import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.VectorFileLogger import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler @@ -58,6 +55,8 @@ interface VectorComponent { fun currentSession(): Session + fun notificationUtils(): NotificationUtils + fun notificationDrawerManager(): NotificationDrawerManager fun appContext(): Context @@ -72,7 +71,7 @@ interface VectorComponent { fun emojiCompatFontProvider(): EmojiCompatFontProvider - fun emojiCompatWrapper() : EmojiCompatWrapper + fun emojiCompatWrapper(): EmojiCompatWrapper fun eventHtmlRenderer(): EventHtmlRenderer diff --git a/vector/src/main/java/im/vector/riotx/core/services/CallService.kt b/vector/src/main/java/im/vector/riotx/core/services/CallService.kt index 92e9e40a55..fae7ae7246 100644 --- a/vector/src/main/java/im/vector/riotx/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/riotx/core/services/CallService.kt @@ -19,6 +19,7 @@ package im.vector.riotx.core.services import android.content.Context import android.content.Intent import androidx.core.content.ContextCompat +import im.vector.riotx.core.extensions.vectorComponent import im.vector.riotx.features.notifications.NotificationUtils import timber.log.Timber @@ -32,11 +33,18 @@ class CallService : VectorService() { */ private var mCallIdInProgress: String? = null + private lateinit var notificationUtils: NotificationUtils + /** * incoming (foreground notification) */ private var mIncomingCallId: String? = null + override fun onCreate() { + super.onCreate() + notificationUtils = vectorComponent().notificationUtils() + } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent == null) { // Service started again by the system. @@ -120,7 +128,7 @@ class CallService : VectorService() { private fun displayCallInProgressNotification(intent: Intent) { val callId = intent.getStringExtra(EXTRA_CALL_ID) - val notification = NotificationUtils.buildPendingCallNotification(applicationContext, + val notification = notificationUtils.buildPendingCallNotification( intent.getBooleanExtra(EXTRA_IS_VIDEO, false), intent.getStringExtra(EXTRA_ROOM_NAME), intent.getStringExtra(EXTRA_ROOM_ID), @@ -136,7 +144,7 @@ class CallService : VectorService() { * Hide the permanent call notifications */ private fun hideCallNotifications() { - val notification = NotificationUtils.buildCallEndedNotification(applicationContext) + val notification = notificationUtils.buildCallEndedNotification() // It's mandatory to startForeground to avoid crash startForeground(NOTIFICATION_ID, notification) diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt index 9c7b793825..75bc488571 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt @@ -18,11 +18,7 @@ package im.vector.riotx.core.utils import android.annotation.TargetApi import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent +import android.content.* import android.net.Uri import android.os.Build import android.os.PowerManager @@ -32,7 +28,7 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import im.vector.riotx.R -import im.vector.riotx.features.notifications.supportNotificationChannels +import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.settings.VectorLocale import timber.log.Timber import java.util.* @@ -138,7 +134,7 @@ fun startNotificationSettingsIntent(activity: AppCompatActivity, requestCode: In */ @TargetApi(Build.VERSION_CODES.O) fun startNotificationChannelSettingsIntent(fragment: Fragment, channelID: String) { - if (!supportNotificationChannels()) return + if (!NotificationUtils.supportNotificationChannels()) return val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { putExtra(Settings.EXTRA_APP_PACKAGE, fragment.context?.packageName) putExtra(Settings.EXTRA_CHANNEL_ID, channelID) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index da2fd5c91a..13a39b5828 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.settings.VectorPreferences import me.gujun.android.span.span import timber.log.Timber @@ -42,7 +43,9 @@ import javax.inject.Singleton */ @Singleton class NotificationDrawerManager @Inject constructor(private val context: Context, + private val notificationUtils: NotificationUtils, private val vectorPreferences: VectorPreferences, + private val stringProvider: StringProvider, private val activeSessionHolder: ActiveSessionHolder, private val iconLoader: IconLoader, private val bitmapLoader: BitmapLoader, @@ -154,7 +157,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context e is NotifiableMessageEvent && e.roomId == roomId } } - NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID) + notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID) } refreshNotificationDrawer() } @@ -254,7 +257,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context if (events.isEmpty() || events.all { it.isRedacted }) { //Just clear this notification Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId has no more events") - NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID) + notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID) continue } @@ -295,7 +298,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context .build() if (event.outGoingMessage && event.outGoingMessageFailed) { - style.addMessage(context.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) + style.addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) roomEventGroupInfo.hasSmartReplyError = true } else { if (!event.isRedacted) { @@ -306,7 +309,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context //It is possible that this event was previously shown as an 'anonymous' simple notif. //And now it will be merged in a single MessageStyle notif, so we can clean to be sure - NotificationUtils.cancelNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID) + notificationUtils.cancelNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID) } try { @@ -332,7 +335,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context summaryInboxStyle.addLine(line) } } else { - val summaryLine = context.resources.getQuantityString( + val summaryLine = stringProvider.getQuantityString( R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size) summaryInboxStyle.addLine(summaryLine) } @@ -351,17 +354,16 @@ class NotificationDrawerManager @Inject constructor(private val context: Context globalLastMessageTimestamp = lastMessageTimestamp } - NotificationUtils.buildMessagesListNotification(context, - vectorPreferences, + val notification = notificationUtils.buildMessagesListNotification( style, roomEventGroupInfo, largeBitmap, lastMessageTimestamp, myUserDisplayName) - ?.let { - //is there an id for this room? - NotificationUtils.showNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID, it) - } + + //is there an id for this room? + notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification) + hasNewEvent = true summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing } else { @@ -374,13 +376,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context for (event in simpleEvents) { //We build a simple event if (firstTime || !event.hasBeenDisplayed) { - NotificationUtils.buildSimpleEventNotification(context, vectorPreferences, event, null, session.myUserId)?.let { - NotificationUtils.showNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID, it) - event.hasBeenDisplayed = true //we can consider it as displayed - hasNewEvent = true - summaryIsNoisy = summaryIsNoisy || event.noisy - summaryInboxStyle.addLine(event.description) - } + val notification = notificationUtils.buildSimpleEventNotification(event, null, session.myUserId) + notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification) + event.hasBeenDisplayed = true //we can consider it as displayed + hasNewEvent = true + summaryIsNoisy = summaryIsNoisy || event.noisy + summaryInboxStyle.addLine(event.description) } } @@ -399,27 +400,21 @@ class NotificationDrawerManager @Inject constructor(private val context: Context // https://developer.android.com/training/notify-user/group if (eventList.isEmpty() || eventList.all { it.isRedacted }) { - NotificationUtils.cancelNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID) + notificationUtils.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) } else { val nbEvents = roomIdToEventMap.size + simpleEvents.size - val sumTitle = context.resources.getQuantityString( - R.plurals.notification_compat_summary_title, nbEvents, nbEvents) + val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) summaryInboxStyle.setBigContentTitle(sumTitle) //TODO get latest event? - .setSummaryText( - context.resources - .getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) + .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) - NotificationUtils.buildSummaryListNotification( - context, - vectorPreferences, + val notification = notificationUtils.buildSummaryListNotification( summaryInboxStyle, sumTitle, noisy = hasNewEvent && summaryIsNoisy, - lastMessageTimestamp = globalLastMessageTimestamp - )?.let { - NotificationUtils.showNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID, it) - } + lastMessageTimestamp = globalLastMessageTimestamp) + + notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification) if (hasNewEvent && summaryIsNoisy) { try { @@ -462,7 +457,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context fun persistInfo() { synchronized(eventList) { if (eventList.isEmpty()) { - deleteCachedRoomNotifications(context) + deleteCachedRoomNotifications() return } try { @@ -494,7 +489,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context return ArrayList() } - private fun deleteCachedRoomNotifications(context: Context) { + private fun deleteCachedRoomNotifications() { val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) if (file.exists()) { file.delete() diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt index 4d7a3edfd8..2a4566d02c 100755 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt @@ -27,7 +27,6 @@ import android.content.Intent import android.graphics.Bitmap import android.net.Uri import android.os.Build -import android.text.TextUtils import androidx.annotation.StringRes import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -37,6 +36,7 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import im.vector.riotx.BuildConfig import im.vector.riotx.R +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.startNotificationChannelSettingsIntent import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.room.detail.RoomDetailActivity @@ -44,51 +44,73 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.settings.VectorPreferences import timber.log.Timber import java.util.* +import javax.inject.Inject +import javax.inject.Singleton -fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - /** * Util class for creating notifications. + * Note: Cannot inject ColorProvider in the constructor, because it requires an Activity */ -object NotificationUtils { +@Singleton +class NotificationUtils @Inject constructor(private val context: Context, + private val stringProvider: StringProvider, + private val vectorPreferences: VectorPreferences) { - /* ========================================================================================== - * IDs for notifications - * ========================================================================================== */ + companion object { + /* ========================================================================================== + * IDs for notifications + * ========================================================================================== */ - /** - * Identifier of the foreground notification used to keep the application alive - * when it runs in background. - * This notification, which is not removable by the end user, displays what - * the application is doing while in background. - */ - const val NOTIFICATION_ID_FOREGROUND_SERVICE = 61 + /** + * Identifier of the foreground notification used to keep the application alive + * when it runs in background. + * This notification, which is not removable by the end user, displays what + * the application is doing while in background. + */ + const val NOTIFICATION_ID_FOREGROUND_SERVICE = 61 - /* ========================================================================================== - * IDs for actions - * ========================================================================================== */ + /* ========================================================================================== + * IDs for actions + * ========================================================================================== */ - const val JOIN_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.JOIN_ACTION" - const val REJECT_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.REJECT_ACTION" - private const val QUICK_LAUNCH_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.QUICK_LAUNCH_ACTION" - const val MARK_ROOM_READ_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.MARK_ROOM_READ_ACTION" - const val SMART_REPLY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.SMART_REPLY_ACTION" - const val DISMISS_SUMMARY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_SUMMARY_ACTION" - const val DISMISS_ROOM_NOTIF_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_ROOM_NOTIF_ACTION" - private const val TAP_TO_VIEW_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.TAP_TO_VIEW_ACTION" + const val JOIN_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.JOIN_ACTION" + const val REJECT_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.REJECT_ACTION" + private const val QUICK_LAUNCH_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.QUICK_LAUNCH_ACTION" + const val MARK_ROOM_READ_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.MARK_ROOM_READ_ACTION" + const val SMART_REPLY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.SMART_REPLY_ACTION" + const val DISMISS_SUMMARY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_SUMMARY_ACTION" + const val DISMISS_ROOM_NOTIF_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_ROOM_NOTIF_ACTION" + private const val TAP_TO_VIEW_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.TAP_TO_VIEW_ACTION" - /* ========================================================================================== - * IDs for channels - * ========================================================================================== */ + /* ========================================================================================== + * IDs for channels + * ========================================================================================== */ - // on devices >= android O, we need to define a channel for each notifications - private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID" + // on devices >= android O, we need to define a channel for each notifications + private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID" - private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID" + private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID" - private const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" - private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2" + private const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" + private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2" + + fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + + fun openSystemSettingsForSilentCategory(fragment: Fragment) { + startNotificationChannelSettingsIntent(fragment, SILENT_NOTIFICATION_CHANNEL_ID) + } + + fun openSystemSettingsForNoisyCategory(fragment: Fragment) { + startNotificationChannelSettingsIntent(fragment, NOISY_NOTIFICATION_CHANNEL_ID) + } + + fun openSystemSettingsForCallCategory(fragment: Fragment) { + startNotificationChannelSettingsIntent(fragment, CALL_NOTIFICATION_CHANNEL_ID) + } + } + + private val notificationManager = NotificationManagerCompat.from(context) /* ========================================================================================== * Channel names @@ -96,17 +118,13 @@ object NotificationUtils { /** * Create notification channels. - * - * @param context the context */ @TargetApi(Build.VERSION_CODES.O) - fun createNotificationChannels(context: Context) { + fun createNotificationChannels() { if (!supportNotificationChannels()) { return } - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) //Migration - the noisy channel was deleted and recreated when sound preference was changed (id was DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE @@ -132,10 +150,10 @@ object NotificationUtils { * intrude. */ notificationManager.createNotificationChannel(NotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID, - context.getString(R.string.notification_noisy_notifications), + stringProvider.getString(R.string.notification_noisy_notifications), NotificationManager.IMPORTANCE_DEFAULT) .apply { - description = context.getString(R.string.notification_noisy_notifications) + description = stringProvider.getString(R.string.notification_noisy_notifications) enableVibration(true) enableLights(true) lightColor = accentColor @@ -145,29 +163,29 @@ object NotificationUtils { * Low notification importance: shows everywhere, but is not intrusive. */ notificationManager.createNotificationChannel(NotificationChannel(SILENT_NOTIFICATION_CHANNEL_ID, - context.getString(R.string.notification_silent_notifications), + stringProvider.getString(R.string.notification_silent_notifications), NotificationManager.IMPORTANCE_LOW) .apply { - description = context.getString(R.string.notification_silent_notifications) + description = stringProvider.getString(R.string.notification_silent_notifications) setSound(null, null) enableLights(true) lightColor = accentColor }) notificationManager.createNotificationChannel(NotificationChannel(LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID, - context.getString(R.string.notification_listening_for_events), + stringProvider.getString(R.string.notification_listening_for_events), NotificationManager.IMPORTANCE_MIN) .apply { - description = context.getString(R.string.notification_listening_for_events) + description = stringProvider.getString(R.string.notification_listening_for_events) setSound(null, null) setShowBadge(false) }) notificationManager.createNotificationChannel(NotificationChannel(CALL_NOTIFICATION_CHANNEL_ID, - context.getString(R.string.call), + stringProvider.getString(R.string.call), NotificationManager.IMPORTANCE_HIGH) .apply { - description = context.getString(R.string.call) + description = stringProvider.getString(R.string.call) setSound(null, null) enableLights(true) lightColor = accentColor @@ -177,12 +195,11 @@ object NotificationUtils { /** * Build a polling thread listener notification * - * @param context Android context * @param subTitleResId subtitle string resource Id of the notification * @return the polling thread listener notification */ @SuppressLint("NewApi") - fun buildForegroundServiceNotification(context: Context, @StringRes subTitleResId: Int, withProgress: Boolean = true): Notification { + fun buildForegroundServiceNotification(@StringRes subTitleResId: Int, withProgress: Boolean = true): Notification { // build the pending intent go to the home screen if this is clicked. val i = Intent(context, HomeActivity::class.java) i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP @@ -191,7 +208,7 @@ object NotificationUtils { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val builder = NotificationCompat.Builder(context, LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID) - .setContentTitle(context.getString(subTitleResId)) + .setContentTitle(stringProvider.getString(subTitleResId)) .setSmallIcon(R.drawable.sync) .setCategory(NotificationCompat.CATEGORY_SERVICE) .setColor(accentColor) @@ -225,7 +242,7 @@ object NotificationUtils { CharSequence::class.java, CharSequence::class.java, PendingIntent::class.java) - deprecatedMethod.invoke(notification, context, context.getString(R.string.app_name), context.getString(subTitleResId), pi) + deprecatedMethod.invoke(notification, context, stringProvider.getString(R.string.app_name), stringProvider.getString(subTitleResId), pi) } catch (ex: Exception) { Timber.e(ex, "## buildNotification(): Exception - setLatestEventInfo() Msg=") } @@ -238,7 +255,6 @@ object NotificationUtils { * Build an incoming call notification. * This notification starts the VectorHomeActivity which is in charge of centralizing the incoming call flow. * - * @param context the context. * @param isVideo true if this is a video call, false for voice call * @param roomName the room name in which the call is pending. * @param matrixId the matrix id @@ -246,20 +262,19 @@ object NotificationUtils { * @return the call notification. */ @SuppressLint("NewApi") - fun buildIncomingCallNotification(context: Context, - isVideo: Boolean, + fun buildIncomingCallNotification(isVideo: Boolean, roomName: String, matrixId: String, callId: String): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) - .setContentTitle(ensureTitleNotEmpty(context, roomName)) + .setContentTitle(ensureTitleNotEmpty(roomName)) .apply { if (isVideo) { - setContentText(context.getString(R.string.incoming_video_call)) + setContentText(stringProvider.getString(R.string.incoming_video_call)) } else { - setContentText(context.getString(R.string.incoming_voice_call)) + setContentText(stringProvider.getString(R.string.incoming_voice_call)) } } .setSmallIcon(R.drawable.incoming_call_notification_transparent) @@ -295,7 +310,6 @@ object NotificationUtils { /** * Build a pending call notification * - * @param context the context. * @param isVideo true if this is a video call, false for voice call * @param roomName the room name in which the call is pending. * @param roomId the room Id @@ -304,20 +318,19 @@ object NotificationUtils { * @return the call notification. */ @SuppressLint("NewApi") - fun buildPendingCallNotification(context: Context, - isVideo: Boolean, + fun buildPendingCallNotification(isVideo: Boolean, roomName: String, roomId: String, matrixId: String, callId: String): Notification { val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) - .setContentTitle(ensureTitleNotEmpty(context, roomName)) + .setContentTitle(ensureTitleNotEmpty(roomName)) .apply { if (isVideo) { - setContentText(context.getString(R.string.video_call_in_progress)) + setContentText(stringProvider.getString(R.string.video_call_in_progress)) } else { - setContentText(context.getString(R.string.call_in_progress)) + setContentText(stringProvider.getString(R.string.call_in_progress)) } } .setSmallIcon(R.drawable.incoming_call_notification_transparent) @@ -355,9 +368,9 @@ object NotificationUtils { /** * Build a temporary (because service will be stopped just after) notification for the CallService, when a call is ended */ - fun buildCallEndedNotification(context: Context): Notification { + fun buildCallEndedNotification(): Notification { return NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) - .setContentTitle(context.getString(R.string.call_ended)) + .setContentTitle(stringProvider.getString(R.string.call_ended)) .setSmallIcon(R.drawable.ic_material_call_end_grey) .setCategory(NotificationCompat.CATEGORY_CALL) .build() @@ -366,17 +379,14 @@ object NotificationUtils { /** * Build a notification for a Room */ - fun buildMessagesListNotification(context: Context, - vectorPreferences: VectorPreferences, - messageStyle: NotificationCompat.MessagingStyle, + fun buildMessagesListNotification(messageStyle: NotificationCompat.MessagingStyle, roomInfo: RoomEventGroupInfo, largeIcon: Bitmap?, lastMessageTimestamp: Long, - senderDisplayNameForReplyCompat: String?): Notification? { - + senderDisplayNameForReplyCompat: String?): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) // Build the pending intent for when the notification is clicked - val openRoomIntent = buildOpenRoomIntent(context, roomInfo.roomId) + val openRoomIntent = buildOpenRoomIntent(roomInfo.roomId) val smallIcon = R.drawable.ic_status_bar val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID @@ -393,18 +403,15 @@ object NotificationUtils { // Title for API < 16 devices. .setContentTitle(roomInfo.roomDisplayName) // Content for API < 16 devices. - .setContentText(context.getString(R.string.notification_new_messages)) + .setContentText(stringProvider.getString(R.string.notification_new_messages)) // Number of new notifications for API <24 (M and below) devices. - .setSubText(context - .resources - .getQuantityString(R.plurals.room_new_messages_notification, messageStyle.messages.size, messageStyle.messages.size) - ) + .setSubText(stringProvider.getQuantityString(R.plurals.room_new_messages_notification, messageStyle.messages.size, messageStyle.messages.size)) // Auto-bundling is enabled for 4 or more notifications on API 24+ (N+) // devices and all Wear devices. But we want a custom grouping, so we specify the groupID // TODO Group should be current user display name - .setGroup(context.getString(R.string.app_name)) + .setGroup(stringProvider.getString(R.string.app_name)) //In order to avoid notification making sound twice (due to the summary notification) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) @@ -440,17 +447,17 @@ object NotificationUtils { addAction(NotificationCompat.Action( R.drawable.ic_material_done_all_white, - context.getString(R.string.action_mark_room_read), + stringProvider.getString(R.string.action_mark_room_read), markRoomReadPendingIntent)) // Quick reply if (!roomInfo.hasSmartReplyError) { - buildQuickReplyIntent(context, roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent -> + buildQuickReplyIntent(roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent -> val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY) - .setLabel(context.getString(R.string.action_quick_reply)) + .setLabel(stringProvider.getString(R.string.action_quick_reply)) .build() NotificationCompat.Action.Builder(R.drawable.vector_notification_quick_reply, - context.getString(R.string.action_quick_reply), replyPendingIntent) + stringProvider.getString(R.string.action_quick_reply), replyPendingIntent) .addRemoteInput(remoteInput) .build()?.let { addAction(it) @@ -477,11 +484,9 @@ object NotificationUtils { } - fun buildSimpleEventNotification(context: Context, - vectorPreferences: VectorPreferences, - simpleNotifiableEvent: NotifiableEvent, + fun buildSimpleEventNotification(simpleNotifiableEvent: NotifiableEvent, largeIcon: Bitmap?, - matrixId: String): Notification? { + matrixId: String): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) // Build the pending intent for when the notification is clicked val smallIcon = R.drawable.ic_status_bar @@ -489,9 +494,9 @@ object NotificationUtils { val channelID = if (simpleNotifiableEvent.noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID return NotificationCompat.Builder(context, channelID) - .setContentTitle(context.getString(R.string.app_name)) + .setContentTitle(stringProvider.getString(R.string.app_name)) .setContentText(simpleNotifiableEvent.description) - .setGroup(context.getString(R.string.app_name)) + .setGroup(stringProvider.getString(R.string.app_name)) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) .setSmallIcon(smallIcon) .setColor(accentColor) @@ -508,7 +513,7 @@ object NotificationUtils { addAction( R.drawable.vector_notification_reject_invitation, - context.getString(R.string.reject), + stringProvider.getString(R.string.reject), rejectIntentPendingIntent) // offer to type a quick accept button @@ -520,7 +525,7 @@ object NotificationUtils { PendingIntent.FLAG_UPDATE_CURRENT) addAction( R.drawable.vector_notification_accept_invitation, - context.getString(R.string.join), + stringProvider.getString(R.string.join), joinIntentPendingIntent) } else { setAutoCancel(true) @@ -551,7 +556,7 @@ object NotificationUtils { .build() } - private fun buildOpenRoomIntent(context: Context, roomId: String): PendingIntent? { + private fun buildOpenRoomIntent(roomId: String): PendingIntent? { val roomIntentTap = RoomDetailActivity.newIntent(context, RoomDetailArgs(roomId)) roomIntentTap.action = TAP_TO_VIEW_ACTION //pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that @@ -564,7 +569,7 @@ object NotificationUtils { .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) } - private fun buildOpenHomePendingIntentForSummary(context: Context): PendingIntent { + private fun buildOpenHomePendingIntentForSummary(): PendingIntent { val intent = HomeActivity.newIntent(context, clearNotification = true) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP intent.data = Uri.parse("foobar://tapSummary") @@ -578,7 +583,7 @@ object NotificationUtils { However, for Android devices running Marshmallow and below (API level 23 and below), it will be more appropriate to use an activity. Since you have to provide your own UI. */ - private fun buildQuickReplyIntent(context: Context, roomId: String, senderName: String?): PendingIntent? { + private fun buildQuickReplyIntent(roomId: String, senderName: String?): PendingIntent? { val intent: Intent if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { intent = Intent(context, NotificationBroadcastReceiver::class.java) @@ -610,12 +615,10 @@ object NotificationUtils { /** * Build the summary notification */ - fun buildSummaryListNotification(context: Context, - vectorPreferences: VectorPreferences, - style: NotificationCompat.InboxStyle, + fun buildSummaryListNotification(style: NotificationCompat.InboxStyle, compatSummary: String, noisy: Boolean, - lastMessageTimestamp: Long): Notification? { + lastMessageTimestamp: Long): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val smallIcon = R.drawable.ic_status_bar @@ -623,12 +626,12 @@ object NotificationUtils { // used in compat < N, after summary is built based on child notifications .setWhen(lastMessageTimestamp) .setStyle(style) - .setContentTitle(context.getString(R.string.app_name)) + .setContentTitle(stringProvider.getString(R.string.app_name)) .setCategory(NotificationCompat.CATEGORY_MESSAGE) .setSmallIcon(smallIcon) //set content text to support devices running API level < 24 .setContentText(compatSummary) - .setGroup(context.getString(R.string.app_name)) + .setGroup(stringProvider.getString(R.string.app_name)) //set this notification as the summary for the group .setGroupSummary(true) .setColor(accentColor) @@ -645,13 +648,12 @@ object NotificationUtils { priority = NotificationCompat.PRIORITY_LOW } } - .setContentIntent(buildOpenHomePendingIntentForSummary(context)) - .setDeleteIntent(getDismissSummaryPendingIntent(context)) + .setContentIntent(buildOpenHomePendingIntentForSummary()) + .setDeleteIntent(getDismissSummaryPendingIntent()) .build() - } - private fun getDismissSummaryPendingIntent(context: Context): PendingIntent { + private fun getDismissSummaryPendingIntent(): PendingIntent { val intent = Intent(context, NotificationBroadcastReceiver::class.java) intent.action = DISMISS_SUMMARY_ACTION intent.data = Uri.parse("foobar://deleteSummary") @@ -659,33 +661,28 @@ object NotificationUtils { 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } - fun showNotificationMessage(context: Context, tag: String?, id: Int, notification: Notification) { - with(NotificationManagerCompat.from(context)) { - notify(tag, id, notification) - } + fun showNotificationMessage(tag: String?, id: Int, notification: Notification) { + notificationManager.notify(tag, id, notification) } - fun cancelNotificationMessage(context: Context, tag: String?, id: Int) { - NotificationManagerCompat.from(context) - .cancel(tag, id) + fun cancelNotificationMessage(tag: String?, id: Int) { + notificationManager.cancel(tag, id) } /** * Cancel the foreground notification service */ - fun cancelNotificationForegroundService(context: Context) { - NotificationManagerCompat.from(context) - .cancel(NOTIFICATION_ID_FOREGROUND_SERVICE) + fun cancelNotificationForegroundService() { + notificationManager.cancel(NOTIFICATION_ID_FOREGROUND_SERVICE) } /** * Cancel all the notification */ - fun cancelAllNotifications(context: Context) { + fun cancelAllNotifications() { // Keep this try catch (reported by GA) try { - NotificationManagerCompat.from(context) - .cancelAll() + notificationManager.cancelAll() } catch (e: Exception) { Timber.e(e, "## cancelAllNotifications() failed " + e.message) } @@ -694,7 +691,7 @@ object NotificationUtils { /** * Return true it the user has enabled the do not disturb mode */ - fun isDoNotDisturbModeOn(context: Context): Boolean { + fun isDoNotDisturbModeOn(): Boolean { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return false } @@ -706,23 +703,11 @@ object NotificationUtils { || setting == NotificationManager.INTERRUPTION_FILTER_ALARMS } - private fun ensureTitleNotEmpty(context: Context, title: String?): CharSequence { - if (TextUtils.isEmpty(title)) { - return context.getString(R.string.app_name) + private fun ensureTitleNotEmpty(title: String?): CharSequence { + if (title.isNullOrBlank()) { + return stringProvider.getString(R.string.app_name) } - return title!! - } - - fun openSystemSettingsForSilentCategory(fragment: Fragment) { - startNotificationChannelSettingsIntent(fragment, SILENT_NOTIFICATION_CHANNEL_ID) - } - - fun openSystemSettingsForNoisyCategory(fragment: Fragment) { - startNotificationChannelSettingsIntent(fragment, NOISY_NOTIFICATION_CHANNEL_ID) - } - - fun openSystemSettingsForCallCategory(fragment: Fragment) { - startNotificationChannelSettingsIntent(fragment, CALL_NOTIFICATION_CHANNEL_ID) + return title } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt index cdd489a9ed..ec70a126cc 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt @@ -29,7 +29,6 @@ import im.vector.riotx.core.extensions.withArgs import im.vector.riotx.core.preference.BingRule import im.vector.riotx.core.preference.BingRulePreference import im.vector.riotx.features.notifications.NotificationUtils -import im.vector.riotx.features.notifications.supportNotificationChannels import javax.inject.Inject class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseFragment() { @@ -56,7 +55,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF override fun bindPref() { val callNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY) - if (supportNotificationChannels()) { + if (NotificationUtils.supportNotificationChannels()) { callNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener { NotificationUtils.openSystemSettingsForCallCategory(this) false @@ -66,7 +65,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF } val noisyNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY) - if (supportNotificationChannels()) { + if (NotificationUtils.supportNotificationChannels()) { noisyNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener { NotificationUtils.openSystemSettingsForNoisyCategory(this) false @@ -76,7 +75,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF } val silentNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY) - if (supportNotificationChannels()) { + if (NotificationUtils.supportNotificationChannels()) { silentNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener { NotificationUtils.openSystemSettingsForSilentCategory(this) false @@ -89,7 +88,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF // Ringtone val ringtonePreference = findPreference(VectorPreferences.SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY) - if (supportNotificationChannels()) { + if (NotificationUtils.supportNotificationChannels()) { ringtonePreference.isVisible = false } else { ringtonePreference.summary = vectorPreferences.getNotificationRingToneName() From 36866dd24e8c4ce3d5288baadcdea8d46a29e4d2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Sep 2019 15:14:17 +0200 Subject: [PATCH 42/44] Save draft of a message when exiting a room with non empty composer (#329) --- CHANGES.md | 2 +- .../main/java/im/vector/matrix/rx/RxRoom.kt | 5 + .../matrix/android/api/session/room/Room.kt | 2 + .../api/session/room/model/RoomSummary.kt | 4 +- .../api/session/room/send/DraftService.kt | 39 ++++ .../api/session/room/send/UserDraft.kt | 38 ++++ .../internal/database/mapper/DraftMapper.kt | 45 +++++ .../database/mapper/RoomSummaryMapper.kt | 5 +- .../internal/database/model/DraftEntity.kt | 34 ++++ .../database/model/RoomSummaryEntity.kt | 3 +- .../database/model/SessionRealmModule.kt | 4 +- .../database/model/UserDraftsEntity.kt | 36 ++++ .../database/query/UserDraftsEntityQueries.kt | 33 ++++ .../internal/session/room/DefaultRoom.kt | 3 + .../internal/session/room/RoomFactory.kt | 3 + .../session/room/draft/DefaultDraftService.kt | 166 ++++++++++++++++ .../session/room/send/DefaultSendService.kt | 29 ++- vector/build.gradle | 5 +- .../home/room/detail/RoomDetailActions.kt | 10 +- .../home/room/detail/RoomDetailFragment.kt | 116 ++++++----- .../home/room/detail/RoomDetailViewModel.kt | 187 ++++++++++++------ .../home/room/detail/RoomDetailViewState.kt | 12 +- .../home/room/list/RoomSummaryItem.kt | 3 + .../home/room/list/RoomSummaryItemFactory.kt | 1 + vector/src/main/res/layout/item_room.xml | 22 ++- 25 files changed, 667 insertions(+), 140 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserDraft.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/DraftMapper.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/DraftEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserDraftsEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UserDraftsEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt diff --git a/CHANGES.md b/CHANGES.md index 75b11bdbd4..63ccaea83a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ Changes in RiotX 0.6.0 (2019-XX-XX) =================================================== Features: - - + - Save draft of a message when exiting a room with non empty composer (#329) Improvements: - Add unread indent on room list (#485) diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 0ff0987dfe..28a3d40070 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import io.reactivex.Observable import io.reactivex.Single @@ -54,6 +55,10 @@ class RxRoom(private val room: Room) { return room.getEventReadReceiptsLive(eventId).asObservable() } + fun liveDrafts(): Observable> { + return room.getDraftsLive().asObservable() + } + } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index ec6b382f8f..92414eb768 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.read.ReadService +import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.timeline.TimelineService @@ -32,6 +33,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineService interface Room : TimelineService, SendService, + DraftService, ReadService, MembershipService, StateService, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index e4bf1bd32b..099deae937 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.model import im.vector.matrix.android.api.session.room.model.tag.RoomTag +import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent /** @@ -36,7 +37,8 @@ data class RoomSummary( val hasUnreadMessages: Boolean = false, val tags: List = emptyList(), val membership: Membership = Membership.NONE, - val versioningState: VersioningState = VersioningState.NONE + val versioningState: VersioningState = VersioningState.NONE, + val userDrafts: List = emptyList() ) { val isVersioned: Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt new file mode 100644 index 0000000000..c700b40a08 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.send + +import androidx.lifecycle.LiveData + +interface DraftService { + + /** + * Save or update a draft to the room + */ + fun saveDraft(draft: UserDraft) + + /** + * Delete the last draft, basically just after sending the message + */ + fun deleteDraft() + + /** + * Return the current drafts if any, as a live data + * The draft list can contain one draft for {regular, reply, quote} and an arbitrary number of {edit} drafts + */ + fun getDraftsLive(): LiveData> + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserDraft.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserDraft.kt new file mode 100644 index 0000000000..8912cc2580 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserDraft.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.send + +/** + * Describes a user draft: + * REGULAR: draft of a classical message + * QUOTE: draft of a message which quotes another message + * EDIT: draft of an edition of a message + * REPLY: draft of a reply of another message + */ +sealed class UserDraft(open val text: String) { + data class REGULAR(override val text: String) : UserDraft(text) + data class QUOTE(val linkedEventId: String, override val text: String) : UserDraft(text) + data class EDIT(val linkedEventId: String, override val text: String) : UserDraft(text) + data class REPLY(val linkedEventId: String, override val text: String) : UserDraft(text) + + fun isValid(): Boolean { + return when (this) { + is REGULAR -> text.isNotBlank() + else -> true + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/DraftMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/DraftMapper.kt new file mode 100644 index 0000000000..6b87951e0a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/DraftMapper.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.mapper + +import im.vector.matrix.android.api.session.room.send.UserDraft +import im.vector.matrix.android.internal.database.model.DraftEntity + +/** + * DraftEntity <-> UserDraft + */ +internal object DraftMapper { + + fun map(entity: DraftEntity): UserDraft { + return when (entity.draftMode) { + DraftEntity.MODE_REGULAR -> UserDraft.REGULAR(entity.content) + DraftEntity.MODE_EDIT -> UserDraft.EDIT(entity.linkedEventId, entity.content) + DraftEntity.MODE_QUOTE -> UserDraft.QUOTE(entity.linkedEventId, entity.content) + DraftEntity.MODE_REPLY -> UserDraft.REPLY(entity.linkedEventId, entity.content) + else -> null + } ?: UserDraft.REGULAR("") + } + + fun map(domain: UserDraft): DraftEntity { + return when (domain) { + is UserDraft.REGULAR -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "") + is UserDraft.EDIT -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId) + is UserDraft.QUOTE -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId) + is UserDraft.REPLY -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId) + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 8cb738807f..4fbe7fe04c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.tag.RoomTag import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import java.util.UUID +import java.util.* import javax.inject.Inject internal class RoomSummaryMapper @Inject constructor( @@ -67,7 +67,8 @@ internal class RoomSummaryMapper @Inject constructor( hasUnreadMessages = roomSummaryEntity.hasUnreadMessages, tags = tags, membership = roomSummaryEntity.membership, - versioningState = roomSummaryEntity.versioningState + versioningState = roomSummaryEntity.versioningState, + userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList() ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/DraftEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/DraftEntity.kt new file mode 100644 index 0000000000..9666ebd9a1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/DraftEntity.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.model + +import io.realm.RealmObject + +internal open class DraftEntity(var content: String = "", + var draftMode: String = MODE_REGULAR, + var linkedEventId: String = "" + +) : RealmObject() { + + companion object { + const val MODE_REGULAR = "REGULAR" + const val MODE_EDIT = "EDIT" + const val MODE_REPLY = "REPLY" + const val MODE_QUOTE = "QUOTE" + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 95308d367e..1c159b23d2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -36,7 +36,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var notificationCount: Int = 0, var highlightCount: Int = 0, var hasUnreadMessages: Boolean = false, - var tags: RealmList = RealmList() + var tags: RealmList = RealmList(), + var userDrafts: UserDraftsEntity? = null ) : RealmObject() { private var membershipStr: String = Membership.NONE.name diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 1d27bf07ee..680e2eac7d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -43,6 +43,8 @@ import io.realm.annotations.RealmModule PushConditionEntity::class, PusherEntity::class, PusherDataEntity::class, - ReadReceiptsSummaryEntity::class + ReadReceiptsSummaryEntity::class, + UserDraftsEntity::class, + DraftEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserDraftsEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserDraftsEntity.kt new file mode 100644 index 0000000000..b713fe1c3f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserDraftsEntity.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects + +/** + * Create a specific table to be able to do direct query on it and keep the draft ordered + */ +internal open class UserDraftsEntity(var userDrafts: RealmList = RealmList() +) : RealmObject() { + + // Link to RoomSummaryEntity + @LinkingObjects("userDrafts") + val roomSummaryEntity: RealmResults? = null + + companion object + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UserDraftsEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UserDraftsEntityQueries.kt new file mode 100644 index 0000000000..ae368c5850 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UserDraftsEntityQueries.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.query + +import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields +import im.vector.matrix.android.internal.database.model.UserDraftsEntity +import im.vector.matrix.android.internal.database.model.UserDraftsEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun UserDraftsEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery { + val query = realm.where() + if (roomId != null) { + query.equalTo(UserDraftsEntityFields.ROOM_SUMMARY_ENTITY + "." + RoomSummaryEntityFields.ROOM_ID, roomId) + } + return query +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index 492dd03543..6b2a6843f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.read.ReadService +import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.timeline.TimelineService @@ -40,6 +41,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, private val roomSummaryMapper: RoomSummaryMapper, private val timelineService: TimelineService, private val sendService: SendService, + private val draftService: DraftService, private val stateService: StateService, private val readService: ReadService, private val cryptoService: CryptoService, @@ -48,6 +50,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, ) : Room, TimelineService by timelineService, SendService by sendService, + DraftService by draftService, StateService by stateService, ReadService by readService, RelationService by relationService, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 53da2d7709..e972f6a98e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper +import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.read.DefaultReadService import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService @@ -38,6 +39,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona private val cryptoService: CryptoService, private val timelineServiceFactory: DefaultTimelineService.Factory, private val sendServiceFactory: DefaultSendService.Factory, + private val draftServiceFactory: DefaultDraftService.Factory, private val stateServiceFactory: DefaultStateService.Factory, private val readServiceFactory: DefaultReadService.Factory, private val relationServiceFactory: DefaultRelationService.Factory, @@ -51,6 +53,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona roomSummaryMapper, timelineServiceFactory.create(roomId), sendServiceFactory.create(roomId), + draftServiceFactory.create(roomId), stateServiceFactory.create(roomId), readServiceFactory.create(roomId), cryptoService, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt new file mode 100644 index 0000000000..c5676f84c8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.draft + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.BuildConfig +import im.vector.matrix.android.api.session.room.send.DraftService +import im.vector.matrix.android.api.session.room.send.UserDraft +import im.vector.matrix.android.internal.database.RealmLiveData +import im.vector.matrix.android.internal.database.mapper.DraftMapper +import im.vector.matrix.android.internal.database.model.DraftEntity +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.model.UserDraftsEntity +import im.vector.matrix.android.internal.database.query.where +import io.realm.kotlin.createObject +import timber.log.Timber + +internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String, + private val monarchy: Monarchy +) : DraftService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): DraftService + } + + /** + * The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft, + * or even move an existing draft to the top of the list + */ + override fun saveDraft(draft: UserDraft) { + Timber.d("Draft: saveDraft ${privacySafe(draft)}") + + monarchy.writeAsync { realm -> + + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) + + val userDraftsEntity = roomSummaryEntity.userDrafts + ?: realm.createObject().also { + roomSummaryEntity.userDrafts = it + } + + userDraftsEntity.let { userDraftEntity -> + // Save only valid draft + if (draft.isValid()) { + // Add a new draft or update the current one? + val newDraft = DraftMapper.map(draft) + + // Is it an update of the top draft? + val topDraft = userDraftEntity.userDrafts.lastOrNull() + + if (topDraft == null) { + Timber.d("Draft: create a new draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } else if (topDraft.draftMode == DraftEntity.MODE_EDIT) { + // top draft is an edit + if (newDraft.draftMode == DraftEntity.MODE_EDIT) { + if (topDraft.linkedEventId == newDraft.linkedEventId) { + // Update the top draft + Timber.d("Draft: update the top edit draft ${privacySafe(draft)}") + topDraft.content = newDraft.content + } else { + // Check a previously EDIT draft with the same id + val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find { + it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId + } + + if (existingEditDraftOfSameEvent != null) { + // Ignore the new text, restore what was typed before, by putting the draft to the top + Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent) + userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent) + } else { + Timber.d("Draft: add a new edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } + } + } else { + // Add a new regular draft to the top + Timber.d("Draft: add a new draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } + } else { + // Top draft is not an edit + if (newDraft.draftMode == DraftEntity.MODE_EDIT) { + Timber.d("Draft: create a new edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } else { + // Update the top draft + Timber.d("Draft: update the top draft ${privacySafe(draft)}") + topDraft.draftMode = newDraft.draftMode + topDraft.content = newDraft.content + topDraft.linkedEventId = newDraft.linkedEventId + } + } + } else { + // There is no draft to save, so the composer was clear + Timber.d("Draft: delete a draft") + + val topDraft = userDraftEntity.userDrafts.lastOrNull() + + if (topDraft == null) { + Timber.d("Draft: nothing to do") + } else { + // Remove the top draft + Timber.d("Draft: remove the top draft") + userDraftEntity.userDrafts.remove(topDraft) + } + } + } + } + } + + private fun privacySafe(o: Any): Any { + if (BuildConfig.LOG_PRIVATE_DATA) { + return o + } + + return "" + } + + override fun deleteDraft() { + Timber.d("Draft: deleteDraft()") + + monarchy.writeAsync { realm -> + UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity -> + if (userDraftsEntity.userDrafts.isNotEmpty()) { + userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1) + } + } + } + } + + override fun getDraftsLive(): LiveData> { + val liveData = RealmLiveData(monarchy.realmConfiguration) { + UserDraftsEntity.where(it, roomId) + } + + return Transformations.map(liveData) { userDraftsEntities -> + userDraftsEntities.firstOrNull()?.let { userDraftEntity -> + userDraftEntity.userDrafts.map { draftEntity -> + DraftMapper.map(draftEntity) + } + } ?: emptyList() + } + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 2c20839b26..a97d03d734 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -17,33 +17,29 @@ package im.vector.matrix.android.internal.session.room.send import android.content.Context -import androidx.work.BackoffPolicy -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.Operation -import androidx.work.WorkManager +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import androidx.work.* import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.isImageMessage -import im.vector.matrix.android.api.session.events.model.isTextMessage -import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.events.model.* import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.CancelableBag +import im.vector.matrix.android.internal.database.RealmLiveData +import im.vector.matrix.android.internal.database.mapper.DraftMapper import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.content.UploadContentWorker @@ -75,6 +71,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private } private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() + override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable { val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also { saveLocalEcho(it) @@ -165,12 +162,10 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private override fun deleteFailedEcho(localEcho: TimelineEvent) { monarchy.writeAsync { realm -> - TimelineEventEntity.where(realm, eventId = localEcho.root.eventId - ?: "").findFirst()?.let { + TimelineEventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { it.deleteFromRealm() } - EventEntity.where(realm, eventId = localEcho.root.eventId - ?: "").findFirst()?.let { + EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { it.deleteFromRealm() } } diff --git a/vector/build.gradle b/vector/build.gradle index 349bf7cf88..697d0f36d0 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -252,8 +252,9 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.0' // RXBinding - implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0-alpha2' - implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0-alpha2' + implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0' + implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0' + implementation 'com.jakewharton.rxbinding3:rxbinding-material:3.0.0' implementation("com.airbnb.android:epoxy:$epoxy_version") kapt "com.airbnb.android:epoxy-processor:$epoxy_version" diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt index e60bc422a8..dda5f611b6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt @@ -18,13 +18,13 @@ package im.vector.riotx.features.home.room.detail import com.jaiselrahman.filepicker.model.MediaFile import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent sealed class RoomDetailActions { + data class SaveDraft(val draft: String) : RoomDetailActions() data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions() data class SendMedia(val mediaFiles: List) : RoomDetailActions() data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() @@ -35,13 +35,15 @@ sealed class RoomDetailActions { data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions() data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions() data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions() - data class HandleTombstoneEvent(val event: Event): RoomDetailActions() + data class HandleTombstoneEvent(val event: Event) : RoomDetailActions() object AcceptInvite : RoomDetailActions() object RejectInvite : RoomDetailActions() data class EnterEditMode(val eventId: String) : RoomDetailActions() - data class EnterQuoteMode(val eventId: String) : RoomDetailActions() - data class EnterReplyMode(val eventId: String) : RoomDetailActions() + data class EnterQuoteMode(val eventId: String, val draft: String) : RoomDetailActions() + data class EnterReplyMode(val eventId: String, val draft: String) : RoomDetailActions() + data class ExitSpecialMode(val draft: String) : RoomDetailActions() + data class ResendMessage(val eventId: String) : RoomDetailActions() data class RemoveFailedEcho(val eventId: String) : RoomDetailActions() object ClearSendQueue : RoomDetailActions() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index cd1ccb01d2..b32d08ea7d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -53,6 +53,7 @@ import com.google.android.material.snackbar.Snackbar import com.jaiselrahman.filepicker.activity.FilePickerActivity import com.jaiselrahman.filepicker.config.Configurations import com.jaiselrahman.filepicker.model.MediaFile +import com.jakewharton.rxbinding3.widget.afterTextChangeEvents import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy @@ -64,7 +65,6 @@ import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent -import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent @@ -107,6 +107,7 @@ import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.themes.ThemeUtils +import io.reactivex.rxkotlin.subscribeBy import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.merge_composer_layout.view.* @@ -114,6 +115,7 @@ import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import org.commonmark.parser.Parser import timber.log.Timber import java.io.File +import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -242,10 +244,10 @@ class RoomDetailFragment : roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode -> when (mode) { - SendMode.REGULAR -> exitSpecialMode() - is SendMode.EDIT -> enterSpecialMode(mode.timelineEvent, R.drawable.ic_edit, true) - is SendMode.QUOTE -> enterSpecialMode(mode.timelineEvent, R.drawable.ic_quote, false) - is SendMode.REPLY -> enterSpecialMode(mode.timelineEvent, R.drawable.ic_reply, false) + is SendMode.REGULAR -> renderRegularMode(mode.text) + is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, mode.text) + is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, mode.text) + is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, mode.text) } } @@ -300,14 +302,16 @@ class RoomDetailFragment : return super.onOptionsItemSelected(item) } - private fun exitSpecialMode() { + private fun renderRegularMode(text: String) { commandAutocompletePolicy.enabled = true composerLayout.collapse() + + updateComposerText(text) } - private fun enterSpecialMode(event: TimelineEvent, - @DrawableRes iconRes: Int, - useText: Boolean) { + private fun renderSpecialMode(event: TimelineEvent, + @DrawableRes iconRes: Int, + defaultContent: String) { commandAutocompletePolicy.enabled = false //switch to expanded bar composerLayout.composerRelatedMessageTitle.apply { @@ -321,19 +325,20 @@ class RoomDetailFragment : if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody - ?: messageContent.body) + ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } - composerLayout.composerRelatedMessageContent.text = formattedBody - ?: nonFormattedBody + composerLayout.composerRelatedMessageContent.text = formattedBody ?: nonFormattedBody + + updateComposerText(defaultContent) - composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "") composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) - avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + avatarRenderer.render(event.senderAvatar, + event.root.senderId ?: "", + event.senderName, + composerLayout.composerRelatedMessageAvatar) - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.expand { //need to do it here also when not using quick reply focusComposerAndShowKeyboard() @@ -341,6 +346,16 @@ class RoomDetailFragment : focusComposerAndShowKeyboard() } + private fun updateComposerText(text: String) { + // Do not update if this is the same text to avoid the cursor to move + if (text != composerLayout.composerEditText.text.toString()) { + // Ignore update to avoid saving a draft + filterComposerTextChange = true + composerLayout.composerEditText.setText(text) + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) + } + } + override fun onResume() { super.onResume() @@ -360,9 +375,9 @@ class RoomDetailFragment : REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -397,32 +412,46 @@ class RoomDetailFragment : if (vectorPreferences.swipeToReplyIsEnabled()) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) - } - } + R.drawable.ic_reply, + object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) + } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED - } - else -> false - } - } - }) + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } } + private var filterComposerTextChange = true + private fun setupComposer() { + composerLayout.composerEditText.afterTextChangeEvents() + .debounce(100, TimeUnit.MILLISECONDS) + .subscribeBy { + if (filterComposerTextChange) { + Timber.d("Draft: ignore text update") + filterComposerTextChange = false + return@subscribeBy + } + roomDetailViewModel.process(RoomDetailActions.SaveDraft(it.editable.toString())) + } + .disposeOnDestroy() + val elevation = 6f val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) Autocomplete.on(composerLayout.composerEditText) @@ -492,8 +521,7 @@ class RoomDetailFragment : } } composerLayout.composerRelatedMessageCloseButton.setOnClickListener { - composerLayout.composerEditText.setText("") - roomDetailViewModel.resetSendMode() + roomDetailViewModel.process(RoomDetailActions.ExitSpecialMode(composerLayout.composerEditText.text.toString())) } } @@ -645,13 +673,11 @@ class RoomDetailFragment : private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { when (sendMessageResult) { is SendMessageResult.MessageSent -> { - // Clear composer - composerLayout.composerEditText.text = null + // Nothing to do, the composer will be cleared with the draft update } is SendMessageResult.SlashCommandHandled -> { sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } - // Clear composer - composerLayout.composerEditText.text = null + // The composer will be cleared with the draft update } is SendMessageResult.SlashCommandError -> { displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) @@ -916,10 +942,10 @@ class RoomDetailFragment : roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId)) } is SimpleAction.Quote -> { - roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId)) + roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString())) } is SimpleAction.Reply -> { - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId)) + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId, composerLayout.composerEditText.text.toString())) } is SimpleAction.CopyPermalink -> { val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 8ec133c642..593a3dfd04 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -42,8 +42,9 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.rx.rx @@ -84,6 +85,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private var timeline = room.createTimeline(eventId, timelineSettings) + // Filter to avoid infinite loop when user enter text in the composer and call SaveDraft + private var filterDraftUpdate = false + // Slot to keep a pending action during permission request var pendingAction: RoomDetailActions? = null @@ -109,6 +113,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeRoomSummary() observeEventDisplayedActions() observeSummaryState() + observeDrafts() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } @@ -116,6 +121,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro fun process(action: RoomDetailActions) { when (action) { + is RoomDetailActions.SaveDraft -> handleSaveDraft(action) is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.SendMedia -> handleSendMedia(action) is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) @@ -129,6 +135,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailActions.EnterEditMode -> handleEditAction(action) is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action) is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) + is RoomDetailActions.ExitSpecialMode -> handleExitSpecialMode(action) is RoomDetailActions.DownloadFile -> handleDownloadFile(action) is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action) is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action) @@ -140,9 +147,64 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } + /** + * Convert a send mode to a draft and save the draft + */ + private fun handleSaveDraft(action: RoomDetailActions.SaveDraft) { + // The text is changed, ignore the next update from DB + filterDraftUpdate = true + + withState { + when (it.sendMode) { + is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft)) + is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + } + } + } + + private fun observeDrafts() { + room.rx().liveDrafts() + .subscribe { + Timber.d("Draft update!") + if (filterDraftUpdate) { + Timber.d(" --> Ignore") + return@subscribe + } + + Timber.d(" --> SetState") + + setState { + val draft = it.lastOrNull() ?: UserDraft.REGULAR("") + copy( + // Create a sendMode from a draft and retrieve the TimelineEvent + sendMode = when (draft) { + is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) + is UserDraft.QUOTE -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.QUOTE(timelineEvent, draft.text) + } + } + is UserDraft.REPLY -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.REPLY(timelineEvent, draft.text) + } + } + is UserDraft.EDIT -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.EDIT(timelineEvent, draft.text) + } + } + } ?: SendMode.REGULAR("") + ) + } + } + .disposeOnClear() + } + private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { - val tombstoneContent = action.event.getClearContent().toModel() - ?: return + val tombstoneContent = action.event.getClearContent().toModel() ?: return val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN @@ -166,22 +228,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } - private fun enterEditMode(event: TimelineEvent) { - setState { - copy( - sendMode = SendMode.EDIT(event) - ) - } - } - - fun resetSendMode() { - setState { - copy( - sendMode = SendMode.REGULAR - ) - } - } - private val _nonBlockingPopAlert = MutableLiveData>>>() val nonBlockingPopAlert: LiveData>>> get() = _nonBlockingPopAlert @@ -218,7 +264,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleSendMessage(action: RoomDetailActions.SendMessage) { withState { state -> when (state.sendMode) { - SendMode.REGULAR -> { + is SendMode.REGULAR -> { val slashCommandResult = CommandParser.parseSplashCommand(action.text) when (slashCommandResult) { @@ -226,6 +272,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro // Send the text message to the room room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) + popDraft() } is ParsedCommand.ErrorSyntax -> { _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command)) @@ -238,6 +285,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } is ParsedCommand.Invite -> { handleInviteSlashCommand(slashCommandResult) + popDraft() } is ParsedCommand.SetUserPowerLevel -> { // TODO @@ -251,6 +299,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro vectorPreferences.setMarkdownEnabled(slashCommandResult.enable) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled( if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) + popDraft() } is ParsedCommand.UnbanUser -> { // TODO @@ -275,9 +324,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is ParsedCommand.SendEmote -> { room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) + popDraft() } is ParsedCommand.ChangeTopic -> { handleChangeTopicSlashCommand(slashCommandResult) + popDraft() } is ParsedCommand.ChangeDisplayName -> { // TODO @@ -285,11 +336,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } } - is SendMode.EDIT -> { + is SendMode.EDIT -> { //is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { //TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -298,27 +349,24 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { - room.editTextMessage(state.sendMode.timelineEvent.root.eventId - ?: "", messageContent?.type - ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) + room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "", + messageContent?.type ?: MessageType.MSGTYPE_TEXT, + action.text, + action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } } - setState { - copy( - sendMode = SendMode.REGULAR - ) - } _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) + popDraft() } - is SendMode.QUOTE -> { + is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text) @@ -333,29 +381,25 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { room.sendFormattedTextMessage(finalText, htmlText) } - setState { - copy( - sendMode = SendMode.REGULAR - ) - } _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) + popDraft() } - is SendMode.REPLY -> { + is SendMode.REPLY -> { state.sendMode.timelineEvent.let { room.replyToMessage(it, action.text, action.autoMarkdown) - setState { - copy( - sendMode = SendMode.REGULAR - ) - } _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) + popDraft() } - } } } } + private fun popDraft() { + filterDraftUpdate = false + room.deleteDraft() + } + private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() var quotedTextMsg = StringBuilder() @@ -469,27 +513,56 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleEditAction(action: RoomDetailActions.EnterEditMode) { - room.getTimeLineEvent(action.eventId)?.let { - enterEditMode(it) + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + timelineEvent.root.eventId?.let { + filterDraftUpdate = false + room.saveDraft(UserDraft.EDIT(it, timelineEvent.getTextEditableContent() ?: "")) + } } } private fun handleQuoteAction(action: RoomDetailActions.EnterQuoteMode) { - room.getTimeLineEvent(action.eventId)?.let { - setState { - copy( - sendMode = SendMode.QUOTE(it) - ) + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + withState { state -> + // Save a new draft and keep the previously entered text, if it was not an edit + timelineEvent.root.eventId?.let { + filterDraftUpdate = false + if (state.sendMode is SendMode.EDIT) { + room.saveDraft(UserDraft.QUOTE(it, "")) + } else { + room.saveDraft(UserDraft.QUOTE(it, action.draft)) + } + } } } } private fun handleReplyAction(action: RoomDetailActions.EnterReplyMode) { - room.getTimeLineEvent(action.eventId)?.let { - setState { - copy( - sendMode = SendMode.REPLY(it) - ) + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + withState { state -> + // Save a new draft and keep the previously entered text, if it was not an edit + timelineEvent.root.eventId?.let { + filterDraftUpdate = false + if (state.sendMode is SendMode.EDIT) { + room.saveDraft(UserDraft.REPLY(it, "")) + } else { + room.saveDraft(UserDraft.REPLY(it, action.draft)) + } + } + } + } + } + + private fun handleExitSpecialMode(action: RoomDetailActions.ExitSpecialMode) { + withState { state -> + // For edit, just delete the current draft + filterDraftUpdate = false + + if (state.sendMode is SendMode.EDIT) { + room.deleteDraft() + } else { + // Save a new draft and keep the previously entered text + room.saveDraft(UserDraft.REGULAR(action.draft)) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index d8358efe16..a47ee56500 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -34,11 +34,11 @@ import im.vector.matrix.android.api.session.user.model.User * * Depending on the state the bottom toolbar will change (icons/preview/actions...) */ -sealed class SendMode { - object REGULAR : SendMode() - data class QUOTE(val timelineEvent: TimelineEvent) : SendMode() - data class EDIT(val timelineEvent: TimelineEvent) : SendMode() - data class REPLY(val timelineEvent: TimelineEvent) : SendMode() +sealed class SendMode(open val text: String) { + data class REGULAR(override val text: String) : SendMode(text) + data class QUOTE(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) + data class EDIT(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) + data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) } data class RoomDetailViewState( @@ -47,7 +47,7 @@ data class RoomDetailViewState( val timeline: Timeline? = null, val asyncInviter: Async = Uninitialized, val asyncRoomSummary: Async = Uninitialized, - val sendMode: SendMode = SendMode.REGULAR, + val sendMode: SendMode = SendMode.REGULAR(""), val isEncrypted: Boolean = false, val tombstoneEvent: Event? = null, val tombstoneEventHandling: Async = Uninitialized, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt index 2ee1f30645..f5b62e4512 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt @@ -40,6 +40,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { @EpoxyAttribute var avatarUrl: String? = null @EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var hasUnreadMessage: Boolean = false + @EpoxyAttribute var hasDraft: Boolean = false @EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var listener: (() -> Unit)? = null @@ -52,6 +53,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { holder.lastEventView.text = lastFormattedEvent holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted)) holder.unreadIndentIndicator.isVisible = hasUnreadMessage + holder.draftView.isVisible = hasDraft avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView) } @@ -60,6 +62,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { val unreadCounterBadgeView by bind(R.id.roomUnreadCounterBadgeView) val unreadIndentIndicator by bind(R.id.roomUnreadIndicator) val lastEventView by bind(R.id.roomLastEventView) + val draftView by bind(R.id.roomDraftBadge) val lastEventTimeView by bind(R.id.roomLastEventTimeView) val avatarImageView by bind(R.id.roomAvatarImageView) val rootView by bind(R.id.itemRoomLayout) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt index 015e54b368..942796961b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt @@ -133,6 +133,7 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte .showHighlighted(showHighlighted) .unreadNotificationCount(unreadCount) .hasUnreadMessage(roomSummary.hasUnreadMessages) + .hasDraft(roomSummary.userDrafts.isNotEmpty()) .listener { listener?.onRoomSelected(roomSummary) } } diff --git a/vector/src/main/res/layout/item_room.xml b/vector/src/main/res/layout/item_room.xml index 7eb2083ecb..741bd47069 100644 --- a/vector/src/main/res/layout/item_room.xml +++ b/vector/src/main/res/layout/item_room.xml @@ -56,13 +56,27 @@ android:textSize="15sp" android:textStyle="bold" app:layout_constrainedWidth="true" - app:layout_constraintEnd_toStartOf="@+id/roomUnreadCounterBadgeView" + app:layout_constraintEnd_toStartOf="@+id/roomDraftBadge" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toEndOf="@id/roomAvatarImageView" app:layout_constraintTop_toTopOf="parent" tools:text="@sample/matrix.json/data/displayName" /> + + + tools:text="4" + tools:visibility="visible" /> Date: Thu, 12 Sep 2019 15:26:38 +0200 Subject: [PATCH 43/44] Display room with draft in the Catchup screen --- .../riotx/features/home/room/list/RoomListDisplayModeFilter.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt index 7c559b7f88..2536b969ff 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt @@ -27,7 +27,8 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListFragment.Displa return false } return when (displayMode) { - RoomListFragment.DisplayMode.HOME -> roomSummary.notificationCount > 0 || roomSummary.membership == Membership.INVITE + RoomListFragment.DisplayMode.HOME -> + roomSummary.notificationCount > 0 || roomSummary.membership == Membership.INVITE || roomSummary.userDrafts.isNotEmpty() RoomListFragment.DisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN RoomListFragment.DisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN RoomListFragment.DisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN From 562acc97025ae5a3084db40c3e558606fedf25d4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Sep 2019 18:08:15 +0200 Subject: [PATCH 44/44] Save Draft only when app goes to background. --- .../home/room/detail/RoomDetailActions.kt | 2 +- .../home/room/detail/RoomDetailFragment.kt | 38 +++++++---------- .../home/room/detail/RoomDetailViewModel.kt | 42 +++++++++---------- 3 files changed, 37 insertions(+), 45 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt index dda5f611b6..4aeb4f973a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt @@ -39,7 +39,7 @@ sealed class RoomDetailActions { object AcceptInvite : RoomDetailActions() object RejectInvite : RoomDetailActions() - data class EnterEditMode(val eventId: String) : RoomDetailActions() + data class EnterEditMode(val eventId: String, val draft: String) : RoomDetailActions() data class EnterQuoteMode(val eventId: String, val draft: String) : RoomDetailActions() data class EnterReplyMode(val eventId: String, val draft: String) : RoomDetailActions() data class ExitSpecialMode(val draft: String) : RoomDetailActions() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index b32d08ea7d..7bc5cf7016 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -53,7 +53,6 @@ import com.google.android.material.snackbar.Snackbar import com.jaiselrahman.filepicker.activity.FilePickerActivity import com.jaiselrahman.filepicker.config.Configurations import com.jaiselrahman.filepicker.model.MediaFile -import com.jakewharton.rxbinding3.widget.afterTextChangeEvents import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy @@ -107,7 +106,6 @@ import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.themes.ThemeUtils -import io.reactivex.rxkotlin.subscribeBy import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.merge_composer_layout.view.* @@ -115,7 +113,6 @@ import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import org.commonmark.parser.Parser import timber.log.Timber import java.io.File -import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -189,7 +186,6 @@ class RoomDetailFragment : @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer @Inject lateinit var vectorPreferences: VectorPreferences - override fun getLayoutResId() = R.layout.fragment_room_detail override fun getMenuRes() = R.menu.menu_timeline @@ -199,6 +195,8 @@ class RoomDetailFragment : @BindView(R.id.composerLayout) lateinit var composerLayout: TextComposerView + private var lockSendButton = false + override fun injectWith(injector: ScreenComponent) { injector.inject(this) } @@ -350,7 +348,6 @@ class RoomDetailFragment : // Do not update if this is the same text to avoid the cursor to move if (text != composerLayout.composerEditText.text.toString()) { // Ignore update to avoid saving a draft - filterComposerTextChange = true composerLayout.composerEditText.setText(text) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) } @@ -366,6 +363,8 @@ class RoomDetailFragment : super.onPause() notificationDrawerManager.setCurrentRoom(null) + + roomDetailViewModel.process(RoomDetailActions.SaveDraft(composerLayout.composerEditText.text.toString())) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -437,21 +436,7 @@ class RoomDetailFragment : } } - private var filterComposerTextChange = true - private fun setupComposer() { - composerLayout.composerEditText.afterTextChangeEvents() - .debounce(100, TimeUnit.MILLISECONDS) - .subscribeBy { - if (filterComposerTextChange) { - Timber.d("Draft: ignore text update") - filterComposerTextChange = false - return@subscribeBy - } - roomDetailViewModel.process(RoomDetailActions.SaveDraft(it.editable.toString())) - } - .disposeOnDestroy() - val elevation = 6f val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) Autocomplete.on(composerLayout.composerEditText) @@ -515,8 +500,13 @@ class RoomDetailFragment : .build() composerLayout.sendButton.setOnClickListener { + if (lockSendButton) { + Timber.w("Send button is locked") + return@setOnClickListener + } val textMessage = composerLayout.composerEditText.text.toString() if (textMessage.isNotBlank()) { + lockSendButton = true roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, vectorPreferences.isMarkdownEnabled())) } } @@ -673,11 +663,11 @@ class RoomDetailFragment : private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { when (sendMessageResult) { is SendMessageResult.MessageSent -> { - // Nothing to do, the composer will be cleared with the draft update + updateComposerText("") } is SendMessageResult.SlashCommandHandled -> { sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } - // The composer will be cleared with the draft update + updateComposerText("") } is SendMessageResult.SlashCommandError -> { displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) @@ -686,7 +676,7 @@ class RoomDetailFragment : displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) } is SendMessageResult.SlashCommandResultOk -> { - // Ignore + updateComposerText("") } is SendMessageResult.SlashCommandResultError -> { displayCommandError(sendMessageResult.throwable.localizedMessage) @@ -695,6 +685,8 @@ class RoomDetailFragment : displayCommandError(getString(R.string.not_implemented)) } } + + lockSendButton = false } private fun displayCommandError(message: String) { @@ -939,7 +931,7 @@ class RoomDetailFragment : roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) } is SimpleAction.Edit -> { - roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId)) + roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId, composerLayout.composerEditText.text.toString())) } is SimpleAction.Quote -> { roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString())) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 593a3dfd04..80f333a76e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -85,9 +85,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private var timeline = room.createTimeline(eventId, timelineSettings) - // Filter to avoid infinite loop when user enter text in the composer and call SaveDraft - private var filterDraftUpdate = false - // Slot to keep a pending action during permission request var pendingAction: RoomDetailActions? = null @@ -151,9 +148,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro * Convert a send mode to a draft and save the draft */ private fun handleSaveDraft(action: RoomDetailActions.SaveDraft) { - // The text is changed, ignore the next update from DB - filterDraftUpdate = true - withState { when (it.sendMode) { is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft)) @@ -167,14 +161,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun observeDrafts() { room.rx().liveDrafts() .subscribe { - Timber.d("Draft update!") - if (filterDraftUpdate) { - Timber.d(" --> Ignore") - return@subscribe - } - - Timber.d(" --> SetState") - + Timber.d("Draft update --> SetState") setState { val draft = it.lastOrNull() ?: UserDraft.REGULAR("") copy( @@ -337,7 +324,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } is SendMode.EDIT -> { - //is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId @@ -396,7 +382,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun popDraft() { - filterDraftUpdate = false room.deleteDraft() } @@ -513,20 +498,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleEditAction(action: RoomDetailActions.EnterEditMode) { + saveCurrentDraft(action.draft) + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> timelineEvent.root.eventId?.let { - filterDraftUpdate = false room.saveDraft(UserDraft.EDIT(it, timelineEvent.getTextEditableContent() ?: "")) } } } private fun handleQuoteAction(action: RoomDetailActions.EnterQuoteMode) { + saveCurrentDraft(action.draft) + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> withState { state -> // Save a new draft and keep the previously entered text, if it was not an edit timelineEvent.root.eventId?.let { - filterDraftUpdate = false if (state.sendMode is SendMode.EDIT) { room.saveDraft(UserDraft.QUOTE(it, "")) } else { @@ -538,11 +525,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleReplyAction(action: RoomDetailActions.EnterReplyMode) { + saveCurrentDraft(action.draft) + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> withState { state -> // Save a new draft and keep the previously entered text, if it was not an edit timelineEvent.root.eventId?.let { - filterDraftUpdate = false if (state.sendMode is SendMode.EDIT) { room.saveDraft(UserDraft.REPLY(it, "")) } else { @@ -553,11 +541,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } + private fun saveCurrentDraft(draft: String) { + // Save the draft with the current text if any + withState { + if (draft.isNotBlank()) { + when (it.sendMode) { + is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(draft)) + is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, draft)) + is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, draft)) + is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, draft)) + } + } + } + } + private fun handleExitSpecialMode(action: RoomDetailActions.ExitSpecialMode) { withState { state -> // For edit, just delete the current draft - filterDraftUpdate = false - if (state.sendMode is SendMode.EDIT) { room.deleteDraft() } else {