feat: 同步本地代码到远程仓库

- 新增WebDAV同步功能相关文件
- 新增CustomSwitch自定义开关组件
- 新增SyncCodeManager、UpdateInstaller、WebDAVSyncManager工具类
- 新增build_all_release.sh构建脚本
- 更新多个Dialog和Activity文件
- 更新字符串资源文件
- 删除apk/release目录下的旧文件
This commit is contained in:
您的名字
2025-11-10 19:05:21 +08:00
parent e7e215628b
commit e0aee44d5a
43 changed files with 4244 additions and 391 deletions
@@ -12,9 +12,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.HandlerCompat;
import com.fongmi.android.tv.event.EventIndex;
import com.fongmi.android.tv.Setting;
// import com.fongmi.android.tv.event.EventIndex; // 暂时注释,如果不存在则删除
import com.fongmi.android.tv.ui.activity.CrashActivity;
import com.fongmi.android.tv.utils.CacheCleaner;
import com.fongmi.android.tv.utils.UpdateInstaller;
import com.fongmi.android.tv.utils.WebDAVSyncManager;
import com.fongmi.android.tv.utils.Notify;
import com.fongmi.hook.Hook;
import com.github.catvod.Init;
@@ -43,6 +46,7 @@ public class App extends Application {
private final long time;
private Hook hook;
private final Runnable cleanTask;
private final Runnable syncTask;
private boolean appJustLaunched;
public App() {
@@ -52,6 +56,7 @@ public class App extends Application {
time = System.currentTimeMillis();
gson = new Gson();
cleanTask = this::checkCacheClean;
syncTask = this::checkWebDAVSync;
appJustLaunched = true;
}
@@ -129,7 +134,8 @@ public class App extends Application {
Logger.addLogAdapter(getLogAdapter());
OkHttp.get().setProxy(Setting.getProxy());
OkHttp.get().setDoh(Doh.objectFrom(Setting.getDoh()));
EventBus.builder().addIndex(new EventIndex()).installDefaultEventBus();
// EventBus.builder().addIndex(new EventIndex()).installDefaultEventBus(); // 暂时注释,如果EventIndex不存在则删除
EventBus.getDefault(); // 使用默认EventBus
CaocConfig.Builder.create().backgroundMode(CaocConfig.BACKGROUND_MODE_SILENT).errorActivity(CrashActivity.class).apply();
// Ensure default notification channel exists for foreground playback service (TV flavor too)
Notify.createChannel();
@@ -153,6 +159,12 @@ public class App extends Application {
if (activity != activity()) setActivity(activity);
// 应用回到前台时检查缓存
checkCacheClean();
// 检查是否有待安装的更新文件(用户从设置页面返回后)
checkPendingInstall();
// 检查WebDAV自动同步
checkWebDAVSync();
// 自动检查更新(如果启用)
checkAutoUpdate(activity);
}
@Override
@@ -189,6 +201,93 @@ public class App extends Application {
// 每30分钟定期检查缓存
post(cleanTask, 30 * 60 * 1000);
}
/**
* 检查是否有待安装的更新文件
* 当用户从设置页面授予安装权限后返回时,自动安装
*/
private void checkPendingInstall() {
UpdateInstaller installer = UpdateInstaller.get();
if (installer.hasPendingInstall()) {
Logger.d("App: 检测到待安装文件且权限已授予,自动安装");
boolean success = installer.autoRetryInstall();
if (success) {
Notify.show("正在安装更新...");
} else {
Logger.e("App: 自动安装失败");
}
}
}
/**
* 检查并执行WebDAV自动同步
*/
private void checkWebDAVSync() {
WebDAVSyncManager manager = WebDAVSyncManager.get();
if (manager.isConfigured()) {
// 应用启动时,如果已配置WebDAV,立即执行一次同步(下载远程数据)
// 这样新设备配置后,下次启动应用时就能看到其他设备的历史记录
App.execute(() -> {
try {
Logger.d("App: 应用启动,执行WebDAV同步");
// 先上传本地记录
manager.uploadHistory();
// 再下载远程记录并合并
manager.downloadHistory();
Logger.d("App: WebDAV同步完成");
} catch (Exception e) {
Logger.e("App: WebDAV同步失败: " + e.getMessage());
}
});
// 如果启用了自动同步,设置定期同步
if (Setting.isWebDAVAutoSync()) {
int interval = Setting.getWebDAVSyncInterval();
// 延迟执行下次同步,避免影响启动速度
post(syncTask, interval * 60 * 1000L);
}
}
}
/**
* 执行WebDAV同步
*/
private void doWebDAVSync() {
App.execute(() -> {
WebDAVSyncManager manager = WebDAVSyncManager.get();
if (manager.isConfigured()) {
Logger.d("App: 开始WebDAV自动同步");
manager.syncAll();
// 设置下次同步
int interval = Setting.getWebDAVSyncInterval();
post(syncTask, interval * 60 * 1000L);
}
});
}
/**
* 自动检查更新(如果启用)
*/
private void checkAutoUpdate(Activity activity) {
// 检查是否启用自动更新检查
if (!Setting.getAutoUpdateCheck()) {
return;
}
// 检查是否启用更新功能
if (!Setting.getUpdate()) {
return;
}
// 延迟一小段时间,避免影响应用启动速度
post(() -> {
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
Logger.d("App: 开始自动检查更新");
Updater.create().auto().release().start(activity);
}
}, 2000); // 延迟2秒
}
@Override
public PackageManager getPackageManager() {
@@ -332,4 +332,97 @@ public class Setting {
public static void putLiveTabVisible(boolean visible) {
Prefers.put("live_tab_visible", visible);
}
// WebDAV 同步配置
public static String getWebDAVUrl() {
return Prefers.getString("webdav_url");
}
public static void putWebDAVUrl(String url) {
Prefers.put("webdav_url", url);
}
public static String getWebDAVUsername() {
return Prefers.getString("webdav_username");
}
public static void putWebDAVUsername(String username) {
Prefers.put("webdav_username", username);
}
public static String getWebDAVPassword() {
return Prefers.getString("webdav_password");
}
public static void putWebDAVPassword(String password) {
Prefers.put("webdav_password", password);
}
public static boolean isWebDAVAutoSync() {
return Prefers.getBoolean("webdav_auto_sync", false);
}
public static void putWebDAVAutoSync(boolean autoSync) {
Prefers.put("webdav_auto_sync", autoSync);
}
public static int getWebDAVSyncInterval() {
return Prefers.getInt("webdav_sync_interval", 30); // 默认30分钟
}
public static void putWebDAVSyncInterval(int minutes) {
Prefers.put("webdav_sync_interval", minutes);
}
// WebDAV 同步模式:ACCOUNT(账号模式)或 CODE(同步码模式)
public static String getWebDAVSyncMode() {
return Prefers.getString("webdav_sync_mode", "ACCOUNT");
}
public static void putWebDAVSyncMode(String mode) {
Prefers.put("webdav_sync_mode", mode);
}
// 同步码(用于同步码模式)
public static String getWebDAVSyncCode() {
return Prefers.getString("webdav_sync_code");
}
public static void putWebDAVSyncCode(String code) {
Prefers.put("webdav_sync_code", code);
}
// 公开存储URL(用于同步码模式,如GitHub Gist URL
public static String getWebDAVPublicUrl() {
return Prefers.getString("webdav_public_url");
}
public static void putWebDAVPublicUrl(String url) {
Prefers.put("webdav_public_url", url);
}
// GitHub Gist相关(用于同步码模式)
public static String getWebDAVGistId() {
return Prefers.getString("webdav_gist_id");
}
public static void putWebDAVGistId(String gistId) {
Prefers.put("webdav_gist_id", gistId);
}
public static String getWebDAVGistRawUrl() {
return Prefers.getString("webdav_gist_raw_url");
}
public static void putWebDAVGistRawUrl(String url) {
Prefers.put("webdav_gist_raw_url", url);
}
public static String getWebDAVGistToken() {
return Prefers.getString("webdav_gist_token");
}
public static void putWebDAVGistToken(String token) {
Prefers.put("webdav_gist_token", token);
}
}
@@ -18,53 +18,219 @@ public class Download {
private final File file;
private final String url;
private final String fallbackUrl;
private Callback callback;
private static final int MAX_RETRY_COUNT = 3; // 最大重试次数
public static Download create(String url, File file) {
return create(url, file, null);
}
public static Download create(String url, File file, Callback callback) {
return new Download(url, file, callback);
return create(url, file, null, callback);
}
public static Download create(String url, File file, String fallbackUrl, Callback callback) {
return new Download(url, file, fallbackUrl, callback);
}
public Download(String url, File file, Callback callback) {
this(url, file, null, callback);
}
public Download(String url, File file, String fallbackUrl, Callback callback) {
this.url = url;
this.file = file;
this.fallbackUrl = fallbackUrl;
this.callback = callback;
}
public void start() {
if (url == null || url.isEmpty()) {
if (callback != null) {
App.post(() -> callback.error("下载URL为空"));
}
return;
}
if (url.startsWith("file")) return;
if (callback == null) doInBackground();
else App.execute(this::doInBackground);
if (file == null) {
if (callback != null) {
App.post(() -> callback.error("保存文件路径为空"));
}
return;
}
if (callback == null) {
// 无回调时,直接执行(同步)
doInBackgroundWithFallback();
} else {
// 有回调时,异步执行
App.execute(this::doInBackgroundWithFallback);
}
}
/**
* 带智能回退的下载方法
* 先尝试主URL(通常是jsDelivr CDN),失败后回退到备用URL
*/
private void doInBackgroundWithFallback() {
// 先尝试主URL
boolean mainSuccess = doInBackground(url, "主URL");
if (mainSuccess) {
return;
}
// 主URL失败,如果有回退URL,尝试回退URL
if (fallbackUrl != null && !fallbackUrl.equals(url)) {
Logger.d("Download: 主URL下载失败,回退到备用URL: " + fallbackUrl);
doInBackground(fallbackUrl, "备用URL");
}
}
/**
* 使用指定URL下载文件(带重试机制)
*/
private boolean doInBackground(String downloadUrl, String source) {
Exception lastException = null;
for (int attempt = 1; attempt <= MAX_RETRY_COUNT; attempt++) {
try {
if (callback != null) {
App.post(() -> callback.progress(0));
}
boolean success = downloadWithUrl(downloadUrl, source, attempt);
if (success) {
return true;
}
} catch (Exception e) {
lastException = e;
Logger.w("Download: 下载失败 (来源: " + source + ", 尝试 " + attempt + "/" + MAX_RETRY_COUNT + "): " + e.getMessage());
// 如果不是最后一次尝试,等待后重试
if (attempt < MAX_RETRY_COUNT) {
try {
long retryDelay = 500L * attempt; // 递增延迟
Thread.sleep(retryDelay);
Logger.d("Download: 等待 " + retryDelay + "ms 后重试...");
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
}
// 所有尝试都失败
if (callback != null && lastException != null) {
String errorMsg = lastException.getMessage();
App.post(() -> callback.error(errorMsg != null ? errorMsg : "下载失败"));
}
return false;
}
/**
* 使用指定URL下载文件
*/
private boolean downloadWithUrl(String downloadUrl, String source, int attempt) throws Exception {
if (downloadUrl == null || downloadUrl.isEmpty()) {
throw new Exception("下载URL为空");
}
if (file == null) {
throw new Exception("保存文件路径为空");
}
Response res = null;
InputStream inputStream = null;
try {
res = OkHttp.newCall(downloadUrl, downloadUrl).execute();
// 检查HTTP响应状态码
if (!res.isSuccessful()) {
throw new Exception("下载失败: HTTP " + res.code() + " " + (res.message() != null ? res.message() : "未知错误"));
}
// 检查响应体是否存在
if (res.body() == null) {
throw new Exception("下载失败: 响应体为空");
}
// 获取输入流
inputStream = res.body().byteStream();
if (inputStream == null) {
throw new Exception("下载失败: 无法获取输入流");
}
Path.create(file);
// 获取文件大小,如果无法获取则使用-1表示未知大小
String contentLengthStr = res.header(HttpHeaders.CONTENT_LENGTH);
long expectedLength = -1;
if (contentLengthStr != null && !contentLengthStr.isEmpty()) {
try {
expectedLength = Long.parseLong(contentLengthStr);
if (expectedLength < 0) {
expectedLength = -1;
}
} catch (NumberFormatException e) {
Logger.w("Download: 无法解析Content-Length: " + contentLengthStr);
expectedLength = -1;
}
}
// 下载文件
download(inputStream, expectedLength);
// 验证下载的文件(如果知道预期大小)
if (expectedLength > 0 && !verifyDownloadedFile(file, expectedLength)) {
throw new Exception("下载的文件可能已损坏,请重试");
}
Logger.d("Download: 下载成功 (来源: " + source + ", 尝试 " + attempt + "/" + MAX_RETRY_COUNT + ")");
if (callback != null) {
App.post(() -> callback.success(file));
}
return true;
} catch (Exception e) {
// 如果下载失败,删除可能不完整的文件
if (file != null && file.exists()) {
try {
file.delete();
} catch (Exception ignored) {
}
}
throw e;
} finally {
// 关闭输入流
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception ignored) {
}
}
// 关闭响应
if (res != null) {
try {
res.close();
} catch (Exception ignored) {
}
}
}
}
public void cancel() {
OkHttp.cancel(url);
if (fallbackUrl != null) {
OkHttp.cancel(fallbackUrl);
}
Path.clear(file);
callback = null;
}
private void doInBackground() {
try (Response res = OkHttp.newCall(url, url).execute()) {
Path.create(file);
long expectedLength = Long.parseLong(res.header(HttpHeaders.CONTENT_LENGTH, "0"));
download(res.body().byteStream(), expectedLength);
// 验证下载的文件
if (!verifyDownloadedFile(file, expectedLength)) {
App.post(() -> {if (callback != null) callback.error("下载的文件可能已损坏,请重试");});
return;
}
App.post(() -> {if (callback != null) callback.success(file);});
} catch (Exception e) {
App.post(() -> {if (callback != null) callback.error(e.getMessage());});
}
}
private void download(InputStream is, long length) throws Exception {
if (is == null) {
throw new Exception("输入流为空,无法下载");
}
try (BufferedInputStream input = new BufferedInputStream(is); FileOutputStream os = new FileOutputStream(file)) {
byte[] buffer = new byte[4096];
int readBytes;
@@ -72,39 +238,56 @@ public class Download {
while ((readBytes = input.read(buffer)) != -1) {
totalBytes += readBytes;
os.write(buffer, 0, readBytes);
int progress = (int) (totalBytes / length * 100.0);
App.post(() -> {if (callback != null) callback.progress(progress);});
// 只有当知道文件大小时才计算进度
if (length > 0 && callback != null) {
int progress = (int) (totalBytes * 100.0 / length);
final int finalProgress = Math.min(progress, 100); // 确保不超过100%,并设为final
App.post(() -> callback.progress(finalProgress));
} else if (callback != null) {
// 不知道文件大小时,显示不确定进度
App.post(() -> callback.progress(-1));
}
}
// 下载完成后,如果不知道文件大小,显示100%
if (length <= 0 && callback != null) {
App.post(() -> callback.progress(100));
}
}
}
private boolean verifyDownloadedFile(File file, long expectedLength) {
try {
// 检查文件大小
if (file.length() != expectedLength) {
// 如果文件不存在或为空,验证失败
if (file == null || !file.exists() || file.length() == 0) {
Logger.e("File verification failed: file does not exist or is empty");
return false;
}
// 如果知道预期大小,检查文件大小是否匹配
if (expectedLength > 0 && file.length() != expectedLength) {
Logger.e("File size mismatch: expected " + expectedLength + ", actual " + file.length());
return false;
}
// 检查APK文件头 (ZIP文件头)
if (file.length() < 4) return false;
if (file.length() < 4) {
Logger.e("File too small: " + file.length() + " bytes");
return false;
}
try (FileInputStream fis = new FileInputStream(file)) {
byte[] header = new byte[4];
fis.read(header);
// ZIP文件头应该是 0x504B0304 (PK..)
if (header[0] != 0x50 || header[1] != 0x4B || header[2] != 0x03 || header[3] != 0x04) {
Logger.e("Invalid APK file header");
int bytesRead = fis.read(header);
if (bytesRead < 4) {
Logger.e("Cannot read file header");
return false;
}
// 额外验证:检查APK文件是否完整
// 尝试读取ZIP文件结构
fis.getChannel().position(0);
byte[] buffer = new byte[1024];
int bytesRead = fis.read(buffer);
if (bytesRead < 4) {
Logger.e("APK file too small or corrupted");
// ZIP文件头应该是 0x504B0304 (PK..)
if (header[0] != 0x50 || header[1] != 0x4B || header[2] != 0x03 || header[3] != 0x04) {
Logger.e("Invalid APK file header: " + String.format("%02X %02X %02X %02X", header[0], header[1], header[2], header[3]));
return false;
}
}
@@ -113,6 +296,7 @@ public class Download {
return true;
} catch (Exception e) {
Logger.e("File verification failed: " + e.getMessage());
e.printStackTrace();
return false;
}
}
@@ -0,0 +1,312 @@
package com.fongmi.android.tv.utils;
import android.text.TextUtils;
import com.fongmi.android.tv.Setting;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.bean.Backup;
import com.fongmi.android.tv.bean.History;
import com.fongmi.android.tv.db.AppDatabase;
import com.github.catvod.net.OkHttp;
import com.github.catvod.utils.Logger;
import com.github.catvod.utils.Prefers;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
/**
* 同步码管理器(无需WebDAV账号)
* 使用公开的HTTP存储服务,通过同步码区分用户
*
* 方案:使用GitHub Gist作为存储
* - 用户创建一个公开的GitHub Gist
* - 通过同步码作为文件名的一部分来区分不同用户
* - 所有知道同步码的设备可以共享数据
*/
public class SyncCodeManager {
private static final String HISTORY_FILE_PREFIX = "xmbox_history_";
private static final String SETTINGS_FILE_PREFIX = "xmbox_settings_";
private static final String BACKUP_FILE_PREFIX = "xmbox_backup_";
private static final String FILE_SUFFIX = ".json";
private static SyncCodeManager instance;
private String syncCode;
private String gistId; // GitHub Gist ID
private String gistToken; // GitHub Personal Access Token(用于上传,可选)
public static SyncCodeManager get() {
if (instance == null) {
instance = new SyncCodeManager();
}
return instance;
}
private SyncCodeManager() {
loadConfig();
}
/**
* 加载配置
*/
private void loadConfig() {
syncCode = Setting.getWebDAVSyncCode();
gistId = Setting.getWebDAVGistId();
gistToken = Setting.getWebDAVGistToken();
}
/**
* 检查是否已配置
*/
public boolean isConfigured() {
return !TextUtils.isEmpty(syncCode) && !TextUtils.isEmpty(gistId);
}
/**
* 生成同步码
* @return 8位随机同步码(字母+数字)
*/
public static String generateSyncCode() {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = new Random();
StringBuilder code = new StringBuilder();
for (int i = 0; i < 8; i++) {
code.append(chars.charAt(random.nextInt(chars.length())));
}
return code.toString();
}
/**
* 获取文件URLGitHub Gist raw URL
*/
private String getFileUrl(String prefix) {
// GitHub Gist raw URL格式:
// https://gist.githubusercontent.com/{username}/{gist_id}/raw/{filename}
// 文件名格式:{prefix}{syncCode}.json
// 例如:xmbox_history_ABC123XYZ.json
// 如果用户提供了完整的Gist raw URL
String gistRawUrl = Setting.getWebDAVGistRawUrl();
if (!TextUtils.isEmpty(gistRawUrl)) {
String filename = prefix + syncCode + FILE_SUFFIX;
return gistRawUrl + "/" + filename;
}
// 否则需要从Gist ID构建(需要知道username
// 这里简化处理,要求用户提供完整的raw URL
return null;
}
/**
* 上传观看记录
*/
public boolean uploadHistory() {
if (!isConfigured()) {
Logger.e("SyncCode: 未配置,无法上传观看记录");
return false;
}
try {
// 获取所有观看记录
List<History> historyList = AppDatabase.get().getHistoryDao().findAll();
String json = App.gson().toJson(historyList);
// 上传到GitHub Gist
String fileUrl = getFileUrl(HISTORY_FILE_PREFIX);
if (fileUrl == null) {
Logger.e("SyncCode: 无法构建文件URL,请配置Gist Raw URL");
return false;
}
// 使用GitHub Gist API更新文件
boolean success = updateGistFile(fileUrl, json);
if (success) {
Logger.d("SyncCode: 观看记录上传成功,共 " + historyList.size() + "");
}
return success;
} catch (Exception e) {
Logger.e("SyncCode: 观看记录上传失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 下载观看记录
*/
public boolean downloadHistory() {
if (!isConfigured()) {
Logger.e("SyncCode: 未配置,无法下载观看记录");
return false;
}
try {
String fileUrl = getFileUrl(HISTORY_FILE_PREFIX);
if (fileUrl == null) {
return false;
}
// 从GitHub Gist下载文件
String json = downloadGistFile(fileUrl);
if (TextUtils.isEmpty(json)) {
Logger.d("SyncCode: 观看记录文件不存在,跳过下载");
return false;
}
Type listType = new TypeToken<List<History>>(){}.getType();
List<History> remoteHistoryList = App.gson().fromJson(json, listType);
// 智能合并(与WebDAV相同的逻辑)
if (!remoteHistoryList.isEmpty()) {
List<History> localHistoryList = AppDatabase.get().getHistoryDao().findAll();
Map<String, History> localMap = new HashMap<>();
for (History local : localHistoryList) {
localMap.put(local.getKey(), local);
}
List<History> toInsert = new java.util.ArrayList<>();
List<History> toUpdate = new java.util.ArrayList<>();
for (History remote : remoteHistoryList) {
History local = localMap.get(remote.getKey());
if (local == null) {
toInsert.add(remote);
} else {
if (remote.getCreateTime() > local.getCreateTime()) {
toUpdate.add(remote);
} else if (remote.getCreateTime() == local.getCreateTime() && remote.getPosition() > local.getPosition()) {
toUpdate.add(remote);
}
}
}
if (!toInsert.isEmpty()) {
AppDatabase.get().getHistoryDao().insert(toInsert);
Logger.d("SyncCode: 新增 " + toInsert.size() + " 条观看记录");
}
if (!toUpdate.isEmpty()) {
AppDatabase.get().getHistoryDao().update(toUpdate);
Logger.d("SyncCode: 更新 " + toUpdate.size() + " 条观看记录");
}
Logger.d("SyncCode: 观看记录合并完成");
return true;
}
return false;
} catch (Exception e) {
Logger.e("SyncCode: 观看记录下载失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 从GitHub Gist下载文件
*/
private String downloadGistFile(String fileUrl) {
try {
Response response = OkHttp.newCall(fileUrl).execute();
if (response.isSuccessful()) {
return response.body().string();
}
return "";
} catch (Exception e) {
Logger.e("SyncCode: 下载文件失败: " + e.getMessage());
return "";
}
}
/**
* 更新GitHub Gist文件
* 注意:GitHub Gist需要通过API更新,不能直接PUT文件
*/
private boolean updateGistFile(String fileUrl, String content) {
// GitHub Gist需要通过REST API更新
// 这里简化处理,实际需要调用GitHub Gist API
// POST https://api.github.com/gists/{gist_id}
if (TextUtils.isEmpty(gistToken)) {
Logger.w("SyncCode: 未提供GitHub Token,无法上传(Gist需要Token才能更新)");
// 可以提示用户:同步码模式需要GitHub Token才能上传
return false;
}
try {
// 构建GitHub Gist API请求
String apiUrl = "https://api.github.com/gists/" + gistId;
String filename = HISTORY_FILE_PREFIX + syncCode + FILE_SUFFIX;
// 构建请求体
Map<String, Object> requestBody = new HashMap<>();
Map<String, Object> files = new HashMap<>();
Map<String, String> fileContent = new HashMap<>();
fileContent.put("content", content);
files.put(filename, fileContent);
requestBody.put("files", files);
String jsonBody = App.gson().toJson(requestBody);
RequestBody body = RequestBody.create(
MediaType.parse("application/json; charset=utf-8"),
jsonBody
);
Request request = new Request.Builder()
.url(apiUrl)
.method("PATCH", body)
.header("Authorization", "Bearer " + gistToken)
.header("Accept", "application/vnd.github.v3+json")
.build();
Response response = OkHttp.client().newCall(request).execute();
boolean success = response.isSuccessful();
response.close();
return success;
} catch (Exception e) {
Logger.e("SyncCode: 更新Gist失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 同步观看记录
*/
public boolean syncHistory() {
if (!isConfigured()) {
return false;
}
App.execute(() -> {
uploadHistory();
downloadHistory();
});
return true;
}
/**
* 重新加载配置
*/
public void reloadConfig() {
loadConfig();
}
}
@@ -0,0 +1,150 @@
package com.fongmi.android.tv.utils;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import androidx.core.content.FileProvider;
import com.fongmi.android.tv.App;
import com.github.catvod.utils.Logger;
import java.io.File;
/**
* Android 更新安装器
* 处理安装权限检查和请求,以及APK安装
*/
public class UpdateInstaller {
private static UpdateInstaller instance;
private File pendingInstallFile; // 待安装的文件
public static UpdateInstaller get() {
if (instance == null) {
instance = new UpdateInstaller();
}
return instance;
}
/**
* 检查是否有安装权限
* @return 是否有安装权限
*/
public boolean hasInstallPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return App.get().getPackageManager().canRequestPackageInstalls();
}
return true; // Android 8.0以下不需要此权限
}
/**
* 请求安装权限(打开设置页面)
*/
public void requestInstallPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
intent.setData(Uri.parse("package:" + App.get().getPackageName()));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
App.get().startActivity(intent);
Logger.d("UpdateInstaller: 已打开安装权限设置页面");
} catch (Exception e) {
Logger.e("UpdateInstaller: 无法打开安装权限设置页面: " + e.getMessage());
e.printStackTrace();
}
}
}
/**
* 安装 APK 文件
* @param apkFile APK 文件
* @return 是否成功启动安装流程
*/
public boolean install(File apkFile) {
return install(apkFile, false);
}
/**
* 安装 APK 文件
* @param apkFile APK 文件
* @param checkPermission 是否检查权限(如果为false,即使没有权限也会尝试安装)
* @return 是否成功启动安装流程
*/
public boolean install(File apkFile, boolean checkPermission) {
try {
// Android 8.0+ 需要请求安装权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (checkPermission && !hasInstallPermission()) {
// 没有权限,保存待安装的文件,返回 false,由调用方处理
this.pendingInstallFile = apkFile;
Logger.d("UpdateInstaller: 没有安装权限,已保存待安装文件: " + apkFile.getAbsolutePath());
return false; // 返回false表示需要权限,但不表示失败
}
}
// 检查文件是否存在
if (!apkFile.exists() || !apkFile.isFile()) {
Logger.e("UpdateInstaller: APK文件不存在或不是文件: " + apkFile.getAbsolutePath());
return false;
}
// 使用 FileProvider 获取 URI
String authority = App.get().getPackageName() + ".provider";
Uri apkUri = FileProvider.getUriForFile(App.get(), authority, apkFile);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
App.get().startActivity(intent);
Logger.d("UpdateInstaller: 已启动安装程序");
this.pendingInstallFile = null; // 清除待安装文件
return true;
} catch (Exception e) {
Logger.e("UpdateInstaller: 安装失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 获取待安装的文件
* @return 待安装的文件,如果没有则返回null
*/
public File getPendingInstallFile() {
return pendingInstallFile;
}
/**
* 检查是否有待安装的文件且权限已授予
* 用于应用恢复时自动检测
*/
public boolean hasPendingInstall() {
return pendingInstallFile != null && pendingInstallFile.exists() && hasInstallPermission();
}
/**
* 自动重试安装(用于应用恢复时)
*/
public boolean autoRetryInstall() {
if (hasPendingInstall()) {
File file = pendingInstallFile;
pendingInstallFile = null; // 清除待安装文件
return install(file, false); // 不检查权限,因为已经检查过了
}
return false;
}
/**
* 清除待安装的文件
*/
public void clearPendingInstall() {
this.pendingInstallFile = null;
}
}
@@ -0,0 +1,759 @@
package com.fongmi.android.tv.utils;
import android.text.TextUtils;
import com.fongmi.android.tv.App;
import com.fongmi.android.tv.bean.Backup;
import com.fongmi.android.tv.bean.History;
import com.fongmi.android.tv.db.AppDatabase;
import com.github.catvod.utils.Logger;
import com.github.catvod.utils.Prefers;
import com.google.gson.Gson;
import com.thegrizzlylabs.sardineandroid.Sardine;
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import com.google.gson.reflect.TypeToken;
import com.fongmi.android.tv.Setting;
/**
* WebDAV同步管理器
* 用于同步观看记录和设置到WebDAV服务器
*/
public class WebDAVSyncManager {
private static final String HISTORY_FILE = "xmbox_history.json";
private static final String SETTINGS_FILE = "xmbox_settings.json";
private static final String BACKUP_FILE = "xmbox_backup.json";
// 同步模式:ACCOUNT(账号模式)或 CODE(同步码模式)
public enum SyncMode {
ACCOUNT, // 使用WebDAV账号
CODE // 使用同步码(无需账号)
}
private static WebDAVSyncManager instance;
private Sardine sardine;
private String baseUrl;
private String username;
private String password;
private String syncCode; // 同步码
private SyncMode syncMode = SyncMode.ACCOUNT; // 默认使用账号模式
private volatile boolean isSyncing = false; // 同步锁,防止重复同步
public static WebDAVSyncManager get() {
if (instance == null) {
instance = new WebDAVSyncManager();
}
return instance;
}
private WebDAVSyncManager() {
loadConfig();
}
/**
* 加载WebDAV配置
*/
private void loadConfig() {
// 检查同步模式
String modeStr = Setting.getWebDAVSyncMode();
if ("CODE".equals(modeStr)) {
syncMode = SyncMode.CODE;
syncCode = Setting.getWebDAVSyncCode();
// 同步码模式:使用公开的WebDAV服务器(如jsDelivr CDN的GitHub仓库)
// 或者使用其他公开存储服务
baseUrl = getPublicStorageUrl();
username = null;
password = null;
} else {
syncMode = SyncMode.ACCOUNT;
baseUrl = Setting.getWebDAVUrl();
username = Setting.getWebDAVUsername();
password = Setting.getWebDAVPassword();
}
if (syncMode == SyncMode.ACCOUNT) {
// 账号模式:需要账号密码
if (!TextUtils.isEmpty(baseUrl) && !TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) {
try {
sardine = new OkHttpSardine();
sardine.setCredentials(username, password);
Logger.d("WebDAV: 账号模式配置已加载");
} catch (Exception e) {
Logger.e("WebDAV: 初始化失败: " + e.getMessage());
sardine = null;
}
} else {
sardine = null;
}
} else {
// 同步码模式:使用公开存储,无需认证
if (!TextUtils.isEmpty(syncCode) && !TextUtils.isEmpty(baseUrl)) {
try {
sardine = new OkHttpSardine();
// 公开存储不需要认证
Logger.d("WebDAV: 同步码模式配置已加载,同步码: " + syncCode);
} catch (Exception e) {
Logger.e("WebDAV: 初始化失败: " + e.getMessage());
sardine = null;
}
} else {
sardine = null;
}
}
}
/**
* 获取公开存储URL(同步码模式使用)
* 方案:使用GitHub Gist作为公开存储
* 用户需要:
* 1. 创建一个GitHub Gist(公开)
* 2. 获取Gist的raw URL
* 3. 输入同步码
*
* 文件路径格式:{gist_raw_url}/{syncCode}/xmbox_history.json
*/
private String getPublicStorageUrl() {
// 获取用户配置的GitHub Gist raw URL
// 例如:https://gist.githubusercontent.com/username/gist_id/raw/
String gistBaseUrl = Setting.getWebDAVPublicUrl();
if (TextUtils.isEmpty(gistBaseUrl)) {
// 如果没有配置,返回null(需要用户配置)
return null;
}
// 将同步码添加到路径中,作为子目录
// 例如:https://gist.githubusercontent.com/username/gist_id/raw/ABC123XYZ/
if (!TextUtils.isEmpty(syncCode)) {
String url = gistBaseUrl.endsWith("/") ? gistBaseUrl : gistBaseUrl + "/";
return url + syncCode + "/";
}
return gistBaseUrl;
}
/**
* 检查WebDAV是否已配置
*/
public boolean isConfigured() {
if (syncMode == SyncMode.CODE) {
// 同步码模式:需要同步码和公开存储URL
return sardine != null && !TextUtils.isEmpty(baseUrl) && !TextUtils.isEmpty(syncCode);
} else {
// 账号模式:需要账号密码和URL
return sardine != null && !TextUtils.isEmpty(baseUrl) && !TextUtils.isEmpty(username) && !TextUtils.isEmpty(password);
}
}
/**
* 生成同步码
* @return 8位随机同步码(字母+数字)
*/
public static String generateSyncCode() {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
java.util.Random random = new java.util.Random();
StringBuilder code = new StringBuilder();
for (int i = 0; i < 8; i++) {
code.append(chars.charAt(random.nextInt(chars.length())));
}
return code.toString();
}
/**
* 测试WebDAV连接
* @return 测试结果,包含成功状态和错误信息
*/
public TestResult testConnectionWithMessage() {
if (!isConfigured()) {
return new TestResult(false, "WebDAV未配置,请检查URL、用户名和密码");
}
try {
// 确保baseUrl以/结尾
String testUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
Logger.d("WebDAV: 测试连接URL: " + testUrl);
Logger.d("WebDAV: 用户名: " + (username != null ? username : "null"));
// 尝试列出目录
sardine.list(testUrl);
Logger.d("WebDAV: 连接测试成功,可以访问目录");
return new TestResult(true, "连接成功!");
} catch (java.io.IOException e) {
String errorMsg = e.getMessage();
Logger.e("WebDAV: 连接测试失败: " + errorMsg);
Logger.e("WebDAV: 异常类型: " + e.getClass().getName());
e.printStackTrace();
// 根据错误类型提供更详细的提示
if (errorMsg != null) {
if (errorMsg.contains("401") || errorMsg.contains("Unauthorized")) {
return new TestResult(false, "认证失败:用户名或密码错误,请检查账号密码。\n提示:坚果云需要使用应用密码,不是登录密码");
} else if (errorMsg.contains("403") || errorMsg.contains("Forbidden")) {
return new TestResult(false, "访问被拒绝:账号可能没有WebDAV权限");
} else if (errorMsg.contains("404") || errorMsg.contains("Not Found")) {
return new TestResult(false, "URL不存在:请检查WebDAV服务器地址是否正确");
} else if (errorMsg.contains("SSL") || errorMsg.contains("Certificate")) {
return new TestResult(false, "SSL证书错误:请检查服务器证书是否有效");
} else if (errorMsg.contains("timeout") || errorMsg.contains("Timeout")) {
return new TestResult(false, "连接超时:请检查网络连接或服务器地址");
} else if (errorMsg.contains("UnknownHost") || errorMsg.contains("unreachable")) {
return new TestResult(false, "无法连接到服务器:请检查网络连接和服务器地址");
}
}
return new TestResult(false, "连接失败:" + (errorMsg != null ? errorMsg : "未知错误"));
} catch (Exception e) {
String errorMsg = e.getMessage();
Logger.e("WebDAV: 连接测试失败: " + errorMsg);
Logger.e("WebDAV: 异常类型: " + e.getClass().getName());
e.printStackTrace();
return new TestResult(false, "连接失败:" + (errorMsg != null ? errorMsg : e.getClass().getSimpleName()));
}
}
/**
* 测试WebDAV连接(兼容旧接口)
*/
public boolean testConnection() {
return testConnectionWithMessage().success;
}
/**
* 测试结果类
*/
public static class TestResult {
public final boolean success;
public final String message;
public TestResult(boolean success, String message) {
this.success = success;
this.message = message;
}
}
/**
* 确保目录存在
*/
private void ensureDirectory(String path) throws Exception {
try {
if (!sardine.exists(path)) {
sardine.createDirectory(path);
Logger.d("WebDAV: 创建目录: " + path);
}
} catch (Exception e) {
Logger.e("WebDAV: 创建目录失败: " + e.getMessage());
throw e;
}
}
/**
* 获取文件完整URL
*/
private String getFileUrl(String filename) {
if (syncMode == SyncMode.CODE) {
// 同步码模式:使用GitHub Gist raw URL
// 格式:https://gist.githubusercontent.com/username/gist_id/raw/{syncCode}/{filename}
String url = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
return url + filename;
} else {
// 账号模式:使用WebDAV URL
// 对于坚果云:https://dav.jianguoyun.com/dav/xmbox_history.json
// 确保 baseUrl 以 / 结尾
String url = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
return url + filename;
}
}
/**
* 上传观看记录
*/
public boolean uploadHistory() {
if (!isConfigured()) {
Logger.e("WebDAV: 未配置,无法上传观看记录");
return false;
}
try {
// 获取所有观看记录
List<History> historyList = AppDatabase.get().getHistoryDao().findAll();
if (historyList == null) {
historyList = new java.util.ArrayList<>();
}
Logger.d("WebDAV: 准备上传观看记录,共 " + historyList.size() + "");
String json = App.gson().toJson(historyList);
if (TextUtils.isEmpty(json)) {
Logger.w("WebDAV: JSON数据为空");
json = "[]"; // 确保至少有一个有效的JSON数组
}
// 确保目录存在(如果baseUrl包含子目录)
if (syncMode == SyncMode.ACCOUNT && !TextUtils.isEmpty(baseUrl)) {
try {
String dirUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
ensureDirectory(dirUrl);
} catch (Exception e) {
Logger.w("WebDAV: 创建目录失败,尝试继续上传: " + e.getMessage());
// 继续尝试上传,某些WebDAV服务可能不需要预先创建目录
}
}
// 上传文件
String fileUrl = getFileUrl(HISTORY_FILE);
Logger.d("WebDAV: 上传文件URL: " + fileUrl);
Logger.d("WebDAV: 上传数据大小: " + json.length() + " 字节");
byte[] data = json.getBytes("UTF-8");
// 对于坚果云等WebDAV服务,直接上传文件即可(会自动创建文件)
// 如果文件已存在,会被覆盖
sardine.put(fileUrl, data);
// 验证上传是否成功:检查文件是否存在
if (sardine.exists(fileUrl)) {
Logger.d("WebDAV: 观看记录上传成功,共 " + historyList.size() + " 条,文件已确认存在");
return true;
} else {
Logger.e("WebDAV: 上传后文件不存在,可能上传失败");
return false;
}
} catch (Exception e) {
Logger.e("WebDAV: 观看记录上传失败: " + e.getMessage());
Logger.e("WebDAV: 异常类型: " + e.getClass().getName());
e.printStackTrace();
return false;
}
}
/**
* 下载观看记录
*/
public boolean downloadHistory() {
if (!isConfigured()) {
Logger.e("WebDAV: 未配置,无法下载观看记录");
return false;
}
try {
String fileUrl = getFileUrl(HISTORY_FILE);
// 检查文件是否存在
if (!sardine.exists(fileUrl)) {
Logger.d("WebDAV: 观看记录文件不存在,跳过下载");
return false;
}
// 下载文件(使用循环读取,避免available()不准确的问题)
InputStream is = sardine.get(fileUrl);
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
is.close();
byte[] data = baos.toByteArray();
baos.close();
String json = new String(data, "UTF-8");
if (TextUtils.isEmpty(json)) {
Logger.d("WebDAV: 观看记录文件为空");
return true; // 文件存在但为空,也算同步成功
}
Type listType = new TypeToken<List<History>>(){}.getType();
List<History> remoteHistoryList = App.gson().fromJson(json, listType);
// 验证数据
if (remoteHistoryList == null) {
Logger.e("WebDAV: JSON解析失败,返回null");
return false;
}
// 智能合并:比较本地和远程记录,保留较新的
List<History> localHistoryList = AppDatabase.get().getHistoryDao().findAll();
// 创建本地记录的映射(key -> History
java.util.Map<String, History> localMap = new java.util.HashMap<>();
for (History local : localHistoryList) {
if (local != null && local.getKey() != null) {
localMap.put(local.getKey(), local);
}
}
// 合并远程记录
List<History> toInsert = new java.util.ArrayList<>();
List<History> toUpdate = new java.util.ArrayList<>();
for (History remote : remoteHistoryList) {
// 验证远程记录
if (remote == null || TextUtils.isEmpty(remote.getKey())) {
Logger.w("WebDAV: 跳过无效的远程记录(key为空)");
continue;
}
History local = localMap.get(remote.getKey());
if (local == null) {
// 本地没有,直接添加
toInsert.add(remote);
} else {
// 本地有,比较createTime,保留较新的
if (remote.getCreateTime() > local.getCreateTime()) {
// 远程更新,更新本地
toUpdate.add(remote);
} else if (remote.getCreateTime() == local.getCreateTime()) {
// 时间相同,比较position,保留进度更靠后的
// 注意:position可能是C.TIME_UNSET(负数),需要处理
long remotePos = remote.getPosition();
long localPos = local.getPosition();
// 如果都是有效值(>=0),比较大小;如果有无效值,保留有效值
if (remotePos >= 0 && localPos >= 0) {
if (remotePos > localPos) {
toUpdate.add(remote);
}
} else if (remotePos >= 0 && localPos < 0) {
// 远程有效,本地无效,更新
toUpdate.add(remote);
}
// 否则保留本地,不更新
}
// 否则保留本地,不更新
}
}
// 执行插入和更新
if (!toInsert.isEmpty()) {
AppDatabase.get().getHistoryDao().insert(toInsert);
Logger.d("WebDAV: 新增 " + toInsert.size() + " 条观看记录");
}
if (!toUpdate.isEmpty()) {
AppDatabase.get().getHistoryDao().update(toUpdate);
Logger.d("WebDAV: 更新 " + toUpdate.size() + " 条观看记录");
}
Logger.d("WebDAV: 观看记录合并完成,远程 " + remoteHistoryList.size() + " 条,本地 " + localHistoryList.size() + "");
return true; // 即使远程为空,也算同步成功
} catch (Exception e) {
Logger.e("WebDAV: 观看记录下载失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 上传设置
*/
public boolean uploadSettings() {
if (!isConfigured()) {
Logger.e("WebDAV: 未配置,无法上传设置");
return false;
}
try {
// 获取所有设置
Map<String, ?> allPrefs = Prefers.getPrefers().getAll();
String json = App.gson().toJson(allPrefs);
// 确保目录存在(如果baseUrl包含子目录)
if (syncMode == SyncMode.ACCOUNT && !TextUtils.isEmpty(baseUrl)) {
try {
String dirUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
ensureDirectory(dirUrl);
} catch (Exception e) {
Logger.w("WebDAV: 创建目录失败,尝试继续上传: " + e.getMessage());
}
}
// 上传文件
String fileUrl = getFileUrl(SETTINGS_FILE);
byte[] data = json.getBytes("UTF-8");
sardine.put(fileUrl, data);
Logger.d("WebDAV: 设置上传成功");
return true;
} catch (Exception e) {
Logger.e("WebDAV: 设置上传失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 下载设置
*/
public boolean downloadSettings() {
if (!isConfigured()) {
Logger.e("WebDAV: 未配置,无法下载设置");
return false;
}
try {
String fileUrl = getFileUrl(SETTINGS_FILE);
// 检查文件是否存在
if (!sardine.exists(fileUrl)) {
Logger.d("WebDAV: 设置文件不存在,跳过下载");
return false;
}
// 下载文件
InputStream is = sardine.get(fileUrl);
byte[] buffer = new byte[is.available()];
is.read(buffer);
is.close();
String json = new String(buffer, "UTF-8");
Gson gson = App.gson();
Map<String, Object> settings = gson.fromJson(json, Map.class);
// 应用设置(合并,不覆盖已存在的)
if (settings != null && !settings.isEmpty()) {
for (Map.Entry<String, Object> entry : settings.entrySet()) {
// 只同步非敏感设置,跳过某些本地设置
String key = entry.getKey();
if (!shouldSkipSetting(key)) {
Prefers.put(key, entry.getValue());
}
}
Logger.d("WebDAV: 设置下载成功");
return true;
}
return false;
} catch (Exception e) {
Logger.e("WebDAV: 设置下载失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 判断是否应该跳过某个设置项
*/
private boolean shouldSkipSetting(String key) {
// 跳过WebDAV相关设置,避免循环同步
if (key.startsWith("webdav_")) {
return true;
}
// 跳过设备特定设置
if (key.equals("device_uuid") || key.equals("device_name")) {
return true;
}
return false;
}
/**
* 上传完整备份(包含所有数据)
*/
public boolean uploadBackup() {
if (!isConfigured()) {
Logger.e("WebDAV: 未配置,无法上传备份");
return false;
}
try {
Backup backup = Backup.create();
String json = backup.toString();
// 确保目录存在(如果baseUrl包含子目录)
if (syncMode == SyncMode.ACCOUNT && !TextUtils.isEmpty(baseUrl)) {
try {
String dirUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
ensureDirectory(dirUrl);
} catch (Exception e) {
Logger.w("WebDAV: 创建目录失败,尝试继续上传: " + e.getMessage());
}
}
// 上传文件
String fileUrl = getFileUrl(BACKUP_FILE);
byte[] data = json.getBytes("UTF-8");
sardine.put(fileUrl, data);
Logger.d("WebDAV: 完整备份上传成功");
return true;
} catch (Exception e) {
Logger.e("WebDAV: 完整备份上传失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 下载完整备份
*/
public boolean downloadBackup() {
if (!isConfigured()) {
Logger.e("WebDAV: 未配置,无法下载备份");
return false;
}
try {
String fileUrl = getFileUrl(BACKUP_FILE);
// 检查文件是否存在
if (!sardine.exists(fileUrl)) {
Logger.d("WebDAV: 备份文件不存在,跳过下载");
return false;
}
// 下载文件
InputStream is = sardine.get(fileUrl);
byte[] buffer = new byte[is.available()];
is.read(buffer);
is.close();
String json = new String(buffer, "UTF-8");
Backup backup = Backup.objectFrom(json);
// 恢复备份
if (!backup.getConfig().isEmpty()) {
backup.restore();
Logger.d("WebDAV: 完整备份下载并恢复成功");
return true;
}
return false;
} catch (Exception e) {
Logger.e("WebDAV: 完整备份下载失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 同步观看记录(上传+下载合并)
* @param async 是否异步执行,true=异步,false=同步(阻塞)
*/
public boolean syncHistory(boolean async) {
if (!isConfigured()) {
return false;
}
// 防止重复同步
if (isSyncing) {
Logger.w("WebDAV: 同步正在进行中,跳过本次请求");
return false;
}
Runnable syncTask = () -> {
try {
isSyncing = true;
// 先上传本地记录
uploadHistory();
// 再下载远程记录并合并
downloadHistory();
} finally {
isSyncing = false;
}
};
if (async) {
App.execute(syncTask);
} else {
syncTask.run();
}
return true;
}
/**
* 同步观看记录(异步执行,默认)
*/
public boolean syncHistory() {
return syncHistory(true);
}
/**
* 同步设置(上传+下载合并)
* @param async 是否异步执行
*/
public boolean syncSettings(boolean async) {
if (!isConfigured()) {
return false;
}
Runnable syncTask = () -> {
// 先上传本地设置
uploadSettings();
// 再下载远程设置并合并
downloadSettings();
};
if (async) {
App.execute(syncTask);
} else {
syncTask.run();
}
return true;
}
/**
* 同步设置(异步执行,默认)
*/
public boolean syncSettings() {
return syncSettings(true);
}
/**
* 完整同步(观看记录+设置)
* @param async 是否异步执行
*/
public boolean syncAll(boolean async) {
if (!isConfigured()) {
return false;
}
// 防止重复同步
if (isSyncing) {
Logger.w("WebDAV: 同步正在进行中,跳过本次请求");
return false;
}
Runnable syncTask = () -> {
try {
isSyncing = true;
// 先上传本地记录
uploadHistory();
// 再下载远程记录并合并
downloadHistory();
// 同步设置
syncSettings(false); // 设置同步使用同步方式,避免嵌套异步
} finally {
isSyncing = false;
}
};
if (async) {
App.execute(syncTask);
} else {
syncTask.run();
}
return true;
}
/**
* 完整同步(异步执行,默认)
*/
public boolean syncAll() {
return syncAll(true);
}
/**
* 重新加载配置(配置更改后调用)
*/
public void reloadConfig() {
loadConfig();
}
}
+1 -1
View File
@@ -91,7 +91,7 @@
<string name="setting_app">应用设置</string>
<string name="setting_network">网络设置</string>
<string name="setting_data">数据管理</string>
<string name="app_version">v3.0.9</string>
<string name="app_version">v3.1.1</string>
<string name="about_github">在GitHub上查看</string>
<!-- Backup & Restore -->
+1 -1
View File
@@ -89,7 +89,7 @@
<string name="setting_app">應用設置</string>
<string name="setting_network">網絡設置</string>
<string name="setting_data">數據管理</string>
<string name="app_version">v3.0.9</string>
<string name="app_version">v3.1.1</string>
<string name="about_github">在GitHub上查看</string>
<!-- Backup & Restore -->
+1 -1
View File
@@ -92,7 +92,7 @@
<string name="setting_choose">Choose</string>
<string name="setting_off">Off</string>
<string name="setting_on">On</string>
<string name="app_version">v3.0.9</string>
<string name="app_version">v3.1.1</string>
<string name="about_github">View on GitHub</string>
<!-- Backup & Restore -->