feat: 灵动岛参数回退单一根节点+岛开关;新增应用内快捷更新(检查/下载/安装)
This commit is contained in:
@@ -13,8 +13,8 @@ android {
|
|||||||
applicationId = "com.smbfast.app"
|
applicationId = "com.smbfast.app"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 20
|
versionCode = 21
|
||||||
versionName = "1.18.0"
|
versionName = "1.19.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import javax.crypto.spec.GCMParameterSpec
|
|||||||
data class SpeedSettings(
|
data class SpeedSettings(
|
||||||
var streams: Int = 4, // 并行流数(每个流独立 SMB 句柄,分块并行下载)
|
var streams: Int = 4, // 并行流数(每个流独立 SMB 句柄,分块并行下载)
|
||||||
var chunkKB: Int = 1024, // 每个请求的分块大小 KB
|
var chunkKB: Int = 1024, // 每个请求的分块大小 KB
|
||||||
|
var islandOn: Boolean = true, // HyperOS 灵动岛/焦点通知
|
||||||
|
var updateUrl: String = "http://192.168.31.3:8080/", // 应用内快捷更新的分发服务器
|
||||||
)
|
)
|
||||||
|
|
||||||
class Prefs(context: Context) {
|
class Prefs(context: Context) {
|
||||||
|
|||||||
@@ -138,14 +138,16 @@ class TransferService : Service() {
|
|||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||||
)
|
)
|
||||||
b.addAction(NotificationCompat.Action(android.R.drawable.ic_delete, "全部停止", stopIntent))
|
b.addAction(NotificationCompat.Action(android.R.drawable.ic_delete, "全部停止", stopIntent))
|
||||||
runCatching {
|
if (islandOn()) {
|
||||||
val extras = Bundle()
|
runCatching {
|
||||||
extras.putBundle("miui.focus.pics", islandPics())
|
val extras = Bundle()
|
||||||
b.addExtras(extras)
|
extras.putBundle("miui.focus.pics", islandPics())
|
||||||
|
b.addExtras(extras)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val n = b.build()
|
val n = b.build()
|
||||||
if (!running.isNullOrEmpty()) {
|
if (!running.isNullOrEmpty() && islandOn()) {
|
||||||
// 小米 HyperOS 焦点通知/超级岛:客户端本地上岛(官方 extras 协议)。
|
// 小米 HyperOS 焦点通知/超级岛:客户端本地上岛(官方 extras 协议)。
|
||||||
// 非小米系统会直接忽略该字段,不影响普通通知展示。
|
// 非小米系统会直接忽略该字段,不影响普通通知展示。
|
||||||
// islandFirstFloat 只在第一条通知为 true,否则每次更新都会重新弹出岛。
|
// islandFirstFloat 只在第一条通知为 true,否则每次更新都会重新弹出岛。
|
||||||
@@ -156,6 +158,10 @@ class TransferService : Service() {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun islandOn(): Boolean =
|
||||||
|
runCatching { com.smbfast.app.data.Prefs(applicationContext).loadSettings().islandOn }
|
||||||
|
.getOrDefault(true)
|
||||||
|
|
||||||
/** 岛内图标:直接复用应用图标 */
|
/** 岛内图标:直接复用应用图标 */
|
||||||
private fun islandPics(): Bundle = Bundle().apply {
|
private fun islandPics(): Bundle = Bundle().apply {
|
||||||
val icon = Icon.createWithResource(applicationContext, R.mipmap.ic_launcher)
|
val icon = Icon.createWithResource(applicationContext, R.mipmap.ic_launcher)
|
||||||
@@ -223,12 +229,9 @@ class TransferService : Service() {
|
|||||||
.put("param_island", paramIsland)
|
.put("param_island", paramIsland)
|
||||||
.put("baseInfo", baseInfo)
|
.put("baseInfo", baseInfo)
|
||||||
|
|
||||||
val paramOs2 = JSONObject().put("baseInfo", baseInfo)
|
// 单一根节点 param_v2:曾测试过 param+param_v2 双轨,HyperOS 解析异常会把
|
||||||
|
// 整条通知吞掉(比不用岛更糟),故保持单一根节点
|
||||||
return JSONObject()
|
return JSONObject().put("param_v2", paramV2).toString()
|
||||||
.put("param", paramOs2)
|
|
||||||
.put("param_v2", paramV2)
|
|
||||||
.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun acquireLocks() {
|
private fun acquireLocks() {
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Bolt
|
import androidx.compose.material.icons.filled.Bolt
|
||||||
import androidx.compose.material.icons.filled.BugReport
|
import androidx.compose.material.icons.filled.BugReport
|
||||||
|
import androidx.compose.material.icons.filled.CloudDownload
|
||||||
import androidx.compose.material.icons.filled.Folder
|
import androidx.compose.material.icons.filled.Folder
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
import androidx.compose.material.icons.filled.Speed
|
import androidx.compose.material.icons.filled.Speed
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -33,6 +35,8 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.SliderDefaults
|
import androidx.compose.material3.SliderDefaults
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@@ -41,6 +45,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -48,6 +53,7 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -56,9 +62,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import com.smbfast.app.ui.AppViewModel
|
import com.smbfast.app.ui.AppViewModel
|
||||||
import com.smbfast.app.ui.components.AuroraBackground
|
import com.smbfast.app.ui.components.AuroraBackground
|
||||||
import com.smbfast.app.ui.components.GlassCard
|
import com.smbfast.app.ui.components.GlassCard
|
||||||
|
import com.smbfast.app.ui.components.GlassTextField
|
||||||
import com.smbfast.app.ui.components.GradientText
|
import com.smbfast.app.ui.components.GradientText
|
||||||
import com.smbfast.app.ui.components.NeonIconBox
|
import com.smbfast.app.ui.components.NeonIconBox
|
||||||
|
import com.smbfast.app.ui.components.NeonProgress
|
||||||
import com.smbfast.app.ui.theme.Neon
|
import com.smbfast.app.ui.theme.Neon
|
||||||
|
import com.smbfast.app.util.Updater
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -147,6 +158,36 @@ fun SettingsScreen(vm: AppViewModel) {
|
|||||||
subtitle = vm.downloadDir.absolutePath,
|
subtitle = vm.downloadDir.absolutePath,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
SectionCard(
|
||||||
|
icon = Icons.Default.Notifications,
|
||||||
|
brush = Neon.Primary,
|
||||||
|
title = "灵动岛 / 焦点通知",
|
||||||
|
subtitle = "传输时在小米 HyperOS 岛区域显示进度(其他品牌无影响)。若岛不出现或通知异常,可关闭后退回普通通知",
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.height(6.dp))
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Switch(
|
||||||
|
checked = s.islandOn,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
vm.updateSettings { it.copy(islandOn = checked) }
|
||||||
|
},
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = Color(0xFF04121E),
|
||||||
|
checkedTrackColor = Neon.Cyan,
|
||||||
|
uncheckedTrackColor = Color.White.copy(alpha = 0.12f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
if (s.islandOn) "已开启" else "已关闭(仅普通通知)",
|
||||||
|
color = Neon.TextDim,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateSection(vm = vm, currentName = currentVersionName(), currentCode = currentVersionCode())
|
||||||
|
|
||||||
SectionCard(
|
SectionCard(
|
||||||
icon = Icons.Default.Speed,
|
icon = Icons.Default.Speed,
|
||||||
brush = Neon.Sunset,
|
brush = Neon.Sunset,
|
||||||
@@ -161,7 +202,7 @@ fun SettingsScreen(vm: AppViewModel) {
|
|||||||
icon = Icons.Default.Info,
|
icon = Icons.Default.Info,
|
||||||
brush = Neon.File,
|
brush = Neon.File,
|
||||||
title = "隐私提示",
|
title = "隐私提示",
|
||||||
subtitle = "连接密码保存在本机应用私有数据中(未加密)。请勿在共享设备上保存敏感凭据。",
|
subtitle = "连接密码使用 AndroidKeyStore 加密后保存在本机应用私有数据中,root/备份不可直接读取。",
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
SectionCard(
|
SectionCard(
|
||||||
@@ -220,6 +261,124 @@ fun SettingsScreen(vm: AppViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun currentVersionCode(): Long {
|
||||||
|
val ctx = LocalContext.current
|
||||||
|
return remember {
|
||||||
|
val pi = ctx.packageManager.getPackageInfo(ctx.packageName, 0)
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= 28) pi.longVersionCode
|
||||||
|
else @Suppress("DEPRECATION") pi.versionCode.toLong()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun currentVersionName(): String {
|
||||||
|
val ctx = LocalContext.current
|
||||||
|
return remember {
|
||||||
|
ctx.packageManager.getPackageInfo(ctx.packageName, 0).versionName ?: "?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 应用内快捷更新:检查 version.json → 下载 APK → 拉起系统安装器 */
|
||||||
|
@Composable
|
||||||
|
private fun UpdateSection(vm: AppViewModel, currentName: String, currentCode: Long) {
|
||||||
|
val s by vm.settings.collectAsStateWithLifecycle()
|
||||||
|
val ctx = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var checking by remember { mutableStateOf(false) }
|
||||||
|
var info by remember { mutableStateOf<Updater.Info?>(null) }
|
||||||
|
var msg by remember { mutableStateOf<String?>(null) }
|
||||||
|
var progress by remember { mutableStateOf<Float?>(null) }
|
||||||
|
|
||||||
|
SectionCard(
|
||||||
|
icon = Icons.Default.CloudDownload,
|
||||||
|
brush = Neon.Sunset,
|
||||||
|
title = "应用更新",
|
||||||
|
subtitle = "当前版本 $currentName ($currentCode) · 从分发服务器一键检查、下载并安装新版本",
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
GlassTextField(
|
||||||
|
value = s.updateUrl,
|
||||||
|
onValueChange = { v -> vm.updateSettings { it.copy(updateUrl = v) } },
|
||||||
|
label = "分发服务器地址",
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
MiniChip(if (checking) "检查中…" else "检查更新") {
|
||||||
|
if (checking || progress != null) return@MiniChip
|
||||||
|
checking = true
|
||||||
|
info = null
|
||||||
|
msg = null
|
||||||
|
scope.launch {
|
||||||
|
Updater.check(s.updateUrl, currentCode).fold(
|
||||||
|
onSuccess = { found ->
|
||||||
|
checking = false
|
||||||
|
if (found == null) {
|
||||||
|
msg = "已是最新版本($currentName / $currentCode)"
|
||||||
|
} else {
|
||||||
|
info = found
|
||||||
|
msg = "发现新版本 ${found.versionName} (${found.versionCode})" +
|
||||||
|
(found.note?.let { " · $it" } ?: "")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
checking = false
|
||||||
|
msg = "检查失败:${it.message ?: "网络不可达"}"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (info != null) {
|
||||||
|
MiniChip(
|
||||||
|
if (progress != null) "下载中 ${((progress ?: 0f) * 100).toInt()}%"
|
||||||
|
else "下载并安装",
|
||||||
|
) {
|
||||||
|
val cur = info ?: return@MiniChip
|
||||||
|
if (progress != null) return@MiniChip
|
||||||
|
progress = 0f
|
||||||
|
scope.launch {
|
||||||
|
val dir = ctx.getExternalFilesDir("update") ?: ctx.filesDir
|
||||||
|
val apk = File(dir, "SmbFast-update.apk")
|
||||||
|
Updater.download(Updater.resolveUrl(s.updateUrl, cur), apk) { p -> progress = p }
|
||||||
|
.fold(
|
||||||
|
onSuccess = { f ->
|
||||||
|
progress = null
|
||||||
|
runCatching {
|
||||||
|
val uri = androidx.core.content.FileProvider.getUriForFile(
|
||||||
|
ctx, "${ctx.packageName}.fileprovider", f,
|
||||||
|
)
|
||||||
|
val i = android.content.Intent(android.content.Intent.ACTION_VIEW)
|
||||||
|
.setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
.addFlags(
|
||||||
|
android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
or android.content.Intent.FLAG_ACTIVITY_NEW_TASK,
|
||||||
|
)
|
||||||
|
ctx.startActivity(i)
|
||||||
|
}.onFailure { msg = "无法启动安装器:${it.message}" }
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
progress = null
|
||||||
|
msg = "下载失败:${it.message ?: "网络错误"}"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val m = msg
|
||||||
|
if (m != null) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(m, color = Neon.TextDim, fontSize = 11.sp, lineHeight = 15.sp)
|
||||||
|
}
|
||||||
|
val p = progress
|
||||||
|
if (p != null) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
NeonProgress(fraction = p, modifier = Modifier.fillMaxWidth(), brush = Neon.Primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MiniChip(text: String, onClick: () -> Unit) {
|
private fun MiniChip(text: String, onClick: () -> Unit) {
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.smbfast.app.util
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用内快捷更新:从分发服务器(dist/ 目录的 HTTP 服务)检查并下载新版本。
|
||||||
|
* 服务器需提供 version.json:{"versionCode":21,"versionName":"1.19.0","file":"SmbFast-release-v21.apk","note":"..."}
|
||||||
|
*/
|
||||||
|
object Updater {
|
||||||
|
|
||||||
|
data class Info(
|
||||||
|
val versionCode: Long,
|
||||||
|
val versionName: String,
|
||||||
|
val file: String,
|
||||||
|
val note: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 拉取 {base}/version.json;服务器版本比本地新时返回信息,否则返回 null */
|
||||||
|
suspend fun check(base: String, currentCode: Long): Result<Info?> = withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
val u = if (base.endsWith("/")) base else "$base/"
|
||||||
|
val conn = URL(u + "version.json").openConnection() as HttpURLConnection
|
||||||
|
conn.connectTimeout = 5000
|
||||||
|
conn.readTimeout = 5000
|
||||||
|
val body = try {
|
||||||
|
conn.inputStream.use { it.readBytes().decodeToString() }
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
val o = JSONObject(body)
|
||||||
|
val info = Info(
|
||||||
|
versionCode = o.getLong("versionCode"),
|
||||||
|
versionName = o.optString("versionName", ""),
|
||||||
|
file = o.getString("file"),
|
||||||
|
note = o.optString("note", "").ifBlank { null },
|
||||||
|
)
|
||||||
|
if (info.versionCode > currentCode) info else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拼出 APK 下载地址(file 为相对名时基于 base) */
|
||||||
|
fun resolveUrl(base: String, info: Info): String =
|
||||||
|
if (info.file.startsWith("http")) info.file
|
||||||
|
else (if (base.endsWith("/")) base else "$base/") + info.file
|
||||||
|
|
||||||
|
/** 下载 APK 到 dest,onProgress 回调 0~1 */
|
||||||
|
suspend fun download(url: String, dest: File, onProgress: (Float) -> Unit): Result<File> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
val conn = URL(url).openConnection() as HttpURLConnection
|
||||||
|
conn.connectTimeout = 5000
|
||||||
|
conn.readTimeout = 15000
|
||||||
|
val total = conn.contentLengthLong
|
||||||
|
dest.parentFile?.mkdirs()
|
||||||
|
try {
|
||||||
|
dest.outputStream().use { out ->
|
||||||
|
conn.inputStream.use { input ->
|
||||||
|
val buf = ByteArray(256 * 1024)
|
||||||
|
var done = 0L
|
||||||
|
while (true) {
|
||||||
|
val n = input.read(buf)
|
||||||
|
if (n < 0) break
|
||||||
|
out.write(buf, 0, n)
|
||||||
|
done += n
|
||||||
|
if (total > 0) {
|
||||||
|
onProgress((done.toDouble() / total).coerceIn(0.0, 1.0).toFloat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
dest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,4 +3,5 @@
|
|||||||
<external-path name="external_download" path="Download/SMBFast/" />
|
<external-path name="external_download" path="Download/SMBFast/" />
|
||||||
<external-files-path name="app_download" path="SMBFast/" />
|
<external-files-path name="app_download" path="SMBFast/" />
|
||||||
<files-path name="internal_files" path="SMBFast/" />
|
<files-path name="internal_files" path="SMBFast/" />
|
||||||
|
<external-files-path name="update" path="update/" />
|
||||||
</paths>
|
</paths>
|
||||||
|
|||||||
Reference in New Issue
Block a user