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