Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ kotlin {

dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.livedata)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.constraintlayout)
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

<application
android:name=".AceApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.acetimetracker

import android.app.Application
import com.example.acetimetracker.account.AccountServices
import com.example.acetimetracker.account.firebaseAccountServices

open class AceApplication : Application() {
open val accounts: AccountServices by lazy { firebaseAccountServices() }
}
17 changes: 9 additions & 8 deletions app/src/main/java/com/example/acetimetracker/Categories.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,23 @@ import android.widget.Button
import android.widget.EditText
import android.widget.ImageButton
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.acetimetracker.account.AccountActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.FirebaseFirestore

class Categories : AppCompatActivity() {
class Categories : AccountActivity() {
private lateinit var recyclerView: RecyclerView
private lateinit var categoryAdapter: CategoryAdapter
private lateinit var firestore: FirebaseFirestore
private lateinit var auth: FirebaseAuth

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!sessionReady) return
setContentView(R.layout.activity_categories)

// Initialize Firebase
firestore = FirebaseFirestore.getInstance()
auth = FirebaseAuth.getInstance()

// Initialize views
recyclerView = findViewById(R.id.recyclerViewCategories)
Expand Down Expand Up @@ -55,13 +53,14 @@ class Categories : AppCompatActivity() {
}

private fun fetchDataFromFirestore() {
val userId = auth.currentUser?.uid
val userId = currentSessionUid()
if (userId != null) {
firestore.collection("users")
.document(userId)
.collection("categories")
.get()
.addOnSuccessListener { result ->
if (currentSessionUid() != userId) return@addOnSuccessListener
val categories = mutableListOf<Category>()
for (document in result) {
val name = document.getString("name") ?: ""
Expand Down Expand Up @@ -111,13 +110,14 @@ class Categories : AppCompatActivity() {
"timeSpent" to 0L // Consider if you need this field
)

val userId = auth.currentUser?.uid
val userId = currentSessionUid()
if (userId != null) {
firestore.collection("users")
.document(userId)
.collection("categories")
.add(categoryData)
.addOnSuccessListener { documentReference ->
if (currentSessionUid() != userId) return@addOnSuccessListener
Log.d("Categories", "Category added with ID: ${documentReference.id}")
Toast.makeText(this, "Category added: $categoryName", Toast.LENGTH_SHORT).show()
// Clear input fields
Expand All @@ -144,7 +144,7 @@ class Categories : AppCompatActivity() {

// Function to delete a category (add this inside the Categories activity)
private fun deleteCategory(category: Category) {
val userId = auth.currentUser?.uid
val userId = currentSessionUid()
if (userId != null) {

firestore.collection("users")
Expand All @@ -153,6 +153,7 @@ class Categories : AppCompatActivity() {
.document(category.documentId) // Use category.documentId
.delete()
.addOnSuccessListener {
if (currentSessionUid() != userId) return@addOnSuccessListener
Log.d("Categories", "Category deleted with ID: ${category.documentId}")
Toast.makeText(this, "Category deleted: ${category.name}", Toast.LENGTH_SHORT).show()
fetchDataFromFirestore()
Expand Down
201 changes: 40 additions & 161 deletions app/src/main/java/com/example/acetimetracker/EditProfileActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,183 +2,62 @@ package com.example.acetimetracker

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.WindowManager
import android.widget.Button
import android.widget.EditText
import android.widget.ImageView
import android.widget.Toast
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieDrawable
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.ViewModelProvider
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CircleCrop
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.SetOptions
import com.google.firebase.storage.FirebaseStorage
import java.util.UUID
import com.example.acetimetracker.account.*

class EditProfileActivity : AppCompatActivity() {

private lateinit var auth: FirebaseAuth
private lateinit var firestore: FirebaseFirestore
private lateinit var storage: FirebaseStorage
private var selectedImageUri: Uri? = null
private lateinit var profilePictureUrl: String
private lateinit var profileImageView: ImageView
private lateinit var profilePictureLoadingAnimation: LottieAnimationView

// Use an ActivityResultLauncher for the image picker
private val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
selectedImageUri = result.data?.data
if (selectedImageUri != null) {
uploadImageToFirebaseStorage(selectedImageUri)
}
}
class EditProfileActivity : AccountActivity() {
private val model by lazy { ViewModelProvider(this, AccountViewModelFactory(accounts))[ProfileViewModel::class.java] }
private var displayedPhoto: String? = null
private val picker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK && currentSessionUid() != null) result.data?.data?.let(model::upload)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!sessionReady) return
setContentView(R.layout.activity_edit_profile)

auth = FirebaseAuth.getInstance()
firestore = FirebaseFirestore.getInstance()
storage = FirebaseStorage.getInstance()

val editDisplayNameEditText = findViewById<EditText>(R.id.editDisplayNameEditText)
val editEmailEditText = findViewById<EditText>(R.id.editEmailEditText)
val uploadProfilePictureButton = findViewById<Button>(R.id.uploadProfilePictureButton)
val saveProfileChangesButton = findViewById<Button>(R.id.saveProfileChangesButton)

// Initialize profileImageView, profilePictureLoadingAnimation, and loadingTextView in onCreate()
profileImageView = findViewById(R.id.profileImageView)
profilePictureLoadingAnimation = findViewById(R.id.profilePictureLoadingAnimation)

fetchUserData()

uploadProfilePictureButton.setOnClickListener {
openImagePicker()
val name = findViewById<EditText>(R.id.editDisplayNameEditText)
findViewById<EditText>(R.id.editEmailEditText).apply { setText(sessionAccount.email.orEmpty()); isEnabled = false }
name.doAfterTextChanged { model.editName(it.toString()) }
findViewById<Button>(R.id.saveProfileChangesButton).setOnClickListener {
if (!model.state.value!!.loaded) model.reload() else model.save()
}

saveProfileChangesButton.setOnClickListener {
val newDisplayName = editDisplayNameEditText.text.toString()
val newEmail = editEmailEditText.text.toString()

updateUserProfile(newDisplayName, newEmail, profilePictureUrl)

finish()
findViewById<Button>(R.id.uploadProfilePictureButton).setOnClickListener {
picker.launch(Intent(Intent.ACTION_OPEN_DOCUMENT).apply { type = "image/*"; addCategory(Intent.CATEGORY_OPENABLE) })
}
}

private fun fetchUserData() {
val userId = auth.currentUser?.uid ?: ""
firestore.collection("users").document(userId)
.get()
.addOnSuccessListener { document ->
val displayName = document.getString("displayName")
val email = document.getString("email")
val profilePictureUrl = document.getString("profilePictureUrl")

val editDisplayNameEditText = findViewById<EditText>(R.id.editDisplayNameEditText)
val editEmailEditText = findViewById<EditText>(R.id.editEmailEditText)

editDisplayNameEditText.setText(displayName)
editEmailEditText.setText(email)

// Load the profile picture using Glide (if available)
if (profilePictureUrl != null) {
Glide.with(this)
.load(profilePictureUrl)
.transform(CircleCrop())
.into(profileImageView)
} else {
// Handle the case where there's no profile picture
profileImageView.setImageResource(R.drawable.ic_account_circle) // Or set a default image
}
model.state.observe(this) { state ->
if (currentSessionUid() == null) return@observe
if (name.text.toString() != state.name) name.setText(state.name)
name.isEnabled = state.loaded && !state.saving
val busy = state.loading || state.saving || state.uploading
findViewById<Button>(R.id.saveProfileChangesButton).apply {
isEnabled = !busy
setText(if (!state.loaded) R.string.account_retry else R.string.account_save)
}
.addOnFailureListener { exception ->
Log.e("EditProfileActivity", "Error fetching user data: ${exception.message}", exception)
findViewById<Button>(R.id.uploadProfilePictureButton).isEnabled = state.loaded && !busy
findViewById<View>(R.id.profilePictureLoadingAnimation).visibility = if (busy) View.VISIBLE else View.INVISIBLE
findViewById<TextView>(R.id.profileStatusTextView).text = when {
state.saving -> getString(R.string.account_saving)
busy -> getString(R.string.account_working)
else -> state.problem?.let { getString(it.messageResource()) }.orEmpty()
}
}

private fun openImagePicker() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
type = "image/*"
addCategory(Intent.CATEGORY_OPENABLE)
}
startForResult.launch(intent)
}

private fun uploadImageToFirebaseStorage(imageUri: Uri?) {
if (imageUri != null) {
val storageRef = storage.reference
val imageName = UUID.randomUUID().toString()
val imageRef = storageRef.child("profile_pictures/$imageName")

// Show the loading animation and disable interaction
profilePictureLoadingAnimation.visibility = View.VISIBLE
profilePictureLoadingAnimation.repeatCount = LottieDrawable.INFINITE
profilePictureLoadingAnimation.repeatMode = LottieDrawable.RESTART
profilePictureLoadingAnimation.playAnimation()


window.setFlags(
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
)

imageRef.putFile(imageUri)
.addOnSuccessListener { taskSnapshot ->
imageRef.downloadUrl.addOnSuccessListener { uri ->
// Update profilePictureUrl after successful upload
profilePictureUrl = uri.toString()
Log.d("EditProfileActivity", "Image uploaded successfully: $profilePictureUrl")
Toast.makeText(this, "Image uploaded successfully!", Toast.LENGTH_SHORT).show()

// Stop the animation and re-enable interaction
profilePictureLoadingAnimation.cancelAnimation()
profilePictureLoadingAnimation.visibility = View.INVISIBLE
window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
}
}
.addOnFailureListener { exception ->
Log.e("EditProfileActivity", "Error uploading image: ${exception.message}", exception)
Toast.makeText(this, "Error uploading image", Toast.LENGTH_SHORT).show()

// Stop the animation and re-enable interaction
profilePictureLoadingAnimation.cancelAnimation()
profilePictureLoadingAnimation.visibility = View.INVISIBLE
window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
}
} else {
Toast.makeText(this, "Please select an image", Toast.LENGTH_SHORT).show()
}
}

private fun updateUserProfile(displayName: String, email: String, profilePictureUrl: String) {
val userId = auth.currentUser?.uid ?: ""
val userProfile = hashMapOf(
"displayName" to displayName,
"email" to email,
"profilePictureUrl" to profilePictureUrl
)
val userProfileMap: Map<String, Any> = userProfile as Map<String, Any>

firestore.collection("users").document(userId)
.set(userProfileMap, SetOptions.merge())
.addOnSuccessListener {
Log.d("EditProfileActivity", "User profile updated successfully")
Toast.makeText(this, "Profile updated successfully", Toast.LENGTH_SHORT).show()
}
.addOnFailureListener { exception ->
Log.e("EditProfileActivity", "Error updating user profile: ${exception.message}", exception)
Toast.makeText(this, "Error updating profile", Toast.LENGTH_SHORT).show()
if (displayedPhoto != state.photo) {
displayedPhoto = state.photo
val image = findViewById<ImageView>(R.id.profileImageView)
if (state.photo.isBlank()) image.setImageResource(R.drawable.ic_account_circle)
else Glide.with(this).load(state.photo).transform(CircleCrop()).into(image)
}
if (state.saved) finish()
}
model.load(sessionAccount)
}
}
}
9 changes: 5 additions & 4 deletions app/src/main/java/com/example/acetimetracker/GraphActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@ package com.example.acetimetracker
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.example.acetimetracker.account.AccountActivity
import com.github.mikephil.charting.charts.PieChart
import com.github.mikephil.charting.data.PieData
import com.github.mikephil.charting.data.PieDataSet
import com.github.mikephil.charting.data.PieEntry
import com.github.mikephil.charting.formatter.PercentFormatter
import com.github.mikephil.charting.utils.ColorTemplate
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.FirebaseFirestore

class GraphActivity : AppCompatActivity() {
class GraphActivity : AccountActivity() {

private lateinit var chart: PieChart
private lateinit var firestore: FirebaseFirestore

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!sessionReady) return
setContentView(R.layout.activity_graph)

chart = findViewById(R.id.chart) // Assuming you change the ID in the layout
Expand All @@ -29,11 +29,12 @@ class GraphActivity : AppCompatActivity() {
}

private fun fetchAndDisplayChartData() {
val userId = FirebaseAuth.getInstance().currentUser?.uid ?: ""
val userId = currentSessionUid() ?: return

firestore.collection("users").document(userId)
.get()
.addOnSuccessListener { document ->
if (currentSessionUid() != userId) return@addOnSuccessListener
val dailyTimesheetEntries = document.get("dailyTimesheetEntries") as List<Map<String, Any>>?

if (dailyTimesheetEntries != null) {
Expand Down
Loading