feat: Add settings activity
This commit is contained in:
parent
49ca0f79a2
commit
5cd48cfed6
|
@ -3,8 +3,10 @@ package com.p_vacho.neat_calendar
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.p_vacho.neat_calendar.activities.ApiUnreachableActivity
|
||||
import com.p_vacho.neat_calendar.activities.LoginActivity
|
||||
import com.p_vacho.neat_calendar.api.models.ValidationError
|
||||
import com.p_vacho.neat_calendar.util.ExceptionSerializer
|
||||
import com.p_vacho.neat_calendar.util.SerializedException
|
||||
import retrofit2.HttpException
|
||||
|
@ -43,6 +45,19 @@ class GlobalExceptionHandler(
|
|||
return
|
||||
}
|
||||
|
||||
if (e.code() == 422) {
|
||||
Log.e("GlobalExceptionHandler", "Caught HTTP 422 Exception")
|
||||
val errorBody = e.response()?.errorBody()?.string()
|
||||
val validationError = Gson().fromJson(errorBody, ValidationError::class.java)
|
||||
Log.e("GlobalExceptionHandler", "Details: $validationError")
|
||||
|
||||
// This exception is still unhandled, the above just logs it in a nicer way
|
||||
// so we can see what exactly went wrong. We still want to propagate the exception
|
||||
// up to default handler now.
|
||||
defaultHandler?.uncaughtException(t, e)
|
||||
return
|
||||
}
|
||||
|
||||
Log.e("GlobalExceptionHandler", "Propgating unhandled exception", e)
|
||||
defaultHandler?.uncaughtException(t, e)
|
||||
return
|
||||
|
|
|
@ -1,13 +1,40 @@
|
|||
package com.p_vacho.neat_calendar.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.p_vacho.neat_calendar.MyApplication
|
||||
import com.p_vacho.neat_calendar.R
|
||||
import com.p_vacho.neat_calendar.api.RetrofitClient
|
||||
import com.p_vacho.neat_calendar.api.models.PartialUserRequest
|
||||
import com.p_vacho.neat_calendar.api.models.UserResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import retrofit2.HttpException
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
private lateinit var etBaseUrl: TextInputEditText
|
||||
private lateinit var etUsername: TextInputEditText
|
||||
private lateinit var etEmail: TextInputEditText
|
||||
private lateinit var etNewPassword: TextInputEditText
|
||||
private lateinit var btnLogout: MaterialButton
|
||||
private lateinit var btnDeleteAccount: MaterialButton
|
||||
private lateinit var btnSave: MaterialButton
|
||||
private lateinit var btnBack: MaterialButton
|
||||
|
||||
private lateinit var user: UserResponse
|
||||
private lateinit var oldBaseUrl: String
|
||||
private lateinit var oldUsername: String
|
||||
private lateinit var oldEmail: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
@ -17,5 +44,114 @@ class SettingsActivity : AppCompatActivity() {
|
|||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
insets
|
||||
}
|
||||
|
||||
etBaseUrl = findViewById(R.id.etBaseUrl)
|
||||
etUsername = findViewById(R.id.etUsername)
|
||||
etEmail = findViewById(R.id.etEmail)
|
||||
etNewPassword = findViewById(R.id.etNewPassword)
|
||||
btnLogout = findViewById(R.id.btnLogout)
|
||||
btnDeleteAccount = findViewById(R.id.btnDeleteAccount)
|
||||
btnSave = findViewById(R.id.btnSave)
|
||||
btnBack = findViewById(R.id.btnBack)
|
||||
|
||||
oldBaseUrl = RetrofitClient.getCurrentBaseUrl()
|
||||
etBaseUrl.setText(oldBaseUrl)
|
||||
|
||||
lifecycleScope.launch {
|
||||
user = withContext(Dispatchers.IO) {
|
||||
RetrofitClient.usersService.getCurrentUser()
|
||||
}
|
||||
|
||||
oldEmail = user.email
|
||||
etEmail.setText(oldEmail)
|
||||
|
||||
oldUsername = user.username
|
||||
etUsername.setText(oldUsername)
|
||||
}
|
||||
|
||||
btnBack.setOnClickListener { finish() }
|
||||
btnSave.setOnClickListener { onSave() }
|
||||
btnLogout.setOnClickListener { onLogout() }
|
||||
btnDeleteAccount.setOnClickListener { onDeleteAccount() }
|
||||
}
|
||||
|
||||
private fun onSave() {
|
||||
var changed = false
|
||||
|
||||
val newBaseUrl = etBaseUrl.text.toString()
|
||||
if (newBaseUrl != oldBaseUrl) {
|
||||
RetrofitClient.updateBaseUrl(newBaseUrl)
|
||||
oldBaseUrl = newBaseUrl
|
||||
changed = true
|
||||
}
|
||||
|
||||
val newUsername = etUsername.text.toString()
|
||||
val newEmail = etEmail.text.toString()
|
||||
val newPassword = etNewPassword.text.toString()
|
||||
|
||||
lifecycleScope.launch() {
|
||||
val req = PartialUserRequest(
|
||||
username = if (newUsername != oldUsername) newUsername else null,
|
||||
email = if (newEmail != oldEmail) newEmail else null,
|
||||
password = if (newPassword.isNotEmpty()) newPassword else null,
|
||||
)
|
||||
|
||||
if (req.username != null || req.email != null || req.password != null) {
|
||||
try {
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
RetrofitClient.usersService.updateUser(user.user_id, req)
|
||||
}
|
||||
|
||||
oldUsername = response.username
|
||||
oldEmail = response.email
|
||||
etNewPassword.setText("")
|
||||
changed = true
|
||||
} catch (e: HttpException) {
|
||||
if (e.code() != 409) {
|
||||
throw e
|
||||
}
|
||||
|
||||
Toast.makeText(this@SettingsActivity, "This username or email is already taken", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
Toast.makeText(this@SettingsActivity, "Changes saved", Toast.LENGTH_SHORT).show()
|
||||
// Make a ping request, to re-check api connectivity
|
||||
// TODO: Ping isn't a good enough check here, we should also validate tokens
|
||||
withContext(Dispatchers.IO) {
|
||||
RetrofitClient.ping()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this@SettingsActivity, "No changes made", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLogout() {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
(application as MyApplication).authRepository.logout()
|
||||
navigateToLoginActivity()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun onDeleteAccount() {
|
||||
// TODO: Add a modal asking for confirmation
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
RetrofitClient.usersService.deleteUser(user.user_id)
|
||||
}
|
||||
|
||||
(application as MyApplication).tokenManager.clearTokens()
|
||||
navigateToLoginActivity()
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToLoginActivity() {
|
||||
val intent = Intent(this, LoginActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
|
@ -8,3 +8,9 @@ data class UserResponse(
|
|||
val email: String,
|
||||
val created_at: OffsetDateTime,
|
||||
)
|
||||
|
||||
data class PartialUserRequest(
|
||||
val username: String?,
|
||||
val password: String?,
|
||||
val email: String?,
|
||||
)
|
|
@ -1,13 +1,26 @@
|
|||
package com.p_vacho.neat_calendar.api.services
|
||||
|
||||
import com.p_vacho.neat_calendar.api.models.PartialUserRequest
|
||||
import com.p_vacho.neat_calendar.api.models.UserResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.PATCH
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface UsersService {
|
||||
@GET("users")
|
||||
suspend fun getUsers(): List<UserResponse>
|
||||
|
||||
@GET("users/me")
|
||||
suspend fun getCurrentUser(): UserResponse
|
||||
|
||||
@GET("users/{user_id}")
|
||||
suspend fun getUser(@Path("user_id") userId: String): UserResponse
|
||||
|
||||
@DELETE("users/{user_id}")
|
||||
suspend fun deleteUser(@Path("user_id") userId: String): Unit
|
||||
|
||||
@PATCH("users/{user_id}")
|
||||
suspend fun updateUser(@Path("user_id") userId: String, @Body userData: PartialUserRequest): UserResponse
|
||||
}
|
9
app/src/main/res/drawable/ic_warning.xml
Normal file
9
app/src/main/res/drawable/ic_warning.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="m40,840 l440,-760 440,760L40,840ZM178,760h604L480,240 178,760ZM480,720q17,0 28.5,-11.5T520,680q0,-17 -11.5,-28.5T480,640q-17,0 -28.5,11.5T440,680q0,17 11.5,28.5T480,720ZM440,600h80v-200h-80v200ZM480,500Z"
|
||||
android:fillColor="#e8eaed"/>
|
||||
</vector>
|
|
@ -1,10 +1,193 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".activities.SettingsActivity">
|
||||
android:fillViewport="true"
|
||||
android:padding="16dp">
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<LinearLayout
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- A single card containing all settings sections -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- ============= Section 1: Connection ============= -->
|
||||
<TextView
|
||||
android:id="@+id/tvConnectionSectionHeader"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/connection_settings_section"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp" />
|
||||
|
||||
<!-- Divider for the 'Connection' section -->
|
||||
<com.google.android.material.divider.MaterialDivider
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:dividerColor="@android:color/darker_gray" />
|
||||
|
||||
<!-- Base URL field -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilBaseUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/base_url"
|
||||
app:boxStrokeWidth="1dp"
|
||||
app:boxStrokeColor="?attr/colorPrimary">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etBaseUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
<!-- ========== Section 2: User Management ========== -->
|
||||
<TextView
|
||||
android:id="@+id/tvUserManagementHeader"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/user_settings_section"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
android:layout_marginTop="16dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp" />
|
||||
|
||||
<!-- Divider for the 'User Management' section -->
|
||||
<com.google.android.material.divider.MaterialDivider
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:dividerColor="@android:color/darker_gray" />
|
||||
|
||||
<!-- Username field -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilUsername"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/username"
|
||||
app:boxStrokeWidth="1dp"
|
||||
app:boxStrokeColor="?attr/colorPrimary"
|
||||
android:layout_marginTop="4dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etUsername"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Email field -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilEmail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/email"
|
||||
app:boxStrokeWidth="1dp"
|
||||
app:boxStrokeColor="?attr/colorPrimary"
|
||||
android:layout_marginTop="4dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etEmail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textEmailAddress" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Password field (for changing password) -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/new_password"
|
||||
app:boxStrokeWidth="1dp"
|
||||
app:boxStrokeColor="?attr/colorPrimary"
|
||||
android:layout_marginTop="4dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etNewPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Logout Button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnLogout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:text="@string/logout"
|
||||
app:icon="@drawable/ic_exit"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="8dp"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||
|
||||
<!-- Account Deletion Button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnDeleteAccount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/delete_account"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:layout_marginTop="16dp"
|
||||
app:icon="@drawable/ic_warning"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="8dp"
|
||||
app:iconTint="@android:color/holo_red_dark"
|
||||
app:strokeColor="@android:color/holo_red_dark"
|
||||
app:strokeWidth="2dp"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||
|
||||
<!-- ========== Section 3: Controls ========== -->
|
||||
<com.google.android.material.divider.MaterialDivider
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:dividerColor="@android:color/darker_gray" />
|
||||
|
||||
<!-- Save Settings Button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSave"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/save"
|
||||
app:cornerRadius="8dp" />
|
||||
|
||||
<!-- Go back Button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnBack"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:text="@string/back"
|
||||
app:icon="@drawable/ic_arrow_back"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="8dp"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
<string name="delete_event">Delete this event</string>
|
||||
<string name="edit_event">Edit this event</string>
|
||||
<string name="add_event">Add a new event</string>
|
||||
<string name="back">Go to the previous page</string>
|
||||
<string name="back">Go back</string>
|
||||
<string name="event_title">Event Title</string>
|
||||
<string name="event_description">Event Description</string>
|
||||
<string name="instant_event">Instant Event</string>
|
||||
|
@ -47,4 +47,10 @@
|
|||
<string name="leave_invited_event">Leave invited event</string>
|
||||
<string name="open_app_settings">Open Settings</string>
|
||||
<string name="open_notifications">Open Notifications</string>
|
||||
<string name="connection_settings_section">Connection</string>
|
||||
<string name="user_settings_section">User Settings</string>
|
||||
<string name="new_password">New Password</string>
|
||||
<string name="delete_account">Delete Account</string>
|
||||
<string name="controls">Controls</string>
|
||||
<string name="logout">Log out</string>
|
||||
</resources>
|
Loading…
Reference in a new issue