Properly integrated SSO in app

This commit is contained in:
Sylvain Berfini 2024-02-19 14:38:50 +01:00
parent 9c3102392b
commit 59ece2f8a8
9 changed files with 338 additions and 178 deletions

View file

@ -173,6 +173,7 @@ dependencies {
implementation 'com.google.firebase:firebase-messaging' implementation 'com.google.firebase:firebase-messaging'
implementation 'com.google.firebase:firebase-crashlytics-ndk' implementation 'com.google.firebase:firebase-crashlytics-ndk'
// https://github.com/openid/AppAuth-Android/blob/master/LICENSE Apache v2.0
implementation 'net.openid:appauth:0.11.1' implementation 'net.openid:appauth:0.11.1'
android.defaultConfig.manifestPlaceholders = [appAuthRedirectScheme: 'org.linphone'] android.defaultConfig.manifestPlaceholders = [appAuthRedirectScheme: 'org.linphone']

View file

@ -111,11 +111,6 @@
android:launchMode="singleTask" android:launchMode="singleTask"
android:resizeableActivity="true" /> android:resizeableActivity="true" />
<activity
android:name=".ui.sso.OpenIdActivity"
android:launchMode="singleTask"
android:resizeableActivity="true"/>
<activity <activity
android:name=".ui.call.CallActivity" android:name=".ui.call.CallActivity"
android:theme="@style/Theme.LinphoneInCall" android:theme="@style/Theme.LinphoneInCall"

View file

@ -36,7 +36,6 @@ import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantLandingFragmentBinding import org.linphone.databinding.AssistantLandingFragmentBinding
import org.linphone.ui.assistant.model.AcceptConditionsAndPolicyDialogModel import org.linphone.ui.assistant.model.AcceptConditionsAndPolicyDialogModel
import org.linphone.ui.assistant.viewmodel.LandingViewModel import org.linphone.ui.assistant.viewmodel.LandingViewModel
import org.linphone.ui.sso.OpenIdActivity
import org.linphone.utils.DialogUtils import org.linphone.utils.DialogUtils
@UiThread @UiThread
@ -99,19 +98,23 @@ class LandingFragment : Fragment() {
viewModel.redirectToSingleSignOnEvent.observe(viewLifecycleOwner) { viewModel.redirectToSingleSignOnEvent.observe(viewLifecycleOwner) {
it.consume { address -> it.consume { address ->
goToSingleSignOnActivity(address) goToSingleSignOnFragment(address)
} }
} }
} }
private fun goToLoginFragment(identity: String) { private fun goToLoginFragment(identity: String) {
Log.i(
"$TAG Going to Linphone credentials based authentication fragment for SIP account [$identity]"
)
val action = LandingFragmentDirections.actionLandingFragmentToLoginFragment(identity) val action = LandingFragmentDirections.actionLandingFragmentToLoginFragment(identity)
findNavController().navigate(action) findNavController().navigate(action)
} }
private fun goToSingleSignOnActivity(identity: String) { private fun goToSingleSignOnFragment(identity: String) {
startActivity(Intent(requireContext(), OpenIdActivity::class.java)) Log.i("$TAG Going to Single Sign On fragment for SIP account [$identity]")
requireActivity().finish() val action = LandingFragmentDirections.actionLandingFragmentToSingleSignOnFragment(identity)
findNavController().navigate(action)
} }
private fun goToRegisterFragment() { private fun goToRegisterFragment() {

View file

@ -0,0 +1,109 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.assistant.fragment
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationResponse
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantSingleSignOnFragmentBinding
import org.linphone.ui.assistant.AssistantActivity
import org.linphone.ui.assistant.viewmodel.SingleSignOnViewModel
@UiThread
class SingleSignOnFragment : Fragment() {
companion object {
private const val TAG = "[Single Sign On Fragment]"
private const val ACTIVITY_RESULT_ID = 666
}
private lateinit var binding: AssistantSingleSignOnFragmentBinding
private lateinit var viewModel: SingleSignOnViewModel
private val args: SingleSignOnFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = AssistantSingleSignOnFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
viewModel = ViewModelProvider(this)[SingleSignOnViewModel::class.java]
binding.viewModel = viewModel
val identity = args.sipIdentity
Log.i("$TAG SIP Identity found in arguments is [$identity]")
viewModel.preFilledUser = identity
viewModel.singleSignOnProcessCompletedEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Process complete, leaving assistant")
requireActivity().finish()
}
}
viewModel.startAuthIntentEvent.observe(viewLifecycleOwner) {
it.consume { intent ->
Log.i("$TAG Starting auth intent activity")
startActivityForResult(intent, ACTIVITY_RESULT_ID)
}
}
viewModel.onErrorEvent.observe(viewLifecycleOwner) {
it.consume { errorMessage ->
(requireActivity() as AssistantActivity).showRedToast(errorMessage, R.drawable.x)
findNavController().popBackStack()
}
}
viewModel.setUp()
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == ACTIVITY_RESULT_ID && data != null) {
val resp = AuthorizationResponse.fromIntent(data)
val ex = AuthorizationException.fromIntent(data)
viewModel.processAuthIntentResponse(resp, ex)
}
super.onActivityResult(requestCode, resultCode, data)
}
}

View file

@ -17,107 +17,93 @@
* 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.sso package org.linphone.ui.assistant.viewmodel
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.databinding.DataBindingUtil import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.io.File import java.io.File
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.openid.appauth.AuthState import net.openid.appauth.AuthState
import net.openid.appauth.AuthState.AuthStateAction
import net.openid.appauth.AuthorizationException import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationRequest import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationResponse import net.openid.appauth.AuthorizationResponse
import net.openid.appauth.AuthorizationService import net.openid.appauth.AuthorizationService
import net.openid.appauth.AuthorizationServiceConfiguration import net.openid.appauth.AuthorizationServiceConfiguration
import net.openid.appauth.AuthorizationServiceConfiguration.RetrieveConfigurationCallback
import net.openid.appauth.ResponseTypeValues import net.openid.appauth.ResponseTypeValues
import org.linphone.R import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.SingleSignOnOpenIdActivityBinding import org.linphone.utils.Event
import org.linphone.ui.GenericActivity
import org.linphone.utils.FileUtils import org.linphone.utils.FileUtils
import org.linphone.utils.TimestampUtils import org.linphone.utils.TimestampUtils
@UiThread class SingleSignOnViewModel : ViewModel() {
class OpenIdActivity : GenericActivity() {
companion object { companion object {
private const val TAG = "[Open ID Activity]" private const val TAG = "[Single Sign On ViewModel]"
private const val WELL_KNOWN = "https://sso.onhexagone.com//realms/ONHEXAGONE/.well-known/openid-configuration" private const val WELL_KNOWN = "https://sso.onhexagone.com//realms/ONHEXAGONE/.well-known/openid-configuration"
private const val CLIENT_ID = "account" private const val CLIENT_ID = "account"
private const val SCOPE = "openid email profile" private const val SCOPE = "openid email profile"
private const val REDIRECT_URI = "org.linphone:/openidcallback" private const val REDIRECT_URI = "org.linphone:/openidcallback"
private const val ACTIVITY_RESULT_ID = 666
} }
private lateinit var binding: SingleSignOnOpenIdActivityBinding val singleSignOnProcessCompletedEvent = MutableLiveData<Event<Boolean>>()
val startAuthIntentEvent: MutableLiveData<Event<Intent>> by lazy {
MutableLiveData<Event<Intent>>()
}
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
var preFilledUser: String = ""
private lateinit var authState: AuthState private lateinit var authState: AuthState
private lateinit var authService: AuthorizationService private lateinit var authService: AuthorizationService
override fun onCreate(savedInstanceState: Bundle?) { @UiThread
super.onCreate(savedInstanceState) fun setUp() {
viewModelScope.launch {
binding = DataBindingUtil.setContentView(this, R.layout.single_sign_on_open_id_activity) Log.i("$TAG Setting up SSO environment, redirect URI is [$REDIRECT_URI]")
binding.lifecycleOwner = this
lifecycleScope.launch {
authState = getAuthState() authState = getAuthState()
updateTokenInfo() updateTokenInfo()
} }
}
binding.setSingleSignOnClickListener { @UiThread
lifecycleScope.launch { fun processAuthIntentResponse(resp: AuthorizationResponse?, ex: AuthorizationException?) {
singleSignOn() if (::authState.isInitialized) {
} Log.i("$TAG Updating AuthState object after authorization response")
authState.update(resp, ex)
} }
binding.setRefreshTokenClickListener { if (resp != null) {
lifecycleScope.launch { Log.i("$TAG Response isn't null, performing request token")
performRefreshToken() performRequestToken(resp)
} } else {
Log.e("$TAG Can't perform request token [$ex]")
onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty()))
} }
} }
@Deprecated("Deprecated in Java") @UiThread
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == ACTIVITY_RESULT_ID && data != null) {
val resp = AuthorizationResponse.fromIntent(data)
val ex = AuthorizationException.fromIntent(data)
if (::authState.isInitialized) {
Log.i("$TAG Updating AuthState object after authorization response")
authState.update(resp, ex)
}
if (resp != null) {
Log.i("$TAG Response isn't null, performing request token")
performRequestToken(resp)
} else {
Log.e("$TAG Can't perform request token [$ex]")
}
}
super.onActivityResult(requestCode, resultCode, data)
}
private fun singleSignOn() { private fun singleSignOn() {
Log.i("$TAG Fetch from issuer") Log.i("$TAG Fetch from issuer")
AuthorizationServiceConfiguration.fetchFromUrl( AuthorizationServiceConfiguration.fetchFromUrl(
Uri.parse(WELL_KNOWN), Uri.parse(WELL_KNOWN),
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")
onErrorEvent.postValue(Event("Failed to fetch configuration"))
return@RetrieveConfigurationCallback return@RetrieveConfigurationCallback
} }
if (serviceConfiguration == null) { if (serviceConfiguration == null) {
Log.e("$TAG Service configuration is null!") Log.e("$TAG Service configuration is null!")
onErrorEvent.postValue(Event("Service configuration is null"))
return@RetrieveConfigurationCallback return@RetrieveConfigurationCallback
} }
@ -134,43 +120,25 @@ class OpenIdActivity : GenericActivity() {
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()) {
authRequestBuilder.setLoginHint(preFilledUser)
}
val authRequest = authRequestBuilder val authRequest = authRequestBuilder
.setScope(SCOPE) .setScope(SCOPE)
.build() .build()
authService = AuthorizationService(this) authService = AuthorizationService(coreContext.context)
val authIntent = authService.getAuthorizationRequestIntent(authRequest) val authIntent = authService.getAuthorizationRequestIntent(authRequest)
startActivityForResult(authIntent, ACTIVITY_RESULT_ID) startAuthIntentEvent.postValue(Event(authIntent))
} }
) )
} }
private fun performRequestToken(response: AuthorizationResponse) { @UiThread
if (::authService.isInitialized) {
Log.i("$TAG Starting perform token request")
authService.performTokenRequest(
response.createTokenExchangeRequest()
) { resp, ex ->
if (resp != null) {
Log.i("$TAG Token exchange succeeded!")
if (::authState.isInitialized) {
Log.i("$TAG Updating AuthState object after token response")
authState.update(resp, ex)
storeAuthStateAsJsonFile()
}
useToken()
} else {
Log.e("$TAG Failed to perform token request [$ex]")
}
}
}
}
private fun performRefreshToken() { private fun performRefreshToken() {
if (::authState.isInitialized) { if (::authState.isInitialized) {
if (!::authService.isInitialized) { if (!::authService.isInitialized) {
authService = AuthorizationService(this) authService = AuthorizationService(coreContext.context)
} }
Log.i("$TAG Starting refresh token request") Log.i("$TAG Starting refresh token request")
@ -190,9 +158,14 @@ class OpenIdActivity : GenericActivity() {
Log.e( Log.e(
"$TAG Failed to perform token refresh [$ex], destroying auth_state.json file" "$TAG Failed to perform token refresh [$ex], destroying auth_state.json file"
) )
val file = File(applicationContext.filesDir.absolutePath, "auth_state.json") onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty()))
lifecycleScope.launch {
val file = File(coreContext.context.filesDir.absolutePath, "auth_state.json")
viewModelScope.launch {
FileUtils.deleteFile(file.absolutePath) FileUtils.deleteFile(file.absolutePath)
Log.w(
"$TAG Previous auth_state.json file deleted, starting single sign on process from scratch"
)
singleSignOn() singleSignOn()
} }
} }
@ -200,6 +173,32 @@ class OpenIdActivity : GenericActivity() {
} }
} }
@UiThread
private fun performRequestToken(response: AuthorizationResponse) {
if (::authService.isInitialized) {
Log.i("$TAG Starting perform token request")
authService.performTokenRequest(
response.createTokenExchangeRequest()
) { resp, ex ->
if (resp != null) {
Log.i("$TAG Token exchange succeeded!")
if (::authState.isInitialized) {
Log.i("$TAG Updating AuthState object after token response")
authState.update(resp, ex)
storeAuthStateAsJsonFile()
}
useToken()
} else {
Log.e("$TAG Failed to perform token request [$ex]")
onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty()))
}
}
}
}
@UiThread
private fun useToken() { private fun useToken() {
if (::authState.isInitialized && ::authService.isInitialized) { if (::authState.isInitialized && ::authService.isInitialized) {
if (authState.needsTokenRefresh && authState.refreshToken.isNullOrEmpty()) { if (authState.needsTokenRefresh && authState.refreshToken.isNullOrEmpty()) {
@ -207,10 +206,11 @@ class OpenIdActivity : GenericActivity() {
return return
} }
Log.i("$TAG Performing action with fresh token") singleSignOnProcessCompletedEvent.postValue(Event(true))
/*Log.i("$TAG Performing action with fresh token")
authState.performActionWithFreshTokens( authState.performActionWithFreshTokens(
authService, authService,
AuthStateAction { accessToken, idToken, ex -> AuthState.AuthStateAction { accessToken, idToken, ex ->
if (ex != null) { if (ex != null) {
Log.e("$TAG Failed to use token [$ex]") Log.e("$TAG Failed to use token [$ex]")
return@AuthStateAction return@AuthStateAction
@ -221,13 +221,15 @@ class OpenIdActivity : GenericActivity() {
storeAuthStateAsJsonFile() storeAuthStateAsJsonFile()
} }
) )*/
} }
} }
@UiThread
private suspend fun getAuthState(): AuthState { private suspend fun getAuthState(): AuthState {
val file = File(applicationContext.filesDir.absolutePath, "auth_state.json") val file = File(coreContext.context.filesDir.absolutePath, "auth_state.json")
if (file.exists()) { if (file.exists()) {
Log.i("$TAG Auth state file found, trying to read it")
val content = FileUtils.readFile(file) val content = FileUtils.readFile(file)
if (content.isNotEmpty()) { if (content.isNotEmpty()) {
Log.i("$TAG Initializing AuthState from local JSON file") Log.i("$TAG Initializing AuthState from local JSON file")
@ -236,19 +238,23 @@ class OpenIdActivity : GenericActivity() {
return AuthState.jsonDeserialize(content) return AuthState.jsonDeserialize(content)
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e("$TAG Failed to use serialized AuthState [$exception]") Log.e("$TAG Failed to use serialized AuthState [$exception]")
onErrorEvent.postValue(Event("Failed to read stored AuthState"))
} }
} }
} else {
Log.i("$TAG Auth state file not found yet...")
} }
return AuthState() return AuthState()
} }
@UiThread
private fun storeAuthStateAsJsonFile() { private fun storeAuthStateAsJsonFile() {
Log.i("$TAG Trying to save serialized authState as JSON file") Log.i("$TAG Trying to save serialized authState as JSON file")
val data = authState.jsonSerializeString() val data = authState.jsonSerializeString()
Log.d("$TAG Date to save is [$data]") Log.d("$TAG Date to save is [$data]")
val file = File(applicationContext.filesDir.absolutePath, "auth_state.json") val file = File(coreContext.context.filesDir.absolutePath, "auth_state.json")
lifecycleScope.launch { viewModelScope.launch {
if (FileUtils.dumpStringToFile(data, file)) { if (FileUtils.dumpStringToFile(data, file)) {
Log.i("$TAG Service configuration saved as JSON as [${file.absolutePath}]") Log.i("$TAG Service configuration saved as JSON as [${file.absolutePath}]")
} else { } else {
@ -259,20 +265,19 @@ class OpenIdActivity : GenericActivity() {
} }
} }
@UiThread
private fun updateTokenInfo() { private fun updateTokenInfo() {
Log.i("$TAG Updating token info")
if (::authState.isInitialized) { if (::authState.isInitialized) {
if (authState.isAuthorized) { if (authState.isAuthorized) {
Log.i("$TAG User is already authenticated!") Log.i("$TAG User is already authenticated!")
binding.sso.visibility = View.GONE
binding.tokenRefresh.visibility = View.GONE
binding.tokenExpires.visibility = View.VISIBLE
val expiration = authState.accessTokenExpirationTime val expiration = authState.accessTokenExpirationTime
if (expiration != null) { if (expiration != null) {
if (expiration < System.currentTimeMillis()) { if (expiration < System.currentTimeMillis()) {
Log.w("$TAG Access token is expired") Log.w("$TAG Access token is expired")
binding.tokenExpires.text = "Token expired!" performRefreshToken()
binding.tokenRefresh.visibility = View.VISIBLE
} else { } else {
val date = if (TimestampUtils.isToday(expiration, timestampInSecs = false)) { val date = if (TimestampUtils.isToday(expiration, timestampInSecs = false)) {
"today" "today"
@ -285,18 +290,23 @@ class OpenIdActivity : GenericActivity() {
} }
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]")
binding.tokenExpires.text = "Token expires $date at $time" singleSignOnProcessCompletedEvent.postValue(Event(true))
} }
} else { } else {
Log.w("$TAG Access token expiration info not available") Log.w("$TAG Access token expiration info not available")
binding.tokenExpires.text = "Can't access token expiration!" val file = File(coreContext.context.filesDir.absolutePath, "auth_state.json")
viewModelScope.launch {
FileUtils.deleteFile(file.absolutePath)
singleSignOn()
}
} }
} else { } else {
Log.w("$TAG User isn't authenticated yet!") Log.w("$TAG User isn't authenticated yet")
binding.sso.visibility = View.VISIBLE singleSignOn()
binding.tokenRefresh.visibility = View.GONE
binding.tokenExpires.visibility = View.GONE
} }
} else {
Log.i("$TAG Auth state hasn't been created yet")
singleSignOn()
} }
} }
} }

View file

@ -0,0 +1,92 @@
<?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.assistant.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"/>
<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_single_sign_on_title"
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>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="singleSignOnClickListener"
type="View.OnClickListener" />
<variable
name="refreshTokenClickListener"
type="View.OnClickListener" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.sso.OpenIdActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
style="@style/primary_button_label_style"
android:id="@+id/sso"
android:onClick="@{singleSignOnClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:text="Single sign on"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/token_expires"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/primary_button_label_style"
android:id="@+id/token_refresh"
android:onClick="@{refreshTokenClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:text="Refresh token"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/token_expires"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -172,6 +172,25 @@
app:popExitAnim="@anim/slide_out_right" app:popExitAnim="@anim/slide_out_right"
app:launchSingleTop="true" /> app:launchSingleTop="true" />
<action
android:id="@+id/action_landingFragment_to_singleSignOnFragment"
app:destination="@id/singleSignOnFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:launchSingleTop="true" />
</fragment>
<fragment
android:id="@+id/singleSignOnFragment"
android:name="org.linphone.ui.assistant.fragment.SingleSignOnFragment"
android:label="SingleSignOnFragment"
tools:layout="@layout/assistant_single_sign_on_fragment">
<argument
android:name="sipIdentity"
app:argType="string" />
</fragment> </fragment>
</navigation> </navigation>

View file

@ -218,6 +218,7 @@
<string name="assistant_permissions_title">Grant permissions</string> <string name="assistant_permissions_title">Grant permissions</string>
<string name="assistant_permissions_grant_all_of_them">OK</string> <string name="assistant_permissions_grant_all_of_them">OK</string>
<string name="assistant_permissions_skip_permissions">Do it later</string> <string name="assistant_permissions_skip_permissions">Do it later</string>
<string name="assistant_single_sign_on_title">Single Sign On</string>
<string name="assistant_permissions_subtitle">To fully enjoy &appName; we need you to grant us the following permissions :</string> <string name="assistant_permissions_subtitle">To fully enjoy &appName; we need you to grant us the following permissions :</string>
<string name="assistant_permissions_read_contacts_title"><b>Read contacts:</b> To display your contacts and find whom is using &appName;.</string> <string name="assistant_permissions_read_contacts_title"><b>Read contacts:</b> To display your contacts and find whom is using &appName;.</string>