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" />
+
+
+
+
+
+
+
+
diff --git a/ui/cyface/src/main/res/values-de/strings.xml b/ui/cyface/src/main/res/values-de/strings.xml
index 7f48599b..11e68a1f 100644
--- a/ui/cyface/src/main/res/values-de/strings.xml
+++ b/ui/cyface/src/main/res/values-de/strings.xml
@@ -22,4 +22,12 @@
Externer Sensor
Fehler beim Starten der Bluetooth Komponente
Sensorfrequenz
+ Account löschen
+ Nutzeraccount löschen
+ Sind Sie sicher, dass Sie Ihren Account und alle verbundenen Daten unwiderruflich löschen möchten?
+ Abbrechen
+ Account und Daten wurden gelöscht
+ Account löschen fehlgeschlafen
+ Fehler beim Account löschen
+ Fehler bei der Authentifizierung
diff --git a/ui/cyface/src/main/res/values/strings.xml b/ui/cyface/src/main/res/values/strings.xml
index adab8c20..604fcd49 100644
--- a/ui/cyface/src/main/res/values/strings.xml
+++ b/ui/cyface/src/main/res/values/strings.xml
@@ -24,4 +24,12 @@
Error starting the bluetooth component.
Sensor frequency
Hz
+ Delete account
+ Delete user account
+ Are you sure you want to delete your account and all linked data irreversibly?
+ Cancel
+ Account and data successfully deleted
+ Account deletion failed
+ Error while deleting accuont
+ Error during authentication
diff --git a/ui/digural/src/main/kotlin/de/cyface/app/digural/auth/WebdavAuth.kt b/ui/digural/src/main/kotlin/de/cyface/app/digural/auth/WebdavAuth.kt
index 414ea6c9..5f616124 100644
--- a/ui/digural/src/main/kotlin/de/cyface/app/digural/auth/WebdavAuth.kt
+++ b/ui/digural/src/main/kotlin/de/cyface/app/digural/auth/WebdavAuth.kt
@@ -25,6 +25,7 @@ import android.content.Context
import android.os.Build
import android.os.Bundle
import android.util.Log
+import androidx.fragment.app.FragmentActivity
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
import com.thegrizzlylabs.sardineandroid.impl.SardineException
import de.cyface.app.digural.upload.WebdavSyncService
@@ -62,6 +63,14 @@ class WebdavAuth(private val context: Context, private val settings: Synchroniza
action(WebdavAuthenticator.DUMMY_TOKEN, WebdavAuthenticator.DUMMY_TOKEN, null)
}
+ override fun userId(): String {
+ return DUMMY_USER_ID
+ }
+
+ override fun endSession(activity: FragmentActivity) {
+ TODO("Not yet implemented")
+ }
+
/**
* Updates the credentials
*
@@ -220,6 +229,8 @@ class WebdavAuth(private val context: Context, private val settings: Synchroniza
}
companion object {
+ const val DUMMY_USER_ID = "WEBDAV_DOES_NOT_USE_USER_DELETION_NO_ID_REQUIRED"
+
/**
* Returns a dummy auth config as the [WebdavAuth] does not require such.
*/
diff --git a/ui/r4r/build.gradle b/ui/r4r/build.gradle
index 84367b3f..91515547 100644
--- a/ui/r4r/build.gradle
+++ b/ui/r4r/build.gradle
@@ -83,6 +83,7 @@ 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", "incentivesServer", "\"${project.findProperty('cyface.local_incentives_api')}\""
//buildConfigField "String", "oauthDiscovery", "\"${project.findProperty('cyface.local_oauth_discovery')}\""
//manifestPlaceholders = [usesCleartextTraffic:"true"]
@@ -90,18 +91,21 @@ android {
// 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", "incentivesServer", "\"${project.findProperty('cyface.local_incentives_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", "incentivesServer", "\"${project.findProperty('cyface.emulator_incentives_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')}\""
@@ -110,6 +114,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
@@ -122,6 +127,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"]
@@ -218,6 +224,8 @@ dependencies {
// For the action bar (at the bottom of the screen)
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.ext.navigationVersion"
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"
// Kotlin components
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.ext.coroutinesVersion"
diff --git a/ui/r4r/src/main/kotlin/de/cyface/app/r4r/capturing/settings/SettingsFragment.kt b/ui/r4r/src/main/kotlin/de/cyface/app/r4r/capturing/settings/SettingsFragment.kt
index d3bf4516..6809e720 100644
--- a/ui/r4r/src/main/kotlin/de/cyface/app/r4r/capturing/settings/SettingsFragment.kt
+++ b/ui/r4r/src/main/kotlin/de/cyface/app/r4r/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,27 +18,45 @@
*/
package de.cyface.app.r4r.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.r4r.Application
+import de.cyface.app.r4r.BuildConfig
+import de.cyface.app.r4r.R
import de.cyface.app.r4r.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.utils.settings.AppSettings
+import de.cyface.synchronization.Auth
+import io.sentry.Sentry
+import net.openid.appauth.AuthorizationException
+import okhttp3.Call
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Callback
+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.
*/
@@ -54,11 +72,6 @@ class SettingsFragment : Fragment() {
*/
private lateinit var capturing: CyfaceDataCapturingService
- /**
- * The settings used by both, UIs and libraries.
- */
- private lateinit var appSettings: AppSettings
-
/**
* The [SettingsViewModel] for this fragment.
*/
@@ -69,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")
}
@@ -103,6 +118,9 @@ class SettingsFragment : Fragment() {
capturing
)
)
+ binding.deleteAccountButton.setOnClickListener {
+ showDeleteAccountConfirmationDialog()
+ }
// Observe view model and update UI
viewModel.centerMap.observe(viewLifecycleOwner) { centerMapValue ->
@@ -119,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/r4r/src/main/res/layout/fragment_settings.xml b/ui/r4r/src/main/res/layout/fragment_settings.xml
index 37cc2661..7c43e843 100644
--- a/ui/r4r/src/main/res/layout/fragment_settings.xml
+++ b/ui/r4r/src/main/res/layout/fragment_settings.xml
@@ -7,55 +7,87 @@
android:orientation="vertical"
tools:context=".capturing.settings.SettingsFragment">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/r4r/src/main/res/values-de/strings.xml b/ui/r4r/src/main/res/values-de/strings.xml
index fd2cbe7a..facf50d8 100644
--- a/ui/r4r/src/main/res/values-de/strings.xml
+++ b/ui/r4r/src/main/res/values-de/strings.xml
@@ -6,4 +6,14 @@
Datenerfassung inaktiv
+
+
+ Account löschen
+ Nutzeraccount löschen
+ Sind Sie sicher, dass Sie Ihren Account und alle verbundenen Daten unwiderruflich löschen möchten?
+ Abbrechen
+ Account und Daten wurden gelöscht
+ Account löschen fehlgeschlafen
+ Fehler beim Account löschen
+ Fehler bei der Authentifizierung
\ No newline at end of file
diff --git a/ui/r4r/src/main/res/values/strings.xml b/ui/r4r/src/main/res/values/strings.xml
index 1001107c..51527ba9 100644
--- a/ui/r4r/src/main/res/values/strings.xml
+++ b/ui/r4r/src/main/res/values/strings.xml
@@ -10,4 +10,14 @@
"%1$s (custom tab)"
+
+
+ Delete account
+ Delete user account
+ Are you sure you want to delete your account and all linked data irreversibly?
+ Cancel
+ Account and data successfully deleted
+ Account deletion failed
+ Error while deleting accuont
+ Error during authentication
\ No newline at end of file