|
|
|
|
@ -15,16 +15,29 @@ import androidx.work.WorkerParameters;
|
|
|
|
|
import com.google.firebase.auth.FirebaseAuth;
|
|
|
|
|
import com.google.firebase.firestore.FirebaseFirestore;
|
|
|
|
|
import com.google.firebase.firestore.QueryDocumentSnapshot;
|
|
|
|
|
import com.google.gson.JsonObject;
|
|
|
|
|
|
|
|
|
|
import net.micode.notes.data.Notes;
|
|
|
|
|
import net.micode.notes.data.Notes.NoteColumns;
|
|
|
|
|
import net.micode.notes.model.CloudNote;
|
|
|
|
|
import net.micode.notes.tool.AiNotificationHelper;
|
|
|
|
|
import net.micode.notes.tool.SyncMapper;
|
|
|
|
|
import net.micode.notes.tool.ai.AiDataSyncHelper;
|
|
|
|
|
import net.micode.notes.tool.ai.CozeClient;
|
|
|
|
|
import net.micode.notes.tool.ai.CozeRequest;
|
|
|
|
|
import net.micode.notes.tool.ai.CozeResponse;
|
|
|
|
|
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
import java.util.concurrent.CountDownLatch;
|
|
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
|
|
|
|
public class SyncWorker extends Worker {
|
|
|
|
|
private static final String TAG = "SyncWorker";
|
|
|
|
|
public static final String KEY_SYNC_MODE = "sync_mode";
|
|
|
|
|
public static final int MODE_ALL = 0; // 默认:全量同步
|
|
|
|
|
public static final int MODE_PUSH = 1; // 仅上传
|
|
|
|
|
public static final int MODE_PULL = 2; // 仅拉取
|
|
|
|
|
public static final int MODE_REMINDER = 3;
|
|
|
|
|
|
|
|
|
|
public SyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
|
|
|
|
super(context, workerParams);
|
|
|
|
|
@ -33,34 +46,209 @@ public class SyncWorker extends Worker {
|
|
|
|
|
@NonNull
|
|
|
|
|
@Override
|
|
|
|
|
public Result doWork() {
|
|
|
|
|
// [确切位置]:在方法入口处立即进行安全隔离
|
|
|
|
|
// 准备一个锁,数量为 1
|
|
|
|
|
final CountDownLatch latch = new CountDownLatch(1);
|
|
|
|
|
// 用于记录是否发生错误
|
|
|
|
|
final boolean[] isSuccess = {true};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
String uid = FirebaseAuth.getInstance().getUid();
|
|
|
|
|
if (uid == null) {
|
|
|
|
|
Log.w(TAG, "Sync skipped: No user logged in.");
|
|
|
|
|
return Result.success();
|
|
|
|
|
}
|
|
|
|
|
if (uid == null) return Result.success();
|
|
|
|
|
|
|
|
|
|
FirebaseFirestore db = FirebaseFirestore.getInstance();
|
|
|
|
|
int mode = getInputData().getInt(KEY_SYNC_MODE, MODE_ALL);
|
|
|
|
|
|
|
|
|
|
// 1. 先执行上行同步
|
|
|
|
|
performPush(db, uid);
|
|
|
|
|
Log.d(TAG, "Starting Sync with mode: " + mode);
|
|
|
|
|
|
|
|
|
|
// --- PUSH 逻辑 (保持同步执行即可,因为它本身不依赖回调返回数据给UI) ---
|
|
|
|
|
if (mode == MODE_PUSH || mode == MODE_ALL) {
|
|
|
|
|
performPush(db, uid);
|
|
|
|
|
syncToCozeAgent();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- PULL 逻辑 (异步变同步) ---
|
|
|
|
|
if (mode == MODE_PULL || mode == MODE_ALL) {
|
|
|
|
|
// 传递 latch 进去
|
|
|
|
|
performPull(db, uid, latch, isSuccess);
|
|
|
|
|
|
|
|
|
|
// [关键]:主线程在这里死等,直到 latch.countDown() 被调用,或者超时(10秒)
|
|
|
|
|
try {
|
|
|
|
|
latch.await(10, TimeUnit.SECONDS);
|
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
return Result.retry();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 后执行下行同步
|
|
|
|
|
performPull(db, uid);
|
|
|
|
|
// 2. 在 doWork() 中增加判断
|
|
|
|
|
if (mode == MODE_REMINDER) {
|
|
|
|
|
String title = getInputData().getString("reminder_title");
|
|
|
|
|
requestAiReminderReply(title);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Result.success();
|
|
|
|
|
return isSuccess[0] ? Result.success() : Result.failure();
|
|
|
|
|
|
|
|
|
|
} catch (SecurityException e) {
|
|
|
|
|
// [核心修复]:捕获 "Unknown calling package name 'com.google.android.gms'"
|
|
|
|
|
Log.e(TAG, "GMS Security Exception caught, retrying later: " + e.getMessage());
|
|
|
|
|
return Result.retry(); // 告诉系统稍后重试,不要直接闪退进程
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
Log.e(TAG, "General sync error: " + e.getMessage());
|
|
|
|
|
return Result.failure();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 实现提醒请求逻辑
|
|
|
|
|
private void requestAiReminderReply(String agendaTitle) {
|
|
|
|
|
try {
|
|
|
|
|
// 1. 构造请求参数
|
|
|
|
|
com.google.gson.JsonObject json = new com.google.gson.JsonObject();
|
|
|
|
|
json.addProperty("intent", "reminder");
|
|
|
|
|
json.addProperty("payload", "日程即将开始:" + agendaTitle);
|
|
|
|
|
json.addProperty("current_time", AiDataSyncHelper.getCurrentTime());
|
|
|
|
|
|
|
|
|
|
// 2. 发起对话
|
|
|
|
|
CozeRequest request = new CozeRequest(CozeClient.BOT_ID, "user_demo", json.toString());
|
|
|
|
|
retrofit2.Response<CozeResponse> response = CozeClient.getInstance()
|
|
|
|
|
.chat(CozeClient.getAuthToken(), request).execute();
|
|
|
|
|
|
|
|
|
|
// 声明变量,用于存放最终结果
|
|
|
|
|
String finalAnswer = null;
|
|
|
|
|
|
|
|
|
|
if (response.isSuccessful() && response.body() != null && response.body().data != null) {
|
|
|
|
|
String chatId = response.body().data.id;
|
|
|
|
|
String convId = response.body().data.conversation_id;
|
|
|
|
|
|
|
|
|
|
// 3. 轮询状态直到完成
|
|
|
|
|
String status = "";
|
|
|
|
|
int retries = 0;
|
|
|
|
|
while (!"completed".equals(status) && retries < 15) {
|
|
|
|
|
Thread.sleep(2000);
|
|
|
|
|
retrofit2.Response<CozeResponse> pollResp = CozeClient.getInstance()
|
|
|
|
|
.retrieveChat(CozeClient.getAuthToken(), chatId, convId).execute();
|
|
|
|
|
|
|
|
|
|
if (pollResp.isSuccessful() && pollResp.body() != null && pollResp.body().data != null) {
|
|
|
|
|
status = pollResp.body().data.status;
|
|
|
|
|
}
|
|
|
|
|
retries++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. [核心修复点]:状态完成后,请求消息列表并解析出 content
|
|
|
|
|
if ("completed".equals(status)) {
|
|
|
|
|
retrofit2.Response<com.google.gson.JsonObject> msgListResp = CozeClient.getInstance()
|
|
|
|
|
.getMessageList(CozeClient.getAuthToken(), chatId, convId).execute();
|
|
|
|
|
|
|
|
|
|
if (msgListResp.isSuccessful() && msgListResp.body() != null) {
|
|
|
|
|
com.google.gson.JsonArray messages = msgListResp.body().getAsJsonArray("data");
|
|
|
|
|
|
|
|
|
|
// 遍历寻找 AI 的回答内容
|
|
|
|
|
for (com.google.gson.JsonElement el : messages) {
|
|
|
|
|
com.google.gson.JsonObject m = el.getAsJsonObject();
|
|
|
|
|
if ("assistant".equals(m.get("role").getAsString()) &&
|
|
|
|
|
"answer".equals(m.get("type").getAsString())) {
|
|
|
|
|
finalAnswer = m.get("content").getAsString();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. 统一处理最终结果
|
|
|
|
|
if (finalAnswer != null && !finalAnswer.isEmpty()) {
|
|
|
|
|
saveReminderToLocal(finalAnswer);
|
|
|
|
|
AiNotificationHelper.sendAiNotification(getApplicationContext(), finalAnswer);
|
|
|
|
|
Log.d("AiReminder", "Successfully got AI reminder: " + finalAnswer);
|
|
|
|
|
} else {
|
|
|
|
|
Log.w("AiReminder", "AI returned empty answer for reminder.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
Log.e("AiReminder", "Error in requestAiReminderReply", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void saveReminderToLocal(String content) {
|
|
|
|
|
android.content.ContentValues values = new android.content.ContentValues();
|
|
|
|
|
values.put(Notes.ChatColumns.SENDER_TYPE, 1); // AI
|
|
|
|
|
values.put(Notes.ChatColumns.MSG_TYPE, 1); // [重要] 设为提醒卡片类型
|
|
|
|
|
values.put(Notes.ChatColumns.CONTENT, content);
|
|
|
|
|
values.put(Notes.ChatColumns.CREATED_AT, System.currentTimeMillis());
|
|
|
|
|
getApplicationContext().getContentResolver().insert(
|
|
|
|
|
android.net.Uri.parse("content://micode_notes/chat_messages"), values);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 在 SyncWorker 类中添加
|
|
|
|
|
private void syncToCozeAgent() {
|
|
|
|
|
try {
|
|
|
|
|
com.google.gson.JsonObject payloadJson = new com.google.gson.JsonObject();
|
|
|
|
|
payloadJson.addProperty("intent", "sync");
|
|
|
|
|
payloadJson.addProperty("payload", AiDataSyncHelper.getSyncPayload(getApplicationContext()));
|
|
|
|
|
payloadJson.addProperty("current_time", AiDataSyncHelper.getCurrentTime());
|
|
|
|
|
|
|
|
|
|
CozeRequest request = new CozeRequest(CozeClient.BOT_ID, "user_demo", payloadJson.toString());
|
|
|
|
|
|
|
|
|
|
// 1. 发起对话
|
|
|
|
|
retrofit2.Response<CozeResponse> response = CozeClient.getInstance()
|
|
|
|
|
.chat(CozeClient.getAuthToken(), request).execute();
|
|
|
|
|
|
|
|
|
|
if (response.isSuccessful() && response.body() != null && response.body().data != null) {
|
|
|
|
|
String chatId = response.body().data.id;
|
|
|
|
|
String convId = response.body().data.conversation_id; // [关键] 获取会话ID
|
|
|
|
|
|
|
|
|
|
String status = "";
|
|
|
|
|
int retries = 0;
|
|
|
|
|
while (!"completed".equals(status) && retries < 15) {
|
|
|
|
|
Thread.sleep(2000);
|
|
|
|
|
// [关键] 传入两个 ID 轮询
|
|
|
|
|
retrofit2.Response<CozeResponse> pollResp = CozeClient.getInstance()
|
|
|
|
|
.retrieveChat(CozeClient.getAuthToken(), chatId, convId).execute();
|
|
|
|
|
|
|
|
|
|
if (pollResp.isSuccessful() && pollResp.body() != null && pollResp.body().data != null) {
|
|
|
|
|
status = pollResp.body().data.status;
|
|
|
|
|
}
|
|
|
|
|
retries++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 完成后抓取真正的“Answer”
|
|
|
|
|
if ("completed".equals(status)) {
|
|
|
|
|
retrofit2.Response<com.google.gson.JsonObject> msgListResp = CozeClient.getInstance()
|
|
|
|
|
.getMessageList(CozeClient.getAuthToken(), chatId, convId).execute();
|
|
|
|
|
|
|
|
|
|
if (msgListResp.isSuccessful() && msgListResp.body() != null) {
|
|
|
|
|
com.google.gson.JsonArray messages = msgListResp.body().getAsJsonArray("data");
|
|
|
|
|
String finalAnswer = null;
|
|
|
|
|
|
|
|
|
|
// 遍历寻找 role=assistant 且 type=answer 的内容
|
|
|
|
|
for (com.google.gson.JsonElement el : messages) {
|
|
|
|
|
com.google.gson.JsonObject m = el.getAsJsonObject();
|
|
|
|
|
if ("assistant".equals(m.get("role").getAsString()) &&
|
|
|
|
|
"answer".equals(m.get("type").getAsString())) {
|
|
|
|
|
finalAnswer = m.get("content").getAsString();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (finalAnswer != null && !finalAnswer.isEmpty()) {
|
|
|
|
|
// 插入本地数据库并弹通知
|
|
|
|
|
saveChatMessageToLocal(finalAnswer);
|
|
|
|
|
AiNotificationHelper.sendAiNotification(getApplicationContext(), finalAnswer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
Log.e("CozeSync", "Sync Failed", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 辅助方法:将 AI 消息存入本地 chat_messages 表
|
|
|
|
|
private void saveChatMessageToLocal(String content) {
|
|
|
|
|
android.content.ContentValues values = new android.content.ContentValues();
|
|
|
|
|
values.put(net.micode.notes.data.Notes.ChatColumns.SENDER_TYPE, 1); // AI
|
|
|
|
|
values.put(net.micode.notes.data.Notes.ChatColumns.MSG_TYPE, 0); // 文本
|
|
|
|
|
values.put(net.micode.notes.data.Notes.ChatColumns.CONTENT, content);
|
|
|
|
|
values.put(net.micode.notes.data.Notes.ChatColumns.CREATED_AT, System.currentTimeMillis());
|
|
|
|
|
getApplicationContext().getContentResolver().insert(
|
|
|
|
|
android.net.Uri.parse("content://micode_notes/chat_messages"), values);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* [上行逻辑]:查询本地所有 sync_state = 1 的记录并上传
|
|
|
|
|
*/
|
|
|
|
|
@ -114,19 +302,33 @@ public class SyncWorker extends Worker {
|
|
|
|
|
/**
|
|
|
|
|
* [下行逻辑]:拉取云端所有数据并合并
|
|
|
|
|
*/
|
|
|
|
|
private void performPull(FirebaseFirestore db, String uid) {
|
|
|
|
|
// 修改 performPull 方法签名,增加 latch 和 状态标记
|
|
|
|
|
private void performPull(FirebaseFirestore db, String uid, CountDownLatch latch, boolean[] isSuccess) {
|
|
|
|
|
db.collection("users").document(uid).collection("notes")
|
|
|
|
|
.get()
|
|
|
|
|
.addOnSuccessListener(queryDocumentSnapshots -> {
|
|
|
|
|
for (QueryDocumentSnapshot doc : queryDocumentSnapshots) {
|
|
|
|
|
CloudNote cloudNote = doc.toObject(CloudNote.class);
|
|
|
|
|
if (cloudNote != null) {
|
|
|
|
|
mergeCloudNoteToLocal(cloudNote);
|
|
|
|
|
try {
|
|
|
|
|
for (QueryDocumentSnapshot doc : queryDocumentSnapshots) {
|
|
|
|
|
CloudNote cloudNote = doc.toObject(CloudNote.class);
|
|
|
|
|
if (cloudNote != null) {
|
|
|
|
|
mergeCloudNoteToLocal(cloudNote);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Log.d(TAG, "Pull complete. Cloud docs processed.");
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
Log.e(TAG, "Error merging data", e);
|
|
|
|
|
isSuccess[0] = false;
|
|
|
|
|
} finally {
|
|
|
|
|
// [关键]:通知主线程,活干完了,可以放行了
|
|
|
|
|
latch.countDown();
|
|
|
|
|
}
|
|
|
|
|
Log.d(TAG, "Pull complete. Cloud docs processed.");
|
|
|
|
|
})
|
|
|
|
|
.addOnFailureListener(e -> Log.e(TAG, "Pull failed", e));
|
|
|
|
|
.addOnFailureListener(e -> {
|
|
|
|
|
Log.e(TAG, "Pull failed", e);
|
|
|
|
|
isSuccess[0] = false;
|
|
|
|
|
// [关键]:失败了也要通知放行,否则会死锁
|
|
|
|
|
latch.countDown();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|