From e86460b57825f7e3b471505b50eed79913c3f3cf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 7 May 2020 17:37:27 +0200 Subject: [PATCH] Import and adapt Terms Of Service management: SDK and UI (compiling) --- .../matrix/android/api/session/Session.kt | 2 + .../api/session/terms/GetTermsResponse.kt | 24 +++ .../android/api/session/terms/TermsService.kt | 37 ++++ .../LocalizedFlowDataLoginTerms.kt | 2 +- .../internal/network/NetworkConstants.kt | 3 + .../internal/session/DefaultSession.kt | 3 + .../internal/session/SessionComponent.kt | 2 + .../identity/DefaultIdentityService.kt | 4 +- .../sync/model/accountdata/UserAccountData.kt | 1 + .../UserAccountDataAcceptedTerms.kt | 31 +++ .../internal/session/terms/AcceptTermsBody.kt | 29 +++ .../session/terms/DefaultTermsService.kt | 122 ++++++++++++ .../internal/session/terms/TermsAPI.kt | 39 ++++ .../internal/session/terms/TermsModule.kt | 46 +++++ .../internal/session/terms/TermsResponse.kt | 56 ++++++ .../accountdata/UpdateUserAccountDataTask.kt | 10 + vector/src/main/AndroidManifest.xml | 2 + .../im/vector/riotx/core/di/FragmentModule.kt | 8 +- .../vector/riotx/core/di/ScreenComponent.kt | 2 + .../riotx/core/platform/VectorBaseActivity.kt | 12 ++ .../discovery/DiscoverySettingsFragment.kt | 24 ++- .../change/SetIdentityServerFragment.kt | 22 +-- .../change/SetIdentityServerViewModel.kt | 51 ++--- .../LocalizedFlowDataLoginTermsChecked.kt | 2 +- .../login/terms/LoginTermsFragment.kt | 2 +- .../login/terms/LoginTermsViewState.kt | 2 +- .../features/login/terms/PolicyController.kt | 2 +- .../riotx/features/login/terms/converter.kt | 2 +- .../features/navigation/DefaultNavigator.kt | 8 + .../riotx/features/navigation/Navigator.kt | 5 + .../features/terms/ReviewTermsActivity.kt | 84 ++++++++ .../features/terms/ReviewTermsFragment.kt | 112 +++++++++++ .../features/terms/ReviewTermsViewModel.kt | 184 ++++++++++++++++++ .../riotx/features/terms/ServiceTermsArgs.kt | 27 +++ .../vector/riotx/features/terms/TermItem.kt | 60 ++++++ .../riotx/features/terms/TermsController.kt | 56 ++++++ .../main/res/layout/fragment_review_terms.xml | 63 ++++++ vector/src/main/res/layout/item_tos.xml | 54 +++++ 38 files changed, 1128 insertions(+), 67 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/GetTermsResponse.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/TermsService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAcceptedTerms.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsResponse.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsActivity.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/terms/ServiceTermsArgs.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/terms/TermItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/terms/TermsController.kt create mode 100644 vector/src/main/res/layout/fragment_review_terms.xml create mode 100644 vector/src/main/res/layout/item_tos.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 1143732b09..ee2823db47 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -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, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/GetTermsResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/GetTermsResponse.kt new file mode 100644 index 0000000000..29c6a7a921 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/GetTermsResponse.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/TermsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/TermsService.kt new file mode 100644 index 0000000000..36e6a411e3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/TermsService.kt @@ -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): Cancelable + + fun agreeToTerms(serviceType: ServiceType, + baseUrl: String, + agreedUrls: List, + token: String?, + callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt index 2cd52f702e..5bdc9579e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt index ab6745148f..1503190936 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt @@ -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/" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index edebc2987d..91b31d68a4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -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, private val pushRuleService: Lazy, private val pushersService: Lazy, + private val termsService: Lazy, private val cryptoService: Lazy, private val fileService: Lazy, private val secureStorageService: Lazy, @@ -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(), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index a6e3d9493c..ca8ab42ab8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -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, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt index 326b1fe595..ab264dd8b9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt @@ -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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt index ce46d3ba77..c2e36604e3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt @@ -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" } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAcceptedTerms.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAcceptedTerms.kt new file mode 100644 index 0000000000..ef34503463 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAcceptedTerms.kt @@ -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 = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt new file mode 100644 index 0000000000..c5827b822f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt new file mode 100644 index 0000000000..84b3eb4ef9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt @@ -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, + 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): 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(null) { + apiCall = termsAPI.getTerms("${url}terms") + } + + GetTermsResponse(termsResponse, getAlreadyAcceptedTermUrlsFromAccountData()) + } + } + + override fun agreeToTerms(serviceType: TermsService.ServiceType, + baseUrl: String, + agreedUrls: List, + token: String?, + callback: MatrixCallback): 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(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 { + return accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ACCEPTED_TERMS) + ?.content + ?.toModel() + ?.acceptedTerms + ?.toSet() + .orEmpty() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt new file mode 100644 index 0000000000..03b745f8d7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt @@ -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 + + /** + * This request requires authentication + */ + @POST + fun agreeToTerms(@Url url: String, @Body params: AcceptTermsBody, @Header(HttpHeaders.Authorization) token: String): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt new file mode 100644 index 0000000000..eee7e22134 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt @@ -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, + retrofitFactory: RetrofitFactory): TermsAPI { + val retrofit = retrofitFactory.create(unauthenticatedOkHttpClient, "https://foo.bar") + return retrofit.create(TermsAPI::class.java) + } + } + + @Binds + abstract fun bindTermsService(service: DefaultTermsService): TermsService +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsResponse.kt new file mode 100644 index 0000000000..86ebda88b5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsResponse.kt @@ -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 { + 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" + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt index 65babd6b2c..07242984b5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -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> diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index ae0ffa1f91..7c2939707f 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -161,6 +161,8 @@ android:name=".features.attachments.preview.AttachmentsPreviewActivity" android:theme="@style/AppTheme.AttachmentsPreview" /> + + VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { + viewEvents + .observe() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + observer(it) + } + .disposeOnDestroy() + } + /* ========================================================================================== * DATA * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt index 2a9cf5de0f..4878be65bc 100644 --- a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerFragment.kt b/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerFragment.kt index 0ea3f6ebe7..ffb4b94870 100644 --- a/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerFragment.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerViewModel.kt index 32bb418082..4d80166691 100644 --- a/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerViewModel.kt @@ -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 { - override fun onSuccess(info: GetTermsResponse) { + object : MatrixCallback { + 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 - ) - } - } }) - - */ } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt index 52aaa9d4a4..6659d65dd6 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt index 09746adc87..88f4fc2f5f 100755 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt index 104ea88daa..77293fbef6 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt index c301463c2a..42ed87a280 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt @@ -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>() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt index c9e6dcf3fd..d28d12eee8 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt @@ -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. diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index ac725eb850..b2213eb223 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index cc8e7cac34..224c6d4b19 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsActivity.kt b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsActivity.kt new file mode 100644 index 0000000000..2860956951 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsActivity.kt @@ -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)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsFragment.kt b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsFragment.kt new file mode 100644 index 0000000000..70d8ab8658 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsFragment.kt @@ -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) { + 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) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsViewModel.kt new file mode 100644 index 0000000000..dd8929869a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsViewModel.kt @@ -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> = Uninitialized, + val acceptingTerms: Async = 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(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: ReviewTermsViewState): ReviewTermsViewModel + } + + companion object : MvRxViewModelFactory { + + @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 { + 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 { + 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)) + } + }) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/terms/ServiceTermsArgs.kt b/vector/src/main/java/im/vector/riotx/features/terms/ServiceTermsArgs.kt new file mode 100644 index 0000000000..663b1a9050 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/ServiceTermsArgs.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/features/terms/TermItem.kt b/vector/src/main/java/im/vector/riotx/features/terms/TermItem.kt new file mode 100644 index 0000000000..d0081d6cd6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/TermItem.kt @@ -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() { + + @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(R.id.term_accept_checkbox) + val title by bind(R.id.term_name) + val description by bind(R.id.term_description) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/terms/TermsController.kt b/vector/src/main/java/im/vector/riotx/features/terms/TermsController.kt new file mode 100644 index 0000000000..5504009b8d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/TermsController.kt @@ -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>() { + + var description: String? = null + var listener: Listener? = null + + override fun buildModels(data: List?) { + 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) + } +} diff --git a/vector/src/main/res/layout/fragment_review_terms.xml b/vector/src/main/res/layout/fragment_review_terms.xml new file mode 100644 index 0000000000..4ff1752dbc --- /dev/null +++ b/vector/src/main/res/layout/fragment_review_terms.xml @@ -0,0 +1,63 @@ + + + + + + + +