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
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.example.slowclock

import android.app.UiModeManager
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Bitmap
import android.widget.TextView
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.scrollTo
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.platform.app.InstrumentationRegistry
import com.example.slowclock.data.model.ThemeMode
import com.example.slowclock.data.remote.repository.SettingsRepository
import com.example.slowclock.ui.alarm.AlarmFullScreenActivity
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File
import com.example.slowclock.core.alarm.R as AlarmR

class ThemeAndAlarmAccessibilityTest {
@Test
fun applicationModeOverridesSystemAndCanReturnToSystem() {
val context = ApplicationProvider.getApplicationContext<Context>()
val settings = SettingsRepository(context)
val previous = settings.getThemeMode()
val systemNight = context.getSystemService(UiModeManager::class.java).nightMode
try {
settings.setThemeMode(ThemeMode.DARK)
awaitNightMode(context, Configuration.UI_MODE_NIGHT_YES)
assertEquals(0xFF14120F.toInt(), context.getColor(R.color.window_background))
settings.setThemeMode(ThemeMode.LIGHT)
awaitNightMode(context, Configuration.UI_MODE_NIGHT_NO)
assertEquals(0xFFFAF6F0.toInt(), context.getColor(R.color.window_background))
settings.setThemeMode(ThemeMode.SYSTEM)
if (systemNight == UiModeManager.MODE_NIGHT_YES || systemNight == UiModeManager.MODE_NIGHT_NO) {
awaitNightMode(
context,
if (systemNight == UiModeManager.MODE_NIGHT_YES) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO,
)
}
} finally {
settings.setThemeMode(previous)
}
}

// 글자 배율은 기기 전역 설정이라 계측 테스트에서 바꾸지 않는다. 큰 글자에서의 알람 화면
// 확인은 실기기 몫으로 남긴다(#220). 여기서는 기본 배율의 표기와 도달 가능성만 본다.
@Test
fun alarmClockReadsTwelveHourTimeAndActionsStayReachable() {
val context = ApplicationProvider.getApplicationContext<Context>()
val intent =
Intent(context, AlarmFullScreenActivity::class.java)
.putExtra("title", "혈압약 먹기")
.putExtra("desc", "식사 후 물과 함께 복용해 주세요.")
ActivityScenario.launch<AlarmFullScreenActivity>(intent).use { scenario ->
onView(withId(AlarmR.id.currentTimeText)).check(matches(isDisplayed()))
scenario.onActivity { activity ->
val clock = activity.findViewById<TextView>(AlarmR.id.currentTimeText)
assertTrue(clock.text.toString().matches(Regex("(오전|오후) [0-9]{1,2}:[0-9]{2}")))
val layout = clock.layout
for (line in 0 until layout.lineCount) {
assertEquals(0, layout.getEllipsisCount(line))
assertTrue(layout.getLineRight(line) <= clock.width - clock.totalPaddingLeft - clock.totalPaddingRight)
}
}
capture(context, "alarm-clock.png")
onView(withId(AlarmR.id.dismissButton)).perform(scrollTo()).check(matches(isDisplayed()))
capture(context, "alarm-actions.png")
}
}

private fun awaitNightMode(
context: Context,
expected: Int,
) {
val deadline = System.currentTimeMillis() + 5_000
while ((context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) != expected &&
System.currentTimeMillis() < deadline
) {
Thread.sleep(50)
}
assertEquals(expected, context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK)
}

private fun capture(
context: Context,
name: String,
) {
// 창 진입 애니메이션의 중간 프레임을 증빙으로 저장하지 않는다.
Thread.sleep(500)
val bitmap = InstrumentationRegistry.getInstrumentation().uiAutomation.takeScreenshot()
File(context.cacheDir, name).outputStream().use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) }
bitmap.recycle()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.example.slowclock

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasScrollAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.espresso.matcher.ViewMatchers.withId
import com.example.slowclock.ui.recommendation.RecommendationScreen
import com.example.slowclock.ui.theme.SlowClockTheme
import com.example.slowclock.ui.timeline.TimelineDatePickerDialog
import org.hamcrest.Matchers.endsWith
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import java.util.Calendar

class WindowAccessibilityTest {
@get:Rule
val compose = createComposeRule()

@Test
fun healthNoticeDoesNotPreventScrollingToStudentRecommendations() {
compose.setContent {
SlowClockTheme {
RecommendationScreen(onSelectRecommendation = {}, onNavigateBack = {})
}
}
compose.onNodeWithText("학생").performClick()
// LazyColumn 은 보이지 않는 줄을 아직 만들지 않는다. performScrollTo 는 이미 만들어진
// 줄만 찾으므로 화면이 짧거나 글자 배율이 높으면 노드를 못 찾고 실패한다. 목록을
// 스크롤해 가며 찾는 performScrollToNode 를 쓴다.
compose.onNode(hasScrollAction()).performScrollToNode(hasText("자습"))
compose.onNodeWithText("자습").assertIsDisplayed()
}

@Test
fun calendarIsRemovedWithItsScreenWithoutCallingDisposedCallbacks() {
var mounted by mutableStateOf(true)
var dismisses = 0
compose.setContent {
SlowClockTheme {
if (mounted) {
TimelineDatePickerDialog(Calendar.getInstance(), { _, _, _ -> }, { dismisses++ })
}
}
}
onView(withClassName(endsWith("DatePicker"))).check(matches(isDisplayed()))
compose.runOnIdle { mounted = false }
compose.waitForIdle()
onView(withClassName(endsWith("DatePicker"))).check(doesNotExist())
compose.runOnIdle { assertEquals(0, dismisses) }
}

@Test
fun calendarUsesCurrentCallbackWithoutOpeningAnotherWindow() {
var generation by mutableIntStateOf(0)
var selectedGeneration = -1
var dismisses = 0
compose.setContent {
val current = generation
TimelineDatePickerDialog(
Calendar.getInstance(),
{ _, _, _ -> selectedGeneration = current },
{ dismisses++ },
)
}
compose.runOnIdle { generation = 1 }
onView(withId(android.R.id.button1)).perform(click())
compose.runOnIdle {
assertEquals(1, selectedGeneration)
assertEquals(1, dismisses)
}
}
}
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
</activity>
<activity
android:name=".ui.alarm.AlarmFullScreenActivity"
android:theme="@style/Theme.SlowClock.Alarm"
android:excludeFromRecents="true"
android:launchMode="singleInstance"
android:showOnLockScreen="true"
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/com/example/slowclock/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.core.content.edit
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
Expand Down Expand Up @@ -117,7 +118,9 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
// 테마는 사용자가 정보 화면에서 고른 값을 따른다. 기본은 기기 설정이다.
val themeMode by settingsRepository.observeThemeMode().collectAsStateWithLifecycle(ThemeMode.SYSTEM)
val initialTheme = remember { settingsRepository.getThemeMode() }
val themeFlow = remember { settingsRepository.observeThemeMode() }
val themeMode by themeFlow.collectAsStateWithLifecycle(initialTheme)
SlowClockTheme(
darkTheme =
when (themeMode) {
Expand Down
12 changes: 11 additions & 1 deletion app/src/main/java/com/example/slowclock/SlowClockApplication.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
package com.example.slowclock

import android.app.Application
import com.example.slowclock.data.remote.repository.SettingsRepository
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject

/**
* Firebase 초기화는 google-services 플러그인이 넣는 FirebaseInitProvider 가 앱 시작 시 한다.
* Firestore 오프라인 캐시 설정은 인스턴스를 제공하는 `FirebaseModule` 이 맡는다.
*/
@HiltAndroidApp
class SlowClockApplication : Application()
class SlowClockApplication : Application() {
@Inject
lateinit var settingsRepository: SettingsRepository

override fun onCreate() {
super.onCreate()
settingsRepository.applyThemeMode()
}
}
5 changes: 5 additions & 0 deletions app/src/main/res/values-night/colors.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- SlowClockTheme의 PaperBackground/NightBackground와 시작 창을 맞춘다. -->
<color name="window_background">#14120F</color>
</resources>
8 changes: 8 additions & 0 deletions app/src/main/res/values-night/themes.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.SlowClock" parent="android:Theme.Material.NoActionBar">
<item name="android:windowBackground">@color/window_background</item>
<item name="android:windowSplashScreenBackground">@color/window_background</item>
<item name="android:forceDarkAllowed">false</item>
</style>
</resources>
5 changes: 5 additions & 0 deletions app/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- SlowClockTheme의 PaperBackground/NightBackground와 시작 창을 맞춘다. -->
<color name="window_background">#FAF6F0</color>
</resources>
9 changes: 6 additions & 3 deletions app/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>

<style name="Theme.SlowClock" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
<style name="Theme.SlowClock" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:windowBackground">@color/window_background</item>
<item name="android:windowSplashScreenBackground">@color/window_background</item>
<item name="android:forceDarkAllowed">false</item>
</style>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.android.tools.screenshot.PreviewTest
import com.example.slowclock.ui.addschedule.components.RecommendationPlaceholder
import com.example.slowclock.ui.recommendation.RecommendationScreen
import com.example.slowclock.ui.theme.SlowClockTheme

@PreviewTest
Expand All @@ -18,3 +19,13 @@ internal fun RecommendationLargeTextScreenshot() {
}
}
}

@PreviewTest
@Preview(name = "추천 안내 360", widthDp = 360, heightDp = 800)
@Preview(name = "추천 안내 큰 글자 320", widthDp = 320, heightDp = 1000, fontScale = 2f)
@Composable
internal fun RecommendationHealthNoticeScreenshot() {
SlowClockTheme(darkTheme = false) {
RecommendationScreen(onSelectRecommendation = {}, onNavigateBack = {})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.example.slowclock

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.android.tools.screenshot.PreviewTest
import com.example.slowclock.ui.addschedule.ScheduleTimeInput
import com.example.slowclock.ui.addschedule.components.TimePickerSection
import com.example.slowclock.ui.addschedule.components.TitleInputSection
import com.example.slowclock.ui.theme.SlowClockTheme

@PreviewTest
@Preview(name = "입력 안내 큰 글자 밝게", widthDp = 320, heightDp = 1600, fontScale = 2f)
@Composable
internal fun ScheduleInputLightScreenshot() = ScheduleInputPreview(dark = false)

@PreviewTest
@Preview(name = "입력 안내 큰 글자 어둡게", widthDp = 320, heightDp = 1600, fontScale = 2f)
@Composable
internal fun ScheduleInputDarkScreenshot() = ScheduleInputPreview(dark = true)

@Composable
private fun ScheduleInputPreview(dark: Boolean) {
SlowClockTheme(darkTheme = dark) {
Surface(color = MaterialTheme.colorScheme.background) {
Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 24.dp)) {
TitleInputSection(title = "", description = "", onTitleChange = {}, onDescriptionChange = {})
TimePickerSection(
startTime = ScheduleTimeInput("14", "30"),
endTime = ScheduleTimeInput("25", "00"),
onTimeSelect = {},
onEndTimeSelect = {},
)
}
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,13 @@ class AlarmFullScreenActivity : Activity() {
}

private fun updateCurrentTime(timeTextView: TextView) {
val formatter = SimpleDateFormat("a h:mm", Locale.KOREAN)
timeRunnable =
object : Runnable {
override fun run() {
val sdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
timeTextView.text = sdf.format(Date())
timeHandler.postDelayed(this, 1000)
val now = System.currentTimeMillis()
timeTextView.text = formatter.format(Date(now))
timeHandler.postDelayed(this, 60_000L - now % 60_000L)
}
}
timeHandler.post(timeRunnable)
Expand Down Expand Up @@ -150,7 +151,7 @@ class AlarmFullScreenActivity : Activity() {
startService(AlarmTriggerService.dismissIntent(this, ringingRequestCode(), intent.getStringExtra(AlarmNotifications.EXTRA_TOKEN)))
}

/** 화면이 안 보이면 시계를 멈춘다. 보이지 않는 화면을 1초마다 다시 그릴 이유가 없다(#131). */
/** 화면이 안 보이면 시계를 멈춘다. 보이지 않는 화면을 다시 그릴 이유가 없다(#131). */
override fun onStop() {
super.onStop()
timeHandler.removeCallbacks(timeRunnable)
Expand Down
6 changes: 4 additions & 2 deletions core/alarm/src/main/res/layout/activity_alarm_fullscreen.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#107 에서 화면 밝기 선택에 같은 함정이 있었고 같은 방법으로 고쳤다(#132).
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/alarm_background"
Expand All @@ -20,11 +21,12 @@

<TextView
android:id="@+id/currentTimeText"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:fontFamily="monospace"
android:text="00:00:00"
tools:text="오전 12:00"
android:textAlignment="center"
android:textColor="@color/alarm_on_background"
android:textSize="48sp"
android:textStyle="bold" />
Expand Down
8 changes: 8 additions & 0 deletions core/alarm/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.SlowClock.Alarm" parent="android:Theme.Material.NoActionBar">
<item name="android:windowBackground">@color/alarm_background_start</item>
<item name="android:windowSplashScreenBackground">@color/alarm_background_start</item>
<item name="android:forceDarkAllowed">false</item>
</style>
</resources>
Loading
Loading