diff --git a/src/res/layout/activity_main.xml b/src/res/layout/activity_main.xml
new file mode 100644
index 0000000..1a66676
--- /dev/null
+++ b/src/res/layout/activity_main.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/res/layout/fragment_mi_steward.xml b/src/res/layout/fragment_mi_steward.xml
new file mode 100644
index 0000000..bbcbcd3
--- /dev/null
+++ b/src/res/layout/fragment_mi_steward.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/res/layout/item_chat_ai.xml b/src/res/layout/item_chat_ai.xml
new file mode 100644
index 0000000..4c4c527
--- /dev/null
+++ b/src/res/layout/item_chat_ai.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/res/layout/item_chat_user.xml b/src/res/layout/item_chat_user.xml
new file mode 100644
index 0000000..313f0f3
--- /dev/null
+++ b/src/res/layout/item_chat_user.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/res/menu/bottom_nav_menu.xml b/src/res/menu/bottom_nav_menu.xml
new file mode 100644
index 0000000..df01352
--- /dev/null
+++ b/src/res/menu/bottom_nav_menu.xml
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/src/res/xml/file_paths.xml b/src/res/xml/file_paths.xml
new file mode 100644
index 0000000..f6a96e3
--- /dev/null
+++ b/src/res/xml/file_paths.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/src/net/micode/notes/ai/AIService.java b/src/src/net/micode/notes/ai/AIService.java
new file mode 100644
index 0000000..b7fd72a
--- /dev/null
+++ b/src/src/net/micode/notes/ai/AIService.java
@@ -0,0 +1,277 @@
+package net.micode.notes.ai;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/**
+ * AI 服务类
+ * 专门负责连接 DeepSeek 大模型
+ */
+public class AIService {
+
+ // ==========================================
+ // 请在这里填入您的 DeepSeek API Key
+ // ==========================================
+ private static final String API_KEY = "sk-e9644b634818458f8238fbd6c682ff41";
+
+ // DeepSeek 的官方接口地址
+ private static final String API_URL = "https://api.deepseek.com/chat/completions";
+ // 模型名称:deepseek-chat (目前指向 DeepSeek-V3)
+ private static final String MODEL_NAME = "deepseek-chat";
+
+ private static final String TAG = "AIService";
+
+ // 定义一个接口,用来通知界面:AI 是成功了还是失败了
+ public interface AIResultCallback {
+ void onSuccess(String result);
+ void onError(String error);
+ }
+
+ /**
+ * 发送请求给 DeepSeek 进行便签整理
+ * @param content 便签的原文
+ * @param callback 结果回调
+ */
+ public static void callDeepSeek(final String content, final AIResultCallback callback) {
+ // 开启一个新线程去联网,防止卡死主线程
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // 1. 准备连接
+ URL url = new URL(API_URL);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("POST");
+ conn.setRequestProperty("Authorization", "Bearer " + API_KEY);
+ conn.setRequestProperty("Content-Type", "application/json");
+ conn.setDoOutput(true);
+ conn.setConnectTimeout(10000); // 10秒超时
+ conn.setReadTimeout(30000); // 30秒读取超时
+
+ // 2. 准备发送给 AI 的数据 (JSON格式)
+ // 构造提示词 (Prompt)
+ String prompt = "请对以下便签内容进行整理、润色和排版,使其条理清晰,如果内容较短则进行扩写。直接输出结果,不要啰嗦:\n\n" + content;
+
+ JSONObject jsonBody = new JSONObject();
+ jsonBody.put("model", MODEL_NAME);
+
+ // 构建消息列表
+ JSONArray messages = new JSONArray();
+ JSONObject userMessage = new JSONObject();
+ userMessage.put("role", "user");
+ userMessage.put("content", prompt);
+ messages.put(userMessage);
+
+ jsonBody.put("messages", messages);
+ jsonBody.put("stream", false); // 不使用流式传输,一次性返回
+
+ // 3. 发送数据
+ try (OutputStream os = conn.getOutputStream()) {
+ byte[] input = jsonBody.toString().getBytes("utf-8");
+ os.write(input, 0, input.length);
+ }
+
+ // 4. 接收结果
+ int code = conn.getResponseCode();
+ if (code == 200) {
+ // 读取返回的数据
+ BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
+ StringBuilder response = new StringBuilder();
+ String responseLine;
+ while ((responseLine = br.readLine()) != null) {
+ response.append(responseLine.trim());
+ }
+
+ // 解析 DeepSeek 返回的 JSON
+ JSONObject responseJson = new JSONObject(response.toString());
+ // 提取核心回复内容
+ String aiText = responseJson.getJSONArray("choices")
+ .getJSONObject(0)
+ .getJSONObject("message")
+ .getString("content");
+
+ // 成功!通知界面 (切换回主线程)
+ runOnMainThread(callback, aiText, null);
+ } else {
+ // 失败
+ runOnMainThread(callback, null, "服务器错误: " + code);
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ runOnMainThread(callback, null, "网络异常: " + e.getMessage());
+ }
+ }
+ }).start();
+ }
+ /**
+ * AI 智能分类与标签生成
+ * 返回格式 JSON:{"color_id": 2, "tags": ["会议", "方案"]}
+ */
+ public static void classifyNote(final String content, final AIResultCallback callback) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ URL url = new URL(API_URL);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("POST");
+ conn.setRequestProperty("Authorization", "Bearer " + API_KEY);
+ conn.setRequestProperty("Content-Type", "application/json");
+ conn.setDoOutput(true);
+
+ // 构造提示词:这是最关键的一步!
+ // 我们告诉 AI 颜色的 ID 对应关系,让它直接返回 ID
+ String prompt = "你是一个便签整理助手。请分析以下便签内容,完成两件事:\n" +
+ "1. 从以下分类中选择最匹配的一个,并返回对应的 color_id:\n" +
+ " - 生活/记事: 0\n" +
+ " - 紧急/重要: 1\n" +
+ " - 工作/学习: 2\n" +
+ " - 旅行/规划: 3\n" +
+ " - 灵感/创作: 4\n" +
+ "2. 提炼 2 个简短的关键词标签(每个不超过4个字)。\n" +
+ "\n" +
+ "便签内容:" + content + "\n" +
+ "\n" +
+ "请务必只返回标准的 JSON 格式,不要包含任何其他文字或Markdown标记,格式如下:\n" +
+ "{\"color_id\": 整数, \"tags\": [\"标签1\", \"标签2\"]}";
+
+ JSONObject jsonBody = new JSONObject();
+ jsonBody.put("model", MODEL_NAME);
+
+ JSONArray messages = new JSONArray();
+ JSONObject userMessage = new JSONObject();
+ userMessage.put("role", "user");
+ userMessage.put("content", prompt);
+ messages.put(userMessage);
+
+ jsonBody.put("messages", messages);
+
+ // JSON Mode (DeepSeek 部分模型支持,为了稳妥我们用普通 prompt 约束)
+ // jsonBody.put("response_format", new JSONObject().put("type", "json_object"));
+
+ try (OutputStream os = conn.getOutputStream()) {
+ byte[] input = jsonBody.toString().getBytes("utf-8");
+ os.write(input, 0, input.length);
+ }
+
+ int code = conn.getResponseCode();
+ if (code == 200) {
+ BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
+ StringBuilder response = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) response.append(line);
+
+ JSONObject responseJson = new JSONObject(response.toString());
+ String aiContent = responseJson.getJSONArray("choices")
+ .getJSONObject(0).getJSONObject("message").getString("content");
+
+ // 清理一下 AI 可能返回的 Markdown 代码块符号 (```json ... ```)
+ aiContent = aiContent.replace("```json", "").replace("```", "").trim();
+
+ runOnMainThread(callback, aiContent, null);
+ } else {
+ runOnMainThread(callback, null, "Server Error: " + code);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ runOnMainThread(callback, null, "Error: " + e.getMessage());
+ }
+ }
+ }).start();
+ }
+
+ // 辅助方法:把结果切换回主线程(因为更新 UI 必须在主线程)
+ private static void runOnMainThread(final AIResultCallback callback, final String result, final String error) {
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ if (error != null) {
+ callback.onError(error);
+ } else {
+ callback. onSuccess(result);
+ }
+ }
+ });
+ }
+ /**
+ * RAG 对话接口
+ * @param systemPrompt 系统提示词(包含知识库内容)
+ * @param userQuery 用户的问题
+ * @param callback 回调
+ */
+ public static void chatWithKnowledge(final String systemPrompt, final String userQuery, final AIResultCallback callback) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // ... (连接建立代码与之前相同) ...
+ URL url = new URL(API_URL);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("POST");
+ conn.setRequestProperty("Authorization", "Bearer " + API_KEY);
+ conn.setRequestProperty("Content-Type", "application/json");
+ conn.setDoOutput(true);
+
+ // 构建 Chat 消息体
+ JSONObject jsonBody = new JSONObject();
+ jsonBody.put("model", "deepseek-chat");
+
+ JSONArray messages = new JSONArray();
+
+ // 1. System Message (包含知识库)
+ JSONObject sysMsg = new JSONObject();
+ sysMsg.put("role", "system");
+ sysMsg.put("content", systemPrompt);
+ messages.put(sysMsg);
+
+ // 2. User Message (用户问题)
+ JSONObject userMsg = new JSONObject();
+ userMsg.put("role", "user");
+ userMsg.put("content", userQuery);
+ messages.put(userMsg);
+
+ jsonBody.put("messages", messages);
+ jsonBody.put("stream", false);
+
+ // ... (发送和接收代码与之前完全相同) ...
+ try (java.io.OutputStream os = conn.getOutputStream()) {
+ byte[] input = jsonBody.toString().getBytes("utf-8");
+ os.write(input, 0, input.length);
+ }
+
+ int code = conn.getResponseCode();
+ if (code == 200) {
+ // ... (读取响应代码与之前相同) ...
+ java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(conn.getInputStream(), "utf-8"));
+ StringBuilder response = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) response.append(line);
+
+ JSONObject responseJson = new JSONObject(response.toString());
+ String aiText = responseJson.getJSONArray("choices")
+ .getJSONObject(0).getJSONObject("message").getString("content");
+
+ runOnMainThread(callback, aiText, null);
+ } else {
+ runOnMainThread(callback, null, "Server Error: " + code);
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ runOnMainThread(callback, null, "Network Error: " + e.getMessage());
+ }
+ }
+ }).start();
+ }
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/ai/NoteRetriever.java b/src/src/net/micode/notes/ai/NoteRetriever.java
new file mode 100644
index 0000000..f2dbacc
--- /dev/null
+++ b/src/src/net/micode/notes/ai/NoteRetriever.java
@@ -0,0 +1,148 @@
+package net.micode.notes.ai;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.text.TextUtils;
+import android.util.Log;
+
+import net.micode.notes.data.Notes;
+import net.micode.notes.data.Notes.DataColumns;
+import net.micode.notes.data.Notes.NoteColumns;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 记忆检索器 (Retrieval Layer)
+ * 职责:根据用户问题,在本地数据库中检索相关的便签内容。
+ */
+public class NoteRetriever {
+
+ public static class RetrievedNote {
+ public long id;
+ public String title;
+ public String content;
+ public long modifiedDate;
+
+ public RetrievedNote(long id, String title, String content, long date) {
+ this.id = id;
+ this.title = title;
+ this.content = content;
+ this.modifiedDate = date;
+ }
+ }
+
+ /**
+ * 核心检索方法
+ * @param query 用户的问题
+ * @return 相关的便签列表
+ */
+ public static List searchRelevantNotes(Context context, String query) {
+ List results = new ArrayList<>();
+ // 1. 尝试关键词搜索
+ // ... (保留你之前的代码逻辑) ...
+
+ // ---【修改开始:在这里插入逻辑】---
+
+ // ... (执行原本的 SQL 查询) ...
+
+ // 关键修改点:如果关键词搜索结果为空,或者用户只发了简单的问候
+ if (results.isEmpty()) {
+ Log.d("NoteRetriever", "关键词搜索无果,启动兜底策略:获取最近 10 条便签");
+ results.addAll(getRecentNotes(context, 10));
+ } else {
+ Log.d("NoteRetriever", "关键词搜索成功,找到 " + results.size() + " 条");
+ }
+
+ return results;
+ }
+
+ // 【新增方法】获取最近的 N 条便签
+ private static List getRecentNotes(Context context, int limit) {
+ List list = new ArrayList<>();
+ Cursor cursor = null;
+ try {
+ cursor = context.getContentResolver().query(
+ Notes.CONTENT_NOTE_URI, // 直接查 Note 主表
+ new String[]{NoteColumns.ID, NoteColumns.SNIPPET, NoteColumns.MODIFIED_DATE},
+ NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?",
+ new String[]{String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER)},
+ NoteColumns.MODIFIED_DATE + " DESC LIMIT " + limit // 按时间倒序,取前 N 条
+ );
+
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ long id = cursor.getLong(0);
+ String snippet = cursor.getString(1); // 标题
+ long date = cursor.getLong(2);
+
+ // 再去 Data 表查这一条的完整内容
+ String content = getNoteContent(context, id);
+
+ // 如果内容为空,用标题代替
+ if (TextUtils.isEmpty(content)) content = snippet;
+
+ list.add(new RetrievedNote(id, snippet, content, date));
+ } while (cursor.moveToNext());
+ }
+ } catch (Exception e) {
+ Log.e("NoteRetriever", "Get recent notes failed", e);
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ return list;
+ }
+
+ // 【新增辅助方法】获取单条便签的内容
+ private static String getNoteContent(Context context, long noteId) {
+ String content = "";
+ Cursor c = null;
+ try {
+ c = context.getContentResolver().query(
+ Notes.CONTENT_DATA_URI,
+ new String[]{DataColumns.CONTENT},
+ DataColumns.NOTE_ID + "=? AND " + DataColumns.MIME_TYPE + "=?",
+ new String[]{String.valueOf(noteId), Notes.TextNote.CONTENT_ITEM_TYPE},
+ null
+ );
+ if (c != null && c.moveToFirst()) {
+ content = c.getString(0);
+ }
+ } finally {
+ if (c != null) c.close();
+ }
+ return content;
+ }
+
+ // 辅助方法:获取标题
+ private static String getSnippet(Context context, long noteId) {
+ String snippet = "无标题";
+ Cursor c = context.getContentResolver().query(
+ Notes.CONTENT_NOTE_URI,
+ new String[]{NoteColumns.SNIPPET},
+ NoteColumns.ID + "=?",
+ new String[]{String.valueOf(noteId)},
+ null);
+ if (c != null) {
+ if (c.moveToFirst()) snippet = c.getString(0);
+ c.close();
+ }
+ return snippet;
+ }
+
+ // 辅助方法:获取时间
+ private static long getDate(Context context, long noteId) {
+ long date = 0;
+ Cursor c = context.getContentResolver().query(
+ Notes.CONTENT_NOTE_URI,
+ new String[]{NoteColumns.MODIFIED_DATE},
+ NoteColumns.ID + "=?",
+ new String[]{String.valueOf(noteId)},
+ null);
+ if (c != null) {
+ if (c.moveToFirst()) date = c.getLong(0);
+ c.close();
+ }
+ return date;
+ }
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/ai/PromptBuilder.java b/src/src/net/micode/notes/ai/PromptBuilder.java
new file mode 100644
index 0000000..153fc7d
--- /dev/null
+++ b/src/src/net/micode/notes/ai/PromptBuilder.java
@@ -0,0 +1,52 @@
+package net.micode.notes.ai;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public class PromptBuilder {
+
+ /**
+ * 构建系统提示词
+ * @param relatedNotes 检索到的便签
+ * @param useRAG 是否开启 RAG(知识库)模式
+ */
+ public static String buildSystemPrompt(List relatedNotes, boolean useRAG) {
+ // === 分支 1:通用 AI 模式 (未勾选知识库) ===
+ if (!useRAG) {
+ return "你是一个智能且博学的 AI 助手。请根据用户的提问直接给出准确、有帮助的回答。不需要参考任何本地便签信息。";
+ }
+
+ // === 分支 2:私人管家模式 (勾选知识库) ===
+ StringBuilder sb = new StringBuilder();
+
+ // 1. 身份定义与当前环境
+ String currentTime = new SimpleDateFormat("yyyy年MM月dd日 EEEE HH:mm", Locale.CHINA).format(new Date());
+ sb.append("你是一个基于用户个人便签的私人管家 'Mi管家'。\n");
+ sb.append("当前时间是:").append(currentTime).append("。\n");
+ sb.append("你需要根据用户提供的【参考便签】来回答用户的问题。\n\n");
+
+ // 2. 注入参考资料
+ if (relatedNotes != null && !relatedNotes.isEmpty()) {
+ sb.append("【参考便签】:\n");
+ for (NoteRetriever.RetrievedNote note : relatedNotes) {
+ sb.append("[RefID:").append(note.id).append("] ");
+ sb.append("标题:").append(note.title).append("\n");
+ sb.append("内容:").append(note.content).append("\n");
+ sb.append("修改时间:").append(new SimpleDateFormat("MM-dd", Locale.CHINA).format(new Date(note.modifiedDate))).append("\n");
+ sb.append("----------------\n");
+ }
+ } else {
+ sb.append("【参考便签】:(无相关记录)\n");
+ }
+
+ // 3. 回答约束 (严苛模式)
+ sb.append("\n【回答要求】:\n");
+ sb.append("1. 请根据当前时间和参考便签进行推理。\n");
+ sb.append("2. 如果在参考便签中找到了答案,请简明扼要地回答,并**必须**在句末标注来源,格式为 [Ref:便签标题]。\n");
+ sb.append("3. 如果参考便签中没有相关信息,请诚实回答“我在您的便签中没找到相关记录”,不要编造。\n");
+
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/model/ChatMessage.java b/src/src/net/micode/notes/model/ChatMessage.java
new file mode 100644
index 0000000..64a0c2f
--- /dev/null
+++ b/src/src/net/micode/notes/model/ChatMessage.java
@@ -0,0 +1,34 @@
+package net.micode.notes.model;
+
+import java.util.List;
+
+public class ChatMessage {
+ public static final int TYPE_USER = 0;
+ public static final int TYPE_AI = 1;
+
+ private int type;
+ private String content;
+ private boolean isThinking; // 是否正在思考(仅 AI 有效)
+ private List references; // 参考资料标题列表(仅 AI 有效)
+
+ // 用户消息构造
+ public ChatMessage(String content) {
+ this.type = TYPE_USER;
+ this.content = content;
+ }
+
+ // AI 消息构造
+ public ChatMessage() {
+ this.type = TYPE_AI;
+ this.content = "";
+ this.isThinking = true; // 默认一开始在思考
+ }
+
+ public int getType() { return type; }
+ public String getContent() { return content; }
+ public void setContent(String content) { this.content = content; }
+ public boolean isThinking() { return isThinking; }
+ public void setThinking(boolean thinking) { isThinking = thinking; }
+ public List getReferences() { return references; }
+ public void setReferences(List references) { this.references = references; }
+}
diff --git a/src/src/net/micode/notes/tool/MediaUtils.java b/src/src/net/micode/notes/tool/MediaUtils.java
new file mode 100644
index 0000000..4148eb4
--- /dev/null
+++ b/src/src/net/micode/notes/tool/MediaUtils.java
@@ -0,0 +1,126 @@
+package net.micode.notes.tool;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * 媒体处理工具类
+ * 职责:封装所有与图片文件IO、Uri解析、Bitmap压缩相关的底层逻辑。
+ * 符合“高内聚、信息隐藏”原则。
+ */
+public class MediaUtils {
+ private static final String TAG = "MediaUtils";
+ private static final String IMAGE_DIR_NAME = "images";
+
+ /**
+ * 创建一个用于存放相机拍摄照片的空文件
+ * 位置:/Android/data/包名/files/Pictures/images/
+ */
+ public static File createImageFile(Context context) throws IOException {
+ String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
+ String imageFileName = "NOTE_" + timeStamp + "_";
+
+ // 使用应用私有目录,不需要运行时存储权限即可写入
+ File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
+ if (storageDir != null && !storageDir.exists()) {
+ storageDir.mkdirs();
+ }
+
+ // 创建临时文件
+ return File.createTempFile(
+ imageFileName, /* prefix */
+ ".jpg", /* suffix */
+ storageDir /* directory */
+ );
+ }
+
+ /**
+ * 将 Uri 指向的图片(可能是相册里的 ContentProvider Uri)复制到应用沙盒
+ * 目的:防止用户在相册删除原图后,便签内图片丢失。
+ * @return 复制后的本地文件路径
+ */
+ public static String copyUriToInternalStorage(Context context, Uri sourceUri) {
+ InputStream inputStream = null;
+ FileOutputStream outputStream = null;
+ try {
+ ContentResolver contentResolver = context.getContentResolver();
+ // 1. 获取输入流
+ inputStream = contentResolver.openInputStream(sourceUri);
+ if (inputStream == null) return null;
+
+ // 2. 创建目标文件
+ File targetFile = createImageFile(context);
+
+ // 3. 写入文件
+ outputStream = new FileOutputStream(targetFile);
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, length);
+ }
+
+ return targetFile.getAbsolutePath();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to copy image", e);
+ return null;
+ } finally {
+ // 安全关闭流
+ try {
+ if (inputStream != null) inputStream.close();
+ if (outputStream != null) outputStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * 获取压缩后的 Bitmap,防止 OOM (Out Of Memory)
+ * @param path 图片路径
+ * @param reqWidth 期望的宽度(通常是屏幕宽度)
+ * @param reqHeight 期望的高度
+ */
+ public static Bitmap getCompressedBitmap(String path, int reqWidth, int reqHeight) {
+ // 1. 只读取图片的尺寸信息,不加载像素到内存
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(path, options);
+
+ // 2. 计算压缩比例
+ options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
+
+ // 3. 真正加载图片
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeFile(path, options);
+ }
+
+ // 计算采样率的辅助算法
+ private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+ final int halfHeight = height / 2;
+ final int halfWidth = width / 2;
+ // 计算最大的 2 的幂次,保证宽高仍大于期望宽高
+ while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
+ inSampleSize *= 2;
+ }
+ }
+ return inSampleSize;
+ }
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/tool/ThreadExecutor.java b/src/src/net/micode/notes/tool/ThreadExecutor.java
new file mode 100644
index 0000000..a10f64e
--- /dev/null
+++ b/src/src/net/micode/notes/tool/ThreadExecutor.java
@@ -0,0 +1,40 @@
+package net.micode.notes.tool;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * 线程池工具类
+ * 作用:专门负责在后台执行耗时操作,解放主线程。
+ */
+public class ThreadExecutor {
+ // 单例模式:保证整个APP只有一个线程池,避免资源浪费
+ private static ThreadExecutor instance;
+
+ // 核心成员:ExecutorService 就是 Java 里的“线程池管理者”
+ private final ExecutorService mService;
+
+ // 构造函数私有化:不准外面随便 new
+ private ThreadExecutor() {
+ // newSingleThreadExecutor:创建一个单线程的线程池。
+ // 就像开设了一个“单人窗口”,所有任务按顺序排队执行。
+ // 为什么用单线程?因为数据库写入最好按顺序来,防止两个线程同时写同一个便签导致冲突。
+ mService = Executors.newSingleThreadExecutor();
+ }
+
+ // 获取唯一实例的方法
+ public static synchronized ThreadExecutor getInstance() {
+ if (instance == null) {
+ instance = new ThreadExecutor();
+ }
+ return instance;
+ }
+
+ /**
+ * 执行任务的方法
+ * @param task 一个 Runnable 对象,里面包裹着你要干的活
+ */
+ public void execute(Runnable task) {
+ mService.execute(task);
+ }
+}
diff --git a/src/src/net/micode/notes/ui/CenterImageSpan.java b/src/src/net/micode/notes/ui/CenterImageSpan.java
new file mode 100644
index 0000000..146d62a
--- /dev/null
+++ b/src/src/net/micode/notes/ui/CenterImageSpan.java
@@ -0,0 +1,46 @@
+package net.micode.notes.ui;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.style.ImageSpan;
+
+public class CenterImageSpan extends ImageSpan {
+
+ public CenterImageSpan(Drawable d, String source) {
+ super(d, source);
+ }
+
+ @Override
+ public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
+ Drawable d = getDrawable();
+ Rect rect = d.getBounds();
+
+ if (fm != null) {
+ // 调整行高,确保能容纳图片
+ fm.ascent = -rect.bottom;
+ fm.descent = 0;
+ fm.top = fm.ascent;
+ fm.bottom = 0;
+ }
+ return rect.right;
+ }
+
+ @Override
+ public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
+ Drawable b = getDrawable();
+ canvas.save();
+
+ // 调试模式:不计算居中,直接画在 x 坐标(靠左)
+ // y 是基线位置,bottom 是行底
+ // 我们让图片底部对齐到底部
+ int transY = bottom - b.getBounds().bottom;
+
+ // 暂时直接用 x,确保能看见
+ canvas.translate(x, transY);
+
+ b.draw(canvas);
+ canvas.restore();
+ }
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/ui/ChatAdapter.java b/src/src/net/micode/notes/ui/ChatAdapter.java
new file mode 100644
index 0000000..572a546
--- /dev/null
+++ b/src/src/net/micode/notes/ui/ChatAdapter.java
@@ -0,0 +1,116 @@
+package net.micode.notes.ui;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import net.micode.notes.R;
+import net.micode.notes.model.ChatMessage;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ChatAdapter extends RecyclerView.Adapter {
+
+ private List mData = new ArrayList<>();
+
+ public void addMessage(ChatMessage message) {
+ mData.add(message);
+ notifyItemInserted(mData.size() - 1);
+ }
+
+ public void clear() {
+ mData.clear();
+ notifyDataSetChanged();
+ }
+
+ // 获取最后一条消息(用于流式更新)
+ public ChatMessage getLastMessage() {
+ if (mData.isEmpty()) return null;
+ return mData.get(mData.size() - 1);
+ }
+
+ public void updateLastMessage() {
+ if (!mData.isEmpty()) {
+ notifyItemChanged(mData.size() - 1);
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return mData.get(position).getType();
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ if (viewType == ChatMessage.TYPE_USER) {
+ View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chat_user, parent, false);
+ return new UserViewHolder(v);
+ } else {
+ View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chat_ai, parent, false);
+ return new AIViewHolder(v);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ ChatMessage msg = mData.get(position);
+ if (holder instanceof UserViewHolder) {
+ ((UserViewHolder) holder).tvContent.setText(msg.getContent());
+ } else if (holder instanceof AIViewHolder) {
+ AIViewHolder aiHolder = (AIViewHolder) holder;
+
+ if (msg.isThinking()) {
+ aiHolder.layoutThinking.setVisibility(View.VISIBLE);
+ aiHolder.tvContent.setVisibility(View.GONE);
+ aiHolder.layoutRefs.setVisibility(View.GONE);
+ } else {
+ aiHolder.layoutThinking.setVisibility(View.GONE);
+ aiHolder.tvContent.setVisibility(View.VISIBLE);
+ aiHolder.tvContent.setText(msg.getContent());
+
+ // 处理参考资料
+ if (msg.getReferences() != null && !msg.getReferences().isEmpty()) {
+ aiHolder.layoutRefs.setVisibility(View.VISIBLE);
+ StringBuilder refs = new StringBuilder();
+ for (String ref : msg.getReferences()) {
+ refs.append("📄 ").append(ref).append("\n");
+ }
+ aiHolder.tvRefs.setText(refs.toString().trim());
+ } else {
+ aiHolder.layoutRefs.setVisibility(View.GONE);
+ }
+ }
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mData.size();
+ }
+
+ static class UserViewHolder extends RecyclerView.ViewHolder {
+ TextView tvContent;
+ UserViewHolder(View itemView) {
+ super(itemView);
+ tvContent = itemView.findViewById(R.id.tv_user_content);
+ }
+ }
+
+ static class AIViewHolder extends RecyclerView.ViewHolder {
+ View layoutThinking;
+ TextView tvContent;
+ View layoutRefs;
+ TextView tvRefs;
+ AIViewHolder(View itemView) {
+ super(itemView);
+ layoutThinking = itemView.findViewById(R.id.layout_thinking);
+ tvContent = itemView.findViewById(R.id.tv_ai_content);
+ layoutRefs = itemView.findViewById(R.id.layout_references);
+ tvRefs = itemView.findViewById(R.id.tv_reference_list);
+ }
+ }
+}
diff --git a/src/src/net/micode/notes/ui/MainActivity.java b/src/src/net/micode/notes/ui/MainActivity.java
new file mode 100644
index 0000000..bfc2977
--- /dev/null
+++ b/src/src/net/micode/notes/ui/MainActivity.java
@@ -0,0 +1,72 @@
+package net.micode.notes.ui;
+
+import android.os.Bundle;
+import android.view.MenuItem;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+import androidx.viewpager2.widget.ViewPager2;
+import com.google.android.material.bottomnavigation.BottomNavigationView;
+import net.micode.notes.R;
+
+public class MainActivity extends AppCompatActivity {
+
+ private ViewPager2 mViewPager;
+ private BottomNavigationView mBottomNav;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ mViewPager = findViewById(R.id.view_pager);
+ mBottomNav = findViewById(R.id.bottom_navigation);
+
+ // 设置 Adapter
+ mViewPager.setAdapter(new FragmentStateAdapter(this) {
+ @NonNull
+ @Override
+ public Fragment createFragment(int position) {
+ if (position == 0) {
+ return new MiStewardFragment(); // 默认第一页是管家
+ } else {
+ return new NotesListFragment(); // 第二页是便签列表
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return 2;
+ }
+ });
+
+ // 联动逻辑:点击底部 -> 切换 ViewPager
+ mBottomNav.setOnItemSelectedListener(new BottomNavigationView.OnItemSelectedListener() {
+ @Override
+ public boolean onNavigationItemSelected(@NonNull MenuItem item) {
+ if (item.getItemId() == R.id.nav_mi_steward) {
+ mViewPager.setCurrentItem(0);
+ return true;
+ } else if (item.getItemId() == R.id.nav_notes) {
+ mViewPager.setCurrentItem(1);
+ return true;
+ }
+ return false;
+ }
+ });
+
+ // 联动逻辑:滑动 ViewPager -> 切换底部按钮
+ mViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
+ @Override
+ public void onPageSelected(int position) {
+ super.onPageSelected(position);
+ if (position == 0) {
+ mBottomNav.setSelectedItemId(R.id.nav_mi_steward);
+ } else {
+ mBottomNav.setSelectedItemId(R.id.nav_notes);
+ }
+ }
+ });
+ }
+}
diff --git a/src/src/net/micode/notes/ui/MiStewardFragment.java b/src/src/net/micode/notes/ui/MiStewardFragment.java
new file mode 100644
index 0000000..78cfc0b
--- /dev/null
+++ b/src/src/net/micode/notes/ui/MiStewardFragment.java
@@ -0,0 +1,159 @@
+package net.micode.notes.ui;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.Toast;
+import net.micode.notes.ai.NoteRetriever;
+import net.micode.notes.ai.PromptBuilder;
+import net.micode.notes.ai.AIService;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import android.widget.CheckBox;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.micode.notes.R;
+import net.micode.notes.model.ChatMessage;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MiStewardFragment extends Fragment {
+
+ private RecyclerView mRecyclerView;
+ private ChatAdapter mAdapter;
+ private EditText mInputBox;
+ private View mLogoArea;
+ private View mTopBar;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_mi_steward, container, false);
+ initViews(view);
+ return view;
+ }
+
+ private void initViews(View view) {
+ mRecyclerView = view.findViewById(R.id.recycler_view);
+ mInputBox = view.findViewById(R.id.et_input);
+ mLogoArea = view.findViewById(R.id.center_logo_area);
+ mTopBar = view.findViewById(R.id.top_bar);
+ Button btnSend = view.findViewById(R.id.btn_send);
+ ImageView btnNewChat = view.findViewById(R.id.btn_new_chat);
+
+ mAdapter = new ChatAdapter();
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ mRecyclerView.setAdapter(mAdapter);
+
+ // 发送按钮点击
+ btnSend.setOnClickListener(v -> {
+ String text = mInputBox.getText().toString().trim();
+ if (TextUtils.isEmpty(text)) return;
+
+ // 1. 切换 UI 状态
+ mLogoArea.setVisibility(View.GONE);
+ mRecyclerView.setVisibility(View.VISIBLE);
+
+ // 2. 添加用户消息
+ mAdapter.addMessage(new ChatMessage(text));
+ mInputBox.setText("");
+ scrollToBottom();
+
+ // 3. 模拟 AI 思考 (这是个占位逻辑,下一阶段会接真实的 DeepSeek)
+ performAIQuery(text);
+ });
+
+ // 新对话按钮点击
+ btnNewChat.setOnClickListener(v -> {
+ mAdapter.clear();
+ mLogoArea.setVisibility(View.VISIBLE);
+ mRecyclerView.setVisibility(View.GONE);
+ Toast.makeText(getContext(), "已开启新对话", Toast.LENGTH_SHORT).show();
+ });
+ }
+
+ private void scrollToBottom() {
+ mRecyclerView.scrollToPosition(mAdapter.getItemCount() - 1);
+ }
+
+ // 模拟 AI 回复 (测试 UI 用)
+ // 真实的 AI 响应处理
+ private void performAIQuery(String userQuery) {
+ // 先在 UI 上显示“思考中”
+ ChatMessage aiMsg = new ChatMessage();
+ mAdapter.addMessage(aiMsg);
+ scrollToBottom();
+
+ // 1. 判断是否勾选“使用知识库”
+ CheckBox cbUseKnowledge = getView().findViewById(R.id.cb_use_knowledge);
+ boolean useRAG = cbUseKnowledge.isChecked();
+
+ // 2. 异步检索知识库 (检索需要在子线程,虽然 SQLite 很快,但为了严谨)
+ new Thread(() -> {
+ List relatedNotes = null;
+ if (useRAG) {
+ // 去数据库检索
+ relatedNotes = NoteRetriever.searchRelevantNotes(getContext(), userQuery);
+ }
+
+ // 3. 构建 Prompt
+ String systemPrompt = PromptBuilder.buildSystemPrompt(relatedNotes,useRAG);
+
+ // 4. 调用 API
+ AIService.chatWithKnowledge(systemPrompt, userQuery, new AIService.AIResultCallback() {
+ @Override
+ public void onSuccess(String result) {
+ // 解析结果中的引用 [Ref:xxx]
+ List refs = new ArrayList<>();
+ // 使用正则提取引用并从文本中移除(或者保留,看你喜好,这里我们提取出来单独展示)
+ String cleanContent = result;
+
+ // 正则匹配 [Ref:...]
+ Pattern pattern = Pattern.compile("\\[Ref:(.*?)\\]");
+ Matcher matcher = pattern.matcher(result);
+ while (matcher.find()) {
+ String refTitle = matcher.group(1);
+ if (!refs.contains(refTitle)) {
+ refs.add("📌 " + refTitle);
+ }
+ }
+ // 如果你想把正文里的 [Ref:xxx] 去掉,可以取消下面这行的注释
+ cleanContent = matcher.replaceAll("").trim();
+
+ // 更新 UI
+ ChatMessage lastMsg = mAdapter.getLastMessage();
+ if (lastMsg != null && lastMsg.getType() == ChatMessage.TYPE_AI) {
+ lastMsg.setThinking(false);
+ lastMsg.setContent(cleanContent);
+ lastMsg.setReferences(refs);
+
+ mAdapter.updateLastMessage();
+ scrollToBottom();
+ }
+ }
+
+ @Override
+ public void onError(String error) {
+ ChatMessage lastMsg = mAdapter.getLastMessage();
+ if (lastMsg != null) {
+ lastMsg.setThinking(false);
+ lastMsg.setContent("大脑短路了... 错误原因:" + error);
+ mAdapter.updateLastMessage();
+ }
+ }
+ });
+ }).start();
+ }
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/ui/NotesListFragment.java b/src/src/net/micode/notes/ui/NotesListFragment.java
new file mode 100644
index 0000000..bbcb49c
--- /dev/null
+++ b/src/src/net/micode/notes/ui/NotesListFragment.java
@@ -0,0 +1,882 @@
+package net.micode.notes.ui;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.appwidget.AppWidgetManager;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Display;
+import android.view.HapticFeedbackConstants;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnCreateContextMenuListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import net.micode.notes.R;
+import net.micode.notes.data.Notes;
+import net.micode.notes.data.Notes.NoteColumns;
+import net.micode.notes.gtask.remote.GTaskSyncService;
+import net.micode.notes.model.WorkingNote;
+import net.micode.notes.tool.BackupUtils;
+import net.micode.notes.tool.DataUtils;
+import net.micode.notes.tool.ResourceParser;
+import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
+import net.micode.notes.widget.NoteWidgetProvider_2x;
+import net.micode.notes.widget.NoteWidgetProvider_4x;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.HashSet;
+
+public class NotesListFragment extends Fragment implements OnClickListener, OnItemLongClickListener {
+ private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0;
+ private static final int FOLDER_LIST_QUERY_TOKEN = 1;
+ private static final int MENU_FOLDER_DELETE = 0;
+ private static final int MENU_FOLDER_VIEW = 1;
+ private static final int MENU_FOLDER_CHANGE_NAME = 2;
+ private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction";
+
+ private enum ListEditState {
+ NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER
+ };
+
+ private ListEditState mState;
+ private BackgroundQueryHandler mBackgroundQueryHandler;
+ private NotesListAdapter mNotesListAdapter;
+ private ListView mNotesListView;
+ private Button mAddNewNote;
+ private boolean mDispatch;
+ private int mOriginY;
+ private int mDispatchY;
+ private TextView mTitleBar;
+ private long mCurrentFolderId;
+ private ContentResolver mContentResolver;
+ private ModeCallback mModeCallBack;
+ private static final String TAG = "NotesListFragment";
+ private NoteItemData mFocusNoteDataItem;
+
+ private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?";
+ private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>"
+ + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR ("
+ + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND "
+ + NoteColumns.NOTES_COUNT + ">0)";
+
+ private final static int REQUEST_CODE_OPEN_NODE = 102;
+ private final static int REQUEST_CODE_NEW_NODE = 103;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.note_list, container, false);
+ // 重要:告诉系统这个 Fragment 有自己的菜单项(Sync, Setting等)
+ setHasOptionsMenu(true);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ initResources(view);
+ setAppInfoFromRawRes();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == Activity.RESULT_OK
+ && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) {
+ mNotesListAdapter.changeCursor(null);
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ private void setAppInfoFromRawRes() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) {
+ StringBuilder sb = new StringBuilder();
+ InputStream in = null;
+ try {
+ in = getResources().openRawResource(R.raw.introduction);
+ if (in != null) {
+ InputStreamReader isr = new InputStreamReader(in);
+ BufferedReader br = new BufferedReader(isr);
+ char [] buf = new char[1024];
+ int len = 0;
+ while ((len = br.read(buf)) > 0) {
+ sb.append(buf, 0, len);
+ }
+ } else {
+ Log.e(TAG, "Read introduction file error");
+ return;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return;
+ } finally {
+ if(in != null) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ WorkingNote note = WorkingNote.createEmptyNote(getActivity(), Notes.ID_ROOT_FOLDER,
+ AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE,
+ ResourceParser.RED);
+ note.setWorkingText(sb.toString());
+ if (note.saveNote()) {
+ sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit();
+ } else {
+ Log.e(TAG, "Save introduction note error");
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ startAsyncNotesListQuery();
+ }
+
+ private void initResources(View view) {
+ mContentResolver = getActivity().getContentResolver();
+ mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver);
+ mCurrentFolderId = Notes.ID_ROOT_FOLDER;
+ mNotesListView = (ListView) view.findViewById(R.id.notes_list);
+ mNotesListView.addFooterView(LayoutInflater.from(getActivity()).inflate(R.layout.note_list_footer, null),
+ null, false);
+ mNotesListView.setOnItemClickListener(new OnListItemClickListener());
+ mNotesListView.setOnItemLongClickListener(this);
+ mNotesListAdapter = new NotesListAdapter(getActivity());
+ mNotesListView.setAdapter(mNotesListAdapter);
+ mAddNewNote = (Button) view.findViewById(R.id.btn_new_note);
+ mAddNewNote.setOnClickListener(this);
+ mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener());
+ mDispatch = false;
+ mDispatchY = 0;
+ mOriginY = 0;
+ mTitleBar = (TextView) view.findViewById(R.id.tv_title_bar);
+ mState = ListEditState.NOTE_LIST;
+ mModeCallBack = new ModeCallback();
+ }
+
+ private class ModeCallback implements ListView.MultiChoiceModeListener, MenuItem.OnMenuItemClickListener {
+ private DropdownMenu mDropDownMenu;
+ private ActionMode mActionMode;
+ private MenuItem mMoveMenu;
+
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ getActivity().getMenuInflater().inflate(R.menu.note_list_options, menu);
+ menu.findItem(R.id.delete).setOnMenuItemClickListener(this);
+ mMoveMenu = menu.findItem(R.id.move);
+ if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER
+ || DataUtils.getUserFolderCount(mContentResolver) == 0) {
+ mMoveMenu.setVisible(false);
+ } else {
+ mMoveMenu.setVisible(true);
+ mMoveMenu.setOnMenuItemClickListener(this);
+ }
+ mActionMode = mode;
+ mNotesListAdapter.setChoiceMode(true);
+ mNotesListView.setLongClickable(false);
+ mAddNewNote.setVisibility(View.GONE);
+
+ View customView = LayoutInflater.from(getActivity()).inflate(
+ R.layout.note_list_dropdown_menu, null);
+ mode.setCustomView(customView);
+ mDropDownMenu = new DropdownMenu(getActivity(),
+ (Button) customView.findViewById(R.id.selection_menu),
+ R.menu.note_list_dropdown);
+ mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
+ public boolean onMenuItemClick(MenuItem item) {
+ mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected());
+ updateMenu();
+ return true;
+ }
+
+ });
+ return true;
+ }
+
+ private void updateMenu() {
+ int selectedCount = mNotesListAdapter.getSelectedCount();
+ String format = getResources().getString(R.string.menu_select_title, selectedCount);
+ mDropDownMenu.setTitle(format);
+ MenuItem item = mDropDownMenu.findItem(R.id.action_select_all);
+ if (item != null) {
+ if (mNotesListAdapter.isAllSelected()) {
+ item.setChecked(true);
+ item.setTitle(R.string.menu_deselect_all);
+ } else {
+ item.setChecked(false);
+ item.setTitle(R.string.menu_select_all);
+ }
+ }
+ }
+
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return false;
+ }
+
+ public void onDestroyActionMode(ActionMode mode) {
+ mNotesListAdapter.setChoiceMode(false);
+ mNotesListView.setLongClickable(true);
+ mAddNewNote.setVisibility(View.VISIBLE);
+ }
+
+ public void finishActionMode() {
+ mActionMode.finish();
+ }
+
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
+ boolean checked) {
+ mNotesListAdapter.setCheckedItem(position, checked);
+ updateMenu();
+ }
+
+ public boolean onMenuItemClick(MenuItem item) {
+ if (mNotesListAdapter.getSelectedCount() == 0) {
+ Toast.makeText(getActivity(), getString(R.string.menu_select_none),
+ Toast.LENGTH_SHORT).show();
+ return true;
+ }
+
+ switch (item.getItemId()) {
+ case R.id.delete:
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getString(R.string.alert_title_delete));
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setMessage(getString(R.string.alert_message_delete_notes,
+ mNotesListAdapter.getSelectedCount()));
+ builder.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ batchDelete();
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.show();
+ break;
+ case R.id.move:
+ startQueryDestinationFolders();
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+ }
+
+ private class NewNoteOnTouchListener implements OnTouchListener {
+
+ public boolean onTouch(View v, MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ Display display = getActivity().getWindowManager().getDefaultDisplay();
+ int screenHeight = display.getHeight();
+ int newNoteViewHeight = mAddNewNote.getHeight();
+ int start = screenHeight - newNoteViewHeight;
+ int eventY = start + (int) event.getY();
+ if (mState == ListEditState.SUB_FOLDER) {
+ eventY -= mTitleBar.getHeight();
+ start -= mTitleBar.getHeight();
+ }
+ if (event.getY() < (event.getX() * (-0.12) + 94)) {
+ View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1
+ - mNotesListView.getFooterViewsCount());
+ if (view != null && view.getBottom() > start
+ && (view.getTop() < (start + 94))) {
+ mOriginY = (int) event.getY();
+ mDispatchY = eventY;
+ event.setLocation(event.getX(), mDispatchY);
+ mDispatch = true;
+ return mNotesListView.dispatchTouchEvent(event);
+ }
+ }
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ if (mDispatch) {
+ mDispatchY += (int) event.getY() - mOriginY;
+ event.setLocation(event.getX(), mDispatchY);
+ return mNotesListView.dispatchTouchEvent(event);
+ }
+ break;
+ }
+ default: {
+ if (mDispatch) {
+ event.setLocation(event.getX(), mDispatchY);
+ mDispatch = false;
+ return mNotesListView.dispatchTouchEvent(event);
+ }
+ break;
+ }
+ }
+ return false;
+ }
+
+ };
+
+ private void startAsyncNotesListQuery() {
+ String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION
+ : NORMAL_SELECTION;
+ mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null,
+ Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] {
+ String.valueOf(mCurrentFolderId)
+ }, NoteColumns.TYPE + " DESC," + NoteColumns.BG_COLOR_ID + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
+ }
+
+ private final class BackgroundQueryHandler extends AsyncQueryHandler {
+ public BackgroundQueryHandler(ContentResolver contentResolver) {
+ super(contentResolver);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ switch (token) {
+ case FOLDER_NOTE_LIST_QUERY_TOKEN:
+ mNotesListAdapter.changeCursor(cursor);
+ break;
+ case FOLDER_LIST_QUERY_TOKEN:
+ if (cursor != null && cursor.getCount() > 0) {
+ showFolderListMenu(cursor);
+ } else {
+ Log.e(TAG, "Query folder failed");
+ }
+ break;
+ default:
+ return;
+ }
+ }
+ }
+
+ private void showFolderListMenu(Cursor cursor) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.menu_title_select_folder);
+ final FoldersListAdapter adapter = new FoldersListAdapter(getActivity(), cursor);
+ builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
+
+ public void onClick(DialogInterface dialog, int which) {
+ DataUtils.batchMoveToFolder(mContentResolver,
+ mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which));
+ Toast.makeText(
+ getActivity(),
+ getString(R.string.format_move_notes_to_folder,
+ mNotesListAdapter.getSelectedCount(),
+ adapter.getFolderName(getActivity(), which)),
+ Toast.LENGTH_SHORT).show();
+ mModeCallBack.finishActionMode();
+ }
+ });
+ builder.show();
+ }
+
+ private void createNewNote() {
+ Intent intent = new Intent(getActivity(), NoteEditActivity.class);
+ intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
+ intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId);
+ this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE);
+ }
+
+ private void batchDelete() {
+ new AsyncTask>() {
+ protected HashSet doInBackground(Void... unused) {
+ HashSet widgets = mNotesListAdapter.getSelectedWidget();
+ if (!isSyncMode()) {
+ if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter
+ .getSelectedItemIds())) {
+ } else {
+ Log.e(TAG, "Delete notes error, should not happens");
+ }
+ } else {
+ if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter
+ .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) {
+ Log.e(TAG, "Move notes to trash folder error, should not happens");
+ }
+ }
+ return widgets;
+ }
+
+ @Override
+ protected void onPostExecute(HashSet widgets) {
+ if (widgets != null) {
+ for (AppWidgetAttribute widget : widgets) {
+ if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
+ && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
+ updateWidget(widget.widgetId, widget.widgetType);
+ }
+ }
+ }
+ mModeCallBack.finishActionMode();
+ }
+ }.execute();
+ }
+
+ private void deleteFolder(long folderId) {
+ if (folderId == Notes.ID_ROOT_FOLDER) {
+ Log.e(TAG, "Wrong folder id, should not happen " + folderId);
+ return;
+ }
+
+ HashSet ids = new HashSet();
+ ids.add(folderId);
+ HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver,
+ folderId);
+ if (!isSyncMode()) {
+ DataUtils.batchDeleteNotes(mContentResolver, ids);
+ } else {
+ DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER);
+ }
+ if (widgets != null) {
+ for (AppWidgetAttribute widget : widgets) {
+ if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
+ && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
+ updateWidget(widget.widgetId, widget.widgetType);
+ }
+ }
+ }
+ }
+
+ private void openNode(NoteItemData data) {
+ Intent intent = new Intent(getActivity(), NoteEditActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.putExtra(Intent.EXTRA_UID, data.getId());
+ this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE);
+ }
+
+ private void openFolder(NoteItemData data) {
+ mCurrentFolderId = data.getId();
+ startAsyncNotesListQuery();
+ if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
+ mState = ListEditState.CALL_RECORD_FOLDER;
+ mAddNewNote.setVisibility(View.GONE);
+ } else {
+ mState = ListEditState.SUB_FOLDER;
+ }
+ if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
+ mTitleBar.setText(R.string.call_record_folder_name);
+ } else {
+ mTitleBar.setText(data.getSnippet());
+ }
+ mTitleBar.setVisibility(View.VISIBLE);
+ }
+
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.btn_new_note:
+ createNewNote();
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void showSoftInput() {
+ InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (inputMethodManager != null) {
+ inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
+ }
+ }
+
+ private void hideSoftInput(View view) {
+ InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+
+ private void showCreateOrModifyFolderDialog(final boolean create) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ View view = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_edit_text, null);
+ final EditText etName = (EditText) view.findViewById(R.id.et_foler_name);
+ showSoftInput();
+ if (!create) {
+ if (mFocusNoteDataItem != null) {
+ etName.setText(mFocusNoteDataItem.getSnippet());
+ builder.setTitle(getString(R.string.menu_folder_change_name));
+ } else {
+ Log.e(TAG, "The long click data item is null");
+ return;
+ }
+ } else {
+ etName.setText("");
+ builder.setTitle(this.getString(R.string.menu_create_folder));
+ }
+
+ builder.setPositiveButton(android.R.string.ok, null);
+ builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ hideSoftInput(etName);
+ }
+ });
+
+ // 把 Dialog 改成 AlertDialog
+ final AlertDialog dialog = builder.setView(view).show();
+ final Button positive = (Button)dialog.findViewById(android.R.id.button1);
+ positive.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ hideSoftInput(etName);
+ String name = etName.getText().toString();
+ if (DataUtils.checkVisibleFolderName(mContentResolver, name)) {
+ Toast.makeText(getActivity(), getString(R.string.folder_exist, name),
+ Toast.LENGTH_LONG).show();
+ etName.setSelection(0, etName.length());
+ return;
+ }
+ if (!create) {
+ if (!TextUtils.isEmpty(name)) {
+ ContentValues values = new ContentValues();
+ values.put(NoteColumns.SNIPPET, name);
+ values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
+ values.put(NoteColumns.LOCAL_MODIFIED, 1);
+ mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID
+ + "=?", new String[] {
+ String.valueOf(mFocusNoteDataItem.getId())
+ });
+ }
+ } else if (!TextUtils.isEmpty(name)) {
+ ContentValues values = new ContentValues();
+ values.put(NoteColumns.SNIPPET, name);
+ values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
+ mContentResolver.insert(Notes.CONTENT_NOTE_URI, values);
+ }
+ dialog.dismiss();
+ }
+ });
+
+ if (TextUtils.isEmpty(etName.getText())) {
+ positive.setEnabled(false);
+ }
+ etName.addTextChangedListener(new TextWatcher() {
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (TextUtils.isEmpty(etName.getText())) {
+ positive.setEnabled(false);
+ } else {
+ positive.setEnabled(true);
+ }
+ }
+ public void afterTextChanged(Editable s) { }
+ });
+ }
+
+ // 重点:Fragment 没有 onBackPressed,这个方法供 MainActivity 调用
+ public boolean onBackPressed() {
+ switch (mState) {
+ case SUB_FOLDER:
+ mCurrentFolderId = Notes.ID_ROOT_FOLDER;
+ mState = ListEditState.NOTE_LIST;
+ startAsyncNotesListQuery();
+ mTitleBar.setVisibility(View.GONE);
+ return true; // 消费了返回事件
+ case CALL_RECORD_FOLDER:
+ mCurrentFolderId = Notes.ID_ROOT_FOLDER;
+ mState = ListEditState.NOTE_LIST;
+ mAddNewNote.setVisibility(View.VISIBLE);
+ mTitleBar.setVisibility(View.GONE);
+ startAsyncNotesListQuery();
+ return true; // 消费了返回事件
+ case NOTE_LIST:
+ default:
+ return false; // 不消费,让 MainActivity 处理(退出应用)
+ }
+ }
+
+ private void updateWidget(int appWidgetId, int appWidgetType) {
+ Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
+ if (appWidgetType == Notes.TYPE_WIDGET_2X) {
+ intent.setClass(getActivity(), NoteWidgetProvider_2x.class);
+ } else if (appWidgetType == Notes.TYPE_WIDGET_4X) {
+ intent.setClass(getActivity(), NoteWidgetProvider_4x.class);
+ } else {
+ Log.e(TAG, "Unspported widget type");
+ return;
+ }
+
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {
+ appWidgetId
+ });
+
+ getActivity().sendBroadcast(intent);
+ getActivity().setResult(Activity.RESULT_OK, intent);
+ }
+
+ private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() {
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ if (mFocusNoteDataItem != null) {
+ menu.setHeaderTitle(mFocusNoteDataItem.getSnippet());
+ menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view);
+ menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete);
+ menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name);
+ }
+ }
+ };
+
+ @Override
+ public boolean onContextItemSelected(@NonNull MenuItem item) {
+ if (mFocusNoteDataItem == null) {
+ Log.e(TAG, "The long click data item is null");
+ return false;
+ }
+ switch (item.getItemId()) {
+ case MENU_FOLDER_VIEW:
+ openFolder(mFocusNoteDataItem);
+ break;
+ case MENU_FOLDER_DELETE:
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getString(R.string.alert_title_delete));
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setMessage(getString(R.string.alert_message_delete_folder));
+ builder.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ deleteFolder(mFocusNoteDataItem.getId());
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.show();
+ break;
+ case MENU_FOLDER_CHANGE_NAME:
+ showCreateOrModifyFolderDialog(false);
+ break;
+ default:
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+ menu.clear();
+ if (mState == ListEditState.NOTE_LIST) {
+ inflater.inflate(R.menu.note_list, menu);
+ menu.findItem(R.id.menu_sync).setTitle(
+ GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync);
+ } else if (mState == ListEditState.SUB_FOLDER) {
+ inflater.inflate(R.menu.sub_folder, menu);
+ } else if (mState == ListEditState.CALL_RECORD_FOLDER) {
+ inflater.inflate(R.menu.call_record_folder, menu);
+ } else {
+ Log.e(TAG, "Wrong state:" + mState);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_new_folder: {
+ showCreateOrModifyFolderDialog(true);
+ break;
+ }
+ case R.id.menu_export_text: {
+ exportNoteToText();
+ break;
+ }
+ case R.id.menu_sync: {
+ if (isSyncMode()) {
+ if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) {
+ GTaskSyncService.startSync(getActivity());
+ } else {
+ GTaskSyncService.cancelSync(getActivity());
+ }
+ } else {
+ startPreferenceActivity();
+ }
+ break;
+ }
+ case R.id.menu_setting: {
+ startPreferenceActivity();
+ break;
+ }
+ case R.id.menu_new_note: {
+ createNewNote();
+ break;
+ }
+ case R.id.menu_search:
+ onSearchRequested();
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+ public boolean onSearchRequested() {
+ // Fragment 没有 startSearch,需要调用 Activity 的
+ startActivity(new Intent(getActivity(), NotesListActivity.class).setAction(Intent.ACTION_SEARCH));
+ // 实际上 Fragment 很难直接调用系统的 SearchDialog 并回调,这里简单处理
+ return true;
+ }
+
+ private void exportNoteToText() {
+ final BackupUtils backup = BackupUtils.getInstance(getActivity());
+ new AsyncTask() {
+
+ @Override
+ protected Integer doInBackground(Void... unused) {
+ return backup.exportToText();
+ }
+
+ @Override
+ protected void onPostExecute(Integer result) {
+ if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getActivity()
+ .getString(R.string.failed_sdcard_export));
+ builder.setMessage(getActivity()
+ .getString(R.string.error_sdcard_unmounted));
+ builder.setPositiveButton(android.R.string.ok, null);
+ builder.show();
+ } else if (result == BackupUtils.STATE_SUCCESS) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getActivity()
+ .getString(R.string.success_sdcard_export));
+ builder.setMessage(getActivity().getString(
+ R.string.format_exported_file_location, backup
+ .getExportedTextFileName(), backup.getExportedTextFileDir()));
+ builder.setPositiveButton(android.R.string.ok, null);
+ builder.show();
+ } else if (result == BackupUtils.STATE_SYSTEM_ERROR) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getActivity()
+ .getString(R.string.failed_sdcard_export));
+ builder.setMessage(getActivity()
+ .getString(R.string.error_sdcard_export));
+ builder.setPositiveButton(android.R.string.ok, null);
+ builder.show();
+ }
+ }
+
+ }.execute();
+ }
+
+ private boolean isSyncMode() {
+ return NotesPreferenceActivity.getSyncAccountName(getActivity()).trim().length() > 0;
+ }
+
+ private void startPreferenceActivity() {
+ Intent intent = new Intent(getActivity(), NotesPreferenceActivity.class);
+ startActivity(intent);
+ }
+
+ private class OnListItemClickListener implements AdapterView.OnItemClickListener {
+
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ if (view instanceof NotesListItem) {
+ NoteItemData item = ((NotesListItem) view).getItemData();
+ if (mNotesListAdapter.isInChoiceMode()) {
+ if (item.getType() == Notes.TYPE_NOTE) {
+ position = position - mNotesListView.getHeaderViewsCount();
+ mModeCallBack.onItemCheckedStateChanged(null, position, id,
+ !mNotesListAdapter.isSelectedItem(position));
+ }
+ return;
+ }
+
+ switch (mState) {
+ case NOTE_LIST:
+ if (item.getType() == Notes.TYPE_FOLDER
+ || item.getType() == Notes.TYPE_SYSTEM) {
+ openFolder(item);
+ } else if (item.getType() == Notes.TYPE_NOTE) {
+ openNode(item);
+ } else {
+ Log.e(TAG, "Wrong note type in NOTE_LIST");
+ }
+ break;
+ case SUB_FOLDER:
+ case CALL_RECORD_FOLDER:
+ if (item.getType() == Notes.TYPE_NOTE) {
+ openNode(item);
+ } else {
+ Log.e(TAG, "Wrong note type in SUB_FOLDER");
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ }
+
+ private void startQueryDestinationFolders() {
+ String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?";
+ selection = (mState == ListEditState.NOTE_LIST) ? selection:
+ "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")";
+
+ mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN,
+ null,
+ Notes.CONTENT_NOTE_URI,
+ FoldersListAdapter.PROJECTION,
+ selection,
+ new String[] {
+ String.valueOf(Notes.TYPE_FOLDER),
+ String.valueOf(Notes.ID_TRASH_FOLER),
+ String.valueOf(mCurrentFolderId)
+ },
+ NoteColumns.MODIFIED_DATE + " DESC");
+ }
+
+ public boolean onItemLongClick(AdapterView> parent, View view, int position, long id) {
+ if (view instanceof NotesListItem) {
+ mFocusNoteDataItem = ((NotesListItem) view).getItemData();
+ if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) {
+ if (mNotesListView.startActionMode(mModeCallBack) != null) {
+ mModeCallBack.onItemCheckedStateChanged(null, position, id, true);
+ mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ } else {
+ Log.e(TAG, "startActionMode fails");
+ }
+ } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) {
+ mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener);
+ }
+ }
+ return false;
+ }
+}