diff --git a/.github/workflows/gradle_build.yml b/.github/workflows/gradle_build.yml index bceb2fc5..148fadb0 100644 --- a/.github/workflows/gradle_build.yml +++ b/.github/workflows/gradle_build.yml @@ -1,8 +1,6 @@ # This workflow ensures the building step works # # @author Armin Schnabel -# @version 1.0.0 -# @since 3.2.0 name: Gradle Build on: @@ -48,6 +46,7 @@ jobs: echo "githubToken=${{ secrets.GH_READ_TOKEN }}" >> gradle.properties # This mock API accepts all credentials and allows the UI test to skip the login echo "cyface.staging_api=https://demo.cyface.de/api/v2" >> gradle.properties + echo "cyface.staging_provider_api=https://demo.cyface.de/provider" >> gradle.properties echo "cyface.staging_incentives_api=https://demo.cyface.de/incentives" >> gradle.properties echo "cyface.staging_oauth_discovery=https://demo.cyface.de/auth" >> gradle.properties echo "cyface.staging_user=guestLogin" >> gradle.properties diff --git a/.github/workflows/gradle_connected-tests.yml b/.github/workflows/gradle_connected-tests.yml index e967fede..c6ccce70 100644 --- a/.github/workflows/gradle_connected-tests.yml +++ b/.github/workflows/gradle_connected-tests.yml @@ -1,8 +1,6 @@ # This workflow ensures the connected tests keep working # # @author Armin Schnabel -# @version 1.0.0 -# @since 3.2.0 name: Gradle Connected Tests on: @@ -87,6 +85,7 @@ jobs: run: | # This mock API accepts all credentials and allows the UI test to skip the login echo "cyface.staging_api=https://demo.cyface.de/api/v2" >> gradle.properties + echo "cyface.staging_provider_api=https://demo.cyface.de/provider" >> gradle.properties echo "cyface.staging_incentives_api=https://demo.cyface.de/incentives" >> gradle.properties echo "cyface.staging_oauth_discovery=https://demo.cyface.de/auth" >> gradle.properties echo "cyface.staging_user=guestLogin" >> gradle.properties diff --git a/.github/workflows/gradle_publish.yml b/.github/workflows/gradle_publish.yml index ad034e24..dd1e4b5d 100644 --- a/.github/workflows/gradle_publish.yml +++ b/.github/workflows/gradle_publish.yml @@ -1,8 +1,6 @@ # This workflow creates a signed Android Bundle which can be uploaded to Play Store Console # # @author Armin Schnabel -# @version 1.0.0 -# @since 3.2.0 name: Gradle Publish on: @@ -51,6 +49,7 @@ jobs: echo "githubToken=${{ secrets.GITHUB_TOKEN }}" >> gradle.properties # Inject Cyface APIs URL echo "cyface.api=${{ secrets.PUBLIC_API }}" >> gradle.properties + echo "cyface.provider_api=${{ secrets.PUBLIC_PROVIDER_API }}" >> gradle.properties echo "cyface.incentives_api=${{ secrets.PUBLIC_INCENTIVES_API }}" >> gradle.properties echo "cyface.oauth_discovery=${{ secrets.PUBLIC_OAUTH_DISCOVERY }}" >> gradle.properties # Inject OAuth redirect URIs diff --git a/backend b/backend index e81ac271..fece2976 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit e81ac2713bea3b98e2819a96e2669f658ad0ef1e +Subproject commit fece29764e06f81e5ee9ee9e456440e4934baec3 diff --git a/build.gradle b/build.gradle index 9881c361..1272536f 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ ext { cyfaceUploaderVersion = "1.3.0" // These versions just document the currently linked version of this dependency. // To upgrade the dependencies you need to update the submodules to a newer commit. - cyfaceAndroidBackendVersion = "7.11.2" + cyfaceAndroidBackendVersion = "7.12.0" cyfaceEnergySettingsVersion = "4.0.6" cyfaceCameraServiceVersion = "4.5.7" diff --git a/ui/cyface/build.gradle b/ui/cyface/build.gradle index 8b271edf..f41334e3 100644 --- a/ui/cyface/build.gradle +++ b/ui/cyface/build.gradle @@ -62,8 +62,6 @@ android { vectorDrawables.useSupportLibrary = true buildConfigField("String", "localHostIp", '"10.0.2.2"') - // we don't describe this provider build variable in the README so it's probably deprecated - // buildConfigField("String", "provider", '"de.cyface.app.provider"') // If our terms change that much that they to be re-accepted, increase this (see Confluence!) buildConfigField "int", "currentTerms", "5" @@ -92,22 +90,26 @@ android { // Phone - to local collector - ! only if iptables allow connection from outside //buildConfigField "String", "cyfaceServer", "\"${project.findProperty('cyface.local_api')}\"" + //buildConfigField "String", "providerServer", "\"${project.findProperty('cyface.local_provider_api')}\"" //buildConfigField "String", "oauthDiscovery", "\"${project.findProperty('cyface.local_oauth_discovery')}\"" //manifestPlaceholders = [usesCleartextTraffic:"true"] // Phone - to local production - ! only if iptables allow connection from outside // CertPathValidatorException: Trust anchor for certification path not found. //buildConfigField "String", "cyfaceServer", "\"${project.findProperty('cyface.local_production_api')}\"" + //buildConfigField "String", "providerServer", "\"${project.findProperty('cyface.local_provider_api')}\"" //buildConfigField "String", "oauthDiscovery", "\"${project.findProperty('cyface.local_oauth_discovery')}\"" //manifestPlaceholders = [usesCleartextTraffic:"false"] // EMULATOR - to local collector //buildConfigField "String", "cyfaceServer", "\"${project.findProperty('cyface.emulator_api')}\"" + //buildConfigField "String", "providerServer", "\"${project.findProperty('cyface.emulator_provider_api')}\"" //buildConfigField "String", "oauthDiscovery", "\"${project.findProperty('cyface.emulator_oauth_discovery')}\"" //manifestPlaceholders = [usesCleartextTraffic:"true"] // for local collector testing // Staging buildConfigField "String", "cyfaceServer", "\"${project.findProperty('cyface.staging_api')}\"" + buildConfigField "String", "providerServer", "\"${project.findProperty('cyface.staging_provider_api')}\"" buildConfigField "String", "incentivesServer", "\"${project.findProperty('cyface.staging_incentives_api')}\"" buildConfigField "String", "oauthDiscovery", "\"${project.findProperty('cyface.staging_oauth_discovery')}\"" buildConfigField "String", "testLogin", "\"${project.findProperty('cyface.staging_user')}\"" @@ -116,6 +118,7 @@ android { // MOCK-API - only supports login - used by UI test on CI //buildConfigField "String", "cyfaceServer", "\"${project.findProperty('cyface.demo_api')}\"" + //buildConfigField "String", "providerServer", "\"${project.findProperty('cyface.demo_provider_api')}\"" //buildConfigField "String", "incentivesServer", "\"${project.findProperty('cyface.demo_incentives_api')}\"" //buildConfigField "String", "oauthDiscovery", "\"${project.findProperty('cyface.demo_oauth_discovery')}\"" //manifestPlaceholders = [usesCleartextTraffic:"false"] // for local collector testing @@ -128,6 +131,7 @@ android { // signingConfig is set by the CI buildConfigField "String", "cyfaceServer", "\"${project.findProperty('cyface.api')}\"" + buildConfigField "String", "providerServer", "\"${project.findProperty('cyface.provider_api')}\"" buildConfigField "String", "incentivesServer", "\"${project.findProperty('cyface.incentives_api')}\"" buildConfigField "String", "oauthDiscovery", "\"${project.findProperty('cyface.oauth_discovery')}\"" manifestPlaceholders = [usesCleartextTraffic:"false"] @@ -225,6 +229,8 @@ dependencies { implementation "androidx.localbroadcastmanager:localbroadcastmanager:$rootProject.ext.localbroadcastmanagerVersion" implementation "androidx.preference:preference:$rootProject.ext.androidPreferencesVersion" coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$rootProject.ext.coreLibraryDesugaringVersion" + // Http Requests (can't use Volley lib as it does not return status code in failure-/handler) + implementation "com.squareup.okhttp3:okhttp:$rootProject.ext.okHttpVersion" // OAuth 2.0 with OpenID Connect implementation "net.openid:appauth:$rootProject.ext.appAuthVersion" // Move to uploader [RFR-581] diff --git a/ui/cyface/src/main/kotlin/de/cyface/app/capturing/settings/SettingsFragment.kt b/ui/cyface/src/main/kotlin/de/cyface/app/capturing/settings/SettingsFragment.kt index 5cb5782b..fc5134d6 100644 --- a/ui/cyface/src/main/kotlin/de/cyface/app/capturing/settings/SettingsFragment.kt +++ b/ui/cyface/src/main/kotlin/de/cyface/app/capturing/settings/SettingsFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Cyface GmbH + * Copyright 2023-2024 Cyface GmbH * * This file is part of the Cyface App for Android. * @@ -18,26 +18,45 @@ */ package de.cyface.app.capturing.settings +import android.app.AlertDialog import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import de.cyface.app.BuildConfig import de.cyface.app.MeasuringClient +import de.cyface.app.R import de.cyface.app.databinding.FragmentSettingsBinding import de.cyface.app.utils.ServiceProvider +import de.cyface.app.utils.SharedConstants +import de.cyface.app.utils.trips.incentives.AuthExceptionListener import de.cyface.datacapturing.CyfaceDataCapturingService +import de.cyface.synchronization.Auth +import io.sentry.Sentry +import net.openid.appauth.AuthorizationException +import okhttp3.Call +import okhttp3.Callback +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import java.io.IOException /** * The [Fragment] which shows the settings to the user. * * @author Armin Schnabel - * @version 2.0.1 - * @since 3.2.0 */ class SettingsFragment : Fragment() { + /** + * The authenticator to get the auth token from. + */ + private lateinit var auth: Auth + /** * This property is only valid between onCreateView and onDestroyView. */ @@ -63,7 +82,9 @@ class SettingsFragment : Fragment() { // Get dependencies if (activity is ServiceProvider) { - capturing = (activity as ServiceProvider).capturing + val serviceProvider = activity as ServiceProvider + capturing = serviceProvider.capturing + auth = serviceProvider.auth } else { throw RuntimeException("Context does not support the Fragment, implement ServiceProvider") } @@ -97,6 +118,9 @@ class SettingsFragment : Fragment() { capturing ) ) + binding.deleteAccountButton.setOnClickListener { + showDeleteAccountConfirmationDialog() + } // Observe view model and update UI viewModel.centerMap.observe(viewLifecycleOwner) { centerMapValue -> @@ -113,6 +137,94 @@ class SettingsFragment : Fragment() { return binding.root } + private fun showDeleteAccountConfirmationDialog() { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.confirm_delete_account_title) + .setMessage(R.string.confirm_delete_account_message) + .setPositiveButton(R.string.delete_account) { _, _ -> + deleteAccount( + object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.code == 202) { + requireActivity().runOnUiThread { + Toast.makeText( + context, + R.string.delete_account_success, + Toast.LENGTH_LONG + ).show() + } + + // This inform the auth server that the user wants to end its session + auth.endSession(requireActivity()) + } else { + Sentry.captureMessage("Account deletion failed: ${response.code}") + requireActivity().runOnUiThread { + Toast.makeText( + context, + R.string.delete_account_failed, + Toast.LENGTH_LONG + ).show() + } + } + } + + override fun onFailure(call: Call, e: IOException) { + Sentry.captureException(e) + requireActivity().runOnUiThread { + Toast.makeText( + context, + R.string.delete_account_error, + Toast.LENGTH_LONG + ).show() + } + } + }, + object : AuthExceptionListener { + override fun onException(e: AuthorizationException) { + Sentry.captureException(e) + requireActivity().runOnUiThread { + Toast.makeText(context, R.string.auth_error, Toast.LENGTH_LONG) + .show() + } + } + } + ) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + /** + * Requests the account and user data deletion for the currently logged in user. + * + * @param handler the handler which receives the response in case of success + * @param authErrorHandler the handler which receives the auth errors + */ + private fun deleteAccount( + handler: Callback, + authErrorHandler: AuthExceptionListener + ) { + val client = OkHttpClient() + auth.performActionWithFreshTokens { accessToken, _, ex -> + if (ex != null) { + authErrorHandler.onException(ex as AuthorizationException) + return@performActionWithFreshTokens + } + + val userId = auth.userId()!! + + // Try to send the request + val url = BuildConfig.providerServer + "/users/$userId" + Log.d(SharedConstants.TAG, "Account deletion request to $url") + val request = Request.Builder() + .url(url) + .addHeader("Authorization", "Bearer $accessToken") + .delete() + .build() + client.newCall(request).enqueue(handler) + } + } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/ui/cyface/src/main/res/layout/fragment_settings.xml b/ui/cyface/src/main/res/layout/fragment_settings.xml index f40173b9..fa85e62c 100644 --- a/ui/cyface/src/main/res/layout/fragment_settings.xml +++ b/ui/cyface/src/main/res/layout/fragment_settings.xml @@ -64,5 +64,32 @@ app:layout_constraintTop_toTopOf="parent" /> + + + + + +