Skip to content

Commit

Permalink
[RFR-1046] Add account deletion
Browse files Browse the repository at this point in the history
[RFR-1046] Upgrade SDK and add user account deletion
  • Loading branch information
hb0 authored May 30, 2024
2 parents ff93c94 + 5f77c70 commit efbc35e
Show file tree
Hide file tree
Showing 16 changed files with 406 additions and 71 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/gradle_build.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/gradle_connected-tests.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/gradle_publish.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
10 changes: 8 additions & 2 deletions ui/cyface/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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')}\""
Expand All @@ -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
Expand All @@ -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"]
Expand Down Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Cyface GmbH
* Copyright 2023-2024 Cyface GmbH
*
* This file is part of the Cyface App for Android.
*
Expand All @@ -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.
*/
Expand All @@ -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")
}
Expand Down Expand Up @@ -97,6 +118,9 @@ class SettingsFragment : Fragment() {
capturing
)
)
binding.deleteAccountButton.setOnClickListener {
showDeleteAccountConfirmationDialog()
}

// Observe view model and update UI
viewModel.centerMap.observe(viewLifecycleOwner) { centerMapValue ->
Expand All @@ -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
Expand Down
27 changes: 27 additions & 0 deletions ui/cyface/src/main/res/layout/fragment_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,32 @@
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

<!-- account deletion -->

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/delete_account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/synchronization_wrapper"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/delete_account"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/delete_account_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/delete_account"
android:backgroundTint="@color/red_700"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
8 changes: 8 additions & 0 deletions ui/cyface/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,12 @@
<string name="drawer_setting_speed_sensor">Externer Sensor</string>
<string name="bluetooth_setup_failed">Fehler beim Starten der Bluetooth Komponente</string>
<string name="settings_sensor_frequency">Sensorfrequenz</string>
<string name="delete_account">Account löschen</string>
<string name="confirm_delete_account_title">Nutzeraccount löschen</string>
<string name="confirm_delete_account_message">Sind Sie sicher, dass Sie Ihren Account und alle verbundenen Daten unwiderruflich löschen möchten?</string>
<string name="cancel">Abbrechen</string>
<string name="delete_account_success">Account und Daten wurden gelöscht</string>
<string name="delete_account_failed">Account löschen fehlgeschlafen</string>
<string name="delete_account_error">Fehler beim Account löschen</string>
<string name="auth_error">Fehler bei der Authentifizierung</string>
</resources>
8 changes: 8 additions & 0 deletions ui/cyface/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,12 @@
<string name="bluetooth_setup_failed">Error starting the bluetooth component.</string>
<string name="settings_sensor_frequency">Sensor frequency</string>
<string name="settings_sensor_frequency_unit" translatable="false">Hz</string>
<string name="delete_account">Delete account</string>
<string name="confirm_delete_account_title">Delete user account</string>
<string name="confirm_delete_account_message">Are you sure you want to delete your account and all linked data irreversibly?</string>
<string name="cancel">Cancel</string>
<string name="delete_account_success">Account and data successfully deleted</string>
<string name="delete_account_failed">Account deletion failed</string>
<string name="delete_account_error">Error while deleting accuont</string>
<string name="auth_error">Error during authentication</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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.
*/
Expand Down
Loading

0 comments on commit efbc35e

Please sign in to comment.