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
6 changes: 3 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ version: 2
multi-ecosystem-groups:
all-dependencies:
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
# Every two months, on the 1st. Security updates are not bound to this schedule.
interval: "cron"
cronjob: "0 9 1 */2 *"
timezone: "Asia/Seoul"
open-pull-requests-limit: 5
labels:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
"${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "cmake;3.22.1"

- name: Initialize CodeQL
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4
with:
languages: c-cpp
build-mode: manual
Expand All @@ -62,7 +62,7 @@ jobs:
--stacktrace

- name: Analyze C/C++
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4
with:
category: "/language:c-cpp"

Expand Down Expand Up @@ -92,7 +92,7 @@ jobs:
run: chmod +x gradlew

- name: Initialize CodeQL
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4
with:
languages: java-kotlin
build-mode: manual
Expand All @@ -116,6 +116,6 @@ jobs:
--stacktrace

- name: Analyze Kotlin
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4
with:
category: "/language:java-kotlin"
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@

All notable changes to mobileGF2logger are documented here.

## 2.6.0 - 2026-10-19

### 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.
- Record members automatically from standby mode: five minutes before a saved
server's game day ends, standby starts capture, opens the game, taps Start,
waits for the roster to be saved, closes the game, and locks the phone once
every chosen server is done. With Platoons on several servers, standby asks
which to record each time, one server per client, and runs them one after
another. Switching to another app or Home while the game is open stands
down without closing anything or locking. Needs the new GF2logger
accessibility service; a summary of each night's results appears on Home
until it is closed.

## 2.5.0 - 2026-10-16

### Added
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ never stored.
- Keeps a newly detected Platoon's packets in bounded memory until the user confirms one of the verified client's compatible servers; unconfirmed data is discarded on force-stop or process death.
- Provides profile management for correcting server metadata without moving data and for deleting one isolated Platoon behind two confirmations and an exact-name check.
- Guides first-time users through Main, Settings, Platoon management, weekly controls, and parsed-packet pages, with a persistent English/Korean selector and Skip action; the guide can be replayed from Settings.
- Offers an overnight standby mode from Home's moon button: an all-black,
minimum-brightness screen that keeps the phone awake. With the optional
GF2logger accessibility service turned on, standby records members by itself
five minutes before each chosen server's daily reset: it starts capture, opens
the game, taps Start on the title screen, waits for the roster, closes the game
from Recents, and locks the phone after the last server. The service only taps
while the game is in front and reads nothing on screen except the foreground
app and the app names on Recents cards.
- Draws edge to edge and keeps drafts, selections, and pending exports across rotation, theme or language changes, and process recreation.
- Supports English and Korean, System/Light/Dark themes, and the six known
Darkwinter/HaoPlay server-region reset presets converted to the phone timezone.
Expand Down
1 change: 1 addition & 0 deletions README_KR.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ mobileGF2logger는 서클장을 위한 가벼운 비루팅 앱입니다. Android
- 새 서클은 확인된 클라이언트에 맞는 서버를 사용자가 선택할 때까지 패킷을 제한된 메모리에만 보관하며, 강제 종료 또는 프로세스 종료 시 확인되지 않은 데이터를 버립니다.
- 서버 정보는 데이터를 이동하지 않고 수정할 수 있으며, 서클 삭제는 두 번의 확인과 정확한 서클 이름 입력을 거쳐 해당 격리 데이터만 제거합니다.
- 첫 사용 시 메인, 설정, 서클 관리, 주간 기능, 파싱 패킷 화면을 안내하며, 한국어/English 전환과 건너뛰기를 지원합니다. 안내는 설정에서 다시 볼 수 있습니다.
- 홈의 달 버튼으로 밤샘 대기 모드를 켤 수 있습니다. 화면을 완전히 검게, 밝기를 최소로 두고 휴대전화를 깨어 있게 유지합니다. 선택 사항인 GF2logger 접근성 서비스를 켜면, 선택한 서버마다 일일 초기화 5분 전에 캡처를 시작하고 게임을 열어 타이틀 화면의 시작을 누른 뒤 멤버 명단을 기다렸다가, 최근 앱 화면에서 게임을 닫고 마지막 서버가 끝나면 휴대전화를 잠급니다. 이 서비스는 게임이 앞에 있을 때만 누르며, 앞에 있는 앱과 최근 앱 카드의 앱 이름 외에는 화면 내용을 읽지 않습니다.
- 화면을 가장자리까지 그리며, 회전·테마·언어 변경이나 프로세스 재생성 후에도 작성 중인 내용과 선택, 대기 중인 내보내기를 유지합니다.
- 영어와 한국어, 시스템/라이트/다크 테마를 지원합니다. 알려진 Darkwinter/HaoPlay 6개 서버 지역의 초기화 시각을 기기 시간대로 환산하며, 감지된 서클을 선택하면 해당 프로필에 저장된 지역을 자동으로 따릅니다.
- HaoPlay(`com.haoplay.game.and.exilium`)와 Darkwinter(`com.Sunborn.SnqxExilium.Glo`) Android 클라이언트를 별도의 VPN 대상으로 등록합니다.
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ android {
applicationId = 'dev.gf2log'
minSdk = 26
targetSdk = 36
versionCode = 20500
versionName = '2.5.0'
versionCode = 20600
versionName = '2.6.0'
testInstrumentationRunner = 'androidx.test.runner.AndroidJUnitRunner'

ndk {
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!-- Asked once, the first time standby mode is entered, so overnight standby is not cut short. -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- Backup for closing the game after a standby recording; it is closed from Recents first,
since some phones and Android 14+ refuse this for other apps. -->
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />

<application
android:name=".MobileGF2LoggerApplication"
Expand Down Expand Up @@ -83,11 +88,32 @@
android:exported="false"
android:screenOrientation="portrait" />

<activity
android:name=".StandbyActivity"
android:configChanges="uiMode|orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden|density"
android:excludeFromRecents="true"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@style/GF2StandbyTheme" />

<activity
android:name=".MemberDetailActivity"
android:exported="false"
android:screenOrientation="portrait" />

<service
android:name=".standby.StandbyAutomationService"
android:exported="false"
android:label="@string/app_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/standby_automation_service" />
</service>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
Expand Down
13 changes: 10 additions & 3 deletions app/src/main/java/dev/gf2log/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class MainActivity : BaseActivity() {
private val work = BackgroundWork(this, "GF2Home")
private lateinit var csvImport: CsvImportFlow
private lateinit var packetHistory: HomePacketHistory
private lateinit var standbyFlow: StandbyFlow

private lateinit var capturePanel: HomeCapturePanel
private lateinit var undoRow: View
Expand Down Expand Up @@ -107,6 +108,7 @@ class MainActivity : BaseActivity() {
packetHistory.restoreState(savedInstanceState)
csvImport = CsvImportFlow(this, work, { profileBinding.scope }, ::showProgress)
csvImport.restoreState(savedInstanceState)
standbyFlow = StandbyFlow(this, work)
captureOnceRequested = savedInstanceState?.getBoolean(STATE_CAPTURE_ONCE) == true
screen = Screen(this, PrimaryNavigation.Destination.HOME)
screen.topBar
Expand All @@ -116,9 +118,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)) { standbyFlow.start() },
BarAction(R.drawable.ic_settings, getString(R.string.open_options)) {
startActivity(Intent(this, OptionsActivity::class.java))
},
))
screen.setBody(Screen.scrollingBody(this, buildContent()))
}

Expand All @@ -142,6 +147,7 @@ class MainActivity : BaseActivity() {
loadRosterSummary()
loadUndoAvailability()
csvImport.onScreenReady()
standbyFlow.showUnseenResults()
}

override fun onSaveInstanceState(outState: Bundle) {
Expand Down Expand Up @@ -227,6 +233,7 @@ class MainActivity : BaseActivity() {

// endregion


// region Data tools

private fun showProgress(message: CharSequence?) {
Expand Down
137 changes: 137 additions & 0 deletions app/src/main/java/dev/gf2log/app/StandbyActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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 java.time.ZoneId
import dev.gf2log.app.standby.StandbyAutomation
import dev.gf2log.app.ui.Labels
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. Leaving also cancels any planned
* recording; [StandbyAutomation] replaces this screen with the game itself when a recording runs.
*
* 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)
})
nextRecording()?.let { next ->
addView(text(next, TextRole.SECONDARY) {
gravity = Gravity.CENTER
setTextColor(getColor(R.color.standby_hint))
setPadding(0, dp(12), 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() }

// A last guard independent of the automation: never outlive the planned deadline.
StandbyAutomation.deadline?.let { deadline ->
val delay = deadline.toEpochMilli() + DEADLINE_MARGIN_MS - System.currentTimeMillis()
root.postDelayed({ finishForAutomation() }, delay.coerceAtLeast(0))
}
}

override fun onStart() {
super.onStart()
StandbyAutomation.onStandbyShown(this)
}

override fun onStop() {
super.onStop()
// Screen off, lock, or switching apps ends standby; a configuration change does not.
if (isChangingConfigurations) return
StandbyAutomation.onStandbyStopped(this)
leaving = true
finish()
}

/** Closes standby without cancelling the plan, for the automation that owns it. */
internal fun finishForAutomation() {
if (leaving) return
leaving = true
finish()
}

private fun leave() {
if (leaving) return
leaving = true
StandbyAutomation.cancel()
finish()
}

/** "Next recording: HaoPlay South Korea · 04:55", or null when nothing is planned. */
private fun nextRecording(): String? {
val next = StandbyAutomation.pendingRuns().firstOrNull() ?: return null
val time = Labels.format(Labels.TIME, next.startAt, ZoneId.systemDefault())
return getString(R.string.standby_next_recording, Labels.region(this, next.server.region), time)
}

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
const val DEADLINE_MARGIN_MS = 60_000L
}
}
Loading
Loading