Reworked auth requested callback & SSO activity to handle Bearer authentication requests

This commit is contained in:
Sylvain Berfini 2024-04-30 11:24:11 +02:00
parent ebf9fa9145
commit 507fb8a3ce
11 changed files with 324 additions and 124 deletions

View file

@ -114,21 +114,6 @@
android:launchMode="singleTask" android:launchMode="singleTask"
android:resizeableActivity="true" /> android:resizeableActivity="true" />
<activity
android:name=".ui.assistant.SingleSignOnActivity"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="linphone-sso" />
</intent-filter>
</activity>
<activity <activity
android:name=".ui.call.CallActivity" android:name=".ui.call.CallActivity"
android:theme="@style/Theme.LinphoneInCall" android:theme="@style/Theme.LinphoneInCall"

View file

@ -72,6 +72,17 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
private val mainThread = Handler(Looper.getMainLooper()) private val mainThread = Handler(Looper.getMainLooper())
var bearerAuthInfoPendingPasswordUpdate: AuthInfo? = null
var digestAuthInfoPendingPasswordUpdate: AuthInfo? = null
val bearerAuthenticationRequestedEvent: MutableLiveData<Event<Pair<String, String?>>> by lazy {
MutableLiveData<Event<Pair<String, String?>>>()
}
val digestAuthenticationRequestedEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
val greenToastToShowEvent: MutableLiveData<Event<Pair<String, Int>>> by lazy { val greenToastToShowEvent: MutableLiveData<Event<Pair<String, Int>>> by lazy {
MutableLiveData<Event<Pair<String, Int>>>() MutableLiveData<Event<Pair<String, Int>>>()
} }
@ -210,6 +221,57 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
} }
} }
} }
@WorkerThread
override fun onAuthenticationRequested(core: Core, authInfo: AuthInfo, method: AuthMethod) {
if (authInfo.username == null || authInfo.domain == null || authInfo.realm == null) {
Log.e(
"$TAG Authentication request but either username [${authInfo.username}], domain [${authInfo.domain}] or realm [${authInfo.realm}] is null!"
)
return
}
when (method) {
AuthMethod.Bearer -> {
val serverUrl = authInfo.authorizationServer
val username = authInfo.username
if (!serverUrl.isNullOrEmpty()) {
Log.i(
"$TAG Authentication requested method is Bearer, starting Single Sign On activity with server URL [$serverUrl] and username [$username]"
)
bearerAuthInfoPendingPasswordUpdate = authInfo
bearerAuthenticationRequestedEvent.postValue(
Event(Pair(serverUrl, username))
)
} else {
Log.e(
"$TAG Authentication requested method is Bearer but no authorization server was found in auth info!"
)
}
}
AuthMethod.HttpDigest -> {
val accountFound = core.accountList.find {
it.params.identityAddress?.username == authInfo.username && it.params.identityAddress?.domain == authInfo.domain
}
if (accountFound == null) {
Log.w(
"$TAG Failed to find account matching auth info, aborting auth dialog"
)
return
}
val identity = "${authInfo.username}@${authInfo.domain}"
Log.i(
"$TAG Authentication requested method is HttpDigest, showing dialog asking user for password for identity [$identity]"
)
digestAuthInfoPendingPasswordUpdate = authInfo
digestAuthenticationRequestedEvent.postValue(Event(identity))
}
AuthMethod.Tls -> {
Log.w("$TAG Authentication requested method is TLS, not doing anything...")
}
}
}
} }
private val loggingServiceListener = object : LoggingServiceListenerStub() { private val loggingServiceListener = object : LoggingServiceListenerStub() {
@ -392,6 +454,22 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
} }
} }
@WorkerThread
fun updateAuthInfo(password: String) {
val authInfo = digestAuthInfoPendingPasswordUpdate
if (authInfo != null) {
Log.i(
"$TAG Updating password for username [${authInfo.username}] using auth info [$authInfo]"
)
authInfo.password = password
core.addAuthInfo(authInfo)
digestAuthInfoPendingPasswordUpdate = null
core.refreshRegisters()
} else {
Log.e("$TAG No pending auth info for digest authentication!")
}
}
@WorkerThread @WorkerThread
fun isAddressMyself(address: Address): Boolean { fun isAddressMyself(address: Address): Boolean {
val found = core.accountList.find { val found = core.accountList.find {

View file

@ -53,6 +53,7 @@ import org.linphone.ui.GenericActivity
import org.linphone.ui.assistant.AssistantActivity import org.linphone.ui.assistant.AssistantActivity
import org.linphone.ui.main.chat.fragment.ConversationsListFragmentDirections import org.linphone.ui.main.chat.fragment.ConversationsListFragmentDirections
import org.linphone.ui.main.fragment.AuthRequestedDialogModel import org.linphone.ui.main.fragment.AuthRequestedDialogModel
import org.linphone.ui.main.sso.fragment.SingleSignOnFragmentDirections
import org.linphone.ui.main.viewmodel.MainViewModel import org.linphone.ui.main.viewmodel.MainViewModel
import org.linphone.ui.main.viewmodel.SharedMainViewModel import org.linphone.ui.main.viewmodel.SharedMainViewModel
import org.linphone.ui.welcome.WelcomeActivity import org.linphone.ui.welcome.WelcomeActivity
@ -164,12 +165,6 @@ class MainActivity : GenericActivity() {
} }
} }
viewModel.authenticationRequestedEvent.observe(this) {
it.consume { identity ->
showAuthenticationRequestedDialog(identity)
}
}
binding.root.doOnAttach { binding.root.doOnAttach {
Log.i("$TAG Report UI has been fully drawn (TTFD)") Log.i("$TAG Report UI has been fully drawn (TTFD)")
try { try {
@ -179,6 +174,28 @@ class MainActivity : GenericActivity() {
} }
} }
coreContext.bearerAuthenticationRequestedEvent.observe(this) {
it.consume { pair ->
val serverUrl = pair.first
val username = pair.second
Log.i(
"$TAG Navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]"
)
val action = SingleSignOnFragmentDirections.actionGlobalSingleSignOnFragment(
serverUrl,
username
)
findNavController().navigate(action)
}
}
coreContext.digestAuthenticationRequestedEvent.observe(this) {
it.consume { identity ->
showAuthenticationRequestedDialog(identity)
}
}
coreContext.greenToastToShowEvent.observe(this) { coreContext.greenToastToShowEvent.observe(this) {
it.consume { pair -> it.consume { pair ->
val message = pair.first val message = pair.first
@ -586,7 +603,9 @@ class MainActivity : GenericActivity() {
model.confirmEvent.observe(this) { model.confirmEvent.observe(this) {
it.consume { password -> it.consume { password ->
viewModel.updateAuthInfo(password) coreContext.postOnCoreThread {
coreContext.updateAuthInfo(password)
}
dialog.dismiss() dialog.dismiss()
} }
} }

View file

@ -30,6 +30,7 @@ import org.linphone.ui.main.fragment.GenericFragment
@UiThread @UiThread
class RecordingsFragment : GenericFragment() { class RecordingsFragment : GenericFragment() {
private lateinit var binding: RecordingsFragmentBinding private lateinit var binding: RecordingsFragmentBinding
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,

View file

@ -17,83 +17,80 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.linphone.ui.assistant package org.linphone.ui.main.sso.fragment
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.annotation.UiThread import android.view.LayoutInflater
import androidx.databinding.DataBindingUtil import android.view.View
import androidx.lifecycle.ViewModelProvider import android.view.ViewGroup
import androidx.navigation.fragment.navArgs
import androidx.navigation.navGraphViewModels
import net.openid.appauth.AuthorizationException import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationResponse import net.openid.appauth.AuthorizationResponse
import org.linphone.R import org.linphone.R
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantSingleSignOnActivityBinding import org.linphone.databinding.SingleSignOnFragmentBinding
import org.linphone.ui.GenericActivity import org.linphone.ui.GenericActivity
import org.linphone.ui.assistant.viewmodel.SingleSignOnViewModel import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.ui.main.sso.viewmodel.SingleSignOnViewModel
@UiThread class SingleSignOnFragment : GenericFragment() {
class SingleSignOnActivity : GenericActivity() {
companion object { companion object {
private const val TAG = "[Single Sign On Activity]" private const val TAG = "[Single Sign On Fragment]"
private const val ACTIVITY_RESULT_ID = 666 private const val ACTIVITY_RESULT_ID = 666
} }
private lateinit var binding: AssistantSingleSignOnActivityBinding private lateinit var binding: SingleSignOnFragmentBinding
private lateinit var viewModel: SingleSignOnViewModel private val viewModel: SingleSignOnViewModel by navGraphViewModels(
R.id.main_nav_graph
)
override fun onCreate(savedInstanceState: Bundle?) { private val args: SingleSignOnFragmentArgs by navArgs()
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.assistant_single_sign_on_activity) override fun onCreateView(
binding.lifecycleOwner = this inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = SingleSignOnFragmentBinding.inflate(layoutInflater)
return binding.root
}
viewModel = ViewModelProvider(this)[SingleSignOnViewModel::class.java] override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel binding.viewModel = viewModel
setUpToastsArea(binding.toastsArea) viewModel.singleSignOnProcessCompletedEvent.observe(viewLifecycleOwner) {
if (intent != null) {
Log.i(
"$TAG Handling intent action [${intent.action}], type [${intent.type}] and data [${intent.data}]"
)
val uri = intent.data?.toString() ?: ""
if (uri.startsWith("linphone-sso:")) {
val ssoUrl = uri.replace("linphone-sso:", "https:")
Log.i("$TAG Setting SSO URL [$ssoUrl]")
viewModel.singleSignOnUrl.value = ssoUrl
}
}
viewModel.singleSignOnUrl.observe(this) { url ->
Log.i("$TAG SSO URL found [$url], setting it up")
viewModel.setUp()
}
viewModel.singleSignOnProcessCompletedEvent.observe(this) {
it.consume { it.consume {
Log.i("$TAG Process complete, leaving assistant") Log.i("$TAG Process complete, going back")
finish() goBack()
} }
} }
viewModel.startAuthIntentEvent.observe(this) { viewModel.startAuthIntentEvent.observe(viewLifecycleOwner) {
it.consume { intent -> it.consume { intent ->
Log.i("$TAG Starting auth intent activity") Log.i("$TAG Starting auth intent activity")
startActivityForResult(intent, ACTIVITY_RESULT_ID) startActivityForResult(intent, ACTIVITY_RESULT_ID)
} }
} }
viewModel.onErrorEvent.observe(this) { viewModel.onErrorEvent.observe(viewLifecycleOwner) {
it.consume { errorMessage -> it.consume { errorMessage ->
showRedToast( (requireActivity() as GenericActivity).showRedToast(
errorMessage, errorMessage,
R.drawable.warning_circle R.drawable.warning_circle
) )
} }
} }
val serverUrl = args.serverUrl
val username = args.username
Log.i("$TAG Found server URL [$serverUrl] and username [$username] in args")
viewModel.setUp(serverUrl, username.orEmpty())
} }
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")

View file

@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.linphone.ui.assistant.viewmodel package org.linphone.ui.main.sso.viewmodel
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@ -35,6 +35,7 @@ import net.openid.appauth.AuthorizationService
import net.openid.appauth.AuthorizationServiceConfiguration import net.openid.appauth.AuthorizationServiceConfiguration
import net.openid.appauth.ResponseTypeValues import net.openid.appauth.ResponseTypeValues
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Factory
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.utils.Event import org.linphone.utils.Event
import org.linphone.utils.FileUtils import org.linphone.utils.FileUtils
@ -50,7 +51,9 @@ class SingleSignOnViewModel : ViewModel() {
val singleSignOnProcessCompletedEvent = MutableLiveData<Event<Boolean>>() val singleSignOnProcessCompletedEvent = MutableLiveData<Event<Boolean>>()
val singleSignOnUrl = MutableLiveData<String>() private var singleSignOnUrl = ""
private var username: String = ""
val startAuthIntentEvent: MutableLiveData<Event<Intent>> by lazy { val startAuthIntentEvent: MutableLiveData<Event<Intent>> by lazy {
MutableLiveData<Event<Intent>>() MutableLiveData<Event<Intent>>()
@ -60,15 +63,18 @@ class SingleSignOnViewModel : ViewModel() {
MutableLiveData<Event<String>>() MutableLiveData<Event<String>>()
} }
private var preFilledUser: String = ""
private lateinit var authState: AuthState private lateinit var authState: AuthState
private lateinit var authService: AuthorizationService private lateinit var authService: AuthorizationService
@UiThread @UiThread
fun setUp() { fun setUp(ssoUrl: String, user: String = "") {
viewModelScope.launch { viewModelScope.launch {
Log.i("$TAG Setting up SSO environment, redirect URI is [$REDIRECT_URI]") singleSignOnUrl = ssoUrl
username = user
Log.i(
"$TAG Setting up SSO environment for username [$username] and URL [$singleSignOnUrl], redirect URI is [$REDIRECT_URI]"
)
authState = getAuthState() authState = getAuthState()
updateTokenInfo() updateTokenInfo()
} }
@ -94,7 +100,7 @@ class SingleSignOnViewModel : ViewModel() {
private fun singleSignOn() { private fun singleSignOn() {
Log.i("$TAG Fetch from issuer") Log.i("$TAG Fetch from issuer")
AuthorizationServiceConfiguration.fetchFromUrl( AuthorizationServiceConfiguration.fetchFromUrl(
Uri.parse(singleSignOnUrl.value), Uri.parse(singleSignOnUrl),
AuthorizationServiceConfiguration.RetrieveConfigurationCallback { serviceConfiguration, ex -> AuthorizationServiceConfiguration.RetrieveConfigurationCallback { serviceConfiguration, ex ->
if (ex != null) { if (ex != null) {
Log.e("$TAG Failed to fetch configuration") Log.e("$TAG Failed to fetch configuration")
@ -120,8 +126,8 @@ class SingleSignOnViewModel : ViewModel() {
Uri.parse(REDIRECT_URI) // the redirect URI to which the auth response is sent Uri.parse(REDIRECT_URI) // the redirect URI to which the auth response is sent
) )
if (preFilledUser.isNotEmpty()) { if (username.isNotEmpty()) {
authRequestBuilder.setLoginHint(preFilledUser) authRequestBuilder.setLoginHint(username)
} }
val authRequest = authRequestBuilder.build() val authRequest = authRequestBuilder.build()
@ -187,7 +193,7 @@ class SingleSignOnViewModel : ViewModel() {
storeAuthStateAsJsonFile() storeAuthStateAsJsonFile()
} }
useToken() storeTokensInAuthInfo()
} else { } else {
Log.e("$TAG Failed to perform token request [$ex]") Log.e("$TAG Failed to perform token request [$ex]")
onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty())) onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty()))
@ -214,9 +220,7 @@ class SingleSignOnViewModel : ViewModel() {
return@AuthStateAction return@AuthStateAction
} }
Log.i("$$TAG Access & id tokens are now available") Log.i("$TAG Access & id tokens are now available")
Log.d("$TAG Access token [$accessToken], id token [$idToken]")
storeAuthStateAsJsonFile() storeAuthStateAsJsonFile()
} }
)*/ )*/
@ -288,7 +292,7 @@ class SingleSignOnViewModel : ViewModel() {
} }
val time = TimestampUtils.toString(expiration, timestampInSecs = false) val time = TimestampUtils.toString(expiration, timestampInSecs = false)
Log.i("$TAG Access token expires [$date] [$time]") Log.i("$TAG Access token expires [$date] [$time]")
singleSignOnProcessCompletedEvent.postValue(Event(true)) storeTokensInAuthInfo()
} }
} else { } else {
Log.w("$TAG Access token expiration info not available") Log.w("$TAG Access token expiration info not available")
@ -307,4 +311,35 @@ class SingleSignOnViewModel : ViewModel() {
singleSignOn() singleSignOn()
} }
} }
@UiThread
private fun storeTokensInAuthInfo() {
coreContext.postOnCoreThread { core ->
val expire = authState.accessTokenExpirationTime
if (expire == null) {
Log.e("$TAG Access token expiration time is null!")
onErrorEvent.postValue(Event("Invalid access token expiration time"))
} else {
val accessToken =
Factory.instance().createBearerToken(authState.accessToken, expire)
val refreshToken =
Factory.instance().createBearerToken(authState.refreshToken, expire)
val authInfo = coreContext.bearerAuthInfoPendingPasswordUpdate
if (authInfo == null) {
Log.e("$TAG No pending auth info in CoreContext!")
return@postOnCoreThread
}
authInfo.accessToken = accessToken
authInfo.refreshToken = refreshToken
core.addAuthInfo(authInfo)
Log.i(
"$TAG Auth info for username [$username] filled with access token [${authState.accessToken}], refresh token [${authState.refreshToken}] and expire [$expire], refreshing REGISTERs"
)
core.refreshRegisters()
singleSignOnProcessCompletedEvent.postValue(Event(true))
}
}
}
} }

View file

@ -34,8 +34,6 @@ import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
import org.linphone.core.Account import org.linphone.core.Account
import org.linphone.core.AuthInfo
import org.linphone.core.AuthMethod
import org.linphone.core.Call import org.linphone.core.Call
import org.linphone.core.Core import org.linphone.core.Core
import org.linphone.core.CoreListenerStub import org.linphone.core.CoreListenerStub
@ -93,10 +91,6 @@ class MainViewModel @UiThread constructor() : ViewModel() {
MutableLiveData<Event<Boolean>>() MutableLiveData<Event<Boolean>>()
} }
val authenticationRequestedEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
var accountsFound = -1 var accountsFound = -1
var mainIntentHandled = false var mainIntentHandled = false
@ -109,8 +103,6 @@ class MainViewModel @UiThread constructor() : ViewModel() {
private var firstAccountRegistered: Boolean = false private var firstAccountRegistered: Boolean = false
private var authInfoPendingPasswordUpdate: AuthInfo? = null
private val coreListener = object : CoreListenerStub() { private val coreListener = object : CoreListenerStub() {
@WorkerThread @WorkerThread
override fun onLastCallEnded(core: Core) { override fun onLastCallEnded(core: Core) {
@ -237,27 +229,6 @@ class MainViewModel @UiThread constructor() : ViewModel() {
core.defaultAccount = core.accountList.firstOrNull() core.defaultAccount = core.accountList.firstOrNull()
} }
} }
@WorkerThread
override fun onAuthenticationRequested(core: Core, authInfo: AuthInfo, method: AuthMethod) {
if (authInfo.username == null || authInfo.domain == null || authInfo.realm == null) {
return
}
Log.w(
"$TAG Authentication requested for account [${authInfo.username}@${authInfo.domain}] with realm [${authInfo.realm}] using method [$method]"
)
val accountFound = core.accountList.find {
it.params.identityAddress?.username == authInfo.username && it.params.identityAddress?.domain == authInfo.domain
}
if (accountFound == null) {
Log.w("$TAG Failed to find account matching auth info, aborting auth dialog")
return
}
val identity = "${authInfo.username}@${authInfo.domain}"
authInfoPendingPasswordUpdate = authInfo
authenticationRequestedEvent.postValue(Event(identity))
}
} }
init { init {
@ -340,22 +311,6 @@ class MainViewModel @UiThread constructor() : ViewModel() {
} }
} }
@UiThread
fun updateAuthInfo(password: String) {
coreContext.postOnCoreThread { core ->
val authInfo = authInfoPendingPasswordUpdate
if (authInfo != null) {
Log.i(
"$TAG Updating password for username [${authInfo.username}] using auth info [$authInfo]"
)
authInfo.password = password
core.addAuthInfo(authInfo)
authInfoPendingPasswordUpdate = null
core.refreshRegisters()
}
}
}
@WorkerThread @WorkerThread
private fun updateCallAlert() { private fun updateCallAlert() {
val core = coreContext.core val core = coreContext.core

View file

@ -250,7 +250,7 @@
android:textColor="@color/gray_main2_600" android:textColor="@color/gray_main2_600"
android:maxLines="1" android:maxLines="1"
android:background="@drawable/edit_text_background" android:background="@drawable/edit_text_background"
android:inputType="text|textPersonName" android:inputType="text|textPersonName|textCapSentences"
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintWidth_max="@dimen/text_input_max_width" app:layout_constraintWidth_max="@dimen/text_input_max_width"
app:layout_constraintTop_toBottomOf="@id/display_name_label" app:layout_constraintTop_toBottomOf="@id/display_name_label"
@ -403,6 +403,7 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:contentDescription="@null" android:contentDescription="@null"
android:src="@drawable/shape_squircle_white_background" android:src="@drawable/shape_squircle_white_background"
android:visibility="@{viewModel.showModeSelection ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/connection_background" app:layout_constraintTop_toBottomOf="@id/connection_background"
@ -416,6 +417,7 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:text="@{viewModel.isCurrentlySelectedModeSecure ? @string/manage_account_e2e_encrypted_mode_default_title : @string/manage_account_e2e_encrypted_mode_interoperable_title, default=@string/manage_account_e2e_encrypted_mode_default_title}" android:text="@{viewModel.isCurrentlySelectedModeSecure ? @string/manage_account_e2e_encrypted_mode_default_title : @string/manage_account_e2e_encrypted_mode_interoperable_title, default=@string/manage_account_e2e_encrypted_mode_default_title}"
android:visibility="@{viewModel.showModeSelection ? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toTopOf="@id/mode_background" app:layout_constraintTop_toTopOf="@id/mode_background"
app:layout_constraintStart_toStartOf="@id/mode_background" app:layout_constraintStart_toStartOf="@id/mode_background"
app:layout_constraintBottom_toBottomOf="@id/mode_background"/> app:layout_constraintBottom_toBottomOf="@id/mode_background"/>
@ -436,6 +438,7 @@
android:text="@string/manage_account_change_mode" android:text="@string/manage_account_change_mode"
android:maxLines="1" android:maxLines="1"
android:ellipsize="end" android:ellipsize="end"
android:visibility="@{viewModel.showModeSelection ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="@id/mode_background" app:layout_constraintEnd_toEndOf="@id/mode_background"
app:layout_constraintTop_toTopOf="@id/mode_background" app:layout_constraintTop_toTopOf="@id/mode_background"
app:layout_constraintBottom_toBottomOf="@id/mode_background"/> app:layout_constraintBottom_toBottomOf="@id/mode_background"/>

View file

@ -6,7 +6,7 @@
<import type="android.view.View" /> <import type="android.view.View" />
<variable <variable
name="viewModel" name="viewModel"
type="org.linphone.ui.assistant.viewmodel.SingleSignOnViewModel" /> type="org.linphone.ui.main.sso.viewmodel.SingleSignOnViewModel" />
</data> </data>
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout

View file

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="org.linphone.ui.main.sso.viewmodel.SingleSignOnViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/color_main2_000">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/back"
android:layout_width="@dimen/top_bar_height"
android:layout_height="@dimen/top_bar_height"
android:padding="15dp"
android:src="@drawable/caret_left"
android:visibility="invisible"
android:contentDescription="@string/content_description_go_back_icon"
app:tint="?attr/color_main2_500"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<ImageView
android:id="@+id/header"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="19dp"
android:src="@drawable/mountains"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:contentDescription="@null"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/back"
app:layout_constraintBottom_toBottomOf="@id/title"
app:tint="@color/main1_500"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/assistant_page_title_style"
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:paddingBottom="27dp"
android:text="@string/assistant_login_using_single_sign_on"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:indicatorColor="?attr/color_main1_500"
android:indeterminate="true"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/message"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/operation_in_progress_overlay"
android:textColor="?attr/color_main1_500"
android:textSize="18sp"
android:layout_below="@id/progress"
android:layout_centerHorizontal="true"
app:layout_constraintTop_toBottomOf="@id/progress"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/toasts_area"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="@dimen/toast_top_margin"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
app:layout_constraintWidth_max="@dimen/toast_max_width"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -437,4 +437,25 @@
android:label="SettingsAdvancedFragment" android:label="SettingsAdvancedFragment"
tools:layout="@layout/settings_advanced_fragment"/> tools:layout="@layout/settings_advanced_fragment"/>
<fragment
android:id="@+id/singleSignOnFragment"
android:name="org.linphone.ui.main.sso.fragment.SingleSignOnFragment"
android:label="SingleSignOnFragment"
tools:layout="@layout/single_sign_on_fragment">
<argument
android:name="serverUrl"
app:argType="string" />
<argument
android:name="username"
app:argType="string"
app:nullable="true"
android:defaultValue="@null" />
</fragment>
<action android:id="@+id/action_global_singleSignOnFragment"
app:destination="@id/singleSignOnFragment"
app:launchSingleTop="true"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out" />
</navigation> </navigation>