Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ local.properties
/keystore
/release
app-release.aab
.artifacts
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ android {
applicationId "de.practicetime.practicetime"
minSdk 23
targetSdk 35
versionCode 15
versionName "1.2.0"
versionCode 16
versionName "1.3.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,37 +100,68 @@ abstract class SessionDao : BaseDao<Session>(tableName = "session") {
newRating: Int,
newSections: List<SectionWithCategory>,
newComment: String,
newTimestamp: Long? = null
) {
// the difference session will save the difference in the section duration
// between the original session and the edited sections
val sessionWithSectionsWithCategoriesWithGoals =
getWithSectionsWithCategoriesWithGoals(sessionId)

sessionWithSectionsWithCategoriesWithGoals.sections.forEach { (section, _) ->
section.duration = (newSections.find {
it.section.id == section.id
}?.section?.duration ?: 0) - (section.duration ?: 0)
}
if (newTimestamp == null) {
// Existing logic for when timestamp doesn't change
sessionWithSectionsWithCategoriesWithGoals.sections.forEach { (section, _) ->
section.duration = (newSections.find {
it.section.id == section.id
}?.section?.duration ?: 0) - (section.duration ?: 0)
}

val goalProgress = PracticeTime.goalDescriptionDao.computeGoalProgressForSession(
sessionWithSectionsWithCategoriesWithGoals,
checkArchived = true
)

// get all active goal instances at the time of the session
PracticeTime.goalInstanceDao.apply {
get(
goalDescriptionIds = goalProgress.keys.toList(),
from = sessionWithSectionsWithCategoriesWithGoals.sections.first().section.timestamp
// add the progress
).onEach { instance ->
goalProgress[instance.goalDescriptionId].also { progress ->
if (progress != null) {
// progress should not get lower than 0
instance.progress = maxOf(0 , instance.progress + progress)
val goalProgress = PracticeTime.goalDescriptionDao.computeGoalProgressForSession(
sessionWithSectionsWithCategoriesWithGoals,
checkArchived = true
)

// get all active goal instances at the time of the session
PracticeTime.goalInstanceDao.apply {
get(
goalDescriptionIds = goalProgress.keys.toList(),
from = sessionWithSectionsWithCategoriesWithGoals.sections.first().section.timestamp
// add the progress
).onEach { instance ->
goalProgress[instance.goalDescriptionId].also { progress ->
if (progress != null) {
// progress should not get lower than 0
instance.progress = maxOf(0, instance.progress + progress)
}
}
update(instance)
}
}
} else {
// Logic for when timestamp changes
// 1. Remove OLD progress
val oldGoalProgress = PracticeTime.goalDescriptionDao.computeGoalProgressForSession(
sessionWithSectionsWithCategoriesWithGoals,
checkArchived = true
)
PracticeTime.goalInstanceDao.apply {
get(
goalDescriptionIds = oldGoalProgress.keys.toList(),
from = sessionWithSectionsWithCategoriesWithGoals.sections.first().section.timestamp
).onEach { instance ->
oldGoalProgress[instance.goalDescriptionId].also { progress ->
if (progress != null) {
instance.progress = maxOf(0, instance.progress - progress)
}
}
update(instance)
}
update(instance)
}

// 2. Update timestamps in data structures
val diff = newTimestamp - sessionWithSectionsWithCategoriesWithGoals.session.createdAt
sessionWithSectionsWithCategoriesWithGoals.session.createdAt = newTimestamp
newSections.forEach { (section, _) ->
section.timestamp += diff
}
}

Expand All @@ -145,5 +176,28 @@ abstract class SessionDao : BaseDao<Session>(tableName = "session") {
}

update(sessionWithSectionsWithCategoriesWithGoals.session)

if (newTimestamp != null) {
// 3. Add NEW progress at the new timestamp
// Refetch to get the updated relations with goal descriptions at the new time
val updatedSessionWithGoals = getWithSectionsWithCategoriesWithGoals(sessionId)
val newGoalProgress = PracticeTime.goalDescriptionDao.computeGoalProgressForSession(
updatedSessionWithGoals,
checkArchived = true
)
PracticeTime.goalInstanceDao.apply {
get(
goalDescriptionIds = newGoalProgress.keys.toList(),
from = newTimestamp
).onEach { instance ->
newGoalProgress[instance.goalDescriptionId].also { progress ->
if (progress != null) {
instance.progress += progress
}
}
update(instance)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ data class Section (
@ColumnInfo(name="session_id", index = true) var sessionId: Long?,
@ColumnInfo(name="category_id", index = true) val categoryId: Long,
@ColumnInfo(name="duration") var duration: Int?,
@ColumnInfo(name="timestamp") val timestamp: Long,
@ColumnInfo(name="timestamp") var timestamp: Long,
) : BaseModel()
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
package de.practicetime.practicetime.ui.sessionlist

import android.app.AlertDialog
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
Expand Down Expand Up @@ -35,14 +37,18 @@ import de.practicetime.practicetime.database.entities.SessionWithSectionsWithCat
import de.practicetime.practicetime.shared.EditTimeDialog
import de.practicetime.practicetime.ui.MainActivity
import de.practicetime.practicetime.utils.TIME_FORMAT_HUMAN_PRETTY
import de.practicetime.practicetime.utils.epochSecondsToDate
import de.practicetime.practicetime.utils.getDurationString
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter


class FullscreenSessionActivity : AppCompatActivity() {

private lateinit var ratingBarView: RatingBar
private lateinit var dateTextView: TextView
private lateinit var sectionListView: RecyclerView
private lateinit var commentFieldView: TextView

Expand All @@ -55,12 +61,14 @@ class FullscreenSessionActivity : AppCompatActivity() {
private var sessionWithSectionsWithCategories: SessionWithSectionsWithCategories? = null
private var selectedSection: Section? = null

private var sessionTimestamp: Long = 0
private var timestampEdited = false
private var showCommentPlaceholder = true

private var sessionEdited = false

override fun onBackPressed() {
if(!sessionEdited) return super.onBackPressed()
if(!sessionEdited && !timestampEdited) return super.onBackPressed()
confirmationDialog.apply {
setMessage(getString(R.string.discard_changes_dialog_message))
show()
Expand Down Expand Up @@ -103,7 +111,7 @@ class FullscreenSessionActivity : AppCompatActivity() {
if (sessionId != null) {
showFullscreenSession(sessionId)
findViewById<MaterialButton>(R.id.fullscreen_session_cancel).setOnClickListener {
if(!sessionEdited) return@setOnClickListener exitActivity()
if(!sessionEdited && !timestampEdited) return@setOnClickListener exitActivity()
confirmationDialog.apply {
setMessage(getString(R.string.discard_changes_dialog_message))
show()
Expand Down Expand Up @@ -133,6 +141,7 @@ class FullscreenSessionActivity : AppCompatActivity() {
newSections = sectionAdapterData,
newComment = if(showCommentPlaceholder) ""
else commentFieldView.text.toString(),
newTimestamp = if (timestampEdited) sessionTimestamp else null
)
dismiss()
exitActivity()
Expand All @@ -148,6 +157,7 @@ class FullscreenSessionActivity : AppCompatActivity() {
}

private fun showFullscreenSession(sessionId: Long) {
dateTextView = findViewById(R.id.fullscreen_session_date_text)
ratingBarView = findViewById(R.id.fullscreen_session_rating_bar)
sectionListView = findViewById(R.id.fullscreen_session_section_list)
commentFieldView = findViewById(R.id.fullscreen_session_comment_field)
Expand All @@ -169,6 +179,13 @@ class FullscreenSessionActivity : AppCompatActivity() {
PracticeTime.sessionDao.getWithSectionsWithCategories(sessionId)
val (session, sectionsWithCategories) = sessionWithSectionsWithCategories!!

sessionTimestamp = session.createdAt
updateDateUI()

findViewById<View>(R.id.fullscreen_session_date_layout).setOnClickListener {
showDateTimePicker()
}

ratingBarView.progress = session.rating
ratingBarView.setOnRatingBarChangeListener { _, _, _ ->
sessionEdited = true
Expand All @@ -189,6 +206,46 @@ class FullscreenSessionActivity : AppCompatActivity() {

}

private fun updateDateUI() {
val zonedDateTime = epochSecondsToDate(sessionTimestamp)
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm")

dateTextView.text = getString(
R.string.fullscreen_session_date,
zonedDateTime.format(dateFormatter),
zonedDateTime.format(timeFormatter)
)
}

private fun showDateTimePicker() {
val currentDateTime = epochSecondsToDate(sessionTimestamp)

DatePickerDialog(
this,
{ _, year, month, dayOfMonth ->
TimePickerDialog(
this,
{ _, hourOfDay, minute ->
val newDateTime = ZonedDateTime.of(
year, month + 1, dayOfMonth, hourOfDay, minute, 0, 0,
currentDateTime.zone
)
sessionTimestamp = newDateTime.toEpochSecond()
timestampEdited = true
updateDateUI()
},
currentDateTime.hour,
currentDateTime.minute,
true
).show()
},
currentDateTime.year,
currentDateTime.monthValue - 1,
currentDateTime.dayOfMonth
).show()
}

private fun editSectionDurationHandler(section: Section?, newSectionDuration: Int) {
section?.duration = newSectionDuration
sectionAdapterData.indexOfFirst {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* This software is licensed under the MIT license
*
* Copyright (c) 2022, Javier Carbone, author Matthias Emde
*/

package de.practicetime.practicetime.ui.timer

import android.media.AudioManager
import android.media.ToneGenerator
import android.os.Bundle
import android.os.CountDownTimer
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.google.android.material.switchmaterial.SwitchMaterial
import de.practicetime.practicetime.R
import java.util.Locale

class TimerFragment : Fragment() {

private lateinit var countdownText: TextView
private lateinit var inputMinutes: EditText
private lateinit var inputSeconds: EditText
private lateinit var startBtn: Button
private lateinit var stopBtn: Button
private lateinit var loopSwitch: SwitchMaterial
private lateinit var beepSwitch: SwitchMaterial

private var timer: CountDownTimer? = null
private var isRunning = false
private val toneGenerator = ToneGenerator(AudioManager.STREAM_ALARM, 100)

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_timer, container, false)
countdownText = view.findViewById(R.id.timerCountdown)
inputMinutes = view.findViewById(R.id.timerInputMinutes)
inputSeconds = view.findViewById(R.id.timerInputSeconds)
startBtn = view.findViewById(R.id.timerStartBtn)
stopBtn = view.findViewById(R.id.timerStopBtn)
loopSwitch = view.findViewById(R.id.timerLoopSwitch)
beepSwitch = view.findViewById(R.id.timerBeepSwitch)

startBtn.setOnClickListener { startTimer() }
stopBtn.setOnClickListener { stopTimer() }

return view
}

private fun startTimer() {
val minutes = inputMinutes.text.toString().toLongOrNull() ?: 0L
val seconds = inputSeconds.text.toString().toLongOrNull() ?: 30L
val totalMillis = (minutes * 60 + seconds) * 1000

if (totalMillis <= 0) return

isRunning = true
startBtn.isEnabled = false
stopBtn.isEnabled = true
inputMinutes.isEnabled = false
inputSeconds.isEnabled = false
loopSwitch.isEnabled = false
beepSwitch.isEnabled = false

// Keep screen on while timer is running
requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

timer = object : CountDownTimer(totalMillis, 100) {
override fun onTick(millisUntilFinished: Long) {
val sec = (millisUntilFinished / 1000) % 60
val min = (millisUntilFinished / 1000) / 60
countdownText.text = String.format(Locale.getDefault(), "%02d:%02d", min, sec)
}

override fun onFinish() {
if (beepSwitch.isChecked) {
toneGenerator.startTone(ToneGenerator.TONE_PROP_BEEP, 750)
}
if (loopSwitch.isChecked && isRunning) {
startTimer() // Restart for endless loop
} else {
stopTimer()
}
}
}.start()
}

private fun stopTimer() {
isRunning = false
timer?.cancel()
timer = null
startBtn.isEnabled = true
stopBtn.isEnabled = false
inputMinutes.isEnabled = true
inputSeconds.isEnabled = true
loopSwitch.isEnabled = true
beepSwitch.isEnabled = true

val defaultDuration = getString(R.string.goalDialogDefaultDuration)
countdownText.text = String.format(Locale.getDefault(), "%s:%s", defaultDuration, defaultDuration)

// Allow screen to turn off when timer stops
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}

override fun onDestroy() {
super.onDestroy()
timer?.cancel()
toneGenerator.release()
}
}
Loading