Skip to content
Merged
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
Expand Up @@ -4,7 +4,6 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
Expand All @@ -19,6 +18,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.afternote.afternote_fe.EXTRA_DEBUG_START_TIMELETTER
import com.afternote.afternote_fe.MainActivity
import com.afternote.afternote_fe.enableLightEdgeToEdge
import dagger.hilt.android.AndroidEntryPoint

/**
Expand All @@ -30,7 +30,7 @@ import dagger.hilt.android.AndroidEntryPoint
class DebugSettingsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
enableLightEdgeToEdge()
setContent {
Surface(
modifier = Modifier.fillMaxSize(),
Expand Down
25 changes: 25 additions & 0 deletions app/src/main/java/com/afternote/afternote_fe/LightEdgeToEdge.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.afternote.afternote_fe

import android.graphics.Color
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge

/**
* 시스템바 아이콘까지 라이트로 고정한 edge-to-edge (#1719).
*
* 무인자 [enableEdgeToEdge] 는 `SystemBarStyle.auto` 라 `uiMode == NIGHT_YES` 면
* `isAppearanceLightStatusBars` 를 `false` 로 잡아 **흰 아이콘**을 고른다. 그런데
* `AfternoteTheme` 이 라이트로 잠긴 뒤로 Compose 는 다크 기기에서도 `gray1`(#FAFAFA) 을
* 상태바 밑까지 그린다. 둘을 그대로 두면 밝은 배경 위에 흰 시계·배터리·제스처 바가 얹혀
* 모든 화면에서 보이지 않는다.
*
* 그래서 시스템바 스타일도 함께 라이트로 못박는다. **`AfternoteTheme` 의 `isDarkTheme`
* 기본값을 시스템 추종으로 되돌릴 때 이 함수도 같이 되돌린다** — 두 자리는 한 쌍이다.
*/
fun ComponentActivity.enableLightEdgeToEdge() {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT),
navigationBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT),
)
}
3 changes: 1 addition & 2 deletions app/src/main/java/com/afternote/afternote_fe/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.afternote.afternote_fe
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
Expand All @@ -24,7 +23,7 @@ class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
enableLightEdgeToEdge()

enqueueNotificationIntent(intent)

Expand Down
19 changes: 19 additions & 0 deletions app/src/main/res/values-v27/themes.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>

<!--
`android:windowLightNavigationBar` 는 API 27 부터다. minSdk 26 이라 기본 values/ 에 두면
`:app:lintDebug` 가 NewApi 로 죽는다 (#1873).

내용은 values/ 의 같은 스타일과 한 쌍이다 — 앱이 라이트로 잠긴 동안 스플래시의 밝은 배경
(#FAFAFA) 위에 흰 시스템바 아이콘이 얹히지 않게 한다. 다크 팔레트를 되살릴 때
AfternoteTheme 의 isDarkTheme·enableLightEdgeToEdge·values/themes.xml 과 함께 되돌린다.
-->
<style name="Theme.Afternotefe.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splash_background</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
<item name="postSplashScreenTheme">@style/Theme.Afternotefe.Main</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar">true</item>
</style>
</resources>
9 changes: 9 additions & 0 deletions app/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,14 @@
<item name="windowSplashScreenBackground">@color/splash_background</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
<item name="postSplashScreenTheme">@style/Theme.Afternotefe.Main</item>
<!--
Theme.SplashScreen 은 DayNight 라 night 에서 isLightTheme=false 로 떨어지는데
windowSplashScreenBackground 는 #FAFAFA 고정이다. 그대로 두면 다크 기기에서
밝은 스플래시 위에 흰 시스템바 아이콘이 얹혀 보이지 않는다 — 앱이 라이트로
잠긴 동안은(#1719) 배경과 아이콘을 같은 쪽으로 맞춘다. 다크 팔레트를 되살릴 때
AfternoteTheme 의 isDarkTheme·enableLightEdgeToEdge 와 함께 되돌린다.
-->
<!-- windowLightNavigationBar 는 API 27+ 라 values-v27/ 에 둔다 (minSdk 26). -->
<item name="android:windowLightStatusBar">true</item>
</style>
</resources>
18 changes: 16 additions & 2 deletions core/ui/src/main/kotlin/com/afternote/core/ui/theme/Theme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,22 @@ fun AfternoteTheme(
colors: AfternoteColors = lightColors(),
darkColors: AfternoteColors = darkColors(),
typography: AfternoteTypography = AfternoteTypography(),
// 시스템 설정이 다크 모드인지 확인
isDarkTheme: Boolean = isSystemInDarkTheme(),
/**
* 다크 팔레트를 물릴지. **기본값은 라이트 고정이다** (#1719).
*
* 배선만 있고 화면이 안 맞춰진 중간 상태가 양쪽 끝보다 나빴다 — 실측에서 「마음의 기록」
* 헤더가 검정 배경에 검정 글자로 거의 보이지 않았고, 주간 요약 카드는 배경만 밝은 채로
* 남아 라벨이 저대비로 흐려졌다. 토큰을 쓰지 않고 색을 박은 자리가 반전을 따라가지
* 못해서다(`feature/mindrecord/presentation` main 에만 29곳).
*
* 시안의 다크 팔레트가 확정되고 하드코딩 색이 토큰으로 옮겨지면 기본값을
* [isSystemInDarkTheme] 으로 되돌린다. [darkColors] 와 이 파라미터는 그때를 위해 남겨
* 둔다 — 값을 넘기면 지금도 다크로 그릴 수 있다.
*
* **되돌릴 때 `app` 의 `enableLightEdgeToEdge()` 도 같이 되돌린다.** 이 잠금만 풀면
* 시스템바 아이콘이 라이트로 고정된 채 배경만 다크가 되어 반대 방향으로 안 보인다.
*/
isDarkTheme: Boolean = false,
content: @Composable () -> Unit,
) {
val currentColor = if (isDarkTheme) darkColors else colors
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.afternote.core.ui.theme

import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.v2.createComposeRule
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

/**
* 시스템 다크모드를 따라가지 않는다는 계약 (#1719).
*
* 종전 기본값은 `isSystemInDarkTheme()` 이었는데, 다크 팔레트를 물려도 **화면이 따라오지
* 않았다** — 색을 토큰이 아니라 코드에 박은 자리가 반전되지 않아, 「마음의 기록」 헤더가
* 검정 배경에 검정 글자로 남고 주간 요약 카드는 배경만 밝은 채로 라벨이 흐려졌다.
* 「배선은 됐고 화면은 안 맞은」 중간 상태가 라이트 고정보다도, 완전한 다크 지원보다도
* 나빴다. 그래서 팔레트가 확정될 때까지 라이트로 잠근다.
*
* 이 테스트가 지키는 것은 **잠금이 조용히 풀리지 않는 것**이다. 기본값을
* `isSystemInDarkTheme()` 으로 되돌리면 첫 번째 테스트가 night 한정자에서 실패한다.
* 되돌릴 때는 하드코딩 색 29곳을 토큰으로 옮긴 뒤 이 테스트도 같이 고쳐야 한다.
*/
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [35])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] 클래스 @Config(sdk=[35]) 와 메서드 @Config(sdk=[35], qualifiers=…) 의 sdk 가 중복입니다 — 메서드엔 qualifiers 만 둬도 병합됩니다. 무해합니다.

class ThemeDarkModeLockTest {
@get:Rule
val composeRule = createComposeRule()

private fun resolveIsLightMode(theme: @Composable (@Composable () -> Unit) -> Unit): Boolean {
var isLightMode: Boolean? = null
composeRule.setContent {
theme { isLightMode = AfternoteDesign.colors.isLightMode }
}
composeRule.waitForIdle()
return requireNotNull(isLightMode) { "테마가 content 를 부르지 않았다" }
}

@Test
@Config(qualifiers = "night")
fun `시스템이 다크여도 기본값은 라이트 팔레트다`() {
assertEquals(true, resolveIsLightMode { content -> AfternoteTheme(content = content) })
}

@Test
@Config(qualifiers = "notnight")
fun `시스템이 라이트일 때도 당연히 라이트 팔레트다`() {
assertEquals(true, resolveIsLightMode { content -> AfternoteTheme(content = content) })
}

/**
* 잠금은 **기본값** 에만 걸린다. 다크 팔레트와 파라미터는 팔레트 확정 뒤를 위해 살아 있어야
* 하고, 그때 이 값을 다시 시스템에 묶는 것으로 되돌린다.
*/
@Test
@Config(qualifiers = "notnight")
fun `명시로 넘기면 다크 팔레트를 여전히 그릴 수 있다`() {
assertEquals(
false,
resolveIsLightMode { content -> AfternoteTheme(isDarkTheme = true, content = content) },
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.afternote.konsist

import com.lemonappdev.konsist.api.verify.assertFalse
import org.junit.Test

/**
* 시스템바가 라이트로 잠긴 채 남는지 (#1719).
*
* `AfternoteTheme` 이 라이트로 잠긴 동안 Compose 는 다크 기기에서도 `gray1`(#FAFAFA) 을
* 상태바 밑까지 그린다. 무인자 `enableEdgeToEdge()` 는 `SystemBarStyle.auto` 라 그 기기에서
* 흰 아이콘을 고르므로, 밝은 배경 위에 흰 시계·배터리가 얹혀 사라진다.
*
* 그래서 `app` 의 호출부는 [com.afternote.afternote_fe.enableLightEdgeToEdge] 하나로 모았다.
* 이 가드는 **무인자 호출이 다시 스며드는 것**을 막는다 — 새 Activity 를 만들 때 손이 먼저
* 기억하는 형태가 그쪽이라서다.
*
* 다크 팔레트가 확정돼 잠금을 풀 때는 이 가드도 함께 지운다. 되돌릴 자리는 셋이다 —
* `AfternoteTheme` 의 `isDarkTheme` 기본값 · `enableLightEdgeToEdge()` · `themes.xml` 의
* `windowLightStatusBar`.
*/
class LightSystemBarLockKonsistTest {
@Test
fun `app 은 무인자 enableEdgeToEdge 를 쓰지 않는다`() {
AfternoteKonsistScope
.files
.filter { file -> file.path.contains("/app/src/") }
.filterNot { file -> file.path.endsWith("/$HELPER_FILE") }
.assertFalse { file -> BARE_CALL.containsMatchIn(file.text) }
}

private companion object {
/** 잠금을 실제로 거는 자리. 여기만 `enableEdgeToEdge` 를 인자와 함께 부른다. */
const val HELPER_FILE = "LightEdgeToEdge.kt"

/** `enableEdgeToEdge()` — 인자 없는 호출. 공백만 허용한다. */
val BARE_CALL = Regex("""\benableEdgeToEdge\s*\(\s*\)""")
}
}
Loading