Add Android control app and embedded server

Inspired by HNKServer/honoka-chan-apk-server, but implemented as a JNI-based embedded runtime instead of the original approach.

- Add an Android Compose controller app that starts and stops honoka-chan through JNI, shows runtime and health status, manages data directory mounting, and supports backup import/export for data.db
- Add an embeddable Go server entrypoint plus JNI-exported status, health, and reload hooks so desktop and Android builds share the same startup and shutdown path
- Add Android build scripts, runtime packaging, and project documentation, including generated default config.json content for honoka_runtime.zip
- Add runtime bundle hash tracking so updated honoka_runtime.zip assets are automatically redeployed while preserving config.json and user data files
- Add in-app service settings for unlock_all_special_rotation with immediate config persistence and automatic reload when the service is running
- Split SQLite driver selection by platform, using go-sqlite3 on Android and modernc.org/sqlite elsewhere to avoid Android x86_64 seccomp crashes
- Update startup, database initialization, and system health/reload handlers to support the embedded runtime and Android control flow

Signed-off-by: Sean Du <do4suki@gmail.com>
This commit is contained in:
2026-06-12 19:17:21 +08:00
parent 5c2d68f979
commit 91bbc7f607
64 changed files with 3230 additions and 286 deletions
+239 -201
View File
@@ -1,6 +1,7 @@
package startup
import (
"fmt"
"honoka-chan/internal/constant"
ghomemodel "honoka-chan/internal/model/ghome"
loginmodel "honoka-chan/internal/model/login"
@@ -18,31 +19,43 @@ var (
userEng *xorm.Session
)
func CreateTables() {
// db.UserEng.ShowSQL(true)
db.UserEng.Sync2(new(ghomemodel.DeviceKey))
db.UserEng.Sync2(new(loginmodel.AuthKey))
db.UserEng.Sync2(new(usermodel.UserAccessoryWear))
db.UserEng.Sync2(new(usermodel.UserDeck))
db.UserEng.Sync2(new(usermodel.UserDeckUnit))
db.UserEng.Sync2(new(usermodel.UserKey))
db.UserEng.Sync2(new(usermodel.UserLiveGoal))
db.UserEng.Sync2(new(usermodel.UserLiveStatus))
db.UserEng.Sync2(new(usermodel.UserLiveInProgress))
db.UserEng.Sync2(new(usermodel.UserLiveRandom))
db.UserEng.Sync2(new(usermodel.UserLiveRecord))
db.UserEng.Sync2(new(usermodel.UserFriend))
db.UserEng.Sync2(new(usermodel.UserGreet))
db.UserEng.Sync2(new(usermodel.UserPref))
db.UserEng.Sync2(new(usermodel.Users))
db.UserEng.Sync2(new(usermodel.UserUnit))
db.UserEng.Sync2(new(usermodel.UserUnitSkillEquip))
func CreateTables() error {
models := []any{
new(ghomemodel.DeviceKey),
new(loginmodel.AuthKey),
new(usermodel.UserAccessoryWear),
new(usermodel.UserDeck),
new(usermodel.UserDeckUnit),
new(usermodel.UserKey),
new(usermodel.UserLiveGoal),
new(usermodel.UserLiveStatus),
new(usermodel.UserLiveInProgress),
new(usermodel.UserLiveRandom),
new(usermodel.UserLiveRecord),
new(usermodel.UserFriend),
new(usermodel.UserGreet),
new(usermodel.UserPref),
new(usermodel.Users),
new(usermodel.UserUnit),
new(usermodel.UserUnitSkillEquip),
}
MigrateUserPref()
MigrateUserLiveData()
for _, model := range models {
if err := db.UserEng.Sync2(model); err != nil {
return fmt.Errorf("同步表失败: %w", err)
}
}
if err := MigrateUserPref(); err != nil {
return err
}
if err := MigrateUserLiveData(); err != nil {
return err
}
return nil
}
func MigrateUserPref() {
func MigrateUserPref() error {
session := db.UserEng.NewSession()
defer session.Close()
@@ -52,7 +65,7 @@ func MigrateUserPref() {
Or("profile_version IS NULL").
Find(&prefList)
if err != nil {
log.Fatalln("迁移 user_pref 失败:", err.Error())
return fmt.Errorf("迁移 user_pref 失败: %w", err)
}
for _, pref := range prefList {
@@ -62,9 +75,10 @@ func MigrateUserPref() {
Cols(usermodel.UserPrefProfileColumns()...).
Update(&pref)
if err != nil {
log.Fatalln("迁移 user_pref 失败:", err.Error())
return fmt.Errorf("迁移 user_pref 失败: %w", err)
}
}
return nil
}
type liveGoalRewardMigrationRow struct {
@@ -105,18 +119,18 @@ func liveRankForMigration(value int, cRank int, bRank int, aRank int, sRank int)
}
}
func MigrateUserLiveData() {
func MigrateUserLiveData() error {
session := db.UserEng.NewSession()
defer session.Close()
recordRows := []usermodel.UserLiveRecord{}
if err := session.Table(new(usermodel.UserLiveRecord)).Find(&recordRows); err != nil {
log.Fatalln("迁移 user_live_status 失败:", err.Error())
return fmt.Errorf("迁移 user_live_status 失败: %w", err)
}
existingStatusRows := []usermodel.UserLiveStatus{}
if err := session.Table(new(usermodel.UserLiveStatus)).Find(&existingStatusRows); err != nil {
log.Fatalln("迁移 user_live_status 失败:", err.Error())
return fmt.Errorf("迁移 user_live_status 失败: %w", err)
}
type liveStatusKey struct {
@@ -144,7 +158,7 @@ func MigrateUserLiveData() {
UpdateDate: now,
}
if _, err := session.Insert(&status); err != nil {
log.Fatalln("迁移 user_live_status 失败:", err.Error())
return fmt.Errorf("迁移 user_live_status 失败: %w", err)
}
statusMap[key] = status
continue
@@ -169,14 +183,14 @@ func MigrateUserLiveData() {
Where("user_id = ? AND live_difficulty_id = ?", status.UserID, status.LiveDifficultyID).
Cols("hi_score", "hi_combo_count", "clear_cnt", "update_date").
Update(&status); err != nil {
log.Fatalln("迁移 user_live_status 失败:", err.Error())
return fmt.Errorf("迁移 user_live_status 失败: %w", err)
}
statusMap[key] = status
}
}
liveGoalInfoMap := map[int]liveGoalInfoMigrationRow{}
loadLiveGoalInfoRows := func(table string) {
loadLiveGoalInfoRows := func(table string) error {
rows := []liveGoalInfoMigrationRow{}
err := db.MainEng.Table(table).Alias("live").
Join("LEFT", "live_setting_m setting", "live.live_setting_id = setting.live_setting_id").
@@ -197,20 +211,25 @@ func MigrateUserLiveData() {
`).
Find(&rows)
if err != nil {
log.Fatalln("迁移 user_live_goal 失败:", err.Error())
return fmt.Errorf("迁移 user_live_goal 失败: %w", err)
}
for _, row := range rows {
liveGoalInfoMap[row.LiveDifficultyID] = row
}
return nil
}
if err := loadLiveGoalInfoRows("normal_live_m"); err != nil {
return err
}
if err := loadLiveGoalInfoRows("special_live_m"); err != nil {
return err
}
loadLiveGoalInfoRows("normal_live_m")
loadLiveGoalInfoRows("special_live_m")
goalRewardRows := []liveGoalRewardMigrationRow{}
if err := db.MainEng.Table("live_goal_reward_m").
OrderBy("live_difficulty_id ASC, live_goal_type ASC, rank ASC, live_goal_reward_id ASC").
Find(&goalRewardRows); err != nil {
log.Fatalln("迁移 user_live_goal 失败:", err.Error())
return fmt.Errorf("迁移 user_live_goal 失败: %w", err)
}
goalRewardMap := map[int][]liveGoalRewardMigrationRow{}
@@ -222,7 +241,7 @@ func MigrateUserLiveData() {
if err := session.Table(new(usermodel.UserLiveGoal)).
Where("live_goal_reward_id > 0").
Find(&existingGoalRows); err != nil {
log.Fatalln("迁移 user_live_goal 失败:", err.Error())
return fmt.Errorf("迁移 user_live_goal 失败: %w", err)
}
existingGoalMap := make(map[liveStatusKey]map[int]struct{})
@@ -284,7 +303,7 @@ func MigrateUserLiveData() {
Rank: reward.Rank,
CompletedAt: now,
}); err != nil {
log.Fatalln("迁移 user_live_goal 失败:", err.Error())
return fmt.Errorf("迁移 user_live_goal 失败: %w", err)
}
if existingGoalMap[key] == nil {
existingGoalMap[key] = map[int]struct{}{}
@@ -292,13 +311,29 @@ func MigrateUserLiveData() {
existingGoalMap[key][reward.LiveGoalRewardID] = struct{}{}
}
}
return nil
}
func LoadUnitData() {
func LoadUnitData() (err error) {
userEng = db.UserEng.NewSession()
defer userEng.Close()
defer func() {
if recovered := recover(); recovered != nil {
if userEng != nil {
_ = userEng.Rollback()
}
switch v := recovered.(type) {
case error:
err = fmt.Errorf("同步失败: %w", v)
default:
err = fmt.Errorf("同步失败: %v", v)
}
}
if userEng != nil {
userEng.Close()
}
}()
err := userEng.Begin()
err = userEng.Begin()
CheckErr(err)
commonUnitExist, err := userEng.IsTableExist(new(unitmodel.CommonUnitData))
@@ -306,183 +341,186 @@ func LoadUnitData() {
userUnitExist, err := userEng.IsTableExist(new(usermodel.UserUnitData))
CheckErr(err)
if !commonUnitExist || !userUnitExist {
log.Println("卡片数据不存在,正在同步...")
if commonUnitExist && userUnitExist {
_ = userEng.Rollback()
return nil
}
userEng.DropTable(new(unitmodel.CommonUnitData))
userEng.CreateTable(new(unitmodel.CommonUnitData))
userEng.DropTable(new(usermodel.UserUnitData))
userEng.CreateTable(new(usermodel.UserUnitData))
log.Println("卡片数据不存在,正在同步...")
var unitData []unitmodel.UnitM
err = db.MainEng.Table(new(unitmodel.UnitM)).OrderBy("unit_id ASC").Find(&unitData)
CheckErr(userEng.DropTable(new(unitmodel.CommonUnitData)))
CheckErr(userEng.CreateTable(new(unitmodel.CommonUnitData)))
CheckErr(userEng.DropTable(new(usermodel.UserUnitData)))
CheckErr(userEng.CreateTable(new(usermodel.UserUnitData)))
var unitData []unitmodel.UnitM
err = db.MainEng.Table(new(unitmodel.UnitM)).OrderBy("unit_id ASC").Find(&unitData)
CheckErr(err)
checked := false
for _, u := range unitData {
// 判断卡片最大等级
var unitMaxLevel, nextExp, sumExp int
_, err = db.MainEng.Table("unit_level_up_pattern_m").
Where("unit_level_up_pattern_id = ?", u.UnitLevelUpPatternId).
Select("MAX(unit_level),next_exp").Get(&unitMaxLevel, &nextExp)
CheckErr(err)
checked := false
for _, u := range unitData {
// 判断卡片最大等级
var unitMaxLevel, nextExp, sumExp int
_, err = db.MainEng.Table("unit_level_up_pattern_m").
Where("unit_level_up_pattern_id = ?", u.UnitLevelUpPatternId).
Select("MAX(unit_level),next_exp").Get(&unitMaxLevel, &nextExp)
// 计算突破前的经验总和
_, err = db.MainEng.Table("unit_level_up_pattern_m").
Where("unit_level_up_pattern_id = ?", u.UnitLevelUpPatternId).
Where("unit_level = ?", unitMaxLevel-1).Cols("next_exp").Get(&sumExp)
CheckErr(err)
// 计算突破前的属性
var smileMax, pureMax, coolMax int
smileMax = u.SmileMax
pureMax = u.PureMax
coolMax = u.CoolMax
// 如果 nexpExp 不为零,则说明卡片等级没有达到上限
if nextExp != 0 {
// 计算突破后的经验总和
_, err = db.MainEng.Table("unit_level_limit_pattern_m").
Where("unit_level_limit_id = 1 AND unit_level = 349").
Cols("next_exp").Get(&sumExp)
CheckErr(err)
// 计算突破前的经验总和
_, err = db.MainEng.Table("unit_level_up_pattern_m").
Where("unit_level_up_pattern_id = ?", u.UnitLevelUpPatternId).
Where("unit_level = ?", unitMaxLevel-1).Cols("next_exp").Get(&sumExp)
CheckErr(err)
// 突破后最大等级
unitMaxLevel = 350
// 计算突破的属性
var smileMax, pureMax, coolMax int
smileMax = u.SmileMax
pureMax = u.PureMax
coolMax = u.CoolMax
// 如果 nexpExp 不为零,则说明卡片等级没有达到上限
if nextExp != 0 {
// 计算突破后的经验总和
_, err = db.MainEng.Table("unit_level_limit_pattern_m").
Where("unit_level_limit_id = 1 AND unit_level = 349").
Cols("next_exp").Get(&sumExp)
CheckErr(err)
// 突破后最大等级
unitMaxLevel = 350
// 计算突破后的属性
smileMax += 6000
pureMax += 6000
coolMax += 6000
}
// 计算绊值、技能等级、技能经验
var maxLove, skillLevel, skillExp, removableSkillCapacity, levelLimitID int
switch u.Rarity {
case 1:
maxLove = 50
skillExp = 0
skillLevel = 0
removableSkillCapacity = 0
levelLimitID = 0
case 2:
maxLove = 200
skillExp = 490
skillLevel = 8
removableSkillCapacity = 1
levelLimitID = 0
case 3:
maxLove = 500
skillExp = 4900
skillLevel = 8
removableSkillCapacity = 2
levelLimitID = 0
case 4:
maxLove = 1000
skillExp = 29900
skillLevel = 8
removableSkillCapacity = 8
levelLimitID = 1
case 5:
maxLove = 750
skillExp = 12700
skillLevel = 8
removableSkillCapacity = 3
levelLimitID = 0
}
// 针对技能卡等应援卡片
if smileMax == 1 {
maxLove = 0
skillExp = 0
skillLevel = 0
removableSkillCapacity = 0
}
// 检查是否签名卡
var isSigned bool
exist, err := db.MainEng.Table("unit_sign_asset_m").Where("unit_id = ?", u.UnitId).Exist()
CheckErr(err)
if exist {
isSigned = true
}
// 生成公共卡片
unitCommon := unitmodel.CommonUnitData{
UnitNumber: u.UnitNumber,
UnitID: u.UnitId,
UnitTypeID: u.UnitTypeId,
Name: *u.NameEn,
Eponym: u.EponymEn,
Rarity: u.Rarity,
Attribute: u.AttributeId,
Smile: smileMax,
Cute: pureMax,
Cool: coolMax,
Exp: sumExp,
Level: unitMaxLevel,
MaxLevel: unitMaxLevel,
LevelLimitID: levelLimitID,
Rank: u.RankMin,
MaxRank: u.RankMax,
Love: maxLove,
MaxLove: maxLove,
UnitSkillExp: skillExp,
UnitSkillLevel: skillLevel,
MaxHp: u.HpMax,
UnitRemovableSkillCapacity: removableSkillCapacity,
IsRankMax: true,
IsLoveMax: true,
IsLevelMax: true,
IsSigned: isSigned,
IsSkillLevelMax: true,
IsRemovableSkillCapacityMax: true,
InsertDate: time.Now().Unix(),
}
_, err = userEng.Insert(&unitCommon)
CheckErr(err)
var userID []int
err = db.UserEng.Table(new(usermodel.Users)).Cols("user_id").Find(&userID)
CheckErr(err)
for _, id := range userID {
userUnit := usermodel.UserUnitData{
UnitID: u.UnitId,
FavoriteFlag: false,
DisplayRank: u.RankMax,
UserID: id,
InsertDate: time.Now().Unix(),
}
// 检查表里是否已经有数据
if !checked {
ct, err := userEng.Table(new(usermodel.UserUnitData)).Count()
CheckErr(err)
if ct == 0 {
userUnit.UnitOwningUserID = 38383
}
checked = true
}
_, err = userEng.Insert(&userUnit)
CheckErr(err)
}
// 计算突破的属性
smileMax += 6000
pureMax += 6000
coolMax += 6000
}
err = userEng.Commit()
// 计算绊值、技能等级、技能经验
var maxLove, skillLevel, skillExp, removableSkillCapacity, levelLimitID int
switch u.Rarity {
case 1:
maxLove = 50
skillExp = 0
skillLevel = 0
removableSkillCapacity = 0
levelLimitID = 0
case 2:
maxLove = 200
skillExp = 490
skillLevel = 8
removableSkillCapacity = 1
levelLimitID = 0
case 3:
maxLove = 500
skillExp = 4900
skillLevel = 8
removableSkillCapacity = 2
levelLimitID = 0
case 4:
maxLove = 1000
skillExp = 29900
skillLevel = 8
removableSkillCapacity = 8
levelLimitID = 1
case 5:
maxLove = 750
skillExp = 12700
skillLevel = 8
removableSkillCapacity = 3
levelLimitID = 0
}
// 针对技能卡等应援卡片
if smileMax == 1 {
maxLove = 0
skillExp = 0
skillLevel = 0
removableSkillCapacity = 0
}
// 检查是否签名卡
var isSigned bool
exist, err := db.MainEng.Table("unit_sign_asset_m").Where("unit_id = ?", u.UnitId).Exist()
CheckErr(err)
if exist {
isSigned = true
}
// 生成公共卡片
unitCommon := unitmodel.CommonUnitData{
UnitNumber: u.UnitNumber,
UnitID: u.UnitId,
UnitTypeID: u.UnitTypeId,
Name: *u.NameEn,
Eponym: u.EponymEn,
Rarity: u.Rarity,
Attribute: u.AttributeId,
Smile: smileMax,
Cute: pureMax,
Cool: coolMax,
Exp: sumExp,
Level: unitMaxLevel,
MaxLevel: unitMaxLevel,
LevelLimitID: levelLimitID,
Rank: u.RankMin,
MaxRank: u.RankMax,
Love: maxLove,
MaxLove: maxLove,
UnitSkillExp: skillExp,
UnitSkillLevel: skillLevel,
MaxHp: u.HpMax,
UnitRemovableSkillCapacity: removableSkillCapacity,
IsRankMax: true,
IsLoveMax: true,
IsLevelMax: true,
IsSigned: isSigned,
IsSkillLevelMax: true,
IsRemovableSkillCapacityMax: true,
InsertDate: time.Now().Unix(),
}
_, err = userEng.Insert(&unitCommon)
CheckErr(err)
log.Println("同步完成!")
var userID []int
err = db.UserEng.Table(new(usermodel.Users)).Cols("user_id").Find(&userID)
CheckErr(err)
for _, id := range userID {
userUnit := usermodel.UserUnitData{
UnitID: u.UnitId,
FavoriteFlag: false,
DisplayRank: u.RankMax,
UserID: id,
InsertDate: time.Now().Unix(),
}
// 检查表里是否已经有数据
if !checked {
ct, err := userEng.Table(new(usermodel.UserUnitData)).Count()
CheckErr(err)
if ct == 0 {
userUnit.UnitOwningUserID = 38383
}
checked = true
}
_, err = userEng.Insert(&userUnit)
CheckErr(err)
}
}
err = userEng.Commit()
CheckErr(err)
log.Println("同步完成!")
return nil
}
func CheckErr(err error) {
if err != nil {
userEng.Rollback()
log.Fatalln("同步失败:", err.Error())
panic(err)
}
}
+6 -4
View File
@@ -1,6 +1,7 @@
package startup
import (
"fmt"
"honoka-chan/internal/handler/ghome/account"
usermodel "honoka-chan/internal/model/user"
"log"
@@ -10,16 +11,17 @@ const (
defaultPassword = "klsbgames"
)
func CreateDefaultUser() {
func CreateDefaultUser() error {
_, code, msg, created, err := account.AddUser(usermodel.DefaultSystemPhone, defaultPassword, true)
if err != nil {
log.Fatalln("默认用户创建失败:", err.Error())
return fmt.Errorf("默认用户创建失败: %w", err)
}
if code != 0 {
log.Fatalf("默认用户创建失败: code=%d msg=%s", code, msg)
return fmt.Errorf("默认用户创建失败: code=%d msg=%s", code, msg)
}
if created {
log.Printf("默认用户创建成功, 账号: %s, 密码: %s\n", usermodel.DefaultSystemPhone, defaultPassword)
return
return nil
}
return nil
}
+7 -6
View File
@@ -1,17 +1,17 @@
package startup
import (
"fmt"
usermodel "honoka-chan/internal/model/user"
"honoka-chan/pkg/db"
"log"
)
func EnsureDefaultFriends() {
func EnsureDefaultFriends() error {
session := db.UserEng.NewSession()
defer session.Close()
if err := session.Begin(); err != nil {
log.Fatalln("初始化默认好友失败:", err.Error())
return fmt.Errorf("初始化默认好友失败: %w", err)
}
userIDs := []int{}
@@ -20,19 +20,20 @@ func EnsureDefaultFriends() {
Find(&userIDs)
if err != nil {
session.Rollback()
log.Fatalln("初始化默认好友失败:", err.Error())
return fmt.Errorf("初始化默认好友失败: %w", err)
}
for _, userID := range userIDs {
err = usermodel.EnsureDefaultFriendship(session, userID)
if err != nil {
session.Rollback()
log.Fatalln("初始化默认好友失败:", err.Error())
return fmt.Errorf("初始化默认好友失败: %w", err)
}
}
if err := session.Commit(); err != nil {
session.Rollback()
log.Fatalln("初始化默认好友失败:", err.Error())
return fmt.Errorf("初始化默认好友失败: %w", err)
}
return nil
}
+14 -5
View File
@@ -1,8 +1,17 @@
package startup
func StartUp() {
CreateTables()
LoadUnitData()
CreateDefaultUser()
EnsureDefaultFriends()
func StartUp() error {
if err := CreateTables(); err != nil {
return err
}
if err := LoadUnitData(); err != nil {
return err
}
if err := CreateDefaultUser(); err != nil {
return err
}
if err := EnsureDefaultFriends(); err != nil {
return err
}
return nil
}