Initial commit - v1.1.9

This commit is contained in:
2026-02-22 12:03:04 +09:00
commit 27339dc7b7
180 changed files with 12908 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
package com.example.shiftalarm
import org.junit.Test
import java.io.File
import java.io.FileOutputStream
import java.nio.charset.StandardCharsets
import java.time.LocalDate
import java.time.DayOfWeek
import java.time.temporal.ChronoUnit
import java.time.format.DateTimeFormatter
class CrossVersionTest {
// ==========================================
// 1. Simulation Configuration
// ==========================================
val START_DATE = LocalDate.of(2026, 2, 1)
val END_DATE = LocalDate.of(2026, 3, 31)
// Target Android Versions
val TARGET_VERSIONS = listOf(
26 to "Android 8.0 (Oreo)",
29 to "Android 10 (Q)",
31 to "Android 12 (S)",
33 to "Android 13 (T)",
34 to "Android 14 (U)"
)
// ==========================================
// 2. Logic Engines (Replica of App Code)
// ==========================================
object SimShiftManager {
// Jeonju Logic
val JEONJU_CYCLE = listOf(
"석간", "석간", "석간", "휴무", "휴무",
"주간", "주간", "주간", "주간", "주간", "휴무", "휴무",
"야간", "야간", "야간", "야간", "야간", "휴무",
"석간", "석간"
)
val JEONJU_BASE = LocalDate.of(2026, 2, 1)
val TEAM_OFFSETS = mapOf("A" to 0, "B" to 15, "C" to 10, "D" to 5)
fun getJeonju(date: LocalDate, team: String): String {
val offset = TEAM_OFFSETS[team] ?: 0
val days = ChronoUnit.DAYS.between(JEONJU_BASE, date).toInt()
val index = Math.floorMod(days + offset, JEONJU_CYCLE.size)
return JEONJU_CYCLE[index]
}
}
// ==========================================
// 3. Android API Simulation Logic
// ==========================================
fun simulateAlarmBehavior(sdkInt: Int, alarmTime: String): String {
// Core Logic: We use AlarmManager.setAlarmClock across ALL versions for maximum reliability
// (As confirmed in AlarmUtils.kt implementation)
var method = "AlarmManager.setAlarmClock"
var desc = "Reliable, shows icon"
// Permission Check
var perms = "None"
if (sdkInt >= 31) { // Android 12+
// SCHEDULE_EXACT_ALARM is required for exact alarms
perms = "SCHEDULE_EXACT_ALARM"
}
if (sdkInt >= 33) { // Android 13+
// Include Notification Permission
perms += ", POST_NOTIFICATIONS"
}
// PendingIntent Flags
var flags = "FLAG_UPDATE_CURRENT"
if (sdkInt >= 23) { // Android 6.0+
flags += " | FLAG_IMMUTABLE" // Mandatory on Android 12+, Good practice on M+
}
// Doze Mode
var doze = "Wakes Device (Idle)"
if (method == "AlarmManager.setAlarmClock") {
doze = "Bypasses Doze Mode (Highest Priority)"
}
return "Using $method ($desc) | Flags: $flags | Perms: $perms | Doze: $doze"
}
// ==========================================
// 4. Runner
// ==========================================
@Test
fun runCrossVersionSimulation() {
val outputFile = File("CROSS_VERSION_REPORT.md")
val writer = outputFile.bufferedWriter(StandardCharsets.UTF_8)
writer.write("# 📱 Cross-Version Alarm Accuracy Report (Android 8.0 ~ 14)\n")
writer.write("**Period**: 2026-02-01 ~ 2026-03-31\n")
writer.write("**Scenario**: Jeonju Factory - Team C (Standard)\n\n")
writer.write("| Date | Shift | Alarm Time | Android Version | Simulation Result (API Behavior) |\n")
writer.write("|---|---|---|---|---|\n")
var curr = START_DATE
val factory = "Jeonju"
val team = "C"
while (!curr.isAfter(END_DATE)) {
val shift = SimShiftManager.getJeonju(curr, team)
// Determine Alarm Time
var time: String? = null
if (shift == "주간") time = "06:00"
else if (shift == "석간") time = "14:00"
else if (shift == "야간") time = "22:00"
if (time != null) {
// For each day, test against ALL target Android versions
for ((sdk, verName) in TARGET_VERSIONS) {
val behavior = simulateAlarmBehavior(sdk, time)
writer.write("| $curr | $shift | **$time** | $verName | $behavior |\n")
}
} else {
// write just one line for OFF day to reduce noise, or skip?
// Let's print one line for clarity that logic works
writer.write("| $curr | $shift | - | All Versions | No Alarm Scheduled (OFF) |\n")
}
curr = curr.plusDays(1)
}
writer.close()
println("Cross-Version Report Generated at CROSS_VERSION_REPORT.md")
}
}

View File

@@ -0,0 +1,231 @@
package com.example.shiftalarm
import org.junit.Test
import java.io.File
import java.io.FileOutputStream
import java.nio.charset.StandardCharsets
import java.time.LocalDate
import java.time.DayOfWeek
import java.time.temporal.ChronoUnit
class SimulationTest {
// ==========================================
// 1. Simulation Configuration
// ==========================================
val START_DATE = LocalDate.of(2026, 2, 1)
val END_DATE = LocalDate.of(2026, 3, 31)
// Holidays (Mock)
val HOLIDAYS = setOf(
LocalDate.of(2026, 2, 11), // Mock Holiday
LocalDate.of(2026, 3, 1) // Samiljeol
)
// ==========================================
// 2. Logic (Duplicated for Safety in Unit Test)
// ==========================================
// Internal Logic Simulating ShiftCalculator
object SimShiftCalculator {
val JEONJU_CYCLE = listOf(
"석간", "석간", "석간", "휴무", "휴무",
"주간", "주간", "주간", "주간", "주간", "휴무", "휴무",
"야간", "야간", "야간", "야간", "야간", "휴무",
"석간", "석간"
)
val JEONJU_BASE_DATE = LocalDate.of(2026, 2, 1) // Verify base date usage in actual code
val TEAM_OFFSETS = mapOf("A" to 0, "B" to 15, "C" to 10, "D" to 5)
fun getJeonjuShift(date: LocalDate, team: String): String {
val offset = TEAM_OFFSETS[team] ?: 0
val days = ChronoUnit.DAYS.between(JEONJU_BASE_DATE, date).toInt()
val index = Math.floorMod(days + offset, JEONJU_CYCLE.size)
return JEONJU_CYCLE[index]
}
fun getNonsanShift(date: LocalDate, team: String): String {
val dayOfWeek = date.dayOfWeek
if (dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY) return "휴무"
val baseDate = LocalDate.of(2026, 2, 9)
val alignedDate = date.minusDays((dayOfWeek.value - 1).toLong())
val weeksPassed = ChronoUnit.WEEKS.between(baseDate, alignedDate).toInt()
val rotation = listOf("주간", "야간", "석간")
val startOffset = when (team) {
"A" -> 0; "B" -> 1; "C" -> 2; else -> 0
}
val index = Math.floorMod(startOffset + weeksPassed, 3)
return rotation[index]
}
}
// Internal Logic Simulating ShiftAlarmDefaults values
fun getDefaultAlarmTime(shift: String, factory: String): String? {
if (!listOf("주간", "석간", "야간", "야간 맞교대", "주간 맞교대").contains(shift)) return null
return if (factory == "Nonsan") {
when (shift) {
"주간" -> "07:00"
"석간" -> "15:00"
"야간" -> "23:00"
"야간 맞교대" -> "19:00"
else -> null
}
} else {
when (shift) {
"주간" -> "06:00"
"석간" -> "14:00"
"야간" -> "22:00"
"야간 맞교대" -> "18:00"
"주간 맞교대" -> "06:00"
else -> null
}
}
}
// ==========================================
// 3. User Data Structures
// ==========================================
data class UserSettings(
val factory: String,
val team: String,
val customShifts: Map<LocalDate, String> = emptyMap(),
val customAlarmTimes: Map<LocalDate, String> = emptyMap(),
val customShiftAlarmTimes: Map<String, String> = emptyMap(),
val name: String
)
data class SimResult(
val date: LocalDate,
val shift: String,
val alarmTime: String?,
val isHoliday: Boolean,
val notes: String
)
// ==========================================
// 4. Engine
// ==========================================
fun runSimulation(user: UserSettings): List<SimResult> {
val results = mutableListOf<SimResult>()
var currentDate = START_DATE
while (!currentDate.isAfter(END_DATE)) {
var shift = if (user.factory == "Nonsan") {
SimShiftCalculator.getNonsanShift(currentDate, user.team)
} else {
SimShiftCalculator.getJeonjuShift(currentDate, user.team)
}
var notes = ""
if (user.customShifts.containsKey(currentDate)) {
shift = user.customShifts[currentDate]!!
notes += "[Manual Override] "
}
var alarmTime: String? = null
if (shift == "휴무" || shift == "비번") {
alarmTime = null
} else {
alarmTime = getDefaultAlarmTime(shift, user.factory)
if (user.customShiftAlarmTimes.containsKey(shift)) {
alarmTime = user.customShiftAlarmTimes[shift]
notes += "[Shift Rule] "
}
if (user.customAlarmTimes.containsKey(currentDate)) {
alarmTime = user.customAlarmTimes[currentDate]
notes += "[Date Rule] "
}
}
val isHoliday = HOLIDAYS.contains(currentDate)
if (isHoliday) notes += "HOLIDAY "
results.add(SimResult(currentDate, shift, alarmTime, isHoliday, notes))
currentDate = currentDate.plusDays(1)
}
return results
}
// ==========================================
// 5. Reporting
// ==========================================
fun generateReport(user: UserSettings, results: List<SimResult>): String {
val sb = StringBuilder()
sb.append("\n### Simulation Report: ${user.name} (${user.factory} - Team ${user.team})\n")
sb.append("| Date | Day | Shift | Alarm Time | Status | Logic Check |\n")
sb.append("|---|---|---|---|---|---|\n")
val dayNames = listOf("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")
for (r in results) {
val dayOfW = r.date.dayOfWeek.value
// 1=Mon...7=Sun.
// list index 0=Sun, 6=Sat.
// 1 % 7 = 1 (Mon). 7 % 7 = 0 (Sun). Correct.
val dayStr = dayNames[dayOfW % 7]
val alarmStr = r.alarmTime ?: "-"
val status = if (r.alarmTime != null) "ON" else "OFF"
var check = "OK"
if (r.shift == "주간" && user.factory == "Jeonju" && r.alarmTime == "06:00") check = "Valid (Default)"
if (r.shift == "주간" && user.factory == "Nonsan" && r.alarmTime == "07:00") check = "Valid (Nonsan)"
sb.append("| ${r.date} | $dayStr | ${r.shift} | $alarmStr | $status | $check ${r.notes} |\n")
}
return sb.toString()
}
@Test
fun runFullSimulation() {
val outputFile = File("simulation_result.md")
// Use UTF-8 explicitly
val writer = outputFile.bufferedWriter(StandardCharsets.UTF_8)
writer.write("# ShiftRing Alarm Logic Simulation V2 (Robust)\n")
writer.write("Period: $START_DATE ~ $END_DATE\n")
// Scenario A: Jeonju C
val userJeonju = UserSettings("Jeonju", "C", name = "User 1 (Jeonju-C Standard)")
val resA = runSimulation(userJeonju)
val reportA = generateReport(userJeonju, resA)
writer.write(reportA)
println(reportA)
// Scenario B: Nonsan A
val userNonsan = UserSettings("Nonsan", "A", name = "User 2 (Nonsan-A Standard)")
val resB = runSimulation(userNonsan)
val reportB = generateReport(userNonsan, resB)
writer.write(reportB)
println(reportB)
// Scenario C: Custom
val userCustom = UserSettings(
factory = "Jeonju",
team = "A",
customShifts = mapOf(
LocalDate.of(2026, 2, 14) to "휴무",
LocalDate.of(2026, 2, 15) to "주간"
),
customAlarmTimes = mapOf(
LocalDate.of(2026, 2, 20) to "05:00"
),
name = "User 3 (Jeonju-A Customizer)"
)
val resC = runSimulation(userCustom)
val reportC = generateReport(userCustom, resC)
writer.write(reportC)
println(reportC)
writer.close()
}
}