Add profile page, implement authentication

This commit is contained in:
Reimar 2025-05-01 12:02:55 +02:00
parent 9d9a2975f5
commit 7c45526563
Signed by: Reimar
GPG Key ID: 93549FA07F0AE268
17 changed files with 164 additions and 105 deletions

View File

@ -25,8 +25,7 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="false" android:exported="false" />
android:label="@string/title_activity_main" />
<activity <activity
android:name=".WelcomeActivity" android:name=".WelcomeActivity"

View File

@ -12,8 +12,10 @@ import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import tech.mercantec.easyeat.databinding.ActivityMainBinding import tech.mercantec.easyeat.databinding.ActivityMainBinding
import tech.mercantec.easyeat.helpers.getUserInfo
import tech.mercantec.easyeat.models.Dish import tech.mercantec.easyeat.models.Dish
import tech.mercantec.easyeat.ui.dishes.DishAdapter import tech.mercantec.easyeat.ui.dishes.DishAdapter
import kotlin.concurrent.thread
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@ -30,11 +32,15 @@ class MainActivity : AppCompatActivity() {
val navController = findNavController(R.id.nav_host_fragment_activity_main) val navController = findNavController(R.id.nav_host_fragment_activity_main)
val appBarConfiguration = AppBarConfiguration( val appBarConfiguration = AppBarConfiguration(
setOf( setOf(
R.id.navigation_dishes, R.id.navigation_shopping_list, R.id.navigation_notifications R.id.navigation_dishes, R.id.navigation_shopping_list, R.id.navigation_profile
) )
) )
setupActionBarWithNavController(navController, appBarConfiguration) setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController) navView.setupWithNavController(navController)
findViewById<FloatingActionButton>(R.id.add_dish).setOnClickListener {
val intent = Intent(this, CreateDishActivity::class.java)
startActivity(intent)
}
} }
} }

View File

@ -1,6 +1,7 @@
package tech.mercantec.easyeat.helpers package tech.mercantec.easyeat.helpers
import android.content.Context import android.content.Context
import android.content.Intent
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -10,32 +11,46 @@ import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import tech.mercantec.easyeat.LoginActivity
import java.io.IOException import java.io.IOException
class ApiRequestException(message: String, cause: Throwable?) : Exception(message, cause) class ApiRequestException(message: String, cause: Throwable?) : Exception(message, cause)
class HttpResponse(val body: String, val code: Int) class HttpResponse(val body: String, val code: Int)
fun request(ctx: Context, method: String, path: String, data: String?): HttpResponse { fun request(ctx: Context, method: String, path: String, data: String?, autoRefresh: Boolean = true): HttpResponse {
val url = URL(BuildConfig.API_BASE_URL + path) val url = URL(BuildConfig.API_BASE_URL + path)
val prefs = ctx.getSharedPreferences("easyeat", Context.MODE_PRIVATE) val prefs = ctx.getSharedPreferences("easyeat", Context.MODE_PRIVATE)
val authToken = prefs.getString("auth-token", null) val authToken = prefs.getString("auth-token", null)
val refreshToken = prefs.getString("auth-token", null)
try { try {
with(url.openConnection() as HttpURLConnection) { with(url.openConnection() as HttpURLConnection) {
requestMethod = method requestMethod = method
if (authToken != null)
setRequestProperty("Authorization", "Bearer $authToken")
if (data != null) { if (data != null) {
setRequestProperty("Content-Type", "application/json") setRequestProperty("Content-Type", "application/json")
if (authToken != null)
setRequestProperty("Authorization", "Bearer $authToken")
outputStream.write(data.toByteArray()) outputStream.write(data.toByteArray())
outputStream.flush() outputStream.flush()
} }
if (responseCode == 401 && refreshToken != null) {
if (!autoRefresh || !refreshAuthToken(ctx, refreshToken)) {
val intent = Intent(ctx, LoginActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ctx.startActivity(intent)
throw ApiRequestException("Please sign in again", null)
}
return request(ctx, method, path, data, false)
}
if (responseCode >= 400) if (responseCode >= 400)
return HttpResponse(errorStream.readBytes().decodeToString(), responseCode) return HttpResponse(errorStream.readBytes().decodeToString(), responseCode)

View File

@ -1,6 +1,7 @@
package tech.mercantec.easyeat.helpers package tech.mercantec.easyeat.helpers
import android.content.Context import android.content.Context
import android.util.Log
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -47,6 +48,9 @@ fun refreshAuthToken(ctx: Context, refreshToken: String): Boolean {
RefreshTokenRequest(refreshToken) RefreshTokenRequest(refreshToken)
) )
} catch (e: ApiRequestException) { } catch (e: ApiRequestException) {
if (e.message != null)
Log.e("EasyEat", e.message!!)
return false return false
} }
@ -70,3 +74,12 @@ fun isLoggedIn(ctx: Context): Boolean {
return true return true
} }
@Serializable
data class UserInfoResponse(val id: Int, val userName: String, val email: String)
fun getUserInfo(ctx: Context): UserInfoResponse {
val response = requestJson<Unit, UserInfoResponse>(ctx, "GET", "/api/User/get", null)
return response
}

View File

@ -1,42 +0,0 @@
package tech.mercantec.easyeat.ui.notifications
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import tech.mercantec.easyeat.databinding.FragmentNotificationsBinding
class NotificationsFragment : Fragment() {
private var _binding: FragmentNotificationsBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val notificationsViewModel =
ViewModelProvider(this).get(NotificationsViewModel::class.java)
_binding = FragmentNotificationsBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textNotifications
notificationsViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -1,13 +0,0 @@
package tech.mercantec.easyeat.ui.notifications
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class NotificationsViewModel : ViewModel() {
private val _text = MutableLiveData<String>().apply {
value = "This is notifications Fragment"
}
val text: LiveData<String> = _text
}

View File

@ -0,0 +1,39 @@
package tech.mercantec.easyeat.ui.profile
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import tech.mercantec.easyeat.databinding.FragmentProfileBinding
import tech.mercantec.easyeat.helpers.ApiRequestException
import tech.mercantec.easyeat.helpers.UserInfoResponse
import tech.mercantec.easyeat.helpers.getUserInfo
import kotlin.concurrent.thread
class ProfileFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val binding = FragmentProfileBinding.inflate(inflater, container, false)
thread {
val userInfo: UserInfoResponse
try {
userInfo = getUserInfo(requireContext())
} catch (e: ApiRequestException) {
activity?.runOnUiThread {
Toast.makeText(activity?.applicationContext, e.message, Toast.LENGTH_LONG).show()
}
return@thread
}
activity?.runOnUiThread {
binding.username.text = userInfo.userName
binding.email.text = userInfo.email
}
}
return binding.root
}
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M234,684Q285,645 348,622.5Q411,600 480,600Q549,600 612,622.5Q675,645 726,684Q761,643 780.5,591Q800,539 800,480Q800,347 706.5,253.5Q613,160 480,160Q347,160 253.5,253.5Q160,347 160,480Q160,539 179.5,591Q199,643 234,684ZM480,520Q421,520 380.5,479.5Q340,439 340,380Q340,321 380.5,280.5Q421,240 480,240Q539,240 579.5,280.5Q620,321 620,380Q620,439 579.5,479.5Q539,520 480,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,480Q414,480 367,433Q320,386 320,320Q320,254 367,207Q414,160 480,160Q546,160 593,207Q640,254 640,320Q640,386 593,433Q546,480 480,480ZM160,800L160,688Q160,654 177.5,625.5Q195,597 224,582Q286,551 350,535.5Q414,520 480,520Q546,520 610,535.5Q674,551 736,582Q765,597 782.5,625.5Q800,654 800,688L800,800L160,800Z"/>
</vector>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.notifications.NotificationsFragment">
<TextView
android:id="@+id/text_notifications"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="30sp"
android:paddingEnd="30sp"
android:gravity="center">
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:indeterminate="true"
/>
<ImageView
android:layout_width="match_parent"
android:layout_height="72dp"
android:src="@drawable/ic_account_circle_24px"
/>
<TextView
android:id="@+id/username"
android:layout_marginTop="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textStyle="bold"
android:textAlignment="center"
android:text="@string/loading"
/>
<TextView
android:id="@+id/email"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textAlignment="center"
android:text="@string/loading"
/>
<Button
android:layout_marginTop="40dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit_profile_label"
/>
</LinearLayout>

View File

@ -3,17 +3,17 @@
<item <item
android:id="@+id/navigation_dishes" android:id="@+id/navigation_dishes"
android:icon="@drawable/ic_restaurant_black_24px" android:icon="@drawable/ic_restaurant_24px"
android:title="@string/title_your_dishes" /> android:title="@string/title_your_dishes" />
<item <item
android:id="@+id/navigation_shopping_list" android:id="@+id/navigation_shopping_list"
android:icon="@drawable/ic_list_alt_black_24px" android:icon="@drawable/ic_list_alt_24px"
android:title="@string/title_shopping_list" /> android:title="@string/title_shopping_list" />
<item <item
android:id="@+id/navigation_notifications" android:id="@+id/navigation_profile"
android:icon="@drawable/ic_notifications_black_24dp" android:icon="@drawable/ic_person_24px"
android:title="@string/title_notifications" /> android:title="@string/title_profile" />
</menu> </menu>

View File

@ -18,8 +18,8 @@
tools:layout="@layout/fragment_shopping_list" /> tools:layout="@layout/fragment_shopping_list" />
<fragment <fragment
android:id="@+id/navigation_notifications" android:id="@+id/navigation_profile"
android:name="tech.mercantec.easyeat.ui.notifications.NotificationsFragment" android:name="tech.mercantec.easyeat.ui.profile.ProfileFragment"
android:label="@string/title_notifications" android:label="@string/title_profile"
tools:layout="@layout/fragment_notifications" /> tools:layout="@layout/fragment_profile" />
</navigation> </navigation>

View File

@ -1,9 +1,8 @@
<resources> <resources>
<string name="app_name">EasyEat</string> <string name="app_name">EasyEat</string>
<string name="title_activity_main">MainActivity</string>
<string name="title_your_dishes">Your Dishes</string> <string name="title_your_dishes">Your Dishes</string>
<string name="title_shopping_list">Shopping list</string> <string name="title_shopping_list">Shopping list</string>
<string name="title_notifications">Notifications</string> <string name="title_profile">Profile</string>
<string name="welcome_heading">Welcome to <font color="#0099BA">EasyEat</font></string> <string name="welcome_heading">Welcome to <font color="#0099BA">EasyEat</font></string>
<string name="login_label">Login</string> <string name="login_label">Login</string>
<string name="login_heading">Login to EasyEat</string> <string name="login_heading">Login to EasyEat</string>
@ -22,5 +21,6 @@
<string name="measurement_label">Measurement</string> <string name="measurement_label">Measurement</string>
<string name="amount_label">Amount</string> <string name="amount_label">Amount</string>
<string name="add_label">Add</string> <string name="add_label">Add</string>
<string name="edit_profile_label">Edit profile</string>
<string name="loading">Loading…</string>
</resources> </resources>