();
-
- // 没映射表就建表,有就查缓存中有没有这个联系人
}
if(sContactCache.containsKey(phoneNumber)) {
return sContactCache.get(phoneNumber);
}
- //返回从表里查询到的对应名字
+
String selection = CALLER_ID_SELECTION.replace("+",
PhoneNumberUtils.toCallerIDMinMatch(phoneNumber));
- //构造一个SQL查询条件:CALLER_ID_SELECTION中的"+"被替换为电话号码的最小匹配值
- //然后执行查询语句
Cursor cursor = context.getContentResolver().query(
Data.CONTENT_URI,
new String [] { Phone.DISPLAY_NAME },
selection,
new String[] { phoneNumber },
null);
- //判断查询结果:
- //查询结果不为空,且能够移动到第一条记录:
- // 那么就尝试从Cursor中获取联系人姓名,并将其存入缓存sContactCache。然后返回联系人姓名。
- // 异常情况:如果在获取字符串时发生数组越界异常,则记录一个错误日志并返回null。
- // 最后都要确保关闭Cursor对象,以避免内存泄漏。
- //如果查询结果为空或者没有记录可以移动到(即没有找到匹配的联系人):
- // 则记录一条调试日志并返回null
+
if (cursor != null && cursor.moveToFirst()) {
try {
String name = cursor.getString(0);
diff --git a/src/Notes-master/src/net/micode/notes/data/Messages.java b/src/Notes-master/src/net/micode/notes/data/Messages.java
new file mode 100644
index 0000000..d6f2629
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/data/Messages.java
@@ -0,0 +1,23 @@
+package net.micode.notes.data;
+
+public class Messages {
+
+ public interface MessageColumns {
+ public static final String ID = "id";
+ public static final String SENDER_ID = "sender_id";
+ public static final String RECEIVER_ID = "receiver_id";
+ public static final String CONTENT = "content";
+ public static final String MESSAGE_TYPE = "message_type";
+ public static final String CREATED_DATE = "created_date";
+ public static final String IS_READ = "is_read";
+ }
+
+ public interface MessageType {
+ public static final int TEXT = 0;
+ public static final int IMAGE = 1;
+ public static final int EMOTION = 2;
+ public static final int NOTE = 3;
+ }
+
+ public static final long ID_INVALID = -1;
+}
diff --git a/src/Notes-master/src/net/micode/notes/data/Notes.java b/src/Notes-master/src/net/micode/notes/data/Notes.java
index 9d829ab..ec79df1 100644
--- a/src/Notes-master/src/net/micode/notes/data/Notes.java
+++ b/src/Notes-master/src/net/micode/notes/data/Notes.java
@@ -18,11 +18,8 @@ package net.micode.notes.data;
import android.net.Uri;
public class Notes {
- //用于表示笔记应用中的各种类型、标识符以及Intent的额外数据
public static final String AUTHORITY = "micode_notes";
public static final String TAG = "Notes";
- //对NoteColumns.TYPE的值进行设置时使用:
- //即不同种类:笔记、文件夹和系统文件夹
public static final int TYPE_NOTE = 0;
public static final int TYPE_FOLDER = 1;
public static final int TYPE_SYSTEM = 2;
@@ -33,18 +30,11 @@ public class Notes {
* {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder
* {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records
*/
- //以下id是系统文件夹的标识符(即系统文件夹的分类)
- //ID_ROOT_FOLDER:默认文件夹
- //ID_TEMPARAY_FOLDER:不属于文件夹的笔记
- //ID_CALL_RECORD_FOLDER:用于存储通话记录,以便返回
- //ID_TRASH_FOLER:垃圾回收站
public static final int ID_ROOT_FOLDER = 0;
public static final int ID_TEMPARAY_FOLDER = -1;
public static final int ID_CALL_RECORD_FOLDER = -2;
public static final int ID_TRASH_FOLER = -3;
- // 额外的数据键,个人理解为就是定义一些布局的ID
- // 这部分就是用于设置UI界面的一些布局或小组件的id,给它定义成常量了。
- // (这样的封装性可能比较好?因为如果有部分要修改,则直接来这边修改即可,不用在activity部分一个一个修改。)
+
public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date";
public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id";
public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id";
@@ -55,14 +45,12 @@ public class Notes {
public static final int TYPE_WIDGET_INVALIDE = -1;
public static final int TYPE_WIDGET_2X = 0;
public static final int TYPE_WIDGET_4X = 1;
- // 数据常量:里面定义了两种类型:文本便签和通话记录
+
public static class DataConstants {
public static final String NOTE = TextNote.CONTENT_ITEM_TYPE;
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE;
}
- //定义一堆访问笔记和文件的uri
- //GPT:Android开发中常见的用于定义内容提供者(Content Provider)URI
- //内容提供者是一种Android组件,它允许应用程序共享和存储数据。这里定义了一个URI来查询数据
+
/**
* Uri to query all notes and folders
*/
@@ -109,8 +97,13 @@ public class Notes {
* Folder's name or text content of note
* Type: TEXT
*/
- // 摘要
public static final String SNIPPET = "snippet";
+
+ /**
+ * Note's title
+ * Type: TEXT
+ */
+ public static final String TITLE = "title";
/**
* Note's widget id
@@ -153,8 +146,6 @@ public class Notes {
* The last sync id
* Type: INTEGER (long)
*/
-
- //在数据同步过程中,这个ID可能用来跟踪和识别每次同步操作的唯一性,确保数据的一致性。
public static final String SYNC_ID = "sync_id";
/**
@@ -180,12 +171,39 @@ public class Notes {
* Type : INTEGER (long)
*/
public static final String VERSION = "version";
+
+ /**
+ * Whether the note is pinned
+ * Type: INTEGER (1=pinned, 0=unpinned)
+ */
+ public static final String PINNED = "pinned";
+
+ /**
+ * Sort order for notes in the same folder
+ * Type: INTEGER
+ */
+ public static final String SORT_ORDER = "sort_order";
+
+ /**
+ * Whether the note is locked
+ * Type: INTEGER (1=locked, 0=unlocked)
+ */
+ public static final String LOCKED = "locked";
+
+ /**
+ * The user id that this note belongs to
+ * Type: INTEGER (long)
+ */
+ public static final String USER_ID = "user_id";
+
+ /**
+ * Whether the note is public
+ * Type: INTEGER (1=public, 0=private)
+ */
+ public static final String PUBLIC = "public";
}
public interface DataColumns {
-
- // DataColumns的接口,这个接口包含了一系列静态常量,这些常量代表了数据库表中用于存储数据的列名。
- // 每个常量都有相应的注释,说明该列的作用和数据类型。
/**
* The unique ID for a row
* Type: INTEGER (long)
@@ -196,42 +214,32 @@ public class Notes {
* The MIME type of the item represented by this row.
* Type: Text
*/
- //MIME类型是一种标准,用于标识文档、文件或字节流的性质和格式。在数据库中,这个字段可以用来识别不同类型的数据,例如文本、图片、音频或视频等。
public static final String MIME_TYPE = "mime_type";
/**
* The reference id to note that this data belongs to
* Type: INTEGER (long)
*/
-
- //归属的Note的ID
public static final String NOTE_ID = "note_id";
/**
* Created data for note or folder
* Type: INTEGER (long)
*/
- //创建日期
public static final String CREATED_DATE = "created_date";
/**
* Latest modified date
* Type: INTEGER (long)
*/
- //最近修改日期
public static final String MODIFIED_DATE = "modified_date";
/**
* Data's content
* Type: TEXT
*/
-
- //数据内容
public static final String CONTENT = "content";
- // 以下5个是通用数据列,它们的具体意义取决于MIME类型(由MIME_TYPE字段指定)。
- // 不同的MIME类型可能需要存储不同类型的数据,这五个字段提供了灵活性,允许根据MIME类型来存储相应的数据。
- // 读后面的代码感觉这部分是在表示内容的不同状态
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
@@ -269,41 +277,39 @@ public class Notes {
public static final String DATA5 = "data5";
}
- //以下是文本便签的定义
public static final class TextNote implements DataColumns {
/**
* Mode to indicate the text in check list mode or not
* Type: Integer 1:check list mode 0: normal mode
*/
- public static final String MODE = DATA1;//模式?这个被存在DATA1列中
+ public static final String MODE = DATA1;
- public static final int MODE_CHECK_LIST = 1;//所处检查列表模式
+ public static final int MODE_CHECK_LIST = 1;
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note";// 定义了MIME类型,用于标识文本标签的目录
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note";
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note";// 定义了MIME类型,用于标识文本标签的单个项
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note";
- public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");//文本标签内容提供者(Content Provider)的URI,用于访问文本标签数据
+ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");
}
- // 通话记录的定义
public static final class CallNote implements DataColumns {
/**
* Call date for this record
* Type: INTEGER (long)
*/
- public static final String CALL_DATE = DATA1;//一个字符串常量,表示通话记录的日期
+ public static final String CALL_DATE = DATA1;
/**
* Phone number for this record
* Type: TEXT
*/
- public static final String PHONE_NUMBER = DATA3;//意味着在数据库表中,这个电话号码信息将被存储在DATA3列中
+ public static final String PHONE_NUMBER = DATA3;
- public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note";// 同样定义了MIME类型,是用于标识通话记录的目录。
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note";
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";// 同样定义了MIME类型,是用于标识通话记录的单个项
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";
- public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");//定义了通话记录内容提供者的URI,用于访问通话记录数据
+ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");
}
}
diff --git a/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java b/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java
index 4a1ba5a..0ef5b73 100644
--- a/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java
+++ b/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package net.micode.notes.data;
import android.content.ContentValues;
@@ -9,256 +25,223 @@ import android.util.Log;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
+import net.micode.notes.data.Messages;
+import net.micode.notes.data.Users;
public class NotesDatabaseHelper extends SQLiteOpenHelper {
- // 数据库帮助类,用于管理名为 note.db 的 SQLite 数据库。
-// 它继承自 SQLiteOpenHelper 类,这是 Android提供的一个方便的工具类,用于管理数据库的创建和版本更新.
- // 数据库的基本信息;数据库名称和版本信息(在创建实例对象时会用到)
private static final String DB_NAME = "note.db";
- private static final int DB_VERSION = 4;
+ private static final int DB_VERSION = 12;
- //内部接口:个人理解为两个表名,一个note,一个data
public interface TABLE {
public static final String NOTE = "note";
public static final String DATA = "data";
+
+ public static final String USER = "user";
+
+ public static final String MESSAGE = "message";
}
- //一个标签,方便日志输出时识别出信息来自哪里
private static final String TAG = "NotesDatabaseHelper";
- //静态所有变量,提供一个全局访问点来获取数据库辅助类的唯一实例,使得在应用的任何地方都可以方便地使用它
private static NotesDatabaseHelper mInstance;
- /* 以下都是一些SQL语句,辅助我们来对数据库进行操作 */
- //创建note表的语句,这里的NoteColumns就是我们刚刚在Notes中定义的一个接口,里面定义了一系列静态的数据库表中的列名
- private static final String CREATE_NOTE_TABLE_SQL =
- "CREATE TABLE " + TABLE.NOTE + "(" +
- NoteColumns.ID + " INTEGER PRIMARY KEY," +
- NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
- NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," +
- NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," +
- NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
- NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," +
- NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
- NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," +
- NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," +
- NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," +
- NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," +
- NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," +
- NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," +
- NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
- NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
- NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
- NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
- ")";
-
- //同上,创建data表的语句,这里的DataColumns就是我们刚刚在Notes中定义的一个接口,里面定义了一系列静态的数据库表中的列名
+ private static final String CREATE_NOTE_TABLE_SQL =
+ "CREATE TABLE " + TABLE.NOTE + "(" +
+ NoteColumns.ID + " INTEGER PRIMARY KEY," +
+ NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
+ NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
+ NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," +
+ NoteColumns.TITLE + " TEXT NOT NULL DEFAULT ''," +
+ NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," +
+ NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
+ NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.PINNED + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.SORT_ORDER + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.LOCKED + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.USER_ID + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.PUBLIC + " INTEGER NOT NULL DEFAULT 0" +
+ ")";
+
private static final String CREATE_DATA_TABLE_SQL =
- "CREATE TABLE " + TABLE.DATA + "(" +
- DataColumns.ID + " INTEGER PRIMARY KEY," +
- DataColumns.MIME_TYPE + " TEXT NOT NULL," +
- DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," +
- NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
- NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
- DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," +
- DataColumns.DATA1 + " INTEGER," +
- DataColumns.DATA2 + " INTEGER," +
- DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," +
- DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," +
- DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
- ")";
-
- // 功能简介:
- // 创建一个以note的ID为索引
- // 解读:
- // 用于在TABLE.DATA表上创建一个名为note_id_index的索引。
- // 这个索引是基于DataColumns.NOTE_ID列的。IF NOT EXISTS确保了如果索引已经存在,那么就不会尝试重新创建它,避免了可能的错误。
- // 索引通常用于提高查询性能,特别是在对某个字段进行频繁查询时。
+ "CREATE TABLE " + TABLE.DATA + "(" +
+ DataColumns.ID + " INTEGER PRIMARY KEY," +
+ DataColumns.MIME_TYPE + " TEXT NOT NULL," +
+ DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
+ NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
+ DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," +
+ DataColumns.DATA1 + " INTEGER," +
+ DataColumns.DATA2 + " INTEGER," +
+ DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," +
+ DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," +
+ DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
+ ")";
+
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
- "CREATE INDEX IF NOT EXISTS note_id_index ON " +
- TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
-
- /* 以下是一些对便签增删改定义的触发器 */
- /* 总结
- * 这些触发器都是用来维护NOTE表和与之相关联的DATA表之间数据一致性的。
- * 当在NOTE表中发生删除或更新操作时,这些触发器会自动执行相应的数据清理或更新操作,确保数据库中的数据保持正确和一致。
- * 特别是在处理文件夹和回收站等逻辑时,这些触发器起到了非常重要的作用,可以自动管理数据的移动和删除。*/
+ "CREATE INDEX IF NOT EXISTS note_id_index ON " +
+ TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
+
+ private static final String CREATE_USER_TABLE_SQL =
+ "CREATE TABLE " + TABLE.USER + "(" +
+ Users.UserColumns.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Users.UserColumns.USERNAME + " TEXT NOT NULL UNIQUE," +
+ Users.UserColumns.PASSWORD + " TEXT NOT NULL," +
+ Users.UserColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
+ Users.UserColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)" +
+ ")";
+
+ private static final String CREATE_MESSAGE_TABLE_SQL =
+ "CREATE TABLE " + TABLE.MESSAGE + "(" +
+ Messages.MessageColumns.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Messages.MessageColumns.SENDER_ID + " INTEGER NOT NULL," +
+ Messages.MessageColumns.RECEIVER_ID + " INTEGER NOT NULL," +
+ Messages.MessageColumns.CONTENT + " TEXT NOT NULL," +
+ Messages.MessageColumns.MESSAGE_TYPE + " INTEGER NOT NULL DEFAULT 0," +
+ Messages.MessageColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
+ Messages.MessageColumns.IS_READ + " INTEGER NOT NULL DEFAULT 0" +
+ ")";
+
/**
* Increase folder's note count when move note to the folder
*/
- // 功能简介:
- // 添加触发器:增加文件夹的便签个数记录(因为我们会移动便签进入文件夹,这时候文件夹的计数要进行更新)
- // 解读:
- // 定义了一个SQL触发器increase_folder_count_on_update。
- // 触发器是一种特殊的存储过程,它会在指定表上的指定事件(如INSERT、UPDATE、DELETE)发生时自动执行。
- // 这个触发器会在TABLE.NOTE表的NoteColumns.PARENT_ID字段更新后执行。
- // 触发器的逻辑是:当某个笔记的PARENT_ID(即父文件夹ID)被更新时,它会找到对应的文件夹(通过新的PARENT_ID),并将该文件夹的NOTES_COUNT(即笔记数)增加1。
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
- "CREATE TRIGGER increase_folder_count_on_update "+
- " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
- " BEGIN " +
- " UPDATE " + TABLE.NOTE +
- " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
- " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
- " END";
+ "CREATE TRIGGER increase_folder_count_on_update "+
+ " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
+ " BEGIN " +
+ " UPDATE " + TABLE.NOTE +
+ " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
+ " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
+ " END";
/**
* Decrease folder's note count when move note from folder
*/
- // 功能简介:(触发器和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
- // 添加触发器:减少文件夹的便签个数记录(因为我们会移动便签移出文件夹,这时候文件夹的计数要进行更新)
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
- "CREATE TRIGGER decrease_folder_count_on_update " +
- " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
- " BEGIN " +
- " UPDATE " + TABLE.NOTE +
- " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
- " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
- " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" +
- " END";
+ "CREATE TRIGGER decrease_folder_count_on_update " +
+ " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
+ " BEGIN " +
+ " UPDATE " + TABLE.NOTE +
+ " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
+ " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
+ " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" +
+ " END";
/**
* Increase folder's note count when insert new note to the folder
*/
- // 功能简介:(触发器原理和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
- // 添加触发器:当我们在文件夹插入便签时,增加文件夹的便签个数记录
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER =
- "CREATE TRIGGER increase_folder_count_on_insert " +
- " AFTER INSERT ON " + TABLE.NOTE +
- " BEGIN " +
- " UPDATE " + TABLE.NOTE +
- " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
- " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
- " END";
+ "CREATE TRIGGER increase_folder_count_on_insert " +
+ " AFTER INSERT ON " + TABLE.NOTE +
+ " BEGIN " +
+ " UPDATE " + TABLE.NOTE +
+ " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
+ " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
+ " END";
/**
* Decrease folder's note count when delete note from the folder
*/
- // 功能简介:(触发器原理和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
- // 添加触发器:当我们在文件夹删除便签时,减少文件夹的便签个数记录
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER =
- "CREATE TRIGGER decrease_folder_count_on_delete " +
- " AFTER DELETE ON " + TABLE.NOTE +
- " BEGIN " +
- " UPDATE " + TABLE.NOTE +
- " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
- " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
- " AND " + NoteColumns.NOTES_COUNT + ">0;" +
- " END";
+ "CREATE TRIGGER decrease_folder_count_on_delete " +
+ " AFTER DELETE ON " + TABLE.NOTE +
+ " BEGIN " +
+ " UPDATE " + TABLE.NOTE +
+ " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
+ " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
+ " AND " + NoteColumns.NOTES_COUNT + ">0;" +
+ " END";
/**
* Update note's content when insert data with type {@link DataConstants#NOTE}
*/
- // 功能简介:
- // 添加触发器:当向DATA表中插入类型为NOTE(便签)的数据时,更新note表对应的笔记内容。
- // 解读:
- // 在DATA表上进行INSERT操作后,如果新插入的数据的MIME_TYPE为NOTE,则触发此操作。
- // 它会更新NOTE表,将与新插入数据相关联的标签的SNIPPET(摘要)字段设置为新插入数据的CONTENT字段的值
private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER =
- "CREATE TRIGGER update_note_content_on_insert " +
- " AFTER INSERT ON " + TABLE.DATA +
- " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
- " BEGIN" +
- " UPDATE " + TABLE.NOTE +
- " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
- " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
- " END";
+ "CREATE TRIGGER update_note_content_on_insert " +
+ " AFTER INSERT ON " + TABLE.DATA +
+ " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
+ " BEGIN" +
+ " UPDATE " + TABLE.NOTE +
+ " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
+ " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
+ " END";
/**
* Update note's content when data with {@link DataConstants#NOTE} type has changed
*/
- // 功能简介:
- // 添加触发器:当DATA表中,类型为NOTE(便签)的数据更改时,更新note表对应的笔记内容。
- // 解读:
- // 在DATA表上进行UPDATE操作后,如果更新前的数据的MIME_TYPE为NOTE,则触发此操作。
- // 它会更新NOTE表,将与更新后的数据相关联的笔记的SNIPPET字段设置为新数据的CONTENT字段的值
private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER =
- "CREATE TRIGGER update_note_content_on_update " +
- " AFTER UPDATE ON " + TABLE.DATA +
- " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
- " BEGIN" +
- " UPDATE " + TABLE.NOTE +
- " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
- " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
- " END";
+ "CREATE TRIGGER update_note_content_on_update " +
+ " AFTER UPDATE ON " + TABLE.DATA +
+ " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
+ " BEGIN" +
+ " UPDATE " + TABLE.NOTE +
+ " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
+ " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
+ " END";
/**
* Update note's content when data with {@link DataConstants#NOTE} type has deleted
*/
- // 功能简介:
- // 添加触发器:当DATA表中,类型为NOTE(便签)的数据删除时,更新note表对应的笔记内容(置空)。
- // 解读:
- // 在DATA表上进行DELETE操作后,如果删除的数据的MIME_TYPE为NOTE,则触发此操作。
- // 它会更新NOTE表,将与删除的数据相关联的笔记的SNIPPET字段设置为空字符串。
private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER =
- "CREATE TRIGGER update_note_content_on_delete " +
- " AFTER delete ON " + TABLE.DATA +
- " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
- " BEGIN" +
- " UPDATE " + TABLE.NOTE +
- " SET " + NoteColumns.SNIPPET + "=''" +
- " WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" +
- " END";
+ "CREATE TRIGGER update_note_content_on_delete " +
+ " AFTER delete ON " + TABLE.DATA +
+ " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
+ " BEGIN" +
+ " UPDATE " + TABLE.NOTE +
+ " SET " + NoteColumns.SNIPPET + "=''" +
+ " WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" +
+ " END";
/**
* Delete datas belong to note which has been deleted
*/
- // 功能简介:
- // 添加触发器:当从NOTE表中删除笔记时,删除与该笔记相关联的数据(就是删除data表中为该note的数据)
- // 解读:
- // 在NOTE表上进行DELETE操作后,此触发器被激活。
- // 它会从DATA表中删除所有与已删除的笔记(由old.ID表示)相关联的数据行(通过比较DATA表中的NOTE_ID字段与已删除笔记的ID来实现)
private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER =
- "CREATE TRIGGER delete_data_on_delete " +
- " AFTER DELETE ON " + TABLE.NOTE +
- " BEGIN" +
- " DELETE FROM " + TABLE.DATA +
- " WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" +
- " END";
+ "CREATE TRIGGER delete_data_on_delete " +
+ " AFTER DELETE ON " + TABLE.NOTE +
+ " BEGIN" +
+ " DELETE FROM " + TABLE.DATA +
+ " WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" +
+ " END";
/**
* Delete notes belong to folder which has been deleted
*/
- // 功能简介:
- // 添加触发器:当从NOTE表中删除一个文件夹时,删除该文件夹下的所有笔记。
- // 解读:
- // 在NOTE表上进行DELETE操作后,如果删除的是一个文件夹(由old.ID表示)
- // 触发器会删除所有以该文件夹为父级(PARENT_ID)的笔记(通过比较NOTE表中的PARENT_ID字段与已删除文件夹的ID来实现)
private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER =
- "CREATE TRIGGER folder_delete_notes_on_delete " +
- " AFTER DELETE ON " + TABLE.NOTE +
- " BEGIN" +
- " DELETE FROM " + TABLE.NOTE +
- " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
- " END";
+ "CREATE TRIGGER folder_delete_notes_on_delete " +
+ " AFTER DELETE ON " + TABLE.NOTE +
+ " BEGIN" +
+ " DELETE FROM " + TABLE.NOTE +
+ " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
+ " END";
/**
* Move notes belong to folder which has been moved to trash folder
*/
- // 功能简介:
- // 添加触发器:当某个文件夹被移动到回收站时,移动该文件夹下的所有笔记到回收站
- // 解读:
- // 在NOTE表上进行UPDATE操作后,如果某个文件夹的新PARENT_ID字段值等于回收站的ID(Notes.ID_TRASH_FOLER)
- // 触发器会更新所有以该文件夹为父级(PARENT_ID)的笔记,将它们也移动到回收站。
private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER =
- "CREATE TRIGGER folder_move_notes_on_trash " +
- " AFTER UPDATE ON " + TABLE.NOTE +
- " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
- " BEGIN" +
- " UPDATE " + TABLE.NOTE +
- " SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
- " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
- " END";
-
- // 构造器
+ "CREATE TRIGGER folder_move_notes_on_trash " +
+ " AFTER UPDATE ON " + TABLE.NOTE +
+ " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
+ " BEGIN" +
+ " UPDATE " + TABLE.NOTE +
+ " SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
+ " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
+ " END";
+
public NotesDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
- // 创建note(标签)表
public void createNoteTable(SQLiteDatabase db) {
db.execSQL(CREATE_NOTE_TABLE_SQL);
reCreateNoteTableTriggers(db);
@@ -266,9 +249,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
Log.d(TAG, "note table has been created");
}
- // 重新创建或更新与笔记表相关的触发器。
- // 首先,使用DROP TRIGGER IF EXISTS语句删除已存在的触发器。确保在重新创建触发器之前,不存在同名的触发器。
- // 然后,使用db.execSQL()方法执行预定义的SQL语句,这些语句用于创建新的触发器。
private void reCreateNoteTableTriggers(SQLiteDatabase db) {
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update");
@@ -287,17 +267,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER);
}
- /* 以下部分是操作SQLite数据库部分 */
- // 功能简介:
- // 创建通话记录文件夹、默认文件夹、临时文件夹和回收站,并插入相关数据
- // 具体解读:
- // ContentValues是一个用于存储键值对的类,常用于SQLite数据库的插入操作
- // values.put方法可以向ContentValues对象中添加数据。
- // NoteColumns.ID是存储文件夹ID的列名,Notes.ID_CALL_RECORD_FOLDER是通话记录文件夹的ID。
- // NoteColumns.TYPE是存储文件夹类型的列名,Notes.TYPE_SYSTEM表示这是一个系统文件夹。
- // 使用db.insert方法将values中的数据插入到TABLE.NOTE(即标签表)中。
- // 每次插入新数据前,都使用values.clear()方法清除ContentValues对象中的旧数据,确保不会重复插入旧数据。
- // 然后分别创建默认文件夹、临时文件夹和回收站,并以同样的方法插入数据。
private void createSystemFolder(SQLiteDatabase db) {
ContentValues values = new ContentValues();
@@ -311,7 +280,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
* root folder which is default folder
*/
- // 创建默认文件夹:重复上述步骤,但这次是为根文件夹插入数据。
values.clear();
values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
@@ -320,7 +288,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
* temporary folder which is used for moving note
*/
- // 创建“临时”文件夹:同样地,为临时文件夹插入数据。
values.clear();
values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
@@ -329,21 +296,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
* create trash folder
*/
- // 创建“回收站”文件夹:最后,为回收站文件夹插入数据。
values.clear();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
- //功能简介:
- //创建data(数据)表
- //解读:
- //这个方法用于创建数据表,以及与之相关的触发器。
- //创建数据表:使用db.execSQL方法执行预定义的SQL语句CREATE_DATA_TABLE_SQL,用于创建数据表。
- //重新创建数据表触发器:调用reCreateDataTableTriggers方法,用于删除并重新创建与数据表相关的触发器。
- //创建索引:使用db.execSQL方法执行CREATE_DATA_NOTE_ID_INDEX_SQL语句,为数据表创建索引。
- //记录日志:使用Log.d方法记录一条调试级别的日志,表示数据表已经创建。
public void createDataTable(SQLiteDatabase db) {
db.execSQL(CREATE_DATA_TABLE_SQL);
reCreateDataTableTriggers(db);
@@ -351,10 +309,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
Log.d(TAG, "data table has been created");
}
- //和上面的note表的reCreate...同理
- //重新创建或更新与笔记表相关的触发器。
- //首先,使用DROP TRIGGER IF EXISTS语句删除已存在的触发器。确保在重新创建触发器之前,不存在同名的触发器。
- //然后,使用db.execSQL()方法执行预定义的SQL语句,这些语句用于创建新的触发器。
private void reCreateDataTableTriggers(SQLiteDatabase db) {
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update");
@@ -365,31 +319,31 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
- //解读:
- //synchronized关键字确保在多线程环境下,只有一个线程能够进入这个方法,防止了同时创建多个实例的情况
- //getInstance(Context context)方法使用了单例模式来确保整个应用程序中只有一个NotesDatabaseHelper实例。
- //它首先检查mInstance(类的静态成员变量,没有在代码片段中显示)是否为null。
- //如果是null,则创建一个新的NotesDatabaseHelper实例,并将其赋值给mInstance。最后返回mInstance。
- static synchronized NotesDatabaseHelper getInstance(Context context) {
+ public static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}
return mInstance;
}
- //功能简介:
- //当数据库首次创建时,onCreate方法会被调用。
- //这里重写onCreate方法,它调用了上述createNoteTable(db)和createDataTable(db)两个方法
- //这样首次创建数据库时就多出了两张表。
+ public void createUserTable(SQLiteDatabase db) {
+ db.execSQL(CREATE_USER_TABLE_SQL);
+ Log.d(TAG, "user table has been created");
+ }
+
+ public void createMessageTable(SQLiteDatabase db) {
+ db.execSQL(CREATE_MESSAGE_TABLE_SQL);
+ Log.d(TAG, "message table has been created");
+ }
+
@Override
public void onCreate(SQLiteDatabase db) {
createNoteTable(db);
createDataTable(db);
+ createUserTable(db);
+ createMessageTable(db);
}
- //功能简介:
- //当数据库需要升级时(即数据库的版本号改变),onUpgrade方法会被调用。
- //该方法会根据当前的oldVersion和新的newVersion来执行相应的升级操作
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
boolean reCreateTriggers = false;
@@ -412,22 +366,57 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
oldVersion++;
}
+ if (oldVersion == 4) {
+ upgradeToV5(db);
+ oldVersion++;
+ }
+
+ if (oldVersion == 5) {
+ upgradeToV6(db);
+ oldVersion++;
+ }
+
+ if (oldVersion == 6) {
+ upgradeToV7(db);
+ oldVersion++;
+ }
+
+ if (oldVersion == 7) {
+ createUserTable(db);
+ oldVersion++;
+ }
+
+ if (oldVersion == 8) {
+ upgradeToV9(db);
+ oldVersion++;
+ }
+
+ if (oldVersion == 9) {
+ upgradeToV10(db);
+ oldVersion++;
+ }
+
+ if (oldVersion == 10) {
+ upgradeToV11(db);
+ oldVersion++;
+ }
+
+ if (oldVersion == 11) {
+ createMessageTable(db);
+ oldVersion++;
+ }
+
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
}
- if (oldVersion != newVersion) { //数据库升级失败,抛出一个异常,表示数据库升级失败
+ if (oldVersion != newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion
+ "fails");
}
}
- //功能简介:
- // 将数据库从版本1升级到版本2。
- //解读:
- // 首先,它删除了已经存在的NOTE和DATA表(如果存在的话)。DROP TABLE IF EXISTS语句确保了即使这些表不存在,也不会抛出错误。
- // 然后,它调用了createNoteTable(db)和createDataTable(db)方法来重新创建这两个表。这意味着在升级到版本2时,这两个表的内容会被完全清除,并重新创建新的空表。
private void upgradeToV2(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
@@ -435,12 +424,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
createDataTable(db);
}
- //功能简介:
- // 将数据库从版本2(或可能是跳过版本2的某个状态)升级到版本3。
- //解读:
- // 首先,删除了三个不再使用的触发器(如果存在的话)。触发器是数据库中的一种对象,可以在插入、更新或删除记录时自动执行某些操作。
- // 然后,使用ALTER TABLE语句修改表结构,向NOTE表中添加了一个名为GTASK_ID的新列,并设置默认值为空字符串。
- // 最后,向NOTE表中插入了一条新的系统文件夹记录,表示一个名为“trash folder”的系统文件夹。这可能是用于存储已删除笔记的回收站功能。
private void upgradeToV3(SQLiteDatabase db) {
// drop unused triggers
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert");
@@ -456,12 +439,38 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.insert(TABLE.NOTE, null, values);
}
- //功能简介:
- // 这个方法负责将数据库从版本3升级到版本4。
- //解读:
- // 它向NOTE表中添加了一个名为VERSION的新列,并设置了默认值为0。这个新列用于记录标签版本信息。
private void upgradeToV4(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
-}
\ No newline at end of file
+
+ private void upgradeToV5(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.PINNED
+ + " INTEGER NOT NULL DEFAULT 0");
+ }
+
+ private void upgradeToV6(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.SORT_ORDER
+ + " INTEGER NOT NULL DEFAULT 0");
+ }
+
+ private void upgradeToV7(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.LOCKED
+ + " INTEGER NOT NULL DEFAULT 0");
+ }
+
+ private void upgradeToV9(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.USER_ID
+ + " INTEGER NOT NULL DEFAULT 0");
+ }
+
+ private void upgradeToV10(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.PUBLIC
+ + " INTEGER NOT NULL DEFAULT 0");
+ }
+
+ private void upgradeToV11(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.TITLE
+ + " TEXT NOT NULL DEFAULT ''");
+ }
+}
diff --git a/src/Notes-master/src/net/micode/notes/data/NotesProvider.java b/src/Notes-master/src/net/micode/notes/data/NotesProvider.java
index e688aa2..6a59beb 100644
--- a/src/Notes-master/src/net/micode/notes/data/NotesProvider.java
+++ b/src/Notes-master/src/net/micode/notes/data/NotesProvider.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package net.micode.notes.data;
@@ -17,27 +33,16 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
+import net.micode.notes.tool.UserManager;
public class NotesProvider extends ContentProvider {
-// Android 应用程序中的一部分:内容提供者(ContentProvider)。
-// 内容提供者是 Android 四大组件之一,它允许应用程序之间共享数据。
-
- //概述:
- //NotesProvider的主要功能是作为一个内容提供者,为其他应用程序或组件提供对“Notes”数据的访问。
- //它允许其他应用程序查询、插入、更新或删除标签数据。
- //通过URI匹配,NotesProvider能够区分对哪种数据类型的请求(例如,单独的标签、标签的数据、文件夹操作等),并执行相应的操作。
-
- //用于匹配不同URI的UriMatcher对象,通常用于解析传入的URI,并确定应该执行哪种操作。
private static final UriMatcher mMatcher;
- //NotesDatabaseHelper实类,用来操作SQLite数据库,负责创建、更新和查询数据库。
private NotesDatabaseHelper mHelper;
- //标签,输出日志时用来表示是该类发出的消息
private static final String TAG = "NotesProvider";
- //6个URI的匹配码,用于区分不同的URI类型
private static final int URI_NOTE = 1;
private static final int URI_NOTE_ITEM = 2;
private static final int URI_DATA = 3;
@@ -46,23 +51,13 @@ public class NotesProvider extends ContentProvider {
private static final int URI_SEARCH = 5;
private static final int URI_SEARCH_SUGGEST = 6;
- //进一步定义了URI匹配规则和搜索查询的投影
- //功能概述:
- //初始化了一个UriMatcher对象mMatcher,并添加了一系列的URI匹配规则。
- //解读:
static {
- //创建了一个UriMatcher实例,并设置默认匹配码为NO_MATCH,表示如果没有任何URI匹配,则返回这个码。
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
- //添加规则,当URI的authority为Notes.AUTHORITY,路径为note时,返回匹配码URI_NOTE。
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE);
- //添加规则,当URI的authority为Notes.AUTHORITY,路径为note/后跟一个数字(#代表数字)时,返回匹配码URI_NOTE_ITEM。
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM);
- //和上面两句同理,但用于匹配数据相关的URI
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA);
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM);
- //用于匹配搜索相关的URI
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH);
- //这两行用于匹配搜索建议相关的URI
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST);
}
@@ -71,147 +66,236 @@ public class NotesProvider extends ContentProvider {
* x'0A' represents the '\n' character in sqlite. For title and content in the search result,
* we will trim '\n' and white space in order to show more information.
*/
- //功能概述:
- //一个 SQL 查询的投影部分,用于定义查询返回的结果集中应该包含哪些列。
- //解读:(每行对应)
- //返回笔记的 ID。
- //笔记的 ID 也被重命名为 SUGGEST_COLUMN_INTENT_EXTRA_DATA,这通常用于 Android 的搜索建议中,作为传递给相关 Intent 的额外数据。
- //对 SNIPPET 列的处理:首先使用 REPLACE 函数将 x'0A'(即换行符 \n)替换为空字符串,然后使用 TRIM 函数删除前后的空白字符,处理后的结果分别重命名为 SUGGEST_COLUMN_TEXT_1
- //对 SNIPPET 列的处理:首先使用 REPLACE 函数将 x'0A'(即换行符 \n)替换为空字符串,然后使用 TRIM 函数删除前后的空白字符,处理后的结果分别重命名为 SUGGEST_COLUMN_TEXT_2
- //返回一个用于搜索建议图标的资源 ID,并命名为 SUGGEST_COLUMN_ICON_1。
- //返回一个固定的 Intent 动作 ACTION_VIEW,并命名为 SUGGEST_COLUMN_INTENT_ACTION。
- //返回一个内容类型,并命名为 SUGGEST_COLUMN_INTENT_DATA。
- private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," //返回笔记的 ID
- + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + ","
- + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + ","
- + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + ","
- + R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + ","
- + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + ","
- + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA;
+ private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + ","
+ + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + ","
+ + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + ","
+ + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + ","
+ + R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + ","
+ + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + ","
+ + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA;
- //功能概述:
- //完整的 SQL 查询语句,用于从 TABLE.NOTE 表中检索信息
- //解读:
- // 使用上面定义的投影来选择数据。
- // 并指定从哪个表中选择数据。
- //WHERE子句包含三个条件:
- // ①搜索 SNIPPET 列中包含特定模式的行(? 是一个占位符,实际查询时会用具体的值替换)。
- // ②父ID不为回收站的ID:排除那些父 ID 为回收站的行。
- // ③只选择类型为note(标签)的行。
private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION
- + " FROM " + TABLE.NOTE
- + " WHERE " + NoteColumns.SNIPPET + " LIKE ?"
- + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER
- + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
+ + " FROM " + TABLE.NOTE
+ + " WHERE " + NoteColumns.SNIPPET + " LIKE ?"
+ + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER
+ + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
- //重写onCreate方法:
- //getContext() 方法被调用以获取当前组件的上下文(Context),以便 NotesDatabaseHelper 能够访问应用程序的资源和其他功能
- //mHelper用于存储从 NotesDatabaseHelper.getInstance 方法返回的实例。这样,该实例就可以在整个组件的其他方法中被访问和使用。
@Override
public boolean onCreate() {
mHelper = NotesDatabaseHelper.getInstance(getContext());
return true;
}
- //功能:查询数据
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
- String sortOrder) {
- //初始化变量:
- //Cursor对象 c,用来存储查询结果
- //使用 NotesDatabaseHelper 的实例 mHelper来获取一个可读的数据库实例
- //定义一个字符串id,用来存储从URI中解析出的ID
+ String sortOrder) {
Cursor c = null;
- SQLiteDatabase db = mHelper.getReadableDatabase();
+ SQLiteDatabase db = null;
String id = null;
-
- //根据匹配不同的URI来进行不同的查询
- switch (mMatcher.match(uri)) {
- // URI_NOTE:查询整个 NOTE 表。
- // URI_NOTE_ITEM:查询 NOTE 表中的特定项。ID 从 URI 的路径段中获取,并添加到查询条件中。
- // URI_DATA:查询整个 DATA 表。
- // URI_DATA_ITEM:查询 DATA 表中的特定项。ID 的获取和处理方式与 URI_NOTE_ITEM 相同。
- case URI_NOTE:
- c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null,
- sortOrder);
- break;
- case URI_NOTE_ITEM:
- id = uri.getPathSegments().get(1);
- c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id
- + parseSelection(selection), selectionArgs, null, null, sortOrder);
- break;
- case URI_DATA:
- c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null,
- sortOrder);
- break;
- case URI_DATA_ITEM:
- id = uri.getPathSegments().get(1);
- c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id
- + parseSelection(selection), selectionArgs, null, null, sortOrder);
- break;
-
- //URI_SEARCH 和 URI_SEARCH_SUGGEST:处理搜索查询。
- // 代码首先检查是否提供了不应与搜索查询一起使用的参数(如 sortOrder, selection, selectionArgs, 或 projection)。
- // 如果提供了这些参数,则抛出一个 IllegalArgumentException。
- // 根据 URI 类型,从 URI 的路径段或查询参数中获取搜索字符串 searchString。
- // 如果 searchString 为空或无效,则返回 null,表示没有搜索结果。
- case URI_SEARCH:
- case URI_SEARCH_SUGGEST:
- if (sortOrder != null || projection != null) {
- throw new IllegalArgumentException(
- "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query");
- }
-
- String searchString = null;
- if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) {
- if (uri.getPathSegments().size() > 1) {
- searchString = uri.getPathSegments().get(1);
+
+ try {
+ db = mHelper.getReadableDatabase();
+
+ // 获取当前用户ID,添加空指针检查
+ long currentUserId = 0;
+ if (getContext() != null) {
+ currentUserId = UserManager.getInstance(getContext()).getCurrentUserId();
+ }
+
+ // 构建用户ID过滤条件,确保currentUserId不为-1
+ long userIdForFilter = currentUserId > 0 ? currentUserId : 0;
+ String userFilter = NoteColumns.USER_ID + " = " + userIdForFilter;
+
+ switch (mMatcher.match(uri)) {
+ case URI_NOTE:
+ // 构建查询条件:
+ // 1. 如果selection中包含USER_ID条件,说明是查询特定用户的便签,直接使用selection
+ // 2. 如果selection中包含PUBLIC条件,说明是查询公开便签,直接使用selection
+ // 3. 否则,系统文件夹(ID <= 0)不受用户ID限制,普通便签需要匹配当前用户ID
+ String finalSelection;
+ if (TextUtils.isEmpty(selection)) {
+ finalSelection = "(" + NoteColumns.ID + " <= 0) OR (" + userFilter + ")";
+ } else {
+ // 检查selection中是否包含USER_ID或PUBLIC条件
+ if (selection.contains(NoteColumns.USER_ID) || selection.contains(NoteColumns.PUBLIC)) {
+ // 如果包含USER_ID或PUBLIC条件,直接使用selection,绕过当前用户过滤
+ finalSelection = selection;
+ } else {
+ // 否则,添加用户ID过滤
+ finalSelection = "((" + NoteColumns.ID + " <= 0) OR (" + userFilter + ")) AND (" + selection + ")";
+ }
+ }
+ c = db.query(TABLE.NOTE, projection, finalSelection, selectionArgs, null, null,
+ sortOrder);
+ break;
+ case URI_NOTE_ITEM:
+ id = uri.getPathSegments().get(1);
+ // 对于单个便签详情,不添加用户过滤条件,允许访问任何便签
+ // 因为FriendNoteListActivity已经确保只显示公开便签
+ if (TextUtils.isEmpty(selection)) {
+ selection = null;
+ }
+ c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id
+ + parseSelection(selection), selectionArgs, null, null, sortOrder);
+ break;
+ case URI_DATA:
+ // 处理DATA表查询,需要考虑便签的PUBLIC属性
+ if (selection != null && selection.contains(DataColumns.NOTE_ID)) {
+ // 如果查询包含NOTE_ID,需要确保对应的便签是公开的或属于当前用户
+ try {
+ // 从selection中提取note_id值
+ String noteIdStr = selection;
+ noteIdStr = noteIdStr.substring(noteIdStr.indexOf('=') + 1);
+ if (noteIdStr.contains("AND")) {
+ noteIdStr = noteIdStr.substring(0, noteIdStr.indexOf("AND")).trim();
+ }
+ long noteId = Long.parseLong(noteIdStr);
+
+ // 查询对应便签的PUBLIC和USER_ID属性
+ Cursor noteCursor = db.query(
+ TABLE.NOTE,
+ new String[]{NoteColumns.PUBLIC, NoteColumns.USER_ID},
+ NoteColumns.ID + "=?",
+ new String[]{String.valueOf(noteId)},
+ null,
+ null,
+ null
+ );
+
+ boolean canAccess = false;
+ if (noteCursor != null && noteCursor.moveToFirst()) {
+ int isPublic = noteCursor.getInt(0);
+ long noteUserId = noteCursor.getLong(1);
+ // 允许访问的条件:便签是公开的或便签属于当前用户
+ canAccess = (isPublic == 1 || noteUserId == currentUserId);
+ noteCursor.close();
+ }
+
+ if (!canAccess) {
+ // 如果不能访问,返回空cursor
+ return null;
+ }
+ } catch (Exception e) {
+ // 如果解析失败,默认允许访问,避免误判
+ Log.e(TAG, "Error parsing note_id from selection: " + e.getMessage());
+ }
+ }
+ c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null,
+ sortOrder);
+ break;
+ case URI_DATA_ITEM:
+ // 处理单个DATA项查询,同样需要考虑便签的PUBLIC属性
+ id = uri.getPathSegments().get(1);
+ // 先查询该DATA项对应的note_id
+ Cursor dataCursor = null;
+ boolean canAccess = false;
+ try {
+ dataCursor = db.query(
+ TABLE.DATA,
+ new String[]{DataColumns.NOTE_ID},
+ DataColumns.ID + "=?",
+ new String[]{id},
+ null,
+ null,
+ null
+ );
+
+ if (dataCursor != null && dataCursor.moveToFirst()) {
+ long noteId = dataCursor.getLong(0);
+
+ // 查询对应便签的PUBLIC和USER_ID属性
+ Cursor noteCursor = null;
+ try {
+ noteCursor = db.query(
+ TABLE.NOTE,
+ new String[]{NoteColumns.PUBLIC, NoteColumns.USER_ID},
+ NoteColumns.ID + "=?",
+ new String[]{String.valueOf(noteId)},
+ null,
+ null,
+ null
+ );
+
+ if (noteCursor != null && noteCursor.moveToFirst()) {
+ int isPublic = noteCursor.getInt(0);
+ long noteUserId = noteCursor.getLong(1);
+ // 允许访问的条件:便签是公开的或便签属于当前用户
+ canAccess = (isPublic == 1 || noteUserId == currentUserId);
+ }
+ } finally {
+ if (noteCursor != null) {
+ noteCursor.close();
+ }
+ }
+ }
+ } finally {
+ if (dataCursor != null) {
+ dataCursor.close();
+ }
+ }
+
+ if (canAccess) {
+ c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id
+ + parseSelection(selection), selectionArgs, null, null, sortOrder);
+ }
+ // 如果不能访问,返回null,默认处理
+ break;
+ case URI_SEARCH:
+ case URI_SEARCH_SUGGEST:
+ if (sortOrder != null || projection != null) {
+ throw new IllegalArgumentException(
+ "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query");
}
- } else {
- searchString = uri.getQueryParameter("pattern");
- }
- if (TextUtils.isEmpty(searchString)) {
- return null;
- }
+ String searchString = null;
+ if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) {
+ if (uri.getPathSegments().size() > 1) {
+ searchString = uri.getPathSegments().get(1);
+ }
+ } else {
+ searchString = uri.getQueryParameter("pattern");
+ }
- //字符串格式化:格式化后的字符串就会是 "%s%",即包含s是任何文本
- //然后执行原始SQL查询
- try {
- searchString = String.format("%%%s%%", searchString);
- c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
- new String[] { searchString });
- } catch (IllegalStateException ex) {
- Log.e(TAG, "got exception: " + ex.toString());
- }
- break;
+ if (TextUtils.isEmpty(searchString)) {
+ return null;
+ }
- //未知URI处理:
- default:
- throw new IllegalArgumentException("Unknown URI " + uri);
- }
- //如果查询结果不为空(即 Cursor 对象 c 不是 null),则为其设置一个通知 URI。
- //这意味着当与这个 URI 关联的数据发生变化时,任何注册了监听这个 URI 的 ContentObserver 都会被通知。
- if (c != null) {
- c.setNotificationUri(getContext().getContentResolver(), uri);
+ try {
+ searchString = String.format("%%%s%%", searchString);
+ c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
+ new String[] { searchString });
+ } catch (IllegalStateException ex) {
+ Log.e(TAG, "got exception: " + ex.toString());
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ if (c != null) {
+ c.setNotificationUri(getContext().getContentResolver(), uri);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "query exception: " + e.toString());
+ e.printStackTrace();
}
return c;
}
- //功能:插入数据
- //参数:Uri 用来标识要插入数据的表,ContentValues对象包含要插入的键值对
@Override
public Uri insert(Uri uri, ContentValues values) {
- //获取数据库
- //三个长整型变量,分别用来存储数据项ID、便签ID 和插入行的ID
SQLiteDatabase db = mHelper.getWritableDatabase();
long dataId = 0, noteId = 0, insertedId = 0;
-
- //对于 URI_NOTE,将values插入到 TABLE.NOTE 表中,并返回插入行的 ID。
- //对于 URI_DATA,首先检查values是否包含 DataColumns.NOTE_ID,如果包含,则获取其值。如果不包含,记录一条日志信息。然后,将 values 插入到 TABLE.DATA 表中,并返回插入行的 ID。
- //如果 uri 不是已知的 URI 类型,则抛出一个 IllegalArgumentException。
+
+ // 获取当前用户ID,添加空指针检查
+ long currentUserId = 0;
+ if (getContext() != null) {
+ currentUserId = UserManager.getInstance(getContext()).getCurrentUserId();
+ }
+
switch (mMatcher.match(uri)) {
case URI_NOTE:
+ // 插入新便签时添加当前用户ID
+ values.put(NoteColumns.USER_ID, currentUserId);
insertedId = noteId = db.insert(TABLE.NOTE, null, values);
break;
case URI_DATA:
@@ -225,10 +309,6 @@ public class NotesProvider extends ContentProvider {
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
-
- //功能:通知变化
- //如果noteId 或 dataId 大于 0(即成功插入了数据),则使用 ContentResolver 的 notifyChange 方法通知监听这些 URI 的观察者,告知数据已经改变。
- //ContentUris.withAppendedId 方法用于在基本 URI 后面追加一个 ID,形成完整的 URI。
// Notify the note uri
if (noteId > 0) {
getContext().getContentResolver().notifyChange(
@@ -241,29 +321,33 @@ public class NotesProvider extends ContentProvider {
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
}
- //返回包含新插入数据项ID 的 Uri。允许调用者知道新插入的数据项的位置
return ContentUris.withAppendedId(uri, insertedId);
}
- //功能:删除数据项
- //参数:uri:标识要删除数据的表或数据项。 selection:一个可选的 WHERE 子句,用于指定删除条件。 selectionArgs:一个可选的字符串数组,用于替换 selection 中的占位符
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
- //count:记录被删除的行数。
- //id:用于存储从 URI 中解析出的数据项 ID。
- //db:可写的数据库对象,用于执行删除操作。
- //deleteData:一个布尔值,用于标记是否删除了 DATA 表中的数据。
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean deleteData = false;
-
+
+ // 获取当前用户ID,添加空指针检查
+ long currentUserId = 0;
+ if (getContext() != null) {
+ currentUserId = UserManager.getInstance(getContext()).getCurrentUserId();
+ }
+
+ // 构建用户ID过滤条件
+ String userFilter = NoteColumns.USER_ID + " = " + currentUserId;
+
switch (mMatcher.match(uri)) {
- //URI_NOTE: 修改 selection 语句:确保只删除 ID 大于 0 的笔记。然后执行删除操作并返回被删除的行数。
- //URI_NOTE_ITEM: 从 URI 中解析出 ID。检查 ID 是否小于等于 0,如果是,则不执行删除操作;否则执行删除操作并返回被删除的行数
- //URI_DATA: 执行删除操作并返回被删除的行数。设置 deleteData 为 true,表示删除了 DATA 表中的数据。
- //URI_DATA_ITEM: 先从 URI 中解析出 ID,然后执行删除操作并返回被删除的行数,并设置 deleteData 为 true,表示删除了 DATA 表中的数据。
case URI_NOTE:
+ // 添加用户ID过滤
+ if (TextUtils.isEmpty(selection)) {
+ selection = userFilter;
+ } else {
+ selection = userFilter + " AND (" + selection + ")";
+ }
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
count = db.delete(TABLE.NOTE, selection, selectionArgs);
break;
@@ -277,6 +361,12 @@ public class NotesProvider extends ContentProvider {
if (noteId <= 0) {
break;
}
+ // 添加用户ID过滤
+ if (TextUtils.isEmpty(selection)) {
+ selection = userFilter;
+ } else {
+ selection = userFilter + " AND (" + selection + ")";
+ }
count = db.delete(TABLE.NOTE,
NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
break;
@@ -293,45 +383,45 @@ public class NotesProvider extends ContentProvider {
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
-
- //如果 count 大于 0,说明有数据被删除。
- //如果 deleteData 为 true,则通知监听 Notes.CONTENT_NOTE_URI 的观察者,数据已改变。
- //通知监听传入 uri 的观察者数据已改变。
if (count > 0) {
if (deleteData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
getContext().getContentResolver().notifyChange(uri, null);
}
-
return count;
}
- //功能:更新数据库的数据
- //参数:uri:标识要更新数据的表或数据项。 values:一个包含新值的键值对集合。
- // selection:一个可选的 WHERE 子句,用于指定更新条件。 selectionArgs:一个可选的字符串数组,用于替换 selection 中的占位符。
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- //count:记录被更新的行数。
- //id:用于存储从 URI 中解析出的数据项 ID。
- //db:可写的 SQLite 数据库对象,用于执行更新操作。
- //updateData:用于标记是否更新了 data 表中的数据。
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean updateData = false;
-
+
+ // 获取当前用户ID,添加空指针检查
+ long currentUserId = 0;
+ if (getContext() != null) {
+ currentUserId = UserManager.getInstance(getContext()).getCurrentUserId();
+ }
+
+ // 构建用户ID过滤条件
+ String userFilter = NoteColumns.USER_ID + " = " + currentUserId;
+
switch (mMatcher.match(uri)) {
- //URI_NOTE:调用 increaseNoteVersion 方法(用于增加便签版本),然后在note表执行更新操作并返回被更新的行数。
- //URI_NOTE_ITEM:从 URI 中解析出 ID,并调用 increaseNoteVersion 方法,传入解析出的 ID,最后在note表执行更新操作并返回被更新的行数。
- //URI_DATA:在data表执行更新操作并返回被更新的行数。设置 updateData 为 true,表示更新了 DATA 表中的数据。
- //URI_DATA_ITEM:从 URI 中解析出 ID。执行更新操作并返回被更新的行数。置 updateData 为 true,表示更新了 DATA 表中的数据。
case URI_NOTE:
+ // 添加用户ID过滤
+ if (TextUtils.isEmpty(selection)) {
+ selection = userFilter;
+ } else {
+ selection = userFilter + " AND (" + selection + ")";
+ }
increaseNoteVersion(-1, selection, selectionArgs);
count = db.update(TABLE.NOTE, values, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
+ // 对于单个便签更新,NoteColumns.ID是主键,已经足够唯一,不需要添加用户ID过滤
increaseNoteVersion(Long.valueOf(id), selection, selectionArgs);
count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
@@ -350,9 +440,6 @@ public class NotesProvider extends ContentProvider {
throw new IllegalArgumentException("Unknown URI " + uri);
}
- //如果 count 大于 0,说明有数据被更新。
- //如果 updateData 为 true,则通知监听 Notes.CONTENT_NOTE_URI 的观察者数据已改变。
- //通知监听传入 uri 的观察者数据已改变。
if (count > 0) {
if (updateData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
@@ -362,12 +449,10 @@ public class NotesProvider extends ContentProvider {
return count;
}
- //解析传入的条件语句:一个 SQL WHERE 子句的一部分
private String parseSelection(String selection) {
return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
}
- //更新note表的version列,将其值增加 1。
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
@@ -399,4 +484,4 @@ public class NotesProvider extends ContentProvider {
return null;
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/data/Users.java b/src/Notes-master/src/net/micode/notes/data/Users.java
new file mode 100644
index 0000000..1ba7b7f
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/data/Users.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.micode.notes.data;
+
+public class Users {
+ public interface UserColumns {
+ public static final String ID = "_id";
+ public static final String USERNAME = "username";
+ public static final String PASSWORD = "password";
+ public static final String CREATED_DATE = "created_date";
+ public static final String MODIFIED_DATE = "modified_date";
+ }
+
+ public interface UserConstants {
+ // 可以添加一些用户相关的常量
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/MetaData.java b/src/Notes-master/src/net/micode/notes/gtask/data/MetaData.java
index 2506b02..3a2050b 100644
--- a/src/Notes-master/src/net/micode/notes/gtask/data/MetaData.java
+++ b/src/Notes-master/src/net/micode/notes/gtask/data/MetaData.java
@@ -16,124 +16,67 @@
package net.micode.notes.gtask.data;
-import android.database.Cursor; // 数据库查询游标:父类方法参数,此类未实际使用
-import android.util.Log; // 日志工具:打印错误/警告日志
+import android.database.Cursor;
+import android.util.Log;
-import net.micode.notes.tool.GTaskStringUtils; // GTask字符串工具类:存储元数据相关常量(如Gid字段名)
+import net.micode.notes.tool.GTaskStringUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
-import org.json.JSONException; // JSON解析异常:JSON操作失败时抛出
-import org.json.JSONObject; // JSON对象:存储和解析元数据键值对
-/**
- * GTask元数据封装类
- * 通俗说:这个类是GTask的“关联信息管家”,继承自GTask任务类(Task),
- * 专门用来存储本地便签和远程GTask任务之间的关联元信息(核心是远程GTask的唯一ID),
- * 负责元信息的JSON序列化(转为字符串存储)和反序列化(从字符串解析出关联ID)
- */
public class MetaData extends Task {
- // 日志标签:使用类名作为标签,方便定位该类的日志信息
private final static String TAG = MetaData.class.getSimpleName();
- // 关联的远程GTask唯一ID:用来绑定本地便签和远程GTask任务(null表示未关联)
private String mRelatedGid = null;
- /**
- * 设置元数据(绑定远程GTask ID并序列化)
- * 通俗说:把远程GTask的唯一ID存入元信息JSON对象,再将JSON转为字符串作为任务备注,
- * 同时把任务名称设为元数据专用名称,方便GTask识别这是元数据任务
- * @param gid 远程GTask任务的唯一ID(关联标识)
- * @param metaInfo 元信息JSON对象(用于存储额外元数据)
- */
public void setMeta(String gid, JSONObject metaInfo) {
try {
- // 将远程GTask ID存入JSON对象,键为元数据头部GTask ID常量
metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid);
} catch (JSONException e) {
- // JSON存入失败,打印错误日志
Log.e(TAG, "failed to put related gid");
}
- // 将元信息JSON对象转为字符串,设置为当前任务的备注(存储元数据)
setNotes(metaInfo.toString());
- // 将任务名称设为元数据专用名称,标识这是元数据任务
setName(GTaskStringUtils.META_NOTE_NAME);
}
- /**
- * 获取关联的远程GTask唯一ID
- * 通俗说:外部调用该方法,获取本地便签绑定的远程GTask任务ID
- * @return 远程GTask ID(null表示未关联)
- */
public String getRelatedGid() {
return mRelatedGid;
}
- /**
- * 重写父类方法:判断元数据是否值得保存
- * 通俗说:只要元数据的备注(JSON字符串)不为空,就表示有有效元信息,值得保存到GTask
- * @return true=值得保存,false=无需保存
- */
@Override
public boolean isWorthSaving() {
return getNotes() != null;
}
- /**
- * 重写父类方法:从远程GTask的JSON数据中解析元信息
- * 通俗说:当从GTask服务器拉取数据时,用该方法解析元数据任务,提取出关联的GTask ID
- * @param js 远程GTask返回的JSON对象(包含元数据任务信息)
- */
@Override
public void setContentByRemoteJSON(JSONObject js) {
- // 先调用父类方法,解析通用的GTask任务内容(如名称、备注等)
super.setContentByRemoteJSON(js);
-
- // 如果任务备注(元数据JSON字符串)不为空
if (getNotes() != null) {
try {
- // 将备注字符串转为JSON对象(去除首尾空白字符,避免解析失败)
JSONObject metaInfo = new JSONObject(getNotes().trim());
- // 从JSON对象中提取关联的远程GTask ID
mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID);
} catch (JSONException e) {
- // JSON解析失败,打印警告日志,并将关联ID置为null
Log.w(TAG, "failed to get related gid");
mRelatedGid = null;
}
}
}
- /**
- * 重写父类方法:从本地JSON设置内容(禁止调用)
- * 通俗说:MetaData只处理远程GTask的元数据,不需要从本地JSON加载内容,
- * 一旦调用该方法,直接抛出异常提示非法访问
- */
@Override
public void setContentByLocalJSON(JSONObject js) {
- // 抛出非法访问错误,提示该方法不应该被调用
+ // this function should not be called
throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called");
}
- /**
- * 重写父类方法:从内容生成本地JSON(禁止调用)
- * 通俗说:MetaData不需要生成本地JSON数据,一旦调用该方法,直接抛出异常提示非法访问
- * @return 无返回值(直接抛出异常)
- */
@Override
public JSONObject getLocalJSONFromContent() {
- // 抛出非法访问错误,提示该方法不应该被调用
throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called");
}
- /**
- * 重写父类方法:获取同步动作(禁止调用)
- * 通俗说:MetaData不需要处理本地数据库的同步动作判断,一旦调用该方法,直接抛出异常提示非法访问
- * @param c 数据库查询游标(此类未使用)
- * @return 无返回值(直接抛出异常)
- */
@Override
public int getSyncAction(Cursor c) {
- // 抛出非法访问错误,提示该方法不应该被调用
throw new IllegalAccessError("MetaData:getSyncAction should not be called");
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/Node.java b/src/Notes-master/src/net/micode/notes/gtask/data/Node.java
index 92e99d1..63950e0 100644
--- a/src/Notes-master/src/net/micode/notes/gtask/data/Node.java
+++ b/src/Notes-master/src/net/micode/notes/gtask/data/Node.java
@@ -16,147 +16,86 @@
package net.micode.notes.gtask.data;
-import android.database.Cursor; // 数据库查询游标:用于子类查询本地数据库,判断同步动作
+import android.database.Cursor;
-import org.json.JSONObject; // JSON对象:用于封装GTask接口所需参数,或存储本地数据
+import org.json.JSONObject;
-/**
- * GTask同步抽象基类
- * 通俗说:这个类是所有GTask同步数据(如任务、元数据、文件夹)的“通用模板”,
- * 定义了同步所需的所有动作常量(比如新增、删除、更新)、通用属性(远程ID、名称等),
- * 还约定了子类必须实现的同步相关抽象方法,统一了GTask同步的核心流程规范
- */
public abstract class Node {
- // 同步动作常量:表示无任何同步操作(本地和远程数据一致)
public static final int SYNC_ACTION_NONE = 0;
- // 同步动作常量:往远程GTask服务器添加新数据(本地有新增,远程没有)
+
public static final int SYNC_ACTION_ADD_REMOTE = 1;
- // 同步动作常量:往本地数据库添加新数据(远程有新增,本地没有)
+
public static final int SYNC_ACTION_ADD_LOCAL = 2;
- // 同步动作常量:删除远程GTask服务器上的数据(本地已删除,远程还存在)
+
public static final int SYNC_ACTION_DEL_REMOTE = 3;
- // 同步动作常量:删除本地数据库里的数据(远程已删除,本地还存在)
+
public static final int SYNC_ACTION_DEL_LOCAL = 4;
- // 同步动作常量:更新远程GTask服务器上的数据(本地修改比远程新)
+
public static final int SYNC_ACTION_UPDATE_REMOTE = 5;
- // 同步动作常量:更新本地数据库里的数据(远程修改比本地新)
+
public static final int SYNC_ACTION_UPDATE_LOCAL = 6;
- // 同步动作常量:同步冲突(本地和远程都有修改,无法自动合并)
+
public static final int SYNC_ACTION_UPDATE_CONFLICT = 7;
- // 同步动作常量:同步异常(同步过程中出现错误,无法判断动作类型)
+
public static final int SYNC_ACTION_ERROR = 8;
- // 远程GTask唯一标识(Gid):每个同步数据在GTask服务器上的唯一ID(null表示未上传到远程)
private String mGid;
- // 节点名称:同步数据的名称(比如便签标题、文件夹名称)
+
private String mName;
- // 最后修改时间戳:数据最后一次被修改的时间(用于判断本地和远程哪个更新)
+
private long mLastModified;
- // 删除标记:是否被标记为删除(true=已删除,需要同步删除操作)
+
private boolean mDeleted;
- /**
- * 构造方法:初始化同步节点的默认属性
- * 通俗说:创建Node对象时,给所有通用属性设置默认值(未关联远程、空名称、无修改、未删除)
- */
public Node() {
- mGid = null; // 默认未关联远程GTask,无Gid
- mName = ""; // 默认名称为空字符串
- mLastModified = 0; // 默认最后修改时间为0(未修改)
- mDeleted = false; // 默认未被删除
+ mGid = null;
+ mName = "";
+ mLastModified = 0;
+ mDeleted = false;
}
- /**
- * 抽象方法:获取创建动作的JSON对象
- * 通俗说:子类必须实现该方法,封装往GTask服务器创建数据所需的参数(转为JSON格式),
- * 供GTask接口调用(比如创建新便签任务)
- * @param actionId 动作ID(GTask接口所需的唯一动作标识)
- * @return 创建动作的JSON参数对象
- */
public abstract JSONObject getCreateAction(int actionId);
- /**
- * 抽象方法:获取更新动作的JSON对象
- * 通俗说:子类必须实现该方法,封装往GTask服务器更新数据所需的参数(转为JSON格式),
- * 供GTask接口调用(比如更新已有的便签内容)
- * @param actionId 动作ID(GTask接口所需的唯一动作标识)
- * @return 更新动作的JSON参数对象
- */
public abstract JSONObject getUpdateAction(int actionId);
- /**
- * 抽象方法:从远程GTask的JSON数据中设置内容
- * 通俗说:子类必须实现该方法,当从GTask服务器拉取数据后,
- * 解析返回的JSON对象,把数据赋值给本地属性(比如解析远程便签内容,存入本地对象)
- * @param js 远程GTask返回的JSON数据对象
- */
public abstract void setContentByRemoteJSON(JSONObject js);
- /**
- * 抽象方法:从本地JSON数据中设置内容
- * 通俗说:子类必须实现该方法,解析本地存储的JSON数据,
- * 把数据赋值给当前节点的属性(比如从本地数据库读取JSON格式的便签数据,初始化对象)
- * @param js 本地存储的JSON数据对象
- */
public abstract void setContentByLocalJSON(JSONObject js);
- /**
- * 抽象方法:从节点内容生成本地JSON对象
- * 通俗说:子类必须实现该方法,把当前节点的属性(比如便签内容、修改时间)
- * 封装为JSON对象,用于存储到本地数据库(方便后续同步对比)
- * @return 包含节点内容的本地JSON对象
- */
public abstract JSONObject getLocalJSONFromContent();
- /**
- * 抽象方法:判断当前节点的同步动作类型
- * 通俗说:子类必须实现该方法,通过查询本地数据库(Cursor),
- * 对比本地数据和远程数据的状态(是否新增、修改、删除),返回对应的同步动作常量(比如新增远程、更新本地等)
- * @param c 本地数据库查询游标(包含本地数据的查询结果)
- * @return 同步动作类型(对应上面定义的SYNC_ACTION_XXX常量)
- */
public abstract int getSyncAction(Cursor c);
- // 以下是通用属性的Setter方法:设置对应的属性值,供子类或外部调用
- /** 设置远程GTask的唯一标识(Gid) */
public void setGid(String gid) {
this.mGid = gid;
}
- /** 设置节点名称(如便签标题、文件夹名称) */
public void setName(String name) {
this.mName = name;
}
- /** 设置最后修改时间戳(用于同步对比新旧) */
public void setLastModified(long lastModified) {
this.mLastModified = lastModified;
}
- /** 设置删除标记(标记是否需要同步删除操作) */
public void setDeleted(boolean deleted) {
this.mDeleted = deleted;
}
- // 以下是通用属性的Getter方法:获取对应的属性值,供子类或外部调用
- /** 获取远程GTask的唯一标识(Gid) */
public String getGid() {
return this.mGid;
}
- /** 获取节点名称(如便签标题、文件夹名称) */
public String getName() {
return this.mName;
}
- /** 获取最后修改时间戳(用于同步对比新旧) */
public long getLastModified() {
return this.mLastModified;
}
- /** 获取删除标记(判断是否需要同步删除操作) */
public boolean getDeleted() {
return this.mDeleted;
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/SqlData.java b/src/Notes-master/src/net/micode/notes/gtask/data/SqlData.java
index 7dca5e6..d3ec3be 100644
--- a/src/Notes-master/src/net/micode/notes/gtask/data/SqlData.java
+++ b/src/Notes-master/src/net/micode/notes/gtask/data/SqlData.java
@@ -35,260 +35,155 @@ import org.json.JSONException;
import org.json.JSONObject;
-/**
- * 本地便签数据库(数据明细表)操作工具
- * 通俗说:这个类专门用来处理本地便签数据库里“数据明细”表的内容,
- * 负责创建新数据、加载已有数据、记录数据变更、把变更保存到数据库
- */
public class SqlData {
- // 日志标签:打印这个类的日志时,用来标识日志来源
private static final String TAG = SqlData.class.getSimpleName();
- // 无效的编号:用来标记数据还没有有效的唯一ID
private static final int INVALID_ID = -99999;
- // 查询数据明细表时,要获取的字段清单(相当于要查的列名)
public static final String[] PROJECTION_DATA = new String[] {
- DataColumns.ID, // 数据唯一ID
- DataColumns.MIME_TYPE, // 数据类型(比如普通便签、通话便签)
- DataColumns.CONTENT, // 数据内容(比如便签正文)
- DataColumns.DATA1, // 扩展字段1(存一些额外信息,比如便签模式)
- DataColumns.DATA3 // 扩展字段3(存一些额外信息)
+ DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1,
+ DataColumns.DATA3
};
- // 上面字段清单对应的索引位置,方便从查询结果里快速取值
- public static final int DATA_ID_COLUMN = 0; // 数据ID在查询结果里的位置
- public static final int DATA_MIME_TYPE_COLUMN = 1; // 数据类型在查询结果里的位置
- public static final int DATA_CONTENT_COLUMN = 2; // 数据内容在查询结果里的位置
- public static final int DATA_CONTENT_DATA_1_COLUMN = 3; // 扩展字段1在查询结果里的位置
- public static final int DATA_CONTENT_DATA_3_COLUMN = 4; // 扩展字段3在查询结果里的位置
+ public static final int DATA_ID_COLUMN = 0;
+
+ public static final int DATA_MIME_TYPE_COLUMN = 1;
+
+ public static final int DATA_CONTENT_COLUMN = 2;
+
+ public static final int DATA_CONTENT_DATA_1_COLUMN = 3;
+
+ public static final int DATA_CONTENT_DATA_3_COLUMN = 4;
- // 数据库操作工具:用来和本地便签数据库打交道(增删改查)
private ContentResolver mContentResolver;
- // 是否是新数据:true=新创建的(还没存到数据库),false=已存在的(从数据库加载的)
private boolean mIsCreate;
- // 这条数据的唯一ID
private long mDataId;
- // 这条数据的类型(比如普通便签、通话便签)
private String mDataMimeType;
- // 这条数据的内容(比如便签的正文文字)
private String mDataContent;
- // 扩展字段1的值(存额外信息,比如便签是普通模式还是清单模式)
private long mDataContentData1;
- // 扩展字段3的值(存额外信息)
private String mDataContentData3;
- // 数据变更记录:用来存和原来不一样的内容(只更新有变化的部分,提高效率)
private ContentValues mDiffDataValues;
- /**
- * 构造方法:创建一条新的数据(还没存到数据库)
- * @param context 上下文(用来获取数据库操作工具)
- */
public SqlData(Context context) {
- // 获取数据库操作工具
mContentResolver = context.getContentResolver();
- // 标记为新数据(还没存数据库)
mIsCreate = true;
- // 初始化为无效ID(还没有数据库分配的唯一ID)
mDataId = INVALID_ID;
- // 初始数据类型为普通便签
mDataMimeType = DataConstants.NOTE;
- // 初始内容为空字符串
mDataContent = "";
- // 初始扩展字段1的值为0
mDataContentData1 = 0;
- // 初始扩展字段3的值为空字符串
mDataContentData3 = "";
- // 初始化数据变更记录容器
mDiffDataValues = new ContentValues();
}
- /**
- * 构造方法:加载数据库里已有的数据
- * @param context 上下文(用来获取数据库操作工具)
- * @param c 数据库查询结果(里面存着已有的数据信息)
- */
public SqlData(Context context, Cursor c) {
- // 获取数据库操作工具
mContentResolver = context.getContentResolver();
- // 标记为已有数据(不是新创建的)
mIsCreate = false;
- // 从查询结果里加载数据到当前对象
loadFromCursor(c);
- // 初始化数据变更记录容器
mDiffDataValues = new ContentValues();
}
- /**
- * 从数据库查询结果里加载数据
- * 通俗说:把查询结果里的各项信息取出来,赋值给当前对象的属性
- * @param c 数据库查询结果
- */
private void loadFromCursor(Cursor c) {
- // 从查询结果里取数据ID
mDataId = c.getLong(DATA_ID_COLUMN);
- // 从查询结果里取数据类型
mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN);
- // 从查询结果里取数据内容
mDataContent = c.getString(DATA_CONTENT_COLUMN);
- // 从查询结果里取扩展字段1的值
mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN);
- // 从查询结果里取扩展字段3的值
mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN);
}
- /**
- * 从JSON数据包里设置数据内容,并记录变更
- * 通俗说:把JSON里的信息解析出来,更新当前对象的属性,
- * 只有和原来不一样的内容,才记录到变更容器里(准备后续更新数据库)
- * @param js 包含数据信息的JSON数据包
- * @throws JSONException JSON解析失败时抛出异常
- */
public void setContent(JSONObject js) throws JSONException {
- // 从JSON里取数据ID,没有的话就用无效ID
long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID;
- // 如果是新数据,或者当前ID和JSON里的ID不一样,就记录这个变更
if (mIsCreate || mDataId != dataId) {
mDiffDataValues.put(DataColumns.ID, dataId);
}
- // 更新当前数据ID
mDataId = dataId;
- // 从JSON里取数据类型,没有的话就默认是普通便签
String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE)
: DataConstants.NOTE;
- // 如果是新数据,或者当前类型和JSON里的类型不一样,就记录这个变更
if (mIsCreate || !mDataMimeType.equals(dataMimeType)) {
mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType);
}
- // 更新当前数据类型
mDataMimeType = dataMimeType;
- // 从JSON里取数据内容,没有的话就为空字符串
String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : "";
- // 如果是新数据,或者当前内容和JSON里的内容不一样,就记录这个变更
if (mIsCreate || !mDataContent.equals(dataContent)) {
mDiffDataValues.put(DataColumns.CONTENT, dataContent);
}
- // 更新当前数据内容
mDataContent = dataContent;
- // 从JSON里取扩展字段1的值,没有的话就为0
long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0;
- // 如果是新数据,或者当前扩展字段1和JSON里的不一样,就记录这个变更
if (mIsCreate || mDataContentData1 != dataContentData1) {
mDiffDataValues.put(DataColumns.DATA1, dataContentData1);
}
- // 更新当前扩展字段1的值
mDataContentData1 = dataContentData1;
- // 从JSON里取扩展字段3的值,没有的话就为空字符串
String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : "";
- // 如果是新数据,或者当前扩展字段3和JSON里的不一样,就记录这个变更
if (mIsCreate || !mDataContentData3.equals(dataContentData3)) {
mDiffDataValues.put(DataColumns.DATA3, dataContentData3);
}
- // 更新当前扩展字段3的值
mDataContentData3 = dataContentData3;
}
- /**
- * 把当前数据打包成JSON格式
- * 通俗说:把当前对象里的所有属性信息,整理成JSON数据包,方便后续使用
- * @return 包含当前数据信息的JSON数据包
- * @throws JSONException JSON打包失败时抛出异常
- */
public JSONObject getContent() throws JSONException {
- // 如果是新数据(还没存到数据库),打印错误日志并返回null
if (mIsCreate) {
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
- // 创建空的JSON数据包
JSONObject js = new JSONObject();
- // 把数据ID存入JSON
js.put(DataColumns.ID, mDataId);
- // 把数据类型存入JSON
js.put(DataColumns.MIME_TYPE, mDataMimeType);
- // 把数据内容存入JSON
js.put(DataColumns.CONTENT, mDataContent);
- // 把扩展字段1的值存入JSON
js.put(DataColumns.DATA1, mDataContentData1);
- // 把扩展字段3的值存入JSON
js.put(DataColumns.DATA3, mDataContentData3);
- // 返回打包好的JSON
return js;
}
- /**
- * 把数据变更提交到数据库
- * 通俗说:新数据就插入到数据库,已有数据就更新变更的部分,最后清空变更记录
- * @param noteId 这条数据所属的便签ID(关联便签主表)
- * @param validateVersion 是否验证便签版本(避免同步时多人同时修改导致冲突)
- * @param version 要验证的便签版本号
- */
public void commit(long noteId, boolean validateVersion, long version) {
- // 如果是新数据(要插入数据库)
+
if (mIsCreate) {
- // 如果数据ID是无效的,就把ID从变更记录里移除(数据库会自动生成唯一ID)
if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) {
mDiffDataValues.remove(DataColumns.ID);
}
- // 把所属的便签ID存入变更记录(关联到便签主表)
+
mDiffDataValues.put(DataColumns.NOTE_ID, noteId);
- // 把变更记录里的内容插入到数据明细表,返回新数据的URI
Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues);
try {
- // 从返回的URI里提取数据库分配的唯一数据ID
mDataId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
- // 提取ID失败,打印错误日志并抛出异常
Log.e(TAG, "Get note id error :" + e.toString());
throw new ActionFailureException("create note failed");
}
} else {
- // 如果是已有数据(要更新数据库),且有变更内容
if (mDiffDataValues.size() > 0) {
int result = 0;
- // 如果不需要验证便签版本,直接更新数据
if (!validateVersion) {
- result = mContentResolver.update(
- ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, mDataId),
- mDiffDataValues, null, null);
+ result = mContentResolver.update(ContentUris.withAppendedId(
+ Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, null, null);
} else {
- // 如果需要验证便签版本,只有版本匹配时才更新(避免冲突)
- result = mContentResolver.update(
- ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, mDataId),
- mDiffDataValues,
+ result = mContentResolver.update(ContentUris.withAppendedId(
+ Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues,
" ? in (SELECT " + NoteColumns.ID + " FROM " + TABLE.NOTE
- + " WHERE " + NoteColumns.VERSION + "=?)",
- new String[] { String.valueOf(noteId), String.valueOf(version) });
+ + " WHERE " + NoteColumns.VERSION + "=?)", new String[] {
+ String.valueOf(noteId), String.valueOf(version)
+ });
}
- // 如果更新结果为0,说明没有更新成功(可能版本不匹配或数据已被修改)
if (result == 0) {
Log.w(TAG, "there is no update. maybe user updates note when syncing");
}
}
}
- // 清空变更记录(本次提交完成,下次变更重新记录)
mDiffDataValues.clear();
- // 标记为非新数据(就算是刚插入的,现在也已经存到数据库了)
mIsCreate = false;
}
- /**
- * 获取这条数据的唯一ID
- * @return 数据唯一ID
- */
public long getId() {
return mDataId;
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/SqlNote.java b/src/Notes-master/src/net/micode/notes/gtask/data/SqlNote.java
index ae3864f..79a4095 100644
--- a/src/Notes-master/src/net/micode/notes/gtask/data/SqlNote.java
+++ b/src/Notes-master/src/net/micode/notes/gtask/data/SqlNote.java
@@ -38,313 +38,222 @@ import org.json.JSONObject;
import java.util.ArrayList;
-/**
- * 本地便签(主表)操作工具
- * 通俗说:这个类专门管本地便签数据库里“便签主表”的内容,
- * 包含便签的基本信息(比如标题、背景色、创建时间),还关联着便签的明细内容(比如正文),
- * 负责新便签创建、已有便签加载、内容修改记录、打包和保存到数据库
- */
public class SqlNote {
- // 日志标签:打印这个类的日志时,用来标识日志来自这个类
private static final String TAG = SqlNote.class.getSimpleName();
- // 无效的便签ID:用来标记便签还没有有效的唯一编号
private static final int INVALID_ID = -99999;
- // 查询便签主表时,要获取的字段清单(相当于要查的列名,对应便签的各项基本信息)
public static final String[] PROJECTION_NOTE = new String[] {
- NoteColumns.ID, // 便签唯一ID
- NoteColumns.ALERTED_DATE, // 提醒时间
- NoteColumns.BG_COLOR_ID, // 背景颜色ID
- NoteColumns.CREATED_DATE, // 创建时间
- NoteColumns.HAS_ATTACHMENT, // 是否有附件(0=没有,1=有)
- NoteColumns.MODIFIED_DATE, // 最后修改时间
- NoteColumns.NOTES_COUNT, // 子便签数量
- NoteColumns.PARENT_ID, // 所属文件夹ID
- NoteColumns.SNIPPET, // 便签摘要(标题/部分正文)
- NoteColumns.TYPE, // 便签类型(普通便签/文件夹/系统文件夹)
- NoteColumns.WIDGET_ID, // 桌面小组件ID
- NoteColumns.WIDGET_TYPE, // 桌面小组件类型
- NoteColumns.SYNC_ID, // 同步编号
- NoteColumns.LOCAL_MODIFIED, // 本地修改标记
- NoteColumns.ORIGIN_PARENT_ID, // 原始所属文件夹ID
- NoteColumns.GTASK_ID, // 对应云端GTask的ID
- NoteColumns.VERSION // 便签版本号(用来避免同步冲突)
+ NoteColumns.ID, NoteColumns.ALERTED_DATE, NoteColumns.BG_COLOR_ID,
+ NoteColumns.CREATED_DATE, NoteColumns.HAS_ATTACHMENT, NoteColumns.MODIFIED_DATE,
+ NoteColumns.NOTES_COUNT, NoteColumns.PARENT_ID, NoteColumns.SNIPPET, NoteColumns.TYPE,
+ NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, NoteColumns.SYNC_ID,
+ NoteColumns.LOCAL_MODIFIED, NoteColumns.ORIGIN_PARENT_ID, NoteColumns.GTASK_ID,
+ NoteColumns.VERSION
};
- // 上面字段清单对应的索引位置,方便从查询结果里快速取值
- public static final int ID_COLUMN = 0; // 便签ID在查询结果里的位置
- public static final int ALERTED_DATE_COLUMN = 1; // 提醒时间在查询结果里的位置
- public static final int BG_COLOR_ID_COLUMN = 2; // 背景颜色ID在查询结果里的位置
- public static final int CREATED_DATE_COLUMN = 3; // 创建时间在查询结果里的位置
- public static final int HAS_ATTACHMENT_COLUMN = 4; // 是否有附件在查询结果里的位置
- public static final int MODIFIED_DATE_COLUMN = 5; // 最后修改时间在查询结果里的位置
- public static final int NOTES_COUNT_COLUMN = 6; // 子便签数量在查询结果里的位置
- public static final int PARENT_ID_COLUMN = 7; // 所属文件夹ID在查询结果里的位置
- public static final int SNIPPET_COLUMN = 8; // 便签摘要在查询结果里的位置
- public static final int TYPE_COLUMN = 9; // 便签类型在查询结果里的位置
- public static final int WIDGET_ID_COLUMN = 10; // 桌面小组件ID在查询结果里的位置
- public static final int WIDGET_TYPE_COLUMN = 11; // 桌面小组件类型在查询结果里的位置
- public static final int SYNC_ID_COLUMN = 12; // 同步编号在查询结果里的位置
- public static final int LOCAL_MODIFIED_COLUMN = 13; // 本地修改标记在查询结果里的位置
- public static final int ORIGIN_PARENT_ID_COLUMN = 14; // 原始所属文件夹ID在查询结果里的位置
- public static final int GTASK_ID_COLUMN = 15; // 云端GTaskID在查询结果里的位置
- public static final int VERSION_COLUMN = 16; // 便签版本号在查询结果里的位置
-
- // 上下文:用来获取数据库操作工具、默认配置等
+ public static final int ID_COLUMN = 0;
+
+ public static final int ALERTED_DATE_COLUMN = 1;
+
+ public static final int BG_COLOR_ID_COLUMN = 2;
+
+ public static final int CREATED_DATE_COLUMN = 3;
+
+ public static final int HAS_ATTACHMENT_COLUMN = 4;
+
+ public static final int MODIFIED_DATE_COLUMN = 5;
+
+ public static final int NOTES_COUNT_COLUMN = 6;
+
+ public static final int PARENT_ID_COLUMN = 7;
+
+ public static final int SNIPPET_COLUMN = 8;
+
+ public static final int TYPE_COLUMN = 9;
+
+ public static final int WIDGET_ID_COLUMN = 10;
+
+ public static final int WIDGET_TYPE_COLUMN = 11;
+
+ public static final int SYNC_ID_COLUMN = 12;
+
+ public static final int LOCAL_MODIFIED_COLUMN = 13;
+
+ public static final int ORIGIN_PARENT_ID_COLUMN = 14;
+
+ public static final int GTASK_ID_COLUMN = 15;
+
+ public static final int VERSION_COLUMN = 16;
+
private Context mContext;
- // 数据库操作工具:用来和本地便签数据库打交道(增删改查)
+
private ContentResolver mContentResolver;
- // 是否是新便签:true=新创建的(还没存到数据库),false=已存在的(从数据库加载的)
+
private boolean mIsCreate;
- // 便签唯一ID
+
private long mId;
- // 提醒时间(比如设置了早上8点提醒,这里存的是对应的时间戳)
+
private long mAlertDate;
- // 背景颜色ID(对应不同的便签背景色,比如白色、黄色)
+
private int mBgColorId;
- // 创建时间(存的是时间戳,记录便签什么时候被创建)
+
private long mCreatedDate;
- // 是否有附件(0=没有附件,1=有附件)
+
private int mHasAttachment;
- // 最后修改时间(存的是时间戳,记录便签最后一次被修改的时间)
+
private long mModifiedDate;
- // 所属文件夹ID(标记这个便签在哪个文件夹里)
+
private long mParentId;
- // 便签摘要(一般是便签的标题,或者正文的前几句)
+
private String mSnippet;
- // 便签类型(普通便签/文件夹/系统文件夹)
+
private int mType;
- // 桌面小组件ID(如果这个便签添加到桌面,这里存小组件的编号)
+
private int mWidgetId;
- // 桌面小组件类型(标记桌面小组件的样式)
+
private int mWidgetType;
- // 原始所属文件夹ID(记录便签最初在哪个文件夹里)
+
private long mOriginParent;
- // 便签版本号(每次修改都会递增,用来避免同步时多人同时修改导致冲突)
+
private long mVersion;
- // 便签主表的变更记录:只存和原来不一样的内容,后续只更新这些变更,提高效率
+
private ContentValues mDiffNoteValues;
- // 便签的明细数据列表(比如普通便签的正文内容,用SqlData对象存储)
+
private ArrayList mDataList;
- /**
- * 构造方法:创建一条新的便签(还没存到数据库)
- * @param context 上下文(用来获取数据库操作工具、默认背景色等)
- */
public SqlNote(Context context) {
mContext = context;
- // 获取数据库操作工具
mContentResolver = context.getContentResolver();
- // 标记为新便签(还没存数据库)
mIsCreate = true;
- // 初始化为无效ID(还没有数据库分配的唯一ID)
mId = INVALID_ID;
- // 初始提醒时间为0(没有设置提醒)
mAlertDate = 0;
- // 初始背景色为系统默认背景色
mBgColorId = ResourceParser.getDefaultBgId(context);
- // 初始创建时间为当前时间(获取系统当前时间戳)
mCreatedDate = System.currentTimeMillis();
- // 初始没有附件
mHasAttachment = 0;
- // 初始最后修改时间为当前时间
mModifiedDate = System.currentTimeMillis();
- // 初始所属文件夹ID为0(默认在根目录)
mParentId = 0;
- // 初始摘要为空字符串
mSnippet = "";
- // 初始类型为普通便签
mType = Notes.TYPE_NOTE;
- // 初始桌面小组件ID为无效ID(没有添加到桌面)
mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
- // 初始桌面小组件类型为无效类型
mWidgetType = Notes.TYPE_WIDGET_INVALIDE;
- // 初始原始所属文件夹ID为0
mOriginParent = 0;
- // 初始版本号为0
mVersion = 0;
- // 初始化便签主表的变更记录容器
mDiffNoteValues = new ContentValues();
- // 初始化明细数据列表
mDataList = new ArrayList();
}
- /**
- * 构造方法:从数据库查询结果里加载已有便签
- * @param context 上下文(用来获取数据库操作工具)
- * @param c 数据库查询结果(里面存着已有便签的基本信息)
- */
public SqlNote(Context context, Cursor c) {
mContext = context;
- // 获取数据库操作工具
mContentResolver = context.getContentResolver();
- // 标记为已有便签(不是新创建的)
mIsCreate = false;
- // 从查询结果里加载便签基本信息
loadFromCursor(c);
- // 初始化明细数据列表
mDataList = new ArrayList();
- // 如果是普通便签,加载对应的明细内容(比如正文)
if (mType == Notes.TYPE_NOTE)
loadDataContent();
- // 初始化便签主表的变更记录容器
mDiffNoteValues = new ContentValues();
}
- /**
- * 构造方法:通过便签ID加载数据库里已有的便签
- * @param context 上下文(用来获取数据库操作工具)
- * @param id 要加载的便签ID
- */
public SqlNote(Context context, long id) {
mContext = context;
- // 获取数据库操作工具
mContentResolver = context.getContentResolver();
- // 标记为已有便签(不是新创建的)
mIsCreate = false;
- // 通过便签ID查询数据库,再加载便签基本信息
loadFromCursor(id);
- // 初始化明细数据列表
mDataList = new ArrayList();
- // 如果是普通便签,加载对应的明细内容(比如正文)
if (mType == Notes.TYPE_NOTE)
loadDataContent();
- // 初始化便签主表的变更记录容器
mDiffNoteValues = new ContentValues();
+
}
- /**
- * 通过便签ID查询数据库,再加载便签基本信息
- * @param id 要查询的便签ID
- */
private void loadFromCursor(long id) {
- Cursor c = null; // 数据库查询结果容器
+ Cursor c = null;
try {
- // 根据便签ID查询便签主表,获取该便签的基本信息
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)",
- new String[] { String.valueOf(id) }, null);
+ new String[] {
+ String.valueOf(id)
+ }, null);
if (c != null) {
- // 移动到查询结果的第一条(因为ID是唯一的,只有一条结果)
c.moveToNext();
- // 从查询结果里加载数据到当前对象
loadFromCursor(c);
} else {
- // 查询结果为空,打印警告日志
Log.w(TAG, "loadFromCursor: cursor = null");
}
} finally {
- // 不管查询成功与否,最后都关闭查询结果,避免占用资源
if (c != null)
c.close();
}
}
- /**
- * 从数据库查询结果里加载便签基本信息
- * 通俗说:把查询结果里的各项便签信息取出来,赋值给当前对象的属性
- * @param c 数据库查询结果
- */
private void loadFromCursor(Cursor c) {
- mId = c.getLong(ID_COLUMN); // 取便签ID
- mAlertDate = c.getLong(ALERTED_DATE_COLUMN); // 取提醒时间
- mBgColorId = c.getInt(BG_COLOR_ID_COLUMN); // 取背景颜色ID
- mCreatedDate = c.getLong(CREATED_DATE_COLUMN); // 取创建时间
- mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN); // 取是否有附件
- mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN); // 取最后修改时间
- mParentId = c.getLong(PARENT_ID_COLUMN); // 取所属文件夹ID
- mSnippet = c.getString(SNIPPET_COLUMN); // 取便签摘要
- mType = c.getInt(TYPE_COLUMN); // 取便签类型
- mWidgetId = c.getInt(WIDGET_ID_COLUMN); // 取桌面小组件ID
- mWidgetType = c.getInt(WIDGET_TYPE_COLUMN); // 取桌面小组件类型
- mVersion = c.getLong(VERSION_COLUMN); // 取便签版本号
+ mId = c.getLong(ID_COLUMN);
+ mAlertDate = c.getLong(ALERTED_DATE_COLUMN);
+ mBgColorId = c.getInt(BG_COLOR_ID_COLUMN);
+ mCreatedDate = c.getLong(CREATED_DATE_COLUMN);
+ mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN);
+ mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN);
+ mParentId = c.getLong(PARENT_ID_COLUMN);
+ mSnippet = c.getString(SNIPPET_COLUMN);
+ mType = c.getInt(TYPE_COLUMN);
+ mWidgetId = c.getInt(WIDGET_ID_COLUMN);
+ mWidgetType = c.getInt(WIDGET_TYPE_COLUMN);
+ mVersion = c.getLong(VERSION_COLUMN);
}
- /**
- * 加载当前便签关联的明细内容(比如普通便签的正文)
- * 通俗说:根据便签ID查询数据明细表,把关联的明细数据加载到mDataList里
- */
private void loadDataContent() {
- Cursor c = null; // 数据库查询结果容器
- // 先清空现有的明细数据列表
+ Cursor c = null;
mDataList.clear();
try {
- // 根据便签ID查询数据明细表,获取该便签的明细内容
c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA,
- "(note_id=?)", new String[] { String.valueOf(mId) }, null);
+ "(note_id=?)", new String[] {
+ String.valueOf(mId)
+ }, null);
if (c != null) {
- // 如果没有查询到明细数据,打印警告日志并返回
if (c.getCount() == 0) {
Log.w(TAG, "it seems that the note has not data");
return;
}
- // 遍历所有明细数据,逐个加载到mDataList
while (c.moveToNext()) {
- SqlData data = new SqlData(mContext, c); // 从查询结果创建明细数据对象
- mDataList.add(data); // 添加到明细数据列表
+ SqlData data = new SqlData(mContext, c);
+ mDataList.add(data);
}
} else {
- // 查询结果为空,打印警告日志
Log.w(TAG, "loadDataContent: cursor = null");
}
} finally {
- // 不管查询成功与否,最后都关闭查询结果,避免占用资源
if (c != null)
c.close();
}
}
- /**
- * 从JSON数据包里设置便签内容,并记录变更
- * 通俗说:把JSON里的便签信息解析出来,更新当前对象的属性,
- * 有变更的内容就记录到变更容器里,同时处理明细数据(正文)
- * @param js 包含便签信息的JSON数据包
- * @return true=设置成功,false=设置失败(JSON解析出错)
- */
public boolean setContent(JSONObject js) {
try {
- // 从JSON里取出便签基本信息的数据包
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
-
- // 如果是系统文件夹,不允许修改,打印警告日志
if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
Log.w(TAG, "cannot set system folder");
- }
- // 如果是普通文件夹,只能更新摘要和类型
- else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
- // 从JSON里取文件夹摘要,没有的话为空字符串
+ } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
+ // for folder we can only update the snnipet and type
String snippet = note.has(NoteColumns.SNIPPET) ? note
.getString(NoteColumns.SNIPPET) : "";
- // 如果是新文件夹,或者摘要有变化,记录变更
if (mIsCreate || !mSnippet.equals(snippet)) {
mDiffNoteValues.put(NoteColumns.SNIPPET, snippet);
}
- // 更新当前文件夹摘要
mSnippet = snippet;
- // 从JSON里取文件夹类型,没有的话默认是普通便签
int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE)
: Notes.TYPE_NOTE;
- // 如果是新文件夹,或者类型有变化,记录变更
if (mIsCreate || mType != type) {
mDiffNoteValues.put(NoteColumns.TYPE, type);
}
- // 更新当前文件夹类型
mType = type;
- }
- // 如果是普通便签,更新所有基本信息和明细内容
- else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) {
- // 取出明细数据的JSON数组(比如正文内容)
+ } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) {
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
-
- // 处理便签ID
long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID;
if (mIsCreate || mId != id) {
mDiffNoteValues.put(NoteColumns.ID, id);
}
mId = id;
- // 处理提醒时间
long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note
.getLong(NoteColumns.ALERTED_DATE) : 0;
if (mIsCreate || mAlertDate != alertDate) {
@@ -352,7 +261,6 @@ public class SqlNote {
}
mAlertDate = alertDate;
- // 处理背景颜色ID
int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note
.getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext);
if (mIsCreate || mBgColorId != bgColorId) {
@@ -360,7 +268,6 @@ public class SqlNote {
}
mBgColorId = bgColorId;
- // 处理创建时间
long createDate = note.has(NoteColumns.CREATED_DATE) ? note
.getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis();
if (mIsCreate || mCreatedDate != createDate) {
@@ -368,7 +275,6 @@ public class SqlNote {
}
mCreatedDate = createDate;
- // 处理是否有附件
int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note
.getInt(NoteColumns.HAS_ATTACHMENT) : 0;
if (mIsCreate || mHasAttachment != hasAttachment) {
@@ -376,7 +282,6 @@ public class SqlNote {
}
mHasAttachment = hasAttachment;
- // 处理最后修改时间
long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note
.getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis();
if (mIsCreate || mModifiedDate != modifiedDate) {
@@ -384,7 +289,6 @@ public class SqlNote {
}
mModifiedDate = modifiedDate;
- // 处理所属文件夹ID
long parentId = note.has(NoteColumns.PARENT_ID) ? note
.getLong(NoteColumns.PARENT_ID) : 0;
if (mIsCreate || mParentId != parentId) {
@@ -392,7 +296,6 @@ public class SqlNote {
}
mParentId = parentId;
- // 处理便签摘要
String snippet = note.has(NoteColumns.SNIPPET) ? note
.getString(NoteColumns.SNIPPET) : "";
if (mIsCreate || !mSnippet.equals(snippet)) {
@@ -400,7 +303,6 @@ public class SqlNote {
}
mSnippet = snippet;
- // 处理便签类型
int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE)
: Notes.TYPE_NOTE;
if (mIsCreate || mType != type) {
@@ -408,7 +310,6 @@ public class SqlNote {
}
mType = type;
- // 处理桌面小组件ID
int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID)
: AppWidgetManager.INVALID_APPWIDGET_ID;
if (mIsCreate || mWidgetId != widgetId) {
@@ -416,7 +317,6 @@ public class SqlNote {
}
mWidgetId = widgetId;
- // 处理桌面小组件类型
int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note
.getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE;
if (mIsCreate || mWidgetType != widgetType) {
@@ -424,7 +324,6 @@ public class SqlNote {
}
mWidgetType = widgetType;
- // 处理原始所属文件夹ID
long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note
.getLong(NoteColumns.ORIGIN_PARENT_ID) : 0;
if (mIsCreate || mOriginParent != originParent) {
@@ -432,12 +331,9 @@ public class SqlNote {
}
mOriginParent = originParent;
- // 处理明细数据(比如正文内容)
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
SqlData sqlData = null;
-
- // 如果明细数据有ID,先在现有列表里找对应的明细对象
if (data.has(DataColumns.ID)) {
long dataId = data.getLong(DataColumns.ID);
for (SqlData temp : mDataList) {
@@ -447,46 +343,32 @@ public class SqlNote {
}
}
- // 如果没找到对应的明细对象,创建新的并加入列表
if (sqlData == null) {
sqlData = new SqlData(mContext);
mDataList.add(sqlData);
}
- // 给明细对象设置内容,并记录变更
sqlData.setContent(data);
}
}
} catch (JSONException e) {
- // JSON解析出错,打印错误日志并返回false
Log.e(TAG, e.toString());
e.printStackTrace();
return false;
}
- // 设置成功,返回true
return true;
}
- /**
- * 把当前便签内容打包成JSON格式
- * 通俗说:把便签的基本信息和明细内容整理成JSON数据包,方便后续同步或存储
- * @return 包含便签信息的JSON数据包(新便签/打包失败返回null)
- */
public JSONObject getContent() {
try {
- // 创建空的JSON数据包
JSONObject js = new JSONObject();
- // 如果是新便签(还没存到数据库),打印错误日志并返回null
if (mIsCreate) {
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
- // 创建存储便签基本信息的JSON对象
JSONObject note = new JSONObject();
-
- // 如果是普通便签,打包所有基本信息和明细内容
if (mType == Notes.TYPE_NOTE) {
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.ALERTED_DATE, mAlertDate);
@@ -500,174 +382,111 @@ public class SqlNote {
note.put(NoteColumns.WIDGET_ID, mWidgetId);
note.put(NoteColumns.WIDGET_TYPE, mWidgetType);
note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent);
- // 把便签基本信息存入总JSON
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
- // 创建存储明细数据的JSON数组
JSONArray dataArray = new JSONArray();
for (SqlData sqlData : mDataList) {
- // 把每个明细数据打包成JSON
JSONObject data = sqlData.getContent();
if (data != null) {
dataArray.put(data);
}
}
- // 把明细数据数组存入总JSON
js.put(GTaskStringUtils.META_HEAD_DATA, dataArray);
- }
- // 如果是文件夹/系统文件夹,只打包ID、类型和摘要
- else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) {
+ } else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) {
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.TYPE, mType);
note.put(NoteColumns.SNIPPET, mSnippet);
- // 把文件夹信息存入总JSON
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
}
- // 返回打包好的JSON
return js;
} catch (JSONException e) {
- // JSON打包出错,打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
}
- // 打包失败,返回null
return null;
}
- /**
- * 设置便签所属的文件夹ID,并记录变更
- * @param id 文件夹ID
- */
public void setParentId(long id) {
mParentId = id;
mDiffNoteValues.put(NoteColumns.PARENT_ID, id);
}
- /**
- * 设置便签对应的云端GTask ID,并记录变更
- * @param gid 云端GTask的唯一ID
- */
public void setGtaskId(String gid) {
mDiffNoteValues.put(NoteColumns.GTASK_ID, gid);
}
- /**
- * 设置便签的同步编号,并记录变更
- * @param syncId 同步编号
- */
public void setSyncId(long syncId) {
mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId);
}
- /**
- * 重置本地修改标记(标记为未修改),并记录变更
- */
public void resetLocalModified() {
mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0);
}
- /**
- * 获取便签的唯一ID
- * @return 便签ID
- */
public long getId() {
return mId;
}
- /**
- * 获取便签所属的文件夹ID
- * @return 文件夹ID
- */
public long getParentId() {
return mParentId;
}
- /**
- * 获取便签的摘要
- * @return 便签摘要
- */
public String getSnippet() {
return mSnippet;
}
- /**
- * 判断当前是否是普通便签类型
- * @return true=普通便签,false=文件夹/系统文件夹
- */
public boolean isNoteType() {
return mType == Notes.TYPE_NOTE;
}
- /**
- * 把便签的变更提交到数据库
- * 通俗说:新便签就插入到主表,再插入明细数据;已有便签就更新变更的部分,
- * 之后刷新便签信息,清空变更记录
- * @param validateVersion 是否验证便签版本(避免同步时多人同时修改导致冲突)
- */
public void commit(boolean validateVersion) {
- // 如果是新便签(要插入数据库)
if (mIsCreate) {
- // 如果便签ID是无效的,就把ID从变更记录里移除(数据库会自动生成唯一ID)
if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) {
mDiffNoteValues.remove(NoteColumns.ID);
}
- // 把便签基本信息插入到便签主表,返回新便签的URI
Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues);
try {
- // 从返回的URI里提取数据库分配的唯一便签ID
mId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
- // 提取ID失败,打印错误日志并抛出异常
Log.e(TAG, "Get note id error :" + e.toString());
throw new ActionFailureException("create note failed");
}
- // 如果提取的ID为0,说明创建失败,抛出异常
if (mId == 0) {
throw new IllegalStateException("Create thread id failed");
}
- // 如果是普通便签,把明细数据也插入到数据明细表
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, false, -1);
}
}
- }
- // 如果是已有便签(要更新数据库)
- else {
- // 验证便签ID是否有效(除了根文件夹和通话记录文件夹,其他ID不能<=0)
+ } else {
if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) {
Log.e(TAG, "No such note");
throw new IllegalStateException("Try to update note with invalid id");
}
-
- // 如果有变更内容,才执行更新操作
if (mDiffNoteValues.size() > 0) {
- // 版本号递增(每次修改都升级版本)
mVersion ++;
int result = 0;
-
- // 如果不需要验证版本,直接更新便签主表
if (!validateVersion) {
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "("
- + NoteColumns.ID + "=?)", new String[] { String.valueOf(mId) });
- }
- // 如果需要验证版本,只有版本匹配时才更新(避免同步冲突)
- else {
+ + NoteColumns.ID + "=?)", new String[] {
+ String.valueOf(mId)
+ });
+ } else {
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "("
- + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)",
- new String[] { String.valueOf(mId), String.valueOf(mVersion) });
+ + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)",
+ new String[] {
+ String.valueOf(mId), String.valueOf(mVersion)
+ });
}
-
- // 如果更新结果为0,说明没有更新成功(可能版本不匹配或数据已被修改)
if (result == 0) {
Log.w(TAG, "there is no update. maybe user updates note when syncing");
}
}
- // 如果是普通便签,更新对应的明细数据
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, validateVersion, mVersion);
@@ -675,14 +494,12 @@ public class SqlNote {
}
}
- // 刷新便签信息(重新从数据库加载最新数据)
+ // refresh local info
loadFromCursor(mId);
if (mType == Notes.TYPE_NOTE)
loadDataContent();
- // 清空便签主表的变更记录(本次提交完成,下次变更重新记录)
mDiffNoteValues.clear();
- // 标记为非新便签(就算是刚插入的,现在也已经存到数据库了)
mIsCreate = false;
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/Task.java b/src/Notes-master/src/net/micode/notes/gtask/data/Task.java
index dfc9553..6a19454 100644
--- a/src/Notes-master/src/net/micode/notes/gtask/data/Task.java
+++ b/src/Notes-master/src/net/micode/notes/gtask/data/Task.java
@@ -32,174 +32,142 @@ import org.json.JSONException;
import org.json.JSONObject;
-/**
- * GTask云端任务处理类(对应本地普通便签)
- * 通俗说:这个类是用来处理和GTask云端交互的“任务”(对应本地的普通便签),
- * 继承自同步基础模板Node,负责打包创建/更新云端任务的请求、解析云端/本地数据、
- * 判断同步操作类型,还关联着任务的所属列表和兄弟任务
- */
public class Task extends Node {
- // 日志标签:打印这个类的日志时,用来标识日志来自这个类
private static final String TAG = Task.class.getSimpleName();
- // 任务是否完成(true=已完成,false=未完成)
private boolean mCompleted;
- // 任务备注(额外的说明信息)
+
private String mNotes;
- // 本地便签的元信息(存储便签的详细配置,是个JSON数据包)
+
private JSONObject mMetaInfo;
- // 上一个兄弟任务(当前任务在列表里的前一个任务,用来确定任务顺序)
+
private Task mPriorSibling;
- // 所属任务列表(对应本地的文件夹,当前任务在哪个列表/文件夹里)
+
private TaskList mParent;
- /**
- * 构造方法:初始化任务的默认状态
- * 通俗说:创建任务对象时,设置默认值,同时调用父类的初始化方法
- */
public Task() {
- super(); // 调用父类Node的构造方法,初始化同步相关的默认属性
- mCompleted = false; // 默认任务未完成
- mNotes = null; // 默认没有任务备注
- mPriorSibling = null; // 默认没有上一个兄弟任务
- mParent = null; // 默认没有所属任务列表
- mMetaInfo = null; // 默认没有本地便签元信息
+ super();
+ mCompleted = false;
+ mNotes = null;
+ mPriorSibling = null;
+ mParent = null;
+ mMetaInfo = null;
}
- /**
- * 打包“创建云端任务”的请求参数
- * 通俗说:把本地新便签的信息整理成云端能识别的JSON格式,传给云端用来创建新任务
- * @param actionId 动作编号(云端用来识别这次操作的唯一标记)
- * @return 整理好的创建任务请求参数(JSON格式)
- */
public JSONObject getCreateAction(int actionId) {
- JSONObject js = new JSONObject(); // 创建空的JSON数据包
+ JSONObject js = new JSONObject();
try {
- // 1. 设置动作类型:告诉云端这次是“创建任务”
+ // action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
- // 2. 设置动作ID:给这次创建操作一个唯一编号,方便云端识别
+ // action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
- // 3. 设置任务位置:告诉云端这个新任务在所属列表里的排序位置
+ // index
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this));
- // 4. 设置任务核心信息:打包任务的名称、创建者等信息
+ // entity_delta
JSONObject entity = new JSONObject();
- entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 任务名称(对应便签摘要/内容)
- entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); // 创建者ID(这里默认传null)
+ entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
+ entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
- GTaskStringUtils.GTASK_JSON_TYPE_TASK); // 实体类型:告诉云端这是一个“任务”
- if (getNotes() != null) { // 如果有任务备注,就把备注也传过去
+ GTaskStringUtils.GTASK_JSON_TYPE_TASK);
+ if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
- js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 把任务核心信息存入总JSON
+ js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
- // 5. 设置所属列表ID:告诉云端这个任务属于哪个列表(对应本地文件夹)
+ // parent_id
js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid());
- // 6. 设置所属列表类型:告诉云端父级是“任务列表”(文件夹类型)
+ // dest_parent_type
js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
- // 7. 设置列表ID:和所属列表ID一致,确认任务归属
+ // list_id
js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid());
- // 8. 设置上一个兄弟任务ID:如果有前一个任务,就传过去,保证任务排序
+ // prior_sibling_id
if (mPriorSibling != null) {
js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid());
}
} catch (JSONException e) {
- // JSON打包出错,打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-create jsonobject");
}
- return js; // 返回打包好的创建请求参数
+ return js;
}
- /**
- * 打包“更新云端任务”的请求参数
- * 通俗说:把本地修改后的便签信息整理成云端能识别的JSON格式,传给云端用来更新已有任务
- * @param actionId 动作编号(云端用来识别这次操作的唯一标记)
- * @return 整理好的更新任务请求参数(JSON格式)
- */
public JSONObject getUpdateAction(int actionId) {
- JSONObject js = new JSONObject(); // 创建空的JSON数据包
+ JSONObject js = new JSONObject();
try {
- // 1. 设置动作类型:告诉云端这次是“更新任务”
+ // action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
- // 2. 设置动作ID:给这次更新操作一个唯一编号,方便云端识别
+ // action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
- // 3. 设置任务ID:告诉云端要更新哪个任务(用云端给的唯一ID)
+ // id
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
- // 4. 设置要更新的任务信息:打包修改后的名称、备注、删除状态
+ // entity_delta
JSONObject entity = new JSONObject();
- entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 更新后的任务名称
- if (getNotes() != null) { // 如果有备注,就更新备注
+ entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
+ if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
- entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); // 任务是否被删除(标记云端是否要删除该任务)
- js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 把更新信息存入总JSON
+ entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
+ js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
- // JSON打包出错,打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-update jsonobject");
}
- return js; // 返回打包好的更新请求参数
+ return js;
}
- /**
- * 解析云端返回的任务数据,赋值给当前任务对象
- * 通俗说:从云端获取到任务信息后,把数据拆解开,存到当前对象的属性里(同步到本地)
- * @param js 云端返回的任务数据包(JSON格式)
- */
public void setContentByRemoteJSON(JSONObject js) {
- if (js != null) { // 如果云端返回的数据不为空
+ if (js != null) {
try {
- // 1. 提取任务ID(云端给的唯一编号),设置到当前对象
+ // id
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
- // 2. 提取任务最后修改时间,设置到当前对象
+ // last_modified
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
- // 3. 提取任务名称,设置到当前对象
+ // name
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
- // 4. 提取任务备注,设置到当前对象
+ // notes
if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) {
setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES));
}
- // 5. 提取任务删除状态,设置到当前对象
+ // deleted
if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) {
setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED));
}
- // 6. 提取任务完成状态,设置到当前对象
+ // completed
if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) {
setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED));
}
} catch (JSONException e) {
- // JSON解析出错,打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get task content from jsonobject");
@@ -207,180 +175,135 @@ public class Task extends Node {
}
}
- /**
- * 解析本地便签的JSON数据,设置任务内容
- * 通俗说:把本地便签的JSON数据拆解开,提取便签内容作为任务名称(同步到云端任务)
- * @param js 本地便签的JSON数据包
- */
public void setContentByLocalJSON(JSONObject js) {
- // 如果JSON数据为空,或者缺少便签信息/明细内容,打印警告日志
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)
|| !js.has(GTaskStringUtils.META_HEAD_DATA)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
try {
- // 1. 提取本地便签的基本信息
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
- // 2. 提取本地便签的明细内容(比如正文)
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
- // 3. 如果不是普通便签类型,打印错误日志并返回
if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) {
Log.e(TAG, "invalid type");
return;
}
- // 4. 遍历明细内容,提取普通便签的正文作为任务名称
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
- // 判断是否是普通便签的明细内容
if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) {
- setName(data.getString(DataColumns.CONTENT)); // 把便签正文设为任务名称
- break; // 找到后就停止遍历
+ setName(data.getString(DataColumns.CONTENT));
+ break;
}
}
} catch (JSONException e) {
- // JSON解析出错,打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
- /**
- * 把当前任务数据打包成本地便签的JSON格式
- * 通俗说:把云端任务的信息整理成本地便签能识别的JSON格式,方便保存到本地数据库
- * @return 本地便签格式的JSON数据包(打包失败返回null)
- */
public JSONObject getLocalJSONFromContent() {
- String name = getName(); // 获取任务名称(对应便签内容)
+ String name = getName();
try {
- // 情况1:没有本地元信息(云端新建的任务,要同步到本地)
if (mMetaInfo == null) {
- // 如果任务名称为空,打印警告日志并返回null
+ // new task created from web
if (name == null) {
Log.w(TAG, "the note seems to be an empty one");
return null;
}
- // 创建本地便签的JSON数据包
JSONObject js = new JSONObject();
- JSONObject note = new JSONObject(); // 便签基本信息
- JSONArray dataArray = new JSONArray(); // 便签明细内容
- JSONObject data = new JSONObject(); // 明细内容(正文)
- data.put(DataColumns.CONTENT, name); // 把任务名称设为便签正文
- dataArray.put(data); // 把正文加入明细数组
- js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); // 存入明细内容
- note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 标记为普通便签类型
- js.put(GTaskStringUtils.META_HEAD_NOTE, note); // 存入便签基本信息
- return js; // 返回打包好的本地便签JSON
- }
- // 情况2:有本地元信息(已同步过的任务,更新本地内容)
- else {
- // 提取已有的本地便签元信息
+ JSONObject note = new JSONObject();
+ JSONArray dataArray = new JSONArray();
+ JSONObject data = new JSONObject();
+ data.put(DataColumns.CONTENT, name);
+ dataArray.put(data);
+ js.put(GTaskStringUtils.META_HEAD_DATA, dataArray);
+ note.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
+ js.put(GTaskStringUtils.META_HEAD_NOTE, note);
+ return js;
+ } else {
+ // synced task
JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
- // 遍历明细内容,更新便签正文(用最新的任务名称)
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
- // 找到普通便签的明细内容
if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) {
- data.put(DataColumns.CONTENT, getName()); // 更新便签正文
- break; // 找到后停止遍历
+ data.put(DataColumns.CONTENT, getName());
+ break;
}
}
- note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 确保是普通便签类型
- return mMetaInfo; // 返回更新后的本地元信息
+ note.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
+ return mMetaInfo;
}
} catch (JSONException e) {
- // JSON打包出错,打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
return null;
}
}
- /**
- * 设置本地便签的元信息
- * 通俗说:把MetaData里的本地便签信息,转换成JSON格式存到当前任务对象
- * @param metaData 本地便签的元数据对象
- */
public void setMetaInfo(MetaData metaData) {
- // 如果元数据不为空,且包含便签信息
if (metaData != null && metaData.getNotes() != null) {
try {
- // 把元数据里的便签信息转换成JSON对象,存入mMetaInfo
mMetaInfo = new JSONObject(metaData.getNotes());
} catch (JSONException e) {
- // 转换失败,打印警告日志,清空元信息
Log.w(TAG, e.toString());
mMetaInfo = null;
}
}
}
- /**
- * 对比本地和云端数据,判断该执行哪种同步操作
- * 通俗说:查本地数据库,对比便签和云端任务的状态,确定是上传、下载还是冲突
- * @param c 本地数据库查询结果(存着本地便签的信息)
- * @return 同步操作类型(对应Node类里的SYNC_ACTION_XXX常量)
- */
public int getSyncAction(Cursor c) {
try {
JSONObject noteInfo = null;
- // 提取本地便签的元信息(如果存在)
if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) {
noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
}
- // 1. 如果本地元信息为空,说明本地便签已删除,返回“更新云端”(让云端也删除)
if (noteInfo == null) {
Log.w(TAG, "it seems that note meta has been deleted");
return SYNC_ACTION_UPDATE_REMOTE;
}
- // 2. 如果元信息里没有便签ID,说明本地便签无效,返回“更新本地”(用云端数据覆盖)
if (!noteInfo.has(NoteColumns.ID)) {
Log.w(TAG, "remote note id seems to be deleted");
return SYNC_ACTION_UPDATE_LOCAL;
}
- // 3. 验证便签ID是否匹配(本地便签ID和元信息里的ID不一致,返回“更新本地”)
+ // validate the note id now
if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) {
Log.w(TAG, "note id doesn't match");
return SYNC_ACTION_UPDATE_LOCAL;
}
- // 4. 判断本地是否有修改
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
- // 本地没有修改
+ // there is no local update
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
- // 本地和云端都没有修改,返回“无需操作”
+ // no update both side
return SYNC_ACTION_NONE;
} else {
- // 云端有修改,返回“更新本地”(用云端数据覆盖本地)
+ // apply remote to local
return SYNC_ACTION_UPDATE_LOCAL;
}
} else {
- // 本地有修改
- // 验证云端任务ID是否匹配(不匹配返回“同步异常”)
+ // validate gtask id
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "gtask id doesn't match");
return SYNC_ACTION_ERROR;
}
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
- // 只有本地修改,返回“更新云端”(把本地修改同步到云端)
+ // local modification only
return SYNC_ACTION_UPDATE_REMOTE;
} else {
- // 本地和云端都有修改,返回“同步冲突”(需要用户手动选择)
return SYNC_ACTION_UPDATE_CONFLICT;
}
}
} catch (Exception e) {
- // 出现异常,打印错误日志,返回“同步异常”
Log.e(TAG, e.toString());
e.printStackTrace();
}
@@ -388,79 +311,41 @@ public class Task extends Node {
return SYNC_ACTION_ERROR;
}
- /**
- * 判断当前任务是否值得保存
- * 通俗说:判断任务是否有有效内容,避免保存空任务
- * @return true=值得保存(有元信息/名称/备注),false=不值得保存(空任务)
- */
public boolean isWorthSaving() {
- return mMetaInfo != null // 有本地元信息
- || (getName() != null && getName().trim().length() > 0) // 任务名称不为空
- || (getNotes() != null && getNotes().trim().length() > 0); // 任务备注不为空
+ return mMetaInfo != null || (getName() != null && getName().trim().length() > 0)
+ || (getNotes() != null && getNotes().trim().length() > 0);
}
- /**
- * 设置任务的完成状态
- * @param completed true=已完成,false=未完成
- */
public void setCompleted(boolean completed) {
this.mCompleted = completed;
}
- /**
- * 设置任务的备注信息
- * @param notes 任务备注
- */
public void setNotes(String notes) {
this.mNotes = notes;
}
- /**
- * 设置任务的上一个兄弟任务(用来确定任务排序)
- * @param priorSibling 上一个兄弟任务
- */
public void setPriorSibling(Task priorSibling) {
this.mPriorSibling = priorSibling;
}
- /**
- * 设置任务的所属任务列表(对应本地文件夹)
- * @param parent 所属任务列表
- */
public void setParent(TaskList parent) {
this.mParent = parent;
}
- /**
- * 获取任务的完成状态
- * @return true=已完成,false=未完成
- */
public boolean getCompleted() {
return this.mCompleted;
}
- /**
- * 获取任务的备注信息
- * @return 任务备注
- */
public String getNotes() {
return this.mNotes;
}
- /**
- * 获取任务的上一个兄弟任务
- * @return 上一个兄弟任务(没有则返回null)
- */
public Task getPriorSibling() {
return this.mPriorSibling;
}
- /**
- * 获取任务的所属任务列表
- * @return 所属任务列表(没有则返回null)
- */
public TaskList getParent() {
return this.mParent;
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/TaskList.java b/src/Notes-master/src/net/micode/notes/gtask/data/TaskList.java
index 101266d..4ea21c5 100644
--- a/src/Notes-master/src/net/micode/notes/gtask/data/TaskList.java
+++ b/src/Notes-master/src/net/micode/notes/gtask/data/TaskList.java
@@ -30,130 +30,98 @@ import org.json.JSONObject;
import java.util.ArrayList;
-/**
- * GTask云端任务列表处理类(对应本地文件夹)
- * 通俗说:这个类是用来处理和GTask云端交互的“任务列表”(对应本地的便签文件夹),
- * 继承自同步基础模板Node,负责打包创建/更新云端列表的请求、解析云端/本地文件夹数据、
- * 判断同步操作类型,还负责管理列表下的子任务(对应文件夹里的普通便签)
- */
public class TaskList extends Node {
- // 日志标签:打印这个类的日志时,用来标识日志来自这个类
private static final String TAG = TaskList.class.getSimpleName();
- // 列表排序索引:用来确定这个任务列表在云端的显示顺序
private int mIndex;
- // 子任务列表:存储当前列表下的所有任务(对应本地文件夹里的普通便签)
+
private ArrayList mChildren;
- /**
- * 构造方法:初始化任务列表的默认状态
- * 通俗说:创建任务列表对象时,设置默认值,同时调用父类的初始化方法
- */
public TaskList() {
- super(); // 调用父类Node的构造方法,初始化同步相关的默认属性
- mChildren = new ArrayList(); // 初始化子任务列表(空列表)
- mIndex = 1; // 默认排序索引为1(控制列表在云端的显示顺序)
+ super();
+ mChildren = new ArrayList();
+ mIndex = 1;
}
- /**
- * 打包“创建云端任务列表”的请求参数
- * 通俗说:把本地新文件夹的信息整理成云端能识别的JSON格式,传给云端用来创建新列表
- * @param actionId 动作编号(云端用来识别这次操作的唯一标记)
- * @return 整理好的创建列表请求参数(JSON格式)
- */
public JSONObject getCreateAction(int actionId) {
- JSONObject js = new JSONObject(); // 创建空的JSON数据包
+ JSONObject js = new JSONObject();
try {
- // 1. 设置动作类型:告诉云端这次是“创建任务列表”
+ // action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
- // 2. 设置动作ID:给这次创建操作一个唯一编号,方便云端识别
+ // action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
- // 3. 设置列表排序索引:告诉云端这个新列表在云端的显示顺序
+ // index
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex);
- // 4. 设置列表核心信息:打包列表的名称、创建者等信息
+ // entity_delta
JSONObject entity = new JSONObject();
- entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 列表名称(对应本地文件夹名称)
- entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); // 创建者ID(这里默认传null)
+ entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
+ entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
- GTaskStringUtils.GTASK_JSON_TYPE_GROUP); // 实体类型:告诉云端这是一个“任务列表”(文件夹)
- js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 把列表核心信息存入总JSON
+ GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
+ js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
- // JSON打包出错,打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate tasklist-create jsonobject");
}
- return js; // 返回打包好的创建请求参数
+ return js;
}
- /**
- * 打包“更新云端任务列表”的请求参数
- * 通俗说:把本地修改后的文件夹信息整理成云端能识别的JSON格式,传给云端用来更新已有列表
- * @param actionId 动作编号(云端用来识别这次操作的唯一标记)
- * @return 整理好的更新列表请求参数(JSON格式)
- */
public JSONObject getUpdateAction(int actionId) {
- JSONObject js = new JSONObject(); // 创建空的JSON数据包
+ JSONObject js = new JSONObject();
try {
- // 1. 设置动作类型:告诉云端这次是“更新任务列表”
+ // action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
- // 2. 设置动作ID:给这次更新操作一个唯一编号,方便云端识别
+ // action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
- // 3. 设置列表ID:告诉云端要更新哪个列表(用云端给的唯一ID)
+ // id
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
- // 4. 设置要更新的列表信息:打包修改后的名称、删除状态
+ // entity_delta
JSONObject entity = new JSONObject();
- entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 更新后的列表名称
- entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); // 列表是否被删除(标记云端是否要删除该列表)
- js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 把更新信息存入总JSON
+ entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
+ entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
+ js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
- // JSON打包出错,打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate tasklist-update jsonobject");
}
- return js; // 返回打包好的更新请求参数
+ return js;
}
- /**
- * 解析云端返回的列表数据,赋值给当前列表对象
- * 通俗说:从云端获取到列表信息后,把数据拆解开,存到当前对象的属性里(同步到本地)
- * @param js 云端返回的列表数据包(JSON格式)
- */
public void setContentByRemoteJSON(JSONObject js) {
- if (js != null) { // 如果云端返回的数据不为空
+ if (js != null) {
try {
- // 1. 提取列表ID(云端给的唯一编号),设置到当前对象
+ // id
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
- // 2. 提取列表最后修改时间,设置到当前对象
+ // last_modified
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
- // 3. 提取列表名称,设置到当前对象
+ // name
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
} catch (JSONException e) {
- // JSON解析出错,打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get tasklist content from jsonobject");
@@ -161,128 +129,86 @@ public class TaskList extends Node {
}
}
- /**
- * 解析本地文件夹的JSON数据,设置列表内容
- * 通俗说:把本地文件夹的JSON数据拆解开,提取文件夹名称,设置为云端列表名称(加专属前缀)
- * @param js 本地文件夹的JSON数据包
- */
public void setContentByLocalJSON(JSONObject js) {
- // 如果JSON数据为空,或者缺少文件夹信息,打印警告日志
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
try {
- // 1. 提取本地文件夹的基本信息
JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
- // 2. 如果是普通文件夹(用户自己创建的文件夹)
if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
- // 提取文件夹名称,加上MIUI专属前缀(区分云端其他列表)
String name = folder.getString(NoteColumns.SNIPPET);
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name);
- }
- // 3. 如果是系统文件夹(自带的根目录/通话记录文件夹)
- else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
- // 根目录文件夹:设置默认名称(加前缀)
+ } else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER)
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT);
- // 通话记录文件夹:设置通话便签文件夹名称(加前缀)
else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER)
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_CALL_NOTE);
- // 其他系统文件夹:打印错误日志
else
Log.e(TAG, "invalid system folder");
- }
- // 4. 无效类型:打印错误日志
- else {
+ } else {
Log.e(TAG, "error type");
}
} catch (JSONException e) {
- // JSON解析出错,打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
- /**
- * 把当前列表数据打包成本地文件夹的JSON格式
- * 通俗说:把云端列表的信息整理成本地文件夹能识别的JSON格式,方便保存到本地数据库
- * @return 本地文件夹格式的JSON数据包(打包失败返回null)
- */
public JSONObject getLocalJSONFromContent() {
try {
- // 创建本地文件夹的JSON数据包
JSONObject js = new JSONObject();
- JSONObject folder = new JSONObject(); // 文件夹基本信息
+ JSONObject folder = new JSONObject();
- // 1. 提取云端列表名称,去掉MIUI专属前缀(还原成本地文件夹名称)
String folderName = getName();
if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX))
folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(),
folderName.length());
-
- // 2. 设置文件夹名称(摘要)
folder.put(NoteColumns.SNIPPET, folderName);
-
- // 3. 判断文件夹类型(系统文件夹/普通文件夹)
if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT)
- || folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE)) {
- // 根目录/通话记录文件夹:标记为系统文件夹
+ || folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE))
folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
- } else {
- // 其他文件夹:标记为普通文件夹
+ else
folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
- }
- // 4. 把文件夹信息存入总JSON
js.put(GTaskStringUtils.META_HEAD_NOTE, folder);
- return js; // 返回打包好的本地文件夹JSON
+ return js;
} catch (JSONException e) {
- // JSON打包出错,打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
return null;
}
}
- /**
- * 对比本地和云端列表数据,判断该执行哪种同步操作
- * 通俗说:查本地数据库,对比文件夹和云端列表的状态,确定是上传、下载还是无需操作
- * @param c 本地数据库查询结果(存着本地文件夹的信息)
- * @return 同步操作类型(对应Node类里的SYNC_ACTION_XXX常量)
- */
public int getSyncAction(Cursor c) {
try {
- // 1. 判断本地文件夹是否有修改
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
- // 本地没有修改
+ // there is no local update
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
- // 本地和云端都没有修改,返回“无需操作”
+ // no update both side
return SYNC_ACTION_NONE;
} else {
- // 云端有修改,返回“更新本地”(用云端列表数据覆盖本地文件夹)
+ // apply remote to local
return SYNC_ACTION_UPDATE_LOCAL;
}
} else {
- // 本地有修改
- // 2. 验证云端列表ID是否匹配(不匹配返回“同步异常”)
+ // validate gtask id
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "gtask id doesn't match");
return SYNC_ACTION_ERROR;
}
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
- // 只有本地修改,返回“更新云端”(把本地文件夹修改同步到云端)
+ // local modification only
return SYNC_ACTION_UPDATE_REMOTE;
} else {
- // 本地和云端都有修改,文件夹冲突时优先本地修改,返回“更新云端”
+ // for folder conflicts, just apply local modification
return SYNC_ACTION_UPDATE_REMOTE;
}
}
} catch (Exception e) {
- // 出现异常,打印错误日志,返回“同步异常”
Log.e(TAG, e.toString());
e.printStackTrace();
}
@@ -290,89 +216,62 @@ public class TaskList extends Node {
return SYNC_ACTION_ERROR;
}
- /**
- * 获取当前列表下的子任务数量
- * 通俗说:统计这个文件夹里有多少个普通便签(子任务)
- * @return 子任务数量
- */
public int getChildTaskCount() {
return mChildren.size();
}
- /**
- * 给当前列表添加子任务(直接添加到列表末尾)
- * 通俗说:往文件夹里添加一个普通便签,自动设置便签的父文件夹和上一个兄弟便签
- * @param task 要添加的子任务(普通便签)
- * @return true=添加成功,false=添加失败(任务为空/已存在)
- */
public boolean addChildTask(Task task) {
boolean ret = false;
- // 如果任务不为空,且列表中没有该任务
if (task != null && !mChildren.contains(task)) {
- ret = mChildren.add(task); // 添加任务到列表末尾
+ ret = mChildren.add(task);
if (ret) {
- // 设置任务的上一个兄弟任务(列表最后一个任务的前一个)
+ // need to set prior sibling and parent
task.setPriorSibling(mChildren.isEmpty() ? null : mChildren
.get(mChildren.size() - 1));
- task.setParent(this); // 设置任务的所属列表(当前文件夹)
+ task.setParent(this);
}
}
return ret;
}
- /**
- * 给当前列表添加子任务(指定位置添加)
- * 通俗说:往文件夹的指定位置插入一个普通便签,更新该任务和前后任务的兄弟关系
- * @param task 要添加的子任务(普通便签)
- * @param index 要插入的位置索引
- * @return true=添加成功,false=添加失败(索引无效/任务已存在)
- */
public boolean addChildTask(Task task, int index) {
- // 索引无效(小于0或大于列表长度),打印错误日志并返回false
if (index < 0 || index > mChildren.size()) {
Log.e(TAG, "add child task: invalid index");
return false;
}
- // 查找任务在列表中的位置(-1表示不存在)
int pos = mChildren.indexOf(task);
if (task != null && pos == -1) {
- mChildren.add(index, task); // 在指定位置插入任务
+ mChildren.add(index, task);
- // 获取该任务的前一个和后一个任务
+ // update the task list
Task preTask = null;
Task afterTask = null;
if (index != 0)
- preTask = mChildren.get(index - 1); // 前一个任务(上一个兄弟)
+ preTask = mChildren.get(index - 1);
if (index != mChildren.size() - 1)
- afterTask = mChildren.get(index + 1); // 后一个任务
+ afterTask = mChildren.get(index + 1);
- task.setPriorSibling(preTask); // 设置当前任务的上一个兄弟
+ task.setPriorSibling(preTask);
if (afterTask != null)
- afterTask.setPriorSibling(task); // 更新后一个任务的上一个兄弟为当前任务
+ afterTask.setPriorSibling(task);
}
return true;
}
- /**
- * 从当前列表移除子任务
- * 通俗说:从文件夹里删除一个普通便签,重置该便签的父文件夹和兄弟关系,更新后续便签的兄弟关系
- * @param task 要移除的子任务(普通便签)
- * @return true=移除成功,false=移除失败(任务不存在)
- */
public boolean removeChildTask(Task task) {
boolean ret = false;
- // 查找任务在列表中的位置
int index = mChildren.indexOf(task);
if (index != -1) {
- ret = mChildren.remove(task); // 移除该任务
+ ret = mChildren.remove(task);
if (ret) {
- task.setPriorSibling(null); // 重置该任务的上一个兄弟关系
- task.setParent(null); // 重置该任务的所属列表
+ // reset prior sibling and parent
+ task.setPriorSibling(null);
+ task.setParent(null);
- // 如果移除的不是最后一个任务,更新后续任务的兄弟关系
+ // update the task list
if (index != mChildren.size()) {
mChildren.get(index).setPriorSibling(
index == 0 ? null : mChildren.get(index - 1));
@@ -382,40 +281,24 @@ public class TaskList extends Node {
return ret;
}
- /**
- * 移动子任务在列表中的位置
- * 通俗说:调整文件夹里普通便签的显示顺序,先移除再添加到指定位置
- * @param task 要移动的子任务(普通便签)
- * @param index 要移动到的目标位置
- * @return true=移动成功,false=移动失败(索引无效/任务不存在/位置未变)
- */
public boolean moveChildTask(Task task, int index) {
- // 索引无效(小于0或大于等于列表长度),打印错误日志并返回false
+
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "move child task: invalid index");
return false;
}
- // 查找任务当前位置(-1表示不存在)
int pos = mChildren.indexOf(task);
if (pos == -1) {
Log.e(TAG, "move child task: the task should in the list");
return false;
}
- // 如果当前位置和目标位置一致,无需移动,返回true
if (pos == index)
return true;
- // 先移除任务,再添加到目标位置,返回操作结果
return (removeChildTask(task) && addChildTask(task, index));
}
- /**
- * 通过云端ID查找列表下的子任务
- * 通俗说:根据便签对应的云端唯一ID,在文件夹里找到对应的普通便签
- * @param gid 云端唯一ID
- * @return 找到的子任务(普通便签),找不到返回null
- */
public Task findChildTaskByGid(String gid) {
for (int i = 0; i < mChildren.size(); i++) {
Task t = mChildren.get(i);
@@ -426,24 +309,11 @@ public class TaskList extends Node {
return null;
}
- /**
- * 获取子任务在列表中的位置索引
- * 通俗说:查找某个普通便签在文件夹里的排序位置
- * @param task 要查找的子任务(普通便签)
- * @return 任务的位置索引(不存在返回-1)
- */
public int getChildTaskIndex(Task task) {
return mChildren.indexOf(task);
}
- /**
- * 通过位置索引获取子任务
- * 通俗说:根据排序位置,获取文件夹里对应的普通便签
- * @param index 位置索引
- * @return 对应的子任务(普通便签),索引无效返回null
- */
public Task getChildTaskByIndex(int index) {
- // 索引无效,打印错误日志并返回null
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "getTaskByIndex: invalid index");
return null;
@@ -451,12 +321,6 @@ public class TaskList extends Node {
return mChildren.get(index);
}
- /**
- * 通过云端ID查找列表下的子任务(和findChildTaskByGid功能一致)
- * 通俗说:根据便签对应的云端唯一ID,在文件夹里找到对应的普通便签
- * @param gid 云端唯一ID
- * @return 找到的子任务(普通便签),找不到返回null
- */
public Task getChilTaskByGid(String gid) {
for (Task task : mChildren) {
if (task.getGid().equals(gid))
@@ -465,30 +329,15 @@ public class TaskList extends Node {
return null;
}
- /**
- * 获取当前列表的所有子任务
- * 通俗说:获取文件夹里所有的普通便签,返回完整列表
- * @return 子任务列表(所有普通便签)
- */
public ArrayList getChildTaskList() {
return this.mChildren;
}
- /**
- * 设置当前列表的排序索引
- * 通俗说:设置这个文件夹在云端的显示顺序
- * @param index 排序索引
- */
public void setIndex(int index) {
this.mIndex = index;
}
- /**
- * 获取当前列表的排序索引
- * 通俗说:获取这个文件夹在云端的显示顺序
- * @return 排序索引
- */
public int getIndex() {
return this.mIndex;
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/gtask/exception/ActionFailureException.java b/src/Notes-master/src/net/micode/notes/gtask/exception/ActionFailureException.java
index f455c7a..15504be 100644
--- a/src/Notes-master/src/net/micode/notes/gtask/exception/ActionFailureException.java
+++ b/src/Notes-master/src/net/micode/notes/gtask/exception/ActionFailureException.java
@@ -1,27 +1,28 @@
/*
- * Description:支持小米便签运行过程中的运行异常处理。
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
package net.micode.notes.gtask.exception;
public class ActionFailureException extends RuntimeException {
private static final long serialVersionUID = 4425249765923293627L;
- /*
- * serialVersionUID相当于java类的身份证。主要用于版本控制。
- * serialVersionUID作用是序列化时保持版本的兼容性,即在版本升级时反序列化仍保持对象的唯一性。
- * Made By Cuican
- */
public ActionFailureException() {
super();
}
- /*
- * 在JAVA类中使用super来引用父类的成分,用this来引用当前对象.
- * 如果一个类从另外一个类继承,我们new这个子类的实例对象的时候,这个子类对象里面会有一个父类对象。
- * 怎么去引用里面的父类对象呢?使用super来引用
- * 也就是说,此处super()以及super (paramString)可认为是Exception ()和Exception (paramString)
- * Made By Cuican
- */
+
public ActionFailureException(String paramString) {
super(paramString);
}
@@ -29,4 +30,4 @@ public class ActionFailureException extends RuntimeException {
public ActionFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/gtask/exception/NetworkFailureException.java b/src/Notes-master/src/net/micode/notes/gtask/exception/NetworkFailureException.java
index 63a1adb..b08cfb1 100644
--- a/src/Notes-master/src/net/micode/notes/gtask/exception/NetworkFailureException.java
+++ b/src/Notes-master/src/net/micode/notes/gtask/exception/NetworkFailureException.java
@@ -1,28 +1,28 @@
/*
- * Description:支持小米便签运行过程中的网络异常处理。
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
package net.micode.notes.gtask.exception;
public class NetworkFailureException extends Exception {
private static final long serialVersionUID = 2107610287180234136L;
- /*
- * serialVersionUID相当于java类的身份证。主要用于版本控制。
- * serialVersionUID作用是序列化时保持版本的兼容性,即在版本升级时反序列化仍保持对象的唯一性。
- * Made By Cuican
- */
public NetworkFailureException() {
super();
}
- /*
- * 在JAVA类中使用super来引用父类的成分,用this来引用当前对象.
- * 如果一个类从另外一个类继承,我们new这个子类的实例对象的时候,这个子类对象里面会有一个父类对象。
- * 怎么去引用里面的父类对象呢?使用super来引用
- * 也就是说,此处super()以及super (paramString)可认为是Exception ()和Exception (paramString)
- * Made By Cuican
- */
public NetworkFailureException(String paramString) {
super(paramString);
}
@@ -30,4 +30,4 @@ public class NetworkFailureException extends Exception {
public NetworkFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskASyncTask.java b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskASyncTask.java
index b99f6c2..08d20f7 100644
--- a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskASyncTask.java
+++ b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskASyncTask.java
@@ -1,3 +1,4 @@
+
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
@@ -16,264 +17,125 @@
package net.micode.notes.gtask.remote;
+import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
-import android.os.Build;
-// 导入弱引用工具:防止因一直持有页面/服务导致的内存浪费(通俗说就是不占无用内存)
-import java.lang.ref.WeakReference;
-
-// 导入兼容版通知工具:让老安卓手机和新安卓手机都能正常显示通知
-import androidx.core.app.NotificationCompat;
-// 导入通知权限检查工具:用来判断用户有没有开启这个APP的通知权限
-import androidx.core.app.NotificationManagerCompat;
-// 导入APP资源:获取APP里的文字、图标等内容
import net.micode.notes.R;
-// 导入便签列表页面:通知点击后跳转到这个页面
import net.micode.notes.ui.NotesListActivity;
-// 导入便签设置页面:同步失败时,通知点击后跳转到这个页面
import net.micode.notes.ui.NotesPreferenceActivity;
-/**
- * 这个类是用来在后台同步GTask和本地便签的(不会卡住手机界面)
- * 注意:类名里的"ASync"写错了,应该是"Async",而且类名要和文件名一模一样
- */
-// 类名和文件名保持一致:GTaskASyncTask(建议改成GTaskAsyncTask更规范)
+
public class GTaskASyncTask extends AsyncTask {
- // 同步通知的唯一编号:用来区分这个同步通知和其他通知,不会弄混
- private static final int GTASK_SYNC_NOTIFICATION_ID = 5234235;
- // 通知分类ID(安卓8.0以上必须有):给同步通知单独分个类
- private static final String GTASK_SYNC_CHANNEL_ID = "gtask_sync_channel";
- // 通知分类名称(安卓8.0以上必须有):在手机设置里能看到这个分类的名字
- private static final String GTASK_SYNC_CHANNEL_NAME = "GTask同步通知";
+ private static int GTASK_SYNC_NOTIFICATION_ID = 5234235;
- /**
- * 任务完成后的回调接口
- * 通俗说:同步任务不管成功、失败还是取消,都会告诉外部“我做完了”
- */
public interface OnCompleteListener {
- /**
- * 任务完成后会调用这个方法
- */
void onComplete();
}
- // 用弱引用存上下文(页面/服务信息):页面关闭后,这个引用不会占着内存不放
- private final WeakReference mContextRef;
- // 用弱引用存回调监听器:外部页面关闭后,这个监听器不会浪费内存
- private final WeakReference mListenerRef;
- // 通知管理器:用来发送、关闭手机通知的工具
- private NotificationManager mNotificationManager;
- // GTask管理工具:专门处理GTask同步逻辑的单例(整个APP只有这一个实例)
- private final GTaskManager mTaskManager;
+ private Context mContext;
+
+ private NotificationManager mNotifiManager;
+
+ private GTaskManager mTaskManager;
+
+ private OnCompleteListener mOnCompleteListener;
- /**
- * 构造方法:初始化这个同步任务
- * @param context 页面/服务的上下文(用来获取图标、跳转页面等)
- * @param listener 任务完成后的回调(告诉外部任务做完了)
- */
public GTaskASyncTask(Context context, OnCompleteListener listener) {
- // 把上下文存成弱引用,防止内存浪费
- mContextRef = new WeakReference<>(context);
- // 把回调监听器存成弱引用,防止内存浪费
- mListenerRef = new WeakReference<>(listener);
- // 先获取上下文(弱引用要先取出来才能用,还要判断不为空)
- Context ctx = mContextRef.get();
- if (ctx != null) {
- // 获取手机的通知管理服务(用来发通知的)
- mNotificationManager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
- // 创建通知分类
- createNotificationChannel();
- }
- // 获取GTask管理工具的实例
+ mContext = context;
+ mOnCompleteListener = listener;
+ mNotifiManager = (NotificationManager) mContext
+ .getSystemService(Context.NOTIFICATION_SERVICE);
mTaskManager = GTaskManager.getInstance();
}
- /**
- * 取消同步任务
- * 通俗说:不想同步了,调用这个方法就能停止
- */
public void cancelSync() {
mTaskManager.cancelSync();
}
- /**
- * 发送同步进度信息
- * 通俗说:把后台同步的进度(比如“正在登录账号”)告诉主线程,用来显示通知
- * @param message 进度提示文字(比如“正在同步第2条便签”)
- */
- public void publishProgress(String message) {
- // 调用系统方法,把进度信息传出去
- publishProgress(new String[]{message});
+ public void publishProgess(String message) {
+ publishProgress(new String[] {
+ message
+ });
}
- /**
- * 创建通知分类(安卓8.0以上必须有)
- * 通俗说:给同步通知单独建个分类,用户可以在手机设置里单独开关这个分类的通知
- */
- private void createNotificationChannel() {
- // 先获取上下文
- Context ctx = mContextRef.get();
- // 上下文为空,或者手机系统低于安卓8.0,就不用创建分类
- if (ctx == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
- return;
- }
- // 构建通知分类
- android.app.NotificationChannel channel = new android.app.NotificationChannel(
- GTASK_SYNC_CHANNEL_ID, // 分类ID
- GTASK_SYNC_CHANNEL_NAME, // 分类名称
- NotificationManager.IMPORTANCE_DEFAULT // 通知重要性:默认级别(有提示音,不震动)
- );
- // 开启通知指示灯(如果手机有指示灯的话)
- channel.enableLights(true);
- // 不在手机桌面APP图标上显示角标
- channel.setShowBadge(false);
- // 把分类注册到手机系统里
- if (mNotificationManager != null) {
- mNotificationManager.createNotificationChannel(channel);
- }
+// private void showNotification(int tickerId, String content) {
+// Notification notification = new Notification(R.drawable.notification, mContext
+// .getString(tickerId), System.currentTimeMillis());
+// notification.defaults = Notification.DEFAULT_LIGHTS;
+// notification.flags = Notification.FLAG_AUTO_CANCEL;
+// PendingIntent pendingIntent;
+// if (tickerId != R.string.ticker_success) {
+// pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
+// NotesPreferenceActivity.class), 0);
+//
+// } else {
+// pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
+// NotesListActivity.class), 0);
+// }
+// notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content,
+// pendingIntent);
+// mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
+// }
+private void showNotification(int tickerId, String content) {
+ PendingIntent pendingIntent;
+ if (tickerId != R.string.ticker_success) {
+ pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
+ NotesPreferenceActivity.class), PendingIntent.FLAG_IMMUTABLE);
+ } else {
+ pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
+ NotesListActivity.class), PendingIntent.FLAG_IMMUTABLE);
}
-
- /**
- * 显示同步通知
- * 通俗说:根据同步状态(正在同步/成功/失败),在手机通知栏显示对应的提示
- * @param tickerId 通知顶部一闪而过的文字ID(比如“正在同步GTask”)
- * @param content 通知正文文字(比如“同步成功:共3条便签”)
- */
- private void showNotification(int tickerId, String content) {
- // 先获取上下文
- Context ctx = mContextRef.get();
- // 上下文或通知管理器为空,就不显示通知
- if (ctx == null || mNotificationManager == null) {
- return;
- }
-
- // 安卓13以上要检查通知权限:用户没开权限,就不显示通知
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- if (!NotificationManagerCompat.from(ctx).areNotificationsEnabled()) {
- // 这里可以提示用户开权限,现在先直接返回
- return;
- }
- }
-
- // 构建通知点击后的跳转意图
- PendingIntent pendingIntent;
- Intent intent;
- // 同步成功就跳转到便签列表,失败/取消就跳转到便签设置
- if (tickerId != R.string.ticker_success) {
- intent = new Intent(ctx, NotesPreferenceActivity.class); // 跳设置页面
- } else {
- intent = new Intent(ctx, NotesListActivity.class); // 跳便签列表
- }
-
- // 配置跳转意图的标志:更新已有意图,防止重复创建
- int flags = PendingIntent.FLAG_UPDATE_CURRENT;
- // 安卓6.0以上添加不可变标志,防止报错
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- flags |= PendingIntent.FLAG_IMMUTABLE;
- }
- // 创建跳转意图
- pendingIntent = PendingIntent.getActivity(ctx, 0, intent, flags);
-
- // 构建通知内容
- NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx, GTASK_SYNC_CHANNEL_ID)
- .setSmallIcon(R.drawable.notification) // 通知小图标(必须设置,不然不显示)
- .setContentTitle(ctx.getString(R.string.app_name)) // 通知标题(显示APP名字)
- .setContentText(content) // 通知正文
- .setTicker(ctx.getString(tickerId)) // 通知顶部一闪而过的文字(老手机有效)
- .setWhen(System.currentTimeMillis()) // 通知创建时间
- .setDefaults(NotificationCompat.DEFAULT_LIGHTS) // 开启默认指示灯
- .setAutoCancel(true) // 点击通知后,通知自动消失
- .setContentIntent(pendingIntent); // 通知点击后跳转到指定页面
-
- // 显示通知
- mNotificationManager.notify(GTASK_SYNC_NOTIFICATION_ID, builder.build());
- }
-
- /**
- * 后台执行同步任务(运行在子线程,不会卡界面)
- * @param unused 没有参数
- * @return 同步结果(成功/网络错误/内部错误/取消)
- */
+ Notification.Builder builder = new Notification.Builder(mContext)
+ .setAutoCancel(true)
+ .setContentTitle(mContext.getString(R.string.app_name))
+ .setContentText(content)
+ .setContentIntent(pendingIntent)
+ .setWhen(System.currentTimeMillis())
+ .setOngoing(true);
+ Notification notification=builder.getNotification();
+ mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
+}
@Override
protected Integer doInBackground(Void... unused) {
- // 获取上下文
- Context ctx = mContextRef.get();
- if (ctx != null) {
- // 发送进度:提示正在登录同步账号
- publishProgress(ctx.getString(R.string.sync_progress_login,
- NotesPreferenceActivity.getSyncAccountName(ctx)));
- }
- // 执行同步逻辑,返回同步结果
- return mTaskManager.sync(ctx, this);
+ publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity
+ .getSyncAccountName(mContext)));
+ return mTaskManager.sync(mContext, this);
}
- /**
- * 进度更新回调(运行在主线程,可以更新界面/显示通知)
- * @param progress 进度信息
- */
@Override
protected void onProgressUpdate(String... progress) {
- // 显示“正在同步”的通知
showNotification(R.string.ticker_syncing, progress[0]);
- // 获取上下文
- Context ctx = mContextRef.get();
- // 如果上下文是GTask同步服务,就发送广播告诉服务当前进度
- if (ctx instanceof GTaskSyncService) {
- ((GTaskSyncService) ctx).sendBroadcast(progress[0]);
+ if (mContext instanceof GTaskSyncService) {
+ ((GTaskSyncService) mContext).sendBroadcast(progress[0]);
}
}
- /**
- * 任务完成回调(运行在主线程,同步结束后会调用)
- * @param result 同步结果(成功/失败/取消等)
- */
@Override
protected void onPostExecute(Integer result) {
- // 获取上下文
- Context ctx = mContextRef.get();
- if (ctx != null) {
- // 根据同步结果显示对应通知
- if (result == GTaskManager.STATE_SUCCESS) {
- // 同步成功:显示成功通知,记录最后同步时间
- showNotification(R.string.ticker_success, ctx.getString(
- R.string.success_sync_account, mTaskManager.getSyncAccount()));
- NotesPreferenceActivity.setLastSyncTime(ctx, System.currentTimeMillis());
- } else if (result == GTaskManager.STATE_NETWORK_ERROR) {
- // 网络错误:显示网络异常通知
- showNotification(R.string.ticker_fail, ctx.getString(R.string.error_sync_network));
- } else if (result == GTaskManager.STATE_INTERNAL_ERROR) {
- // 内部错误:显示内部异常通知
- showNotification(R.string.ticker_fail, ctx.getString(R.string.error_sync_internal));
- } else if (result == GTaskManager.STATE_SYNC_CANCELLED) {
- // 同步取消:显示取消通知
- showNotification(R.string.ticker_cancel, ctx.getString(R.string.error_sync_cancelled));
- }
- }
-
- // 获取回调监听器,告诉外部任务完成了
- OnCompleteListener listener = mListenerRef.get();
- if (listener != null) {
- listener.onComplete();
+ if (result == GTaskManager.STATE_SUCCESS) {
+ showNotification(R.string.ticker_success, mContext.getString(
+ R.string.success_sync_account, mTaskManager.getSyncAccount()));
+ NotesPreferenceActivity.setLastSyncTime(mContext, System.currentTimeMillis());
+ } else if (result == GTaskManager.STATE_NETWORK_ERROR) {
+ showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_network));
+ } else if (result == GTaskManager.STATE_INTERNAL_ERROR) {
+ showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_internal));
+ } else if (result == GTaskManager.STATE_SYNC_CANCELLED) {
+ showNotification(R.string.ticker_cancel, mContext
+ .getString(R.string.error_sync_cancelled));
}
- }
+ if (mOnCompleteListener != null) {
+ new Thread(new Runnable() {
- /**
- * 任务被取消时的回调
- * 通俗说:调用cancel()方法取消任务后,会执行这个方法
- * @param result 同步结果
- */
- @Override
- protected void onCancelled(Integer result) {
- super.onCancelled(result);
- // 告诉外部任务完成了
- OnCompleteListener listener = mListenerRef.get();
- if (listener != null) {
- listener.onComplete();
+ public void run() {
+ mOnCompleteListener.onComplete();
+ }
+ }).start();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskClient.java b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskClient.java
index bb03c97..c67dfdf 100644
--- a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskClient.java
+++ b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskClient.java
@@ -16,248 +16,190 @@
package net.micode.notes.gtask.remote;
-import android.accounts.Account; // 谷歌账号相关:存储账号名称、类型等信息
-import android.accounts.AccountManager; // 账号管理器:获取手机里的谷歌账号、登录凭证等
-import android.accounts.AccountManagerFuture; // 账号操作结果:异步获取账号登录凭证的返回值
-import android.app.Activity; // 页面相关:用来关联登录的页面
-import android.os.Bundle; // 数据容器:用来存放账号登录凭证等数据
-import android.text.TextUtils; // 文本工具:判断字符串是否为空、去除空格等
-import android.util.Log; // 日志工具:打印调试信息,方便查找问题
-
-import net.micode.notes.gtask.data.Node; // 基础数据节点:任务/任务列表的通用数据模型
-import net.micode.notes.gtask.data.Task; // 任务数据模型:存储单个GTask任务的信息
-import net.micode.notes.gtask.data.TaskList; // 任务列表数据模型:存储GTask任务列表的信息
-import net.micode.notes.gtask.exception.ActionFailureException; // 操作失败异常:创建/删除任务等操作失败时抛出
-import net.micode.notes.gtask.exception.NetworkFailureException; // 网络失败异常:网络不通或请求失败时抛出
-import net.micode.notes.tool.GTaskStringUtils; // 字符串工具:存储GTask接口用到的固定字符串(比如接口参数名)
-import net.micode.notes.ui.NotesPreferenceActivity; // 便签设置页面:获取用户设置的同步账号名称
-
-import org.apache.http.HttpEntity; // 网络请求实体:存储网络请求/响应的内容
-import org.apache.http.HttpResponse; // 网络响应:存储服务器返回的所有数据(状态码、内容等)
-import org.apache.http.client.ClientProtocolException; // 客户端协议异常:网络请求格式错误时抛出
-import org.apache.http.client.entity.UrlEncodedFormEntity; // 表单实体:把请求参数封装成表单格式
-import org.apache.http.client.methods.HttpGet; // GET请求:向服务器获取数据的请求方式
-import org.apache.http.client.methods.HttpPost; // POST请求:向服务器提交数据的请求方式
-import org.apache.http.cookie.Cookie; // 登录凭证:用来保持登录状态,不用每次都输账号密码
-import org.apache.http.impl.client.BasicCookieStore; // Cookie容器:存储登录后的Cookie信息
-import org.apache.http.impl.client.DefaultHttpClient; // 网络请求客户端:发送GET/POST请求的工具
-import org.apache.http.message.BasicNameValuePair; // 键值对:存储请求参数(比如"key=value")
-import org.apache.http.params.BasicHttpParams; // 网络参数容器:存储网络请求的配置(比如超时时间)
-import org.apache.http.params.HttpConnectionParams; // 连接参数:设置网络连接的超时时间
-import org.apache.http.params.HttpParams; // 网络参数:通用的网络配置参数
-import org.apache.http.params.HttpProtocolParams; // 协议参数:设置网络请求的协议配置
-import org.json.JSONArray; // JSON数组:存储一组格式化数据(比如多个任务信息)
-import org.json.JSONException; // JSON解析异常:数据格式错误导致解析失败时抛出
-import org.json.JSONObject; // JSON对象:存储键值对格式的数据(比如单个任务信息)
-
-import java.io.BufferedReader; // 缓冲读取器:高效读取服务器返回的文本内容
-import java.io.IOException; // IO异常:读取/写入数据失败时抛出
-import java.io.InputStream; // 输入流:读取服务器返回的原始数据
-import java.io.InputStreamReader; // 输入流读取器:把原始字节数据转换成文本数据
-import java.util.LinkedList; // 链表:存储请求参数的容器,有序且方便添加
-import java.util.List; // 集合:存储一组数据的通用接口
-import java.util.zip.GZIPInputStream; // GZIP解压流:解压服务器返回的GZIP格式数据
-import java.util.zip.Inflater; // 解压工具:处理DEFLATE格式的解压
-import java.util.zip.InflaterInputStream; // DEFLATE解压流:解压服务器返回的DEFLATE格式数据
-
-/**
- * GTask客户端工具类
- * 通俗说:这个类专门负责和谷歌任务(GTask)服务器打交道,比如登录、创建任务、获取任务列表等
- * 特点:整个APP只有这一个实例(不会创建多个重复对象),节省内存
- */
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerFuture;
+import android.app.Activity;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import net.micode.notes.gtask.data.Node;
+import net.micode.notes.gtask.data.Task;
+import net.micode.notes.gtask.data.TaskList;
+import net.micode.notes.gtask.exception.ActionFailureException;
+import net.micode.notes.gtask.exception.NetworkFailureException;
+import net.micode.notes.tool.GTaskStringUtils;
+import net.micode.notes.ui.NotesPreferenceActivity;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.cookie.Cookie;
+import org.apache.http.impl.client.BasicCookieStore;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpProtocolParams;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+
public class GTaskClient {
- // 日志标签:打印日志时用来标识是这个类的日志,方便查找问题
private static final String TAG = GTaskClient.class.getSimpleName();
- // GTask基础地址:谷歌任务的根地址
private static final String GTASK_URL = "https://mail.google.com/tasks/";
- // GTask GET请求地址:用来从服务器获取数据(比如任务列表)
private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig";
- // GTask POST请求地址:用来向服务器提交数据(比如创建任务、删除任务)
private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig";
- // 本类的唯一实例:保证整个APP只有一个GTaskClient对象
private static GTaskClient mInstance = null;
- // 网络请求客户端:用来发送GET/POST请求,和服务器通信
private DefaultHttpClient mHttpClient;
- // 实际使用的GET请求地址:可能是默认地址,也可能是自定义域名地址
private String mGetUrl;
- // 实际使用的POST请求地址:可能是默认地址,也可能是自定义域名地址
private String mPostUrl;
- // 客户端版本号:和服务器交互时需要传递的版本信息,用来兼容不同版本
private long mClientVersion;
- // 登录状态标记:true=已登录,false=未登录
private boolean mLoggedin;
- // 上次登录时间:用来判断登录状态是否过期
private long mLastLoginTime;
- // 操作ID:每次和服务器交互的操作都会分配一个唯一ID,自增累加
private int mActionId;
- // 同步账号:当前用来同步GTask的谷歌账号
private Account mAccount;
- // 更新数据数组:存储待提交的更新操作(比如修改任务、新增任务),批量提交更高效
private JSONArray mUpdateArray;
- /**
- * 私有构造方法
- * 通俗说:不让外部直接创建这个类的对象,只能通过getInstance()获取唯一实例
- */
private GTaskClient() {
- mHttpClient = null; // 初始化网络请求客户端为null
- mGetUrl = GTASK_GET_URL; // 默认使用GTask默认GET地址
- mPostUrl = GTASK_POST_URL; // 默认使用GTask默认POST地址
- mClientVersion = -1; // 客户端版本号初始化为-1(未获取)
- mLoggedin = false; // 初始状态为未登录
- mLastLoginTime = 0; // 上次登录时间初始化为0
- mActionId = 1; // 操作ID从1开始自增
- mAccount = null; // 同步账号初始化为null
- mUpdateArray = null; // 更新数据数组初始化为null
+ mHttpClient = null;
+ mGetUrl = GTASK_GET_URL;
+ mPostUrl = GTASK_POST_URL;
+ mClientVersion = -1;
+ mLoggedin = false;
+ mLastLoginTime = 0;
+ mActionId = 1;
+ mAccount = null;
+ mUpdateArray = null;
}
- /**
- * 获取GTaskClient的唯一实例
- * 通俗说:整个APP只能通过这个方法拿到GTaskClient对象,保证只有一个实例
- * @return GTaskClient唯一实例
- */
public static synchronized GTaskClient getInstance() {
- // 如果实例为null,就创建一个新的(懒加载:用到时才创建)
if (mInstance == null) {
mInstance = new GTaskClient();
}
return mInstance;
}
- /**
- * 登录GTask服务器
- * 通俗说:验证谷歌账号,获取登录凭证,保持和服务器的登录状态
- * @param activity 登录关联的页面(用来获取账号信息、处理登录回调)
- * @return true=登录成功,false=登录失败
- */
public boolean login(Activity activity) {
- // 假设登录凭证5分钟过期,过期后需要重新登录
+ // we suppose that the cookie would expire after 5 minutes
+ // then we need to re-login
final long interval = 1000 * 60 * 5;
if (mLastLoginTime + interval < System.currentTimeMillis()) {
- mLoggedin = false; // 登录过期,标记为未登录
+ mLoggedin = false;
}
- // 如果已经登录,但账号和设置里的同步账号不一致(用户切换了账号),也需要重新登录
+ // need to re-login after account switch
if (mLoggedin
&& !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity
- .getSyncAccountName(activity))) {
+ .getSyncAccountName(activity))) {
mLoggedin = false;
}
- // 如果已经登录且未过期、账号未切换,直接返回登录成功
if (mLoggedin) {
- Log.d(TAG, "already logged in"); // 打印日志:已登录
+ Log.d(TAG, "already logged in");
return true;
}
- // 记录本次登录时间
mLastLoginTime = System.currentTimeMillis();
- // 获取谷歌账号的登录凭证(令牌)
String authToken = loginGoogleAccount(activity, false);
if (authToken == null) {
- Log.e(TAG, "login google account failed"); // 打印日志:谷歌账号登录失败
+ Log.e(TAG, "login google account failed");
return false;
}
- // 处理非gmail/googlemail域名的账号(自定义域名账号,需要切换请求地址)
+ // login with custom domain if necessary
if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase()
.endsWith("googlemail.com"))) {
- // 拼接自定义域名的请求地址
StringBuilder url = new StringBuilder(GTASK_URL).append("a/");
- int index = mAccount.name.indexOf('@') + 1; // 找到@符号的位置,截取域名后缀
+ int index = mAccount.name.indexOf('@') + 1;
String suffix = mAccount.name.substring(index);
url.append(suffix + "/");
- mGetUrl = url.toString() + "ig"; // 自定义GET地址
- mPostUrl = url.toString() + "r/ig"; // 自定义POST地址
+ mGetUrl = url.toString() + "ig";
+ mPostUrl = url.toString() + "r/ig";
- // 使用自定义地址尝试登录GTask
if (tryToLoginGtask(activity, authToken)) {
- mLoggedin = true; // 登录成功,标记为已登录
+ mLoggedin = true;
}
}
- // 如果自定义地址登录失败,使用默认地址再次尝试登录
+ // try to login with google official url
if (!mLoggedin) {
- mGetUrl = GTASK_GET_URL; // 恢复默认GET地址
- mPostUrl = GTASK_POST_URL; // 恢复默认POST地址
- // 默认地址登录失败,返回false
+ mGetUrl = GTASK_GET_URL;
+ mPostUrl = GTASK_POST_URL;
if (!tryToLoginGtask(activity, authToken)) {
return false;
}
}
- // 所有登录逻辑完成,标记为已登录
mLoggedin = true;
return true;
}
- /**
- * 获取谷歌账号的登录凭证(令牌)
- * 通俗说:从手机的账号管理器中,获取指定谷歌账号的登录凭证,用来登录GTask
- * @param activity 关联页面
- * @param invalidateToken 是否失效旧凭证(旧凭证过期时需要设为true)
- * @return 登录凭证(令牌),null=获取失败
- */
private String loginGoogleAccount(Activity activity, boolean invalidateToken) {
- String authToken; // 登录凭证
- // 获取手机的账号管理器
+ String authToken;
AccountManager accountManager = AccountManager.get(activity);
- // 获取手机里所有的谷歌账号
Account[] accounts = accountManager.getAccountsByType("com.google");
- // 如果没有谷歌账号,返回null
if (accounts.length == 0) {
Log.e(TAG, "there is no available google account");
return null;
}
- // 获取用户在便签设置里指定的同步账号名称
String accountName = NotesPreferenceActivity.getSyncAccountName(activity);
Account account = null;
- // 遍历所有谷歌账号,找到和设置里一致的账号
for (Account a : accounts) {
if (a.name.equals(accountName)) {
account = a;
break;
}
}
- // 找到匹配的账号,保存到成员变量
if (account != null) {
mAccount = account;
} else {
- // 没找到匹配的账号,打印日志并返回null
Log.e(TAG, "unable to get an account with the same name in the settings");
return null;
}
- // 获取账号的登录凭证
+ // get the token now
AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account,
"goanna_mobile", null, activity, null, null);
try {
- // 获取凭证返回结果
Bundle authTokenBundle = accountManagerFuture.getResult();
- // 从结果中提取登录凭证
authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN);
- // 如果需要失效旧凭证,先失效再重新获取
if (invalidateToken) {
accountManager.invalidateAuthToken("com.google", authToken);
loginGoogleAccount(activity, false);
}
} catch (Exception e) {
- // 获取凭证失败,打印日志并设为null
Log.e(TAG, "get auth token failed");
authToken = null;
}
@@ -265,24 +207,16 @@ public class GTaskClient {
return authToken;
}
- /**
- * 尝试登录GTask服务器
- * 通俗说:先用当前凭证登录,失败的话就失效旧凭证,重新获取凭证再登录
- * @param activity 关联页面
- * @param authToken 登录凭证
- * @return true=登录成功,false=登录失败
- */
private boolean tryToLoginGtask(Activity activity, String authToken) {
- // 先用当前凭证登录
if (!loginGtask(authToken)) {
- // 登录失败,说明凭证过期,重新获取凭证
+ // maybe the auth token is out of date, now let's invalidate the
+ // token and try again
authToken = loginGoogleAccount(activity, true);
if (authToken == null) {
Log.e(TAG, "login google account failed");
return false;
}
- // 用新凭证再次尝试登录,还是失败就返回false
if (!loginGtask(authToken)) {
Log.e(TAG, "login gtask failed");
return false;
@@ -291,76 +225,54 @@ public class GTaskClient {
return true;
}
- /**
- * 实际执行GTask登录逻辑
- * 通俗说:用登录凭证发送请求,获取登录状态(Cookie)和客户端版本号
- * @param authToken 登录凭证
- * @return true=登录成功,false=登录失败
- */
private boolean loginGtask(String authToken) {
- // 设置网络连接超时时间:10秒(连接不上服务器就超时)
int timeoutConnection = 10000;
- // 设置网络读取超时时间:15秒(服务器没返回数据就超时)
int timeoutSocket = 15000;
- // 创建网络参数容器
HttpParams httpParameters = new BasicHttpParams();
- // 设置连接超时时间
HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
- // 设置读取超时时间
HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);
- // 初始化网络请求客户端,传入超时参数
mHttpClient = new DefaultHttpClient(httpParameters);
- // 创建Cookie容器,存储登录后的凭证
BasicCookieStore localBasicCookieStore = new BasicCookieStore();
- // 给网络客户端设置Cookie容器
mHttpClient.setCookieStore(localBasicCookieStore);
- // 关闭Expect-Continue协议,避免部分服务器不兼容
HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false);
- // 执行GTask登录请求
+ // login gtask
try {
- // 拼接登录请求地址(带登录凭证)
String loginUrl = mGetUrl + "?auth=" + authToken;
- // 创建GET请求
HttpGet httpGet = new HttpGet(loginUrl);
HttpResponse response = null;
- // 发送请求,获取服务器响应
response = mHttpClient.execute(httpGet);
- // 获取登录后的Cookie,判断是否包含GTask的授权Cookie
+ // get the cookie now
List cookies = mHttpClient.getCookieStore().getCookies();
boolean hasAuthCookie = false;
for (Cookie cookie : cookies) {
if (cookie.getName().contains("GTL")) {
- hasAuthCookie = true; // 包含授权Cookie
+ hasAuthCookie = true;
}
}
if (!hasAuthCookie) {
- Log.w(TAG, "it seems that there is no auth cookie"); // 打印警告:没有授权Cookie
+ Log.w(TAG, "it seems that there is no auth cookie");
}
- // 获取服务器返回的内容,解析出客户端版本号
+ // get the client version
String resString = getResponseContent(response.getEntity());
String jsBegin = "_setup(";
String jsEnd = ")}";
- // 找到JSON数据的起始和结束位置
int begin = resString.indexOf(jsBegin);
int end = resString.lastIndexOf(jsEnd);
String jsString = null;
if (begin != -1 && end != -1 && begin < end) {
- // 截取JSON数据字符串
jsString = resString.substring(begin + jsBegin.length(), end);
}
- // 解析JSON数据,获取客户端版本号
JSONObject js = new JSONObject(jsString);
mClientVersion = js.getLong("v");
} catch (JSONException e) {
- // JSON解析失败,打印日志并返回false
Log.e(TAG, e.toString());
e.printStackTrace();
return false;
} catch (Exception e) {
- // 其他异常(比如网络异常),打印日志并返回false
+ // simply catch all exceptions
Log.e(TAG, "httpget gtask_url failed");
return false;
}
@@ -368,230 +280,152 @@ public class GTaskClient {
return true;
}
- /**
- * 获取自增的操作ID
- * 通俗说:每次和服务器交互,都给操作分配一个唯一ID,用后自增(1→2→3→...)
- * @return 唯一操作ID
- */
private int getActionId() {
return mActionId++;
}
- /**
- * 创建POST请求对象
- * 通俗说:封装POST请求的公共配置(请求头),不用每次创建都重复写
- * @return 配置好的POST请求对象
- */
private HttpPost createHttpPost() {
HttpPost httpPost = new HttpPost(mPostUrl);
- // 设置请求内容类型:表单格式,编码为UTF-8
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
- // 设置AT请求头:固定值1,GTask接口要求
httpPost.setHeader("AT", "1");
return httpPost;
}
- /**
- * 获取网络响应的文本内容
- * 通俗说:把服务器返回的原始数据(字节)转换成文本,还会处理压缩格式(GZIP/DEFLATE)
- * @param entity 网络响应实体(存储服务器返回的内容)
- * @return 服务器返回的文本内容
- * @throws IOException 读取数据失败时抛出
- */
private String getResponseContent(HttpEntity entity) throws IOException {
String contentEncoding = null;
- // 获取响应内容的编码格式(判断是否是压缩格式)
if (entity.getContentEncoding() != null) {
contentEncoding = entity.getContentEncoding().getValue();
Log.d(TAG, "encoding: " + contentEncoding);
}
- // 获取响应的原始输入流
InputStream input = entity.getContent();
- // 如果是GZIP压缩格式,解压后再读取
if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) {
input = new GZIPInputStream(entity.getContent());
- }
- // 如果是DEFLATE压缩格式,解压后再读取
- else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) {
+ } else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) {
Inflater inflater = new Inflater(true);
input = new InflaterInputStream(entity.getContent(), inflater);
}
try {
- // 把字节输入流转换成文本输入流
InputStreamReader isr = new InputStreamReader(input);
- // 用缓冲读取器提高读取效率
BufferedReader br = new BufferedReader(isr);
- StringBuilder sb = new StringBuilder(); // 存储读取到的文本内容
+ StringBuilder sb = new StringBuilder();
- // 循环读取每一行文本,直到读取完毕
while (true) {
String buff = br.readLine();
if (buff == null) {
- return sb.toString(); // 返回读取到的所有文本
+ return sb.toString();
}
- sb = sb.append(buff); // 把每行文本添加到字符串中
+ sb = sb.append(buff);
}
} finally {
- input.close(); // 关闭输入流,释放资源
+ input.close();
}
}
- /**
- * 发送POST请求到GTask服务器
- * 通俗说:把封装好的JSON数据提交给服务器,获取返回结果,处理各种异常
- * @param js 要提交的JSON数据(包含操作类型、参数等)
- * @return 服务器返回的JSON结果
- * @throws NetworkFailureException 网络失败时抛出
- */
private JSONObject postRequest(JSONObject js) throws NetworkFailureException {
- // 如果未登录,抛出异常
if (!mLoggedin) {
Log.e(TAG, "please login first");
throw new ActionFailureException("not logged in");
}
- // 创建配置好的POST请求
HttpPost httpPost = createHttpPost();
try {
- // 创建请求参数列表(存储"r=JSON字符串"这个参数)
LinkedList list = new LinkedList();
list.add(new BasicNameValuePair("r", js.toString()));
- // 把参数列表封装成表单实体
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8");
- // 给POST请求设置请求实体
httpPost.setEntity(entity);
- // 发送POST请求,获取服务器响应
+ // execute the post
HttpResponse response = mHttpClient.execute(httpPost);
- // 获取响应的文本内容
String jsString = getResponseContent(response.getEntity());
- // 把文本内容解析成JSON对象返回
return new JSONObject(jsString);
} catch (ClientProtocolException e) {
- // 客户端协议异常,打印日志并抛出网络失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("postRequest failed");
} catch (IOException e) {
- // IO异常(比如网络断开),打印日志并抛出网络失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("postRequest failed");
} catch (JSONException e) {
- // JSON解析异常,打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("unable to convert response content to jsonobject");
} catch (Exception e) {
- // 其他异常,打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("error occurs when posting request");
}
}
- /**
- * 创建单个GTask任务
- * 通俗说:把本地任务数据提交到GTask服务器,创建新任务,并获取服务器返回的任务ID
- * @param task 要创建的本地任务对象
- * @throws NetworkFailureException 网络失败时抛出
- */
public void createTask(Task task) throws NetworkFailureException {
- // 先提交之前待处理的更新操作
commitUpdate();
try {
- // 创建POST请求的JSON数据
JSONObject jsPost = new JSONObject();
- JSONArray actionList = new JSONArray(); // 操作列表(存储创建任务的操作)
+ JSONArray actionList = new JSONArray();
- // 把创建任务的操作添加到操作列表
+ // action_list
actionList.put(task.getCreateAction(getActionId()));
- // 给JSON数据添加操作列表
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
- // 给JSON数据添加客户端版本号
+ // client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
- // 发送POST请求,获取服务器响应
+ // post
JSONObject jsResponse = postRequest(jsPost);
- // 从响应中获取创建任务的结果
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
- // 把服务器返回的任务ID设置到本地任务对象中
task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));
} catch (JSONException e) {
- // JSON解析失败,打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("create task: handing jsonobject failed");
}
}
- /**
- * 创建GTask任务列表
- * 通俗说:把本地任务列表数据提交到GTask服务器,创建新任务列表,并获取服务器返回的列表ID
- * @param tasklist 要创建的本地任务列表对象
- * @throws NetworkFailureException 网络失败时抛出
- */
public void createTaskList(TaskList tasklist) throws NetworkFailureException {
- // 先提交之前待处理的更新操作
commitUpdate();
try {
- // 创建POST请求的JSON数据
JSONObject jsPost = new JSONObject();
- JSONArray actionList = new JSONArray(); // 操作列表(存储创建任务列表的操作)
+ JSONArray actionList = new JSONArray();
- // 把创建任务列表的操作添加到操作列表
+ // action_list
actionList.put(tasklist.getCreateAction(getActionId()));
- // 给JSON数据添加操作列表
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
- // 给JSON数据添加客户端版本号
+ // client version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
- // 发送POST请求,获取服务器响应
+ // post
JSONObject jsResponse = postRequest(jsPost);
- // 从响应中获取创建任务列表的结果
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
- // 把服务器返回的列表ID设置到本地任务列表对象中
tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));
} catch (JSONException e) {
- // JSON解析失败,打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("create tasklist: handing jsonobject failed");
}
}
- /**
- * 提交待处理的更新操作
- * 通俗说:把mUpdateArray里存储的批量更新操作(修改/新增任务)一次性提交给服务器
- * @throws NetworkFailureException 网络失败时抛出
- */
public void commitUpdate() throws NetworkFailureException {
- // 如果有未提交的更新操作
if (mUpdateArray != null) {
try {
- // 创建POST请求的JSON数据
JSONObject jsPost = new JSONObject();
- // 给JSON数据添加更新操作列表
+ // action_list
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray);
- // 给JSON数据添加客户端版本号
+ // client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
- // 发送POST请求,提交更新
postRequest(jsPost);
- mUpdateArray = null; // 提交后清空更新列表
+ mUpdateArray = null;
} catch (JSONException e) {
- // JSON解析失败,打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("commit update: handing jsonobject failed");
@@ -599,230 +433,153 @@ public class GTaskClient {
}
}
- /**
- * 添加待更新的节点(任务/任务列表)
- * 通俗说:把单个更新操作添加到批量更新列表,累计到10个就自动提交,避免一次性提交太多失败
- * @param node 要更新的节点(任务/任务列表)
- * @throws NetworkFailureException 网络失败时抛出
- */
public void addUpdateNode(Node node) throws NetworkFailureException {
if (node != null) {
- // 如果更新列表不为空,且数量超过10个,先提交一次
+ // too many update items may result in an error
+ // set max to 10 items
if (mUpdateArray != null && mUpdateArray.length() > 10) {
commitUpdate();
}
- // 如果更新列表为空,初始化一个新的JSON数组
if (mUpdateArray == null)
mUpdateArray = new JSONArray();
- // 把节点的更新操作添加到更新列表
mUpdateArray.put(node.getUpdateAction(getActionId()));
}
}
- /**
- * 移动GTask任务
- * 通俗说:把任务从一个任务列表移动到另一个,或在同一个列表内调整位置
- * @param task 要移动的任务
- * @param preParent 任务原来的父列表
- * @param curParent 任务要移动到的目标列表
- * @throws NetworkFailureException 网络失败时抛出
- */
public void moveTask(Task task, TaskList preParent, TaskList curParent)
throws NetworkFailureException {
- // 先提交之前待处理的更新操作
commitUpdate();
try {
- // 创建POST请求的JSON数据
JSONObject jsPost = new JSONObject();
- JSONArray actionList = new JSONArray(); // 操作列表
- JSONObject action = new JSONObject(); // 移动任务的操作
+ JSONArray actionList = new JSONArray();
+ JSONObject action = new JSONObject();
- // 设置操作类型:移动
+ // action_list
action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE);
- // 设置操作ID
action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId());
- // 设置要移动的任务ID
action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid());
- // 如果在同一个列表内移动,且不是第一个任务,设置上一个任务的ID(用来确定位置)
if (preParent == curParent && task.getPriorSibling() != null) {
+ // put prioring_sibing_id only if moving within the tasklist and
+ // it is not the first one
action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling());
}
- // 设置任务原来的列表ID
action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid());
- // 设置任务目标列表的父ID
action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid());
- // 如果在不同列表间移动,设置目标列表ID
if (preParent != curParent) {
+ // put the dest_list only if moving between tasklists
action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid());
}
- // 把移动操作添加到操作列表
actionList.put(action);
- // 给JSON数据添加操作列表
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
- // 给JSON数据添加客户端版本号
+ // client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
- // 发送POST请求,执行移动操作
postRequest(jsPost);
} catch (JSONException e) {
- // JSON解析失败,打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("move task: handing jsonobject failed");
}
}
- /**
- * 删除GTask节点(任务/任务列表)
- * 通俗说:把节点标记为已删除,提交到服务器,完成删除操作
- * @param node 要删除的节点(任务/任务列表)
- * @throws NetworkFailureException 网络失败时抛出
- */
public void deleteNode(Node node) throws NetworkFailureException {
- // 先提交之前待处理的更新操作
commitUpdate();
try {
- // 创建POST请求的JSON数据
JSONObject jsPost = new JSONObject();
- JSONArray actionList = new JSONArray(); // 操作列表
+ JSONArray actionList = new JSONArray();
- // 把节点标记为已删除
+ // action_list
node.setDeleted(true);
- // 把删除操作添加到操作列表
actionList.put(node.getUpdateAction(getActionId()));
- // 给JSON数据添加操作列表
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
- // 给JSON数据添加客户端版本号
+ // client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
- // 发送POST请求,执行删除操作
postRequest(jsPost);
- mUpdateArray = null; // 清空更新列表
+ mUpdateArray = null;
} catch (JSONException e) {
- // JSON解析失败,打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("delete node: handing jsonobject failed");
}
}
- /**
- * 获取所有GTask任务列表
- * 通俗说:从GTask服务器获取用户的所有任务列表(比如“我的任务”“工作任务”)
- * @return 任务列表的JSON数组
- * @throws NetworkFailureException 网络失败时抛出
- */
public JSONArray getTaskLists() throws NetworkFailureException {
- // 如果未登录,抛出异常
if (!mLoggedin) {
Log.e(TAG, "please login first");
throw new ActionFailureException("not logged in");
}
try {
- // 创建GET请求
HttpGet httpGet = new HttpGet(mGetUrl);
HttpResponse response = null;
- // 发送GET请求,获取服务器响应
response = mHttpClient.execute(httpGet);
- // 获取响应的文本内容
+ // get the task list
String resString = getResponseContent(response.getEntity());
String jsBegin = "_setup(";
String jsEnd = ")}";
- // 找到JSON数据的起始和结束位置
int begin = resString.indexOf(jsBegin);
int end = resString.lastIndexOf(jsEnd);
String jsString = null;
if (begin != -1 && end != -1 && begin < end) {
- // 截取JSON数据字符串
jsString = resString.substring(begin + jsBegin.length(), end);
}
- // 解析JSON数据,获取任务列表数组
JSONObject js = new JSONObject(jsString);
return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS);
} catch (ClientProtocolException e) {
- // 客户端协议异常,打印日志并抛出网络失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("gettasklists: httpget failed");
} catch (IOException e) {
- // IO异常,打印日志并抛出网络失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("gettasklists: httpget failed");
} catch (JSONException e) {
- // JSON解析失败,打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("get task lists: handing jasonobject failed");
}
}
- /**
- * 获取指定GTask任务列表的所有任务
- * 通俗说:根据任务列表ID,从服务器获取该列表下的所有任务
- * @param listGid 任务列表ID
- * @return 任务的JSON数组
- * @throws NetworkFailureException 网络失败时抛出
- */
public JSONArray getTaskList(String listGid) throws NetworkFailureException {
- // 先提交之前待处理的更新操作
commitUpdate();
try {
- // 创建POST请求的JSON数据
JSONObject jsPost = new JSONObject();
- JSONArray actionList = new JSONArray(); // 操作列表
- JSONObject action = new JSONObject(); // 获取任务列表的操作
+ JSONArray actionList = new JSONArray();
+ JSONObject action = new JSONObject();
- // 设置操作类型:获取所有任务
+ // action_list
action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL);
- // 设置操作ID
action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId());
- // 设置要获取的任务列表ID
action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid);
- // 设置是否获取已删除的任务:false=不获取
action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false);
- // 把获取操作添加到操作列表
actionList.put(action);
- // 给JSON数据添加操作列表
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
- // 给JSON数据添加客户端版本号
+ // client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
- // 发送POST请求,获取服务器响应
JSONObject jsResponse = postRequest(jsPost);
- // 从响应中获取任务数组并返回
return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS);
} catch (JSONException e) {
- // JSON解析失败,打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("get task list: handing jsonobject failed");
}
}
- /**
- * 获取当前同步的谷歌账号
- * 通俗说:返回当前用来和GTask同步的谷歌账号信息
- * @return 同步账号
- */
public Account getSyncAccount() {
return mAccount;
}
- /**
- * 重置更新数据数组
- * 通俗说:清空待提交的更新操作列表,放弃未提交的更新
- */
public void resetUpdateArray() {
mUpdateArray = null;
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskManager.java b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskManager.java
index 39546fe..d2b4082 100644
--- a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskManager.java
+++ b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskManager.java
@@ -16,145 +16,110 @@
package net.micode.notes.gtask.remote;
-import android.app.Activity; // 页面类:用来关联登录需要的页面
-import android.content.ContentResolver; // 本地数据库操作工具:用来读写手机本地的便签数据库
-import android.content.ContentUris; // URI工具:用来拼接本地数据库的访问地址
-import android.content.ContentValues; // 数据容器:用来存储要更新到本地数据库的键值对数据
-import android.content.Context; // 上下文:保存页面/服务信息,获取资源和数据库工具
-import android.database.Cursor; // 数据库查询游标:类似“查询结果的遍历工具”,用来逐条读取数据库查询结果
-import android.util.Log; // 日志工具:打印调试信息,方便查找同步过程中的问题
-
-import net.micode.notes.R; // 资源类:获取APP里的文字资源(比如同步进度提示)
-import net.micode.notes.data.Notes; // 便签常量类:存储本地便签的类型、文件夹ID等固定值
-import net.micode.notes.data.Notes.DataColumns; // 便签数据列:本地便签数据表里的字段名(比如内容ID)
-import net.micode.notes.data.Notes.NoteColumns; // 便签主列:本地便签主表里的字段名(比如便签ID、修改时间)
-import net.micode.notes.gtask.data.MetaData; // 元数据类:存储便签的额外信息(比如本地数据库ID映射)
-import net.micode.notes.gtask.data.Node; // 基础节点类:GTask任务/任务列表的通用数据模型
-import net.micode.notes.gtask.data.SqlNote; // 本地便签操作类:用来读写本地便签数据库的工具
-import net.micode.notes.gtask.data.Task; // GTask任务类:存储单个GTask任务的信息
-import net.micode.notes.gtask.data.TaskList; // GTask任务列表类:存储GTask任务列表的信息
-import net.micode.notes.gtask.exception.ActionFailureException; // 操作失败异常:本地/远程同步操作失败时抛出
-import net.micode.notes.gtask.exception.NetworkFailureException; // 网络失败异常:网络不通或GTask服务器请求失败时抛出
-import net.micode.notes.tool.DataUtils; // 本地便签工具:提供批量删除、判断便签是否存在等通用方法
-import net.micode.notes.tool.GTaskStringUtils; // 字符串工具:存储GTask同步用到的固定字符串(比如文件夹前缀)
-
-import org.json.JSONArray; // JSON数组:存储一组格式化数据(比如多个便签信息)
-import org.json.JSONException; // JSON解析异常:数据格式错误导致解析失败时抛出
-import org.json.JSONObject; // JSON对象:存储键值对格式的数据(比如单个便签的信息)
-
-import java.util.HashMap; // 哈希表:用来存储“键-值”映射关系(比如GTaskID对应本地便签ID)
-import java.util.HashSet; // 哈希集合:用来存储不重复的数据(比如要删除的本地便签ID)
-import java.util.Iterator; // 迭代器:用来遍历哈希表/集合里的所有数据
-import java.util.Map; // 映射接口:哈希表的通用接口
-
-/**
- * GTask同步总管家类
- * 通俗说:这个类是GTask同步的核心,负责统筹所有同步工作(登录、初始化任务列表、同步文件夹/便签、处理增删改查)
- * 特点:整个APP只有一个实例(不会创建多个重复对象),所有同步相关操作都由它统一调度
- */
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.util.Log;
+
+import net.micode.notes.R;
+import net.micode.notes.data.Notes;
+import net.micode.notes.data.Notes.DataColumns;
+import net.micode.notes.data.Notes.NoteColumns;
+import net.micode.notes.gtask.data.MetaData;
+import net.micode.notes.gtask.data.Node;
+import net.micode.notes.gtask.data.SqlNote;
+import net.micode.notes.gtask.data.Task;
+import net.micode.notes.gtask.data.TaskList;
+import net.micode.notes.gtask.exception.ActionFailureException;
+import net.micode.notes.gtask.exception.NetworkFailureException;
+import net.micode.notes.tool.DataUtils;
+import net.micode.notes.tool.GTaskStringUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+
+
public class GTaskManager {
- // 日志标签:打印日志时用来标识是这个类的日志,方便查找同步问题
private static final String TAG = GTaskManager.class.getSimpleName();
- // 同步状态常量:同步成功
public static final int STATE_SUCCESS = 0;
- // 同步状态常量:网络错误(比如没网、GTask服务器连接失败)
+
public static final int STATE_NETWORK_ERROR = 1;
- // 同步状态常量:内部错误(比如本地数据库查询失败、JSON解析失败)
+
public static final int STATE_INTERNAL_ERROR = 2;
- // 同步状态常量:同步正在进行中(已经有一个同步任务在执行,不能重复启动)
+
public static final int STATE_SYNC_IN_PROGRESS = 3;
- // 同步状态常量:同步被取消(用户主动停止了同步)
+
public static final int STATE_SYNC_CANCELLED = 4;
- // 本类的唯一实例:保证整个APP只有一个GTaskManager对象
private static GTaskManager mInstance = null;
- // 登录关联页面:用来获取谷歌账号的登录凭证(令牌)
private Activity mActivity;
- // 上下文:保存当前页面/服务信息,用来获取数据库工具
+
private Context mContext;
- // 本地数据库操作工具:用来读写本地便签数据库
+
private ContentResolver mContentResolver;
- // 同步中标记:true=正在同步,false=未同步
+
private boolean mSyncing;
- // 同步取消标记:true=用户取消了同步,false=同步正常执行
+
private boolean mCancelled;
- // 远程GTask列表哈希表:存储“GTask列表ID-任务列表对象”的映射,方便快速查找
private HashMap mGTaskListHashMap;
- // 远程GTask节点哈希表:存储“GTask节点ID-节点对象(任务/列表)”的映射,方便快速查找
+
private HashMap mGTaskHashMap;
- // 元数据哈希表:存储“GTask任务ID-元数据对象”的映射,保存便签额外信息
+
private HashMap mMetaHashMap;
- // 元数据列表:GTask上专门存储元数据的任务列表
+
private TaskList mMetaList;
- // 本地待删除便签ID集合:存储需要从本地数据库删除的便签ID(比如远程已删除的便签)
+
private HashSet mLocalDeleteIdMap;
- // GTaskID对应本地便签ID哈希表:存储“GTaskID-本地便签ID”的映射
+
private HashMap mGidToNid;
- // 本地便签ID对应GTaskID哈希表:存储“本地便签ID-GTaskID”的映射
+
private HashMap mNidToGid;
- /**
- * 私有构造方法
- * 通俗说:不让外部直接创建这个类的对象,只能通过getInstance()获取唯一实例
- */
private GTaskManager() {
- mSyncing = false; // 初始状态为未同步
- mCancelled = false; // 初始状态为未取消
- mGTaskListHashMap = new HashMap(); // 初始化远程GTask列表哈希表
- mGTaskHashMap = new HashMap(); // 初始化远程GTask节点哈希表
- mMetaHashMap = new HashMap(); // 初始化元数据哈希表
- mMetaList = null; // 元数据列表初始化为null
- mLocalDeleteIdMap = new HashSet(); // 初始化本地待删除便签ID集合
- mGidToNid = new HashMap(); // 初始化GTaskID-本地ID映射表
- mNidToGid = new HashMap(); // 初始化本地ID-GTaskID映射表
+ mSyncing = false;
+ mCancelled = false;
+ mGTaskListHashMap = new HashMap();
+ mGTaskHashMap = new HashMap();
+ mMetaHashMap = new HashMap();
+ mMetaList = null;
+ mLocalDeleteIdMap = new HashSet();
+ mGidToNid = new HashMap();
+ mNidToGid = new HashMap();
}
- /**
- * 获取GTaskManager的唯一实例
- * 通俗说:整个APP只能通过这个方法拿到GTaskManager对象,保证只有一个实例
- * @return GTaskManager唯一实例
- */
public static synchronized GTaskManager getInstance() {
- // 如果实例为null,就创建一个新的(懒加载:用到时才创建)
if (mInstance == null) {
mInstance = new GTaskManager();
}
return mInstance;
}
- /**
- * 设置登录关联的页面
- * 通俗说:给同步管理器设置一个页面,用来获取谷歌账号的登录凭证(没有这个页面没法登录GTask)
- * @param activity 登录关联的页面
- */
public synchronized void setActivityContext(Activity activity) {
- // 这个页面用来获取谷歌账号的登录令牌
+ // used for getting authtoken
mActivity = activity;
}
- /**
- * 同步主方法:启动GTask和本地便签的同步
- * 通俗说:这是同步的入口方法,负责统筹整个同步流程,返回同步结果状态
- * @param context 上下文(获取数据库工具用)
- * @param asyncTask 异步任务对象(用来发送同步进度提示)
- * @return 同步状态(成功/网络错误/内部错误/正在同步/已取消)
- */
public int sync(Context context, GTaskASyncTask asyncTask) {
- // 如果已经在同步中,打印日志并返回“正在同步”状态
if (mSyncing) {
Log.d(TAG, "Sync is in progress");
return STATE_SYNC_IN_PROGRESS;
}
-
- // 初始化同步所需的变量
mContext = context;
- mContentResolver = mContext.getContentResolver(); // 获取本地数据库操作工具
- mSyncing = true; // 标记为正在同步
- mCancelled = false; // 标记为未取消
- // 清空所有存储映射的容器(避免上次同步的数据残留)
+ mContentResolver = mContext.getContentResolver();
+ mSyncing = true;
+ mCancelled = false;
mGTaskListHashMap.clear();
mGTaskHashMap.clear();
mMetaHashMap.clear();
@@ -163,94 +128,74 @@ public class GTaskManager {
mNidToGid.clear();
try {
- // 获取GTask客户端实例(用来和GTask服务器通信)
GTaskClient client = GTaskClient.getInstance();
- client.resetUpdateArray(); // 清空GTask客户端的待更新操作列表
+ client.resetUpdateArray();
- // 第一步:登录GTask服务器(如果用户没取消同步)
+ // login google task
if (!mCancelled) {
if (!client.login(mActivity)) {
- // 登录失败,抛出网络失败异常
throw new NetworkFailureException("login google task failed");
}
}
- // 第二步:初始化远程GTask任务列表(发送“正在初始化列表”的进度提示)
- asyncTask.publishProgress(mContext.getString(R.string.sync_progress_init_list));
+ // get the task list from google
+ asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list));
initGTaskList();
- // 第三步:执行具体的内容同步工作(发送“正在同步”的进度提示)
- asyncTask.publishProgress(mContext.getString(R.string.sync_progress_syncing));
+ // do content sync work
+ asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing));
syncContent();
-
} catch (NetworkFailureException e) {
- // 网络错误(比如没网),打印日志并返回网络错误状态
Log.e(TAG, e.toString());
return STATE_NETWORK_ERROR;
} catch (ActionFailureException e) {
- // 内部操作错误(比如数据库查询失败),打印日志并返回内部错误状态
Log.e(TAG, e.toString());
return STATE_INTERNAL_ERROR;
} catch (Exception e) {
- // 其他未知错误,打印日志和异常堆栈,返回内部错误状态
Log.e(TAG, e.toString());
e.printStackTrace();
return STATE_INTERNAL_ERROR;
} finally {
- // 无论同步成功还是失败,都清空所有容器,释放资源
mGTaskListHashMap.clear();
mGTaskHashMap.clear();
mMetaHashMap.clear();
mLocalDeleteIdMap.clear();
mGidToNid.clear();
mNidToGid.clear();
- mSyncing = false; // 标记为未同步
+ mSyncing = false;
}
- // 根据是否取消同步,返回对应状态
return mCancelled ? STATE_SYNC_CANCELLED : STATE_SUCCESS;
}
- /**
- * 初始化远程GTask任务列表
- * 通俗说:从GTask服务器获取所有任务列表和任务,存储到本地容器中,方便后续同步
- * @throws NetworkFailureException 网络失败时抛出
- */
private void initGTaskList() throws NetworkFailureException {
- // 如果用户取消了同步,直接返回
if (mCancelled)
return;
-
- // 获取GTask客户端实例
GTaskClient client = GTaskClient.getInstance();
try {
- // 从GTask服务器获取所有任务列表的JSON数据
JSONArray jsTaskLists = client.getTaskLists();
- // 第一步:先初始化元数据列表(存储便签额外信息的列表)
+ // init meta list first
mMetaList = null;
for (int i = 0; i < jsTaskLists.length(); i++) {
JSONObject object = jsTaskLists.getJSONObject(i);
- String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); // 获取任务列表ID
- String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); // 获取任务列表名称
+ String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
+ String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME);
- // 判断是否是元数据列表(名称以固定前缀+META结尾)
if (name
.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) {
mMetaList = new TaskList();
- mMetaList.setContentByRemoteJSON(object); // 用服务器返回的数据初始化元数据列表
+ mMetaList.setContentByRemoteJSON(object);
- // 加载元数据列表里的所有元数据
+ // load meta data
JSONArray jsMetas = client.getTaskList(gid);
for (int j = 0; j < jsMetas.length(); j++) {
object = (JSONObject) jsMetas.getJSONObject(j);
MetaData metaData = new MetaData();
- metaData.setContentByRemoteJSON(object); // 用服务器数据初始化元数据
- // 如果元数据有保存价值(不是空数据)
+ metaData.setContentByRemoteJSON(object);
if (metaData.isWorthSaving()) {
- mMetaList.addChildTask(metaData); // 添加到元数据列表
+ mMetaList.addChildTask(metaData);
if (metaData.getGid() != null) {
- // 存储到元数据哈希表(关联GTask任务ID和元数据)
mMetaHashMap.put(metaData.getRelatedGid(), metaData);
}
}
@@ -258,437 +203,358 @@ public class GTaskManager {
}
}
- // 如果元数据列表不存在,就创建一个新的元数据列表并提交到GTask服务器
+ // create meta list if not existed
if (mMetaList == null) {
mMetaList = new TaskList();
mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX
- + GTaskStringUtils.FOLDER_META); // 设置元数据列表名称
- GTaskClient.getInstance().createTaskList(mMetaList); // 创建远程元数据列表
+ + GTaskStringUtils.FOLDER_META);
+ GTaskClient.getInstance().createTaskList(mMetaList);
}
- // 第二步:初始化普通GTask任务列表(非元数据列表)
+ // init task list
for (int i = 0; i < jsTaskLists.length(); i++) {
JSONObject object = jsTaskLists.getJSONObject(i);
- String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); // 获取任务列表ID
- String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); // 获取任务列表名称
+ String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
+ String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME);
- // 判断是否是MIUI便签对应的任务列表(以固定前缀开头,且不是元数据列表)
if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)
&& !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX
- + GTaskStringUtils.FOLDER_META)) {
+ + GTaskStringUtils.FOLDER_META)) {
TaskList tasklist = new TaskList();
- tasklist.setContentByRemoteJSON(object); // 用服务器数据初始化任务列表
- // 存储到任务列表哈希表和节点哈希表
+ tasklist.setContentByRemoteJSON(object);
mGTaskListHashMap.put(gid, tasklist);
mGTaskHashMap.put(gid, tasklist);
- // 加载该任务列表下的所有任务
+ // load tasks
JSONArray jsTasks = client.getTaskList(gid);
for (int j = 0; j < jsTasks.length(); j++) {
object = (JSONObject) jsTasks.getJSONObject(j);
- gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); // 获取任务ID
+ gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
Task task = new Task();
- task.setContentByRemoteJSON(object); // 用服务器数据初始化任务
- // 如果任务有保存价值(不是空数据)
+ task.setContentByRemoteJSON(object);
if (task.isWorthSaving()) {
- task.setMetaInfo(mMetaHashMap.get(gid)); // 设置任务的元数据
- tasklist.addChildTask(task); // 添加到任务列表
- mGTaskHashMap.put(gid, task); // 存储到节点哈希表
+ task.setMetaInfo(mMetaHashMap.get(gid));
+ tasklist.addChildTask(task);
+ mGTaskHashMap.put(gid, task);
}
}
}
}
} catch (JSONException e) {
- // JSON解析失败,打印日志和堆栈,抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("initGTaskList: handing JSONObject failed");
}
}
- /**
- * 同步核心方法:处理本地和远程的内容同步(文件夹、便签的增删改查)
- * 通俗说:这是同步的核心逻辑,负责对比本地和远程的便签/文件夹,处理各种同步场景(新增、删除、更新)
- * @throws NetworkFailureException 网络失败时抛出
- */
private void syncContent() throws NetworkFailureException {
- int syncType; // 同步类型(新增本地/远程、删除本地/远程、更新本地/远程等)
- Cursor c = null; // 数据库查询游标
- String gid; // GTask节点ID
- Node node; // GTask节点对象(任务/列表)
+ int syncType;
+ Cursor c = null;
+ String gid;
+ Node node;
- // 清空本地待删除便签ID集合
mLocalDeleteIdMap.clear();
- // 如果用户取消了同步,直接返回
if (mCancelled) {
return;
}
- // 第一步:处理本地回收站里的便签(这些是本地已删除的便签,需要同步删除远程对应任务)
+ // for local deleted note
try {
- // 查询本地回收站里的非系统便签(parent_id=回收站ID,type≠系统类型)
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type<>? AND parent_id=?)", new String[] {
String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER)
}, null);
if (c != null) {
- // 逐条读取查询结果
while (c.moveToNext()) {
- gid = c.getString(SqlNote.GTASK_ID_COLUMN); // 获取便签对应的GTaskID
- node = mGTaskHashMap.get(gid); // 根据GTaskID获取远程节点
+ gid = c.getString(SqlNote.GTASK_ID_COLUMN);
+ node = mGTaskHashMap.get(gid);
if (node != null) {
- // 如果远程存在对应节点,从节点哈希表移除,并执行“删除远程节点”操作
mGTaskHashMap.remove(gid);
doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c);
}
- // 将该便签ID加入本地待删除集合(后续批量删除本地数据)
+
mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN));
}
} else {
- // 查询失败,打印警告日志
Log.w(TAG, "failed to query trash folder");
}
} finally {
- // 无论查询成功与否,都关闭游标,释放资源
if (c != null) {
c.close();
c = null;
}
}
- // 第二步:先同步文件夹(文件夹同步优先级高于普通便签)
+ // sync folder first
syncFolder();
- // 第三步:处理本地数据库中存在的普通便签(非回收站、类型为普通便签)
+ // for note existing in database
try {
- // 查询本地非回收站的普通便签(type=普通便签,parent_id≠回收站ID)
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type=? AND parent_id<>?)", new String[] {
String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER)
}, NoteColumns.TYPE + " DESC");
if (c != null) {
- // 逐条读取查询结果
while (c.moveToNext()) {
- gid = c.getString(SqlNote.GTASK_ID_COLUMN); // 获取便签对应的GTaskID
- node = mGTaskHashMap.get(gid); // 根据GTaskID获取远程节点
+ gid = c.getString(SqlNote.GTASK_ID_COLUMN);
+ node = mGTaskHashMap.get(gid);
if (node != null) {
- // 远程存在对应节点:从节点哈希表移除,建立GTaskID和本地ID的映射
mGTaskHashMap.remove(gid);
mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN));
mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid);
- // 获取同步类型(判断是更新本地还是更新远程)
syncType = node.getSyncAction(c);
} else {
- // 远程不存在对应节点
if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) {
- // 本地GTaskID为空,说明是本地新增便签,需要“新增到远程”
+ // local add
syncType = Node.SYNC_ACTION_ADD_REMOTE;
} else {
- // 本地有GTaskID但远程不存在,说明远程已删除,需要“删除本地便签”
+ // remote delete
syncType = Node.SYNC_ACTION_DEL_LOCAL;
}
}
- // 执行具体的同步操作
doContentSync(syncType, node, c);
}
} else {
- // 查询失败,打印警告日志
Log.w(TAG, "failed to query existing note in database");
}
} finally {
- // 关闭游标,释放资源
if (c != null) {
c.close();
c = null;
}
}
- // 第四步:处理远程有但本地没有的节点(远程新增的内容,需要同步到本地)
+ // go through remaining items
Iterator> iter = mGTaskHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = iter.next();
node = entry.getValue();
- // 执行“新增到本地”的同步操作
doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null);
}
- // 第五步:批量删除本地待删除的便签(如果用户没取消同步)
+ // mCancelled can be set by another thread, so we neet to check one by
+ // one
+ // clear local delete table
if (!mCancelled) {
if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) {
- // 批量删除失败,抛出操作失败异常
throw new ActionFailureException("failed to batch-delete local deleted notes");
}
}
- // 第六步:提交远程更新并刷新本地同步ID(如果用户没取消同步)
+ // refresh local sync id
if (!mCancelled) {
- GTaskClient.getInstance().commitUpdate(); // 提交所有待更新的远程操作
- refreshLocalSyncId(); // 刷新本地便签的同步ID(记录最后修改时间)
+ GTaskClient.getInstance().commitUpdate();
+ refreshLocalSyncId();
}
}
- /**
- * 同步文件夹:处理本地和远程文件夹的增删改查
- * 通俗说:专门同步便签文件夹(根文件夹、通话记录文件夹、自定义文件夹)
- * @throws NetworkFailureException 网络失败时抛出
- */
private void syncFolder() throws NetworkFailureException {
- Cursor c = null; // 数据库查询游标
- String gid; // GTask文件夹ID
- Node node; // GTask文件夹节点
- int syncType; // 同步类型
+ Cursor c = null;
+ String gid;
+ Node node;
+ int syncType;
- // 如果用户取消了同步,直接返回
if (mCancelled) {
return;
}
- // 第一步:同步根文件夹(本地默认的根文件夹)
+ // for root folder
try {
- // 查询本地根文件夹(ID=根文件夹ID)
c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI,
Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null);
if (c != null) {
- c.moveToNext(); // 根文件夹只有一个,直接移动到第一条
- gid = c.getString(SqlNote.GTASK_ID_COLUMN); // 获取根文件夹对应的GTaskID
- node = mGTaskHashMap.get(gid); // 根据GTaskID获取远程文件夹
+ c.moveToNext();
+ gid = c.getString(SqlNote.GTASK_ID_COLUMN);
+ node = mGTaskHashMap.get(gid);
if (node != null) {
- // 远程存在对应文件夹:从节点哈希表移除,建立GTaskID和本地ID的映射
mGTaskHashMap.remove(gid);
mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER);
mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid);
- // 系统文件夹只更新名称:如果远程名称和本地默认名称不一致,执行“更新远程名称”操作
+ // for system folder, only update remote name if necessary
if (!node.getName().equals(
GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT))
doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c);
} else {
- // 远程不存在对应文件夹,执行“新增到远程”操作
doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c);
}
} else {
- // 查询失败,打印警告日志
Log.w(TAG, "failed to query root folder");
}
} finally {
- // 关闭游标,释放资源
if (c != null) {
c.close();
c = null;
}
}
- // 第二步:同步通话记录文件夹
+ // for call-note folder
try {
- // 查询本地通话记录文件夹(ID=通话记录文件夹ID)
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)",
new String[] {
- String.valueOf(Notes.ID_CALL_RECORD_FOLDER)
+ String.valueOf(Notes.ID_CALL_RECORD_FOLDER)
}, null);
if (c != null) {
if (c.moveToNext()) {
- gid = c.getString(SqlNote.GTASK_ID_COLUMN); // 获取通话记录文件夹对应的GTaskID
- node = mGTaskHashMap.get(gid); // 根据GTaskID获取远程文件夹
+ gid = c.getString(SqlNote.GTASK_ID_COLUMN);
+ node = mGTaskHashMap.get(gid);
if (node != null) {
- // 远程存在对应文件夹:从节点哈希表移除,建立GTaskID和本地ID的映射
mGTaskHashMap.remove(gid);
mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER);
mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid);
- // 系统文件夹只更新名称:如果远程名称和本地默认名称不一致,执行“更新远程名称”操作
+ // for system folder, only update remote name if
+ // necessary
if (!node.getName().equals(
GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_CALL_NOTE))
doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c);
} else {
- // 远程不存在对应文件夹,执行“新增到远程”操作
doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c);
}
}
} else {
- // 查询失败,打印警告日志
Log.w(TAG, "failed to query call note folder");
}
} finally {
- // 关闭游标,释放资源
if (c != null) {
c.close();
c = null;
}
}
- // 第三步:同步本地自定义文件夹(非系统文件夹、非回收站)
+ // for local existing folders
try {
- // 查询本地自定义文件夹(type=文件夹,parent_id≠回收站ID)
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type=? AND parent_id<>?)", new String[] {
String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)
}, NoteColumns.TYPE + " DESC");
if (c != null) {
- // 逐条读取查询结果
while (c.moveToNext()) {
- gid = c.getString(SqlNote.GTASK_ID_COLUMN); // 获取文件夹对应的GTaskID
- node = mGTaskHashMap.get(gid); // 根据GTaskID获取远程文件夹
+ gid = c.getString(SqlNote.GTASK_ID_COLUMN);
+ node = mGTaskHashMap.get(gid);
if (node != null) {
- // 远程存在对应文件夹:从节点哈希表移除,建立GTaskID和本地ID的映射
mGTaskHashMap.remove(gid);
mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN));
mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid);
- // 获取同步类型(判断是更新本地还是更新远程)
syncType = node.getSyncAction(c);
} else {
- // 远程不存在对应文件夹
if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) {
- // 本地GTaskID为空,说明是本地新增文件夹,需要“新增到远程”
+ // local add
syncType = Node.SYNC_ACTION_ADD_REMOTE;
} else {
- // 本地有GTaskID但远程不存在,说明远程已删除,需要“删除本地文件夹”
+ // remote delete
syncType = Node.SYNC_ACTION_DEL_LOCAL;
}
}
- // 执行具体的同步操作
doContentSync(syncType, node, c);
}
} else {
- // 查询失败,打印警告日志
Log.w(TAG, "failed to query existing folder");
}
} finally {
- // 关闭游标,释放资源
if (c != null) {
c.close();
c = null;
}
}
- // 第四步:处理远程有但本地没有的文件夹(远程新增的文件夹,需要同步到本地)
+ // for remote add folders
Iterator> iter = mGTaskListHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = iter.next();
gid = entry.getKey();
node = entry.getValue();
if (mGTaskHashMap.containsKey(gid)) {
- // 从节点哈希表移除,执行“新增到本地”的同步操作
mGTaskHashMap.remove(gid);
doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null);
}
}
- // 如果用户没取消同步,提交文件夹的远程更新
if (!mCancelled)
GTaskClient.getInstance().commitUpdate();
}
- /**
- * 处理具体的同步操作
- * 通俗说:根据不同的同步类型,调用对应的方法(新增、删除、更新本地/远程内容)
- * @param syncType 同步类型(新增本地/远程、删除本地/远程等)
- * @param node GTask节点对象(任务/文件夹)
- * @param c 数据库查询游标(存储本地便签/文件夹数据)
- * @throws NetworkFailureException 网络失败时抛出
- */
private void doContentSync(int syncType, Node node, Cursor c) throws NetworkFailureException {
- // 如果用户取消了同步,直接返回
if (mCancelled) {
return;
}
- MetaData meta; // 元数据对象
- // 根据同步类型执行不同的操作
+ MetaData meta;
switch (syncType) {
case Node.SYNC_ACTION_ADD_LOCAL:
- // 远程新增:同步到本地
addLocalNode(node);
break;
case Node.SYNC_ACTION_ADD_REMOTE:
- // 本地新增:同步到远程
addRemoteNode(node, c);
break;
case Node.SYNC_ACTION_DEL_LOCAL:
- // 远程删除:删除本地对应内容
meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN));
if (meta != null) {
- GTaskClient.getInstance().deleteNode(meta); // 删除远程元数据
+ GTaskClient.getInstance().deleteNode(meta);
}
- mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); // 加入本地待删除集合
+ mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN));
break;
case Node.SYNC_ACTION_DEL_REMOTE:
- // 本地删除:删除远程对应内容
meta = mMetaHashMap.get(node.getGid());
if (meta != null) {
- GTaskClient.getInstance().deleteNode(meta); // 删除远程元数据
+ GTaskClient.getInstance().deleteNode(meta);
}
- GTaskClient.getInstance().deleteNode(node); // 删除远程节点
+ GTaskClient.getInstance().deleteNode(node);
break;
case Node.SYNC_ACTION_UPDATE_LOCAL:
- // 远程更新:更新本地对应内容
updateLocalNode(node, c);
break;
case Node.SYNC_ACTION_UPDATE_REMOTE:
- // 本地更新:更新远程对应内容
updateRemoteNode(node, c);
break;
case Node.SYNC_ACTION_UPDATE_CONFLICT:
- // 同步冲突:目前简单处理为以本地更新为准,更新远程内容
- // (更优方案可以合并本地和远程的修改,这里暂不实现)
+ // merging both modifications maybe a good idea
+ // right now just use local update simply
updateRemoteNode(node, c);
break;
case Node.SYNC_ACTION_NONE:
- // 无需同步:直接跳过
break;
case Node.SYNC_ACTION_ERROR:
default:
- // 未知同步类型:抛出操作失败异常
throw new ActionFailureException("unkown sync action type");
}
}
- /**
- * 新增本地节点:把远程新增的GTask节点(任务/文件夹)同步到本地数据库
- * @param node 远程GTask节点
- * @throws NetworkFailureException 网络失败时抛出
- */
private void addLocalNode(Node node) throws NetworkFailureException {
- // 如果用户取消了同步,直接返回
if (mCancelled) {
return;
}
- SqlNote sqlNote; // 本地便签操作对象
+ SqlNote sqlNote;
if (node instanceof TaskList) {
- // 同步的是文件夹
if (node.getName().equals(
GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) {
- // 远程是默认文件夹,对应本地根文件夹
sqlNote = new SqlNote(mContext, Notes.ID_ROOT_FOLDER);
} else if (node.getName().equals(
GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) {
- // 远程是通话记录文件夹,对应本地通话记录文件夹
sqlNote = new SqlNote(mContext, Notes.ID_CALL_RECORD_FOLDER);
} else {
- // 远程是自定义文件夹,创建新的本地文件夹
sqlNote = new SqlNote(mContext);
- sqlNote.setContent(node.getLocalJSONFromContent()); // 设置文件夹内容
- sqlNote.setParentId(Notes.ID_ROOT_FOLDER); // 父文件夹为根文件夹
+ sqlNote.setContent(node.getLocalJSONFromContent());
+ sqlNote.setParentId(Notes.ID_ROOT_FOLDER);
}
} else {
- // 同步的是普通任务(便签)
sqlNote = new SqlNote(mContext);
- JSONObject js = node.getLocalJSONFromContent(); // 获取任务的本地JSON数据
+ JSONObject js = node.getLocalJSONFromContent();
try {
- // 处理便签ID:如果ID已存在,移除ID(重新生成新ID)
if (js.has(GTaskStringUtils.META_HEAD_NOTE)) {
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
if (note.has(NoteColumns.ID)) {
long id = note.getLong(NoteColumns.ID);
if (DataUtils.existInNoteDatabase(mContentResolver, id)) {
+ // the id is not available, have to create a new one
note.remove(NoteColumns.ID);
}
}
}
- // 处理便签数据ID:如果数据ID已存在,移除ID(重新生成新ID)
if (js.has(GTaskStringUtils.META_HEAD_DATA)) {
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
for (int i = 0; i < dataArray.length(); i++) {
@@ -696,114 +562,92 @@ public class GTaskManager {
if (data.has(DataColumns.ID)) {
long dataId = data.getLong(DataColumns.ID);
if (DataUtils.existInDataDatabase(mContentResolver, dataId)) {
+ // the data id is not available, have to create
+ // a new one
data.remove(DataColumns.ID);
}
}
}
+
}
} catch (JSONException e) {
- // JSON解析失败,打印警告日志
Log.w(TAG, e.toString());
e.printStackTrace();
}
- sqlNote.setContent(js); // 设置便签内容
+ sqlNote.setContent(js);
- // 获取任务的父文件夹本地ID(通过GTask父文件夹ID映射)
Long parentId = mGidToNid.get(((Task) node).getParent().getGid());
if (parentId == null) {
- // 找不到父文件夹ID,抛出操作失败异常
Log.e(TAG, "cannot find task's parent id locally");
throw new ActionFailureException("cannot add local node");
}
- sqlNote.setParentId(parentId.longValue()); // 设置便签的父文件夹ID
+ sqlNote.setParentId(parentId.longValue());
}
- // 设置便签对应的GTaskID,提交到本地数据库
+ // create the local node
sqlNote.setGtaskId(node.getGid());
- sqlNote.commit(false); // false=新增数据
+ sqlNote.commit(false);
- // 更新GTaskID和本地ID的映射关系
+ // update gid-nid mapping
mGidToNid.put(node.getGid(), sqlNote.getId());
mNidToGid.put(sqlNote.getId(), node.getGid());
- // 更新远程元数据(记录本地便签信息)
+ // update meta
updateRemoteMeta(node.getGid(), sqlNote);
}
- /**
- * 更新本地节点:用远程更新的GTask节点数据,更新本地对应的便签/文件夹
- * @param node 远程更新后的GTask节点
- * @param c 本地数据库查询游标(存储旧的本地数据)
- * @throws NetworkFailureException 网络失败时抛出
- */
private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException {
- // 如果用户取消了同步,直接返回
if (mCancelled) {
return;
}
- // 用本地旧数据初始化便签操作对象
- SqlNote sqlNote = new SqlNote(mContext, c);
- // 用远程更新的数据设置便签内容
+ SqlNote sqlNote;
+ // update the note locally
+ sqlNote = new SqlNote(mContext, c);
sqlNote.setContent(node.getLocalJSONFromContent());
- // 获取父文件夹本地ID(文件夹的父ID默认是根文件夹,任务的父ID通过GTask映射)
Long parentId = (node instanceof Task) ? mGidToNid.get(((Task) node).getParent().getGid())
: new Long(Notes.ID_ROOT_FOLDER);
if (parentId == null) {
- // 找不到父文件夹ID,抛出操作失败异常
Log.e(TAG, "cannot find task's parent id locally");
throw new ActionFailureException("cannot update local node");
}
- sqlNote.setParentId(parentId.longValue()); // 设置父文件夹ID
- sqlNote.commit(true); // true=更新数据
+ sqlNote.setParentId(parentId.longValue());
+ sqlNote.commit(true);
- // 更新远程元数据(记录本地便签的最新信息)
+ // update meta info
updateRemoteMeta(node.getGid(), sqlNote);
}
- /**
- * 新增远程节点:把本地新增的便签/文件夹,同步到GTask服务器
- * @param node (暂时无用,本地新增节点对应远程无节点,传null)
- * @param c 本地数据库查询游标(存储本地新增的便签/文件夹数据)
- * @throws NetworkFailureException 网络失败时抛出
- */
private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException {
- // 如果用户取消了同步,直接返回
if (mCancelled) {
return;
}
- // 用本地数据初始化便签操作对象
SqlNote sqlNote = new SqlNote(mContext, c);
- Node n; // 要同步到远程的节点对象
+ Node n;
- // 更新到远程
+ // update remotely
if (sqlNote.isNoteType()) {
- // 同步的是普通便签
Task task = new Task();
- task.setContentByLocalJSON(sqlNote.getContent()); // 用本地数据初始化GTask任务
+ task.setContentByLocalJSON(sqlNote.getContent());
- // 获取本地父文件夹对应的GTaskID
String parentGid = mNidToGid.get(sqlNote.getParentId());
if (parentGid == null) {
- // 找不到父文件夹的GTaskID,抛出操作失败异常
Log.e(TAG, "cannot find task's parent tasklist");
throw new ActionFailureException("cannot add remote task");
}
- // 将任务添加到对应的远程任务列表
mGTaskListHashMap.get(parentGid).addChildTask(task);
- GTaskClient.getInstance().createTask(task); // 在GTask服务器创建任务
- n = (Node) task; // 赋值给节点对象
+ GTaskClient.getInstance().createTask(task);
+ n = (Node) task;
- // 添加元数据(记录本地便签信息)
+ // add meta
updateRemoteMeta(task.getGid(), sqlNote);
} else {
- // 同步的是文件夹
TaskList tasklist = null;
- // 拼接文件夹的远程名称(固定前缀+本地文件夹名称)
+ // we need to skip folder if it has already existed
String folderName = GTaskStringUtils.MIUI_FOLDER_PREFFIX;
if (sqlNote.getId() == Notes.ID_ROOT_FOLDER)
folderName += GTaskStringUtils.FOLDER_DEFAULT;
@@ -812,7 +656,6 @@ public class GTaskManager {
else
folderName += sqlNote.getSnippet();
- // 检查远程是否已存在该文件夹(避免重复创建)
Iterator> iter = mGTaskListHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = iter.next();
@@ -820,160 +663,126 @@ public class GTaskManager {
TaskList list = entry.getValue();
if (list.getName().equals(folderName)) {
- tasklist = list; // 找到已存在的文件夹
+ tasklist = list;
if (mGTaskHashMap.containsKey(gid)) {
- mGTaskHashMap.remove(gid); // 从节点哈希表移除
+ mGTaskHashMap.remove(gid);
}
break;
}
}
- // 如果远程不存在该文件夹,创建新的远程文件夹
+ // no match we can add now
if (tasklist == null) {
tasklist = new TaskList();
- tasklist.setContentByLocalJSON(sqlNote.getContent()); // 用本地数据初始化GTask文件夹
- GTaskClient.getInstance().createTaskList(tasklist); // 在GTask服务器创建文件夹
- mGTaskListHashMap.put(tasklist.getGid(), tasklist); // 加入远程文件夹哈希表
+ tasklist.setContentByLocalJSON(sqlNote.getContent());
+ GTaskClient.getInstance().createTaskList(tasklist);
+ mGTaskListHashMap.put(tasklist.getGid(), tasklist);
}
- n = (Node) tasklist; // 赋值给节点对象
+ n = (Node) tasklist;
}
- // 更新本地便签的GTaskID,提交更新
+ // update local note
sqlNote.setGtaskId(n.getGid());
- sqlNote.commit(false); // 先提交GTaskID
- sqlNote.resetLocalModified(); // 重置本地修改标记
- sqlNote.commit(true); // 提交修改标记的更新
+ sqlNote.commit(false);
+ sqlNote.resetLocalModified();
+ sqlNote.commit(true);
- // 更新GTaskID和本地ID的映射关系
+ // gid-id mapping
mGidToNid.put(n.getGid(), sqlNote.getId());
mNidToGid.put(sqlNote.getId(), n.getGid());
}
- /**
- * 更新远程节点:用本地更新的便签/文件夹数据,更新GTask服务器对应的节点
- * @param node 远程待更新的GTask节点
- * @param c 本地数据库查询游标(存储本地更新后的便签/文件夹数据)
- * @throws NetworkFailureException 网络失败时抛出
- */
private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException {
- // 如果用户取消了同步,直接返回
if (mCancelled) {
return;
}
- // 用本地更新后的数据初始化便签操作对象
SqlNote sqlNote = new SqlNote(mContext, c);
- // 用本地数据更新远程节点内容
+ // update remotely
node.setContentByLocalJSON(sqlNote.getContent());
- GTaskClient.getInstance().addUpdateNode(node); // 添加到远程待更新列表
+ GTaskClient.getInstance().addUpdateNode(node);
- // 更新远程元数据(记录本地便签的最新信息)
+ // update meta
updateRemoteMeta(node.getGid(), sqlNote);
- // 如果是普通便签,还需要处理任务移动(父文件夹变化)
+ // move task if necessary
if (sqlNote.isNoteType()) {
Task task = (Task) node;
- TaskList preParentList = task.getParent(); // 任务原来的远程父文件夹
+ TaskList preParentList = task.getParent();
- // 获取本地更新后的父文件夹对应的GTaskID
String curParentGid = mNidToGid.get(sqlNote.getParentId());
if (curParentGid == null) {
- // 找不到父文件夹的GTaskID,抛出操作失败异常
Log.e(TAG, "cannot find task's parent tasklist");
throw new ActionFailureException("cannot update remote task");
}
- TaskList curParentList = mGTaskListHashMap.get(curParentGid); // 任务新的远程父文件夹
+ TaskList curParentList = mGTaskListHashMap.get(curParentGid);
- // 如果父文件夹发生变化(任务被移动)
if (preParentList != curParentList) {
- preParentList.removeChildTask(task); // 从原来的父文件夹移除任务
- curParentList.addChildTask(task); // 添加到新的父文件夹
- GTaskClient.getInstance().moveTask(task, preParentList, curParentList); // 在GTask服务器移动任务
+ preParentList.removeChildTask(task);
+ curParentList.addChildTask(task);
+ GTaskClient.getInstance().moveTask(task, preParentList, curParentList);
}
}
- // 重置本地修改标记,提交更新
+ // clear local modified flag
sqlNote.resetLocalModified();
sqlNote.commit(true);
}
- /**
- * 更新远程元数据:把本地便签的信息存储到GTask的元数据列表中
- * @param gid 便签对应的GTaskID
- * @param sqlNote 本地便签操作对象
- * @throws NetworkFailureException 网络失败时抛出
- */
private void updateRemoteMeta(String gid, SqlNote sqlNote) throws NetworkFailureException {
- // 只给普通便签更新元数据
if (sqlNote != null && sqlNote.isNoteType()) {
- MetaData metaData = mMetaHashMap.get(gid); // 获取已有的元数据
+ MetaData metaData = mMetaHashMap.get(gid);
if (metaData != null) {
- // 元数据已存在,更新内容并添加到远程待更新列表
metaData.setMeta(gid, sqlNote.getContent());
GTaskClient.getInstance().addUpdateNode(metaData);
} else {
- // 元数据不存在,创建新的元数据并提交到GTask服务器
metaData = new MetaData();
metaData.setMeta(gid, sqlNote.getContent());
- mMetaList.addChildTask(metaData); // 添加到元数据列表
- mMetaHashMap.put(gid, metaData); // 加入元数据哈希表
- GTaskClient.getInstance().createTask(metaData); // 创建远程元数据
+ mMetaList.addChildTask(metaData);
+ mMetaHashMap.put(gid, metaData);
+ GTaskClient.getInstance().createTask(metaData);
}
}
}
- /**
- * 刷新本地同步ID:把GTask节点的最后修改时间,更新到本地便签的同步ID字段
- * 通俗说:记录本地便签和远程GTask的同步状态,方便下次同步判断是否有修改
- * @throws NetworkFailureException 网络失败时抛出
- */
private void refreshLocalSyncId() throws NetworkFailureException {
- // 如果用户取消了同步,直接返回
if (mCancelled) {
return;
}
- // 重新获取最新的远程GTask列表(确保数据是最新的)
+ // get the latest gtask list
mGTaskHashMap.clear();
mGTaskListHashMap.clear();
mMetaHashMap.clear();
initGTaskList();
- Cursor c = null; // 数据库查询游标
+ Cursor c = null;
try {
- // 查询本地非系统、非回收站的便签/文件夹
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type<>? AND parent_id<>?)", new String[] {
String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER)
}, NoteColumns.TYPE + " DESC");
if (c != null) {
- // 逐条读取查询结果
while (c.moveToNext()) {
- String gid = c.getString(SqlNote.GTASK_ID_COLUMN); // 获取便签对应的GTaskID
- Node node = mGTaskHashMap.get(gid); // 获取远程节点
+ String gid = c.getString(SqlNote.GTASK_ID_COLUMN);
+ Node node = mGTaskHashMap.get(gid);
if (node != null) {
- // 远程节点存在,从节点哈希表移除
mGTaskHashMap.remove(gid);
- // 准备更新的数据:同步ID=远程节点的最后修改时间
ContentValues values = new ContentValues();
values.put(NoteColumns.SYNC_ID, node.getLastModified());
- // 更新本地便签的同步ID
mContentResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI,
c.getLong(SqlNote.ID_COLUMN)), values, null, null);
} else {
- // 远程节点不存在,说明同步有遗漏,抛出操作失败异常
Log.e(TAG, "something is missed");
throw new ActionFailureException(
"some local items don't have gid after sync");
}
}
} else {
- // 查询失败,打印警告日志
Log.w(TAG, "failed to query local note to refresh sync id");
}
} finally {
- // 关闭游标,释放资源
if (c != null) {
c.close();
c = null;
@@ -981,20 +790,11 @@ public class GTaskManager {
}
}
- /**
- * 获取当前同步的谷歌账号名称
- * 通俗说:返回正在用来同步GTask的谷歌账号(比如xxx@gmail.com)
- * @return 谷歌账号名称
- */
public String getSyncAccount() {
return GTaskClient.getInstance().getSyncAccount().name;
}
- /**
- * 取消同步:标记同步为已取消,同步过程中会检测该标记并停止
- * 通俗说:用户不想同步了,调用这个方法就能停止正在执行的同步任务
- */
public void cancelSync() {
mCancelled = true;
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskSyncService.java b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskSyncService.java
index d9f20e0..cca36f7 100644
--- a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskSyncService.java
+++ b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskSyncService.java
@@ -16,206 +16,113 @@
package net.micode.notes.gtask.remote;
-import android.app.Activity; // 页面类:外部启动同步时需要关联的页面
-import android.app.Service; // 后台服务类:在后台执行耗时操作(同步),不占用前台页面资源
-import android.content.Context; // 上下文:保存页面/服务信息,用来启动服务
-import android.content.Intent; // 意图:用来传递指令(启动/取消同步)、发送广播
-import android.os.Bundle; // 数据容器:用来存储意图中的额外参数(比如同步操作类型)
-import android.os.IBinder; // 绑定接口:服务绑定相关(这里用不到)
-
-/**
- * GTask同步后台服务类
- * 通俗说:这个类是后台运行的服务,专门负责管理GTask同步(启动同步、取消同步),
- * 还会发送广播,把同步状态(是否在同步)和进度(比如“正在登录”)通知给前台界面
- * 特点:在后台运行,即使前台页面关闭,同步也能继续执行
- */
+import android.app.Activity;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+
public class GTaskSyncService extends Service {
- // 意图中存储“同步操作类型”的键名:用来区分是“启动同步”还是“取消同步”
public final static String ACTION_STRING_NAME = "sync_action_type";
- // 同步操作类型:启动同步
public final static int ACTION_START_SYNC = 0;
- // 同步操作类型:取消同步
+
public final static int ACTION_CANCEL_SYNC = 1;
- // 同步操作类型:无效操作(传递错误指令时用)
+
public final static int ACTION_INVALID = 2;
- // 同步状态广播的名称:前台界面通过这个名称接收同步状态通知
public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service";
- // 广播中存储“是否正在同步”的键名
+
public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing";
- // 广播中存储“同步进度提示文字”的键名
+
public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg";
- // 静态变量:当前正在执行的同步异步任务(整个APP共享,确保只有一个同步任务在运行)
private static GTaskASyncTask mSyncTask = null;
- // 静态变量:当前同步进度提示文字(比如“正在初始化列表”“正在同步”)
+
private static String mSyncProgress = "";
- /**
- * 启动同步任务
- * 通俗说:创建并执行同步异步任务,确保同一时间只有一个同步任务在运行
- */
private void startSync() {
- // 如果当前没有正在执行的同步任务,才创建新任务(避免重复同步)
if (mSyncTask == null) {
- // 创建GTask同步异步任务,传入服务上下文和任务完成监听器
mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() {
- /**
- * 同步任务完成后的回调方法
- * 通俗说:同步不管成功、失败还是被取消,都会执行这个方法
- */
public void onComplete() {
- mSyncTask = null; // 同步完成,清空当前任务引用
- sendBroadcast(""); // 发送空进度广播,通知界面同步已结束
- stopSelf(); // 停止当前服务(同步完成,不需要后台服务了)
+ mSyncTask = null;
+ sendBroadcast("");
+ stopSelf();
}
});
- sendBroadcast(""); // 发送广播,通知界面同步已开始
- mSyncTask.execute(); // 执行同步异步任务(在后台开始同步)
+ sendBroadcast("");
+ mSyncTask.execute();
}
}
- /**
- * 取消同步任务
- * 通俗说:如果有正在执行的同步任务,就停止它
- */
private void cancelSync() {
- // 如果当前有正在执行的同步任务
if (mSyncTask != null) {
- // 调用异步任务的取消方法,终止同步
mSyncTask.cancelSync();
}
}
- /**
- * 服务创建时的初始化方法
- * 通俗说:这个服务第一次被创建时,会执行这里的代码,初始化同步任务为null
- */
@Override
public void onCreate() {
- mSyncTask = null; // 初始化当前同步任务为null(还没有同步任务执行)
+ mSyncTask = null;
}
- /**
- * 服务启动时的处理逻辑
- * 通俗说:外部通过意图启动服务时,会执行这里的代码,根据意图中的指令执行“启动”或“取消”同步
- * @param intent 外部传递的意图(包含同步操作类型)
- * @param flags 服务启动标记(系统使用)
- * @param startId 服务启动ID(系统使用)
- * @return 服务启动模式(START_STICKY=服务被意外杀死后,系统会尝试重启)
- */
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
- // 获取意图中的额外参数(存储了同步操作类型)
Bundle bundle = intent.getExtras();
- // 如果参数不为空,且包含“同步操作类型”的键
if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) {
- // 根据同步操作类型执行对应逻辑
switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) {
case ACTION_START_SYNC:
- // 收到“启动同步”指令,执行启动同步方法
startSync();
break;
case ACTION_CANCEL_SYNC:
- // 收到“取消同步”指令,执行取消同步方法
cancelSync();
break;
default:
- // 收到无效指令,不做处理
break;
}
- // 返回START_STICKY:服务被意外杀死(比如内存不足),系统会尝试重启服务
return START_STICKY;
}
- // 如果没有有效参数,执行父类默认逻辑
return super.onStartCommand(intent, flags, startId);
}
- /**
- * 手机内存不足时的处理方法
- * 通俗说:当手机内存不够用的时候,系统会调用这个方法,这里选择取消同步来释放内存
- */
@Override
public void onLowMemory() {
- // 如果有正在执行的同步任务,就取消它,释放内存
if (mSyncTask != null) {
mSyncTask.cancelSync();
}
}
- /**
- * 绑定服务的方法
- * 通俗说:这个服务不需要被前台界面绑定,所以直接返回null
- * @param intent 绑定服务的意图
- * @return null(表示不支持绑定)
- */
public IBinder onBind(Intent intent) {
return null;
}
- /**
- * 发送同步状态广播
- * 通俗说:把当前的同步状态(是否在同步)和进度提示,通过广播发送给前台界面,让界面更新显示
- * @param msg 同步进度提示文字(比如“正在登录”“正在同步”)
- */
public void sendBroadcast(String msg) {
- mSyncProgress = msg; // 更新当前同步进度文字
- // 创建广播意图,指定广播名称(前台界面通过这个名称接收)
+ mSyncProgress = msg;
Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME);
- // 存入“是否正在同步”的状态(有同步任务就是正在同步)
intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null);
- // 存入同步进度提示文字
intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg);
- // 发送广播,通知所有监听该广播的界面
sendBroadcast(intent);
}
- /**
- * 静态方法:外部启动GTask同步的快捷方法
- * 通俗说:前台界面可以直接调用这个方法,快速启动GTask同步服务
- * @param activity 关联的页面(用来获取谷歌账号登录凭证)
- */
public static void startSync(Activity activity) {
- // 给GTask管理器设置关联页面(用于登录)
GTaskManager.getInstance().setActivityContext(activity);
- // 创建启动服务的意图,指定服务类
Intent intent = new Intent(activity, GTaskSyncService.class);
- // 存入“启动同步”的操作类型
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC);
- // 启动后台服务(开始同步)
activity.startService(intent);
}
- /**
- * 静态方法:外部取消GTask同步的快捷方法
- * 通俗说:前台界面可以直接调用这个方法,快速取消正在执行的同步
- * @param context 上下文(用来启动服务)
- */
public static void cancelSync(Context context) {
- // 创建启动服务的意图,指定服务类
Intent intent = new Intent(context, GTaskSyncService.class);
- // 存入“取消同步”的操作类型
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC);
- // 启动后台服务(执行取消同步逻辑)
context.startService(intent);
}
- /**
- * 静态方法:判断当前是否正在同步
- * 通俗说:前台界面可以调用这个方法,获取同步状态(用来更新界面按钮,比如同步中就禁用启动按钮)
- * @return true=正在同步,false=未同步
- */
public static boolean isSyncing() {
- return mSyncTask != null; // 有同步任务就是正在同步
+ return mSyncTask != null;
}
- /**
- * 静态方法:获取当前同步进度提示文字
- * 通俗说:前台界面可以调用这个方法,获取同步进度(用来显示在界面上,比如“正在初始化列表”)
- * @return 同步进度提示文字
- */
public static String getProgressString() {
return mSyncProgress;
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/model/Note.java b/src/Notes-master/src/net/micode/notes/model/Note.java
index 8c569c8..5596030 100644
--- a/src/Notes-master/src/net/micode/notes/model/Note.java
+++ b/src/Notes-master/src/net/micode/notes/model/Note.java
@@ -15,239 +15,152 @@
*/
package net.micode.notes.model;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.util.Log;
+
+import net.micode.notes.data.Notes;
+import net.micode.notes.data.Notes.CallNote;
+import net.micode.notes.data.Notes.DataColumns;
+import net.micode.notes.data.Notes.NoteColumns;
+import net.micode.notes.data.Notes.TextNote;
+import net.micode.notes.tool.UserManager;
+
+import java.util.ArrayList;
+
-import android.content.ContentProviderOperation; // 数据库批量操作指令:用来批量执行数据库更新操作
-import android.content.ContentProviderResult; // 数据库批量操作结果:存储批量操作的执行结果
-import android.content.ContentUris; // URI拼接工具:用来拼接便签/数据的数据库访问地址
-import android.content.ContentValues; // 数据键值对容器:用来存储要写入数据库的字段和对应值
-import android.content.Context; // 上下文:用来获取本地数据库操作工具
-import android.content.OperationApplicationException; // 批量操作异常:批量执行数据库操作失败时抛出
-import android.net.Uri; // 数据库访问地址:标识数据库中的某类数据(比如便签表、便签数据表)
-import android.os.RemoteException; // 远程操作异常:跨进程访问数据库失败时抛出
-import android.util.Log; // 日志工具:打印错误信息,方便排查问题
-
-import net.micode.notes.data.Notes; // 便签常量类:存储便签类型、数据库地址等固定值
-import net.micode.notes.data.Notes.CallNote; // 通话便签常量:存储通话便签的MIME类型等
-import net.micode.notes.data.Notes.DataColumns; // 便签数据列:便签数据表的字段名(比如所属便签ID、内容)
-import net.micode.notes.data.Notes.NoteColumns; // 便签主列:便签主表的字段名(比如创建时间、修改时间)
-import net.micode.notes.data.Notes.TextNote; // 文本便签常量:存储文本便签的MIME类型等
-
-import java.util.ArrayList; // 数组列表:用来存储批量操作指令
-
-/**
- * 本地便签数据模型类
- * 通俗说:这个类是本地便签的“数据管家”,负责创建新便签ID、存储便签的主信息(创建时间、所属文件夹等)
- * 和具体数据(文本内容、通话记录内容),还能把便签的修改同步到本地数据库
- */
public class Note {
- // 便签主信息修改容器:存储便签主表的修改字段(比如修改时间、本地修改标记)
private ContentValues mNoteDiffValues;
- // 便签具体数据管理对象:负责存储和管理便签的文本/通话记录数据
private NoteData mNoteData;
- // 日志标签:打印日志时标识是这个类的日志,方便查找问题
private static final String TAG = "Note";
-
/**
- * 生成新便签ID(用于给数据库添加新便签)
- * 通俗说:创建新便签时,先在数据库里生成一个唯一的便签ID,后续用这个ID操作该便签
- * @param context 上下文(获取数据库操作工具用)
- * @param folderId 便签所属文件夹ID(比如根文件夹、自定义文件夹)
- * @return 新便签的唯一ID(返回0表示生成失败)
+ * Create a new note id for adding a new note to databases
*/
public static synchronized long getNewNoteId(Context context, long folderId) {
- // 1. 准备新便签的默认数据(存储到便签主表)
+ // Create a new note in the database
ContentValues values = new ContentValues();
- long createdTime = System.currentTimeMillis(); // 获取当前时间作为创建时间
- values.put(NoteColumns.CREATED_DATE, createdTime); // 存入创建时间
- values.put(NoteColumns.MODIFIED_DATE, createdTime); // 存入修改时间(初始和创建时间一致)
- values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 存入便签类型(普通文本便签)
- values.put(NoteColumns.LOCAL_MODIFIED, 1); // 存入本地修改标记(1=已修改,需要同步)
- values.put(NoteColumns.PARENT_ID, folderId); // 存入所属文件夹ID
-
- // 2. 插入新便签到数据库,获取数据库返回的访问地址(包含新便签ID)
+ long createdTime = System.currentTimeMillis();
+ values.put(NoteColumns.CREATED_DATE, createdTime);
+ values.put(NoteColumns.MODIFIED_DATE, createdTime);
+ values.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
+ values.put(NoteColumns.LOCAL_MODIFIED, 1);
+ values.put(NoteColumns.PARENT_ID, folderId);
+ // 设置当前用户ID
+ long currentUserId = UserManager.getInstance(context).getCurrentUserId();
+ values.put(NoteColumns.USER_ID, currentUserId);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
- // 3. 从访问地址中解析出新便签ID
long noteId = 0;
- try {
- // 数据库返回的Uri格式是:content://xxx/notes/[noteId],取第2个分段就是便签ID
- noteId = Long.valueOf(uri.getPathSegments().get(1));
- } catch (NumberFormatException e) {
- // 解析ID失败,打印错误日志
- Log.e(TAG, "Get note id error :" + e.toString());
- noteId = 0;
+ if (uri != null) {
+ try {
+ noteId = Long.valueOf(uri.getPathSegments().get(1));
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Get note id error :" + e.toString());
+ noteId = 0;
+ } catch (IndexOutOfBoundsException e) {
+ Log.e(TAG, "Get note id error :" + e.toString());
+ noteId = 0;
+ }
}
-
- // 4. 校验便签ID是否有效(-1表示插入失败)
if (noteId == -1) {
throw new IllegalStateException("Wrong note id:" + noteId);
}
return noteId;
}
- /**
- * 构造方法:初始化便签对象
- * 通俗说:创建Note对象时,自动初始化便签主信息容器和具体数据管理对象
- */
public Note() {
- mNoteDiffValues = new ContentValues(); // 初始化便签主信息修改容器
- mNoteData = new NoteData(); // 初始化便签具体数据管理对象
+ mNoteDiffValues = new ContentValues();
+ mNoteData = new NoteData();
}
- /**
- * 设置便签主信息字段
- * 通俗说:修改便签的主信息(比如标题、所属文件夹),同时标记为“已修改”并更新修改时间
- * @param key 便签主表的字段名(比如NoteColumns.TITLE)
- * @param value 字段对应的值(比如便签标题文字)
- */
public void setNoteValue(String key, String value) {
- mNoteDiffValues.put(key, value); // 存入要修改的字段和值
- mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改(需要同步到GTask)
- mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); // 更新修改时间为当前时间
+ mNoteDiffValues.put(key, value);
+ mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
+ mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
- /**
- * 设置便签文本数据字段
- * 通俗说:修改便签的文本内容相关字段(比如正文文字)
- * @param key 便签数据表的文本字段名(比如TextNote.CONTENT)
- * @param value 文本字段对应的值(比如便签正文)
- */
public void setTextData(String key, String value) {
- mNoteData.setTextData(key, value); // 委托给NoteData对象处理
+ mNoteData.setTextData(key, value);
}
- /**
- * 设置文本数据的ID
- * 通俗说:给便签的文本数据设置唯一ID(对应数据库里的文本数据记录ID)
- * @param id 文本数据ID
- */
public void setTextDataId(long id) {
- mNoteData.setTextDataId(id); // 委托给NoteData对象处理
+ mNoteData.setTextDataId(id);
}
- /**
- * 获取文本数据的ID
- * 通俗说:获取便签文本数据对应的数据库记录ID
- * @return 文本数据ID
- */
public long getTextDataId() {
- return mNoteData.mTextDataId; // 从NoteData对象中获取
+ return mNoteData.mTextDataId;
}
- /**
- * 设置通话数据的ID
- * 通俗说:给便签的通话记录数据设置唯一ID(对应数据库里的通话数据记录ID)
- * @param id 通话数据ID
- */
public void setCallDataId(long id) {
- mNoteData.setCallDataId(id); // 委托给NoteData对象处理
+ mNoteData.setCallDataId(id);
}
- /**
- * 设置便签通话数据字段
- * 通俗说:修改便签的通话记录相关字段(比如通话号码、通话时间)
- * @param key 便签数据表的通话字段名(比如CallNote.NUMBER)
- * @param value 通话字段对应的值(比如10086)
- */
public void setCallData(String key, String value) {
- mNoteData.setCallData(key, value); // 委托给NoteData对象处理
+ mNoteData.setCallData(key, value);
}
- /**
- * 判断便签是否有本地修改
- * 通俗说:检查便签的主信息或具体数据是否有修改,用来判断是否需要同步到数据库/GTask
- * @return true=有修改,false=无修改
- */
public boolean isLocalModified() {
- // 主信息容器有数据 或 具体数据有修改,就表示便签有本地修改
return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified();
}
- /**
- * 同步便签修改到本地数据库
- * 通俗说:把便签的主信息修改和具体数据修改,写入本地数据库,完成本地同步
- * @param context 上下文(获取数据库操作工具用)
- * @param noteId 要同步的便签ID
- * @return true=同步成功,false=同步失败
- */
public boolean syncNote(Context context, long noteId) {
- // 1. 校验便签ID是否有效(必须大于0)
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
- // 2. 如果没有本地修改,直接返回成功(无需同步)
if (!isLocalModified()) {
return true;
}
/**
- * 理论上:只要便签数据有修改,就必须更新“本地修改标记”和“修改时间”
- * 为了数据安全:即使便签主信息更新失败,也要尝试更新便签具体数据
+ * In theory, once data changed, the note should be updated on {@link NoteColumns#LOCAL_MODIFIED} and
+ * {@link NoteColumns#MODIFIED_DATE}. For data safety, though update note fails, we also update the
+ * note data info
*/
- // 3. 更新便签主信息到数据库
if (context.getContentResolver().update(
- ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), // 拼接该便签的数据库访问地址
- mNoteDiffValues, // 要更新的主信息数据
- null, null) == 0) {
- // 更新返回0表示失败,打印错误日志(理论上不该发生)
+ ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null,
+ null) == 0) {
Log.e(TAG, "Update note error, should not happen");
- // 不返回,继续尝试更新具体数据
+ // Do not return, fall through
}
- mNoteDiffValues.clear(); // 主信息更新完成(无论成败),清空容器
+ mNoteDiffValues.clear();
- // 4. 更新便签具体数据到数据库
if (mNoteData.isLocalModified()
&& (mNoteData.pushIntoContentResolver(context, noteId) == null)) {
- // 具体数据有修改 且 更新失败,返回同步失败
return false;
}
- // 5. 所有修改同步完成,返回成功
return true;
}
- /**
- * 内部类:便签具体数据管理类
- * 通俗说:专门负责管理便签的文本数据和通话记录数据,处理这两类数据的存储和数据库同步
- */
private class NoteData {
- // 文本数据ID:对应数据库里文本便签数据的唯一ID
private long mTextDataId;
- // 文本数据修改容器:存储文本数据的修改字段和值
+
private ContentValues mTextDataValues;
- // 通话数据ID:对应数据库里通话便签数据的唯一ID
+
private long mCallDataId;
- // 通话数据修改容器:存储通话数据的修改字段和值
+
private ContentValues mCallDataValues;
- // 日志标签:打印该内部类的错误日志
+
private static final String TAG = "NoteData";
- /**
- * 构造方法:初始化便签具体数据管理对象
- */
public NoteData() {
- mTextDataValues = new ContentValues(); // 初始化文本数据容器
- mCallDataValues = new ContentValues(); // 初始化通话数据容器
- mTextDataId = 0; // 初始文本数据ID为0(未关联数据库记录)
- mCallDataId = 0; // 初始通话数据ID为0(未关联数据库记录)
+ mTextDataValues = new ContentValues();
+ mCallDataValues = new ContentValues();
+ mTextDataId = 0;
+ mCallDataId = 0;
}
- /**
- * 判断具体数据是否有本地修改
- * 通俗说:检查文本数据或通话数据是否有修改
- * @return true=有修改,false=无修改
- */
boolean isLocalModified() {
- // 文本数据容器有数据 或 通话数据容器有数据,就表示有修改
return mTextDataValues.size() > 0 || mCallDataValues.size() > 0;
}
- /**
- * 设置文本数据ID
- * 通俗说:给文本数据绑定数据库记录ID,ID必须大于0
- * @param id 文本数据ID
- */
void setTextDataId(long id) {
if(id <= 0) {
throw new IllegalArgumentException("Text data id should larger than 0");
@@ -255,11 +168,6 @@ public class Note {
mTextDataId = id;
}
- /**
- * 设置通话数据ID
- * 通俗说:给通话数据绑定数据库记录ID,ID必须大于0
- * @param id 通话数据ID
- */
void setCallDataId(long id) {
if (id <= 0) {
throw new IllegalArgumentException("Call data id should larger than 0");
@@ -267,122 +175,88 @@ public class Note {
mCallDataId = id;
}
- /**
- * 设置通话数据字段
- * 通俗说:存储通话数据的修改字段和值,同时标记便签主信息为“已修改”
- * @param key 通话数据字段名(比如CallNote.DATE)
- * @param value 通话数据字段值(比如通话时间戳)
- */
void setCallData(String key, String value) {
- mCallDataValues.put(key, value); // 存入通话数据修改
- // 同步更新便签主信息的“本地修改标记”和“修改时间”
+ mCallDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
- /**
- * 设置文本数据字段
- * 通俗说:存储文本数据的修改字段和值,同时标记便签主信息为“已修改”
- * @param key 文本数据字段名(比如TextNote.CONTENT)
- * @param value 文本数据字段值(比如便签正文)
- */
void setTextData(String key, String value) {
- mTextDataValues.put(key, value); // 存入文本数据修改
- // 同步更新便签主信息的“本地修改标记”和“修改时间”
+ mTextDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
- /**
- * 将具体数据同步到本地数据库
- * 通俗说:把文本数据和通话数据的修改,写入本地数据库(新增或更新数据记录)
- * @param context 上下文(获取数据库操作工具用)
- * @param noteId 所属便签ID
- * @return 同步后的便签URI(null表示同步失败)
- */
Uri pushIntoContentResolver(Context context, long noteId) {
- // 1. 校验便签ID是否有效
+ /**
+ * Check for safety
+ */
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
- // 2. 准备数据库批量操作指令列表(批量执行更新,效率更高)
ArrayList operationList = new ArrayList();
- ContentProviderOperation.Builder builder = null; // 批量操作指令构建器
+ ContentProviderOperation.Builder builder = null;
- // 3. 处理文本数据同步
if(mTextDataValues.size() > 0) {
- mTextDataValues.put(DataColumns.NOTE_ID, noteId); // 绑定所属便签ID
+ mTextDataValues.put(DataColumns.NOTE_ID, noteId);
if (mTextDataId == 0) {
- // 文本数据ID为0:表示是新增文本数据,插入到数据库
- mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); // 标记为文本数据类型
- Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mTextDataValues);
+ mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
+ Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
+ mTextDataValues);
try {
- // 解析插入后返回的ID,绑定到文本数据
setTextDataId(Long.valueOf(uri.getPathSegments().get(1)));
} catch (NumberFormatException e) {
- // 插入失败,打印日志并清空数据容器
Log.e(TAG, "Insert new text data fail with noteId" + noteId);
mTextDataValues.clear();
return null;
}
} else {
- // 文本数据ID不为0:表示是更新已有文本数据,添加批量更新指令
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
- Notes.CONTENT_DATA_URI, mTextDataId)); // 拼接文本数据的数据库地址
- builder.withValues(mTextDataValues); // 设置要更新的数据
- operationList.add(builder.build()); // 添加到批量操作列表
+ Notes.CONTENT_DATA_URI, mTextDataId));
+ builder.withValues(mTextDataValues);
+ operationList.add(builder.build());
}
- mTextDataValues.clear(); // 文本数据处理完成,清空容器
+ mTextDataValues.clear();
}
- // 4. 处理通话数据同步(逻辑和文本数据一致)
if(mCallDataValues.size() > 0) {
- mCallDataValues.put(DataColumns.NOTE_ID, noteId); // 绑定所属便签ID
+ mCallDataValues.put(DataColumns.NOTE_ID, noteId);
if (mCallDataId == 0) {
- // 通话数据ID为0:新增通话数据,插入到数据库
- mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE); // 标记为通话数据类型
- Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mCallDataValues);
+ mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE);
+ Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
+ mCallDataValues);
try {
- // 解析插入后返回的ID,绑定到通话数据
setCallDataId(Long.valueOf(uri.getPathSegments().get(1)));
} catch (NumberFormatException e) {
- // 插入失败,打印日志并清空数据容器
Log.e(TAG, "Insert new call data fail with noteId" + noteId);
mCallDataValues.clear();
return null;
}
} else {
- // 通话数据ID不为0:更新已有通话数据,添加批量更新指令
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
- Notes.CONTENT_DATA_URI, mCallDataId)); // 拼接通话数据的数据库地址
- builder.withValues(mCallDataValues); // 设置要更新的数据
- operationList.add(builder.build()); // 添加到批量操作列表
+ Notes.CONTENT_DATA_URI, mCallDataId));
+ builder.withValues(mCallDataValues);
+ operationList.add(builder.build());
}
- mCallDataValues.clear(); // 通话数据处理完成,清空容器
+ mCallDataValues.clear();
}
- // 5. 执行批量更新操作(如果有更新指令)
if (operationList.size() > 0) {
try {
- // 批量执行数据库操作
ContentProviderResult[] results = context.getContentResolver().applyBatch(
Notes.AUTHORITY, operationList);
- // 判断操作结果是否有效,返回便签的数据库地址
return (results == null || results.length == 0 || results[0] == null) ? null
: ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
} catch (RemoteException e) {
- // 跨进程访问失败,打印日志并返回null
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
} catch (OperationApplicationException e) {
- // 批量操作执行失败,打印日志并返回null
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
}
}
- // 没有批量更新指令,返回null(表示无需更新或更新完成)
return null;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/model/WorkingNote.java b/src/Notes-master/src/net/micode/notes/model/WorkingNote.java
index e3fdd05..3762f45 100644
--- a/src/Notes-master/src/net/micode/notes/model/WorkingNote.java
+++ b/src/Notes-master/src/net/micode/notes/model/WorkingNote.java
@@ -16,455 +16,402 @@
package net.micode.notes.model;
-import android.appwidget.AppWidgetManager; // 桌面小组件管理类:用于判断小组件ID是否有效
-import android.content.ContentUris; // URI拼接工具:拼接便签的数据库访问地址
-import android.content.Context; // 上下文:获取数据库操作工具和资源
-import android.database.Cursor; // 数据库查询结果游标:存储数据库查询返回的结果集
-import android.text.TextUtils; // 文本工具类:判断字符串是否为空/空白
-import android.util.Log; // 日志工具:打印错误/调试信息
-
-import net.micode.notes.data.Notes; // 便签常量类:存储便签类型、文件夹ID等固定值
-import net.micode.notes.data.Notes.CallNote; // 通话便签常量:存储通话便签的字段名
-import net.micode.notes.data.Notes.DataColumns; // 便签数据表字段:数据表的列名
-import net.micode.notes.data.Notes.DataConstants; // 便签数据类型常量:区分文本/通话便签
-import net.micode.notes.data.Notes.NoteColumns; // 便签主表字段:主表的列名
-import net.micode.notes.data.Notes.TextNote; // 文本便签常量:存储文本便签的字段名
-import net.micode.notes.tool.ResourceParser.NoteBgResources; // 便签背景资源工具:根据颜色ID获取背景资源
-
-/**
- * 工作便签封装类
- * 通俗说:这个类是便签的“前台操作管家”,封装了便签的所有属性(内容、提醒时间、背景色等)
- * 还负责加载已有便签、创建新便签、保存便签修改,以及通知界面便签设置的变化(比如背景色改变)
- */
+import android.appwidget.AppWidgetManager;
+import android.content.ContentUris;
+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.CallNote;
+import net.micode.notes.data.Notes.DataColumns;
+import net.micode.notes.data.Notes.DataConstants;
+import net.micode.notes.data.Notes.NoteColumns;
+import net.micode.notes.data.Notes.TextNote;
+import net.micode.notes.tool.ResourceParser.NoteBgResources;
+
+
public class WorkingNote {
- // 关联的Note数据对象:负责和本地数据库交互(存储/同步便签数据)
+ // Note for the working note
private Note mNote;
- // 便签唯一ID:对应数据库里的便签ID(0表示新便签,未存入数据库)
+ // Note Id
private long mNoteId;
- // 便签内容:文本便签的正文文字
+ // Note content
private String mContent;
- // 便签模式:区分普通文本模式和清单模式(比如0=普通模式,1=清单模式)
+ // Note title
+ private String mTitle;
+ // Note mode
private int mMode;
- // 提醒时间戳:便签的提醒日期(0表示无提醒)
+
private long mAlertDate;
- // 修改时间戳:便签最后一次修改的时间
+
private long mModifiedDate;
- // 背景颜色ID:便签的背景色标识(对应不同的背景样式)
+
private int mBgColorId;
- // 桌面小组件ID:关联的桌面便签小组件ID(无效时为INVALID_APPWIDGET_ID)
+
private int mWidgetId;
- // 桌面小组件类型:关联的桌面小组件类型(无效时为TYPE_WIDGET_INVALIDE)
+
private int mWidgetType;
- // 所属文件夹ID:便签所在的文件夹ID(比如通话记录文件夹、自定义文件夹)
+
private long mFolderId;
- // 上下文:用于获取数据库工具、资源等
+
private Context mContext;
- // 日志标签:打印该类的日志,方便排查问题
+
private static final String TAG = "WorkingNote";
- // 删除标记:是否标记为已删除(true=已删除,无需保存)
+
private boolean mIsDeleted;
- // 便签设置变化监听器:用于通知界面便签设置的改变(比如背景色、提醒时间变化)
+
private NoteSettingChangedListener mNoteSettingStatusListener;
- // 便签数据表查询投影:查询便签数据时,要获取的字段列表(相当于查询结果的列清单)
public static final String[] DATA_PROJECTION = new String[] {
- DataColumns.ID, // 0: 数据记录ID
- DataColumns.CONTENT, // 1: 数据内容(文本便签的正文)
- DataColumns.MIME_TYPE, // 2: 数据类型(文本/通话便签)
- DataColumns.DATA1, // 3: 扩展字段1(存储便签模式)
- DataColumns.DATA2, // 4: 扩展字段2
- DataColumns.DATA3, // 5: 扩展字段3
- DataColumns.DATA4, // 6: 扩展字段4
+ DataColumns.ID,
+ DataColumns.CONTENT,
+ DataColumns.MIME_TYPE,
+ DataColumns.DATA1,
+ DataColumns.DATA2,
+ DataColumns.DATA3,
+ DataColumns.DATA4,
};
- // 便签主表查询投影:查询便签主信息时,要获取的字段列表
public static final String[] NOTE_PROJECTION = new String[] {
- NoteColumns.PARENT_ID, // 0: 所属文件夹ID
- NoteColumns.ALERTED_DATE,// 1: 提醒时间
- NoteColumns.BG_COLOR_ID, // 2: 背景颜色ID
- NoteColumns.WIDGET_ID, // 3: 桌面小组件ID
- NoteColumns.WIDGET_TYPE, // 4: 桌面小组件类型
- NoteColumns.MODIFIED_DATE// 5: 修改时间
+ NoteColumns.PARENT_ID,
+ NoteColumns.ALERTED_DATE,
+ NoteColumns.BG_COLOR_ID,
+ NoteColumns.WIDGET_ID,
+ NoteColumns.WIDGET_TYPE,
+ NoteColumns.MODIFIED_DATE,
+ NoteColumns.TITLE
};
- // 数据表查询结果的列索引:对应DATA_PROJECTION的字段位置,方便快速取值
- private static final int DATA_ID_COLUMN = 0; // 数据记录ID的索引
- private static final int DATA_CONTENT_COLUMN = 1; // 数据内容的索引
- private static final int DATA_MIME_TYPE_COLUMN = 2; // 数据类型的索引
- private static final int DATA_MODE_COLUMN = 3; // 便签模式的索引
-
- // 主表查询结果的列索引:对应NOTE_PROJECTION的字段位置,方便快速取值
- private static final int NOTE_PARENT_ID_COLUMN = 0; // 所属文件夹ID的索引
- private static final int NOTE_ALERTED_DATE_COLUMN = 1; // 提醒时间的索引
- private static final int NOTE_BG_COLOR_ID_COLUMN = 2; // 背景颜色ID的索引
- private static final int NOTE_WIDGET_ID_COLUMN = 3; // 桌面小组件ID的索引
- private static final int NOTE_WIDGET_TYPE_COLUMN = 4; // 桌面小组件类型的索引
- private static final int NOTE_MODIFIED_DATE_COLUMN = 5; // 修改时间的索引
-
- // 私有构造方法:创建新便签(未存入数据库)
- // 通俗说:初始化一个空白便签,设置默认属性
+ private static final int DATA_ID_COLUMN = 0;
+
+ private static final int DATA_CONTENT_COLUMN = 1;
+
+ private static final int DATA_MIME_TYPE_COLUMN = 2;
+
+ private static final int DATA_MODE_COLUMN = 3;
+
+ private static final int NOTE_PARENT_ID_COLUMN = 0;
+
+ private static final int NOTE_ALERTED_DATE_COLUMN = 1;
+
+ private static final int NOTE_BG_COLOR_ID_COLUMN = 2;
+
+ private static final int NOTE_WIDGET_ID_COLUMN = 3;
+
+ private static final int NOTE_WIDGET_TYPE_COLUMN = 4;
+
+ private static final int NOTE_MODIFIED_DATE_COLUMN = 5;
+
+ private static final int NOTE_TITLE_COLUMN = 6;
+
+ // New note construct
private WorkingNote(Context context, long folderId) {
mContext = context;
- mAlertDate = 0; // 默认无提醒
- mModifiedDate = System.currentTimeMillis(); // 默认修改时间为当前时间
- mFolderId = folderId; // 设置所属文件夹ID
- mNote = new Note(); // 创建关联的Note数据对象
- mNoteId = 0; // 新便签ID为0(未存入数据库)
- mIsDeleted = false; // 默认未删除
- mMode = 0; // 默认普通文本模式
- mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 默认无有效桌面小组件
- }
-
- // 私有构造方法:加载已有便签(从数据库读取)
- // 通俗说:根据便签ID,从数据库加载已有便签的所有信息
+ mAlertDate = 0;
+ mModifiedDate = System.currentTimeMillis();
+ mFolderId = folderId;
+ mNote = new Note();
+ mNoteId = 0;
+ mIsDeleted = false;
+ mMode = 0;
+ mWidgetType = Notes.TYPE_WIDGET_INVALIDE;
+ }
+
+ // Existing note construct
private WorkingNote(Context context, long noteId, long folderId) {
mContext = context;
- mNoteId = noteId; // 设置已有便签的ID
- mFolderId = folderId; // 设置所属文件夹ID
- mIsDeleted = false; // 默认未删除
- mNote = new Note(); // 创建关联的Note数据对象
- loadNote(); // 加载便签主信息
+ mNoteId = noteId;
+ mFolderId = folderId;
+ mIsDeleted = false;
+ mNote = new Note();
+ loadNote();
}
- /**
- * 加载便签主信息(从数据库主表读取)
- * 通俗说:根据便签ID,从数据库便签主表中读取文件夹ID、背景色、提醒时间等主信息
- */
private void loadNote() {
- // 拼接该便签的数据库访问地址,查询主表信息
+ // 执行查询,不添加用户过滤条件,因为NotesProvider已经处理了公开便签的访问权限
Cursor cursor = mContext.getContentResolver().query(
- ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId),
- NOTE_PROJECTION, // 要查询的字段列表
- null, null, null);
+ ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null,
+ null, null);
if (cursor != null) {
- // 如果查询到结果,移动到第一条记录
if (cursor.moveToFirst()) {
- // 从查询结果中读取各字段值,赋值给成员变量
mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN);
mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN);
mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN);
mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);
mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);
+ mTitle = cursor.getString(NOTE_TITLE_COLUMN);
}
- cursor.close(); // 关闭游标,释放资源
+ cursor.close();
} else {
- // 未查询到便签,打印错误日志并抛出异常
Log.e(TAG, "No note with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note with id " + mNoteId);
}
- loadNoteData(); // 主信息加载完成后,加载便签具体数据
+ loadNoteData();
}
- /**
- * 加载便签具体数据(从数据库数据表读取)
- * 通俗说:根据便签ID,从数据库便签数据表中读取文本内容、便签模式等具体数据
- */
private void loadNoteData() {
- // 查询该便签对应的所有数据记录(条件:NOTE_ID等于当前便签ID)
- Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI,
- DATA_PROJECTION,
- DataColumns.NOTE_ID + "=?", // 查询条件
- new String[] { String.valueOf(mNoteId) }, // 条件参数
- null);
-
+ Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION,
+ DataColumns.NOTE_ID + "=?", new String[] {
+ String.valueOf(mNoteId)
+ }, null);
+
+ // 初始化默认值
+ mContent = "";
+ mMode = 0;
+
if (cursor != null) {
- // 如果查询到结果,移动到第一条记录
if (cursor.moveToFirst()) {
- // 循环遍历所有数据记录(一个便签可能对应多条数据,比如文本+通话记录)
do {
- String type = cursor.getString(DATA_MIME_TYPE_COLUMN); // 获取数据类型
+ String type = cursor.getString(DATA_MIME_TYPE_COLUMN);
if (DataConstants.NOTE.equals(type)) {
- // 文本便签:读取内容、模式,并绑定数据ID
mContent = cursor.getString(DATA_CONTENT_COLUMN);
mMode = cursor.getInt(DATA_MODE_COLUMN);
mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN));
} else if (DataConstants.CALL_NOTE.equals(type)) {
- // 通话便签:绑定通话数据ID
mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN));
} else {
- // 未知数据类型,打印调试日志
Log.d(TAG, "Wrong note type with type:" + type);
}
- } while (cursor.moveToNext()); // 移动到下一条记录,继续遍历
+ } while (cursor.moveToNext());
}
- cursor.close(); // 关闭游标,释放资源
+ cursor.close();
} else {
- // 未查询到便签数据,打印错误日志并抛出异常
Log.e(TAG, "No data with id:" + mNoteId);
- throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId);
}
}
- /**
- * 静态方法:创建空白便签(带默认配置)
- * 通俗说:外部调用该方法,创建一个指定文件夹、小组件配置和默认背景色的空白便签
- * @param context 上下文
- * @param folderId 所属文件夹ID
- * @param widgetId 桌面小组件ID
- * @param widgetType 桌面小组件类型
- * @param defaultBgColorId 默认背景颜色ID
- * @return 初始化完成的空白WorkingNote对象
- */
public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId,
int widgetType, int defaultBgColorId) {
- WorkingNote note = new WorkingNote(context, folderId); // 创建空白便签
- note.setBgColorId(defaultBgColorId); // 设置默认背景色
- note.setWidgetId(widgetId); // 设置桌面小组件ID
- note.setWidgetType(widgetType); // 设置桌面小组件类型
+ WorkingNote note = new WorkingNote(context, folderId);
+ note.setBgColorId(defaultBgColorId);
+ note.setWidgetId(widgetId);
+ note.setWidgetType(widgetType);
return note;
}
- /**
- * 静态方法:加载已有便签
- * 通俗说:外部调用该方法,根据便签ID加载数据库中的已有便签
- * @param context 上下文
- * @param id 便签ID
- * @return 加载完成的WorkingNote对象
- */
public static WorkingNote load(Context context, long id) {
return new WorkingNote(context, id, 0);
}
- /**
- * 保存便签到数据库
- * 通俗说:判断便签是否值得保存,若值得则新建/更新数据库记录,并通知小组件更新
- * @return true=保存成功,false=保存失败/无需保存
- */
public synchronized boolean saveNote() {
- // 先判断是否值得保存(已删除、空白新便签、无修改的旧便签都无需保存)
- if (isWorthSaving()) {
- // 如果是新便签(未存入数据库),先生成新便签ID
- if (!existInDatabase()) {
- mNoteId = Note.getNewNoteId(mContext, mFolderId);
- if (mNoteId == 0) {
- // 生成新ID失败,打印错误日志并返回失败
- Log.e(TAG, "Create new note fail with id:" + mNoteId);
- return false;
+ try {
+ if (isWorthSaving()) {
+ if (!existInDatabase()) {
+ if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) {
+ Log.e(TAG, "Create new note fail with id:" + mNoteId);
+ return false;
+ }
}
- }
- // 调用Note对象的同步方法,将修改写入数据库
- mNote.syncNote(mContext, mNoteId);
+ mNote.syncNote(mContext, mNoteId);
- /**
- * 如果该便签关联了有效桌面小组件,且设置了监听器,通知小组件更新内容
- */
- if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
- && mWidgetType != Notes.TYPE_WIDGET_INVALIDE
- && mNoteSettingStatusListener != null) {
- mNoteSettingStatusListener.onWidgetChanged();
+ // 自动分类逻辑
+ try {
+ // 优先使用标题分类,如果标题为空则使用内容分类
+ String contentForCategory = mTitle;
+ if (contentForCategory == null || contentForCategory.isEmpty()) {
+ contentForCategory = mContent;
+ }
+ // 根据标题或内容自动分类
+ String category = net.micode.notes.tool.CategoryUtil.autoCategorize(contentForCategory);
+
+ // 创建或获取对应的文件夹
+ long categoryFolderId = net.micode.notes.tool.DataUtils.createFolder(mContext.getContentResolver(), category);
+ if (categoryFolderId > 0 && mFolderId != categoryFolderId) {
+ // 将便签移动到分类文件夹
+ net.micode.notes.tool.DataUtils.moveNoteToFoler(mContext.getContentResolver(), mNoteId, mFolderId, categoryFolderId);
+ mFolderId = categoryFolderId;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Auto categorize fail: " + e.getMessage());
+ // 自动分类失败不影响便签保存
+ }
+
+ /**
+ * Update widget content if there exist any widget of this note
+ */
+ if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
+ && mWidgetType != Notes.TYPE_WIDGET_INVALIDE
+ && mNoteSettingStatusListener != null) {
+ mNoteSettingStatusListener.onWidgetChanged();
+ }
+ return true;
+ } else {
+ return false;
}
- return true; // 保存成功
- } else {
- return false; // 无需保存
+ } catch (Exception e) {
+ Log.e(TAG, "Save note fail: " + e.getMessage());
+ e.printStackTrace();
+ return false;
}
}
- /**
- * 判断便签是否已存入数据库
- * 通俗说:通过便签ID是否大于0,判断是否是已存在的便签
- * @return true=已存入数据库,false=新便签(未存入)
- */
public boolean existInDatabase() {
return mNoteId > 0;
}
- /**
- * 判断便签是否值得保存
- * 通俗说:满足以下条件之一则无需保存:
- * 1. 已标记为删除;2. 新便签且内容为空;3. 旧便签且无任何本地修改
- * @return true=值得保存,false=无需保存
- */
private boolean isWorthSaving() {
- if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent))
- || (existInDatabase() && !mNote.isLocalModified())) {
+ if (mIsDeleted) {
return false;
} else {
+ // 允许保存空便签
return true;
}
}
- /**
- * 设置便签设置变化监听器
- * 通俗说:给当前便签绑定监听器,以便便签设置变化时通知界面更新
- * @param l 监听器对象
- */
public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) {
mNoteSettingStatusListener = l;
}
- /**
- * 设置便签提醒时间
- * 通俗说:修改便签的提醒时间,并通知监听器提醒时间已变化
- * @param date 新的提醒时间戳
- * @param set 是否设置提醒(true=设置,false=取消)
- */
public void setAlertDate(long date, boolean set) {
if (date != mAlertDate) {
- mAlertDate = date; // 更新提醒时间
- // 将提醒时间存入Note对象,准备同步到数据库
+ mAlertDate = date;
mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate));
}
- // 如果设置了监听器,通知提醒时间变化
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onClockAlertChanged(date, set);
}
}
- /**
- * 标记便签为已删除/未删除
- * 通俗说:设置便签的删除标记,若关联了桌面小组件,通知小组件更新
- * @param mark true=标记为已删除,false=取消删除标记
- */
public void markDeleted(boolean mark) {
- mIsDeleted = mark; // 更新删除标记
- // 如果关联了有效桌面小组件且设置了监听器,通知小组件变化
+ mIsDeleted = mark;
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
}
}
- /**
- * 设置便签背景颜色ID
- * 通俗说:修改便签的背景色,通知监听器背景色已变化,并准备同步到数据库
- * @param id 新的背景颜色ID
- */
public void setBgColorId(int id) {
if (id != mBgColorId) {
- mBgColorId = id; // 更新背景颜色ID
- // 如果设置了监听器,通知背景色变化
+ mBgColorId = id;
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onBackgroundColorChanged();
}
- // 将背景颜色ID存入Note对象,准备同步到数据库
mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id));
}
}
- /**
- * 设置便签模式(普通/清单)
- * 通俗说:切换便签的显示模式,通知监听器模式变化,并准备同步到数据库
- * @param mode 新的便签模式(0=普通,1=清单等)
- */
public void setCheckListMode(int mode) {
if (mMode != mode) {
- // 如果设置了监听器,通知模式变化(传入旧模式和新模式)
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode);
}
- mMode = mode; // 更新便签模式
- // 将便签模式存入Note对象,准备同步到数据库
+ mMode = mode;
mNote.setTextData(TextNote.MODE, String.valueOf(mMode));
}
}
- /**
- * 设置桌面小组件类型
- * 通俗说:修改便签关联的桌面小组件类型,并准备同步到数据库
- * @param type 新的小组件类型
- */
public void setWidgetType(int type) {
if (type != mWidgetType) {
- mWidgetType = type; // 更新小组件类型
- // 将小组件类型存入Note对象,准备同步到数据库
+ mWidgetType = type;
mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType));
}
}
- /**
- * 设置桌面小组件ID
- * 通俗说:修改便签关联的桌面小组件ID,并准备同步到数据库
- * @param id 新的小组件ID
- */
public void setWidgetId(int id) {
if (id != mWidgetId) {
- mWidgetId = id; // 更新小组件ID
- // 将小组件ID存入Note对象,准备同步到数据库
+ mWidgetId = id;
mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId));
}
}
- /**
- * 设置便签文本内容
- * 通俗说:修改便签的正文内容,只有内容变化时才更新,并准备同步到数据库
- * @param text 新的便签内容
- */
public void setWorkingText(String text) {
if (!TextUtils.equals(mContent, text)) {
- mContent = text; // 更新便签内容
- // 将新内容存入Note对象,准备同步到数据库
+ mContent = text;
mNote.setTextData(DataColumns.CONTENT, mContent);
}
}
+
+ public void setWorkingTitle(String title) {
+ if (!TextUtils.equals(mTitle, title)) {
+ mTitle = title;
+ mNote.setNoteValue(NoteColumns.TITLE, title);
+ }
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
- /**
- * 转换为通话便签
- * 通俗说:将当前便签设置为通话便签,存入通话号码和通话时间,并指定到通话记录文件夹
- * @param phoneNumber 通话号码
- * @param callDate 通话时间戳
- */
public void convertToCallNote(String phoneNumber, long callDate) {
- // 存入通话时间和号码
mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate));
mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber);
- // 将便签移动到通话记录文件夹
mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER));
}
- /**
- * 判断是否设置了提醒
- * 通俗说:通过提醒时间戳是否大于0,判断便签是否有有效提醒
- * @return true=有提醒,false=无提醒
- */
public boolean hasClockAlert() {
return (mAlertDate > 0 ? true : false);
}
- // 以下都是属性获取方法:返回对应的便签属性值,供外部界面使用
- public String getContent() { return mContent; }
- public long getAlertDate() { return mAlertDate; }
- public long getModifiedDate() { return mModifiedDate; }
- public int getBgColorResId() { return NoteBgResources.getNoteBgResource(mBgColorId); }
- public int getBgColorId() { return mBgColorId; }
- public int getTitleBgResId() { return NoteBgResources.getNoteTitleBgResource(mBgColorId); }
- public int getCheckListMode() { return mMode; }
- public long getNoteId() { return mNoteId; }
- public long getFolderId() { return mFolderId; }
- public int getWidgetId() { return mWidgetId; }
- public int getWidgetType() { return mWidgetType; }
-
- /**
- * 便签设置变化监听器接口
- * 通俗说:定义了便签设置变化时的回调方法,让界面能感知到变化并更新UI
- */
+ public String getContent() {
+ return mContent;
+ }
+
+ public long getAlertDate() {
+ return mAlertDate;
+ }
+
+ public long getModifiedDate() {
+ return mModifiedDate;
+ }
+
+ public int getBgColorResId() {
+ return NoteBgResources.getNoteBgResource(mBgColorId);
+ }
+
+ public int getBgColorId() {
+ return mBgColorId;
+ }
+
+ public int getTitleBgResId() {
+ return NoteBgResources.getNoteTitleBgResource(mBgColorId);
+ }
+
+ public int getCheckListMode() {
+ return mMode;
+ }
+
+ public long getNoteId() {
+ return mNoteId;
+ }
+
+ public long getFolderId() {
+ return mFolderId;
+ }
+
+ public int getWidgetId() {
+ return mWidgetId;
+ }
+
+ public int getWidgetType() {
+ return mWidgetType;
+ }
+
public interface NoteSettingChangedListener {
/**
- * 便签背景色变化时的回调
- * 通俗说:当便签背景色修改后,会调用该方法,界面可在此更新背景样式
+ * Called when the background color of current note has just changed
*/
void onBackgroundColorChanged();
/**
- * 便签提醒时间变化时的回调
- * 通俗说:当设置/取消便签提醒后,会调用该方法,界面可在此更新提醒相关UI
+ * Called when user set clock
*/
void onClockAlertChanged(long date, boolean set);
/**
- * 桌面小组件相关变化时的回调
- * 通俗说:当便签关联的小组件变化(比如创建/删除小组件便签),会调用该方法更新小组件
+ * Call when user create note from widget
*/
void onWidgetChanged();
/**
- * 便签模式切换时的回调
- * 通俗说:当在普通模式和清单模式之间切换时,会调用该方法,界面可在此更新显示样式
- * @param oldMode 切换前的旧模式
- * @param newMode 切换后的新模式
+ * Call when switch between check list mode and normal mode
+ * @param oldMode is previous mode before change
+ * @param newMode is new mode
*/
void onCheckListModeChanged(int oldMode, int newMode);
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/tool/CategoryUtil.java b/src/Notes-master/src/net/micode/notes/tool/CategoryUtil.java
new file mode 100644
index 0000000..54c6bad
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/tool/CategoryUtil.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.micode.notes.tool;
+
+import android.text.TextUtils;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * 便签自动分类工具类
+ */
+public class CategoryUtil {
+ // 分类关键词映射表
+ private static final Map CATEGORY_KEYWORDS = new HashMap<>();
+
+ static {
+ // 工作相关
+ addKeywords("工作", "工作", "任务", "项目", "会议", "报告", "加班", "上班", "下班", "同事", "领导", "客户", "公司");
+
+ // 学习相关
+ addKeywords("学习", "学习", "考试", "作业", "复习", "预习", "课程", "老师", "学生", "学校", "教材", "笔记", "知识点");
+
+ // 生活相关
+ addKeywords("生活", "生活", "日常", "购物", "吃饭", "旅游", "电影", "音乐", "健身", "运动", "休息", "睡觉", "起床");
+
+ // 想法相关
+ addKeywords("想法", "想法", "创意", "灵感", "思考", "感悟", "心得", "体会", "观点", "意见", "建议");
+
+ // 待办相关
+ addKeywords("待办", "待办", "todo", "需要", "必须", "应该", "计划", "安排", "准备");
+
+ // 其他默认分类
+ addKeywords("其他", "");
+ }
+
+ // 批量添加关键词
+ private static void addKeywords(String category, String... keywords) {
+ for (String keyword : keywords) {
+ CATEGORY_KEYWORDS.put(keyword, category);
+ }
+ }
+
+ /**
+ * 根据便签内容自动分类
+ * @param content 便签内容
+ * @return 分类结果,不超过3个字符
+ */
+ public static String autoCategorize(String content) {
+ if (TextUtils.isEmpty(content)) {
+ return "其他";
+ }
+
+ // 转为小写进行匹配
+ String lowerContent = content.toLowerCase();
+
+ // 遍历关键词,匹配分类
+ for (Map.Entry entry : CATEGORY_KEYWORDS.entrySet()) {
+ String keyword = entry.getKey();
+ String category = entry.getValue();
+
+ // 跳过空关键词(默认分类)
+ if (TextUtils.isEmpty(keyword)) {
+ continue;
+ }
+
+ // 关键词匹配
+ if (lowerContent.contains(keyword.toLowerCase())) {
+ return category;
+ }
+ }
+
+ // 默认分类
+ return "其他";
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/tool/DataUtils.java b/src/Notes-master/src/net/micode/notes/tool/DataUtils.java
index 2a14982..4d30044 100644
--- a/src/Notes-master/src/net/micode/notes/tool/DataUtils.java
+++ b/src/Notes-master/src/net/micode/notes/tool/DataUtils.java
@@ -81,7 +81,7 @@ public class DataUtils {
}
public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids,
- long folderId) {
+ long folderId) {
if (ids == null) {
Log.d(TAG, "the ids is null");
return true;
@@ -91,6 +91,21 @@ public class DataUtils {
for (long id : ids) {
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
+
+ // 如果是移动到回收站,保存原始父文件夹ID
+ if (folderId == Notes.ID_TRASH_FOLER) {
+ // 查询当前父文件夹ID
+ Cursor cursor = resolver.query(
+ ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id),
+ new String[]{NoteColumns.PARENT_ID},
+ null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ long originParentId = cursor.getLong(0);
+ builder.withValue(NoteColumns.ORIGIN_PARENT_ID, originParentId);
+ cursor.close();
+ }
+ }
+
builder.withValue(NoteColumns.PARENT_ID, folderId);
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1);
operationList.add(builder.build());
@@ -184,8 +199,8 @@ public class DataUtils {
public static boolean checkVisibleFolderName(ContentResolver resolver, String name) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null,
NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER +
- " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER +
- " AND " + NoteColumns.SNIPPET + "=?",
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER +
+ " AND " + NoteColumns.SNIPPET + "=?",
new String[] { name }, null);
boolean exist = false;
if(cursor != null) {
@@ -247,7 +262,7 @@ public class DataUtils {
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.NOTE_ID },
CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL("
- + CallNote.PHONE_NUMBER + ",?)",
+ + CallNote.PHONE_NUMBER + ",?)",
new String [] { String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber },
null);
@@ -279,7 +294,8 @@ public class DataUtils {
cursor.close();
return snippet;
}
- throw new IllegalArgumentException("Note is not found with id: " + noteId);
+ // 如果找不到noteId,返回空字符串,而不是抛出异常
+ return "";
}
public static String getFormattedSnippet(String snippet) {
@@ -292,4 +308,61 @@ public class DataUtils {
}
return snippet;
}
+
+ /**
+ * 根据文件夹名称获取文件夹ID
+ * @param resolver ContentResolver
+ * @param folderName 文件夹名称
+ * @return 文件夹ID,若不存在则返回0
+ */
+ public static long getFolderIdByName(ContentResolver resolver, String folderName) {
+ Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
+ new String[] { NoteColumns.ID },
+ NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "=? AND " + NoteColumns.SNIPPET + "=?",
+ new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_ROOT_FOLDER), folderName },
+ null);
+
+ long folderId = 0;
+ if (cursor != null) {
+ if (cursor.moveToFirst()) {
+ try {
+ folderId = cursor.getLong(0);
+ } catch (IndexOutOfBoundsException e) {
+ Log.e(TAG, "Get folder id failed: " + e.toString());
+ }
+ }
+ cursor.close();
+ }
+
+ return folderId;
+ }
+
+ /**
+ * 创建新文件夹
+ * @param resolver ContentResolver
+ * @param folderName 文件夹名称
+ * @return 新创建的文件夹ID,若创建失败则返回0
+ */
+ public static long createFolder(ContentResolver resolver, String folderName) {
+ // 检查文件夹是否已存在
+ long existingFolderId = getFolderIdByName(resolver, folderName);
+ if (existingFolderId > 0) {
+ return existingFolderId;
+ }
+
+ // 创建新文件夹
+ ContentValues values = new ContentValues();
+ values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
+ values.put(NoteColumns.PARENT_ID, Notes.ID_ROOT_FOLDER);
+ values.put(NoteColumns.SNIPPET, folderName);
+ values.put(NoteColumns.NOTES_COUNT, 0);
+
+ android.net.Uri uri = resolver.insert(Notes.CONTENT_NOTE_URI, values);
+ if (uri != null) {
+ return ContentUris.parseId(uri);
+ }
+
+ Log.e(TAG, "Create folder failed: " + folderName);
+ return 0;
+ }
}
diff --git a/src/Notes-master/src/net/micode/notes/tool/UserManager.java b/src/Notes-master/src/net/micode/notes/tool/UserManager.java
new file mode 100644
index 0000000..1254109
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/tool/UserManager.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.micode.notes.tool;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+import net.micode.notes.data.NotesDatabaseHelper;
+import net.micode.notes.data.Users;
+
+/**
+ * 用户管理类,用于保存和获取当前登录用户的信息
+ */
+public class UserManager {
+ private static final String TAG = "UserManager";
+ private static final String PREF_NAME = "user_preferences";
+ private static final String KEY_CURRENT_USER_ID = "current_user_id";
+ private static final String KEY_CURRENT_USERNAME = "current_username";
+
+ private static UserManager sInstance;
+ private SharedPreferences mPrefs;
+ private Context mContext;
+
+ private UserManager(Context context) {
+ mContext = context.getApplicationContext();
+ mPrefs = mContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+ }
+
+ public static synchronized UserManager getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new UserManager(context);
+ }
+ return sInstance;
+ }
+
+ /**
+ * 保存当前登录用户的信息
+ * @param userId 用户ID
+ * @param username 用户名
+ */
+ public void saveCurrentUser(long userId, String username) {
+ SharedPreferences.Editor editor = mPrefs.edit();
+ editor.putLong(KEY_CURRENT_USER_ID, userId);
+ editor.putString(KEY_CURRENT_USERNAME, username);
+ editor.apply();
+ }
+
+ /**
+ * 获取当前登录用户的ID
+ * @return 当前用户ID,未登录返回-1
+ */
+ public long getCurrentUserId() {
+ return mPrefs.getLong(KEY_CURRENT_USER_ID, -1);
+ }
+
+ /**
+ * 获取当前登录用户的用户名
+ * @return 当前用户名,未登录返回null
+ */
+ public String getCurrentUsername() {
+ return mPrefs.getString(KEY_CURRENT_USERNAME, null);
+ }
+
+ /**
+ * 清除当前用户信息,用于退出登录
+ */
+ public void clearCurrentUser() {
+ SharedPreferences.Editor editor = mPrefs.edit();
+ editor.remove(KEY_CURRENT_USER_ID);
+ editor.remove(KEY_CURRENT_USERNAME);
+ editor.apply();
+ }
+
+ /**
+ * 检查是否已登录
+ * @return 是否已登录
+ */
+ public boolean isLoggedIn() {
+ return getCurrentUserId() != -1;
+ }
+
+ /**
+ * 验证用户密码
+ * @param userId 用户ID
+ * @param password 输入的密码
+ * @return 密码是否正确
+ */
+ public boolean validatePassword(long userId, String password) {
+ try {
+ // 直接访问数据库验证密码
+ NotesDatabaseHelper helper = NotesDatabaseHelper.getInstance(mContext);
+ SQLiteDatabase db = helper.getReadableDatabase();
+
+ Cursor cursor = null;
+ try {
+ String selection = Users.UserColumns.ID + " = ? AND " + Users.UserColumns.PASSWORD + " = ?";
+ String[] selectionArgs = {String.valueOf(userId), password};
+
+ cursor = db.query(
+ NotesDatabaseHelper.TABLE.USER,
+ new String[]{Users.UserColumns.ID},
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ );
+
+ return cursor != null && cursor.moveToFirst();
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ /**
+ * 设置当前用户
+ * @param userId 用户ID
+ */
+ public void setCurrentUser(long userId) {
+ try {
+ // 查询用户的用户名
+ NotesDatabaseHelper helper = NotesDatabaseHelper.getInstance(mContext);
+ SQLiteDatabase db = helper.getReadableDatabase();
+
+ Cursor cursor = null;
+ String username = "未知用户";
+ try {
+ String selection = Users.UserColumns.ID + " = ?";
+ String[] selectionArgs = {String.valueOf(userId)};
+
+ cursor = db.query(
+ NotesDatabaseHelper.TABLE.USER,
+ new String[]{Users.UserColumns.USERNAME},
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ );
+
+ if (cursor != null && cursor.moveToFirst()) {
+ username = cursor.getString(0);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ // 保存当前用户信息
+ SharedPreferences.Editor editor = mPrefs.edit();
+ editor.putLong(KEY_CURRENT_USER_ID, userId);
+ editor.putString(KEY_CURRENT_USERNAME, username);
+ editor.apply();
+ } catch (Exception e) {
+ Log.e(TAG, "Error in setCurrentUser: " + e.getMessage());
+ e.printStackTrace();
+ // 即使发生异常,也确保保存用户ID,避免状态不一致
+ try {
+ SharedPreferences.Editor editor = mPrefs.edit();
+ editor.putLong(KEY_CURRENT_USER_ID, userId);
+ editor.putString(KEY_CURRENT_USERNAME, "未知用户");
+ editor.apply();
+ } catch (Exception innerE) {
+ Log.e(TAG, "Error in emergency save: " + innerE.getMessage());
+ innerE.printStackTrace();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java b/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java
index 85723be..2999b28 100644
--- a/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java
+++ b/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java
@@ -30,6 +30,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
+import android.util.Log;
import android.view.Window;
import android.view.WindowManager;
@@ -52,30 +53,42 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
requestWindowFeature(Window.FEATURE_NO_TITLE);
final Window win = getWindow();
- win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ // 添加适当的标志,确保在各种情况下都能显示提醒窗口
+ win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
+
+ // 在Android 10+中,需要确保Intent有正确的标志
+ // 注意:FLAG_ACTIVITY_NEW_TASK和FLAG_ACTIVITY_CLEAR_TASK是Intent的常量,不是WindowManager.LayoutParams的常量
+ // 这些标志已经在AlarmReceiver中通过intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)设置
- if (!isScreenOn()) {
- win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
- | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
- | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
- | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
- }
Intent intent = getIntent();
try {
- mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
- mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
- mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0,
- SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
- : mSnippet;
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
+ // 检查Intent和数据是否存在
+ if (intent != null && intent.getData() != null) {
+ mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
+ mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
+ mSnippet = mSnippet != null && mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0,
+ SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
+ : mSnippet;
+ } else {
+ Log.e("AlarmAlertActivity", "Intent or data is null");
+ finish();
+ return;
+ }
+ } catch (Exception e) {
+ Log.e("AlarmAlertActivity", "Error processing intent: " + e.getMessage(), e);
+ finish();
return;
}
mPlayer = new MediaPlayer();
- if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
+ if (mNoteId > 0 && DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
showActionDialog();
playAlarmSound();
} else {
@@ -85,44 +98,72 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
private boolean isScreenOn() {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
- return pm.isScreenOn();
+ // 在Android 10+中,isScreenOn()方法已被弃用,应使用isInteractive()
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT_WATCH) {
+ return pm.isInteractive();
+ } else {
+ return pm.isScreenOn();
+ }
}
private void playAlarmSound() {
- Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
-
- int silentModeStreams = Settings.System.getInt(getContentResolver(),
- Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0);
-
- if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) {
- mPlayer.setAudioStreamType(silentModeStreams);
- } else {
- mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
+ // 添加空检查,避免崩溃
+ if (mPlayer == null) {
+ return;
}
+
try {
+ Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
+ if (url == null) {
+ // 如果没有默认的闹钟铃声,使用系统默认铃声
+ url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_RINGTONE);
+ }
+ if (url == null) {
+ // 如果没有系统默认铃声,使用通知铃声
+ url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_NOTIFICATION);
+ }
+ if (url == null) {
+ // 如果都没有,直接返回
+ return;
+ }
+
+ int silentModeStreams = Settings.System.getInt(getContentResolver(),
+ Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0);
+
+ if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) {
+ mPlayer.setAudioStreamType(silentModeStreams);
+ } else {
+ mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
+ }
+
mPlayer.setDataSource(this, url);
mPlayer.prepare();
mPlayer.setLooping(true);
mPlayer.start();
- } catch (IllegalArgumentException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (SecurityException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IllegalStateException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IOException e) {
- // TODO Auto-generated catch block
+ } catch (Exception e) {
+ // 捕获所有异常,避免崩溃
e.printStackTrace();
+ // 发生异常时,释放播放器资源
+ if (mPlayer != null) {
+ try {
+ mPlayer.release();
+ mPlayer = null;
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
}
}
private void showActionDialog() {
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle(R.string.app_name);
- dialog.setMessage(mSnippet);
+ // 如果mSnippet为空,显示默认提示信息
+ if (mSnippet == null || mSnippet.isEmpty()) {
+ dialog.setMessage(getString(R.string.set_remind_time_message));
+ } else {
+ dialog.setMessage(mSnippet);
+ }
dialog.setPositiveButton(R.string.notealert_ok, this);
if (isScreenOn()) {
dialog.setNegativeButton(R.string.notealert_enter, this);
@@ -150,9 +191,15 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
private void stopAlarmSound() {
if (mPlayer != null) {
- mPlayer.stop();
- mPlayer.release();
- mPlayer = null;
+ try {
+ mPlayer.stop();
+ mPlayer.release();
+ mPlayer = null;
+ } catch (Exception e) {
+ // 捕获所有异常,避免崩溃
+ e.printStackTrace();
+ mPlayer = null;
+ }
}
}
}
diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java b/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java
index f221202..763cb70 100644
--- a/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java
+++ b/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java
@@ -53,10 +53,17 @@ public class AlarmInitReceiver extends BroadcastReceiver {
long alertDate = c.getLong(COLUMN_ALERTED_DATE);
Intent sender = new Intent(context, AlarmReceiver.class);
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)));
- PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0);
+ // 使用适当的PendingIntent flag,确保在Android 12+中正常工作
+ int flags = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S ? PendingIntent.FLAG_IMMUTABLE : 0;
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, flags);
AlarmManager alermManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
- alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
+ // 在Android 6.0+中,使用setExact方法以确保准确的提醒时间
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+ alermManager.setExact(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
+ } else {
+ alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
+ }
} while (c.moveToNext());
}
c.close();
diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java b/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java
index 54e503b..9f0e9aa 100644
--- a/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java
+++ b/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java
@@ -23,8 +23,17 @@ import android.content.Intent;
public class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- intent.setClass(context, AlarmAlertActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(intent);
+ // 添加空检查,避免崩溃
+ if (context == null || intent == null) {
+ return;
+ }
+ try {
+ intent.setClass(context, AlarmAlertActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ } catch (Exception e) {
+ // 捕获所有异常,避免崩溃
+ e.printStackTrace();
+ }
}
}
diff --git a/src/Notes-master/src/net/micode/notes/ui/ChatActivity.java b/src/Notes-master/src/net/micode/notes/ui/ChatActivity.java
new file mode 100644
index 0000000..3f35133
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/ChatActivity.java
@@ -0,0 +1,432 @@
+package net.micode.notes.ui;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.text.TextUtils;
+import android.graphics.Color;
+
+import net.micode.notes.R;
+import net.micode.notes.data.Messages;
+import net.micode.notes.data.NotesDatabaseHelper;
+import net.micode.notes.tool.UserManager;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+public class ChatActivity extends Activity {
+ private static final String TAG = "ChatActivity";
+
+ private ListView mChatListView;
+ private EditText mMessageEditText;
+ private Button mSendButton;
+ private ChatAdapter mChatAdapter;
+ private List mMessageList;
+
+ private NotesDatabaseHelper mDbHelper;
+ private SQLiteDatabase mDb;
+ private UserManager mUserManager;
+
+ private long mCurrentUserId;
+ private long mFriendId;
+ private String mFriendUsername;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ try {
+ setContentView(R.layout.chat_activity);
+
+ // 设置ActionBar
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ // 获取Intent参数
+ mFriendId = getIntent().getLongExtra("friend_id", -1);
+ mFriendUsername = getIntent().getStringExtra("friend_username");
+
+ if (mFriendId == -1 || mFriendUsername == null) {
+ Toast.makeText(this, "无效的好友信息", Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+
+ // 设置ActionBar标题为好友用户名
+ if (actionBar != null) {
+ actionBar.setTitle(mFriendUsername);
+ }
+
+ // 初始化数据库
+ mDbHelper = NotesDatabaseHelper.getInstance(this);
+ if (mDbHelper != null) {
+ mDb = mDbHelper.getWritableDatabase();
+ }
+
+ // 初始化UserManager
+ mUserManager = UserManager.getInstance(this);
+ if (mUserManager != null) {
+ mCurrentUserId = mUserManager.getCurrentUserId();
+ }
+
+ // 初始化界面控件
+ mChatListView = findViewById(R.id.chat_list);
+ mMessageEditText = findViewById(R.id.message_edit_text);
+ mSendButton = findViewById(R.id.send_button);
+
+ // 初始化消息列表
+ mMessageList = new ArrayList<>();
+ mChatAdapter = new ChatAdapter(this, mMessageList, mCurrentUserId);
+ mChatListView.setAdapter(mChatAdapter);
+
+ // 设置发送按钮点击事件
+ mSendButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ sendMessage();
+ }
+ });
+
+ // 加载聊天记录
+ loadChatHistory();
+ } catch (Exception e) {
+ Log.e(TAG, "Error in onCreate: " + e.getMessage(), e);
+ Toast.makeText(this, "聊天界面初始化失败", Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ try {
+ // 重新获取当前用户ID,确保在账号切换后能使用正确的用户ID
+ if (mUserManager != null) {
+ mCurrentUserId = mUserManager.getCurrentUserId();
+ Log.d(TAG, "Updated current user ID to: " + mCurrentUserId);
+ }
+
+ // 更新适配器的当前用户ID
+ if (mChatAdapter != null) {
+ mChatAdapter.mCurrentUserId = mCurrentUserId;
+ }
+
+ // 重新加载聊天记录
+ loadChatHistory();
+ } catch (Exception e) {
+ Log.e(TAG, "Error in onResume: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 关闭数据库连接
+ if (mDb != null && mDb.isOpen()) {
+ mDb.close();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(android.view.MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // 返回上一级活动
+ finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /**
+ * 加载聊天记录
+ */
+ private void loadChatHistory() {
+ mMessageList.clear();
+
+ // 查询聊天记录
+ String sql = "SELECT * FROM " + NotesDatabaseHelper.TABLE.MESSAGE + " WHERE " +
+ "(" + Messages.MessageColumns.SENDER_ID + " = ? AND " + Messages.MessageColumns.RECEIVER_ID + " = ?) OR " +
+ "(" + Messages.MessageColumns.SENDER_ID + " = ? AND " + Messages.MessageColumns.RECEIVER_ID + " = ?) " +
+ "ORDER BY " + Messages.MessageColumns.CREATED_DATE + " ASC";
+
+ Cursor cursor = mDb.rawQuery(sql, new String[]{
+ String.valueOf(mCurrentUserId), String.valueOf(mFriendId),
+ String.valueOf(mFriendId), String.valueOf(mCurrentUserId)
+ });
+
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(Messages.MessageColumns.ID));
+ long senderId = cursor.getLong(cursor.getColumnIndexOrThrow(Messages.MessageColumns.SENDER_ID));
+ long receiverId = cursor.getLong(cursor.getColumnIndexOrThrow(Messages.MessageColumns.RECEIVER_ID));
+ String content = cursor.getString(cursor.getColumnIndexOrThrow(Messages.MessageColumns.CONTENT));
+ int messageType = cursor.getInt(cursor.getColumnIndexOrThrow(Messages.MessageColumns.MESSAGE_TYPE));
+ long createdDate = cursor.getLong(cursor.getColumnIndexOrThrow(Messages.MessageColumns.CREATED_DATE));
+ int isRead = cursor.getInt(cursor.getColumnIndexOrThrow(Messages.MessageColumns.IS_READ));
+
+ mMessageList.add(new ChatMessage(id, senderId, receiverId, content, messageType, createdDate, isRead));
+ }
+ cursor.close();
+ }
+
+ // 通知适配器数据变化
+ mChatAdapter.notifyDataSetChanged();
+
+ // 滚动到底部
+ if (!mMessageList.isEmpty()) {
+ mChatListView.setSelection(mMessageList.size() - 1);
+ }
+
+ // 将接收到的消息标记为已读
+ markMessagesAsRead();
+ }
+
+ /**
+ * 发送消息
+ */
+ private void sendMessage() {
+ String content = mMessageEditText.getText().toString().trim();
+ if (content.isEmpty()) {
+ Toast.makeText(this, "消息内容不能为空", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 创建消息对象
+ ContentValues values = new ContentValues();
+ values.put(Messages.MessageColumns.SENDER_ID, mCurrentUserId);
+ values.put(Messages.MessageColumns.RECEIVER_ID, mFriendId);
+ values.put(Messages.MessageColumns.CONTENT, content);
+ values.put(Messages.MessageColumns.MESSAGE_TYPE, Messages.MessageType.TEXT);
+ values.put(Messages.MessageColumns.CREATED_DATE, System.currentTimeMillis());
+ values.put(Messages.MessageColumns.IS_READ, 0);
+
+ // 插入消息到数据库
+ long messageId = mDb.insert(NotesDatabaseHelper.TABLE.MESSAGE, null, values);
+ if (messageId != -1) {
+ // 清空输入框
+ mMessageEditText.setText("");
+
+ // 重新加载聊天记录
+ loadChatHistory();
+ } else {
+ Toast.makeText(this, "发送失败", Toast.LENGTH_SHORT).show();
+ Log.e(TAG, "Failed to send message");
+ }
+ }
+
+ /**
+ * 将接收到的消息标记为已读
+ */
+ private void markMessagesAsRead() {
+ ContentValues values = new ContentValues();
+ values.put(Messages.MessageColumns.IS_READ, 1);
+
+ int updatedRows = mDb.update(NotesDatabaseHelper.TABLE.MESSAGE, values,
+ Messages.MessageColumns.SENDER_ID + " = ? AND " + Messages.MessageColumns.RECEIVER_ID + " = ? AND " + Messages.MessageColumns.IS_READ + " = 0",
+ new String[]{String.valueOf(mFriendId), String.valueOf(mCurrentUserId)});
+
+ Log.d(TAG, "Marked " + updatedRows + " messages as read");
+ }
+
+ /**
+ * 显示便签详情
+ */
+ private void showNoteDetail(String noteData) {
+ try {
+ // 解析便签数据
+ String[] noteParts = noteData.split("\\|");
+ if (noteParts.length < 2) {
+ Toast.makeText(this, "无效的便签数据", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ String noteTitle = noteParts[0];
+ String noteContent = noteParts[1];
+
+ if (TextUtils.isEmpty(noteTitle)) {
+ noteTitle = "无标题便签";
+ }
+
+ // 创建并显示便签详情对话框
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(noteTitle);
+ builder.setMessage(noteContent);
+ builder.setPositiveButton("确定", null);
+ builder.show();
+ } catch (Exception e) {
+ Log.e(TAG, "Error showing note detail: " + e.getMessage(), e);
+ Toast.makeText(this, "显示便签详情失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 聊天消息实体类
+ */
+ private static class ChatMessage {
+ long id;
+ long senderId;
+ long receiverId;
+ String content;
+ int messageType;
+ long createdDate;
+ int isRead;
+
+ ChatMessage(long id, long senderId, long receiverId, String content, int messageType, long createdDate, int isRead) {
+ this.id = id;
+ this.senderId = senderId;
+ this.receiverId = receiverId;
+ this.content = content;
+ this.messageType = messageType;
+ this.createdDate = createdDate;
+ this.isRead = isRead;
+ }
+ }
+
+ /**
+ * 聊天消息适配器
+ */
+ private static class ChatAdapter extends BaseAdapter {
+ private static final int VIEW_TYPE_SENT_TEXT = 0;
+ private static final int VIEW_TYPE_RECEIVED_TEXT = 1;
+ private static final int VIEW_TYPE_SENT_NOTE = 2;
+ private static final int VIEW_TYPE_RECEIVED_NOTE = 3;
+
+ private Context mContext;
+ private List mMessageList;
+ public long mCurrentUserId;
+ private LayoutInflater mInflater;
+ private SimpleDateFormat mDateFormat;
+
+ ChatAdapter(Context context, List messageList, long currentUserId) {
+ mContext = context;
+ mMessageList = messageList;
+ mCurrentUserId = currentUserId;
+ mInflater = LayoutInflater.from(context);
+ mDateFormat = new SimpleDateFormat("HH:mm");
+ }
+
+ @Override
+ public int getCount() {
+ return mMessageList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mMessageList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 4; // 四种视图类型:发送文本、接收文本、发送便签、接收便签
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ ChatMessage message = mMessageList.get(position);
+ boolean isSentByMe = message.senderId == mCurrentUserId;
+ if (message.messageType == Messages.MessageType.NOTE) {
+ return isSentByMe ? VIEW_TYPE_SENT_NOTE : VIEW_TYPE_RECEIVED_NOTE;
+ } else {
+ return isSentByMe ? VIEW_TYPE_SENT_TEXT : VIEW_TYPE_RECEIVED_TEXT;
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ChatMessage message = mMessageList.get(position);
+ boolean isSentByMe = message.senderId == mCurrentUserId;
+
+ ViewHolder holder;
+ if (convertView == null) {
+ // 根据视图类型选择不同的布局
+ int viewType = getItemViewType(position);
+ switch (viewType) {
+ case VIEW_TYPE_SENT_NOTE:
+ convertView = mInflater.inflate(R.layout.chat_message_sent_note_item, parent, false);
+ break;
+ case VIEW_TYPE_RECEIVED_NOTE:
+ convertView = mInflater.inflate(R.layout.chat_message_received_note_item, parent, false);
+ break;
+ case VIEW_TYPE_SENT_TEXT:
+ default:
+ convertView = mInflater.inflate(R.layout.chat_message_sent_item, parent, false);
+ break;
+ case VIEW_TYPE_RECEIVED_TEXT:
+ convertView = mInflater.inflate(R.layout.chat_message_received_item, parent, false);
+ break;
+ }
+
+ holder = new ViewHolder();
+ holder.contentTextView = convertView.findViewById(R.id.message_content);
+ holder.timeTextView = convertView.findViewById(R.id.message_time);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ // 设置消息内容
+ if (message.messageType == Messages.MessageType.NOTE) {
+ // 便签类型消息,只显示便签标题
+ String[] noteData = message.content.split("\\|");
+ if (noteData.length >= 2) {
+ String noteTitle = noteData[0];
+ if (TextUtils.isEmpty(noteTitle)) {
+ noteTitle = "无标题便签";
+ }
+ holder.contentTextView.setText(noteTitle);
+ // 设置不同的颜色,区分于普通文本消息
+ holder.contentTextView.setTextColor(Color.BLUE); // 使用蓝色区分便签消息
+ holder.contentTextView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // 点击便签,查看详情
+ ((ChatActivity) mContext).showNoteDetail(message.content);
+ }
+ });
+ }
+ } else {
+ // 普通文本消息
+ holder.contentTextView.setText(message.content);
+ holder.contentTextView.setOnClickListener(null);
+ }
+
+ // 设置消息时间
+ Date date = new Date(message.createdDate);
+ holder.timeTextView.setText(mDateFormat.format(date));
+
+ return convertView;
+ }
+
+ private static class ViewHolder {
+ TextView contentTextView;
+ TextView timeTextView;
+ }
+ }
+}
diff --git a/src/Notes-master/src/net/micode/notes/ui/FriendManagementActivity.java b/src/Notes-master/src/net/micode/notes/ui/FriendManagementActivity.java
new file mode 100644
index 0000000..679971d
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/FriendManagementActivity.java
@@ -0,0 +1,206 @@
+package net.micode.notes.ui;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import net.micode.notes.R;
+import net.micode.notes.data.NotesDatabaseHelper;
+import net.micode.notes.tool.UserManager;
+import net.micode.notes.data.Users;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FriendManagementActivity extends Activity {
+ private ListView mFriendListView;
+ private FriendAdapter mFriendAdapter;
+ private List mFriendList;
+ private NotesDatabaseHelper mDbHelper;
+ private SQLiteDatabase mDb;
+ private UserManager mUserManager;
+ private long mCurrentUserId;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_friend_management);
+
+ // 设置ActionBar
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setTitle("好友管理");
+ }
+
+ // 初始化数据库
+ mDbHelper = NotesDatabaseHelper.getInstance(this);
+ mDb = mDbHelper.getWritableDatabase();
+
+ // 初始化UserManager
+ mUserManager = UserManager.getInstance(this);
+ mCurrentUserId = mUserManager.getCurrentUserId();
+
+ // 初始化ListView
+ mFriendListView = findViewById(R.id.friend_list);
+ mFriendList = new ArrayList<>();
+ mFriendAdapter = new FriendAdapter(this, mFriendList);
+ mFriendListView.setAdapter(mFriendAdapter);
+
+ // 设置ListView点击事件
+ mFriendListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ Friend friend = mFriendList.get(position);
+ // 启动与好友聊天的活动
+ Intent intent = new Intent(FriendManagementActivity.this, ChatActivity.class);
+ intent.putExtra("friend_id", friend.id);
+ intent.putExtra("friend_username", friend.username);
+ startActivity(intent);
+ }
+ });
+
+ // 加载好友列表
+ loadFriendList();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // 重新加载好友列表
+ loadFriendList();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 关闭数据库连接
+ if (mDb != null && mDb.isOpen()) {
+ mDb.close();
+ }
+ }
+
+ /**
+ * 加载好友列表,即除当前用户以外的所有用户
+ */
+ private void loadFriendList() {
+ mFriendList.clear();
+
+ // 查询除当前用户以外的所有用户
+ Cursor cursor = mDb.query(
+ NotesDatabaseHelper.TABLE.USER,
+ new String[]{Users.UserColumns.ID, Users.UserColumns.USERNAME},
+ Users.UserColumns.ID + " != ?",
+ new String[]{String.valueOf(mCurrentUserId)},
+ null,
+ null,
+ null
+ );
+
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(Users.UserColumns.ID));
+ String username = cursor.getString(cursor.getColumnIndexOrThrow(Users.UserColumns.USERNAME));
+ mFriendList.add(new Friend(id, username));
+ }
+ cursor.close();
+ }
+
+ // 通知适配器数据变化
+ mFriendAdapter.notifyDataSetChanged();
+
+ // 如果没有好友,显示提示
+ if (mFriendList.isEmpty()) {
+ Toast.makeText(this, "暂无其他用户", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 好友实体类
+ */
+ private static class Friend {
+ long id;
+ String username;
+
+ Friend(long id, String username) {
+ this.id = id;
+ this.username = username;
+ }
+ }
+
+ /**
+ * 好友列表适配器
+ */
+ private static class FriendAdapter extends BaseAdapter {
+ private Context mContext;
+ private List mFriendList;
+ private LayoutInflater mInflater;
+
+ FriendAdapter(Context context, List friendList) {
+ mContext = context;
+ mFriendList = friendList;
+ mInflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public int getCount() {
+ return mFriendList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mFriendList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.item_friend, parent, false);
+ holder = new ViewHolder();
+ holder.usernameTextView = convertView.findViewById(R.id.friend_username);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ Friend friend = mFriendList.get(position);
+ holder.usernameTextView.setText(friend.username);
+
+ return convertView;
+ }
+
+ private static class ViewHolder {
+ TextView usernameTextView;
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(android.view.MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // 返回上一级活动
+ finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/ui/FriendNoteEditActivity.java b/src/Notes-master/src/net/micode/notes/ui/FriendNoteEditActivity.java
new file mode 100644
index 0000000..d5c6620
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/FriendNoteEditActivity.java
@@ -0,0 +1,239 @@
+package net.micode.notes.ui;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import net.micode.notes.R;
+import net.micode.notes.data.Notes;
+
+
+public class FriendNoteEditActivity extends Activity {
+ private TextView mNoteEditor;
+ private TextView mNoteTitleView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_friend_note_edit);
+
+ // 设置ActionBar
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setTitle("查看便签");
+ }
+
+ // 初始化控件
+ mNoteEditor = findViewById(R.id.note_edit_view);
+ mNoteTitleView = findViewById(R.id.note_title_view);
+
+ // 加载便签数据
+ if (savedInstanceState == null && !initActivityState(getIntent())) {
+ finish();
+ return;
+ }
+ initNoteScreen();
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID));
+ intent.putExtra("friend_id", savedInstanceState.getLong("friend_id"));
+ if (!initActivityState(intent)) {
+ finish();
+ return;
+ }
+ // 恢复状态后重新初始化界面
+ initNoteScreen();
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (getIntent() != null) {
+ outState.putLong(Intent.EXTRA_UID, getIntent().getLongExtra(Intent.EXTRA_UID, 0));
+ outState.putLong("friend_id", getIntent().getLongExtra("friend_id", -1));
+ }
+ }
+
+ private String mNoteContent;
+ private String mNoteTitle;
+ private long mModifiedDate;
+
+ private boolean initActivityState(Intent intent) {
+ /**
+ * 只支持查看模式
+ */
+ long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0);
+ long friendId = intent.getLongExtra("friend_id", -1);
+
+ if (noteId <= 0 || friendId <= 0) {
+ Toast.makeText(this, "便签信息错误", Toast.LENGTH_SHORT).show();
+ finish();
+ return false;
+ }
+
+ try {
+ // 查询便签基本信息,包括标题
+ String[] noteProjection = {
+ net.micode.notes.data.Notes.NoteColumns.MODIFIED_DATE,
+ net.micode.notes.data.Notes.NoteColumns.TITLE
+ };
+
+ android.database.Cursor noteCursor = null;
+ try {
+ // 使用ContentResolver查询便签基本信息
+ noteCursor = getContentResolver().query(
+ android.content.ContentUris.withAppendedId(net.micode.notes.data.Notes.CONTENT_NOTE_URI, noteId),
+ noteProjection,
+ null,
+ null,
+ null
+ );
+
+ if (noteCursor != null) {
+ if (noteCursor.moveToFirst()) {
+ mModifiedDate = noteCursor.getLong(0);
+ mNoteTitle = noteCursor.getString(1);
+ }
+ noteCursor.close();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "查询便签信息失败", Toast.LENGTH_SHORT).show();
+ }
+
+ // 查询便签具体内容,这是必须的,因为完整内容存储在Data表中
+ String[] dataProjection = {net.micode.notes.data.Notes.DataColumns.CONTENT};
+ String dataSelection = net.micode.notes.data.Notes.DataColumns.NOTE_ID + " = ? AND " +
+ net.micode.notes.data.Notes.DataColumns.MIME_TYPE + " = ?";
+ String[] dataSelectionArgs = {String.valueOf(noteId), net.micode.notes.data.Notes.DataConstants.NOTE};
+
+ android.database.Cursor dataCursor = null;
+ try {
+ // 使用ContentResolver查询数据
+ dataCursor = getContentResolver().query(
+ net.micode.notes.data.Notes.CONTENT_DATA_URI,
+ dataProjection,
+ dataSelection,
+ dataSelectionArgs,
+ null
+ );
+
+ if (dataCursor != null) {
+ if (dataCursor.moveToFirst()) {
+ mNoteContent = dataCursor.getString(0);
+ }
+ dataCursor.close();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "查询便签内容失败", Toast.LENGTH_SHORT).show();
+ } finally {
+ if (dataCursor != null) {
+ dataCursor.close();
+ }
+ }
+
+ // 如果查询失败,尝试使用数据库直接查询
+ if (mNoteContent == null || mNoteContent.isEmpty()) {
+ try {
+ // 直接使用数据库查询,绕过ContentProvider的用户过滤
+ net.micode.notes.data.NotesDatabaseHelper helper = net.micode.notes.data.NotesDatabaseHelper.getInstance(FriendNoteEditActivity.this);
+ if (helper != null) {
+ dataCursor = helper.getReadableDatabase().query(
+ net.micode.notes.data.NotesDatabaseHelper.TABLE.DATA,
+ dataProjection,
+ dataSelection,
+ dataSelectionArgs,
+ null,
+ null,
+ null
+ );
+
+ if (dataCursor != null) {
+ if (dataCursor.moveToFirst()) {
+ mNoteContent = dataCursor.getString(0);
+ }
+ dataCursor.close();
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "直接查询便签内容失败", Toast.LENGTH_SHORT).show();
+ } finally {
+ if (dataCursor != null) {
+ dataCursor.close();
+ }
+ }
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "初始化便签失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
+ finish();
+ return false;
+ }
+ return true;
+ }
+
+ private void initNoteScreen() {
+ // 显示修改日期
+ TextView modifiedDateTextView = findViewById(R.id.tv_modified_date);
+ modifiedDateTextView.setText(android.text.format.DateUtils.formatDateTime(
+ this, mModifiedDate,
+ android.text.format.DateUtils.FORMAT_SHOW_DATE |
+ android.text.format.DateUtils.FORMAT_NUMERIC_DATE |
+ android.text.format.DateUtils.FORMAT_SHOW_TIME));
+
+ // 显示便签标题
+ if (mNoteTitle != null && !mNoteTitle.isEmpty()) {
+ mNoteTitleView.setVisibility(View.VISIBLE);
+ mNoteTitleView.setText(mNoteTitle);
+ } else {
+ // 如果没有标题,隐藏标题文本视图
+ mNoteTitleView.setVisibility(View.GONE);
+ }
+
+ // 显示便签内容,添加调试日志
+ if (mNoteContent != null && !mNoteContent.isEmpty()) {
+ mNoteEditor.setText(mNoteContent);
+ // 设置文本颜色为深色,确保可见
+ mNoteEditor.setTextColor(getResources().getColor(android.R.color.primary_text_dark));
+ // 设置背景色为白色,确保可见
+ mNoteEditor.setBackgroundColor(getResources().getColor(android.R.color.white));
+ } else {
+ // 如果内容为空,显示提示文本
+ mNoteEditor.setText("(空便签)");
+ mNoteEditor.setTextColor(getResources().getColor(android.R.color.secondary_text_dark));
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // 不显示菜单,因为是只读模式
+ return false;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(android.view.MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // 返回上一级活动
+ finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/ui/FriendNoteListActivity.java b/src/Notes-master/src/net/micode/notes/ui/FriendNoteListActivity.java
new file mode 100644
index 0000000..6a381eb
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/FriendNoteListActivity.java
@@ -0,0 +1,271 @@
+package net.micode.notes.ui;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import net.micode.notes.R;
+import net.micode.notes.data.Notes;
+import net.micode.notes.data.Notes.NoteColumns;
+import net.micode.notes.model.WorkingNote;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FriendNoteListActivity extends Activity {
+ private ListView mNoteListView;
+ private NoteAdapter mNoteAdapter;
+ private List mNoteList;
+ private long mFriendId;
+ private String mFriendUsername;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_friend_note_list);
+
+ // 获取好友信息
+ mFriendId = getIntent().getLongExtra("friend_id", -1);
+ mFriendUsername = getIntent().getStringExtra("friend_username");
+
+ if (mFriendId == -1 || mFriendUsername == null) {
+ Toast.makeText(this, "好友信息错误", Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+
+ // 设置ActionBar
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setTitle(mFriendUsername + "的公开便签");
+ }
+
+ // 初始化ListView
+ mNoteListView = findViewById(R.id.friend_note_list);
+ mNoteList = new ArrayList<>();
+ mNoteAdapter = new NoteAdapter(this, mNoteList);
+ mNoteListView.setAdapter(mNoteAdapter);
+
+ // 设置ListView点击事件
+ mNoteListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ Note note = mNoteList.get(position);
+ // 启动查看便签详情的活动
+ Intent intent = new Intent(FriendNoteListActivity.this, FriendNoteEditActivity.class);
+ intent.putExtra(Intent.EXTRA_UID, note.id);
+ intent.putExtra("friend_id", mFriendId);
+ startActivity(intent);
+ }
+ });
+
+ // 加载好友的公开便签
+ loadFriendNotes();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // 重新加载好友便签
+ loadFriendNotes();
+ }
+
+ /**
+ * 加载好友的公开便签
+ */
+ private void loadFriendNotes() {
+ try {
+ mNoteList.clear();
+
+ // 查询好友的所有公开便签
+ String[] projection = {
+ NoteColumns.ID,
+ NoteColumns.TITLE,
+ NoteColumns.SNIPPET,
+ NoteColumns.MODIFIED_DATE,
+ NoteColumns.TYPE,
+ NoteColumns.PINNED,
+ NoteColumns.LOCKED,
+ NoteColumns.PUBLIC
+ };
+
+ String selection = NoteColumns.USER_ID + " = ? AND " + NoteColumns.PUBLIC + " = 1 AND " + NoteColumns.PARENT_ID + " <> " + Notes.ID_TRASH_FOLER;
+ String[] selectionArgs = {String.valueOf(mFriendId)};
+
+ Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(
+ Notes.CONTENT_NOTE_URI,
+ projection,
+ selection,
+ selectionArgs,
+ NoteColumns.PINNED + " DESC, " + NoteColumns.MODIFIED_DATE + " DESC"
+ );
+
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.ID));
+ String title = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.TITLE));
+ String content = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.SNIPPET));
+ long modifiedDate = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.MODIFIED_DATE));
+ int type = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.TYPE));
+ int pinned = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.PINNED));
+ int locked = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.LOCKED));
+ int isPublic = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.PUBLIC));
+
+ mNoteList.add(new Note(id, title, content, modifiedDate, type, pinned, locked, isPublic));
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "查询好友便签失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ // 通知适配器数据变化
+ mNoteAdapter.notifyDataSetChanged();
+
+ // 如果没有公开便签,显示提示
+ if (mNoteList.isEmpty()) {
+ Toast.makeText(this, mFriendUsername + "没有公开的便签", Toast.LENGTH_SHORT).show();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "加载好友便签失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 便签实体类
+ */
+ private static class Note {
+ long id;
+ String title;
+ String content;
+ long modifiedDate;
+ int type;
+ int pinned;
+ int locked;
+ int isPublic;
+
+ Note(long id, String title, String content, long modifiedDate, int type, int pinned, int locked, int isPublic) {
+ this.id = id;
+ this.title = title;
+ this.content = content;
+ this.modifiedDate = modifiedDate;
+ this.type = type;
+ this.pinned = pinned;
+ this.locked = locked;
+ this.isPublic = isPublic;
+ }
+ }
+
+ /**
+ * 便签列表适配器
+ */
+ private static class NoteAdapter extends BaseAdapter {
+ private Activity mActivity;
+ private List mNoteList;
+ private LayoutInflater mInflater;
+
+ NoteAdapter(Activity activity, List noteList) {
+ mActivity = activity;
+ mNoteList = noteList;
+ mInflater = LayoutInflater.from(activity);
+ }
+
+ @Override
+ public int getCount() {
+ return mNoteList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mNoteList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.note_item, parent, false);
+ holder = new ViewHolder();
+ holder.titleTextView = convertView.findViewById(R.id.tv_title);
+ holder.modifiedDateTextView = convertView.findViewById(R.id.tv_time);
+ holder.pinnedImageView = convertView.findViewById(R.id.iv_alert_icon);
+ holder.lockedImageView = convertView.findViewById(R.id.iv_lock_icon);
+ holder.publicImageView = convertView.findViewById(R.id.iv_public_icon);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ Note note = mNoteList.get(position);
+ // 显示标题,如果标题为空则显示内容摘要
+ String displayText;
+ if (note.title != null && !note.title.isEmpty()) {
+ displayText = note.title;
+ } else {
+ displayText = note.content;
+ }
+ holder.titleTextView.setText(displayText);
+ holder.modifiedDateTextView.setText(android.text.format.DateUtils.formatDateTime(
+ mActivity, note.modifiedDate,
+ android.text.format.DateUtils.FORMAT_SHOW_DATE |
+ android.text.format.DateUtils.FORMAT_NUMERIC_DATE |
+ android.text.format.DateUtils.FORMAT_SHOW_TIME));
+
+ // 设置置顶图标
+ holder.pinnedImageView.setVisibility(note.pinned == 1 ? View.VISIBLE : View.GONE);
+
+ // 设置锁定图标
+ holder.lockedImageView.setVisibility(note.locked == 1 ? View.VISIBLE : View.GONE);
+
+ // 设置公开图标
+ holder.publicImageView.setVisibility(note.isPublic == 1 ? View.VISIBLE : View.GONE);
+
+ return convertView;
+ }
+
+ private static class ViewHolder {
+ TextView titleTextView;
+ TextView modifiedDateTextView;
+ ImageView pinnedImageView;
+ ImageView lockedImageView;
+ ImageView publicImageView;
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(android.view.MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // 返回上一级活动
+ finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/ui/LoginActivity.java b/src/Notes-master/src/net/micode/notes/ui/LoginActivity.java
new file mode 100644
index 0000000..d613e4a
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/LoginActivity.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.micode.notes.ui;
+
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+
+import net.micode.notes.R;
+import net.micode.notes.data.NotesDatabaseHelper;
+import net.micode.notes.data.Users;
+import net.micode.notes.tool.UserManager;
+
+public class LoginActivity extends AppCompatActivity {
+
+ private EditText etUsername;
+ private EditText etPassword;
+
+ private NotesDatabaseHelper mDbHelper;
+ private SQLiteDatabase mDb;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_login);
+
+ // 初始化数据库
+ mDbHelper = NotesDatabaseHelper.getInstance(this);
+ mDb = mDbHelper.getWritableDatabase();
+
+ // 初始化控件
+ etUsername = findViewById(R.id.et_username);
+ etPassword = findViewById(R.id.et_password);
+ }
+
+ /**
+ * 登录按钮点击事件
+ */
+ public void onLoginClick(View view) {
+ String username = etUsername.getText().toString().trim();
+ String password = etPassword.getText().toString().trim();
+
+ // 验证输入
+ if (username.isEmpty() || password.isEmpty()) {
+ Toast.makeText(this, "用户名和密码不能为空", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 验证用户名和密码
+ long userId = validateUser(username, password);
+ if (userId != -1) {
+ // 登录成功,保存用户信息
+ UserManager userManager = UserManager.getInstance(this);
+ userManager.saveCurrentUser(userId, username);
+
+ // 跳转到便签列表
+ Intent intent = new Intent(this, NotesListActivity.class);
+ startActivity(intent);
+ finish();
+ } else {
+ // 登录失败,提示错误
+ Toast.makeText(this, "用户名或密码错误", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 注册按钮点击事件
+ */
+ public void onRegisterClick(View view) {
+ showRegisterDialog();
+ }
+
+ /**
+ * 修改密码按钮点击事件
+ */
+ public void onChangePasswordClick(View view) {
+ showChangePasswordDialog();
+ }
+
+ /**
+ * 显示注册对话框
+ */
+ private void showRegisterDialog() {
+ View dialogView = getLayoutInflater().inflate(R.layout.dialog_register, null);
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setView(dialogView);
+
+ final AlertDialog dialog = builder.create();
+
+ // 初始化注册对话框控件
+ final EditText etRegisterUsername = dialogView.findViewById(R.id.et_register_username);
+ final EditText etRegisterPassword = dialogView.findViewById(R.id.et_register_password);
+ final EditText etRegisterConfirmPassword = dialogView.findViewById(R.id.et_register_confirm_password);
+ Button btnRegisterCancel = dialogView.findViewById(R.id.btn_register_cancel);
+ Button btnRegisterConfirm = dialogView.findViewById(R.id.btn_register_confirm);
+
+ // 取消按钮点击事件
+ btnRegisterCancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dialog.dismiss();
+ }
+ });
+
+ // 注册按钮点击事件
+ btnRegisterConfirm.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String username = etRegisterUsername.getText().toString().trim();
+ String password = etRegisterPassword.getText().toString().trim();
+ String confirmPassword = etRegisterConfirmPassword.getText().toString().trim();
+
+ // 验证输入
+ if (username.isEmpty() || password.isEmpty() || confirmPassword.isEmpty()) {
+ Toast.makeText(LoginActivity.this, "请填写完整信息", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (!password.equals(confirmPassword)) {
+ Toast.makeText(LoginActivity.this, "两次输入的密码不一致", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 检查用户名是否已存在
+ if (isUsernameExists(username)) {
+ Toast.makeText(LoginActivity.this, "用户名已存在", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 注册用户
+ if (registerUser(username, password)) {
+ Toast.makeText(LoginActivity.this, "注册成功", Toast.LENGTH_SHORT).show();
+ dialog.dismiss();
+ } else {
+ Toast.makeText(LoginActivity.this, "注册失败,请重试", Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ dialog.show();
+ }
+
+ /**
+ * 显示修改密码对话框
+ */
+ private void showChangePasswordDialog() {
+ View dialogView = getLayoutInflater().inflate(R.layout.dialog_change_password, null);
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setView(dialogView);
+
+ final AlertDialog dialog = builder.create();
+
+ // 初始化修改密码对话框控件
+ final EditText etChangeUsername = dialogView.findViewById(R.id.et_change_username);
+ final EditText etCurrentPassword = dialogView.findViewById(R.id.et_current_password);
+ final EditText etNewPassword = dialogView.findViewById(R.id.et_new_password);
+ final EditText etConfirmNewPassword = dialogView.findViewById(R.id.et_confirm_new_password);
+ Button btnChangeCancel = dialogView.findViewById(R.id.btn_change_cancel);
+ Button btnChangeConfirm = dialogView.findViewById(R.id.btn_change_confirm);
+
+ // 取消按钮点击事件
+ btnChangeCancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dialog.dismiss();
+ }
+ });
+
+ // 确认修改按钮点击事件
+ btnChangeConfirm.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String username = etChangeUsername.getText().toString().trim();
+ String currentPassword = etCurrentPassword.getText().toString().trim();
+ String newPassword = etNewPassword.getText().toString().trim();
+ String confirmNewPassword = etConfirmNewPassword.getText().toString().trim();
+
+ // 验证输入
+ if (username.isEmpty() || currentPassword.isEmpty() || newPassword.isEmpty() || confirmNewPassword.isEmpty()) {
+ Toast.makeText(LoginActivity.this, "请填写完整信息", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (!newPassword.equals(confirmNewPassword)) {
+ Toast.makeText(LoginActivity.this, "两次输入的新密码不一致", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 验证当前密码
+ if (validateUser(username, currentPassword) == -1) {
+ Toast.makeText(LoginActivity.this, "用户名或当前密码错误", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 修改密码
+ if (changePassword(username, newPassword)) {
+ Toast.makeText(LoginActivity.this, "密码修改成功", Toast.LENGTH_SHORT).show();
+ dialog.dismiss();
+ } else {
+ Toast.makeText(LoginActivity.this, "密码修改失败,请重试", Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ dialog.show();
+ }
+
+ /**
+ * 验证用户名和密码,并返回用户ID
+ */
+ private long validateUser(String username, String password) {
+ String[] projection = {Users.UserColumns.ID};
+ String selection = Users.UserColumns.USERNAME + " = ? AND " + Users.UserColumns.PASSWORD + " = ?";
+ String[] selectionArgs = {username, password};
+
+ Cursor cursor = mDb.query(
+ NotesDatabaseHelper.TABLE.USER,
+ projection,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ );
+
+ long userId = -1;
+ if (cursor.moveToFirst()) {
+ userId = cursor.getLong(cursor.getColumnIndexOrThrow(Users.UserColumns.ID));
+ }
+ cursor.close();
+ return userId;
+ }
+
+ /**
+ * 检查用户名是否已存在
+ */
+ private boolean isUsernameExists(String username) {
+ String[] projection = {Users.UserColumns.ID};
+ String selection = Users.UserColumns.USERNAME + " = ?";
+ String[] selectionArgs = {username};
+
+ Cursor cursor = mDb.query(
+ NotesDatabaseHelper.TABLE.USER,
+ projection,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ );
+
+ boolean exists = cursor.getCount() > 0;
+ cursor.close();
+ return exists;
+ }
+
+ /**
+ * 注册用户
+ */
+ private boolean registerUser(String username, String password) {
+ ContentValues values = new ContentValues();
+ values.put(Users.UserColumns.USERNAME, username);
+ values.put(Users.UserColumns.PASSWORD, password);
+
+ long result = mDb.insert(NotesDatabaseHelper.TABLE.USER, null, values);
+ return result != -1;
+ }
+
+ /**
+ * 修改密码
+ */
+ private boolean changePassword(String username, String newPassword) {
+ ContentValues values = new ContentValues();
+ values.put(Users.UserColumns.PASSWORD, newPassword);
+ values.put(Users.UserColumns.MODIFIED_DATE, System.currentTimeMillis());
+
+ String selection = Users.UserColumns.USERNAME + " = ?";
+ String[] selectionArgs = {username};
+
+ int result = mDb.update(
+ NotesDatabaseHelper.TABLE.USER,
+ values,
+ selection,
+ selectionArgs
+ );
+
+ return result > 0;
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 关闭数据库连接
+ if (mDb != null && mDb.isOpen()) {
+ mDb.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java b/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java
index 96a9ff8..c034826 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java
@@ -30,11 +30,15 @@ import android.content.SharedPreferences;
import android.graphics.Paint;
import android.os.Bundle;
import android.preference.PreferenceManager;
+import android.graphics.Bitmap;
+import android.net.Uri;
import android.text.Spannable;
import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.style.BackgroundColorSpan;
+import android.text.style.ImageSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -44,6 +48,8 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.CheckBox;
+import android.text.style.URLSpan;
+import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
@@ -64,7 +70,9 @@ import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener;
import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener;
import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;
-
+import net.micode.notes.ui.FoldersListAdapter;
+import android.view.ViewGroup;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -128,14 +136,18 @@ public class NoteEditActivity extends Activity implements OnClickListener,
private View mFontSizeSelector;
- private EditText mNoteEditor;
+ private NoteEditText mNoteEditor;
+ private NoteEditText mNoteTitleEditor; // 标题编辑器(支持富文本)
private View mNoteEditorPanel;
+ private View mCharacterCountLayout; // 字数统计布局
private WorkingNote mWorkingNote;
private SharedPreferences mSharedPrefs;
private int mFontSizeId;
+
+ private TextView mCharacterCountView; // 字数统计显示
private static final String PREFERENCE_FONT_SIZE = "pref_font_size";
@@ -148,6 +160,10 @@ public class NoteEditActivity extends Activity implements OnClickListener,
private String mUserQuery;
private Pattern mPattern;
+
+ // 撤回功能相关
+ private java.util.Stack mUndoStack; // 用于保存撤回历史记录(支持富文本)
+ private boolean mIsUndoing; // 标记是否正在执行撤回操作,避免递归调用
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -159,6 +175,11 @@ public class NoteEditActivity extends Activity implements OnClickListener,
return;
}
initResources();
+
+ // Check if we need to show change background dialog directly
+ if (getIntent().getBooleanExtra("CHANGE_BACKGROUND", false)) {
+ changeBackground();
+ }
}
/**
@@ -271,17 +292,55 @@ public class NoteEditActivity extends Activity implements OnClickListener,
private void initNoteScreen() {
mNoteEditor.setTextAppearance(this, TextAppearanceResources
.getTexAppearanceResource(mFontSizeId));
+ mNoteTitleEditor.setTextAppearance(this, TextAppearanceResources
+ .getTexAppearanceResource(mFontSizeId));
+ // 设置 MovementMethod,确保 ImageSpan 能正确显示
+ mNoteEditor.setMovementMethod(android.text.method.LinkMovementMethod.getInstance());
+
+ // 设置标题
+ mNoteTitleEditor.setText(mWorkingNote.getTitle());
+
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
switchToListMode(mWorkingNote.getContent());
} else {
mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
mNoteEditor.setSelection(mNoteEditor.getText().length());
}
+
for (Integer id : sBgSelectorSelectionMap.keySet()) {
findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE);
}
+
+ // 初始化字数统计显示
+ updateCharacterCount();
+
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
- mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
+
+ // 检查是否有保存的背景图片
+ SharedPreferences sharedPreferences = getSharedPreferences("NoteSettings", Context.MODE_PRIVATE);
+ String backgroundImagePath = sharedPreferences.getString("background_image", null);
+ if (backgroundImagePath != null) {
+ // 加载保存的背景图片
+ try {
+ android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeFile(backgroundImagePath);
+ if (bitmap != null) {
+ android.graphics.drawable.BitmapDrawable drawable = new android.graphics.drawable.BitmapDrawable(getResources(), bitmap);
+ mNoteEditorPanel.setBackground(drawable);
+ } else {
+ // 如果图片加载失败,使用默认背景
+ mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error loading saved background image: " + e.getMessage());
+ mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
+ }
+ } else {
+ // 使用默认背景
+ mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
+ }
+
+ // 设置字数统计布局的背景颜色与便签一致
+ mCharacterCountLayout.setBackgroundResource(mWorkingNote.getBgColorResId());
mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this,
mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE
@@ -358,8 +417,8 @@ public class NoteEditActivity extends Activity implements OnClickListener,
|| ev.getX() > (x + view.getWidth())
|| ev.getY() < y
|| ev.getY() > (y + view.getHeight())) {
- return false;
- }
+ return false;
+ }
return true;
}
@@ -371,7 +430,17 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date);
mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color);
mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this);
- mNoteEditor = (EditText) findViewById(R.id.note_edit_view);
+ mNoteEditor = (NoteEditText) findViewById(R.id.note_edit_view);
+ mNoteTitleEditor = (NoteEditText) findViewById(R.id.note_title_edit); // 初始化标题编辑器(支持富文本)
+ // 显式设置输入法类型,确保支持中文输入
+ mNoteEditor.setInputType(android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE |
+ android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES |
+ android.text.InputType.TYPE_TEXT_FLAG_AUTO_CORRECT |
+ android.text.InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
+ // 为标题编辑器设置输入类型
+ mNoteTitleEditor.setInputType(android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES |
+ android.text.InputType.TYPE_TEXT_FLAG_AUTO_CORRECT |
+ android.text.InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
mNoteEditorPanel = findViewById(R.id.sv_note_edit);
mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector);
for (int id : sBgSelectorBtnsMap.keySet()) {
@@ -395,6 +464,77 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE;
}
mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list);
+
+ // 初始化字数统计显示
+ mCharacterCountLayout = findViewById(R.id.ll_character_count);
+ mCharacterCountView = (TextView) findViewById(R.id.tv_character_count);
+
+ // 初始化撤回栈,保存完整的SpannableStringBuilder,包括富文本格式
+ mUndoStack = new java.util.Stack();
+ mIsUndoing = false;
+
+ // 创建一个通用的文本变化监听器,用于处理标题和内容编辑器
+ final android.text.TextWatcher textWatcher = new android.text.TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ if (!mIsUndoing) {
+ // 获取当前焦点的编辑器
+ View focusedView = getCurrentFocus();
+ if (focusedView == null) return;
+
+ // 保存完整的便签内容,包括标题和正文(支持富文本)
+ android.text.SpannableStringBuilder fullContent = new android.text.SpannableStringBuilder();
+
+ // 先保存标题
+ android.text.SpannableStringBuilder titleContent = new android.text.SpannableStringBuilder(mNoteTitleEditor.getText());
+ fullContent.append(titleContent);
+ // 添加分隔符,用于区分标题和正文
+ fullContent.append("\n---\n");
+ // 再保存正文
+ android.text.SpannableStringBuilder noteContent = new android.text.SpannableStringBuilder(mNoteEditor.getText());
+ fullContent.append(noteContent);
+
+ // 检查是否与栈顶内容相同,避免重复保存
+ if (!mUndoStack.isEmpty()) {
+ String topContent = mUndoStack.peek().toString();
+ if (topContent.equals(fullContent.toString())) {
+ return;
+ }
+ }
+
+ // 保存到撤回栈
+ mUndoStack.push(fullContent);
+
+ // 限制撤回栈的大小
+ if (mUndoStack.size() > 20) {
+ mUndoStack.remove(0);
+ }
+ }
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+ @Override
+ public void afterTextChanged(android.text.Editable s) {
+ // 更新字数统计
+ updateCharacterCount();
+
+ // 实时保存标题
+ mWorkingNote.setWorkingTitle(mNoteTitleEditor.getText().toString());
+ }
+ };
+
+ // 为内容编辑器添加监听器
+ mNoteEditor.addTextChangedListener(textWatcher);
+
+ // 为标题编辑器添加监听器
+ mNoteTitleEditor.addTextChangedListener(textWatcher);
+
+ // 为NoteEditText添加MovementMethod,确保链接可以被点击
+ mNoteEditor.setMovementMethod(android.text.method.LinkMovementMethod.getInstance());
+
+
}
@Override
@@ -418,7 +558,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {
- mWorkingNote.getWidgetId()
+ mWorkingNote.getWidgetId()
});
sendBroadcast(intent);
@@ -478,20 +618,35 @@ public class NoteEditActivity extends Activity implements OnClickListener,
View.VISIBLE);
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
+ // 设置字数统计布局的背景颜色与便签一致
+ mCharacterCountLayout.setBackgroundResource(mWorkingNote.getBgColorResId());
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
+ Log.d(TAG, "onPrepareOptionsMenu called");
if (isFinishing()) {
return true;
}
clearSettingState();
menu.clear();
+
if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) {
+ Log.d(TAG, "Inflating call_note_edit menu");
getMenuInflater().inflate(R.menu.call_note_edit, menu);
} else {
+ Log.d(TAG, "Inflating note_edit menu");
getMenuInflater().inflate(R.menu.note_edit, menu);
}
+
+ // 检查menu_change_background菜单项是否存在
+ MenuItem changeBackgroundItem = menu.findItem(R.id.menu_change_background);
+ if (changeBackgroundItem != null) {
+ Log.d(TAG, "menu_change_background item found in menu");
+ } else {
+ Log.d(TAG, "menu_change_background item NOT found in menu");
+ }
+
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode);
} else {
@@ -507,22 +662,81 @@ public class NoteEditActivity extends Activity implements OnClickListener,
@Override
public boolean onOptionsItemSelected(MenuItem item) {
+ Log.d(TAG, "onOptionsItemSelected called, item id: " + item.getItemId());
switch (item.getItemId()) {
case R.id.menu_new_note:
createNewNote();
break;
+ case R.id.menu_undo:
+ // 实现撤回功能,支持标题和正文的富文本格式
+ if (!mUndoStack.isEmpty()) {
+ try {
+ mIsUndoing = true;
+ android.text.SpannableStringBuilder previousFullContent = mUndoStack.pop();
+
+ // 解析保存的完整内容,分离标题和正文
+ String fullContentStr = previousFullContent.toString();
+ String titleContentStr = fullContentStr;
+ String noteContentStr = "";
+
+ // 查找分隔符
+ int separatorIndex = fullContentStr.indexOf("\n---\n");
+ if (separatorIndex != -1) {
+ titleContentStr = fullContentStr.substring(0, separatorIndex);
+ noteContentStr = fullContentStr.substring(separatorIndex + 5); // 跳过分隔符
+ }
+
+ // 恢复标题(带富文本格式)
+ android.text.SpannableStringBuilder titleContent = new android.text.SpannableStringBuilder(previousFullContent);
+ titleContent = new android.text.SpannableStringBuilder(titleContent.subSequence(0, titleContentStr.length()));
+ mNoteTitleEditor.setText(titleContent, android.widget.TextView.BufferType.SPANNABLE);
+
+ // 恢复正文(带富文本格式)
+ android.text.SpannableStringBuilder noteContent = new android.text.SpannableStringBuilder(previousFullContent);
+ if (separatorIndex != -1) {
+ noteContent = new android.text.SpannableStringBuilder(noteContent.subSequence(separatorIndex + 5, previousFullContent.length()));
+ }
+ mNoteEditor.setText(noteContent, android.widget.TextView.BufferType.SPANNABLE);
+
+ // 更新工作便签和字数统计
+ mWorkingNote.setWorkingTitle(titleContentStr);
+ mWorkingNote.setWorkingText(noteContentStr);
+ updateCharacterCount();
+ } finally {
+ mIsUndoing = false;
+ }
+ }
+ break;
case R.id.menu_delete:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.alert_title_delete));
builder.setIcon(android.R.drawable.ic_dialog_alert);
- builder.setMessage(getString(R.string.alert_message_delete_note));
- builder.setPositiveButton(android.R.string.ok,
+ builder.setMessage("请选择删除方式");
+ builder.setPositiveButton("移动到回收站",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
deleteCurrentNote();
finish();
}
});
+ builder.setNeutralButton("直接删除",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ // 直接彻底删除
+ if (mWorkingNote.existInDatabase()) {
+ HashSet ids = new HashSet();
+ long id = mWorkingNote.getNoteId();
+ if (id != Notes.ID_ROOT_FOLDER) {
+ ids.add(id);
+ } else {
+ Log.d(TAG, "Wrong note id, should not happen");
+ }
+ DataUtils.batchDeleteNotes(getContentResolver(), ids);
+ }
+ mWorkingNote.markDeleted(true);
+ finish();
+ }
+ });
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
break;
@@ -547,6 +761,86 @@ public class NoteEditActivity extends Activity implements OnClickListener,
case R.id.menu_delete_remind:
mWorkingNote.setAlertDate(0, false);
break;
+ case R.id.menu_add_picture:
+ addPicture();
+ break;
+ case R.id.menu_change_background:
+ changeBackground();
+ break;
+ case R.id.menu_format_bold:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置加粗
+ mNoteEditor.setBold();
+ break;
+ case R.id.menu_format_italic:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置斜体
+ mNoteEditor.setItalic();
+ break;
+ case R.id.menu_format_underline:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 切换下划线
+ mNoteEditor.toggleUnderline();
+ break;
+ case R.id.menu_format_strikethrough:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 切换删除线
+ mNoteEditor.toggleStrikethrough();
+ break;
+ case R.id.menu_format_text_color:
+ // 设置文字颜色(内部已保存状态)
+ showTextColorPicker();
+ break;
+ case R.id.menu_format_bg_color:
+ // 设置文字背景颜色(内部已保存状态)
+ showTextBackgroundColorPicker();
+ break;
+ case R.id.menu_format_align_left:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置左对齐
+ mNoteEditor.setAlignLeft();
+ break;
+ case R.id.menu_format_align_center:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置居中对齐
+ mNoteEditor.setAlignCenter();
+ break;
+ case R.id.menu_format_align_right:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置右对齐
+ mNoteEditor.setAlignRight();
+ break;
+ case R.id.menu_format_align_justify:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置两端对齐
+ mNoteEditor.setAlignJustify();
+ break;
+ case R.id.menu_format_font_large:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置大字号
+ mNoteEditor.setTextSize(1.2f);
+ break;
+ case R.id.menu_format_font_normal:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置正常字号
+ mNoteEditor.setTextSize(1.0f);
+ break;
+ case R.id.menu_format_font_small:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置小字号
+ mNoteEditor.setTextSize(0.8f);
+ break;
default:
break;
}
@@ -562,6 +856,93 @@ public class NoteEditActivity extends Activity implements OnClickListener,
});
d.show();
}
+
+ /**
+ * 保存当前便签状态到撤回栈(支持富文本样式修改)
+ */
+ private void saveCurrentStateToUndoStack() {
+ if (mIsUndoing) return;
+
+ // 保存完整的便签内容,包括标题和正文(支持富文本)
+ android.text.SpannableStringBuilder fullContent = new android.text.SpannableStringBuilder();
+
+ // 先保存标题
+ android.text.SpannableStringBuilder titleContent = new android.text.SpannableStringBuilder(mNoteTitleEditor.getText());
+ fullContent.append(titleContent);
+ // 添加分隔符,用于区分标题和正文
+ fullContent.append("\n---\n");
+ // 再保存正文
+ android.text.SpannableStringBuilder noteContent = new android.text.SpannableStringBuilder(mNoteEditor.getText());
+ fullContent.append(noteContent);
+
+ // 检查是否与栈顶内容相同,避免重复保存
+ if (!mUndoStack.isEmpty()) {
+ String topContent = mUndoStack.peek().toString();
+ if (topContent.equals(fullContent.toString())) {
+ return;
+ }
+ }
+
+ // 保存到撤回栈
+ mUndoStack.push(fullContent);
+
+ // 限制撤回栈的大小
+ if (mUndoStack.size() > 20) {
+ mUndoStack.remove(0);
+ }
+ }
+
+ /**
+ * 显示文字颜色选择器
+ */
+ private void showTextColorPicker() {
+ // 创建颜色选择器对话框
+ final int[] colors = {android.graphics.Color.RED, android.graphics.Color.BLUE,
+ android.graphics.Color.GREEN, android.graphics.Color.BLACK,
+ android.graphics.Color.GRAY, android.graphics.Color.YELLOW,
+ android.graphics.Color.MAGENTA, android.graphics.Color.CYAN};
+ final String[] colorNames = {"红色", "蓝色", "绿色", "黑色",
+ "灰色", "黄色", "紫色", "青色"};
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("选择文字颜色");
+ builder.setItems(colorNames, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 应用颜色
+ mNoteEditor.setTextColor(colors[which]);
+ }
+ });
+ builder.show();
+ }
+
+ /**
+ * 显示文字背景颜色选择器
+ */
+ private void showTextBackgroundColorPicker() {
+ // 创建背景颜色选择器对话框
+ final int[] colors = {android.graphics.Color.RED, android.graphics.Color.BLUE,
+ android.graphics.Color.GREEN, android.graphics.Color.YELLOW,
+ android.graphics.Color.MAGENTA, android.graphics.Color.CYAN,
+ android.graphics.Color.WHITE, android.graphics.Color.GRAY};
+ final String[] colorNames = {"红色", "蓝色", "绿色", "黄色",
+ "紫色", "青色", "白色", "灰色"};
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("选择文字背景颜色");
+ builder.setItems(colorNames, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 应用背景颜色
+ mNoteEditor.setTextBackgroundColor(colors[which]);
+ }
+ });
+ builder.show();
+ }
/**
* Share note to apps that support {@link Intent#ACTION_SEND} action
@@ -595,14 +976,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
} else {
Log.d(TAG, "Wrong note id, should not happen");
}
- if (!isSyncMode()) {
- if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) {
- Log.e(TAG, "Delete Note error");
- }
- } else {
- if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) {
- Log.e(TAG, "Move notes to trash folder error, should not happens");
- }
+ // 无论是否同步,都将便签移动到回收站
+ if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) {
+ Log.e(TAG, "Move notes to trash folder error, should not happens");
}
}
mWorkingNote.markDeleted(true);
@@ -623,13 +999,24 @@ public class NoteEditActivity extends Activity implements OnClickListener,
if (mWorkingNote.getNoteId() > 0) {
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId()));
- PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
+ // 使用适当的PendingIntent flag,确保在Android 12+中正常工作
+ int flags = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S ? PendingIntent.FLAG_IMMUTABLE : 0;
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, flags);
AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE));
showAlertHeader();
if(!set) {
alarmManager.cancel(pendingIntent);
+ // 提示用户提醒已取消
+ showToast(R.string.note_alert_canceled);
} else {
- alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent);
+ // 在Android 6.0+中,使用setExact方法以确保准确的提醒时间
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+ alarmManager.setExact(AlarmManager.RTC_WAKEUP, date, pendingIntent);
+ } else {
+ alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent);
+ }
+ // 提示用户提醒设置成功
+ showToast(R.string.note_alert_set_success);
}
} else {
/**
@@ -709,20 +1096,92 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
private Spannable getHighlightQueryResult(String fullText, String userQuery) {
- SpannableString spannable = new SpannableString(fullText == null ? "" : fullText);
+ if (TextUtils.isEmpty(fullText)) {
+ return new SpannableString("");
+ }
+
+ // 创建一个 SpannableStringBuilder 用于构建最终的 Spannable
+ SpannableStringBuilder builder = new SpannableStringBuilder(fullText);
+
+ // 正则表达式匹配 [IMAGE]imageUri[/IMAGE] 格式的图片标记
+ Pattern imagePattern = Pattern.compile("\\[IMAGE\\](.*?)\\[/IMAGE\\]");
+ Matcher imageMatcher = imagePattern.matcher(fullText);
+
+ // 倒序处理,避免替换后影响后续匹配位置
+ ArrayList imageInfos = new ArrayList<>();
+ while (imageMatcher.find()) {
+ String imageUriStr = imageMatcher.group(1);
+ imageInfos.add(new ImageInfo(imageMatcher.start(), imageMatcher.end(), imageUriStr));
+ }
+
+ // 倒序处理图片标记
+ for (int i = imageInfos.size() - 1; i >= 0; i--) {
+ ImageInfo info = imageInfos.get(i);
+ try {
+ // 获取图片URI
+ Uri imageUri = Uri.parse(info.uri);
+
+ // 获取压缩后的 bitmap
+ Bitmap bitmap = getCompressedBitmap(imageUri);
+ if (bitmap != null) {
+ // 计算合适的图片大小(相对于文本大小)
+ float scale = getResources().getDisplayMetrics().density;
+
+ // 使用屏幕宽度,更可靠
+ int screenWidth = getResources().getDisplayMetrics().widthPixels;
+ int maxImageWidth = (int) (screenWidth * 0.8); // 80% of screen width
+ int maxImageHeight = (int) (500 * scale); // 500dp max height
+
+ // 调整图片大小
+ Bitmap scaledBitmap = getScaledBitmap(bitmap, maxImageWidth, maxImageHeight);
+
+ // 创建 ImageSpan(使用兼容旧版本的构造方式)
+ ImageSpan imageSpan = new ImageSpan(this, scaledBitmap);
+
+ // 不替换图片标记,直接在图片标记上应用 ImageSpan
+ // 这样可以保持文本内容不变,避免 getWorkingText 方法中的问题
+ builder.setSpan(imageSpan, info.start, info.end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ // 回收临时 bitmap
+ if (scaledBitmap != bitmap) {
+ bitmap.recycle();
+ }
+ }
+ // 如果图片加载失败,保持原始标记不变
+ } catch (Exception e) {
+ // 发生异常时,保持原始标记不变
+ e.printStackTrace();
+ }
+ }
+
+ // 处理搜索高亮
if (!TextUtils.isEmpty(userQuery)) {
mPattern = Pattern.compile(userQuery);
- Matcher m = mPattern.matcher(fullText);
+ Matcher m = mPattern.matcher(builder);
int start = 0;
while (m.find(start)) {
- spannable.setSpan(
+ builder.setSpan(
new BackgroundColorSpan(this.getResources().getColor(
R.color.user_query_highlight)), m.start(), m.end(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
start = m.end();
}
}
- return spannable;
+
+ return builder;
+ }
+
+ // 用于存储图片信息的内部类
+ private static class ImageInfo {
+ int start;
+ int end;
+ String uri;
+
+ ImageInfo(int start, int end, String uri) {
+ this.start = start;
+ this.end = end;
+ this.uri = uri;
+ }
}
private View getListItem(String item, int index) {
@@ -753,6 +1212,21 @@ public class NoteEditActivity extends Activity implements OnClickListener,
edit.setOnTextViewChangeListener(this);
edit.setIndex(index);
edit.setText(getHighlightQueryResult(item, mUserQuery));
+
+ // 为清单模式的编辑框添加文本变化监听器,用于更新字数统计
+ edit.addTextChangedListener(new android.text.TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+ @Override
+ public void afterTextChanged(android.text.Editable s) {
+ updateCharacterCount();
+ }
+ });
+
return view;
}
@@ -780,6 +1254,8 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mEditTextList.setVisibility(View.GONE);
mNoteEditor.setVisibility(View.VISIBLE);
}
+ // 模式切换后更新字数统计
+ updateCharacterCount();
}
private boolean getWorkingText() {
@@ -799,8 +1275,13 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
}
mWorkingNote.setWorkingText(sb.toString());
+ // 确保便签模式被正确设置
+ mWorkingNote.setCheckListMode(TextNote.MODE_CHECK_LIST);
} else {
+ // 对于普通文本模式,获取编辑器中的文本内容
mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
+ // 确保便签模式被正确设置
+ mWorkingNote.setCheckListMode(0); // 0 表示普通文本模式
}
return hasChecked;
}
@@ -820,6 +1301,31 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
return saved;
}
+
+ /**
+ * 更新字数统计显示
+ */
+ private void updateCharacterCount() {
+ int count = 0;
+ if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
+ // 清单模式:遍历所有编辑框计算总字数
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < mEditTextList.getChildCount(); i++) {
+ View view = mEditTextList.getChildAt(i);
+ NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
+ if (!TextUtils.isEmpty(edit.getText())) {
+ sb.append(edit.getText()).append("\n");
+ }
+ }
+ count = sb.length();
+ } else {
+ // 普通文本模式:直接获取编辑器内容长度
+ count = mNoteEditor.getText().length();
+ }
+
+ // 更新显示
+ mCharacterCountView.setText(getString(R.string.character_count, count));
+ }
private void sendToDesktop() {
/**
@@ -870,4 +1376,375 @@ public class NoteEditActivity extends Activity implements OnClickListener,
private void showToast(int resId, int duration) {
Toast.makeText(this, resId, duration).show();
}
+
+ private static final int REQUEST_CODE_PICK_IMAGE = 100;
+ private static final int REQUEST_CODE_CAMERA = 101;
+ private static final int REQUEST_CODE_PICK_BACKGROUND = 102;
+ private static final int REQUEST_CODE_TAKE_BACKGROUND = 103;
+ private static final int MAX_IMAGE_SIZE = 1024; // 限制图片大小为1024x1024
+
+ private void addPicture() {
+ // 显示选择对话框:从相册导入或相机拍摄
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.menu_add_picture);
+ String[] options = {"从相册导入", "相机拍摄"};
+ builder.setItems(options, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == 0) {
+ // 从相册导入
+ Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ intent.setType("image/*");
+ startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE);
+ } else {
+ // 相机拍摄
+ Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
+ // 使用简单的方式启动相机,让系统处理图片存储
+ startActivityForResult(intent, REQUEST_CODE_CAMERA);
+ }
+ }
+ });
+ builder.show();
+ }
+
+ /**
+ * 更换背景方法
+ */
+ private void changeBackground() {
+ Log.d(TAG, "changeBackground method called");
+
+ // 先显示一个Toast,确认方法被调用
+ Toast.makeText(this, "正在打开更换背景选项", Toast.LENGTH_SHORT).show();
+
+ // 直接使用runOnUiThread确保在主线程执行
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Log.d(TAG, "Creating AlertDialog.Builder");
+
+ // 使用默认主题,避免样式问题
+ AlertDialog.Builder builder = new AlertDialog.Builder(NoteEditActivity.this);
+ builder.setTitle("更换背景");
+ builder.setIcon(android.R.drawable.ic_dialog_info); // 添加图标,让对话框更明显
+ builder.setCancelable(true);
+
+ String[] options = {"从相册选择", "拍照"};
+ builder.setItems(options, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Log.d(TAG, "Dialog item clicked: " + which);
+ dialog.dismiss();
+
+ if (which == 0) {
+ // 从相册选择
+ Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ intent.setType("image/*");
+ startActivityForResult(intent, REQUEST_CODE_PICK_BACKGROUND);
+ } else {
+ // 拍照
+ Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
+ // 使用简单的方式启动相机,让系统处理图片存储
+ startActivityForResult(intent, REQUEST_CODE_TAKE_BACKGROUND);
+ }
+ }
+ });
+
+ // 添加取消按钮
+ builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+
+ Log.d(TAG, "Creating dialog");
+ AlertDialog dialog = builder.create();
+
+ // 设置对话框属性,确保显示在最上层
+ android.view.Window window = dialog.getWindow();
+ if (window != null) {
+ // 移除TYPE_APPLICATION_OVERLAY设置,因为在Android 8.0+中需要特殊权限
+ // 设置窗口属性,确保不会被遮挡
+ window.setFlags(android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
+ android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
+ window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ // 设置对话框背景为白色,确保可见
+ window.setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(android.graphics.Color.WHITE));
+ }
+
+ // 设置对话框可取消
+ dialog.setCanceledOnTouchOutside(true);
+ dialog.setCancelable(true);
+
+ Log.d(TAG, "Showing dialog");
+ // 确保对话框能够显示
+ dialog.show();
+ Log.d(TAG, "Dialog shown successfully");
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error in changeBackground: " + e.getMessage(), e);
+ // 显示错误信息
+ Toast.makeText(NoteEditActivity.this, "更换背景失败: " + e.getMessage(), Toast.LENGTH_LONG).show();
+ }
+ }
+ });
+ }
+
+
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK) {
+ android.net.Uri imageUri = null;
+
+ // 处理从相册导入图片
+ if (requestCode == REQUEST_CODE_PICK_IMAGE) {
+ if (data != null && data.getData() != null) {
+ imageUri = data.getData();
+ }
+ // 处理图片URI(添加图片到便签)
+ if (imageUri != null) {
+ handleAddPicture(imageUri);
+ }
+ }
+ // 处理相机拍摄图片
+ else if (requestCode == REQUEST_CODE_CAMERA) {
+ // 相机拍摄的图片URI处理
+ if (data != null && data.getData() != null) {
+ // 有些相机应用会直接返回URI
+ imageUri = data.getData();
+ } else {
+ // 从媒体库获取最新拍摄的图片
+ try {
+ android.database.Cursor cursor = getContentResolver().query(
+ android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ new String[]{android.provider.MediaStore.Images.Media._ID},
+ null, null, "DATE_ADDED DESC");
+ if (cursor != null && cursor.moveToFirst()) {
+ long id = cursor.getLong(cursor.getColumnIndex(android.provider.MediaStore.Images.Media._ID));
+ imageUri = android.content.ContentUris.withAppendedId(
+ android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
+ cursor.close();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error getting camera image: " + e.getMessage());
+ }
+ }
+ // 处理图片URI(添加图片到便签)
+ if (imageUri != null) {
+ handleAddPicture(imageUri);
+ }
+ }
+ // 处理从相册选择背景图片
+ else if (requestCode == REQUEST_CODE_PICK_BACKGROUND) {
+ if (data != null && data.getData() != null) {
+ imageUri = data.getData();
+ // 设置背景图片
+ setBackgroundImage(imageUri);
+ }
+ }
+ // 处理拍照更换背景
+ else if (requestCode == REQUEST_CODE_TAKE_BACKGROUND) {
+ // 相机拍摄的图片URI处理
+ if (data != null && data.getData() != null) {
+ // 有些相机应用会直接返回URI
+ imageUri = data.getData();
+ } else {
+ // 从媒体库获取最新拍摄的图片
+ try {
+ android.database.Cursor cursor = getContentResolver().query(
+ android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ new String[]{android.provider.MediaStore.Images.Media._ID},
+ null, null, "DATE_ADDED DESC");
+ if (cursor != null && cursor.moveToFirst()) {
+ long id = cursor.getLong(cursor.getColumnIndex(android.provider.MediaStore.Images.Media._ID));
+ imageUri = android.content.ContentUris.withAppendedId(
+ android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
+ cursor.close();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error getting camera image: " + e.getMessage());
+ }
+ }
+ // 设置背景图片
+ if (imageUri != null) {
+ setBackgroundImage(imageUri);
+ }
+ }
+ }
+ }
+
+ /**
+ * 处理添加图片到便签的逻辑
+ */
+ private void handleAddPicture(android.net.Uri imageUri) {
+ try {
+ // 构建图片标记
+ String imageTag = "\n[IMAGE]" + imageUri.toString() + "[/IMAGE]\n";
+
+ // 直接使用 mWorkingNote.getContent() 获取当前内容,它包含完整的图片标记
+ String currentContent = mWorkingNote.getContent();
+
+ // 构建新内容
+ String newContent;
+ if (currentContent.isEmpty()) {
+ // 空便签情况:直接添加图片标记,不需要前面的换行符
+ newContent = "[IMAGE]" + imageUri.toString() + "[/IMAGE]\n";
+ } else {
+ // 非空便签情况:在末尾添加图片标记
+ newContent = currentContent + imageTag;
+ }
+
+ // 更新便签内容
+ mWorkingNote.setWorkingText(newContent);
+
+ // 刷新编辑器显示
+ if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
+ switchToListMode(mWorkingNote.getContent());
+ } else {
+ mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
+ mNoteEditor.setSelection(mNoteEditor.getText().length());
+ }
+
+ // 保存到数据库
+ saveNote();
+
+ // 使用正确的Toast提示
+ Toast.makeText(NoteEditActivity.this, "图片已添加到便签", Toast.LENGTH_SHORT).show();
+ } catch (Exception e) {
+ Log.e(TAG, "Error handling image: " + e.getMessage());
+ showToast(R.string.error_sdcard_export, Toast.LENGTH_SHORT);
+ }
+ }
+
+ /**
+ * 设置背景图片
+ */
+ private void setBackgroundImage(android.net.Uri imageUri) {
+ try {
+ // 获取图片的真实路径
+ String imagePath = getRealPathFromUri(imageUri);
+ if (imagePath != null) {
+ // 将图片路径保存到SharedPreferences
+ SharedPreferences sharedPreferences = getSharedPreferences("NoteSettings", Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putString("background_image", imagePath);
+ editor.apply();
+
+ // 加载图片并设置背景
+ android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeFile(imagePath);
+ if (bitmap != null) {
+ // 创建BitmapDrawable对象
+ android.graphics.drawable.BitmapDrawable drawable = new android.graphics.drawable.BitmapDrawable(getResources(), bitmap);
+ // 设置背景图片
+ mNoteEditorPanel.setBackground(drawable);
+ // 提示用户背景已更换
+ Toast.makeText(NoteEditActivity.this, "背景已更换", Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(NoteEditActivity.this, "图片加载失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error setting background image: " + e.getMessage());
+ Toast.makeText(NoteEditActivity.this, "更换背景失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 从URI获取真实的文件路径
+ */
+ private String getRealPathFromUri(android.net.Uri uri) {
+ String path = null;
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
+ // Android 4.4及以上版本
+ if ("content".equalsIgnoreCase(uri.getScheme())) {
+ String[] projection = {android.provider.MediaStore.Images.Media.DATA};
+ android.database.Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(uri, projection, null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ int column_index = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA);
+ path = cursor.getString(column_index);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ } else if ("file".equalsIgnoreCase(uri.getScheme())) {
+ path = uri.getPath();
+ }
+ } else {
+ // Android 4.4以下版本
+ String[] projection = {android.provider.MediaStore.Images.Media.DATA};
+ android.database.Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(uri, projection, null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ int column_index = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA);
+ path = cursor.getString(column_index);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ return path;
+ }
+
+ private android.graphics.Bitmap getCompressedBitmap(android.net.Uri uri) throws Exception {
+ // 获取图片的宽高
+ android.content.ContentResolver resolver = getContentResolver();
+ android.graphics.BitmapFactory.Options options = new android.graphics.BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ android.graphics.BitmapFactory.decodeStream(resolver.openInputStream(uri), null, options);
+ int width = options.outWidth;
+ int height = options.outHeight;
+
+ // 计算缩放比例
+ int scale = 1;
+ while (width / scale > MAX_IMAGE_SIZE || height / scale > MAX_IMAGE_SIZE) {
+ scale *= 2;
+ }
+
+ // 加载压缩后的图片
+ options.inJustDecodeBounds = false;
+ options.inSampleSize = scale;
+ return android.graphics.BitmapFactory.decodeStream(resolver.openInputStream(uri), null, options);
+ }
+
+ /**
+ * 根据最大宽度和高度缩放图片,保持原始宽高比
+ */
+ private android.graphics.Bitmap getScaledBitmap(android.graphics.Bitmap bitmap, int maxWidth, int maxHeight) {
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+
+ // 计算缩放比例
+ float scale = 1.0f;
+ if (width > maxWidth || height > maxHeight) {
+ float widthScale = (float) maxWidth / width;
+ float heightScale = (float) maxHeight / height;
+ scale = Math.min(widthScale, heightScale);
+ }
+
+ // 如果不需要缩放,直接返回原始 bitmap
+ if (scale == 1.0f) {
+ return bitmap;
+ }
+
+ // 计算新的宽度和高度
+ int newWidth = (int) (width * scale);
+ int newHeight = (int) (height * scale);
+
+ // 创建缩放后的 bitmap
+ return android.graphics.Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
+ }
}
diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java b/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java
index 2afe2a8..73a9af0 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java
@@ -17,12 +17,26 @@
package net.micode.notes.ui;
import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Typeface;
import android.graphics.Rect;
import android.text.Layout;
import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
import android.text.Spanned;
+import android.text.TextPaint;
import android.text.TextUtils;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ClickableSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
+import android.text.style.UnderlineSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextMenu;
@@ -30,6 +44,7 @@ import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
+import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import net.micode.notes.R;
@@ -101,24 +116,32 @@ public class NoteEditText extends EditText {
@Override
public boolean onTouchEvent(MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
-
- int x = (int) event.getX();
- int y = (int) event.getY();
- x -= getTotalPaddingLeft();
- y -= getTotalPaddingTop();
- x += getScrollX();
- y += getScrollY();
-
- Layout layout = getLayout();
- int line = layout.getLineForVertical(y);
- int off = layout.getOffsetForHorizontal(line, x);
- Selection.setSelection(getText(), off);
- break;
+ // 确保获得焦点,无论是否有内容
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ if (!hasFocus()) {
+ requestFocus();
+ }
}
-
- return super.onTouchEvent(event);
+
+ // 调用父类方法处理事件
+ boolean handled = super.onTouchEvent(event);
+
+ // 处理点击事件
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ // 无论是否有layout,都确保光标在正确位置
+ setSelection(getText().length());
+
+ // 显示软键盘
+ Context context = getContext();
+ if (context != null) {
+ InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT);
+ }
+ }
+ }
+
+ return handled;
}
@Override
@@ -214,4 +237,201 @@ public class NoteEditText extends EditText {
}
super.onCreateContextMenu(menu);
}
+
+ /**
+ * 设置文字加粗
+ */
+ public void setBold() {
+ toggleStyle(Typeface.BOLD);
+ }
+
+ /**
+ * 设置文字斜体
+ */
+ public void setItalic() {
+ toggleStyle(Typeface.ITALIC);
+ }
+
+ /**
+ * 设置文字粗斜体
+ */
+ public void setBoldItalic() {
+ applyStyle(Typeface.BOLD_ITALIC);
+ }
+
+ /**
+ * 设置文字正常
+ */
+ public void setNormal() {
+ applyStyle(Typeface.NORMAL);
+ }
+
+ /**
+ * 切换下划线
+ */
+ public void toggleUnderline() {
+ toggleSpan(UnderlineSpan.class);
+ }
+
+ /**
+ * 切换删除线
+ */
+ public void toggleStrikethrough() {
+ toggleSpan(StrikethroughSpan.class);
+ }
+
+ /**
+ * 设置文字颜色
+ */
+ public void setTextColor(int color) {
+ applySpan(new ForegroundColorSpan(color));
+ }
+
+ /**
+ * 设置文字背景颜色
+ */
+ public void setTextBackgroundColor(int color) {
+ applySpan(new BackgroundColorSpan(color));
+ }
+
+ /**
+ * 设置字号大小
+ */
+ public void setTextSize(float size) {
+ // 将字号稍微放大一些
+ float adjustedSize = size * 1.1f;
+ applySpan(new RelativeSizeSpan(adjustedSize));
+ }
+
+ /**
+ * 设置文字左对齐
+ */
+ public void setAlignLeft() {
+ setGravity(android.view.Gravity.LEFT);
+ }
+
+ /**
+ * 设置文字居中对齐
+ */
+ public void setAlignCenter() {
+ setGravity(android.view.Gravity.CENTER);
+ }
+
+ /**
+ * 设置文字右对齐
+ */
+ public void setAlignRight() {
+ setGravity(android.view.Gravity.RIGHT);
+ }
+
+ /**
+ * 设置文字两端对齐
+ */
+ public void setAlignJustify() {
+ setGravity(android.view.Gravity.FILL_HORIZONTAL);
+ }
+
+ /**
+ * 应用样式
+ */
+ private void applyStyle(int style) {
+ Spannable spannable = getText();
+ if (spannable == null) return;
+
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ if (start == end) return;
+
+ StyleSpan[] spans = spannable.getSpans(start, end, StyleSpan.class);
+ for (StyleSpan span : spans) {
+ spannable.removeSpan(span);
+ }
+
+ spannable.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ setSelection(start, end);
+ }
+
+ /**
+ * 切换样式
+ */
+ private void toggleStyle(int style) {
+ Spannable spannable = getText();
+ if (spannable == null) return;
+
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ if (start == end) return;
+
+ // 检查当前样式
+ boolean hasStyle = false;
+ StyleSpan[] spans = spannable.getSpans(start, end, StyleSpan.class);
+ for (StyleSpan span : spans) {
+ if (span.getStyle() == style) {
+ hasStyle = true;
+ spannable.removeSpan(span);
+ }
+ }
+
+ if (!hasStyle) {
+ // 没有该样式,添加
+ spannable.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ setSelection(start, end);
+ }
+
+ /**
+ * 应用Span
+ */
+ private void applySpan(Object span) {
+ Spannable spannable = getText();
+ if (spannable == null) return;
+
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ if (start == end) return;
+
+ spannable.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ setSelection(start, end);
+ }
+
+ /**
+ * 切换Span
+ */
+ private void toggleSpan(Class> spanClass) {
+ Spannable spannable = getText();
+ if (spannable == null) return;
+
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ if (start == end) return;
+
+ // 检查当前是否有该Span
+ boolean hasSpan = false;
+ try {
+ Object[] spans = spannable.getSpans(start, end, spanClass);
+ for (Object span : spans) {
+ hasSpan = true;
+ spannable.removeSpan(span);
+ }
+
+ if (!hasSpan) {
+ // 没有该Span,添加
+ try {
+ // 创建该Span的实例
+ Object newSpan = spanClass.getDeclaredConstructor().newInstance();
+ spannable.setSpan(newSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } catch (Exception e) {
+ // 捕获所有异常,避免崩溃
+ e.printStackTrace();
+ }
+ }
+ } catch (Exception e) {
+ // 捕获所有异常,避免崩溃
+ e.printStackTrace();
+ }
+
+ setSelection(start, end);
+ }
}
+
diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java b/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java
index 0f5a878..a8a3a4b 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java
@@ -37,12 +37,17 @@ public class NoteItemData {
NoteColumns.NOTES_COUNT,
NoteColumns.PARENT_ID,
NoteColumns.SNIPPET,
+ NoteColumns.TITLE,
NoteColumns.TYPE,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
+ NoteColumns.PINNED,
+ NoteColumns.SORT_ORDER,
+ NoteColumns.LOCKED,
+ NoteColumns.PUBLIC,
};
- private static final int ID_COLUMN = 0;
+ public static final int ID_COLUMN = 0;
private static final int ALERTED_DATE_COLUMN = 1;
private static final int BG_COLOR_ID_COLUMN = 2;
private static final int CREATED_DATE_COLUMN = 3;
@@ -51,9 +56,14 @@ public class NoteItemData {
private static final int NOTES_COUNT_COLUMN = 6;
private static final int PARENT_ID_COLUMN = 7;
private static final int SNIPPET_COLUMN = 8;
- private static final int TYPE_COLUMN = 9;
- private static final int WIDGET_ID_COLUMN = 10;
- private static final int WIDGET_TYPE_COLUMN = 11;
+ private static final int TITLE_COLUMN = 9;
+ private static final int TYPE_COLUMN = 10;
+ private static final int WIDGET_ID_COLUMN = 11;
+ private static final int WIDGET_TYPE_COLUMN = 12;
+ private static final int PINNED_COLUMN = 13;
+ private static final int SORT_ORDER_COLUMN = 14;
+ private static final int LOCKED_COLUMN = 15;
+ private static final int PUBLIC_COLUMN = 16;
private long mId;
private long mAlertDate;
@@ -64,9 +74,14 @@ public class NoteItemData {
private int mNotesCount;
private long mParentId;
private String mSnippet;
+ private String mTitle;
private int mType;
private int mWidgetId;
private int mWidgetType;
+ private boolean mPinned;
+ private int mSortOrder;
+ private boolean mLocked;
+ private boolean mPublic;
private String mName;
private String mPhoneNumber;
@@ -88,9 +103,14 @@ public class NoteItemData {
mSnippet = cursor.getString(SNIPPET_COLUMN);
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, "");
+ mTitle = cursor.getString(TITLE_COLUMN);
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
+ mPinned = (cursor.getInt(PINNED_COLUMN) > 0) ? true : false;
+ mSortOrder = cursor.getInt(SORT_ORDER_COLUMN);
+ mLocked = (cursor.getInt(LOCKED_COLUMN) > 0) ? true : false;
+ mPublic = (cursor.getInt(PUBLIC_COLUMN) > 0) ? true : false;
mPhoneNumber = "";
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
@@ -218,6 +238,26 @@ public class NoteItemData {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
+ public boolean isPinned() {
+ return mPinned;
+ }
+
+ public int getSortOrder() {
+ return mSortOrder;
+ }
+
+ public boolean isLocked() {
+ return mLocked;
+ }
+
+ public boolean isPublic() {
+ return mPublic;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);
}
diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java b/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java
index e843aec..00fcba8 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java
@@ -22,6 +22,7 @@ import android.app.Dialog;
import android.appwidget.AppWidgetManager;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
+import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
@@ -49,25 +50,37 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
import android.view.View.OnTouchListener;
+import android.view.KeyEvent;
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.FrameLayout;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.database.sqlite.SQLiteDatabase;
import net.micode.notes.R;
+import net.micode.notes.data.Messages;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
+import net.micode.notes.data.NotesDatabaseHelper;
+import net.micode.notes.data.Users;
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.tool.UserManager;
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;
@@ -76,6 +89,7 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.util.ArrayList;
import java.util.HashSet;
public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener {
@@ -92,7 +106,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction";
private enum ListEditState {
- NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER
+ NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER, TRASH_FOLDER
};
private ListEditState mState;
@@ -102,6 +116,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private NotesListAdapter mNotesListAdapter;
private ListView mNotesListView;
+ private GridView mNotesGridView;
private Button mAddNewNote;
@@ -119,18 +134,51 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private ModeCallback mModeCallBack;
+ // 搜索相关组件
+ private EditText mSearchEditText;
+ private ImageView mSearchImageView;
+ private ImageView mCancelImageView;
+ private LinearLayout mSearchBar;
+ private String mSearchQuery;
+ private boolean mIsSearching;
+
+ // 显示模式常量
+ private static final int DISPLAY_MODE_LIST = 0;
+ private static final int DISPLAY_MODE_GRID = 1;
+
+ // 当前显示模式
+ private int mDisplayMode = DISPLAY_MODE_LIST;
+
+ // 排序方式常量
+ private static final String SORT_BY_CREATE_DATE = NoteColumns.CREATED_DATE + " DESC";
+ private static final String SORT_BY_MODIFIED_DATE = NoteColumns.MODIFIED_DATE + " DESC";
+
+ // 当前排序方式
+ private String mCurrentSortOrder = SORT_BY_MODIFIED_DATE;
+
private static final String TAG = "NotesListActivity";
public static final int NOTES_LISTVIEW_SCROLL_RATE = 30;
private NoteItemData mFocusNoteDataItem;
+ // 拖拽相关变量
+ private boolean mIsDragging = false;
+ private int mDragStartPosition = -1;
+ private int mDragCurrentPosition = -1;
+ private View mDraggingView = null;
+ private int mDraggingViewHeight = 0;
+ private float mDragStartY = 0;
+ private float mDragOffsetY = 0;
+ private NoteItemData[] mDragTempData = null; // 拖拽过程中的临时数据列表
+
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 ("
+ 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)";
+ + NoteColumns.NOTES_COUNT + ">0)" + " OR ( "
+ + NoteColumns.ID + "=" + Notes.ID_TRASH_FOLER + ")"; // 始终显示回收站,不管里面有没有便签
private final static int REQUEST_CODE_OPEN_NODE = 102;
private final static int REQUEST_CODE_NEW_NODE = 103;
@@ -145,6 +193,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
* Insert an introduction when user firstly use this application
*/
setAppInfoFromRawRes();
+
+ // 加载保存的背景图片
+ loadSavedBackground();
}
@Override
@@ -153,6 +204,42 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
&& (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) {
mNotesListAdapter.changeCursor(null);
} else {
+ // 处理背景更换结果
+ Log.d(TAG, "onActivityResult called, requestCode: " + requestCode + ", resultCode: " + resultCode);
+
+ if (resultCode == RESULT_OK) {
+ String imagePath = null;
+
+ switch (requestCode) {
+ case 1: // 从相册选择
+ if (data != null && data.getData() != null) {
+ // 获取图片路径
+ imagePath = getPathFromUri(data.getData());
+ Log.d(TAG, "Image path from gallery: " + imagePath);
+ }
+ break;
+ case 2: // 拍照
+ if (data != null && data.getExtras() != null) {
+ // 获取拍照的图片
+ android.graphics.Bitmap bitmap = (android.graphics.Bitmap) data.getExtras().get("data");
+ // 保存图片到本地
+ imagePath = saveBitmap(bitmap);
+ Log.d(TAG, "Image path from camera: " + imagePath);
+ }
+ break;
+ }
+
+ if (imagePath != null) {
+ // 保存图片路径到SharedPreferences
+ SharedPreferences sharedPreferences = getSharedPreferences("background", MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putString("background_path", imagePath);
+ editor.apply();
+
+ // 设置背景
+ setBackground(imagePath);
+ }
+ }
super.onActivityResult(requestCode, resultCode, data);
}
}
@@ -163,7 +250,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
StringBuilder sb = new StringBuilder();
InputStream in = null;
try {
- in = getResources().openRawResource(R.raw.introduction);
+ in = getResources().openRawResource(R.raw.introduction);
if (in != null) {
InputStreamReader isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
@@ -218,6 +305,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
null, false);
mNotesListView.setOnItemClickListener(new OnListItemClickListener());
mNotesListView.setOnItemLongClickListener(this);
+ mNotesListView.setOnTouchListener(new OnDragTouchListener());
+
+ // 初始化宫格视图
+ mNotesGridView = (GridView) findViewById(R.id.notes_grid);
+ mNotesGridView.setOnItemClickListener(new OnListItemClickListener());
+ mNotesGridView.setOnItemLongClickListener(this);
+ mNotesGridView.setOnTouchListener(new OnDragTouchListener());
+
mNotesListAdapter = new NotesListAdapter(this);
mNotesListView.setAdapter(mNotesListAdapter);
mAddNewNote = (Button) findViewById(R.id.btn_new_note);
@@ -229,6 +324,70 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mTitleBar = (TextView) findViewById(R.id.tv_title_bar);
mState = ListEditState.NOTE_LIST;
mModeCallBack = new ModeCallback();
+
+ // 初始化搜索相关组件
+ mSearchBar = (LinearLayout) findViewById(R.id.search_bar);
+ mSearchEditText = (EditText) findViewById(R.id.et_search);
+ mSearchImageView = (ImageView) findViewById(R.id.iv_search);
+ mCancelImageView = (ImageView) findViewById(R.id.iv_cancel);
+
+ // 初始化省略号按钮
+ ImageView mMenuMoreImageView = (ImageView) findViewById(R.id.iv_menu_more);
+
+ // 设置搜索按钮点击事件
+ mSearchImageView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startSearch();
+ }
+ });
+
+ // 设置取消按钮点击事件
+ mCancelImageView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ cancelSearch();
+ }
+ });
+
+ // 设置省略号按钮点击事件
+ mMenuMoreImageView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showMoreOptionsMenu(v);
+ }
+ });
+
+ // 设置搜索框文本变化监听
+ mSearchEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ mSearchQuery = s.toString();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+
+ // 设置搜索框回车键监听
+ mSearchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_SEARCH ||
+ (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN)) {
+ performSearch();
+ return true;
+ }
+ return false;
+ }
+ });
+
+ mIsSearching = false;
}
private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener {
@@ -239,14 +398,63 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
getMenuInflater().inflate(R.menu.note_list_options, menu);
menu.findItem(R.id.delete).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.pin).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.unpin).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.lock).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.unlock).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.make_public).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.make_private).setOnMenuItemClickListener(this);
mMoveMenu = menu.findItem(R.id.move);
- if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER
- || DataUtils.getUserFolderCount(mContentResolver) == 0) {
+
+ // 根据便签状态显示/隐藏锁和解锁菜单项
+ MenuItem lockItem = menu.findItem(R.id.lock);
+ MenuItem unlockItem = menu.findItem(R.id.unlock);
+
+ if (mFocusNoteDataItem != null && mFocusNoteDataItem.isLocked()) {
+ // 便签已锁定,显示解锁选项,隐藏加锁选项
+ lockItem.setVisible(false);
+ unlockItem.setVisible(true);
+ } else {
+ // 便签未锁定,显示加锁选项,隐藏解锁选项
+ lockItem.setVisible(true);
+ unlockItem.setVisible(false);
+ }
+
+ // 根据便签状态显示/隐藏开放和取消开放菜单项
+ MenuItem makePublicItem = menu.findItem(R.id.make_public);
+ MenuItem makePrivateItem = menu.findItem(R.id.make_private);
+
+ boolean isPublic = mFocusNoteDataItem != null && mFocusNoteDataItem.isPublic();
+ makePublicItem.setVisible(!isPublic);
+ makePrivateItem.setVisible(isPublic);
+
+ // 在回收站中,显示"恢复"选项而不是"移动"选项
+ if (mState == ListEditState.TRASH_FOLDER) {
+ // 隐藏"移动"选项
mMoveMenu.setVisible(false);
+ // 添加"恢复"选项
+ MenuItem restoreMenu = menu.add(0, 100, 0, "恢复");
+ restoreMenu.setOnMenuItemClickListener(this);
} else {
- mMoveMenu.setVisible(true);
- mMoveMenu.setOnMenuItemClickListener(this);
+ // 不在回收站中,使用原来的逻辑
+ if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER
+ || DataUtils.getUserFolderCount(mContentResolver) == 0) {
+ mMoveMenu.setVisible(false);
+ } else {
+ mMoveMenu.setVisible(true);
+ mMoveMenu.setOnMenuItemClickListener(this);
+ }
}
+
+ // 根据便签是否已置顶,显示或隐藏置顶/取消置顶菜单项
+ boolean isPinned = mFocusNoteDataItem.isPinned();
+ menu.findItem(R.id.pin).setVisible(!isPinned);
+ menu.findItem(R.id.unpin).setVisible(isPinned);
+
+ // 添加"发送给"菜单项
+ MenuItem sendToMenu = menu.add(0, 101, 0, "发送给");
+ sendToMenu.setOnMenuItemClickListener(this);
+
mActionMode = mode;
mNotesListAdapter.setChoiceMode(true);
mNotesListView.setLongClickable(false);
@@ -307,7 +515,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
- boolean checked) {
+ boolean checked) {
mNotesListAdapter.setCheckedItem(position, checked);
updateMenu();
}
@@ -319,26 +527,275 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
return true;
}
+ // 获取选中的便签ID列表
+ HashSet selectedIds = mNotesListAdapter.getSelectedItemIds();
+
switch (item.getItemId()) {
+ case R.id.pin:
+ // 处理置顶逻辑
+ try {
+ // 获取当前最小的SORT_ORDER值,用于新置顶的便签
+ long currentTime = System.currentTimeMillis();
+
+ // 更新每个选中的便签
+ for (Long id : selectedIds) {
+ ContentValues pinValues = new ContentValues();
+ pinValues.put(NoteColumns.PINNED, 1);
+ // 设置为当前时间戳的负数,确保最新置顶的便签有最小的SORT_ORDER值,显示在最前面
+ pinValues.put(NoteColumns.SORT_ORDER, -currentTime);
+
+ int updatedRows = mContentResolver.update(
+ Notes.CONTENT_NOTE_URI,
+ pinValues,
+ "_id=?",
+ new String[]{String.valueOf(id)});
+ Log.d(TAG, "Updated " + updatedRows + " rows for pinning id: " + id);
+
+ // 为下一个便签设置更小的SORT_ORDER值
+ currentTime--;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error in pin operation: " + e.getMessage());
+ e.printStackTrace();
+ }
+
+ // 刷新列表
+ mModeCallBack.finishActionMode();
+ startAsyncNotesListQuery();
+ break;
+ case R.id.unpin:
+ // 处理取消置顶逻辑
+ try {
+ // 更新每个选中的便签
+ for (Long id : selectedIds) {
+ ContentValues unpinValues = new ContentValues();
+ unpinValues.put(NoteColumns.PINNED, 0);
+ // 重置SORT_ORDER为0,确保取消置顶后显示在下面
+ unpinValues.put(NoteColumns.SORT_ORDER, 0);
+
+ int updatedRows = mContentResolver.update(
+ Notes.CONTENT_NOTE_URI,
+ unpinValues,
+ "_id=?",
+ new String[]{String.valueOf(id)});
+ Log.d(TAG, "Updated " + updatedRows + " rows for unpinning id: " + id);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error in unpin operation: " + e.getMessage());
+ e.printStackTrace();
+ }
+
+ // 刷新列表
+ mModeCallBack.finishActionMode();
+ startAsyncNotesListQuery();
+ break;
case R.id.delete:
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
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);
+
+ if (mState == ListEditState.TRASH_FOLDER) {
+ // 在回收站中,直接彻底删除
+ 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) {
+ // 直接彻底删除
+ DataUtils.batchDeleteNotes(getContentResolver(), mNotesListAdapter.getSelectedItemIds());
+ mModeCallBack.finishActionMode();
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ } else {
+ // 不在回收站中,让用户选择删除方式
+ builder.setMessage("请选择删除方式");
+ builder.setPositiveButton("移动到回收站",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ batchDelete();
+ }
+ });
+ builder.setNeutralButton("直接删除",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ // 直接彻底删除
+ DataUtils.batchDeleteNotes(getContentResolver(), mNotesListAdapter.getSelectedItemIds());
+ mModeCallBack.finishActionMode();
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ }
builder.show();
break;
case R.id.move:
startQueryDestinationFolders();
break;
+ case R.id.lock:
+ // 处理加锁逻辑
+ if (!NotesPreferenceActivity.isPasswordSet(NotesListActivity.this)) {
+ Toast.makeText(NotesListActivity.this,
+ R.string.preferences_password_empty,
+ Toast.LENGTH_SHORT).show();
+ // 引导用户设置密码
+ Intent intent = new Intent(NotesListActivity.this, NotesPreferenceActivity.class);
+ startActivity(intent);
+ break;
+ }
+
+ // 更新每个选中的便签
+ for (Long id : selectedIds) {
+ ContentValues lockValues = new ContentValues();
+ lockValues.put(NoteColumns.LOCKED, 1);
+
+ int updatedRows = mContentResolver.update(
+ Notes.CONTENT_NOTE_URI,
+ lockValues,
+ "_id=?",
+ new String[]{String.valueOf(id)});
+ Log.d(TAG, "Updated " + updatedRows + " rows for locking id: " + id);
+ }
+
+ // 刷新列表
+ mModeCallBack.finishActionMode();
+ startAsyncNotesListQuery();
+ break;
+ case R.id.unlock:
+ // 处理解锁逻辑
+ if (!NotesPreferenceActivity.isPasswordSet(NotesListActivity.this)) {
+ // 没有密码直接解锁
+ for (Long id : selectedIds) {
+ ContentValues unlockValues = new ContentValues();
+ unlockValues.put(NoteColumns.LOCKED, 0);
+
+ int updatedRows = mContentResolver.update(
+ Notes.CONTENT_NOTE_URI,
+ unlockValues,
+ "_id=?",
+ new String[]{String.valueOf(id)});
+ Log.d(TAG, "Updated " + updatedRows + " rows for unlocking id: " + id);
+ }
+
+ // 刷新列表
+ mModeCallBack.finishActionMode();
+ startAsyncNotesListQuery();
+ break;
+ }
+
+ // 显示密码输入对话框
+ View passwordView = LayoutInflater.from(NotesListActivity.this).inflate(R.layout.password_input_dialog, null);
+ final EditText passwordEdit = (EditText) passwordView.findViewById(R.id.password_input);
+
+ AlertDialog.Builder passwordDialogBuilder = new AlertDialog.Builder(NotesListActivity.this);
+ passwordDialogBuilder.setTitle(R.string.note_lock_password_prompt);
+ passwordDialogBuilder.setView(passwordView);
+
+ passwordDialogBuilder.setPositiveButton(getString(R.string.preferences_button_confirm), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ String password = passwordEdit.getText().toString();
+
+ if (password.equals(NotesPreferenceActivity.getPassword(NotesListActivity.this))) {
+ // 密码正确,解锁便签
+ for (Long id : selectedIds) {
+ ContentValues unlockValues = new ContentValues();
+ unlockValues.put(NoteColumns.LOCKED, 0);
+
+ int updatedRows = mContentResolver.update(
+ Notes.CONTENT_NOTE_URI,
+ unlockValues,
+ "_id=?",
+ new String[]{String.valueOf(id)});
+ Log.d(TAG, "Updated " + updatedRows + " rows for unlocking id: " + id);
+ }
+
+ // 刷新列表
+ mModeCallBack.finishActionMode();
+ startAsyncNotesListQuery();
+ } else {
+ // 密码错误,显示提示
+ Toast.makeText(NotesListActivity.this, R.string.note_lock_password_incorrect, Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ passwordDialogBuilder.setNegativeButton(android.R.string.cancel, null);
+ passwordDialogBuilder.show();
+ break;
+ case R.id.make_public:
+ // 处理开放便签逻辑
+ try {
+ // 更新每个选中的便签
+ for (Long id : selectedIds) {
+ ContentValues publicValues = new ContentValues();
+ publicValues.put(NoteColumns.PUBLIC, 1);
+
+ int updatedRows = mContentResolver.update(
+ Notes.CONTENT_NOTE_URI,
+ publicValues,
+ "_id=?",
+ new String[]{String.valueOf(id)});
+ Log.d(TAG, "Updated " + updatedRows + " rows for making public id: " + id);
+ }
+
+ // 刷新列表
+ mModeCallBack.finishActionMode();
+ startAsyncNotesListQuery();
+ } catch (Exception e) {
+ Log.e(TAG, "Error in make_public operation: " + e.getMessage());
+ e.printStackTrace();
+ }
+ break;
+ case R.id.make_private:
+ // 处理取消开放便签逻辑
+ try {
+ // 更新每个选中的便签
+ for (Long id : selectedIds) {
+ ContentValues privateValues = new ContentValues();
+ privateValues.put(NoteColumns.PUBLIC, 0);
+
+ int updatedRows = mContentResolver.update(
+ Notes.CONTENT_NOTE_URI,
+ privateValues,
+ "_id=?",
+ new String[]{String.valueOf(id)});
+ Log.d(TAG, "Updated " + updatedRows + " rows for making private id: " + id);
+ }
+
+ // 刷新列表
+ mModeCallBack.finishActionMode();
+ startAsyncNotesListQuery();
+ } catch (Exception e) {
+ Log.e(TAG, "Error in make_private operation: " + e.getMessage());
+ e.printStackTrace();
+ }
+ break;
+ case 100:
+ // 恢复选中的便签
+ // 恢复便签到原始文件夹
+ for (long noteId : selectedIds) {
+ // 查询原始文件夹ID
+ Cursor cursor = getContentResolver().query(
+ ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
+ new String[]{NoteColumns.ORIGIN_PARENT_ID},
+ null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ long originParentId = cursor.getLong(0);
+ cursor.close();
+ // 如果原始文件夹ID有效,则恢复到原始文件夹,否则恢复到根文件夹
+ long targetFolderId = (originParentId > 0) ? originParentId : Notes.ID_ROOT_FOLDER;
+ DataUtils.batchMoveToFolder(getContentResolver(), selectedIds, targetFolderId);
+ break; // 只需要处理一次,因为所有选中的便签都使用相同的targetFolderId
+ }
+ }
+ mModeCallBack.finishActionMode();
+ break;
+ case 101:
+ // 处理"发送给"功能
+ showSendToFriendsDialog(selectedIds);
+ break;
default:
return false;
}
@@ -408,13 +865,61 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
};
+ // 开始搜索
+ private void startSearch() {
+ mIsSearching = true;
+ mTitleBar.setVisibility(View.GONE);
+ mSearchEditText.setVisibility(View.VISIBLE);
+ mCancelImageView.setVisibility(View.VISIBLE);
+ mSearchImageView.setVisibility(View.GONE);
+ mSearchEditText.requestFocus();
+ showSoftInput();
+ }
+
+ // 取消搜索
+ private void cancelSearch() {
+ mIsSearching = false;
+ mSearchQuery = null;
+ mSearchEditText.setText("");
+ mSearchEditText.setVisibility(View.GONE);
+ mCancelImageView.setVisibility(View.GONE);
+ mSearchImageView.setVisibility(View.VISIBLE);
+ mTitleBar.setVisibility(mState == ListEditState.NOTE_LIST ? View.GONE : View.VISIBLE);
+ hideSoftInput(mSearchEditText);
+ mNotesListAdapter.setSearchQuery(null);
+ startAsyncNotesListQuery();
+ }
+
+ // 执行搜索
+ private void performSearch() {
+ if (!TextUtils.isEmpty(mSearchQuery)) {
+ hideSoftInput(mSearchEditText);
+ mNotesListAdapter.setSearchQuery(mSearchQuery);
+ startAsyncNotesListQuery();
+ }
+ }
+
private void startAsyncNotesListQuery() {
- String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION
- : NORMAL_SELECTION;
+ String selection;
+ String[] selectionArgs;
+
+ if (mIsSearching && !TextUtils.isEmpty(mSearchQuery)) {
+ // 搜索模式:查询包含搜索关键词的便签
+ selection = NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " +
+ NoteColumns.SNIPPET + " LIKE ?";
+ String searchQuery = "%" + mSearchQuery + "%";
+ selectionArgs = new String[] { searchQuery };
+ } else {
+ // 普通模式:根据当前文件夹查询
+ selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION
+ : NORMAL_SELECTION;
+ selectionArgs = new String[] { String.valueOf(mCurrentFolderId) };
+ }
+
mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null,
- Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] {
- String.valueOf(mCurrentFolderId)
- }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
+ Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, selectionArgs,
+ "CASE WHEN " + NoteColumns.ID + " = " + Notes.ID_TRASH_FOLER + " THEN 0 ELSE 1 END ASC, " +
+ NoteColumns.PINNED + " DESC," + NoteColumns.SORT_ORDER + " ASC," + NoteColumns.TYPE + " DESC," + mCurrentSortOrder);
}
private final class BackgroundQueryHandler extends AsyncQueryHandler {
@@ -427,6 +932,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
switch (token) {
case FOLDER_NOTE_LIST_QUERY_TOKEN:
mNotesListAdapter.changeCursor(cursor);
+ // 如果当前显示的是宫格模式,确保GridView也更新数据
+ if (mDisplayMode == DISPLAY_MODE_GRID) {
+ mNotesGridView.setAdapter(mNotesListAdapter);
+ }
break;
case FOLDER_LIST_QUERY_TOKEN:
if (cursor != null && cursor.getCount() > 0) {
@@ -472,23 +981,96 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private void batchDelete() {
new AsyncTask>() {
protected HashSet doInBackground(Void... unused) {
- HashSet widgets = mNotesListAdapter.getSelectedWidget();
- if (!isSyncMode()) {
- // if not synced, delete notes directly
- if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter
- .getSelectedItemIds())) {
- } else {
- Log.e(TAG, "Delete notes error, should not happens");
+ HashSet selectedIds = mNotesListAdapter.getSelectedItemIds();
+ HashSet allWidgets = new HashSet<>();
+
+ // 分离便签和文件夹
+ HashSet noteIds = new HashSet<>();
+ HashSet folderIds = new HashSet<>();
+
+ Cursor cursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
+ new String[]{NoteColumns.ID, NoteColumns.TYPE},
+ NoteColumns.ID + " IN (" + TextUtils.join(",", selectedIds) + ")",
+ null, null);
+
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ int type = cursor.getInt(1);
+ if (type == Notes.TYPE_NOTE) {
+ noteIds.add(id);
+ } else if (type == Notes.TYPE_FOLDER) {
+ folderIds.add(id);
+ }
}
- } else {
- // in sync mode, we'll move the deleted note into the trash
- // folder
- if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter
- .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) {
+ cursor.close();
+ }
+
+ // 处理便签:直接移至回收站
+ if (!noteIds.isEmpty()) {
+ if (!DataUtils.batchMoveToFolder(mContentResolver, noteIds, Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happens");
}
+ allWidgets.addAll(mNotesListAdapter.getSelectedWidget());
}
- return widgets;
+
+ // 处理文件夹
+ for (long folderId : folderIds) {
+ // 查询文件夹中的便签数量
+ Cursor folderCursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
+ new String[]{"COUNT(*)"},
+ NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "=?",
+ new String[]{String.valueOf(folderId), String.valueOf(Notes.TYPE_NOTE)},
+ null);
+
+ int noteCount = 0;
+ if (folderCursor != null) {
+ if (folderCursor.moveToFirst()) {
+ noteCount = folderCursor.getInt(0);
+ }
+ folderCursor.close();
+ }
+
+ if (noteCount > 0) {
+ // 文件夹中有便签,将便签移至回收站
+ Cursor noteCursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
+ new String[]{NoteColumns.ID},
+ NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "=?",
+ new String[]{String.valueOf(folderId), String.valueOf(Notes.TYPE_NOTE)},
+ null);
+
+ HashSet folderNoteIds = new HashSet<>();
+ if (noteCursor != null) {
+ while (noteCursor.moveToNext()) {
+ folderNoteIds.add(noteCursor.getLong(0));
+ }
+ noteCursor.close();
+ }
+
+ // 移动便签到回收站
+ if (!DataUtils.batchMoveToFolder(mContentResolver, folderNoteIds, Notes.ID_TRASH_FOLER)) {
+ Log.e(TAG, "Move folder notes to trash error");
+ }
+
+ // 获取文件夹中便签的widget信息
+ HashSet folderWidgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId);
+ if (folderWidgets != null) {
+ allWidgets.addAll(folderWidgets);
+ }
+
+ // 删除空文件夹
+ HashSet emptyFolderIds = new HashSet<>();
+ emptyFolderIds.add(folderId);
+ DataUtils.batchDeleteNotes(mContentResolver, emptyFolderIds);
+ } else {
+ // 文件夹为空,直接删除
+ HashSet emptyFolderIds = new HashSet<>();
+ emptyFolderIds.add(folderId);
+ DataUtils.batchDeleteNotes(mContentResolver, emptyFolderIds);
+ }
+ }
+
+ return allWidgets;
}
@Override
@@ -512,32 +1094,141 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
return;
}
- HashSet ids = new HashSet();
- ids.add(folderId);
- HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver,
- folderId);
- if (!isSyncMode()) {
- // if not synced, delete folder directly
- DataUtils.batchDeleteNotes(mContentResolver, ids);
- } else {
- // in sync mode, we'll move the deleted folder into the trash folder
- 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);
- }
- }
+ // 查询文件夹中的便签数量
+ Cursor folderCursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
+ new String[]{"COUNT(*)"},
+ NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "=?",
+ new String[]{String.valueOf(folderId), String.valueOf(Notes.TYPE_NOTE)},
+ null);
+
+ final int noteCount = folderCursor != null && folderCursor.moveToFirst() ? folderCursor.getInt(0) : 0;
+ if (folderCursor != null) {
+ folderCursor.close();
}
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(getString(R.string.alert_title_delete));
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setMessage("请选择删除方式");
+
+ // 移动到回收站选项
+ builder.setPositiveButton("移动到回收站",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (noteCount > 0) {
+ // 文件夹中有便签,将便签移至回收站
+ Cursor noteCursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
+ new String[]{NoteColumns.ID},
+ NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "=?",
+ new String[]{String.valueOf(folderId), String.valueOf(Notes.TYPE_NOTE)},
+ null);
+
+ HashSet folderNoteIds = new HashSet<>();
+ if (noteCursor != null) {
+ while (noteCursor.moveToNext()) {
+ folderNoteIds.add(noteCursor.getLong(0));
+ }
+ noteCursor.close();
+ }
+
+ // 移动便签到回收站
+ if (!DataUtils.batchMoveToFolder(mContentResolver, folderNoteIds, Notes.ID_TRASH_FOLER)) {
+ Log.e(TAG, "Move folder notes to trash error");
+ }
+
+ // 获取文件夹中便签的widget信息
+ HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId);
+ 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);
+ }
+ }
+ }
+ }
+
+ // 删除空文件夹
+ HashSet emptyFolderIds = new HashSet<>();
+ emptyFolderIds.add(folderId);
+ DataUtils.batchDeleteNotes(mContentResolver, emptyFolderIds);
+ }
+ });
+
+ // 直接删除选项
+ builder.setNeutralButton("直接删除",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (noteCount > 0) {
+ // 文件夹中有便签,先删除所有便签
+ Cursor noteCursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
+ new String[]{NoteColumns.ID},
+ NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "=?",
+ new String[]{String.valueOf(folderId), String.valueOf(Notes.TYPE_NOTE)},
+ null);
+
+ HashSet folderNoteIds = new HashSet<>();
+ if (noteCursor != null) {
+ while (noteCursor.moveToNext()) {
+ folderNoteIds.add(noteCursor.getLong(0));
+ }
+ noteCursor.close();
+ }
+
+ // 直接彻底删除便签
+ DataUtils.batchDeleteNotes(mContentResolver, folderNoteIds);
+ }
+
+ // 删除文件夹
+ HashSet folderIds = new HashSet<>();
+ folderIds.add(folderId);
+ DataUtils.batchDeleteNotes(mContentResolver, folderIds);
+ }
+ });
+
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.show();
}
private void openNode(NoteItemData data) {
- Intent intent = new Intent(this, NoteEditActivity.class);
- intent.setAction(Intent.ACTION_VIEW);
- intent.putExtra(Intent.EXTRA_UID, data.getId());
- this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE);
+ if (data.isLocked()) {
+ // 便签被锁定,需要密码验证
+ View passwordView = LayoutInflater.from(this).inflate(R.layout.password_input_dialog, null);
+ final EditText passwordEdit = (EditText) passwordView.findViewById(R.id.password_input);
+ final NoteItemData noteData = data;
+
+ AlertDialog.Builder passwordDialogBuilder = new AlertDialog.Builder(this);
+ passwordDialogBuilder.setTitle(R.string.note_lock_password_prompt);
+ passwordDialogBuilder.setView(passwordView);
+
+ passwordDialogBuilder.setPositiveButton(getString(R.string.preferences_button_confirm), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ String password = passwordEdit.getText().toString();
+
+ if (password.equals(NotesPreferenceActivity.getPassword(NotesListActivity.this))) {
+ // 密码正确,打开便签
+ Intent intent = new Intent(NotesListActivity.this, NoteEditActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.putExtra(Intent.EXTRA_UID, noteData.getId());
+ NotesListActivity.this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE);
+ } else {
+ // 密码错误
+ Toast.makeText(NotesListActivity.this,
+ R.string.note_lock_password_incorrect,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ passwordDialogBuilder.setNegativeButton(getString(R.string.preferences_button_cancel), null);
+ passwordDialogBuilder.show();
+ } else {
+ // 便签未锁定,直接打开
+ Intent intent = new Intent(this, 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) {
@@ -546,11 +1237,16 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mState = ListEditState.CALL_RECORD_FOLDER;
mAddNewNote.setVisibility(View.GONE);
+ } else if (data.getId() == Notes.ID_TRASH_FOLER) {
+ mState = ListEditState.TRASH_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 if (data.getId() == Notes.ID_TRASH_FOLER) {
+ mTitleBar.setText("回收站");
} else {
mTitleBar.setText(data.getSnippet());
}
@@ -579,6 +1275,226 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
+ // 显示更多选项菜单
+ private void showMoreOptionsMenu(View anchorView) {
+ PopupMenu popupMenu = new PopupMenu(this, anchorView);
+
+ // 添加菜单项,移除分隔线以消除空白项
+ MenuItem listModeItem = popupMenu.getMenu().add(0, DISPLAY_MODE_LIST, 1, "列表式呈现");
+ MenuItem gridModeItem = popupMenu.getMenu().add(0, DISPLAY_MODE_GRID, 2, "宫格图呈现");
+ MenuItem sortByCreateDateItem = popupMenu.getMenu().add(0, 3, 3, "按照创建时间排序");
+ MenuItem sortByModifiedDateItem = popupMenu.getMenu().add(0, 4, 4, "按照修改日期排序");
+ MenuItem friendItem = popupMenu.getMenu().add(0, 7, 5, "好友");
+ MenuItem switchAccountItem = popupMenu.getMenu().add(0, 6, 6, "切换账号");
+
+ // 根据当前状态设置菜单项的勾选状态
+ listModeItem.setChecked(mDisplayMode == DISPLAY_MODE_LIST);
+ gridModeItem.setChecked(mDisplayMode == DISPLAY_MODE_GRID);
+ listModeItem.setCheckable(true);
+ gridModeItem.setCheckable(true);
+
+ // 设置菜单项点击监听器
+ popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ switch (item.getItemId()) {
+ case DISPLAY_MODE_LIST:
+ // 切换到列表模式
+ mDisplayMode = DISPLAY_MODE_LIST;
+ updateDisplayMode();
+ item.setChecked(true);
+ break;
+ case DISPLAY_MODE_GRID:
+ // 切换到宫格模式
+ mDisplayMode = DISPLAY_MODE_GRID;
+ updateDisplayMode();
+ item.setChecked(true);
+ break;
+ case 3:
+ // 按照创建时间排序
+ mCurrentSortOrder = SORT_BY_CREATE_DATE;
+ startAsyncNotesListQuery();
+ break;
+ case 4:
+ // 按照修改日期排序
+ mCurrentSortOrder = SORT_BY_MODIFIED_DATE;
+ startAsyncNotesListQuery();
+ break;
+ case 6:
+ // 处理切换账号
+ showSwitchAccountDialog();
+ break;
+ case 7:
+ // 处理好友功能
+ Intent friendIntent = new Intent(NotesListActivity.this, net.micode.notes.ui.FriendManagementActivity.class);
+ startActivity(friendIntent);
+ break;
+ }
+ return true;
+ }
+ });
+
+ // 显示弹出菜单
+ popupMenu.show();
+ }
+
+ /**
+ * 显示切换账号对话框
+ */
+ private void showSwitchAccountDialog() {
+ // 准备用户列表
+ final ArrayList userIds = new ArrayList<>();
+ final ArrayList usernames = new ArrayList<>();
+
+ try {
+ // 直接在主线程中查询所有用户,数据库操作简单,不会导致明显卡顿
+ net.micode.notes.data.NotesDatabaseHelper helper = net.micode.notes.data.NotesDatabaseHelper.getInstance(getApplicationContext());
+ if (helper == null) {
+ Toast.makeText(this, "数据库初始化失败", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ android.database.sqlite.SQLiteDatabase db = helper.getReadableDatabase();
+ if (db == null || !db.isOpen()) {
+ Toast.makeText(this, "数据库连接失败", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ android.database.Cursor cursor = null;
+ try {
+ // 查询所有用户
+ cursor = db.query(
+ net.micode.notes.data.NotesDatabaseHelper.TABLE.USER,
+ new String[]{net.micode.notes.data.Users.UserColumns.ID, net.micode.notes.data.Users.UserColumns.USERNAME},
+ null,
+ null,
+ null,
+ null,
+ null
+ );
+
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ userIds.add(cursor.getLong(0));
+ usernames.add(cursor.getString(1));
+ } while (cursor.moveToNext());
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "查询账号失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
+ return;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "获取账号列表失败", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (userIds.isEmpty()) {
+ Toast.makeText(this, "没有可用的账号", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 显示用户列表对话框
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("选择账号");
+ builder.setItems(usernames.toArray(new String[0]), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ long selectedUserId = userIds.get(which);
+ String selectedUsername = usernames.get(which);
+
+ // 获取当前用户ID
+ long currentUserId = net.micode.notes.tool.UserManager.getInstance(NotesListActivity.this).getCurrentUserId();
+
+ if (selectedUserId == currentUserId) {
+ // 如果选择的是当前账号,显示提示
+ Toast.makeText(NotesListActivity.this, "您已经在" + selectedUsername + "账号上", Toast.LENGTH_SHORT).show();
+ } else {
+ // 否则,显示密码输入对话框
+ showPasswordInputDialog(selectedUserId, selectedUsername);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(NotesListActivity.this, "账号选择失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ builder.show();
+ }
+
+ /**
+ * 显示密码输入对话框
+ */
+ private void showPasswordInputDialog(final long userId, final String username) {
+ try {
+ // 加载密码输入对话框布局
+ View passwordView = LayoutInflater.from(this).inflate(R.layout.password_input_dialog, null);
+ final EditText passwordEdit = (EditText) passwordView.findViewById(R.id.password_input);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("输入密码");
+ builder.setMessage("请输入" + username + "的密码");
+ builder.setView(passwordView);
+
+ // 设置确定按钮点击事件
+ builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ String password = passwordEdit.getText().toString();
+
+ // 验证密码
+ if (net.micode.notes.tool.UserManager.getInstance(NotesListActivity.this).validatePassword(userId, password)) {
+ // 密码正确,切换账号
+ net.micode.notes.tool.UserManager.getInstance(NotesListActivity.this).setCurrentUser(userId);
+
+ // 刷新便签列表
+ startAsyncNotesListQuery();
+
+ // 显示切换成功提示
+ Toast.makeText(NotesListActivity.this, "已切换到" + username + "账号", Toast.LENGTH_SHORT).show();
+ } else {
+ // 密码错误,显示提示
+ Toast.makeText(NotesListActivity.this, "密码错误", Toast.LENGTH_SHORT).show();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(NotesListActivity.this, "切换账号失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ // 设置取消按钮点击事件
+ builder.setNegativeButton(android.R.string.cancel, null);
+
+ // 显示对话框
+ builder.show();
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "打开密码输入框失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ // 更新显示模式
+ private void updateDisplayMode() {
+ if (mDisplayMode == DISPLAY_MODE_LIST) {
+ // 切换到列表模式
+ mNotesListView.setVisibility(View.VISIBLE);
+ mNotesGridView.setVisibility(View.GONE);
+ } else {
+ // 切换到宫格模式
+ mNotesListView.setVisibility(View.GONE);
+ mNotesGridView.setVisibility(View.VISIBLE);
+ mNotesGridView.setAdapter(mNotesListAdapter);
+ }
+ }
+
private void showCreateOrModifyFolderDialog(final boolean create) {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null);
@@ -624,7 +1540,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
values.put(NoteColumns.LOCAL_MODIFIED, 1);
mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID
+ "=?", new String[] {
- String.valueOf(mFocusNoteDataItem.getId())
+ String.valueOf(mFocusNoteDataItem.getId())
});
}
} else if (!TextUtils.isEmpty(name)) {
@@ -668,10 +1584,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
public void onBackPressed() {
switch (mState) {
case SUB_FOLDER:
+ case TRASH_FOLDER:
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
startAsyncNotesListQuery();
mTitleBar.setVisibility(View.GONE);
+ mAddNewNote.setVisibility(View.VISIBLE);
break;
case CALL_RECORD_FOLDER:
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
@@ -700,7 +1618,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {
- appWidgetId
+ appWidgetId
});
sendBroadcast(intent);
@@ -772,6 +1690,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
getMenuInflater().inflate(R.menu.sub_folder, menu);
} else if (mState == ListEditState.CALL_RECORD_FOLDER) {
getMenuInflater().inflate(R.menu.call_record_folder, menu);
+ } else if (mState == ListEditState.TRASH_FOLDER) {
+ // 为回收站添加特定菜单
+ menu.add(0, 100, 0, "恢复");
+ menu.add(0, 101, 0, "清空回收站");
} else {
Log.e(TAG, "Wrong state:" + mState);
}
@@ -812,6 +1734,54 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
case R.id.menu_search:
onSearchRequested();
break;
+ case R.id.menu_change_background:
+ // 直接在主界面更换背景
+ changeBackground();
+ break;
+
+ case 100: {
+ // 恢复选中的便签(兼容旧版本)
+ if (mNotesListAdapter.getSelectedCount() == 0) {
+ Toast.makeText(this, getString(R.string.menu_select_none),
+ Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ // 将便签恢复到根文件夹
+ DataUtils.batchMoveToFolder(getContentResolver(), mNotesListAdapter.getSelectedItemIds(), Notes.ID_ROOT_FOLDER);
+ mModeCallBack.finishActionMode();
+ break;
+ }
+ case 101: {
+ // 清空回收站(兼容旧版本)
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ 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, "所有"));
+ builder.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ // 查询回收站中的所有便签并彻底删除
+ Cursor cursor = getContentResolver().query(Notes.CONTENT_NOTE_URI,
+ new String[] { NoteColumns.ID },
+ NoteColumns.PARENT_ID + "=?",
+ new String[] { String.valueOf(Notes.ID_TRASH_FOLER) },
+ null);
+ if (cursor != null) {
+ HashSet ids = new HashSet();
+ while (cursor.moveToNext()) {
+ ids.add(cursor.getLong(0));
+ }
+ cursor.close();
+ // 彻底删除
+ DataUtils.batchDeleteNotes(getContentResolver(), ids);
+ }
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.show();
+ break;
+ }
default:
break;
}
@@ -882,7 +1852,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
if (view instanceof NotesListItem) {
NoteItemData item = ((NotesListItem) view).getItemData();
if (mNotesListAdapter.isInChoiceMode()) {
- if (item.getType() == Notes.TYPE_NOTE) {
+ // 允许在多选模式下选中文件夹和便签,但禁止选中回收站
+ if (item.getId() != Notes.ID_TRASH_FOLER) {
position = position - mNotesListView.getHeaderViewsCount();
mModeCallBack.onItemCheckedStateChanged(null, position, id,
!mNotesListAdapter.isSelectedItem(position));
@@ -903,10 +1874,11 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
break;
case SUB_FOLDER:
case CALL_RECORD_FOLDER:
+ case TRASH_FOLDER:
if (item.getType() == Notes.TYPE_NOTE) {
openNode(item);
} else {
- Log.e(TAG, "Wrong note type in SUB_FOLDER");
+ Log.e(TAG, "Wrong note type in folder view");
}
break;
default:
@@ -920,7 +1892,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
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 + ")";
+ "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")";
mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN,
null,
@@ -934,21 +1906,770 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
},
NoteColumns.MODIFIED_DATE + " DESC");
}
+
+ /**
+ * 显示选择好友的对话框,用于发送便签
+ */
+ private void showSendToFriendsDialog(final HashSet selectedNoteIds) {
+ try {
+ // 查询所有好友
+ final ArrayList friendIds = new ArrayList<>();
+ final ArrayList friendUsernames = new ArrayList<>();
+ final boolean[] selectedFriends = new boolean[0];
+
+ // 获取当前用户ID
+ long currentUserId = UserManager.getInstance(this).getCurrentUserId();
+
+ // 获取数据库实例
+ SQLiteDatabase db = NotesDatabaseHelper.getInstance(this).getReadableDatabase();
+
+ // 查询除当前用户以外的所有用户
+ Cursor cursor = db.query(
+ NotesDatabaseHelper.TABLE.USER,
+ new String[]{Users.UserColumns.ID, Users.UserColumns.USERNAME},
+ Users.UserColumns.ID + " != ?",
+ new String[]{String.valueOf(currentUserId)},
+ null, null, null);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ long friendId = cursor.getLong(cursor.getColumnIndexOrThrow(Users.UserColumns.ID));
+ String friendUsername = cursor.getString(cursor.getColumnIndexOrThrow(Users.UserColumns.USERNAME));
+ friendIds.add(friendId);
+ friendUsernames.add(friendUsername);
+ } while (cursor.moveToNext());
+ cursor.close();
+ }
+
+ if (friendIds.isEmpty()) {
+ Toast.makeText(this, "没有可用的好友", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 创建选择数组
+ final boolean[] isFriendSelected = new boolean[friendIds.size()];
+
+ // 创建对话框
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("选择好友");
+
+ // 设置多选项
+ builder.setMultiChoiceItems(
+ friendUsernames.toArray(new String[0]),
+ isFriendSelected,
+ new DialogInterface.OnMultiChoiceClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which, boolean isChecked) {
+ isFriendSelected[which] = isChecked;
+ }
+ });
+
+ // 设置确定按钮
+ builder.setPositiveButton("发送", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // 获取选中的好友
+ ArrayList selectedFriendIds = new ArrayList<>();
+ for (int i = 0; i < isFriendSelected.length; i++) {
+ if (isFriendSelected[i]) {
+ selectedFriendIds.add(friendIds.get(i));
+ }
+ }
+
+ if (selectedFriendIds.isEmpty()) {
+ Toast.makeText(NotesListActivity.this, "请选择至少一个好友", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 发送便签到选中的好友
+ sendNotesToFriends(selectedNoteIds, selectedFriendIds);
+
+ // 结束操作模式
+ mModeCallBack.finishActionMode();
+ }
+ });
+
+ // 设置取消按钮
+ builder.setNegativeButton("取消", null);
+
+ // 显示对话框
+ builder.show();
+ } catch (Exception e) {
+ Log.e(TAG, "Error in showSendToFriendsDialog: " + e.getMessage(), e);
+ Toast.makeText(this, "显示好友列表失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 发送便签到选中的好友
+ */
+ private void sendNotesToFriends(HashSet noteIds, ArrayList friendIds) {
+ try {
+ SQLiteDatabase db = NotesDatabaseHelper.getInstance(this).getWritableDatabase();
+ long currentUserId = UserManager.getInstance(this).getCurrentUserId();
+
+ // 遍历每个选中的便签
+ for (Long noteId : noteIds) {
+ // 查询便签内容
+ Cursor noteCursor = db.query(
+ NotesDatabaseHelper.TABLE.NOTE,
+ new String[]{NoteColumns.TITLE, NoteColumns.SNIPPET},
+ NoteColumns.ID + " = ?",
+ new String[]{String.valueOf(noteId)},
+ null, null, null);
+
+ if (noteCursor != null && noteCursor.moveToFirst()) {
+ String noteTitle = noteCursor.getString(noteCursor.getColumnIndexOrThrow(NoteColumns.TITLE));
+ String noteContent = noteCursor.getString(noteCursor.getColumnIndexOrThrow(NoteColumns.SNIPPET));
+ noteCursor.close();
+
+ // 便签内容格式:标题|内容|便签ID
+ String noteData = noteTitle + "|" + noteContent + "|" + noteId;
+
+ // 遍历每个选中的好友,发送便签
+ for (Long friendId : friendIds) {
+ ContentValues values = new ContentValues();
+ values.put(Messages.MessageColumns.SENDER_ID, currentUserId);
+ values.put(Messages.MessageColumns.RECEIVER_ID, friendId);
+ values.put(Messages.MessageColumns.CONTENT, noteData);
+ values.put(Messages.MessageColumns.MESSAGE_TYPE, Messages.MessageType.NOTE);
+ values.put(Messages.MessageColumns.CREATED_DATE, System.currentTimeMillis());
+ values.put(Messages.MessageColumns.IS_READ, 0);
+
+ // 插入消息到数据库
+ long messageId = db.insert(NotesDatabaseHelper.TABLE.MESSAGE, null, values);
+ if (messageId != -1) {
+ Log.d(TAG, "Note sent to friend " + friendId + ", messageId: " + messageId);
+ } else {
+ Log.e(TAG, "Failed to send note to friend " + friendId);
+ }
+ }
+ } else {
+ if (noteCursor != null) {
+ noteCursor.close();
+ }
+ Log.e(TAG, "Failed to get note content for note " + noteId);
+ }
+ }
+
+ Toast.makeText(this, "便签发送成功", Toast.LENGTH_SHORT).show();
+ } catch (Exception e) {
+ Log.e(TAG, "Error in sendNotesToFriends: " + e.getMessage(), e);
+ Toast.makeText(this, "发送便签失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 更换背景方法
+ */
+ private void changeBackground() {
+ Log.d(TAG, "changeBackground called");
+
+ // 创建选择背景的对话框
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("更换背景");
+
+ // 创建列表项
+ String[] items = {"从相册选择", "拍照"};
+
+ // 设置列表项点击事件
+ builder.setItems(items, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent;
+ switch (which) {
+ case 0: // 从相册选择
+ intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ intent.setType("image/*");
+ startActivityForResult(intent, 1);
+ break;
+ case 1: // 拍照
+ intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
+ startActivityForResult(intent, 2);
+ break;
+ }
+ }
+ });
+
+ // 显示对话框
+ builder.show();
+ Log.d(TAG, "Dialog shown");
+ }
+
+
+
+ /**
+ * 根据Uri获取图片路径,适配不同Android版本
+ */
+ private String getPathFromUri(android.net.Uri uri) {
+ Log.d(TAG, "Getting path from Uri: " + uri.toString() + ", scheme: " + uri.getScheme());
+
+ String path = null;
+
+ try {
+ // 检查Uri scheme
+ if ("content".equals(uri.getScheme())) {
+ // 处理content://类型的Uri
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
+ // Android 10及以上,使用MediaStore API
+ path = getPathFromContentUriQ(uri);
+ } else {
+ // Android 9及以下,使用传统方式
+ path = getPathFromContentUriLegacy(uri);
+ }
+ } else if ("file".equals(uri.getScheme())) {
+ // 处理file://类型的Uri
+ path = uri.getPath();
+ Log.d(TAG, "File scheme Uri, path: " + path);
+ }
+
+ Log.d(TAG, "Final path from Uri: " + path);
+ } catch (Exception e) {
+ Log.e(TAG, "Error getting path from Uri: " + e.getMessage(), e);
+ }
+
+ return path;
+ }
+
+ /**
+ * Android 9及以下,根据Content Uri获取图片路径
+ */
+ private String getPathFromContentUriLegacy(android.net.Uri uri) {
+ String path = null;
+ String[] projection = {android.provider.MediaStore.Images.Media.DATA};
+ Cursor cursor = getContentResolver().query(uri, projection, null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ int columnIndex = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA);
+ path = cursor.getString(columnIndex);
+ Log.d(TAG, "Legacy path: " + path);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error in legacy path retrieval: " + e.getMessage(), e);
+ } finally {
+ cursor.close();
+ }
+ }
+ return path;
+ }
+
+ /**
+ * Android 10及以上,根据Content Uri获取图片路径
+ */
+ private String getPathFromContentUriQ(android.net.Uri uri) {
+ String path = null;
+
+ // 对于Android Q及以上,我们可以直接使用Uri打开InputStream,而不需要获取真实路径
+ // 这里我们创建一个临时文件来保存图片
+ try {
+ // 创建临时文件
+ java.io.File tempFile = java.io.File.createTempFile("notes_bg", ".jpg", getExternalCacheDir());
+ tempFile.deleteOnExit();
+
+ // 从Uri复制到临时文件
+ java.io.InputStream inputStream = getContentResolver().openInputStream(uri);
+ if (inputStream != null) {
+ java.io.FileOutputStream outputStream = new java.io.FileOutputStream(tempFile);
+
+ // 复制文件
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, length);
+ }
+
+ outputStream.close();
+ inputStream.close();
+
+ path = tempFile.getAbsolutePath();
+ Log.d(TAG, "Q path: " + path);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error in Q path retrieval: " + e.getMessage(), e);
+ }
+
+ return path;
+ }
+
+ /**
+ * 保存Bitmap到本地
+ */
+ private String saveBitmap(android.graphics.Bitmap bitmap) {
+ String path = android.os.Environment.getExternalStorageDirectory().getAbsolutePath() + "/notes_background.jpg";
+ try {
+ java.io.FileOutputStream fos = new java.io.FileOutputStream(path);
+ bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 100, fos);
+ fos.flush();
+ fos.close();
+ } catch (java.io.IOException e) {
+ e.printStackTrace();
+ path = null;
+ }
+ return path;
+ }
+
+ /**
+ * 设置背景
+ */
+ private void setBackground(String imagePath) {
+ Log.d(TAG, "Setting background with path: " + imagePath);
+
+ try {
+ // 首先检查图片文件是否存在
+ java.io.File imageFile = new java.io.File(imagePath);
+ if (!imageFile.exists()) {
+ Log.e(TAG, "Image file not found: " + imagePath);
+ Toast.makeText(this, "图片文件不存在", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Log.d(TAG, "Image file exists, size: " + imageFile.length() + " bytes");
+
+ // 1. 尝试直接获取note_list.xml中的根FrameLayout
+ FrameLayout rootFrameLayout = findViewById(R.id.root_layout);
+ if (rootFrameLayout != null) {
+ Log.d(TAG, "Found root_layout (note_list.xml root), setting background directly");
+
+ // 尝试加载图片
+ android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeFile(imagePath);
+ if (bitmap != null) {
+ Log.d(TAG, "Bitmap loaded successfully, width: " + bitmap.getWidth() + ", height: " + bitmap.getHeight());
+
+ // 创建Drawable
+ android.graphics.drawable.Drawable drawable;
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ drawable = new android.graphics.drawable.BitmapDrawable(getResources(), bitmap);
+ } else {
+ drawable = new android.graphics.drawable.BitmapDrawable(bitmap);
+ }
+
+ // 直接设置根FrameLayout的背景
+ rootFrameLayout.setBackground(drawable);
+ Log.d(TAG, "Root layout background set successfully");
+ Toast.makeText(this, "背景设置成功", Toast.LENGTH_SHORT).show();
+ } else {
+ Log.e(TAG, "Failed to load bitmap from path: " + imagePath);
+ Toast.makeText(this, "图片加载失败", Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ Log.d(TAG, "root_layout not found, trying other methods");
+
+ // 2. 尝试获取Activity的根View
+ View rootView = getWindow().getDecorView().findViewById(android.R.id.content);
+ if (rootView != null) {
+ Log.d(TAG, "Root view found: " + rootView.getClass().getName());
+
+ // 3. 如果是ViewGroup,尝试设置其所有子View的背景为透明,然后设置自身背景
+ if (rootView instanceof ViewGroup) {
+ Log.d(TAG, "Root view is ViewGroup, clearing child backgrounds");
+ clearChildBackgrounds((ViewGroup) rootView);
+ }
+
+ // 尝试加载图片
+ android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeFile(imagePath);
+ if (bitmap != null) {
+ Log.d(TAG, "Bitmap loaded successfully, width: " + bitmap.getWidth() + ", height: " + bitmap.getHeight());
+
+ // 创建Drawable
+ android.graphics.drawable.Drawable drawable;
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ drawable = new android.graphics.drawable.BitmapDrawable(getResources(), bitmap);
+ } else {
+ drawable = new android.graphics.drawable.BitmapDrawable(bitmap);
+ }
+
+ // 设置背景
+ rootView.setBackground(drawable);
+ Log.d(TAG, "Root view background set successfully");
+ Toast.makeText(this, "背景设置成功", Toast.LENGTH_SHORT).show();
+ } else {
+ Log.e(TAG, "Failed to load bitmap from path: " + imagePath);
+ Toast.makeText(this, "图片加载失败", Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ Log.e(TAG, "Root view not found");
+ Toast.makeText(this, "无法获取根布局", Toast.LENGTH_SHORT).show();
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error setting background: " + e.getMessage(), e);
+ Toast.makeText(this, "设置背景时出错: " + e.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 清除ViewGroup中所有子View的背景
+ */
+ private void clearChildBackgrounds(ViewGroup viewGroup) {
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
+ View child = viewGroup.getChildAt(i);
+ if (child instanceof ViewGroup) {
+ // 递归清除子ViewGroup的背景
+ clearChildBackgrounds((ViewGroup) child);
+ }
+ // 设置子View背景为透明
+ child.setBackgroundColor(getResources().getColor(android.R.color.transparent));
+ }
+ }
+
+ /**
+ * 加载保存的背景图片
+ */
+ private void loadSavedBackground() {
+ // 从SharedPreferences获取保存的图片路径
+ SharedPreferences sharedPreferences = getSharedPreferences("background", MODE_PRIVATE);
+ String imagePath = sharedPreferences.getString("background_path", null);
+
+ if (imagePath != null) {
+ // 设置背景
+ setBackground(imagePath);
+ }
+ }
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 (mFocusNoteDataItem.getId() == Notes.ID_TRASH_FOLER) {
+ return false;
+ }
+ if (!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;
}
+
+ // 拖拽触摸监听器
+ private class OnDragTouchListener implements OnTouchListener {
+ private static final long LONG_PRESS_DURATION = 500; // 长按检测时长
+ private boolean mIsLongPress = false;
+ private long mPressStartTime = 0;
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (mIsDragging) {
+ handleDragEvent(event);
+ return true;
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mPressStartTime = System.currentTimeMillis();
+ mIsLongPress = false;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (!mIsLongPress && System.currentTimeMillis() - mPressStartTime > LONG_PRESS_DURATION) {
+ mIsLongPress = true;
+ startDrag(v, event);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mIsLongPress = false;
+ break;
+ }
+ return false;
+ }
+ }
+
+ // 开始拖拽
+ private void startDrag(View listView, MotionEvent event) {
+ try {
+ if (listView instanceof ListView) {
+ ListView lv = (ListView) listView;
+ int position = lv.pointToPosition((int) event.getX(), (int) event.getY());
+ int listCount = lv.getCount();
+ if (position >= 0 && position < listCount) {
+ // 保存当前列表数据
+ Cursor cursor = mNotesListAdapter.getCursor();
+ if (cursor != null && cursor.getCount() > 0) {
+ try {
+ cursor.moveToFirst();
+ int cursorCount = cursor.getCount();
+ mDragTempData = new NoteItemData[cursorCount];
+ for (int i = 0; i < cursorCount; i++) {
+ if (!cursor.isAfterLast()) {
+ mDragTempData[i] = new NoteItemData(this, cursor);
+ cursor.moveToNext();
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error saving list data: " + e.getMessage(), e);
+ mDragTempData = null;
+ } finally {
+ if (!cursor.isClosed()) {
+ try {
+ cursor.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Error closing cursor: " + e.getMessage(), e);
+ }
+ }
+ }
+ }
+
+ mDragStartPosition = position;
+ mDragCurrentPosition = position;
+ mDragStartY = event.getY();
+ mDragOffsetY = event.getY();
+ mIsDragging = true;
+
+ // 获取拖拽视图
+ int firstVisiblePosition = lv.getFirstVisiblePosition();
+ int childIndex = position - firstVisiblePosition;
+ if (childIndex >= 0 && childIndex < lv.getChildCount()) {
+ View itemView = lv.getChildAt(childIndex);
+ if (itemView != null) {
+ // 创建拖拽视图的副本
+ mDraggingView = itemView;
+ mDraggingViewHeight = itemView.getHeight();
+ // 显示拖拽效果
+ mDraggingView.setAlpha(0.5f);
+ mDraggingView.setScaleX(1.1f);
+ mDraggingView.setScaleY(1.1f);
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error starting drag: " + e.getMessage(), e);
+ // 发生异常时,重置拖拽状态
+ mIsDragging = false;
+ mDragStartPosition = -1;
+ mDragCurrentPosition = -1;
+ mDraggingView = null;
+ mDragTempData = null;
+ }
+ }
+
+ // 处理拖拽事件
+ private void handleDragEvent(MotionEvent event) {
+ try {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_MOVE:
+ // 更新拖拽视图位置
+ if (mDraggingView != null) {
+ float deltaY = event.getY() - mDragOffsetY;
+ mDraggingView.setTranslationY(deltaY);
+ }
+
+ // 计算当前拖拽到的位置
+ ListView lv = mNotesListView;
+ if (lv == null) {
+ break;
+ }
+
+ int firstVisiblePosition = lv.getFirstVisiblePosition();
+ int lastVisiblePosition = lv.getLastVisiblePosition();
+ int childCount = lv.getChildCount();
+
+ // 计算相对于列表顶部的Y坐标
+ int listTop = lv.getTop();
+ int relativeY = (int) event.getY() + listTop;
+
+ // 计算拖拽到的项索引
+ int newPosition = -1;
+
+ // 遍历可见项,计算当前Y坐标对应的项
+ if (childCount > 0) {
+ for (int i = 0; i < childCount; i++) {
+ View child = lv.getChildAt(i);
+ if (child == null) continue;
+
+ int childTop = child.getTop() + listTop;
+ int childBottom = childTop + child.getHeight();
+
+ if (relativeY >= childTop && relativeY <= childBottom) {
+ // 找到了对应的项
+ newPosition = firstVisiblePosition + i;
+ break;
+ }
+ }
+ }
+
+ // 处理边界情况
+ if (newPosition == -1) {
+ if (childCount > 0) {
+ View firstVisibleItem = lv.getChildAt(0);
+ View lastVisibleItem = lv.getChildAt(childCount - 1);
+
+ if (firstVisibleItem != null && event.getY() < firstVisibleItem.getTop()) {
+ // 向上拖拽到可见区域外,应该插入到第一个可见项之前
+ newPosition = firstVisiblePosition;
+ } else if (lastVisibleItem != null && event.getY() > lastVisibleItem.getBottom()) {
+ // 向下拖拽到可见区域外,应该插入到最后一个可见项之后
+ newPosition = lastVisiblePosition + 1;
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+
+ // 确保新位置在有效范围内
+ int listCount = lv.getCount();
+ if (listCount > 0) {
+ newPosition = Math.max(0, Math.min(newPosition, listCount - 1));
+ } else {
+ break;
+ }
+
+ if (newPosition != mDragCurrentPosition && mDragTempData != null) {
+ // 只更新临时数据列表,不立即更新数据库
+ // 这样可以避免在拖拽过程中频繁更新数据库导致的性能问题
+ if (mDragCurrentPosition >= 0 && mDragCurrentPosition < mDragTempData.length) {
+ NoteItemData draggedItem = mDragTempData[mDragCurrentPosition];
+ if (draggedItem != null && newPosition >= 0 && newPosition < mDragTempData.length) {
+ if (newPosition > mDragCurrentPosition) {
+ // 向下移动
+ for (int i = mDragCurrentPosition; i < newPosition; i++) {
+ if (i + 1 < mDragTempData.length) {
+ mDragTempData[i] = mDragTempData[i + 1];
+ }
+ }
+ } else if (newPosition < mDragCurrentPosition) {
+ // 向上移动
+ for (int i = mDragCurrentPosition; i > newPosition; i--) {
+ if (i - 1 >= 0) {
+ mDragTempData[i] = mDragTempData[i - 1];
+ }
+ }
+ }
+ // 将被拖拽的项放到新位置
+ mDragTempData[newPosition] = draggedItem;
+ // 更新当前位置
+ mDragCurrentPosition = newPosition;
+ }
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ stopDrag();
+ break;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error in handleDragEvent: " + e.getMessage(), e);
+ // 发生异常时,停止拖拽
+ stopDrag();
+ }
+ }
+
+ // 更新临时数据列表的顺序
+ private void updateTempDataOrder(int newPosition) {
+ if (mDragTempData == null || mDragStartPosition < 0 || mDragStartPosition >= mDragTempData.length) {
+ return;
+ }
+
+ // 保存当前拖拽位置
+ int currentPosition = mDragCurrentPosition;
+
+ // 先更新当前位置
+ mDragCurrentPosition = newPosition;
+
+ // 保存被拖拽的项
+ NoteItemData draggedItem = mDragTempData[currentPosition];
+
+ // 移动数组中的元素
+ if (newPosition > currentPosition) {
+ // 向下移动
+ for (int i = currentPosition; i < newPosition; i++) {
+ mDragTempData[i] = mDragTempData[i + 1];
+ }
+ } else if (newPosition < currentPosition) {
+ // 向上移动
+ for (int i = currentPosition; i > newPosition; i--) {
+ mDragTempData[i] = mDragTempData[i - 1];
+ }
+ }
+
+ // 将被拖拽的项放到新位置
+ mDragTempData[newPosition] = draggedItem;
+
+ // 更新起始位置为当前新位置
+ mDragStartPosition = newPosition;
+
+ // 立即更新数据库中的排序
+ updateSortOrderInDatabase();
+ }
+
+ // 更新数据库中的排序
+ private void updateSortOrderInDatabase() {
+ if (mDragTempData == null || mDragTempData.length == 0) {
+ return;
+ }
+
+ try {
+ // 使用ContentResolver批量更新,不需要直接操作数据库
+ ContentResolver resolver = getContentResolver();
+ if (resolver == null) {
+ return;
+ }
+
+ for (int i = 0; i < mDragTempData.length; i++) {
+ NoteItemData item = mDragTempData[i];
+ if (item != null && item.getType() == Notes.TYPE_NOTE) {
+ ContentValues values = new ContentValues();
+ values.put(NoteColumns.SORT_ORDER, i);
+ resolver.update(Notes.CONTENT_NOTE_URI,
+ values,
+ "_id=?",
+ new String[]{String.valueOf(item.getId())});
+ }
+ }
+
+ // 刷新列表
+ startAsyncNotesListQuery();
+ } catch (Exception e) {
+ Log.e(TAG, "Error updating sort order: " + e.getMessage(), e);
+ }
+ }
+
+ // 停止拖拽
+ private void stopDrag() {
+ try {
+ if (mDraggingView != null) {
+ // 恢复拖拽视图
+ try {
+ mDraggingView.setAlpha(1.0f);
+ mDraggingView.setScaleX(1.0f);
+ mDraggingView.setScaleY(1.0f);
+ mDraggingView.setTranslationY(0);
+ } catch (Exception e) {
+ Log.e(TAG, "Error resetting dragging view: " + e.getMessage(), e);
+ }
+ }
+
+ // 在拖拽结束时更新数据库
+ if (mDragTempData != null) {
+ updateSortOrderInDatabase();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error in stopDrag: " + e.getMessage(), e);
+ } finally {
+ // 无论如何都重置拖拽状态
+ mIsDragging = false;
+ mDragStartPosition = -1;
+ mDragCurrentPosition = -1;
+ mDraggingView = null;
+ mDragTempData = null;
+ }
+ }
+
+ // 获取指定位置的便签ID
+ private long getItemId(int position) {
+ Cursor cursor = mNotesListAdapter.getCursor();
+ if (cursor != null && cursor.moveToPosition(position)) {
+ return cursor.getLong(0); // ID_COLUMN 是 0
+ }
+ return -1;
+ }
}
+
diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java b/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java
index 51c9cb9..ee6c515 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java
@@ -37,6 +37,7 @@ public class NotesListAdapter extends CursorAdapter {
private HashMap mSelectedIndex;
private int mNotesCount;
private boolean mChoiceMode;
+ private String mSearchQuery;
public static class AppWidgetAttribute {
public int widgetId;
@@ -48,6 +49,13 @@ public class NotesListAdapter extends CursorAdapter {
mSelectedIndex = new HashMap();
mContext = context;
mNotesCount = 0;
+ mSearchQuery = null;
+ }
+
+ // 设置搜索查询
+ public void setSearchQuery(String query) {
+ mSearchQuery = query;
+ notifyDataSetChanged();
}
@Override
@@ -60,7 +68,7 @@ public class NotesListAdapter extends CursorAdapter {
if (view instanceof NotesListItem) {
NoteItemData itemData = new NoteItemData(context, cursor);
((NotesListItem) view).bind(context, itemData, mChoiceMode,
- isSelectedItem(cursor.getPosition()));
+ isSelectedItem(cursor.getPosition()), mSearchQuery);
}
}
@@ -91,17 +99,19 @@ public class NotesListAdapter extends CursorAdapter {
public HashSet getSelectedItemIds() {
HashSet itemSet = new HashSet();
- for (Integer position : mSelectedIndex.keySet()) {
- if (mSelectedIndex.get(position) == true) {
- Long id = getItemId(position);
- if (id == Notes.ID_ROOT_FOLDER) {
- Log.d(TAG, "Wrong item id, should not happen");
- } else {
- itemSet.add(id);
+ Cursor cursor = getCursor();
+ if (cursor != null) {
+ for (Integer position : mSelectedIndex.keySet()) {
+ if (mSelectedIndex.get(position) == true) {
+ if (cursor.moveToPosition(position)) {
+ long id = cursor.getLong(NoteItemData.ID_COLUMN);
+ if (id != Notes.ID_ROOT_FOLDER) {
+ itemSet.add(id);
+ }
+ }
}
}
}
-
return itemSet;
}
diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java b/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java
index 1221e80..ca112b4 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java
@@ -32,9 +32,12 @@ import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
public class NotesListItem extends LinearLayout {
private ImageView mAlert;
+ private ImageView mLock;
+ private ImageView mPublic;
private TextView mTitle;
private TextView mTime;
private TextView mCallName;
+ private TextView mCategory;
private NoteItemData mItemData;
private CheckBox mCheckBox;
@@ -42,13 +45,16 @@ public class NotesListItem extends LinearLayout {
super(context);
inflate(context, R.layout.note_item, this);
mAlert = (ImageView) findViewById(R.id.iv_alert_icon);
+ mLock = (ImageView) findViewById(R.id.iv_lock_icon);
+ mPublic = (ImageView) findViewById(R.id.iv_public_icon);
mTitle = (TextView) findViewById(R.id.tv_title);
mTime = (TextView) findViewById(R.id.tv_time);
mCallName = (TextView) findViewById(R.id.tv_name);
+ mCategory = (TextView) findViewById(R.id.tv_category);
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
}
- public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
+ public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked, String searchQuery) {
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
mCheckBox.setVisibility(View.VISIBLE);
mCheckBox.setChecked(checked);
@@ -64,11 +70,20 @@ public class NotesListItem extends LinearLayout {
mTitle.setText(context.getString(R.string.call_record_folder_name)
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
mAlert.setImageResource(R.drawable.call_record);
+ } else if (data.getId() == Notes.ID_TRASH_FOLER) {
+ mCallName.setVisibility(View.GONE);
+ mAlert.setVisibility(View.VISIBLE);
+ mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
+ mTitle.setText("回收站"
+ + context.getString(R.string.format_folder_files_count, data.getNotesCount()));
+ // 使用现有的clock图标作为临时垃圾桶图标
+ mAlert.setImageResource(R.drawable.clock);
} else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
mCallName.setVisibility(View.VISIBLE);
mCallName.setText(data.getCallName());
mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem);
- mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
+ String formattedSnippet = DataUtils.getFormattedSnippet(data.getSnippet());
+ mTitle.setText(highlightText(formattedSnippet, searchQuery, context));
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
@@ -76,26 +91,69 @@ public class NotesListItem extends LinearLayout {
mAlert.setVisibility(View.GONE);
}
} else {
- mCallName.setVisibility(View.GONE);
- mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
+ mCallName.setVisibility(View.GONE);
+ mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
- if (data.getType() == Notes.TYPE_FOLDER) {
- mTitle.setText(data.getSnippet()
- + context.getString(R.string.format_folder_files_count,
- data.getNotesCount()));
- mAlert.setVisibility(View.GONE);
- } else {
- mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
- if (data.hasAlert()) {
- mAlert.setImageResource(R.drawable.clock);
- mAlert.setVisibility(View.VISIBLE);
- } else {
+ if (data.getType() == Notes.TYPE_FOLDER) {
+ mTitle.setText(data.getSnippet()
+ + context.getString(R.string.format_folder_files_count,
+ data.getNotesCount()));
mAlert.setVisibility(View.GONE);
+ } else {
+ // 显示标题,如果标题为空则显示内容摘要
+ String displayText;
+ if (data.getTitle() != null && !data.getTitle().isEmpty()) {
+ displayText = data.getTitle();
+ } else {
+ String formattedSnippet = DataUtils.getFormattedSnippet(data.getSnippet());
+ displayText = formattedSnippet;
+ }
+ mTitle.setText(highlightText(displayText, searchQuery, context));
+ if (data.hasAlert()) {
+ mAlert.setImageResource(R.drawable.clock);
+ mAlert.setVisibility(View.VISIBLE);
+ } else {
+ mAlert.setVisibility(View.GONE);
+ }
}
}
- }
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
+ // 设置分类标签,优先使用标题进行分类
+ String contentForCategory = data.getTitle();
+ if (contentForCategory == null || contentForCategory.isEmpty()) {
+ contentForCategory = data.getSnippet();
+ }
+ String category = net.micode.notes.tool.CategoryUtil.autoCategorize(contentForCategory);
+ mCategory.setText(category);
+
+ // 处理锁定图标
+ if (data.isLocked() && data.getType() == Notes.TYPE_NOTE) {
+ mLock.setVisibility(View.VISIBLE);
+ mLock.setImageResource(R.drawable.clock); // 使用clock图标作为锁图标
+ } else {
+ mLock.setVisibility(View.GONE);
+ }
+
+ // 处理公开图标
+ if (data.isPublic() && data.getType() == Notes.TYPE_NOTE) {
+ mPublic.setVisibility(View.VISIBLE);
+ mPublic.setImageResource(R.drawable.call_record); // 使用call_record图标作为公开图标,与置顶图标区分
+ } else {
+ mPublic.setVisibility(View.GONE);
+ }
+
+ // 处理置顶和提醒图标
+ if (data.isPinned() && data.getType() == Notes.TYPE_NOTE) {
+ mAlert.setVisibility(View.VISIBLE);
+ mAlert.setImageResource(R.drawable.selected); // 使用selected图标作为置顶图标
+ } else if (data.hasAlert() && data.getType() == Notes.TYPE_NOTE) {
+ mAlert.setVisibility(View.VISIBLE);
+ mAlert.setImageResource(R.drawable.call_record); // 使用call_record图标作为提醒图标
+ } else {
+ mAlert.setVisibility(View.GONE);
+ }
+
setBackground(data);
}
@@ -116,6 +174,33 @@ public class NotesListItem extends LinearLayout {
}
}
+ // 高亮匹配的文本
+ private CharSequence highlightText(String text, String searchQuery, Context context) {
+ if (text == null || searchQuery == null || searchQuery.isEmpty()) {
+ return text;
+ }
+
+ android.text.SpannableString spannable = new android.text.SpannableString(text);
+ try {
+ String lowerText = text.toLowerCase();
+ String lowerQuery = searchQuery.toLowerCase();
+ int startIndex = lowerText.indexOf(lowerQuery);
+
+ while (startIndex != -1) {
+ int endIndex = startIndex + searchQuery.length();
+ spannable.setSpan(
+ new android.text.style.BackgroundColorSpan(context.getResources().getColor(R.color.user_query_highlight)),
+ startIndex, endIndex, android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ startIndex = lowerText.indexOf(lowerQuery, endIndex);
+ }
+ } catch (Exception e) {
+ // 处理可能的异常,比如空指针或索引越界
+ return text;
+ }
+
+ return spannable;
+ }
+
public NoteItemData getItemData() {
return mItemData;
}
diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java b/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java
index 07c5f7e..59e2fcc 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java
@@ -39,6 +39,7 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
+import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
@@ -57,7 +58,12 @@ public class NotesPreferenceActivity extends PreferenceActivity {
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
+ public static final String PREFERENCE_PASSWORD_KEY = "pref_key_password";
+
+ public static final String PREFERENCE_PASSWORD_SET_KEY = "pref_key_password_set";
+
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
+ private static final String PREFERENCE_PASSWORD_SETTING_KEY = "pref_password_setting_key";
private static final String AUTHORITIES_FILTER_KEY = "authorities";
@@ -78,6 +84,50 @@ public class NotesPreferenceActivity extends PreferenceActivity {
addPreferencesFromResource(R.xml.preferences);
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
+
+ // Add password setting preference
+ Preference passwordPreference = new Preference(this);
+ passwordPreference.setTitle(getString(R.string.preferences_password_title));
+ passwordPreference.setSummary(getString(R.string.preferences_password_summary));
+ passwordPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ showPasswordSettingDialog();
+ return true;
+ }
+ });
+
+ // Add friend management preference
+ Preference friendPreference = new Preference(this);
+ friendPreference.setTitle("好友");
+ friendPreference.setSummary("管理和查看好友的公开便签");
+ friendPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ // 启动好友管理活动
+ Intent intent = new Intent(NotesPreferenceActivity.this, FriendManagementActivity.class);
+ startActivity(intent);
+ return true;
+ }
+ });
+
+ // Add change background preference
+ Preference changeBackgroundPreference = new Preference(this);
+ changeBackgroundPreference.setTitle("更换背景");
+ changeBackgroundPreference.setSummary("更换便签界面的背景图片");
+ changeBackgroundPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ // 直接启动NoteEditActivity并传递更换背景的标志
+ Intent intent = new Intent(NotesPreferenceActivity.this, NoteEditActivity.class);
+ intent.putExtra("CHANGE_BACKGROUND", true);
+ startActivity(intent);
+ return true;
+ }
+ });
+
+ PreferenceCategory generalCategory = (PreferenceCategory) getPreferenceScreen().getPreference(1);
+ generalCategory.addPreference(passwordPreference);
+ generalCategory.addPreference(friendPreference);
+ generalCategory.addPreference(changeBackgroundPreference);
+
mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
@@ -360,6 +410,93 @@ public class NotesPreferenceActivity extends PreferenceActivity {
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
+ public static boolean isPasswordSet(Context context) {
+ SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
+ Context.MODE_PRIVATE);
+ return settings.getBoolean(PREFERENCE_PASSWORD_SET_KEY, false);
+ }
+
+ public static String getPassword(Context context) {
+ SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
+ Context.MODE_PRIVATE);
+ return settings.getString(PREFERENCE_PASSWORD_KEY, "");
+ }
+
+ private void setPassword(String password) {
+ SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = settings.edit();
+ editor.putString(PREFERENCE_PASSWORD_KEY, password);
+ editor.putBoolean(PREFERENCE_PASSWORD_SET_KEY, !TextUtils.isEmpty(password));
+ editor.commit();
+ }
+
+ private void showPasswordSettingDialog() {
+ final boolean hasPassword = isPasswordSet(this);
+
+ View view = LayoutInflater.from(this).inflate(R.layout.password_setting_dialog, null);
+ final EditText currentPasswordEdit = (EditText) view.findViewById(R.id.current_password);
+ final EditText newPasswordEdit = (EditText) view.findViewById(R.id.new_password);
+ final EditText confirmPasswordEdit = (EditText) view.findViewById(R.id.confirm_password);
+
+ final TextView currentPasswordLabel = (TextView) view.findViewById(R.id.current_password_label);
+
+ if (!hasPassword) {
+ currentPasswordLabel.setVisibility(View.GONE);
+ currentPasswordEdit.setVisibility(View.GONE);
+ }
+
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
+ dialogBuilder.setTitle(hasPassword ? getString(R.string.preferences_password_change_title) : getString(R.string.preferences_password_set_title));
+ dialogBuilder.setView(view);
+
+ dialogBuilder.setPositiveButton(getString(R.string.preferences_button_confirm), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ String currentPassword = currentPasswordEdit.getText().toString();
+ String newPassword = newPasswordEdit.getText().toString();
+ String confirmPassword = confirmPasswordEdit.getText().toString();
+
+ if (hasPassword) {
+ // 验证当前密码
+ if (!currentPassword.equals(getPassword(NotesPreferenceActivity.this))) {
+ Toast.makeText(NotesPreferenceActivity.this,
+ getString(R.string.preferences_password_incorrect),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+
+ // 验证新密码和确认密码
+ if (TextUtils.isEmpty(newPassword)) {
+ Toast.makeText(NotesPreferenceActivity.this,
+ getString(R.string.preferences_password_empty),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (!newPassword.equals(confirmPassword)) {
+ Toast.makeText(NotesPreferenceActivity.this,
+ getString(R.string.preferences_password_not_match),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 设置密码
+ setPassword(newPassword);
+ Toast.makeText(NotesPreferenceActivity.this,
+ getString(R.string.preferences_password_set_success),
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ dialogBuilder.setNegativeButton(getString(R.string.preferences_button_cancel), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+
+ dialogBuilder.show();
+ }
+
private class GTaskReceiver extends BroadcastReceiver {
@Override
diff --git a/src/Notes-master/src/net/micode/notes/ui/SplashActivity.java b/src/Notes-master/src/net/micode/notes/ui/SplashActivity.java
new file mode 100644
index 0000000..f3d7a3f
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/SplashActivity.java
@@ -0,0 +1,56 @@
+package net.micode.notes.ui;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.widget.LinearLayout;
+
+import net.micode.notes.R;
+
+public class SplashActivity extends Activity {
+
+ private static final int SPLASH_DISPLAY_DURATION = 2000; // 2秒
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_splash);
+
+ // 找到小米logo容器
+ LinearLayout miLogo = findViewById(R.id.mi_logo);
+
+ // 创建淡入淡出动画
+ AlphaAnimation animation = new AlphaAnimation(0.0f, 1.0f);
+ animation.setDuration(1500); // 1.5秒
+ animation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ // 动画开始时的回调
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ // 动画结束后跳转到主界面
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ Intent mainIntent = new Intent(SplashActivity.this, NotesListActivity.class);
+ SplashActivity.this.startActivity(mainIntent);
+ SplashActivity.this.finish();
+ }
+ }, 500); // 延迟500毫秒后跳转
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ // 动画重复时的回调
+ }
+ });
+
+ // 应用动画到小米logo
+ miLogo.startAnimation(animation);
+ }
+}
\ No newline at end of file