Skip to content

Commit bf53959

Browse files
author
Fastace
committed
修改RESTIC初始化逻辑,BUG:目前设置页面正常弹窗初始化URL,下载完成之后设置页面依然找不到,待修复
1 parent 491b858 commit bf53959

8 files changed

Lines changed: 285 additions & 66 deletions

File tree

-27.4 MB
Binary file not shown.
-27.8 MB
Binary file not shown.
-27.9 MB
Binary file not shown.
-29.4 MB
Binary file not shown.

source/app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
33
xmlns:tools="http://schemas.android.com/tools">
44

5+
<!-- 网络权限 -->
6+
<uses-permission android:name="android.permission.INTERNET" />
7+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
8+
59
<application
610
android:name=".DataBackupApplication"
711
android:allowBackup="true"

source/core/restic/src/main/kotlin/com/xayah/core/restic/ResticNative.kt

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.xayah.core.restic
22

33
import android.content.Context
44
import android.os.Build
5+
import android.util.Log // 添加标准 Log 导入
56
import java.io.File
67
import javax.inject.Inject
78
import javax.inject.Singleton
@@ -12,29 +13,82 @@ class ResticNative @Inject constructor(
1213
) {
1314
companion object {
1415
private const val TAG = "ResticNative"
16+
private const val DOWNLOAD_PREFS_NAME = "restic_download"
17+
private const val NEED_DOWNLOAD_KEY = "need_download"
1518
}
1619

1720
// 获取对应架构的restic二进制路径
1821
fun getResticBinaryPath(context: Context): String {
22+
// 优先使用私有目录中的二进制文件
23+
val privateBinaryPath = File(context.filesDir, "restic").absolutePath
24+
val privateBinaryFile = File(privateBinaryPath)
25+
26+
if (privateBinaryFile.exists()) {
27+
logger.logBinaryPathFound(privateBinaryPath)
28+
29+
// 确保文件有执行权限
30+
try {
31+
privateBinaryFile.setExecutable(true, false)
32+
logger.logPermissionSetSuccess()
33+
} catch (e: Exception) {
34+
logger.logPermissionSetFailed(e)
35+
}
36+
37+
return privateBinaryPath
38+
}
39+
40+
// 私有目录中不存在,检查lib目录中的内置版本
1941
val abi = Build.SUPPORTED_ABIS[0]
2042
val libPath = context.applicationInfo.nativeLibraryDir
21-
val resticPath = "$libPath/librestic.so"
43+
val libResticPath = "$libPath/librestic.so"
44+
val libResticFile = File(libResticPath)
2245

23-
logger.logBinaryPathFound(resticPath)
46+
if (libResticFile.exists()) {
47+
logger.logBinaryPathFound(libResticPath)
2448

25-
if (!File(resticPath).exists()) {
26-
logger.logBinaryNotFound(resticPath)
27-
throw RuntimeException("Restic binary not found at: $resticPath")
49+
// 将内置版本复制到私有目录
50+
try {
51+
libResticFile.copyTo(privateBinaryFile, overwrite = true)
52+
privateBinaryFile.setExecutable(true, false)
53+
logger.logPermissionSetSuccess()
54+
logger.logBinaryPathFound(privateBinaryPath)
55+
return privateBinaryPath
56+
} catch (e: Exception) {
57+
logger.logPermissionSetFailed(e)
58+
// 复制失败,直接返回lib路径
59+
return libResticPath
60+
}
2861
}
2962

30-
// 确保文件有执行权限
31-
try {
32-
File(resticPath).setExecutable(true, false)
33-
logger.logPermissionSetSuccess()
34-
} catch (e: Exception) {
35-
logger.logPermissionSetFailed(e)
36-
}
63+
// 内置版本也不存在,触发下载流程
64+
logger.logBinaryNotFound(privateBinaryPath)
65+
triggerDownloadFlow(context)
66+
67+
// 返回私有目录路径(下载完成后会使用)
68+
return privateBinaryPath
69+
}
70+
71+
private fun triggerDownloadFlow(context: Context) {
72+
// 通过SharedPreferences标记需要下载
73+
val prefs = context.getSharedPreferences(DOWNLOAD_PREFS_NAME, Context.MODE_PRIVATE)
74+
prefs.edit().putBoolean(NEED_DOWNLOAD_KEY, true).apply()
75+
Log.d(TAG, "Download flow triggered for Restic binary") // 修改为标准 Log
76+
}
77+
78+
fun isDownloadNeeded(context: Context): Boolean {
79+
val prefs = context.getSharedPreferences(DOWNLOAD_PREFS_NAME, Context.MODE_PRIVATE)
80+
return prefs.getBoolean(NEED_DOWNLOAD_KEY, false)
81+
}
82+
83+
fun clearDownloadFlag(context: Context) {
84+
val prefs = context.getSharedPreferences(DOWNLOAD_PREFS_NAME, Context.MODE_PRIVATE)
85+
prefs.edit().remove(NEED_DOWNLOAD_KEY).apply()
86+
Log.d(TAG, "Download flag cleared") // 修改为标准 Log
87+
}
3788

38-
return resticPath
89+
// 检查私有目录中的二进制文件是否存在且可执行
90+
fun isPrivateBinaryValid(context: Context): Boolean {
91+
val binaryFile = File(context.filesDir, "restic")
92+
return binaryFile.exists() && binaryFile.canExecute()
3993
}
4094
}

source/feature/main/settings/src/main/kotlin/com/xayah/feature/main/settings/Index.kt

Lines changed: 126 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,55 +7,61 @@ import androidx.compose.animation.ExperimentalAnimationApi
77
import androidx.compose.foundation.layout.Arrangement
88
import androidx.compose.foundation.layout.Column
99
import androidx.compose.foundation.layout.ExperimentalLayoutApi
10+
import androidx.compose.foundation.layout.Spacer
1011
import androidx.compose.foundation.layout.fillMaxSize
12+
import androidx.compose.foundation.layout.height
13+
import androidx.compose.foundation.layout.padding
14+
import androidx.compose.foundation.layout.fillMaxWidth
1115
import androidx.compose.foundation.rememberScrollState
1216
import androidx.compose.foundation.verticalScroll
1317
import androidx.compose.material.icons.Icons
1418
import androidx.compose.material.icons.outlined.Block
19+
import androidx.compose.material3.AlertDialog
20+
import androidx.compose.material3.Button
1521
import androidx.compose.material3.ExperimentalMaterial3Api
22+
import androidx.compose.material3.LinearProgressIndicator
23+
import androidx.compose.material3.MaterialTheme
24+
import androidx.compose.material3.OutlinedTextField
25+
import androidx.compose.material3.Scaffold
26+
import androidx.compose.material3.Text
27+
import androidx.compose.material3.TextButton
28+
import androidx.compose.material3.TopAppBar
1629
import androidx.compose.material3.TopAppBarDefaults
1730
import androidx.compose.material3.rememberTopAppBarState
1831
import androidx.compose.runtime.Composable
32+
import androidx.compose.runtime.LaunchedEffect
1933
import androidx.compose.runtime.getValue
34+
import androidx.compose.runtime.mutableStateOf
35+
import androidx.compose.runtime.remember
36+
import androidx.compose.runtime.rememberCoroutineScope
37+
import androidx.compose.runtime.setValue
2038
import androidx.compose.ui.Modifier
2139
import androidx.compose.ui.graphics.vector.ImageVector
2240
import androidx.compose.ui.platform.LocalContext
2341
import androidx.compose.ui.res.stringResource
2442
import androidx.compose.ui.res.vectorResource
43+
import androidx.compose.ui.unit.dp
2544
import androidx.hilt.navigation.compose.hiltViewModel
2645
import androidx.lifecycle.compose.collectAsStateWithLifecycle
27-
import androidx.compose.material3.Text
28-
import androidx.compose.foundation.layout.padding
29-
// DataStore Keys
3046
import com.xayah.core.datastore.KeyAutoScreenOff
3147
import com.xayah.core.datastore.KeyMonet
32-
import com.xayah.core.datastore.KeyResticEnableCompression // 引入 Restic 压缩 Key
33-
// UI Components
48+
import com.xayah.core.datastore.KeyResticEnableCompression
3449
import com.xayah.core.ui.component.Clickable
3550
import com.xayah.core.ui.component.InnerBottomSpacer
3651
import com.xayah.core.ui.component.Switchable
3752
import com.xayah.core.ui.component.Title
38-
import androidx.compose.material3.Scaffold
39-
import androidx.compose.material3.TopAppBar // 补充导入
40-
import androidx.compose.runtime.LaunchedEffect
41-
// Navigation and Routing
4253
import com.xayah.core.ui.route.MainRoutes
43-
import com.xayah.core.ui.util.LocalNavController // 补充导入
44-
// Tokens and Utils
4554
import com.xayah.core.ui.token.SizeTokens
55+
import com.xayah.core.ui.util.LocalNavController
4656
import com.xayah.core.util.LanguageUtil
4757
import com.xayah.core.util.getActivity
4858
import com.xayah.core.util.navigateSingle
4959
import com.xayah.core.util.readMappedLanguage
50-
// Activities and ViewModels
51-
import com.xayah.feature.setup.MainActivity as SetupActivity
52-
import com.xayah.feature.main.settings.R
53-
import com.xayah.feature.main.settings.IndexViewModel // 补充导入
5460
import com.xayah.feature.main.settings.restic.ResticViewModel
61+
import com.xayah.feature.setup.MainActivity as SetupActivity
62+
import kotlinx.coroutines.launch
5563

56-
@ExperimentalLayoutApi
57-
@ExperimentalAnimationApi
58-
@ExperimentalMaterial3Api
64+
@OptIn(ExperimentalLayoutApi::class, ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class)
5965
@Composable
6066
fun PageSettings() {
6167
val context = LocalContext.current
@@ -64,21 +70,28 @@ fun PageSettings() {
6470
val resticViewModel = hiltViewModel<ResticViewModel>()
6571
val directoryState by viewModel.directoryState.collectAsStateWithLifecycle()
6672
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
73+
val scope = rememberCoroutineScope() // 用于在 UI 事件中启动协程
6774

68-
// 收集状态
75+
// 状态收集
6976
val resticVersion by resticViewModel.resticVersionState.collectAsStateWithLifecycle()
7077
val resticInitialized by resticViewModel.resticInitializedState.collectAsStateWithLifecycle(initialValue = false)
7178
val snapshotCount by resticViewModel.resticSnapshotCountState.collectAsStateWithLifecycle(initialValue = 0)
7279
val repoPath by resticViewModel.repoPathState.collectAsStateWithLifecycle()
7380
val resticError by resticViewModel.resticErrorState.collectAsStateWithLifecycle()
74-
// 触发 Restic 状态检查
75-
LaunchedEffect(Unit) {
76-
resticViewModel.checkResticStatus()
81+
val downloadState by resticViewModel.downloadState.collectAsStateWithLifecycle()
82+
83+
var showDownloadDialog by remember { mutableStateOf(false) }
84+
85+
// 逻辑:自动弹出下载对话框
86+
LaunchedEffect(resticVersion, downloadState) {
87+
if (resticVersion == null && downloadState is ResticViewModel.DownloadState.Idle) {
88+
showDownloadDialog = true
89+
}
7790
}
7891

79-
// 监听状态变化
80-
LaunchedEffect(resticVersion, resticInitialized, snapshotCount) {
81-
Log.d("Settings", "Restic状态: Version=$resticVersion, Initialized=$resticInitialized, Snapshots=$snapshotCount")
92+
// 逻辑:进入页面检查状态
93+
LaunchedEffect(Unit) {
94+
resticViewModel.checkResticStatus()
8295
}
8396

8497
Scaffold(
@@ -88,15 +101,14 @@ fun PageSettings() {
88101
scrollBehavior = scrollBehavior
89102
)
90103
}
91-
) { paddingValues -> // 使用明确的参数名
104+
) { paddingValues ->
92105
Column(
93106
modifier = Modifier
94107
.verticalScroll(rememberScrollState())
95108
.fillMaxSize()
96-
.padding(paddingValues), // 使用 paddingValues 而不是 it
109+
.padding(paddingValues),
97110
verticalArrangement = Arrangement.spacedBy(SizeTokens.Level24)
98111
) {
99-
100112
// --- 备份和恢复设置 ---
101113
Column {
102114
Clickable(
@@ -115,8 +127,7 @@ fun PageSettings() {
115127
title = stringResource(id = R.string.setup),
116128
value = stringResource(id = R.string.enter_the_setup_page_again),
117129
) {
118-
// 重新进入设置页面(通常用于重新配置应用)
119-
context.getActivity().finish()
130+
context.getActivity()?.finish()
120131
context.startActivity(Intent(context, SetupActivity::class.java))
121132
}
122133
}
@@ -161,19 +172,11 @@ fun PageSettings() {
161172

162173
// --- Restic 配置 ---
163174
Title(title = stringResource(id = R.string.restic_configuration)) {
164-
// 状态显示部分
165175
Clickable(
166176
title = stringResource(id = R.string.restic_version),
167-
value = if (resticVersion != null) {
168-
resticVersion
169-
} else {
170-
stringResource(id = R.string.restic_not_detected)
171-
}
172-
) {
173-
// 版本信息不可点击,仅用于显示
174-
}
177+
value = resticVersion ?: stringResource(id = R.string.restic_not_detected)
178+
) {}
175179

176-
// 始终显示初始化状态
177180
Clickable(
178181
title = stringResource(id = R.string.restic_initialization_status),
179182
value = when {
@@ -187,7 +190,6 @@ fun PageSettings() {
187190
}
188191
}
189192

190-
// 始终显示快照数量
191193
Clickable(
192194
title = stringResource(id = R.string.restic_snapshot_count),
193195
value = when {
@@ -196,11 +198,8 @@ fun PageSettings() {
196198
snapshotCount > 0 -> stringResource(id = R.string.restic_snapshots_count, snapshotCount)
197199
else -> stringResource(id = R.string.restic_no_snapshots)
198200
}
199-
) {
200-
// 快照信息不可点击,仅用于显示
201-
}
201+
) {}
202202

203-
// 配置选项 - 始终显示
204203
Clickable(
205204
title = stringResource(id = R.string.restic_password),
206205
value = stringResource(id = R.string.restic_password_desc),
@@ -224,7 +223,6 @@ fun PageSettings() {
224223
checkedText = stringResource(id = R.string.auto_screen_off_desc),
225224
)
226225

227-
// 添加缓存管理入口
228226
Clickable(
229227
title = stringResource(id = R.string.cache_management),
230228
value = stringResource(id = R.string.cache_management_desc),
@@ -247,4 +245,86 @@ fun PageSettings() {
247245
InnerBottomSpacer(innerPadding = paddingValues)
248246
}
249247
}
248+
249+
if (showDownloadDialog) {
250+
ResticDownloadDialog(
251+
viewModel = resticViewModel,
252+
onDismiss = { showDownloadDialog = false },
253+
onDownloadComplete = {
254+
showDownloadDialog = false
255+
// 修复点:使用 Composable 作用域中的 scope
256+
scope.launch {
257+
resticViewModel.checkResticStatus()
258+
}
259+
}
260+
)
261+
}
262+
}
263+
264+
@OptIn(ExperimentalMaterial3Api::class)
265+
@Composable
266+
fun ResticDownloadDialog(
267+
viewModel: ResticViewModel,
268+
onDismiss: () -> Unit,
269+
onDownloadComplete: () -> Unit
270+
) {
271+
val downloadState by viewModel.downloadState.collectAsStateWithLifecycle()
272+
var urlInput by remember { mutableStateOf("") }
273+
val scope = rememberCoroutineScope()
274+
275+
AlertDialog(
276+
onDismissRequest = onDismiss,
277+
title = { Text("下载Restic二进制文件") },
278+
text = {
279+
Column {
280+
Text("Restic二进制文件不存在,请输入下载URL:")
281+
Spacer(modifier = Modifier.height(8.dp))
282+
OutlinedTextField(
283+
value = urlInput,
284+
onValueChange = { urlInput = it },
285+
label = { Text("下载URL") },
286+
modifier = Modifier.fillMaxWidth()
287+
)
288+
289+
val state = downloadState
290+
when (state) {
291+
is ResticViewModel.DownloadState.Downloading -> {
292+
Spacer(modifier = Modifier.height(8.dp))
293+
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
294+
Text("正在下载...")
295+
}
296+
is ResticViewModel.DownloadState.Error -> {
297+
Text("下载失败: ${state.message}", color = MaterialTheme.colorScheme.error)
298+
}
299+
is ResticViewModel.DownloadState.Success -> {
300+
Text("下载成功!", color = MaterialTheme.colorScheme.primary)
301+
}
302+
else -> {}
303+
}
304+
}
305+
},
306+
confirmButton = {
307+
val state = downloadState
308+
if (state is ResticViewModel.DownloadState.Success) {
309+
Button(onClick = onDownloadComplete) { Text("完成") }
310+
} else {
311+
Button(
312+
onClick = {
313+
viewModel.setDownloadUrl(urlInput)
314+
scope.launch {
315+
viewModel.downloadResticBinary(urlInput)
316+
}
317+
},
318+
enabled = urlInput.isNotBlank() && state !is ResticViewModel.DownloadState.Downloading
319+
) {
320+
Text(if (state is ResticViewModel.DownloadState.Downloading) "下载中..." else "下载")
321+
}
322+
}
323+
},
324+
dismissButton = {
325+
if (downloadState !is ResticViewModel.DownloadState.Downloading) {
326+
TextButton(onClick = onDismiss) { Text("取消") }
327+
}
328+
}
329+
)
250330
}

0 commit comments

Comments
 (0)