diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java index 277469a57375..191f0c142da2 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -33,6 +33,7 @@ import com.nextcloud.ui.SetStatusDialogFragment; import com.nextcloud.ui.composeActivity.ComposeActivity; import com.nextcloud.ui.fileactions.FileActionsBottomSheet; +import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet; import com.nmc.android.ui.LauncherActivity; import com.owncloud.android.MainApp; import com.owncloud.android.authentication.AuthenticatorActivity; @@ -97,6 +98,7 @@ import com.owncloud.android.ui.dialog.StoragePermissionDialogFragment; import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment; import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment; +import com.owncloud.android.ui.dialog.TermsOfServiceDialog; import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment; import com.owncloud.android.ui.fragment.ExtendedListFragment; import com.owncloud.android.ui.fragment.FeatureFragment; @@ -124,7 +126,6 @@ import com.owncloud.android.ui.preview.PreviewTextFragment; import com.owncloud.android.ui.preview.PreviewTextStringFragment; import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment; -import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet; import com.owncloud.android.ui.trashbin.TrashbinActivity; import androidx.annotation.OptIn; @@ -502,4 +503,6 @@ abstract class ComponentsModule { @ContributesAndroidInjector abstract BackgroundPlayerService backgroundPlayerService(); + @ContributesAndroidInjector + abstract TermsOfServiceDialog termsOfServiceDialog(); } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java index 9cf88983a05a..fa125ec8fea2 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -108,6 +108,7 @@ import com.owncloud.android.ui.dialog.SendShareDialog; import com.owncloud.android.ui.dialog.SortingOrderDialogFragment; import com.owncloud.android.ui.dialog.StoragePermissionDialogFragment; +import com.owncloud.android.ui.dialog.TermsOfServiceDialog; import com.owncloud.android.ui.events.SearchEvent; import com.owncloud.android.ui.events.SyncEventFinished; import com.owncloud.android.ui.events.TokenPushEvent; @@ -153,6 +154,7 @@ import javax.inject.Inject; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.SearchView; @@ -202,6 +204,7 @@ public class FileDisplayActivity extends FileActivity private static final String KEY_WAITING_TO_PREVIEW = "WAITING_TO_PREVIEW"; private static final String KEY_SYNC_IN_PROGRESS = "SYNC_IN_PROGRESS"; private static final String KEY_WAITING_TO_SEND = "WAITING_TO_SEND"; + private static final String DIALOG_TAG_SHOW_TOS = "DIALOG_TAG_SHOW_TOS"; public static final String ACTION_DETAILS = "com.owncloud.android.ui.activity.action.DETAILS"; @@ -773,11 +776,11 @@ public void showFileActions(OCFile file) { listOfFiles.onOverflowIconClicked(file, null); } - public @androidx.annotation.Nullable Fragment getLeftFragment() { + public @Nullable Fragment getLeftFragment() { return getSupportFragmentManager().findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES); } - public @androidx.annotation.Nullable + public @Nullable @Deprecated OCFileListFragment getListOfFilesFragment() { Fragment listOfFiles = getSupportFragmentManager().findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES); if (listOfFiles instanceof OCFileListFragment) { @@ -1433,6 +1436,11 @@ public void onReceive(Context context, Intent intent) { case HOST_NOT_AVAILABLE: showInfoBox(R.string.host_not_available); break; + + case SIGNING_TOS_NEEDED: + showTermsOfServiceDialog(); + + break; default: // nothing to do @@ -1477,6 +1485,11 @@ public void onReceive(Context context, Intent intent) { } } } + private void showTermsOfServiceDialog() { + if (getSupportFragmentManager().findFragmentByTag(DIALOG_TAG_SHOW_TOS) == null) { + new TermsOfServiceDialog().show(getSupportFragmentManager(), DIALOG_TAG_SHOW_TOS); + } + } private boolean checkForRemoteOperationError(RemoteOperationResult syncResult) { return ResultCode.UNAUTHORIZED == syncResult.getCode() || (syncResult.isException() && syncResult.getException() instanceof AuthenticatorException); diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/TermsOfServiceDialog.kt b/app/src/main/java/com/owncloud/android/ui/dialog/TermsOfServiceDialog.kt new file mode 100644 index 000000000000..5d0d9808e613 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/dialog/TermsOfServiceDialog.kt @@ -0,0 +1,142 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Tobias Kaminsky + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.dialog + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import androidx.core.app.ActivityCompat.finishAffinity +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.android.lib.resources.tos.GetTermsRemoteOperation +import com.nextcloud.android.lib.resources.tos.SignTermRemoteOperation +import com.nextcloud.android.lib.resources.tos.Term +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.network.ClientFactory +import com.nextcloud.common.NextcloudClient +import com.nextcloud.utils.extensions.setHtmlContent +import com.owncloud.android.R +import com.owncloud.android.databinding.DialogShowTosBinding +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class TermsOfServiceDialog : DialogFragment(), Injectable { + private lateinit var binding: DialogShowTosBinding + + @Inject + lateinit var clientFactory: ClientFactory + + @Inject + lateinit var accountManager: UserAccountManager + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + lateinit var client: NextcloudClient + lateinit var terms: List + lateinit var languages: Map + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + fetchTerms() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogShowTosBinding.inflate(requireActivity().layoutInflater) + + return createDialogBuilder().create() + } + + private fun updateDialog() { + binding.message.setHtmlContent(terms[0].renderedBody) + + val arrayAdapter: ArrayAdapter = ArrayAdapter( + binding.root.context, + android.R.layout.simple_spinner_item + ) + + for ((_, _, languageCode) in terms) { + arrayAdapter.add(languages[languageCode]) + } + + arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + + binding.languageDropdown.adapter = arrayAdapter + binding.languageDropdown.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(adapterView: AdapterView<*>?, view: View, position: Int, l: Long) { + binding.message + .setHtmlContent(terms[position].renderedBody) + } + + override fun onNothingSelected(adapterView: AdapterView<*>?) = Unit + } + + if (terms.size == 1) { + binding.languageDropdown.visibility = View.GONE + } + } + + private fun fetchTerms() { + // TODO use viewLifecycleOwner instead + CoroutineScope(Dispatchers.IO).launch { + try { + client = clientFactory.createNextcloudClient(accountManager.getUser()) + val result = GetTermsRemoteOperation().execute(client) + + if (result.isSuccess && + !result.resultData.hasSigned && + result.resultData.terms.isNotEmpty() + ) { + languages = result.resultData.languages + terms = result.resultData.terms + + withContext(Dispatchers.Main) { + updateDialog() + } + } + } catch (exception: ClientFactory.CreationException) { + Log_OC.e(TAG, "Error creating client!") + } + } + } + + private fun createDialogBuilder(): MaterialAlertDialogBuilder { + return MaterialAlertDialogBuilder(binding.root.context) + .setView(binding.root) + .setTitle(R.string.terms_of_service_title) + .setNegativeButton(R.string.dialog_close) { _, _ -> + activity?.let { finishAffinity(it) } + } + .setPositiveButton(R.string.terms_of_services_agree) { dialog, _ -> + dialog.dismiss() + Thread { + val id = binding.languageDropdown.selectedItemPosition + val signResult: RemoteOperationResult = + SignTermRemoteOperation(terms.get(id).id).execute(client) + if (!signResult.isSuccess) { + DisplayUtils.showSnackMessage(view, R.string.sign_tos_failed) + } + }.start() + } + } + + companion object { + private const val TAG = "TermsOfServiceDialog" + } +} diff --git a/app/src/main/res/layout/dialog_show_tos.xml b/app/src/main/res/layout/dialog_show_tos.xml new file mode 100644 index 000000000000..2fec6c724046 --- /dev/null +++ b/app/src/main/res/layout/dialog_show_tos.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 257ef2ac637f..99abc8965e4b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1310,4 +1310,8 @@ %1$d download remaining %1$d downloads remaining + Please sign ToS + Please manually check terms of service! + Terms of service + I agree to the above ToS diff --git a/build.gradle b/build.gradle index fb80281e6e08..0dbcfc8188e9 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ */ buildscript { ext { - androidLibraryVersion ="a885642fdd45c076c303e86eea33c7a7b1e91858" + androidLibraryVersion = "8f6d922b05437166ba187e2f68c5db3a29bc0727" androidPluginVersion = '8.8.0' androidxMediaVersion = '1.5.1' androidxTestVersion = "1.6.1" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 703fa7788fd9..408d6be0ea7f 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -9241,6 +9241,14 @@ + + + + + + + + @@ -9321,6 +9329,17 @@ + + + + + + + +