From e4a49ed9603e14890bd4fea949c07c2770b27b81 Mon Sep 17 00:00:00 2001 From: Alex Han Date: Mon, 19 Oct 2026 13:34:44 +0900 Subject: [PATCH 1/6] feat: add overnight standby mode Home's moon button opens an all-black, minimum-brightness screen that keeps the phone awake and unlocked. It explains itself for three seconds on every entry, then turns every pixel off; tapping, Back, the power button, or leaving the app ends it, and the window-level brightness override lifts on its own. The first entry offers the battery-optimization exemption once. --- CHANGELOG.md | 12 +++ app/src/main/AndroidManifest.xml | 10 ++ .../main/java/dev/gf2log/app/MainActivity.kt | 41 +++++++- .../java/dev/gf2log/app/StandbyActivity.kt | 96 +++++++++++++++++++ .../app/standby/StandbyBatteryPrompt.kt | 38 ++++++++ app/src/main/res/drawable/ic_bedtime.xml | 9 ++ app/src/main/res/values-ko/strings.xml | 7 ++ app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/strings.xml | 7 ++ app/src/main/res/values/styles.xml | 7 ++ 10 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/dev/gf2log/app/StandbyActivity.kt create mode 100644 app/src/main/java/dev/gf2log/app/standby/StandbyBatteryPrompt.kt create mode 100644 app/src/main/res/drawable/ic_bedtime.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 672bfe8..7a6598c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to mobileGF2logger are documented here. +## Unreleased (2.6.0) + +### Added + +- Add standby mode from Home's moon button: an all-black, minimum-brightness + screen that keeps the phone awake and unlocked overnight. It shows what it is + for three seconds on every entry, then turns every pixel off; tapping the + screen, Back, the power button, or switching apps leaves it and the previous + brightness returns. +- Offer the battery-optimization exemption once, the first time standby mode is + entered. + ## 2.5.0 - 2026-10-16 ### Added diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fc322b7..32aabfe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,8 @@ + + + + Unit = { capturePanel.render(it) } + /** Standby opens once the system battery-optimization dialog closes, whatever was chosen. */ + private val batteryExemption = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + startActivity(Intent(this, StandbyActivity::class.java)) + } + private val vpnConsent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> startPending = false if (result.resultCode == RESULT_OK) startCaptureService() else CaptureStatus.markStopped() @@ -116,9 +123,12 @@ class MainActivity : BaseActivity() { layoutParams = FrameLayout.LayoutParams(dp(36), dp(36)) }) .setTitle(getString(R.string.app_name)) - .setActions(listOf(BarAction(R.drawable.ic_settings, getString(R.string.open_options)) { - startActivity(Intent(this, OptionsActivity::class.java)) - })) + .setActions(listOf( + BarAction(R.drawable.ic_bedtime, getString(R.string.open_standby_mode)) { openStandby() }, + BarAction(R.drawable.ic_settings, getString(R.string.open_options)) { + startActivity(Intent(this, OptionsActivity::class.java)) + }, + )) screen.setBody(Screen.scrollingBody(this, buildContent())) } @@ -227,6 +237,31 @@ class MainActivity : BaseActivity() { // endregion + // region Standby + + /** The first entry explains and offers the battery-optimization exemption; later ones go straight in. */ + private fun openStandby() { + if (!StandbyBatteryPrompt.shouldAsk(this)) { + startActivity(Intent(this, StandbyActivity::class.java)) + return + } + Dialogs.show( + Dialogs.builder(this) + .setTitle(R.string.standby_battery_title) + .setMessage(R.string.standby_battery_message) + .setNegativeButton(R.string.standby_battery_later) { _, _ -> + StandbyBatteryPrompt.markAsked(this) + startActivity(Intent(this, StandbyActivity::class.java)) + } + .setPositiveButton(R.string.standby_battery_allow) { _, _ -> + StandbyBatteryPrompt.markAsked(this) + batteryExemption.launch(StandbyBatteryPrompt.requestIntent(this)) + }, + ) + } + + // endregion + // region Data tools private fun showProgress(message: CharSequence?) { diff --git a/app/src/main/java/dev/gf2log/app/StandbyActivity.kt b/app/src/main/java/dev/gf2log/app/StandbyActivity.kt new file mode 100644 index 0000000..9a820ec --- /dev/null +++ b/app/src/main/java/dev/gf2log/app/StandbyActivity.kt @@ -0,0 +1,96 @@ +package dev.gf2log.app + +import android.graphics.Color +import android.os.Build +import android.os.Bundle +import android.view.Gravity +import android.view.View +import android.view.WindowManager +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.activity.addCallback +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import dev.gf2log.app.ui.TextRole +import dev.gf2log.app.ui.dp +import dev.gf2log.app.ui.text + +/** + * Standby: an all-black, minimum-brightness screen that keeps the phone awake and unlocked. + * Touching the screen or pressing Back leaves it; so does the screen turning off or the user + * switching away, so a locked phone is never left in standby. + * + * Brightness is a window-level override, so the system brightness and auto-brightness are never + * touched and come back on their own when this window goes away. + */ +class StandbyActivity : BaseActivity() { + + private var leaving = false + + override fun supportsPendingPlatoonAdmission(): Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + window.attributes = window.attributes.apply { + screenBrightness = MIN_BRIGHTNESS + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // Draw black under the camera cutout too, so no lit strip remains. + layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } + } + + // Shown on every entry, so someone who skipped the first-time explanation still learns + // what this black screen is and how to leave it. + val hint = LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + addView(text(getString(R.string.standby_running), TextRole.TITLE) { + gravity = Gravity.CENTER + setTextColor(getColor(R.color.standby_hint)) + }) + addView(text(getString(R.string.standby_touch_to_leave), TextRole.SECONDARY) { + gravity = Gravity.CENTER + setTextColor(getColor(R.color.standby_hint)) + setPadding(0, dp(6), 0, 0) + }) + } + val root = FrameLayout(this).apply { + setBackgroundColor(Color.BLACK) + contentDescription = getString(R.string.standby_touch_to_leave) + setOnClickListener { leave() } + addView(hint, FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.CENTER, + ).apply { marginStart = dp(32); marginEnd = dp(32) }) + } + setContentView(root) + WindowCompat.getInsetsController(window, root).apply { + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + hide(WindowInsetsCompat.Type.systemBars()) + } + // The hint is the only lit content; removing it leaves every pixel off. + hint.postDelayed({ hint.visibility = View.INVISIBLE }, HINT_VISIBLE_MS) + + onBackPressedDispatcher.addCallback(this) { leave() } + } + + override fun onStop() { + super.onStop() + // Screen off, lock, or switching apps ends standby; a configuration change does not. + if (!isChangingConfigurations) leave() + } + + private fun leave() { + if (leaving) return + leaving = true + finish() + } + + private companion object { + /** The lowest backlight level; it is not "off", which would let the screen time out. */ + const val MIN_BRIGHTNESS = 0f + const val HINT_VISIBLE_MS = 3_000L + } +} diff --git a/app/src/main/java/dev/gf2log/app/standby/StandbyBatteryPrompt.kt b/app/src/main/java/dev/gf2log/app/standby/StandbyBatteryPrompt.kt new file mode 100644 index 0000000..81258b7 --- /dev/null +++ b/app/src/main/java/dev/gf2log/app/standby/StandbyBatteryPrompt.kt @@ -0,0 +1,38 @@ +package dev.gf2log.app.standby + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.os.PowerManager +import android.provider.Settings +import androidx.core.content.edit +import androidx.core.net.toUri + +/** + * The one-time offer, made the first time standby mode is entered, to exclude the app from battery + * optimization so the system does not stop it during an overnight standby. It is asked only once; + * after that the user changes it in the app's battery settings. + */ +internal object StandbyBatteryPrompt { + + fun shouldAsk(context: Context): Boolean = + !isExempt(context) && !prefs(context).getBoolean(KEY_ASKED, false) + + fun markAsked(context: Context) { + prefs(context).edit { putBoolean(KEY_ASKED, true) } + } + + fun isExempt(context: Context): Boolean = + context.getSystemService(PowerManager::class.java)?.isIgnoringBatteryOptimizations(context.packageName) == true + + /** The system's own allow/deny dialog for this app; distributed outside Play, so the policy limit does not apply. */ + @SuppressLint("BatteryLife") + fun requestIntent(context: Context): Intent = + Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, "package:${context.packageName}".toUri()) + + private fun prefs(context: Context) = + context.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + + private const val PREFS = "standby_mode" + private const val KEY_ASKED = "battery_optimization_asked" +} diff --git a/app/src/main/res/drawable/ic_bedtime.xml b/app/src/main/res/drawable/ic_bedtime.xml new file mode 100644 index 0000000..758a4b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_bedtime.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 86ba3da..e516a84 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -598,4 +598,11 @@ 캡처 중입니다 캡처 중지 후 계속 출석이 아니요이면 과업을 예로 둘 수 없습니다. + 대기 모드 켜기 + 대기 모드 실행 중 + 화면을 누르면 대기 모드가 해제됩니다. + 밤새 대기 모드 유지 허용 + Android는 배터리를 아끼기 위해 백그라운드의 GF2logger를 중지할 수 있습니다. 배터리 최적화 예외로 설정하면 대기 모드가 밤새 유지됩니다. 이 설정은 나중에 앱의 배터리 설정에서 바꿀 수 있습니다. + 허용 + 나중에 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f747f9a..6abfaa8 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -32,4 +32,6 @@ #D98A8A #C77777 #321A1B + + #6B6B6B diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d052e27..9261dd0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -606,4 +606,11 @@ Capture is running Stop capture Patrol can\'t be yes while login is no. + Turn on standby mode + Standby mode is on + Tap the screen to leave standby mode. + Allow standby mode to run overnight + Android may stop GF2logger in the background to save battery. Exclude it from battery optimization to keep standby mode running overnight. You can change this later in the app\'s battery settings. + Allow + Not now diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 135cf21..3209d36 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -18,6 +18,13 @@ @style/GF2PickerTheme + + +