Login screens: Terms step for registration

This commit is contained in:
Benoit Marty 2019-11-19 14:37:36 +01:00
parent dfbf448bb7
commit 3f80076fb1
17 changed files with 582 additions and 13 deletions

View File

@ -27,5 +27,7 @@ interface RegistrationWizard {
fun performReCaptcha(response: String, callback: MatrixCallback<RegistrationResult>): Cancelable
fun acceptTerms(callback: MatrixCallback<RegistrationResult>): Cancelable
// TODO Add other method here
}

View File

@ -40,5 +40,4 @@ sealed class Stage(open val mandatory: Boolean) {
data class Other(override val mandatory: Boolean, val type: String, val params: Map<*, *>?) : Stage(mandatory)
}
//TODO
class TermPolicies
typealias TermPolicies = Map<*, *>

View File

@ -28,6 +28,7 @@ import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.util.CancelableCoroutine
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
@ -74,6 +75,21 @@ internal class DefaultRegistrationWizard(private val homeServerConnectionConfig:
), callback)
}
override fun acceptTerms(callback: MatrixCallback<RegistrationResult>): Cancelable {
val safeSession = currentSession ?: run {
callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
return NoOpCancellable
}
return performRegistrationRequest(
RegistrationParams(
auth = AuthParams(
type = LoginFlowTypes.TERMS,
session = safeSession
)
), callback)
}
private fun performRegistrationRequest(registrationParams: RegistrationParams, callback: MatrixCallback<RegistrationResult>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val result = runCatching {

View File

@ -84,7 +84,7 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult {
LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String)
?: "")
LoginFlowTypes.DUMMY -> Stage.Dummy
LoginFlowTypes.TERMS -> Stage.Terms(isMandatory, TermPolicies())
LoginFlowTypes.TERMS -> Stage.Terms(isMandatory, params?.get(type) as? TermPolicies ?: emptyMap<String, String>())
LoginFlowTypes.EMAIL_IDENTITY -> Stage.Email(isMandatory)
LoginFlowTypes.MSISDN -> Stage.Msisdn(isMandatory)
else -> Stage.Other(isMandatory, type, (params?.get(type) as? Map<*, *>))

View File

@ -36,6 +36,7 @@ import im.vector.riotx.features.home.group.GroupListFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.login.*
import im.vector.riotx.features.login.terms.LoginTermsFragment
import im.vector.riotx.features.reactions.EmojiSearchResultFragment
import im.vector.riotx.features.roomdirectory.PublicRoomsFragment
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
@ -119,6 +120,11 @@ interface FragmentModule {
@FragmentKey(LoginCaptchaFragment::class)
fun bindLoginCaptchaFragment(fragment: LoginCaptchaFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginTermsFragment::class)
fun bindLoginTermsFragment(fragment: LoginTermsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LoginServerUrlFormFragment::class)

View File

@ -36,6 +36,7 @@ sealed class LoginAction : VectorViewModelAction {
data class AddMsisdn(val msisdn: String) : RegisterAction()
data class ConfirmMsisdn(val code: String) : RegisterAction()
data class CaptchaDone(val captchaResponse: String) : RegisterAction()
object AcceptTerms : RegisterAction()
// Reset actions
open class ResetAction : LoginAction()

View File

@ -32,6 +32,9 @@ import im.vector.riotx.core.extensions.addFragmentToBackstack
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.login.terms.LoginTermsFragment
import im.vector.riotx.features.login.terms.LoginTermsFragmentArgument
import im.vector.riotx.features.login.terms.toLocalizedLoginTerms
import kotlinx.android.synthetic.main.activity_login.*
import javax.inject.Inject
@ -184,21 +187,22 @@ class LoginActivity : VectorBaseActivity() {
private fun handleRegistrationNavigation(flowResult: FlowResult) {
// Complete all mandatory stage first
val mandatoryStages = flowResult.missingStages.filter { it.mandatory }
val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory }
if (mandatoryStages.isEmpty()) {
if (mandatoryStage != null) {
doStage(mandatoryStage)
} else {
// Consider optional stages
val optionalStages = flowResult.missingStages.filter { !it.mandatory }
if (optionalStages.isEmpty()) {
val optionalStage = flowResult.missingStages.firstOrNull { !it.mandatory && it !is Stage.Dummy }
if (optionalStage == null) {
// Should not happen...
} else {
doStage(optionalStages.first())
doStage(optionalStage)
}
} else {
doStage(mandatoryStages.first())
}
}
// TODO Unstack fragment when stage is complete
private fun doStage(stage: Stage) {
when (stage) {
is Stage.ReCaptcha -> addFragmentToBackstack(R.id.loginFragmentContainer,
@ -210,12 +214,13 @@ class LoginActivity : VectorBaseActivity() {
-> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginGenericTextInputFormFragment::class.java,
LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory))
is Stage.Terms
-> TODO()
is Stage.Terms -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginTermsFragment::class.java,
LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))))
else -> TODO()
}
}
override fun onResume() {
super.onResume()

View File

@ -40,6 +40,9 @@ import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.session.SessionListener
import timber.log.Timber
/**
* TODO To speed up registration, consider fetching registration flow instead of login flow at startup
*/
class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState,
private val authenticator: Authenticator,
private val registrationService: RegistrationService,
@ -100,10 +103,43 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
when (action) {
is LoginAction.RegisterWith -> handleRegisterWith(action)
is LoginAction.CaptchaDone -> handleCaptchaDone(action)
is LoginAction.AcceptTerms -> handleAcceptTerms()
// TODO Add other actions here
}
}
private fun handleAcceptTerms() {
setState {
copy(
asyncRegistration = Loading()
)
}
currentTask = registrationWizard?.acceptTerms(object : MatrixCallback<RegistrationResult> {
override fun onSuccess(data: RegistrationResult) {
setState {
copy(
asyncRegistration = Success(data)
)
}
when (data) {
is RegistrationResult.Success -> onSessionCreated(data.session)
is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult)
}
}
override fun onFailure(failure: Throwable) {
// TODO Handled JobCancellationException
setState {
copy(
asyncRegistration = Fail(failure)
)
}
}
})
}
private fun handleRegisterWith(action: LoginAction.RegisterWith) {
setState {
copy(

View File

@ -0,0 +1,22 @@
/*
* 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.login.terms
import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
data class LocalizedFlowDataLoginTermsChecked(val localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms,
var checked: Boolean = false)

View File

@ -0,0 +1,104 @@
/*
* 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.login.terms
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import butterknife.OnClick
import com.airbnb.mvrx.args
import im.vector.riotx.R
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.features.login.AbstractLoginFragment
import im.vector.riotx.features.login.LoginAction
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_login_terms.*
import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
import javax.inject.Inject
@Parcelize
data class LoginTermsFragmentArgument(
val localizedFlowDataLoginTerms: List<LocalizedFlowDataLoginTerms>
) : Parcelable
/**
* LoginTermsFragment displays the list of policies the user has to accept
*/
class LoginTermsFragment @Inject constructor(private val policyController: PolicyController) : AbstractLoginFragment(),
PolicyController.PolicyControllerListener {
private val params: LoginTermsFragmentArgument by args()
override fun getLayoutResId() = R.layout.fragment_login_terms
private var loginTermsViewState: LoginTermsViewState = LoginTermsViewState(emptyList())
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loginTermsPolicyList.setController(policyController)
policyController.listener = this
val list = ArrayList<LocalizedFlowDataLoginTermsChecked>()
params.localizedFlowDataLoginTerms
.forEach {
list.add(LocalizedFlowDataLoginTermsChecked(it))
}
loginTermsViewState = LoginTermsViewState(list)
renderState()
}
private fun renderState() {
policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked)
// Button is enabled only if all checkboxes are checked
loginTermsSubmit.isEnabled = loginTermsViewState.allChecked()
}
override fun setChecked(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, isChecked: Boolean) {
if (isChecked) {
loginTermsViewState.check(localizedFlowDataLoginTerms)
} else {
loginTermsViewState.uncheck(localizedFlowDataLoginTerms)
}
renderState()
}
override fun openPolicy(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms) {
openUrlInExternalBrowser(requireContext(), localizedFlowDataLoginTerms.localizedUrl!!)
// This code crashed, because user is not authenticated yet
//val intent = VectorWebViewActivity.getIntent(requireContext(),
// localizedFlowDataLoginTerms.localizedUrl!!,
// localizedFlowDataLoginTerms.localizedName!!,
// WebViewMode.DEFAULT)
//startActivity(intent)
}
@OnClick(R.id.loginTermsSubmit)
internal fun submit() {
loginViewModel.handle(LoginAction.AcceptTerms)
}
override fun resetViewModel() {
// No op
}
}

View File

@ -0,0 +1,36 @@
/*
* 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.login.terms
import com.airbnb.mvrx.MvRxState
import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
data class LoginTermsViewState(
val localizedFlowDataLoginTermsChecked: List<LocalizedFlowDataLoginTermsChecked>
) : MvRxState {
fun check(data: LocalizedFlowDataLoginTerms) {
localizedFlowDataLoginTermsChecked.find { it.localizedFlowDataLoginTerms == data }?.checked = true
}
fun uncheck(data: LocalizedFlowDataLoginTerms) {
localizedFlowDataLoginTermsChecked.find { it.localizedFlowDataLoginTerms == data }?.checked = false
}
fun allChecked(): Boolean {
return localizedFlowDataLoginTermsChecked.all { it.checked }
}
}

View File

@ -0,0 +1,47 @@
/*
* 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.login.terms
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
import javax.inject.Inject
class PolicyController @Inject constructor() : TypedEpoxyController<List<LocalizedFlowDataLoginTermsChecked>>() {
var listener: PolicyControllerListener? = null
override fun buildModels(data: List<LocalizedFlowDataLoginTermsChecked>) {
data.forEach { entry ->
policy {
id(entry.localizedFlowDataLoginTerms.policyName)
checked(entry.checked)
title(entry.localizedFlowDataLoginTerms.localizedName!!)
clickListener(View.OnClickListener { listener?.openPolicy(entry.localizedFlowDataLoginTerms) })
checkChangeListener { _, isChecked ->
listener?.setChecked(entry.localizedFlowDataLoginTerms, isChecked)
}
}
}
}
interface PolicyControllerListener {
fun setChecked(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, isChecked: Boolean)
fun openPolicy(localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms)
}
}

View File

@ -0,0 +1,62 @@
/*
* 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.login.terms
import android.view.View
import android.widget.CheckBox
import android.widget.CompoundButton
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
@EpoxyModelClass(layout = R.layout.item_policy)
abstract class PolicyModel : EpoxyModelWithHolder<PolicyModel.Holder>() {
@EpoxyAttribute
var checked: Boolean = false
@EpoxyAttribute
var title: String? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var checkChangeListener: CompoundButton.OnCheckedChangeListener? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
holder.let {
it.checkbox.isChecked = checked
it.checkbox.setOnCheckedChangeListener(checkChangeListener)
it.title.text = title
it.view.setOnClickListener(clickListener)
}
}
// Ensure checkbox behaves as expected (remove the listener)
override fun unbind(holder: Holder) {
super.unbind(holder)
holder.checkbox.setOnCheckedChangeListener(null)
}
class Holder : VectorEpoxyHolder() {
val checkbox by bind<CheckBox>(R.id.adapter_item_policy_checkbox)
val title by bind<TextView>(R.id.adapter_item_policy_title)
}
}

View File

@ -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.riotx.features.login.terms
data class UrlAndName(
val url: String,
val name: String
)

View File

@ -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.login.terms
import im.vector.matrix.android.api.auth.registration.TermPolicies
import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
/**
* This method extract the policies from the login terms parameter, regarding the user language.
* For each policy, if user language is not found, the default language is used and if not found, the first url and name are used (not predictable)
*
* Example of Data:
* <pre>
* "m.login.terms": {
* "policies": {
* "privacy_policy": {
* "version": "1.0",
* "en": {
* "url": "http:\/\/matrix.org\/_matrix\/consent?v=1.0",
* "name": "Terms and Conditions"
* }
* }
* }
* }
*</pre>
*
* @param userLanguage the user language
* @param defaultLanguage the default language to use if the user language is not found for a policy in registrationFlowResponse
*/
fun TermPolicies.toLocalizedLoginTerms(userLanguage: String,
defaultLanguage: String = "en"): List<LocalizedFlowDataLoginTerms> {
val result = ArrayList<LocalizedFlowDataLoginTerms>()
val policies = get("policies")
if (policies is Map<*, *>) {
policies.keys.forEach { policyName ->
val localizedFlowDataLoginTerms = LocalizedFlowDataLoginTerms()
localizedFlowDataLoginTerms.policyName = policyName as String
val policy = policies[policyName]
// Enter this policy
if (policy is Map<*, *>) {
// Version
localizedFlowDataLoginTerms.version = policy["version"] as String?
var userLanguageUrlAndName: UrlAndName? = null
var defaultLanguageUrlAndName: UrlAndName? = null
var firstUrlAndName: UrlAndName? = null
// Search for language
policy.keys.forEach { policyKey ->
when (policyKey) {
"version" -> Unit // Ignore
userLanguage -> {
// We found the data for the user language
userLanguageUrlAndName = extractUrlAndName(policy[policyKey])
}
defaultLanguage -> {
// We found default language
defaultLanguageUrlAndName = extractUrlAndName(policy[policyKey])
}
else -> {
if (firstUrlAndName == null) {
// Get at least some data
firstUrlAndName = extractUrlAndName(policy[policyKey])
}
}
}
}
// Copy found language data by priority
when {
userLanguageUrlAndName != null -> {
localizedFlowDataLoginTerms.localizedUrl = userLanguageUrlAndName!!.url
localizedFlowDataLoginTerms.localizedName = userLanguageUrlAndName!!.name
}
defaultLanguageUrlAndName != null -> {
localizedFlowDataLoginTerms.localizedUrl = defaultLanguageUrlAndName!!.url
localizedFlowDataLoginTerms.localizedName = defaultLanguageUrlAndName!!.name
}
firstUrlAndName != null -> {
localizedFlowDataLoginTerms.localizedUrl = firstUrlAndName!!.url
localizedFlowDataLoginTerms.localizedName = firstUrlAndName!!.name
}
}
}
result.add(localizedFlowDataLoginTerms)
}
}
return result
}
private fun extractUrlAndName(policyData: Any?): UrlAndName? {
if (policyData is Map<*, *>) {
val url = policyData["url"] as String?
val name = policyData["name"] as String?
if (url != null && name != null) {
return UrlAndName(url, name)
}
}
return null
}

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/LoginContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/logoImageView"
style="@style/LoginTopIcon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/loginTermsNotice"
style="@style/TextAppearance.Vector.Login.Text.Small"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="27dp"
android:text="@string/auth_accept_policies"
app:layout_constraintTop_toBottomOf="@+id/logoImageView" />
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/loginTermsPolicyList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@+id/loginTermsSubmit"
app:layout_constraintTop_toBottomOf="@+id/loginTermsNotice"
tools:listitem="@layout/item_policy" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginTermsSubmit"
style="@style/Style.Vector.Login.Button"
android:text="@string/accept"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp">
<CheckBox
android:id="@+id/adapter_item_policy_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/adapter_item_policy_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:drawablePadding="8dp"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/adapter_item_policy_arrow"
app:layout_constraintStart_toEndOf="@+id/adapter_item_policy_checkbox"
app:layout_constraintTop_toTopOf="parent"
tools:text="Policy title" />
<!-- Do not use drawableEnd on the TextView because of RTL support -->
<ImageView
android:id="@+id/adapter_item_policy_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:rotationY="@integer/rtl_mirror_flip"
android:src="@drawable/ic_material_chevron_right_black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>