Import and adapt Terms Of Service management: SDK and UI (compiling)

This commit is contained in:
Benoit Marty 2020-05-07 17:37:27 +02:00
parent 8dd5f88dba
commit e86460b578
38 changed files with 1128 additions and 67 deletions

View file

@ -40,6 +40,7 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer
import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.user.UserService
/**
@ -55,6 +56,7 @@ interface Session :
SignOutService,
FilterService,
FileService,
TermsService,
ProfileService,
PushRuleService,
PushersService,

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.terms
import im.vector.matrix.android.internal.session.terms.TermsResponse
data class GetTermsResponse(
val serverResponse: TermsResponse,
val alreadyAcceptedTermUrls: Set<String>
)

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.terms
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
interface TermsService {
enum class ServiceType {
IntegrationManager,
IdentityService
}
fun getTerms(serviceType: ServiceType,
baseUrl: String,
callback: MatrixCallback<GetTermsResponse>): Cancelable
fun agreeToTerms(serviceType: ServiceType,
baseUrl: String,
agreedUrls: List<String>,
token: String?,
callback: MatrixCallback<Unit>): Cancelable
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.matrix.androidsdk.rest.model.login
package im.vector.matrix.android.internal.auth.registration
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View file

@ -32,4 +32,7 @@ internal object NetworkConstants {
const val URI_IDENTITY_PATH_V2 = "_matrix/identity/v2/"
const val URI_API_PREFIX_IDENTITY = "_matrix/identity/api/v1"
// TODO Ganfra, use correct value
const val URI_INTEGRATION_MANAGER_PATH = "TODO/"
}

View file

@ -43,6 +43,7 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer
import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
@ -84,6 +85,7 @@ internal class DefaultSession @Inject constructor(
private val signOutService: Lazy<SignOutService>,
private val pushRuleService: Lazy<PushRuleService>,
private val pushersService: Lazy<PushersService>,
private val termsService: Lazy<TermsService>,
private val cryptoService: Lazy<DefaultCryptoService>,
private val fileService: Lazy<FileService>,
private val secureStorageService: Lazy<SecureStorageService>,
@ -112,6 +114,7 @@ internal class DefaultSession @Inject constructor(
PushRuleService by pushRuleService.get(),
PushersService by pushersService.get(),
FileService by fileService.get(),
TermsService by termsService.get(),
InitialSyncProgressService by initialSyncProgressService.get(),
SecureStorageService by secureStorageService.get(),
HomeServerCapabilitiesService by homeServerCapabilitiesService.get(),

View file

@ -52,6 +52,7 @@ import im.vector.matrix.android.internal.session.sync.SyncModule
import im.vector.matrix.android.internal.session.sync.SyncTask
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import im.vector.matrix.android.internal.session.terms.TermsModule
import im.vector.matrix.android.internal.session.user.UserModule
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule
import im.vector.matrix.android.internal.task.TaskExecutor
@ -74,6 +75,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
PushersModule::class,
OpenIdModule::class,
IdentityModule::class,
TermsModule::class,
AccountDataModule::class,
ProfileModule::class,
SessionAssistedInjectModule::class,

View file

@ -54,7 +54,7 @@ import javax.net.ssl.HttpsURLConnection
@SessionScope
internal class DefaultIdentityService @Inject constructor(
private val identityServiceStore: IdentityServiceStore,
private val openIdTokenTask: GetOpenIdTokenTask,
private val getOpenIdTokenTask: GetOpenIdTokenTask,
private val bulkLookupTask: BulkLookupTask,
private val identityRegisterTask: IdentityRegisterTask,
private val taskExecutor: TaskExecutor,
@ -210,7 +210,7 @@ internal class DefaultIdentityService @Inject constructor(
private suspend fun getIdentityServerToken(url: String) {
val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)
val openIdToken = openIdTokenTask.execute(Unit)
val openIdToken = getOpenIdTokenTask.execute(Unit)
val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken))
identityServiceStore.setToken(token.token)

View file

@ -31,5 +31,6 @@ abstract class UserAccountData : AccountDataContent {
const val TYPE_WIDGETS = "m.widgets"
const val TYPE_PUSH_RULES = "m.push_rules"
const val TYPE_IDENTITY_SERVER = "m.identity_server"
const val TYPE_ACCEPTED_TERMS = "m.accepted_terms"
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UserAccountDataAcceptedTerms(
@Json(name = "type") override val type: String = TYPE_ACCEPTED_TERMS,
@Json(name = "content") val content: AcceptedTermsContent
) : UserAccountData()
@JsonClass(generateAdapter = true)
internal data class AcceptedTermsContent(
@Json(name = "accepted") val acceptedTerms: List<String> = emptyList()
)

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.terms
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* This class represent a list of urls of terms the user wants to accept
*/
@JsonClass(generateAdapter = true)
internal data class AcceptTermsBody(
@Json(name = "user_accepts")
val acceptedTermUrls: List<String>
)

View file

@ -0,0 +1,122 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.terms
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.terms.GetTermsResponse
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.network.NetworkConstants
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI
import im.vector.matrix.android.internal.session.identity.IdentityRegisterTask
import im.vector.matrix.android.internal.session.identity.todelete.AccountDataDataSource
import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask
import im.vector.matrix.android.internal.session.sync.model.accountdata.AcceptedTermsContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import okhttp3.OkHttpClient
import javax.inject.Inject
internal class DefaultTermsService @Inject constructor(
@Unauthenticated
private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>,
private val accountDataDataSource: AccountDataDataSource,
private val termsAPI: TermsAPI,
private val retrofitFactory: RetrofitFactory,
private val getOpenIdTokenTask: GetOpenIdTokenTask,
private val identityRegisterTask: IdentityRegisterTask,
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers
) : TermsService {
override fun getTerms(serviceType: TermsService.ServiceType,
baseUrl: String,
callback: MatrixCallback<GetTermsResponse>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
val sep = if (baseUrl.endsWith("/")) "" else "/"
val url = when (serviceType) {
TermsService.ServiceType.IntegrationManager -> "$baseUrl$sep${NetworkConstants.URI_INTEGRATION_MANAGER_PATH}"
TermsService.ServiceType.IdentityService -> "$baseUrl$sep${NetworkConstants.URI_IDENTITY_PATH_V2}"
}
val termsResponse = executeRequest<TermsResponse>(null) {
apiCall = termsAPI.getTerms("${url}terms")
}
GetTermsResponse(termsResponse, getAlreadyAcceptedTermUrlsFromAccountData())
}
}
override fun agreeToTerms(serviceType: TermsService.ServiceType,
baseUrl: String,
agreedUrls: List<String>,
token: String?,
callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
val sep = if (baseUrl.endsWith("/")) "" else "/"
val url = when (serviceType) {
TermsService.ServiceType.IntegrationManager -> "$baseUrl$sep${NetworkConstants.URI_INTEGRATION_MANAGER_PATH}"
TermsService.ServiceType.IdentityService -> "$baseUrl$sep${NetworkConstants.URI_IDENTITY_PATH_V2}"
}
val tokenToUse = token?.takeIf { it.isNotEmpty() } ?: getToken(baseUrl)
executeRequest<Unit>(null) {
apiCall = termsAPI.agreeToTerms("${url}terms", AcceptTermsBody(agreedUrls), "Bearer $tokenToUse")
}
//client SHOULD update this account data section adding any the URLs
// of any additional documents that the user agreed to this list.
//Get current m.accepted_terms append new ones and update account data
val listOfAcceptedTerms = getAlreadyAcceptedTermUrlsFromAccountData()
val newList = listOfAcceptedTerms.toMutableSet().apply { addAll(agreedUrls) }.toList()
updateUserAccountDataTask.execute(UpdateUserAccountDataTask.AcceptedTermsParams(
acceptedTermsContent = AcceptedTermsContent(newList)
))
}
}
private suspend fun getToken(url: String): String {
// TODO This is duplicated code see DefaultIdentityService
val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)
val openIdToken = getOpenIdTokenTask.execute(Unit)
val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken))
return token.token
}
private fun getAlreadyAcceptedTermUrlsFromAccountData(): Set<String> {
return accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ACCEPTED_TERMS)
?.content
?.toModel<AcceptedTermsContent>()
?.acceptedTerms
?.toSet()
.orEmpty()
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.terms
import im.vector.matrix.android.internal.network.HttpHeaders
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Url
internal interface TermsAPI {
/**
* This request does not require authentication
*/
@GET
fun getTerms(@Url url: String): Call<TermsResponse>
/**
* This request requires authentication
*/
@POST
fun agreeToTerms(@Url url: String, @Body params: AcceptTermsBody, @Header(HttpHeaders.Authorization) token: String): Call<Unit>
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.terms
import dagger.Binds
import dagger.Lazy
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.session.SessionScope
import okhttp3.OkHttpClient
@Module
internal abstract class TermsModule {
@Module
companion object {
@Provides
@JvmStatic
@SessionScope
fun providesTermsAPI(@Unauthenticated unauthenticatedOkHttpClient: Lazy<OkHttpClient>,
retrofitFactory: RetrofitFactory): TermsAPI {
val retrofit = retrofitFactory.create(unauthenticatedOkHttpClient, "https://foo.bar")
return retrofit.create(TermsAPI::class.java)
}
}
@Binds
abstract fun bindTermsService(service: DefaultTermsService): TermsService
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.terms
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.auth.registration.LocalizedFlowDataLoginTerms
/**
* This class represent a localized privacy policy for registration Flow.
*/
@JsonClass(generateAdapter = true)
data class TermsResponse(
@Json(name = "policies")
val policies: JsonDict? = null
) {
fun getLocalizedTerms(userLanguage: String,
defaultLanguage: String = "en"): List<LocalizedFlowDataLoginTerms> {
return policies?.map {
val tos = policies[it.key] as? Map<*, *> ?: return@map null
((tos[userLanguage] ?: tos[defaultLanguage]) as? Map<*, *>)?.let { termsMap ->
val name = termsMap[NAME] as? String
val url = termsMap[URL] as? String
LocalizedFlowDataLoginTerms(
policyName = it.key,
localizedUrl = url,
localizedName = name,
version = tos[VERSION] as? String
)
}
}?.filterNotNull() ?: emptyList()
}
private companion object {
const val VERSION = "version"
const val NAME = "name"
const val URL = "url"
}
}

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.user.accountdata
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.sync.model.accountdata.AcceptedTermsContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
@ -41,6 +42,15 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa
}
}
data class AcceptedTermsParams(override val type: String = UserAccountData.TYPE_ACCEPTED_TERMS,
private val acceptedTermsContent: AcceptedTermsContent
) : Params {
override fun getData(): Any {
return acceptedTermsContent
}
}
// TODO Use [UserAccountDataDirectMessages] class?
data class DirectChatParams(override val type: String = UserAccountData.TYPE_DIRECT_MESSAGES,
private val directMessages: Map<String, List<String>>

View file

@ -161,6 +161,8 @@
android:name=".features.attachments.preview.AttachmentsPreviewActivity"
android:theme="@style/AppTheme.AttachmentsPreview" />
<activity android:name=".features.terms.ReviewTermsActivity" />
<!-- Services -->
<service

View file

@ -22,7 +22,6 @@ import androidx.fragment.app.FragmentFactory
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import im.vector.riotx.features.discovery.DiscoverySettingsFragment
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment
@ -42,6 +41,7 @@ import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeF
import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQRWaitingFragment
import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQrScannedByOtherFragment
import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment
import im.vector.riotx.features.discovery.DiscoverySettingsFragment
import im.vector.riotx.features.discovery.change.SetIdentityServerFragment
import im.vector.riotx.features.grouplist.GroupListFragment
import im.vector.riotx.features.home.HomeDetailFragment
@ -98,6 +98,7 @@ import im.vector.riotx.features.share.IncomingShareFragment
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
import im.vector.riotx.features.userdirectory.KnownUsersFragment
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
import im.vector.riotx.features.terms.ReviewTermsFragment
@Module
interface FragmentModule {
@ -486,4 +487,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(DiscoverySettingsFragment::class)
fun bindDiscoverySettingsFragment(fragment: DiscoverySettingsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(ReviewTermsFragment::class)
fun bindReviewTermsFragment(fragment: ReviewTermsFragment): Fragment
}

View file

@ -61,6 +61,7 @@ import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.settings.devices.DeviceVerificationInfoBottomSheet
import im.vector.riotx.features.share.IncomingShareActivity
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
import im.vector.riotx.features.terms.ReviewTermsActivity
import im.vector.riotx.features.ui.UiStateRepository
@Component(
@ -118,6 +119,7 @@ interface ScreenComponent {
fun inject(activity: SharedSecureStorageActivity)
fun inject(activity: BigImageViewerActivity)
fun inject(activity: InviteUsersToRoomActivity)
fun inject(activity: ReviewTermsActivity)
/* ==========================================================================================
* BottomSheets

View file

@ -70,6 +70,7 @@ import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.themes.ActivityOtherThemes
import im.vector.riotx.features.themes.ThemeUtils
import im.vector.riotx.receivers.DebugReceiver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import timber.log.Timber
@ -94,6 +95,17 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
protected val viewModelProvider
get() = ViewModelProvider(this, viewModelFactory)
// TODO Other Activity should use this also
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
observer(it)
}
.disposeOnDestroy()
}
/* ==========================================================================================
* DATA
* ========================================================================================== */

View file

@ -15,6 +15,7 @@
*/
package im.vector.riotx.features.discovery
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.View
@ -23,6 +24,7 @@ import androidx.lifecycle.Observer
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
@ -30,6 +32,8 @@ import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.discovery.change.SetIdentityServerFragment
import im.vector.riotx.features.discovery.change.SetIdentityServerViewModel
import im.vector.riotx.features.terms.ReviewTermsActivity
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject
@ -86,31 +90,25 @@ class DiscoverySettingsFragment @Inject constructor(
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
/* TODO
if (requestCode == TERMS_REQUEST_CODE) {
if (requestCode == ReviewTermsActivity.TERMS_REQUEST_CODE) {
if (Activity.RESULT_OK == resultCode) {
viewModel.refreshModel()
viewModel.handle(DiscoverySettingsAction.RetrieveBinding)
} else {
//add some error?
}
}
*/
super.onActivityResult(requestCode, resultCode, data)
}
override fun onSelectIdentityServer() = withState(viewModel) { state ->
if (state.termsNotSigned) {
/*
TODO
ReviewTermsActivity.intent(requireContext(),
TermsManager.ServiceType.IdentityService,
// TODO Use ViewEvents?
navigator.openTerms(
this,
TermsService.ServiceType.IdentityService,
SetIdentityServerViewModel.sanitatizeBaseURL(state.identityServer() ?: ""),
null).also {
startActivityForResult(it, TERMS_REQUEST_CODE)
}
*/
null)
}
}

View file

@ -15,6 +15,7 @@
*/
package im.vector.riotx.features.discovery.change
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.text.Editable
@ -30,10 +31,12 @@ import butterknife.OnTextChanged
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.textfield.TextInputLayout
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.discovery.DiscoverySharedViewModel
import im.vector.riotx.features.terms.ReviewTermsActivity
import javax.inject.Inject
class SetIdentityServerFragment @Inject constructor(
@ -66,7 +69,7 @@ class SetIdentityServerFragment @Inject constructor(
mProgressBar.isVisible = false
}
val newText = state.newIdentityServer ?: ""
if (!newText.equals(mKeyTextEdit.text.toString())) {
if (newText != mKeyTextEdit.text.toString()) {
mKeyTextEdit.setText(newText)
}
mKeyInputLayout.error = state.errorMessageId?.let { getString(it) }
@ -122,29 +125,24 @@ class SetIdentityServerFragment @Inject constructor(
}
is SetIdentityServerViewEvents.ShowTerms -> {
/* TODO
ReviewTermsActivity.intent(requireContext(),
TermsManager.ServiceType.IdentityService,
SetIdentityServerViewModel.sanitatizeBaseURL(event.newIdentityServer),
null).also {
startActivityForResult(it, TERMS_REQUEST_CODE)
}
*/
navigator.openTerms(
this,
TermsService.ServiceType.IdentityService,
SetIdentityServerViewModel.sanitatizeBaseURL(it.newIdentityServer),
null)
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
/* TODO
if (requestCode == TERMS_REQUEST_CODE) {
if (requestCode == ReviewTermsActivity.TERMS_REQUEST_CODE) {
if (Activity.RESULT_OK == resultCode) {
processIdentityServerChange()
} else {
//add some error?
}
}
*/
super.onActivityResult(requestCode, resultCode, data)
}

View file

@ -22,7 +22,11 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.terms.GetTermsResponse
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.riotx.R
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewEvents
@ -102,41 +106,42 @@ class SetIdentityServerViewModel @AssistedInject constructor(
}
return@withState
}
// TODO baseUrl = sanitatizeBaseURL(baseUrl)
baseUrl = sanitatizeBaseURL(baseUrl)
setState {
copy(isVerifyingServer = true)
}
/* TODO
mxSession.termsManager.get(TermsManager.ServiceType.IdentityService,
mxSession.getTerms(TermsService.ServiceType.IdentityService,
baseUrl,
object : ApiCallback<GetTermsResponse> {
override fun onSuccess(info: GetTermsResponse) {
object : MatrixCallback<GetTermsResponse> {
override fun onSuccess(data: GetTermsResponse) {
//has all been accepted?
setState {
copy(isVerifyingServer = false)
}
val resp = info.serverResponse
val resp = data.serverResponse
val tos = resp.getLocalizedTerms(userLanguage)
if (tos.isEmpty()) {
//prompt do not define policy
navigateEvent.value = LiveEvent(NavigateEvent.NoTerms)
_viewEvents.post(SetIdentityServerViewEvents.NoTerms)
} else {
val shouldPrompt = tos.any { !info.alreadyAcceptedTermUrls.contains(it.localizedUrl) }
val shouldPrompt = tos.any { !data.alreadyAcceptedTermUrls.contains(it.localizedUrl) }
if (shouldPrompt) {
navigateEvent.value = LiveEvent(NavigateEvent.ShowTerms(baseUrl))
_viewEvents.post(SetIdentityServerViewEvents.ShowTerms(baseUrl))
} else {
navigateEvent.value = LiveEvent(NavigateEvent.TermsAccepted)
_viewEvents.post(SetIdentityServerViewEvents.TermsAccepted)
}
}
}
override fun onUnexpectedError(e: Exception) {
if (e is HttpException && e.httpError.httpCode == 404) {
override fun onFailure(failure: Throwable) {
if (failure is Failure.OtherServerError && failure.httpCode == 404) {
setState {
copy(isVerifyingServer = false)
}
navigateEvent.value = LiveEvent(NavigateEvent.NoTerms)
// 404: Same as NoTerms
// TODO Handle the case where identity
_viewEvents.post(SetIdentityServerViewEvents.NoTerms)
} else {
setState {
copy(
@ -146,26 +151,6 @@ class SetIdentityServerViewModel @AssistedInject constructor(
}
}
}
override fun onNetworkError(e: Exception) {
setState {
copy(
isVerifyingServer = false,
errorMessageId = R.string.settings_discovery_bad_identity_server
)
}
}
override fun onMatrixError(e: MatrixError) {
setState {
copy(
isVerifyingServer = false,
errorMessageId = R.string.settings_discovery_bad_identity_server
)
}
}
})
*/
}
}

View file

@ -16,7 +16,7 @@
package im.vector.riotx.features.login.terms
import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
import im.vector.matrix.android.internal.auth.registration.LocalizedFlowDataLoginTerms
data class LocalizedFlowDataLoginTermsChecked(val localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms,
var checked: Boolean = false)

View file

@ -31,7 +31,7 @@ import im.vector.riotx.features.login.LoginAction
import im.vector.riotx.features.login.LoginViewState
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_login_terms.*
import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
import im.vector.matrix.android.internal.auth.registration.LocalizedFlowDataLoginTerms
import javax.inject.Inject
@Parcelize

View file

@ -17,7 +17,7 @@
package im.vector.riotx.features.login.terms
import com.airbnb.mvrx.MvRxState
import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
import im.vector.matrix.android.internal.auth.registration.LocalizedFlowDataLoginTerms
data class LoginTermsViewState(
val localizedFlowDataLoginTermsChecked: List<LocalizedFlowDataLoginTermsChecked>

View file

@ -18,7 +18,7 @@ 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 im.vector.matrix.android.internal.auth.registration.LocalizedFlowDataLoginTerms
import javax.inject.Inject
class PolicyController @Inject constructor() : TypedEpoxyController<List<LocalizedFlowDataLoginTermsChecked>>() {

View file

@ -17,7 +17,7 @@
package im.vector.riotx.features.login.terms
import im.vector.matrix.android.api.auth.registration.TermPolicies
import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms
import im.vector.matrix.android.internal.auth.registration.LocalizedFlowDataLoginTerms
/**
* This method extract the policies from the login terms parameter, regarding the user language.

View file

@ -23,8 +23,10 @@ import android.view.View
import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.TaskStackBuilder
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
@ -52,6 +54,7 @@ import im.vector.riotx.features.roomprofile.RoomProfileActivity
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.terms.ReviewTermsActivity
import javax.inject.Inject
import javax.inject.Singleton
@ -207,6 +210,11 @@ class DefaultNavigator @Inject constructor(
}
}
override fun openTerms(fragment: Fragment, serviceType: TermsService.ServiceType, baseUrl: String, token: String?, requestCode: Int) {
val intent = ReviewTermsActivity.intent(fragment.requireContext(), serviceType, baseUrl, token)
fragment.startActivityForResult(intent, requestCode)
}
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
if (buildTask) {
val stackBuilder = TaskStackBuilder.create(context)

View file

@ -19,10 +19,13 @@ package im.vector.riotx.features.navigation
import android.app.Activity
import android.content.Context
import android.view.View
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.terms.ReviewTermsActivity
interface Navigator {
@ -67,4 +70,6 @@ interface Navigator {
fun openRoomProfile(context: Context, roomId: String)
fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem)
fun openTerms(fragment: Fragment,serviceType: TermsService.ServiceType, baseUrl: String, token: String?, requestCode : Int = ReviewTermsActivity.TERMS_REQUEST_CODE)
}

View file

@ -0,0 +1,84 @@
/*
* Copyright (c) 2020 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.terms
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.viewModel
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.SimpleFragmentActivity
import javax.inject.Inject
class ReviewTermsActivity : SimpleFragmentActivity() {
@Inject lateinit var errorFormatter: ErrorFormatter
@Inject lateinit var viewModelFactory: ReviewTermsViewModel.Factory
private val reviewTermsViewModel: ReviewTermsViewModel by viewModel()
override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
}
override fun initUiAndData() {
super.initUiAndData()
if (isFirstCreation()) {
replaceFragment(R.id.container, ReviewTermsFragment::class.java)
}
reviewTermsViewModel.termsArgs = intent.getParcelableExtra(EXTRA_INFO) ?: error("Missing parameter")
reviewTermsViewModel.observeViewEvents {
when (it) {
is ReviewTermsViewEvents.Failure -> {
AlertDialog.Builder(this)
.setMessage(errorFormatter.toHumanReadable(it.throwable))
.setPositiveButton(R.string.ok) { _, _ ->
if (it.finish) {
finish()
}
}
.show()
Unit
}
ReviewTermsViewEvents.Success -> {
setResult(Activity.RESULT_OK)
finish()
}
}.exhaustive
}
}
companion object {
const val TERMS_REQUEST_CODE = 15000
private const val EXTRA_INFO = "EXTRA_INFO"
fun intent(context: Context, serviceType: TermsService.ServiceType, baseUrl: String, token: String?): Intent {
return Intent(context, ReviewTermsActivity::class.java).also {
it.putExtra(EXTRA_INFO, ServiceTermsArgs(serviceType, baseUrl, token))
}
}
}
}

View file

@ -0,0 +1,112 @@
/*
* Copyright (c) 2020 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.terms
import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.onClick
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import kotlinx.android.synthetic.main.fragment_review_terms.*
import javax.inject.Inject
class ReviewTermsFragment @Inject constructor(
private val termsController: TermsController
) : VectorBaseFragment(), TermsController.Listener {
private val reviewTermsViewModel: ReviewTermsViewModel by activityViewModel()
override fun getLayoutResId() = R.layout.fragment_review_terms
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
termsController.description = when (reviewTermsViewModel.termsArgs.type) {
TermsService.ServiceType.IdentityService -> getString(R.string.terms_description_for_identity_server)
TermsService.ServiceType.IntegrationManager -> getString(R.string.terms_description_for_integration_manager)
}
termsController.listener = this
reviewTermsRecyclerView.configureWith(termsController)
reviewTermsAccept.onClick { reviewTermsViewModel.handle(ReviewTermsAction.Accept) }
reviewTermsDecline.onClick { activity?.finish() }
reviewTermsViewModel.observeViewEvents {
when (it) {
is ReviewTermsViewEvents.Failure -> {
reviewTermsWaitOverlay.isVisible = false
// Dialog is displayed by the Activity
}
ReviewTermsViewEvents.Success -> {
// Handled by the Activity
}
}.exhaustive
}
reviewTermsViewModel.handle(ReviewTermsAction.LoadTerms(getString(R.string.resources_language)))
}
override fun onDestroyView() {
reviewTermsRecyclerView.cleanup()
termsController.listener = null
super.onDestroyView()
}
override fun invalidate() = withState(reviewTermsViewModel) { state ->
when (state.termsList) {
is Loading -> {
reviewTermsBottomBar.isVisible = false
reviewTermsWaitOverlay.isVisible = true
}
is Success -> {
updateState(state.termsList.invoke())
reviewTermsWaitOverlay.isVisible = false
reviewTermsBottomBar.isVisible = true
reviewTermsAccept.isEnabled = state.termsList.invoke().all { it.accepted }
}
else -> {
reviewTermsWaitOverlay.isVisible = false
}
}
if (!reviewTermsWaitOverlay.isVisible) {
reviewTermsWaitOverlay.isVisible = state.acceptingTerms is Loading
}
}
private fun updateState(terms: List<Term>) {
termsController.setData(terms)
}
override fun setChecked(term: Term, isChecked: Boolean) {
reviewTermsViewModel.handle(ReviewTermsAction.MarkTermAsAccepted(term.url, isChecked))
}
override fun review(term: Term) {
openUrlInExternalBrowser(requireContext(), term.url)
}
}

View file

@ -0,0 +1,184 @@
/*
* Copyright (c) 2020 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.terms
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.terms.GetTermsResponse
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
import timber.log.Timber
data class Term(
val url: String,
val name: String,
val version: String? = null,
val accepted: Boolean = false
)
data class ReviewTermsViewState(
val termsList: Async<List<Term>> = Uninitialized,
val acceptingTerms: Async<Unit> = Uninitialized
) : MvRxState
sealed class ReviewTermsAction : VectorViewModelAction {
data class LoadTerms(val preferredLanguageCode: String) : ReviewTermsAction()
data class MarkTermAsAccepted(val url: String, val accepted: Boolean) : ReviewTermsAction()
object Accept : ReviewTermsAction()
}
sealed class ReviewTermsViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable, val finish: Boolean) : ReviewTermsViewEvents()
object Success : ReviewTermsViewEvents()
}
class ReviewTermsViewModel @AssistedInject constructor(
@Assisted initialState: ReviewTermsViewState,
private val session: Session
) : VectorViewModel<ReviewTermsViewState, ReviewTermsAction, ReviewTermsViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: ReviewTermsViewState): ReviewTermsViewModel
}
companion object : MvRxViewModelFactory<ReviewTermsViewModel, ReviewTermsViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: ReviewTermsViewState): ReviewTermsViewModel? {
val activity: ReviewTermsActivity = (viewModelContext as ActivityViewModelContext).activity()
return activity.viewModelFactory.create(state)
}
}
lateinit var termsArgs: ServiceTermsArgs
override fun handle(action: ReviewTermsAction) {
when (action) {
is ReviewTermsAction.LoadTerms -> loadTerms(action)
is ReviewTermsAction.MarkTermAsAccepted -> markTermAsAccepted(action)
ReviewTermsAction.Accept -> acceptTerms()
}.exhaustive
}
private fun markTermAsAccepted(action: ReviewTermsAction.MarkTermAsAccepted) = withState { state ->
val newList = state.termsList.invoke()?.map {
if (it.url == action.url) {
it.copy(accepted = action.accepted)
} else {
it
}
}
if (newList != null) {
setState {
state.copy(
termsList = Success(newList)
)
}
}
}
private fun acceptTerms() = withState { state ->
val acceptedTerms = state.termsList.invoke() ?: return@withState
if (acceptedTerms.any { it.accepted.not() }) {
// Should not happen
_viewEvents.post(ReviewTermsViewEvents.Failure(IllegalStateException("Please accept all terms"), false))
return@withState
}
setState {
copy(
acceptingTerms = Loading()
)
}
val agreedUrls = acceptedTerms.map { it.url }
session.agreeToTerms(
termsArgs.type,
termsArgs.baseURL,
agreedUrls,
termsArgs.token,
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_viewEvents.post(ReviewTermsViewEvents.Success)
}
override fun onFailure(failure: Throwable) {
Timber.e(failure, "Failed to agree to terms")
setState {
copy(
acceptingTerms = Uninitialized
)
}
_viewEvents.post(ReviewTermsViewEvents.Failure(failure, false))
}
}
)
}
private fun loadTerms(action: ReviewTermsAction.LoadTerms) = withState { state ->
if (state.termsList is Loading || state.termsList is Success) {
return@withState
}
setState {
copy(termsList = Loading())
}
session.getTerms(termsArgs.type, termsArgs.baseURL, object : MatrixCallback<GetTermsResponse> {
override fun onSuccess(data: GetTermsResponse) {
val terms = data.serverResponse.getLocalizedTerms(action.preferredLanguageCode).map {
Term(it.localizedUrl ?: "",
it.localizedName ?: "",
it.version,
accepted = data.alreadyAcceptedTermUrls.contains(it.localizedUrl)
)
}
setState {
copy(
termsList = Success(terms)
)
}
}
override fun onFailure(failure: Throwable) {
Timber.e(failure, "Failed to agree to terms")
setState {
copy(
termsList = Uninitialized
)
}
_viewEvents.post(ReviewTermsViewEvents.Failure(failure, true))
}
})
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2020 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.terms
import android.os.Parcelable
import im.vector.matrix.android.api.session.terms.TermsService
import kotlinx.android.parcel.Parcelize
@Parcelize
data class ServiceTermsArgs(
val type: TermsService.ServiceType,
val baseURL: String,
val token: String? = null
) : Parcelable

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2020 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.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_tos)
abstract class TermItem : EpoxyModelWithHolder<TermItem.Holder>() {
@EpoxyAttribute
var checked: Boolean = false
@EpoxyAttribute
var name: String? = null
@EpoxyAttribute
var description: 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.checkbox.isChecked = checked
holder.title.text = name
holder.description.text = description
holder.checkbox.setOnCheckedChangeListener(checkChangeListener)
holder.view.setOnClickListener(clickListener)
}
class Holder : VectorEpoxyHolder() {
val checkbox by bind<CheckBox>(R.id.term_accept_checkbox)
val title by bind<TextView>(R.id.term_name)
val description by bind<TextView>(R.id.term_description)
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2020 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.terms
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.riotx.R
import im.vector.riotx.features.discovery.settingsSectionTitle
import javax.inject.Inject
class TermsController @Inject constructor() : TypedEpoxyController<List<Term>>() {
var description: String? = null
var listener: Listener? = null
override fun buildModels(data: List<Term>?) {
data?.let {
settingsSectionTitle {
id("header")
titleResId(R.string.widget_integration_review_terms)
}
it.forEach { term ->
termItem {
id(term.url)
name(term.name)
description(description)
checked(term.accepted)
clickListener(View.OnClickListener { listener?.review(term) })
checkChangeListener { _, isChecked ->
listener?.setChecked(term, isChecked)
}
}
}
}
//TODO error mgmt
}
interface Listener {
fun setChecked(term: Term, isChecked: Boolean)
fun review(term: Term)
}
}

View file

@ -0,0 +1,63 @@
<?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="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/reviewTermsRecyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/reviewTermsBottomBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/item_tos" />
<LinearLayout
android:id="@+id/reviewTermsBottomBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
android:gravity="center_vertical|end"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingTop="@dimen/layout_vertical_margin"
android:paddingEnd="16dp"
android:paddingBottom="@dimen/layout_vertical_margin"
app:layout_constraintBottom_toBottomOf="parent">
<Button
android:id="@+id/reviewTermsDecline"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/decline" />
<Button
android:id="@+id/reviewTermsAccept"
style="@style/VectorButtonStyle"
android:layout_width="wrap_content"
android:text="@string/accept" />
</LinearLayout>
<FrameLayout
android:id="@+id/reviewTermsWaitOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true"
android:visibility="gone"
tools:visibility="visible">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,54 @@
<?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:background="?attr/selectableItemBackground"
android:padding="16dp">
<CheckBox
android:id="@+id/term_accept_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/term_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:textColor="?riotx_text_primary"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/term_policy_arrow"
app:layout_constraintStart_toEndOf="@id/term_accept_checkbox"
app:layout_constraintTop_toTopOf="parent"
tools:text="Integration Manager" />
<TextView
android:id="@+id/term_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:textColor="?riotx_text_secondary"
app:layout_constraintEnd_toStartOf="@id/term_policy_arrow"
app:layout_constraintStart_toStartOf="@+id/term_name"
app:layout_constraintTop_toBottomOf="@+id/term_name"
tools:text="Use bots, bridges, widget and sticker packs." />
<!-- Do not use drawableEnd on the TextView because of RTL support -->
<ImageView
android:id="@+id/term_policy_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:rotationY="@integer/rtl_mirror_flip"
android:src="@drawable/ic_material_chevron_right_black"
android:tint="?riotx_text_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>