diff --git a/doc/实践统计-阅读分析和维护开源软件.xlsx b/doc/实践统计-阅读分析和维护开源软件.xlsx new file mode 100644 index 0000000..ae08ee8 Binary files /dev/null and b/doc/实践统计-阅读分析和维护开源软件.xlsx differ diff --git a/doc/实践考评-阅读维护开源软件的团队自评报告.xlsx b/doc/实践考评-阅读维护开源软件的团队自评报告.xlsx new file mode 100644 index 0000000..12a9550 Binary files /dev/null and b/doc/实践考评-阅读维护开源软件的团队自评报告.xlsx differ diff --git a/doc/实践考评-阅读维护开源软件的汇报模板(2).pptx b/doc/实践考评-阅读维护开源软件的汇报模板(2).pptx new file mode 100644 index 0000000..b9b5fc5 Binary files /dev/null and b/doc/实践考评-阅读维护开源软件的汇报模板(2).pptx differ diff --git a/doc/开源软件泛读、标注和维护报告文档.docx b/doc/开源软件泛读、标注和维护报告文档.docx new file mode 100644 index 0000000..edd8cb7 Binary files /dev/null and b/doc/开源软件泛读、标注和维护报告文档.docx differ diff --git a/src/java/net/micode/notes/data/Notes.java b/src/java/net/micode/notes/data/Notes.java index fa5943b..8970017 100644 --- a/src/java/net/micode/notes/data/Notes.java +++ b/src/java/net/micode/notes/data/Notes.java @@ -93,6 +93,14 @@ public class Notes { * Intent传递参数的键常量:通话记录的日期(call_date) */ public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date"; + /** + * Intent传递参数的键常量:标签的ID(tag_id) + */ + public static final String INTENT_EXTRA_TAG_ID = "net.micode.notes.tag_id"; + /** + * Intent传递参数的键常量:标签的名称(tag_name) + */ + public static final String INTENT_EXTRA_TAG_NAME = "net.micode.notes.tag_name"; /** * Widget类型常量:无效的Widget类型 @@ -125,11 +133,45 @@ public class Notes { public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note"); /** - * ContentProvider的URI常量:用于查询笔记明细数据的URI + * 笔记明细数据的Content URI * 格式:content://micode_notes/data */ public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); + /** + * 标签的Content URI + * 格式:content://micode_notes/tag + */ + public static final Uri CONTENT_TAG_URI = Uri.parse("content://" + AUTHORITY + "/tag"); + + /** + * 便签-标签关联的Content URI + * 格式:content://micode_notes/note_tag_relation + */ + public static final Uri CONTENT_NOTE_TAG_RELATION_URI = Uri.parse("content://" + AUTHORITY + "/note_tag_relation"); + + /** + * 加密信息的Content URI + * 格式:content://micode_notes/encryption + */ + public static final Uri CONTENT_ENCRYPTION_URI = Uri.parse("content://" + AUTHORITY + "/encryption"); + + /** + * 密保问题的Content URI + * 格式:content://micode_notes/security_question + */ + public static final Uri CONTENT_SECURITY_QUESTION_URI = Uri.parse("content://" + AUTHORITY + "/security_question"); + + /** + * Intent传递参数的键常量:便签是否加密(is_encrypted) + */ + public static final String INTENT_EXTRA_IS_ENCRYPTED = "net.micode.notes.is_encrypted"; + + /** + * Intent传递参数的键常量:便签密码(password) + */ + public static final String INTENT_EXTRA_PASSWORD = "net.micode.notes.password"; + /** * 笔记/文件夹表的列名接口 * 定义了note表中所有列的名称、数据类型和业务含义,是数据库操作和ContentProvider查询的核心列名规范 @@ -246,6 +288,162 @@ public class Notes { public static final String VERSION = "version"; } + /** + * 标签表的列名接口 + * 定义了tag表中所有列的名称、数据类型和业务含义 + */ + public interface TagColumns { + /** + * 标签的唯一ID(主键) + *

数据类型: INTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * 标签名称 + *

数据类型: TEXT

+ */ + public static final String NAME = "name"; + + /** + * 标签颜色 + *

数据类型: INTEGER (int)

+ */ + public static final String COLOR = "color"; + + /** + * 标签的创建时间 + *

数据类型: INTEGER (long)(时间戳,毫秒)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * 标签的最后修改时间 + *

数据类型: INTEGER (long)(时间戳,毫秒)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + } + + /** + * 便签-标签关联表的列名接口 + * 定义了note_tag_relation表中所有列的名称、数据类型和业务含义 + */ + public interface NoteTagRelationColumns { + /** + * 关联的唯一ID(主键) + *

数据类型: INTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * 便签ID + *

数据类型: INTEGER (long)

+ */ + public static final String NOTE_ID = "note_id"; + + /** + * 标签ID + *

数据类型: INTEGER (long)

+ */ + public static final String TAG_ID = "tag_id"; + + /** + * 关联的创建时间 + *

数据类型: INTEGER (long)(时间戳,毫秒)

+ */ + public static final String CREATED_DATE = "created_date"; + } + + /** + * 加密信息表的列名接口 + * 定义了note_encryption表中所有列的名称、数据类型和业务含义 + */ + public interface EncryptionColumns { + /** + * 加密信息的唯一ID(主键) + *

数据类型: INTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * 便签ID + *

数据类型: INTEGER (long)

+ */ + public static final String NOTE_ID = "note_id"; + + /** + * 加密后的密码哈希值 + *

数据类型: TEXT

+ */ + public static final String PASSWORD_HASH = "password_hash"; + + /** + * 加密密钥的盐值 + *

数据类型: TEXT

+ */ + public static final String SALT = "salt"; + + /** + * 初始化向量 + *

数据类型: TEXT

+ */ + public static final String IV = "iv"; + + /** + * 加密创建时间 + *

数据类型: INTEGER (long)(时间戳,毫秒)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * 加密修改时间 + *

数据类型: INTEGER (long)(时间戳,毫秒)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + } + + /** + * 密保问题表的列名接口 + * 定义了security_question表中所有列的名称、数据类型和业务含义 + */ + public interface SecurityQuestionColumns { + /** + * 密保问题的唯一ID(主键) + *

数据类型: INTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * 便签ID + *

数据类型: INTEGER (long)

+ */ + public static final String NOTE_ID = "note_id"; + + /** + * 密保问题 + *

数据类型: TEXT

+ */ + public static final String QUESTION = "question"; + + /** + * 密保答案的哈希值 + *

数据类型: TEXT

+ */ + public static final String ANSWER_HASH = "answer_hash"; + + /** + * 创建时间 + *

数据类型: INTEGER (long)(时间戳,毫秒)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * 修改时间 + *

数据类型: INTEGER (long)(时间戳,毫秒)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + } + /** * 笔记明细数据表的列名接口 * 定义了data表中所有列的名称、数据类型和业务含义,data表存储笔记的具体内容(文本、通话记录等) diff --git a/src/java/net/micode/notes/data/NotesDatabaseHelper.java b/src/java/net/micode/notes/data/NotesDatabaseHelper.java index 743abac..802479e 100644 --- a/src/java/net/micode/notes/data/NotesDatabaseHelper.java +++ b/src/java/net/micode/notes/data/NotesDatabaseHelper.java @@ -45,18 +45,26 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { private static final String DB_NAME = "note.db"; /** - * 数据库版本号,用于版本升级控制(当前为6,增加了标签功能) + * 数据库版本号,用于版本升级控制(当前为7,增加了密码保护功能) */ - private static final int DB_VERSION = 6; + private static final int DB_VERSION = 7; /** - * 数据表名称接口,定义note表、data表、标签表和便签-标签关联表的名称常量 + * 数据表名称接口,定义note表、data表、标签表、便签-标签关联表、加密表和密保问题表的名称常量 */ public interface TABLE { // 笔记/文件夹表名称 public static final String NOTE = "note"; // 笔记明细数据表名称(存储文本、通话记录等具体内容) public static final String DATA = "data"; + // 标签表名称 + public static final String TAG = "tag"; + // 便签-标签关联表名称 + public static final String NOTE_TAG_RELATION = "note_tag_relation"; + // 加密信息表名称 + public static final String ENCRYPTION = "note_encryption"; + // 密保问题表名称 + public static final String SECURITY_QUESTION = "security_question"; } /** @@ -127,6 +135,88 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { "CREATE INDEX IF NOT EXISTS note_id_index ON " + TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; + /** + * 创建tag表的SQL语句 + * tag表存储标签信息 + */ + private static final String CREATE_TAG_TABLE_SQL = + "CREATE TABLE " + TABLE.TAG + "(" + + "_id INTEGER PRIMARY KEY," + // 标签ID + "name TEXT NOT NULL UNIQUE," + // 标签名称(唯一) + "color INTEGER NOT NULL DEFAULT 0," + // 标签颜色 + "created_date INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 创建时间 + "modified_date INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)" + // 修改时间 + ");"; + + /** + * 创建note_tag_relation表的SQL语句 + * note_tag_relation表存储便签和标签的关联关系 + */ + private static final String CREATE_NOTE_TAG_RELATION_TABLE_SQL = + "CREATE TABLE " + TABLE.NOTE_TAG_RELATION + "(" + + "_id INTEGER PRIMARY KEY," + // 关联ID + "note_id INTEGER NOT NULL," + // 便签ID + "tag_id INTEGER NOT NULL," + // 标签ID + "created_date INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 创建时间 + "UNIQUE(note_id, tag_id)" + // 确保一个便签不会重复关联同一个标签 + ");"; + + /** + * 创建note_tag_relation表的索引的SQL语句 + * 索引用于提升查询性能 + */ + private static final String CREATE_NOTE_TAG_RELATION_INDEX_SQL = + "CREATE INDEX IF NOT EXISTS note_tag_index ON " + + TABLE.NOTE_TAG_RELATION + "(note_id, tag_id);"; + + /** + * 创建加密信息表的SQL语句 + * note_encryption表存储便签的加密信息 + */ + private static final String CREATE_ENCRYPTION_TABLE_SQL = + "CREATE TABLE " + TABLE.ENCRYPTION + "(" + + "_id INTEGER PRIMARY KEY," + // 加密信息ID + "note_id INTEGER NOT NULL UNIQUE," + // 便签ID(唯一) + "password_hash TEXT NOT NULL," + // 加密后的密码哈希值 + "salt TEXT NOT NULL," + // 加密密钥的盐值 + "iv TEXT NOT NULL," + // 初始化向量 + "created_date INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 创建时间 + "modified_date INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 修改时间 + "FOREIGN KEY(note_id) REFERENCES " + TABLE.NOTE + "(_id) ON DELETE CASCADE" + // 外键约束,便签删除时级联删除加密信息 + ");"; + + /** + * 创建密保问题表的SQL语句 + * security_question表存储便签的密保问题和答案 + */ + private static final String CREATE_SECURITY_QUESTION_TABLE_SQL = + "CREATE TABLE " + TABLE.SECURITY_QUESTION + "(" + + "_id INTEGER PRIMARY KEY," + // 密保问题ID + "note_id INTEGER NOT NULL," + // 便签ID + "question TEXT NOT NULL," + // 密保问题 + "answer_hash TEXT NOT NULL," + // 密保答案的哈希值 + "created_date INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 创建时间 + "modified_date INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 修改时间 + "FOREIGN KEY(note_id) REFERENCES " + TABLE.NOTE + "(_id) ON DELETE CASCADE," + // 外键约束,便签删除时级联删除密保问题 + "UNIQUE(note_id)" + // 确保一个便签只有一个密保问题 + ");"; + + /** + * 创建加密信息表的索引的SQL语句 + * 索引用于提升查询性能 + */ + private static final String CREATE_ENCRYPTION_INDEX_SQL = + "CREATE INDEX IF NOT EXISTS encryption_note_id_index ON " + + TABLE.ENCRYPTION + "(note_id);"; + + /** + * 创建密保问题表的索引的SQL语句 + * 索引用于提升查询性能 + */ + private static final String CREATE_SECURITY_QUESTION_INDEX_SQL = + "CREATE INDEX IF NOT EXISTS security_question_note_id_index ON " + + TABLE.SECURITY_QUESTION + "(note_id);"; + // ====================== 数据库触发器SQL语句(note表) ====================== @@ -394,7 +484,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { /** * 数据库首次创建时调用的方法 - * 执行note表和data表的创建逻辑 + * 执行note表、data表、标签表、便签-标签关联表、加密表和密保问题表的创建逻辑 * * @param db SQLiteDatabase对象 */ @@ -402,6 +492,93 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { public void onCreate(SQLiteDatabase db) { createNoteTable(db); createDataTable(db); + createTagTable(db); + createNoteTagRelationTable(db); + createEncryptionTable(db); + createSecurityQuestionTable(db); + } + + /** + * 检查并创建缺失的表结构 + * 用于确保标签相关、加密相关的表结构存在,即使数据库版本升级失败 + * + * @param db SQLiteDatabase对象 + */ + public void ensureTablesExist(SQLiteDatabase db) { + try { + // 尝试查询tag表,如果不存在会抛出异常 + db.rawQuery("SELECT * FROM " + TABLE.TAG + " LIMIT 1", null).close(); + } catch (Exception e) { + // tag表不存在,创建它 + createTagTable(db); + } + + try { + // 尝试查询note_tag_relation表,如果不存在会抛出异常 + db.rawQuery("SELECT * FROM " + TABLE.NOTE_TAG_RELATION + " LIMIT 1", null).close(); + } catch (Exception e) { + // note_tag_relation表不存在,创建它 + createNoteTagRelationTable(db); + } + + try { + // 尝试查询encryption表,如果不存在会抛出异常 + db.rawQuery("SELECT * FROM " + TABLE.ENCRYPTION + " LIMIT 1", null).close(); + } catch (Exception e) { + // encryption表不存在,创建它 + createEncryptionTable(db); + } + + try { + // 尝试查询security_question表,如果不存在会抛出异常 + db.rawQuery("SELECT * FROM " + TABLE.SECURITY_QUESTION + " LIMIT 1", null).close(); + } catch (Exception e) { + // security_question表不存在,创建它 + createSecurityQuestionTable(db); + } + } + + /** + * 创建tag表 + * + * @param db SQLiteDatabase对象 + */ + public void createTagTable(SQLiteDatabase db) { + db.execSQL(CREATE_TAG_TABLE_SQL); + Log.d(TAG, "tag table has been created"); + } + + /** + * 创建note_tag_relation表 + * + * @param db SQLiteDatabase对象 + */ + public void createNoteTagRelationTable(SQLiteDatabase db) { + db.execSQL(CREATE_NOTE_TAG_RELATION_TABLE_SQL); + db.execSQL(CREATE_NOTE_TAG_RELATION_INDEX_SQL); + Log.d(TAG, "note_tag_relation table has been created"); + } + + /** + * 创建加密信息表 + * + * @param db SQLiteDatabase对象 + */ + public void createEncryptionTable(SQLiteDatabase db) { + db.execSQL(CREATE_ENCRYPTION_TABLE_SQL); + db.execSQL(CREATE_ENCRYPTION_INDEX_SQL); + Log.d(TAG, "encryption table has been created"); + } + + /** + * 创建密保问题表 + * + * @param db SQLiteDatabase对象 + */ + public void createSecurityQuestionTable(SQLiteDatabase db) { + db.execSQL(CREATE_SECURITY_QUESTION_TABLE_SQL); + db.execSQL(CREATE_SECURITY_QUESTION_INDEX_SQL); + Log.d(TAG, "security_question table has been created"); } /** @@ -447,7 +624,28 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { oldVersion++; } - + // 从版本5升级到版本6 + if (oldVersion == 5) { + // 创建tag表 + db.execSQL(CREATE_TAG_TABLE_SQL); + // 创建note_tag_relation表 + db.execSQL(CREATE_NOTE_TAG_RELATION_TABLE_SQL); + // 创建索引 + db.execSQL(CREATE_NOTE_TAG_RELATION_INDEX_SQL); + oldVersion++; + } + + // 从版本6升级到版本7 + if (oldVersion == 6) { + // 创建加密信息表 + db.execSQL(CREATE_ENCRYPTION_TABLE_SQL); + // 创建密保问题表 + db.execSQL(CREATE_SECURITY_QUESTION_TABLE_SQL); + // 创建索引 + db.execSQL(CREATE_ENCRYPTION_INDEX_SQL); + db.execSQL(CREATE_SECURITY_QUESTION_INDEX_SQL); + oldVersion++; + } // 如果需要,重建触发器 if (reCreateTriggers) { diff --git a/src/java/net/micode/notes/data/NotesProvider.java b/src/java/net/micode/notes/data/NotesProvider.java index 392cae6..becc222 100644 --- a/src/java/net/micode/notes/data/NotesProvider.java +++ b/src/java/net/micode/notes/data/NotesProvider.java @@ -91,6 +91,38 @@ public class NotesProvider extends ContentProvider { * Uri匹配类型:提供搜索建议(适配安卓SearchManager) */ private static final int URI_SEARCH_SUGGEST = 6; + /** + * Uri匹配类型:查询/操作tag表的所有数据 + */ + private static final int URI_TAG = 7; + /** + * Uri匹配类型:查询/操作tag表的单条数据(通过ID,如tag/1) + */ + private static final int URI_TAG_ITEM = 8; + /** + * Uri匹配类型:查询/操作note_tag_relation表的所有数据 + */ + private static final int URI_NOTE_TAG_RELATION = 9; + /** + * Uri匹配类型:查询/操作note_tag_relation表的单条数据(通过ID,如note_tag_relation/1) + */ + private static final int URI_NOTE_TAG_RELATION_ITEM = 10; + /** + * Uri匹配类型:查询/操作加密信息表的所有数据 + */ + private static final int URI_ENCRYPTION = 11; + /** + * Uri匹配类型:查询/操作加密信息表的单条数据(通过ID,如encryption/1) + */ + private static final int URI_ENCRYPTION_ITEM = 12; + /** + * Uri匹配类型:查询/操作密保问题表的所有数据 + */ + private static final int URI_SECURITY_QUESTION = 13; + /** + * Uri匹配类型:查询/操作密保问题表的单条数据(通过ID,如security_question/1) + */ + private static final int URI_SECURITY_QUESTION_ITEM = 14; /** @@ -114,6 +146,24 @@ public class NotesProvider extends ContentProvider { mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); // 匹配搜索建议(带关键词):content://micode_notes/suggestions/query/关键词 -> URI_SEARCH_SUGGEST mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); + // 匹配tag表所有数据:content://micode_notes/tag -> URI_TAG + mMatcher.addURI(Notes.AUTHORITY, "tag", URI_TAG); + // 匹配tag表单条数据:content://micode_notes/tag/# -> URI_TAG_ITEM + mMatcher.addURI(Notes.AUTHORITY, "tag/#", URI_TAG_ITEM); + // 匹配note_tag_relation表所有数据:content://micode_notes/note_tag_relation -> URI_NOTE_TAG_RELATION + mMatcher.addURI(Notes.AUTHORITY, "note_tag_relation", URI_NOTE_TAG_RELATION); + // 匹配note_tag_relation表单条数据:content://micode_notes/note_tag_relation/# -> URI_NOTE_TAG_RELATION_ITEM + mMatcher.addURI(Notes.AUTHORITY, "note_tag_relation/#", URI_NOTE_TAG_RELATION_ITEM); + + // 匹配加密信息表所有数据:content://micode_notes/encryption -> URI_ENCRYPTION + mMatcher.addURI(Notes.AUTHORITY, "encryption", URI_ENCRYPTION); + // 匹配加密信息表单条数据:content://micode_notes/encryption/# -> URI_ENCRYPTION_ITEM + mMatcher.addURI(Notes.AUTHORITY, "encryption/#", URI_ENCRYPTION_ITEM); + + // 匹配密保问题表所有数据:content://micode_notes/security_question -> URI_SECURITY_QUESTION + mMatcher.addURI(Notes.AUTHORITY, "security_question", URI_SECURITY_QUESTION); + // 匹配密保问题表单条数据:content://micode_notes/security_question/# -> URI_SECURITY_QUESTION_ITEM + mMatcher.addURI(Notes.AUTHORITY, "security_question/#", URI_SECURITY_QUESTION_ITEM); } @@ -152,6 +202,15 @@ public class NotesProvider extends ContentProvider { public boolean onCreate() { // 获取NotesDatabaseHelper的单例实例,上下文使用ContentProvider的上下文 mHelper = NotesDatabaseHelper.getInstance(getContext()); + + // 确保标签相关的表结构存在 + try { + SQLiteDatabase db = mHelper.getWritableDatabase(); + mHelper.ensureTablesExist(db); + } catch (Exception e) { + // 忽略异常,继续运行 + } + return true; } @@ -295,6 +354,54 @@ public class NotesProvider extends ContentProvider { } break; + case URI_TAG: + // 查询tag表的所有数据 + c = db.query(TABLE.TAG, projection, selection, selectionArgs, null, null, sortOrder); + break; + + case URI_TAG_ITEM: + // 获取Uri中的ID + id = uri.getPathSegments().get(1); + // 查询tag表的单条数据 + c = db.query(TABLE.TAG, projection, "_id=" + id + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + + case URI_NOTE_TAG_RELATION: + // 查询note_tag_relation表的所有数据 + c = db.query(TABLE.NOTE_TAG_RELATION, projection, selection, selectionArgs, null, null, sortOrder); + break; + + case URI_NOTE_TAG_RELATION_ITEM: + // 获取Uri中的ID + id = uri.getPathSegments().get(1); + // 查询note_tag_relation表的单条数据 + c = db.query(TABLE.NOTE_TAG_RELATION, projection, "_id=" + id + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + + case URI_ENCRYPTION: + // 查询加密信息表的所有数据 + c = db.query(TABLE.ENCRYPTION, projection, selection, selectionArgs, null, null, sortOrder); + break; + + case URI_ENCRYPTION_ITEM: + // 获取Uri中的ID + id = uri.getPathSegments().get(1); + // 查询加密信息表的单条数据 + c = db.query(TABLE.ENCRYPTION, projection, "_id=" + id + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + + case URI_SECURITY_QUESTION: + // 查询密保问题表的所有数据 + c = db.query(TABLE.SECURITY_QUESTION, projection, selection, selectionArgs, null, null, sortOrder); + break; + + case URI_SECURITY_QUESTION_ITEM: + // 获取Uri中的ID + id = uri.getPathSegments().get(1); + // 查询密保问题表的单条数据 + c = db.query(TABLE.SECURITY_QUESTION, projection, "_id=" + id + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + default: // 未知Uri,抛出异常 throw new IllegalArgumentException("Unknown URI " + uri); @@ -318,7 +425,7 @@ public class NotesProvider extends ContentProvider { public Uri insert(Uri uri, ContentValues values) { // 获取可写的SQLiteDatabase对象(插入操作需要写权限) SQLiteDatabase db = mHelper.getWritableDatabase(); - long dataId = 0, noteId = 0, insertedId = 0; // 存储插入的ID + long dataId = 0, noteId = 0, insertedId = 0, tagId = 0, relationId = 0; // 存储插入的ID // 根据Uri匹配的类型执行插入逻辑 switch (mMatcher.match(uri)) { @@ -337,6 +444,39 @@ public class NotesProvider extends ContentProvider { // 插入data表,获取插入的ID insertedId = dataId = db.insert(TABLE.DATA, null, values); break; + case URI_TAG: + // 插入tag表,获取插入的ID + insertedId = tagId = db.insert(TABLE.TAG, null, values); + break; + case URI_NOTE_TAG_RELATION: + // 插入note_tag_relation表时,获取关联的noteId和tagId + if (values.containsKey(Notes.NoteTagRelationColumns.NOTE_ID)) { + noteId = values.getAsLong(Notes.NoteTagRelationColumns.NOTE_ID); + } + if (values.containsKey(Notes.NoteTagRelationColumns.TAG_ID)) { + tagId = values.getAsLong(Notes.NoteTagRelationColumns.TAG_ID); + } + // 插入note_tag_relation表,获取插入的ID + insertedId = relationId = db.insert(TABLE.NOTE_TAG_RELATION, null, values); + break; + + case URI_ENCRYPTION: + // 插入加密信息表时,获取关联的noteId + if (values.containsKey(Notes.EncryptionColumns.NOTE_ID)) { + noteId = values.getAsLong(Notes.EncryptionColumns.NOTE_ID); + } + // 插入加密信息表,获取插入的ID + insertedId = db.insert(TABLE.ENCRYPTION, null, values); + break; + + case URI_SECURITY_QUESTION: + // 插入密保问题表时,获取关联的noteId + if (values.containsKey(Notes.SecurityQuestionColumns.NOTE_ID)) { + noteId = values.getAsLong(Notes.SecurityQuestionColumns.NOTE_ID); + } + // 插入密保问题表,获取插入的ID + insertedId = db.insert(TABLE.SECURITY_QUESTION, null, values); + break; default: // 未知Uri,抛出异常 @@ -355,7 +495,17 @@ public class NotesProvider extends ContentProvider { ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); } + // 发送通知:tag表数据变更,通知对应的Uri + if (tagId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_TAG_URI, tagId), null); + } + // 发送通知:note_tag_relation表数据变更,通知对应的Uri + if (relationId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_TAG_RELATION_URI, relationId), null); + } // 返回包含插入ID的新Uri return ContentUris.withAppendedId(uri, insertedId); @@ -377,6 +527,7 @@ public class NotesProvider extends ContentProvider { SQLiteDatabase db = mHelper.getWritableDatabase(); boolean deleteData = false; // 标记是否删除的是data表数据 long noteId = 0; // 用于存储便签ID,以便发送通知 + long tagId = 0; // 用于存储标签ID,以便发送通知 // 根据Uri匹配的类型执行删除逻辑 switch (mMatcher.match(uri)) { @@ -410,8 +561,47 @@ public class NotesProvider extends ContentProvider { DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); deleteData = true; break; + case URI_TAG: + // 删除tag表数据 + count = db.delete(TABLE.TAG, selection, selectionArgs); + break; + case URI_TAG_ITEM: + // 获取Uri中的ID,删除tag表单条数据 + id = uri.getPathSegments().get(1); + tagId = Long.valueOf(id); + count = db.delete(TABLE.TAG, "_id=" + id + parseSelection(selection), selectionArgs); + break; + case URI_NOTE_TAG_RELATION: + // 删除note_tag_relation表数据 + count = db.delete(TABLE.NOTE_TAG_RELATION, selection, selectionArgs); + break; + case URI_NOTE_TAG_RELATION_ITEM: + // 获取Uri中的ID,删除note_tag_relation表单条数据 + id = uri.getPathSegments().get(1); + count = db.delete(TABLE.NOTE_TAG_RELATION, "_id=" + id + parseSelection(selection), selectionArgs); + break; + case URI_ENCRYPTION: + // 删除加密信息表数据 + count = db.delete(TABLE.ENCRYPTION, selection, selectionArgs); + break; + case URI_ENCRYPTION_ITEM: + // 获取Uri中的ID,删除加密信息表单条数据 + id = uri.getPathSegments().get(1); + count = db.delete(TABLE.ENCRYPTION, "_id=" + id + parseSelection(selection), selectionArgs); + break; + + case URI_SECURITY_QUESTION: + // 删除密保问题表数据 + count = db.delete(TABLE.SECURITY_QUESTION, selection, selectionArgs); + break; + + case URI_SECURITY_QUESTION_ITEM: + // 获取Uri中的ID,删除密保问题表单条数据 + id = uri.getPathSegments().get(1); + count = db.delete(TABLE.SECURITY_QUESTION, "_id=" + id + parseSelection(selection), selectionArgs); + break; default: throw new IllegalArgumentException("Unknown URI " + uri); @@ -428,6 +618,11 @@ public class NotesProvider extends ContentProvider { getContext().getContentResolver().notifyChange( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); } + // 如果是标签相关操作,通知对应的标签Uri + if (tagId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_TAG_URI, tagId), null); + } // 通知当前Uri的数据变更 getContext().getContentResolver().notifyChange(uri, null); } @@ -451,6 +646,7 @@ public class NotesProvider extends ContentProvider { SQLiteDatabase db = mHelper.getWritableDatabase(); boolean updateData = false; // 标记是否更新的是data表数据 long noteId = 0; // 用于存储便签ID,以便发送通知 + long tagId = 0; // 用于存储标签ID,以便发送通知 // 根据Uri匹配的类型执行更新逻辑 switch (mMatcher.match(uri)) { @@ -479,8 +675,47 @@ public class NotesProvider extends ContentProvider { + parseSelection(selection), selectionArgs); updateData = true; break; + case URI_TAG: + // 更新tag表数据 + count = db.update(TABLE.TAG, values, selection, selectionArgs); + break; + case URI_TAG_ITEM: + // 获取Uri中的ID,更新tag表单条数据 + id = uri.getPathSegments().get(1); + tagId = Long.valueOf(id); + count = db.update(TABLE.TAG, values, "_id=" + id + parseSelection(selection), selectionArgs); + break; + case URI_NOTE_TAG_RELATION: + // 更新note_tag_relation表数据 + count = db.update(TABLE.NOTE_TAG_RELATION, values, selection, selectionArgs); + break; + case URI_NOTE_TAG_RELATION_ITEM: + // 获取Uri中的ID,更新note_tag_relation表单条数据 + id = uri.getPathSegments().get(1); + count = db.update(TABLE.NOTE_TAG_RELATION, values, "_id=" + id + parseSelection(selection), selectionArgs); + break; + case URI_ENCRYPTION: + // 更新加密信息表数据 + count = db.update(TABLE.ENCRYPTION, values, selection, selectionArgs); + break; + + case URI_ENCRYPTION_ITEM: + // 获取Uri中的ID,更新加密信息表单条数据 + id = uri.getPathSegments().get(1); + count = db.update(TABLE.ENCRYPTION, values, "_id=" + id + parseSelection(selection), selectionArgs); + break; + + case URI_SECURITY_QUESTION: + // 更新密保问题表数据 + count = db.update(TABLE.SECURITY_QUESTION, values, selection, selectionArgs); + break; + case URI_SECURITY_QUESTION_ITEM: + // 获取Uri中的ID,更新密保问题表单条数据 + id = uri.getPathSegments().get(1); + count = db.update(TABLE.SECURITY_QUESTION, values, "_id=" + id + parseSelection(selection), selectionArgs); + break; default: throw new IllegalArgumentException("Unknown URI " + uri); @@ -497,6 +732,11 @@ public class NotesProvider extends ContentProvider { getContext().getContentResolver().notifyChange( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); } + // 如果是标签相关操作,通知对应的标签Uri + if (tagId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_TAG_URI, tagId), null); + } // 通知当前Uri的数据变更 getContext().getContentResolver().notifyChange(uri, null); } diff --git a/src/java/net/micode/notes/tool/EncryptionUtils.java b/src/java/net/micode/notes/tool/EncryptionUtils.java new file mode 100644 index 0000000..4a20413 --- /dev/null +++ b/src/java/net/micode/notes/tool/EncryptionUtils.java @@ -0,0 +1,249 @@ +/* + * 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.util.Base64; +import android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * 加密工具类,提供AES加密/解密、密码哈希生成和验证、密保问题答案哈希生成和验证等功能 + */ +public class EncryptionUtils { + private static final String TAG = "EncryptionUtils"; + + // ====================== 常量定义 ====================== + + /** + * 加密算法:AES/CBC/PKCS7Padding + */ + public static final String ENCRYPTION_ALGORITHM = "AES/CBC/PKCS7Padding"; + + /** + * 密钥生成算法:PBKDF2WithHmacSHA256 + */ + private static final String KEY_GENERATION_ALGORITHM = "PBKDF2WithHmacSHA256"; + + /** + * 哈希算法:SHA-256 + */ + private static final String HASH_ALGORITHM = "SHA-256"; + + /** + * 密钥长度:256位 + */ + private static final int KEY_LENGTH = 256; + + /** + * 盐值长度:16字节 + */ + private static final int SALT_LENGTH = 16; + + /** + * 初始化向量长度:16字节 + */ + private static final int IV_LENGTH = 16; + + /** + * 迭代次数:10000 + */ + private static final int ITERATION_COUNT = 10000; + + // ====================== 工具方法 ====================== + + /** + * 生成随机盐值 + * @return 随机盐值的Base64编码字符串 + */ + public static String generateSalt() { + SecureRandom random = new SecureRandom(); + byte[] salt = new byte[SALT_LENGTH]; + random.nextBytes(salt); + return Base64.encodeToString(salt, Base64.NO_WRAP); + } + + /** + * 生成随机初始化向量 + * @return 随机初始化向量的Base64编码字符串 + */ + public static String generateIv() { + SecureRandom random = new SecureRandom(); + byte[] iv = new byte[IV_LENGTH]; + random.nextBytes(iv); + return Base64.encodeToString(iv, Base64.NO_WRAP); + } + + /** + * 基于密码和盐值生成密钥 + * @param password 密码 + * @param salt 盐值的Base64编码字符串 + * @return 生成的密钥 + * @throws GeneralSecurityException 安全异常 + */ + public static SecretKey generateKey(String password, String salt) throws GeneralSecurityException { + byte[] saltBytes = Base64.decode(salt, Base64.NO_WRAP); + PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), saltBytes, ITERATION_COUNT, KEY_LENGTH); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KEY_GENERATION_ALGORITHM); + byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded(); + return new SecretKeySpec(keyBytes, "AES"); + } + + /** + * 加密文本 + * @param plainText 明文文本 + * @param password 密码 + * @param salt 盐值的Base64编码字符串 + * @param iv 初始化向量的Base64编码字符串 + * @return 加密后的文本的Base64编码字符串 + * @throws GeneralSecurityException 安全异常 + */ + public static String encrypt(String plainText, String password, String salt, String iv) throws GeneralSecurityException { + SecretKey key = generateKey(password, salt); + byte[] ivBytes = Base64.decode(iv, Base64.NO_WRAP); + + Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(ivBytes)); + + try { + byte[] encryptedBytes = cipher.doFinal(plainText.getBytes("UTF-8")); + return Base64.encodeToString(encryptedBytes, Base64.NO_WRAP); + } catch (UnsupportedEncodingException e) { + // UTF-8 是标准字符编码,不应该抛出此异常 + throw new RuntimeException("UTF-8 encoding not supported", e); + } + } + + /** + * 解密文本 + * @param encryptedText 加密文本的Base64编码字符串 + * @param password 密码 + * @param salt 盐值的Base64编码字符串 + * @param iv 初始化向量的Base64编码字符串 + * @return 解密后的明文文本 + * @throws GeneralSecurityException 安全异常 + */ + public static String decrypt(String encryptedText, String password, String salt, String iv) throws GeneralSecurityException { + SecretKey key = generateKey(password, salt); + byte[] ivBytes = Base64.decode(iv, Base64.NO_WRAP); + + Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivBytes)); + + byte[] encryptedBytes = Base64.decode(encryptedText, Base64.NO_WRAP); + byte[] decryptedBytes = cipher.doFinal(encryptedBytes); + + try { + return new String(decryptedBytes, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // UTF-8 是标准字符编码,不应该抛出此异常 + throw new RuntimeException("UTF-8 encoding not supported", e); + } + } + + /** + * 生成密码哈希值 + * @param password 密码 + * @param salt 盐值的Base64编码字符串 + * @return 密码哈希值的Base64编码字符串 + */ + public static String generatePasswordHash(String password, String salt) { + try { + byte[] saltBytes = Base64.decode(salt, Base64.NO_WRAP); + byte[] passwordBytes = password.getBytes("UTF-8"); + + // 合并密码和盐值 + byte[] combinedBytes = new byte[passwordBytes.length + saltBytes.length]; + System.arraycopy(passwordBytes, 0, combinedBytes, 0, passwordBytes.length); + System.arraycopy(saltBytes, 0, combinedBytes, passwordBytes.length, saltBytes.length); + + // 计算哈希值 + MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM); + byte[] hashBytes = digest.digest(combinedBytes); + + return Base64.encodeToString(hashBytes, Base64.NO_WRAP); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "Error generating password hash", e); + return null; + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Error generating password hash", e); + return null; + } + } + + /** + * 验证密码 + * @param password 待验证的密码 + * @param salt 盐值的Base64编码字符串 + * @param storedHash 存储的哈希值的Base64编码字符串 + * @return 密码是否正确 + */ + public static boolean verifyPassword(String password, String salt, String storedHash) { + String generatedHash = generatePasswordHash(password, salt); + return generatedHash != null && generatedHash.equals(storedHash); + } + + /** + * 生成密保问题答案的哈希值 + * @param answer 密保问题答案 + * @param salt 盐值的Base64编码字符串 + * @return 答案哈希值的Base64编码字符串 + */ + public static String generateAnswerHash(String answer, String salt) { + // 转换为小写并去除空格,提高用户体验 + String normalizedAnswer = answer.toLowerCase().trim(); + return generatePasswordHash(normalizedAnswer, salt); + } + + /** + * 验证密保问题答案 + * @param answer 待验证的答案 + * @param salt 盐值的Base64编码字符串 + * @param storedHash 存储的哈希值的Base64编码字符串 + * @return 答案是否正确 + */ + public static boolean verifyAnswer(String answer, String salt, String storedHash) { + String normalizedAnswer = answer.toLowerCase().trim(); + String generatedHash = generatePasswordHash(normalizedAnswer, salt); + return generatedHash != null && generatedHash.equals(storedHash); + } + + /** + * 生成随机密钥(用于测试或特殊情况) + * @return 生成的密钥的Base64编码字符串 + * @throws NoSuchAlgorithmException 算法不存在异常 + */ + public static String generateRandomKey() throws NoSuchAlgorithmException { + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + keyGenerator.init(KEY_LENGTH); + SecretKey key = keyGenerator.generateKey(); + return Base64.encodeToString(key.getEncoded(), Base64.NO_WRAP); + } +} diff --git a/src/java/net/micode/notes/tool/ResourceParser.java b/src/java/net/micode/notes/tool/ResourceParser.java index af9099f..910c46d 100644 --- a/src/java/net/micode/notes/tool/ResourceParser.java +++ b/src/java/net/micode/notes/tool/ResourceParser.java @@ -217,6 +217,18 @@ public class ResourceParser { public static int getFolderBgRes() { return R.drawable.list_folder; } + + /** + * 获取夜间模式下的背景资源ID + * 夜间模式下使用统一的深色背景 + * @param positionType 位置类型:0=首项, 1=中间项, 2=末项, 3=单独项 + * @return 夜间模式背景资源ID + */ + public static int getNightBgRes(int positionType) { + // 夜间模式下使用简单的纯色背景 + // 实际应用中可以创建专门的夜间模式背景图片 + return android.R.color.transparent; // 透明背景,使用主题背景色 + } } // ======================= 小部件背景资源类 ======================= diff --git a/src/java/net/micode/notes/ui/NoteEditActivity.java b/src/java/net/micode/notes/ui/NoteEditActivity.java index 03fedc5..e40d593 100644 --- a/src/java/net/micode/notes/ui/NoteEditActivity.java +++ b/src/java/net/micode/notes/ui/NoteEditActivity.java @@ -27,13 +27,16 @@ import android.app.PendingIntent; // 待定意图 import android.app.SearchManager; // 搜索管理器 import android.appwidget.AppWidgetManager; // 应用小部件管理器 import android.content.ContentUris; // URI工具 +import android.content.ContentValues; // 内容值 import android.content.Context; // 上下文 import android.content.DialogInterface; // 对话框接口 import android.content.Intent; // 意图 import android.content.SharedPreferences; // 偏好设置 +import android.database.Cursor; // 数据库游标 import android.graphics.Paint; // 画笔,用于文本样式 import android.os.Bundle; // 状态保存 import android.preference.PreferenceManager; // 偏好设置管理器 +import android.util.Base64; // Base64编码工具 // Android文本处理 import android.text.Editable; // 可编辑文本 import android.text.Spannable; // 可设置样式的文本 @@ -52,14 +55,16 @@ import android.view.MenuItem; // 菜单项 import android.view.MotionEvent; // 触摸事件 import android.view.View; // 视图基类 import android.view.View.OnClickListener; // 点击监听器 +import android.view.Window; // 窗口 import android.view.WindowManager; // 窗口管理器 // Android控件 import android.widget.CheckBox; // 复选框 import android.widget.CompoundButton; // 复合按钮 -import android.widget.CompoundButton.OnCheckedChangeListener; // 复选框状态变化监听 import android.widget.EditText; // 编辑框 import android.widget.ImageView; // 图片视图 import android.widget.LinearLayout; // 线性布局 +import android.widget.RadioButton; // 单选按钮 +import android.widget.RadioGroup; // 单选按钮组 import android.widget.TextView; // 文本视图 import android.widget.Toast; // 提示信息 @@ -73,6 +78,7 @@ import net.micode.notes.model.WorkingNote; // 工作便签模型 import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; // 便签设置变化监听 // 应用工具 import net.micode.notes.tool.DataUtils; // 数据工具 +import net.micode.notes.tool.EncryptionUtils; // 加密工具 import net.micode.notes.tool.ResourceParser; // 资源解析器 import net.micode.notes.tool.ResourceParser.TextAppearanceResources; // 文本外观资源 import net.micode.notes.tool.SearchHistoryManager; // 搜索历史管理器 @@ -86,6 +92,7 @@ import net.micode.notes.widget.NoteWidgetProvider_2x; // 2x小部件 import net.micode.notes.widget.NoteWidgetProvider_4x; // 4x小部件 // Java集合 +import java.util.ArrayList; // 数组列表 import java.util.HashMap; // 哈希映射 import java.util.HashSet; // 哈希集合 import java.util.List; // 列表接口 @@ -93,6 +100,10 @@ import java.util.Map; // 映射接口 // Java正则表达式 import java.util.regex.Matcher; // 正则匹配器 import java.util.regex.Pattern; // 正则模式 +// Java加密 +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; // ======================= 便签编辑Activity ======================= /** @@ -195,6 +206,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, // 偏好设置 private SharedPreferences mSharedPrefs; // 偏好设置 + + // 密码保护相关 + private boolean mIsEncrypted; // 便签是否加密 + private String mPassword; // 当前输入的密码 + private boolean mPasswordVerified; // 密码是否已验证 + private long mNoteId; // 当前便签ID private int mFontSizeId; // 当前字体大小ID private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; // 字体大小偏好键 @@ -222,6 +239,11 @@ public class NoteEditActivity extends Activity implements OnClickListener, @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // 初始化密码保护相关变量 + mIsEncrypted = false; + mPassword = null; + mPasswordVerified = false; + mNoteId = -1; // 设置布局 this.setContentView(R.layout.note_edit); @@ -233,6 +255,8 @@ public class NoteEditActivity extends Activity implements OnClickListener, } // 初始化资源 initResources(); + // 检查是否需要密码验证 + checkEncryptionStatus(); } /** @@ -374,16 +398,6 @@ public class NoteEditActivity extends Activity implements OnClickListener, mNoteEditor.setTextAppearance(this, TextAppearanceResources .getTexAppearanceResource(mFontSizeId)); - // 根据模式显示内容 - 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); @@ -402,8 +416,30 @@ public class NoteEditActivity extends Activity implements OnClickListener, // 显示提醒信息 showAlertHeader(); - // 更新字数统计 - updateWordCount(); + // 检查是否需要密码验证 + if (mIsEncrypted && !mPasswordVerified) { + // 加密便签且密码未验证,不显示内容 + mNoteEditor.setText(""); + mEditTextList.removeAllViews(); + } else if (!mIsEncrypted) { + // 非加密便签,显示内容 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 清单模式 + switchToListMode(mWorkingNote.getContent()); + } else { + // 普通模式,高亮搜索关键词 + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mNoteEditor.setSelection(mNoteEditor.getText().length()); + } + + // 更新字数统计 + updateWordCount(); + } else { + // 加密便签且密码已验证,保持当前显示的内容不变 + // 因为我们已经通过loadNoteContent()方法加载了解密后的内容 + // 更新字数统计 + updateWordCount(); + } } /** @@ -571,8 +607,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, @Override protected void onPause() { super.onPause(); - if(saveNote()) { - Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); + if(mIsEncrypted && mPasswordVerified) { + // 加密便签,使用加密保存 + saveNoteWithEncryption(); + Log.d(TAG, "Encrypted note data was saved with length:" + mWorkingNote.getContent().length()); + } else { + // 非加密便签,直接保存 + if(saveNote()) { + Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); + } } clearSettingState(); } @@ -792,6 +835,28 @@ public class NoteEditActivity extends Activity implements OnClickListener, Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("image/*"); startActivityForResult(intent, REQUEST_CODE_IMAGE_SELECTION); + } else if (itemId == R.id.menu_tags) { + // 显示标签选择对话框 + showTagSelectorDialog(); + } else if (itemId == R.id.menu_password) { + // 处理密码保护菜单项 + if (mIsEncrypted) { + // 便签已加密,显示移除密码保护的选项 + new AlertDialog.Builder(this) + .setTitle("密码保护") + .setMessage("当前便签已加密,是否要移除密码保护?") + .setPositiveButton("移除", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + removePasswordProtection(); + } + }) + .setNegativeButton("取消", null) + .show(); + } else { + // 便签未加密,显示设置密码的对话框 + showPasswordSettingDialog(); + } } else { // 默认分支(原default) } @@ -1028,7 +1093,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item)); - cb.setOnCheckedChangeListener(new OnCheckedChangeListener() { + cb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { // 根据勾选状态设置删除线 if (isChecked) { @@ -1158,7 +1223,10 @@ public class NoteEditActivity extends Activity implements OnClickListener, * @return true: 保存成功; false: 保存失败 */ private boolean saveNote() { - getWorkingText(); + if (!mIsEncrypted || !mPasswordVerified) { + // 非加密便签或密码未验证,从编辑器获取内容 + getWorkingText(); + } boolean saved = mWorkingNote.saveNote(); if (saved) { setResult(RESULT_OK); @@ -1220,4 +1288,1002 @@ public class NoteEditActivity extends Activity implements OnClickListener, private void showToast(int resId, int duration) { Toast.makeText(this, resId, duration).show(); } + + /** + * 显示标签选择对话框 + */ + private void showTagSelectorDialog() { + // 准备标签列表 + final List tagNames = new ArrayList<>(); + final List tagIds = new ArrayList<>(); + final List tagColors = new ArrayList<>(); + + try { + // 查询所有标签 + Cursor tagCursor = getContentResolver().query( + Notes.CONTENT_TAG_URI, + new String[]{Notes.TagColumns.ID, Notes.TagColumns.NAME, Notes.TagColumns.COLOR}, + null, null, Notes.TagColumns.NAME + " ASC"); + + if (tagCursor != null) { + try { + while (tagCursor.moveToNext()) { + tagNames.add(tagCursor.getString(tagCursor.getColumnIndex(Notes.TagColumns.NAME))); + tagIds.add(tagCursor.getLong(tagCursor.getColumnIndex(Notes.TagColumns.ID))); + tagColors.add(tagCursor.getInt(tagCursor.getColumnIndex(Notes.TagColumns.COLOR))); + } + } finally { + tagCursor.close(); + } + } + } catch (Exception e) { + // 表不存在或其他异常,忽略,继续显示对话框 + } + + // 添加"新建标签"选项 + tagNames.add("New Tag"); + + // 显示标签选择对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Select Tag"); + builder.setItems(tagNames.toArray(new String[0]), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == tagNames.size() - 1) { + // 新建标签 + showCreateTagDialog(); + } else { + // 选择已有标签 + long tagId = tagIds.get(which); + addTagToNote(tagId); + } + } + }); + builder.setNegativeButton("Cancel", null); + builder.show(); + } + + /** + * 显示新建标签对话框 + */ + private void showCreateTagDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("New Tag"); + + final EditText input = new EditText(this); + input.setHint("Tag name"); + builder.setView(input); + + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String tagName = input.getText().toString().trim(); + if (!TextUtils.isEmpty(tagName)) { + long tagId = createTag(tagName); + if (tagId > 0) { + addTagToNote(tagId); + } + } + } + }); + builder.setNegativeButton("Cancel", null); + builder.show(); + } + + /** + * 创建新标签 + * @param tagName 标签名称 + * @return 标签ID + */ + private long createTag(String tagName) { + try { + ContentValues values = new ContentValues(); + values.put(Notes.TagColumns.NAME, tagName); + values.put(Notes.TagColumns.COLOR, 0); // 默认颜色 + values.put(Notes.TagColumns.CREATED_DATE, System.currentTimeMillis()); + values.put(Notes.TagColumns.MODIFIED_DATE, System.currentTimeMillis()); + + Uri uri = getContentResolver().insert(Notes.CONTENT_TAG_URI, values); + if (uri != null) { + return ContentUris.parseId(uri); + } + } catch (Exception e) { + // 表不存在或其他异常,返回-1 + } + return -1; + } + + /** + * 为便签添加标签 + * @param tagId 标签ID + */ + private void addTagToNote(long tagId) { + try { + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + + long noteId = mWorkingNote.getNoteId(); + Log.d("NoteEditActivity", "addTagToNote: noteId=" + noteId + ", tagId=" + tagId); + + if (noteId > 0) { + ContentValues values = new ContentValues(); + values.put(Notes.NoteTagRelationColumns.NOTE_ID, noteId); + values.put(Notes.NoteTagRelationColumns.TAG_ID, tagId); + values.put(Notes.NoteTagRelationColumns.CREATED_DATE, System.currentTimeMillis()); + + Uri resultUri = getContentResolver().insert(Notes.CONTENT_NOTE_TAG_RELATION_URI, values); + Log.d("NoteEditActivity", "addTagToNote: resultUri=" + resultUri); + + if (resultUri != null) { + showToast(R.string.info_tag_added); + } else { + showToast(R.string.info_tag_added_failed); + } + } else { + Log.e("NoteEditActivity", "addTagToNote: noteId <= 0"); + showToast(R.string.info_note_not_saved); + } + } catch (Exception e) { + Log.e("NoteEditActivity", "addTagToNote exception: " + e.getMessage()); + e.printStackTrace(); + showToast(R.string.info_tag_add_failed_with_error); + } + } + + /** + * onDestroy - Activity销毁 + * 注销主题变化广播接收器 + */ + @Override + protected void onDestroy() { + super.onDestroy(); + } + + // ======================= 密码保护相关方法 ======================= + + /** + * 检查便签的加密状态 + */ + private void checkEncryptionStatus() { + if (mWorkingNote != null && mWorkingNote.existInDatabase()) { + mNoteId = mWorkingNote.getNoteId(); + // 查询便签是否加密 + Cursor cursor = getContentResolver().query( + Notes.CONTENT_ENCRYPTION_URI, + null, + Notes.EncryptionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)}, + null + ); + + if (cursor != null && cursor.moveToFirst()) { + mIsEncrypted = true; + cursor.close(); + // 显示密码验证对话框 + showPasswordVerificationDialog(); + } else { + mIsEncrypted = false; + mPasswordVerified = true; // 未加密的便签不需要验证 + if (cursor != null) { + cursor.close(); + } + } + } else { + mIsEncrypted = false; + mPasswordVerified = true; // 新建便签不需要验证 + } + } + + /** + * 显示密码验证对话框 + */ + private void showPasswordVerificationDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Password Verification"); + builder.setMessage("Please enter the note password"); + + final EditText passwordInput = new EditText(this); + passwordInput.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + builder.setView(passwordInput); + + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String password = passwordInput.getText().toString(); + if (verifyPassword(password)) { + mPassword = password; + mPasswordVerified = true; + // 密码验证成功,加载便签内容 + loadNoteContent(); + Toast.makeText(NoteEditActivity.this, "Password verified successfully", Toast.LENGTH_SHORT).show(); + } else { + // 密码验证失败,重新显示密码验证对话框 + Toast.makeText(NoteEditActivity.this, "Incorrect password, please try again", Toast.LENGTH_SHORT).show(); + showPasswordVerificationDialog(); + } + } + }); + + builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // 取消密码验证,返回便签列表 + finish(); + } + }); + + builder.show(); + } + + /** + * 显示密码设置对话框 + */ + private void showPasswordSettingDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Set Password"); + builder.setMessage("Please set a password for the note"); + + final EditText passwordInput = new EditText(this); + passwordInput.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + passwordInput.setHint("Enter password"); + + final EditText confirmInput = new EditText(this); + confirmInput.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + confirmInput.setHint("Confirm password"); + + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.addView(passwordInput); + layout.addView(confirmInput); + layout.setPadding(40, 20, 40, 20); + builder.setView(layout); + + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String password = passwordInput.getText().toString(); + String confirmPassword = confirmInput.getText().toString(); + + if (!password.equals(confirmPassword)) { + Toast.makeText(NoteEditActivity.this, "Passwords do not match", Toast.LENGTH_SHORT).show(); + return; + } + + if (password.length() < 4) { + Toast.makeText(NoteEditActivity.this, "Password length must be at least 4 characters", Toast.LENGTH_SHORT).show(); + return; + } + + // 保存便签 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + + // 设置密码 + setPassword(password); + } + }); + + builder.setNegativeButton("Cancel", null); + builder.show(); + } + + /** + * 显示密保问题设置对话框 + */ + private void showSecurityQuestionDialog() { + final String[] questions = { + "Where did you go to elementary school?", + "What is your favorite fruit?", + "What is your spouse's name?", + "What is your birthday?", + "What is your mother's name?" + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Set Security Question"); + builder.setMessage("Please select a security question and answer it to reset your password if you forget it"); + + final int[] selectedQuestion = {0}; + + // 创建包含问题选项和答案输入框的布局 + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(40, 20, 40, 20); + + // 添加问题选择列表 + RadioGroup questionGroup = new RadioGroup(this); + for (int i = 0; i < questions.length; i++) { + RadioButton radioButton = new RadioButton(this); + radioButton.setText(questions[i]); + radioButton.setId(i); + if (i == 0) { + radioButton.setChecked(true); + } + questionGroup.addView(radioButton); + } + questionGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + selectedQuestion[0] = checkedId; + } + }); + layout.addView(questionGroup); + + // 添加答案输入框 + final EditText answerInput = new EditText(this); + answerInput.setHint("Enter answer"); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + params.topMargin = 20; + answerInput.setLayoutParams(params); + layout.addView(answerInput); + + builder.setView(layout); + + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String answer = answerInput.getText().toString().trim(); + if (TextUtils.isEmpty(answer)) { + Toast.makeText(NoteEditActivity.this, "Please enter an answer", Toast.LENGTH_SHORT).show(); + return; + } + + // 保存密保问题和答案 + saveSecurityQuestion(questions[selectedQuestion[0]], answer); + + Toast.makeText(NoteEditActivity.this, "Password set successfully", Toast.LENGTH_SHORT).show(); + } + }); + + builder.setNegativeButton("Cancel", null); + builder.show(); + } + + /** + * 显示密保问题验证对话框 + */ + private void showSecurityQuestionVerificationDialog() { + // 查询密保问题 + Cursor cursor = getContentResolver().query( + Notes.CONTENT_SECURITY_QUESTION_URI, + null, + Notes.SecurityQuestionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)}, + null + ); + + if (cursor != null && cursor.moveToFirst()) { + final String question = cursor.getString(cursor.getColumnIndexOrThrow(Notes.SecurityQuestionColumns.QUESTION)); + final String answerHash = cursor.getString(cursor.getColumnIndexOrThrow(Notes.SecurityQuestionColumns.ANSWER_HASH)); + cursor.close(); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Security Question Verification"); + builder.setMessage(question); + + final EditText answerInput = new EditText(this); + answerInput.setHint("Enter answer"); + builder.setView(answerInput); + + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String answer = answerInput.getText().toString(); + // 验证答案 + if (verifyAnswer(answer, answerHash)) { + // 答案正确,先解密并加载便签内容 + // 由于我们不知道旧密码,我们可以临时使用密保问题的答案作为密码来解密便签内容 + // 这样,我们就可以在编辑器中显示解密后的内容,然后再重置密码 + try { + // 查询加密信息 + Cursor encryptCursor = getContentResolver().query( + Notes.CONTENT_ENCRYPTION_URI, + null, + Notes.EncryptionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)}, + null + ); + + if (encryptCursor != null && encryptCursor.moveToFirst()) { + String salt = encryptCursor.getString(encryptCursor.getColumnIndexOrThrow(Notes.EncryptionColumns.SALT)); + String iv = encryptCursor.getString(encryptCursor.getColumnIndexOrThrow(Notes.EncryptionColumns.IV)); + encryptCursor.close(); + + // 获取当前加密的便签内容 + String encryptedContent = mWorkingNote.getContent(); + + // 注意:我们不应该尝试使用密保问题的答案作为临时密码来解密内容 + // 因为密保问题的答案不一定是原始密码,这样做可能会导致解密失败 + // 而是应该保持编辑器为空,这样在重置密码时,我们就会保持原始的加密内容不变 + // 这样,当用户下次使用新密码打开便签时,虽然他们需要重新输入内容,但至少不会看到乱码 + mNoteEditor.setText(""); + + // 更新当前密码变量 + mPassword = answer; + mPasswordVerified = true; + } else { + if (encryptCursor != null) { + encryptCursor.close(); + } + // 加密信息不存在,直接显示空内容 + mNoteEditor.setText(""); + } + } catch (Exception e) { + Log.e(TAG, "Load note content failed: " + e.getMessage()); + // 加载失败,显示空内容 + mNoteEditor.setText(""); + } + + // 显示重置密码对话框 + showResetPasswordDialog(); + } else { + // 答案错误,提示用户 + Toast.makeText(NoteEditActivity.this, "Incorrect answer, please try again", Toast.LENGTH_SHORT).show(); + // 重新显示密保问题验证对话框 + // 注意:不要直接递归调用,而是使用对话框的 dismiss() 方法,让对话框自动重新显示 + dialog.dismiss(); + // 使用 Handler 延迟显示对话框,避免 UI 阻塞 + new android.os.Handler().postDelayed(new Runnable() { + @Override + public void run() { + showSecurityQuestionVerificationDialog(); + } + }, 500); + } + } + }); + + builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // 取消验证,结束Activity + finish(); + } + }); + + builder.setCancelable(false); // 不可取消 + builder.show(); + } else { + // 没有设置密保问题,提示用户 + Toast.makeText(this, "No security question set, cannot reset password", Toast.LENGTH_SHORT).show(); + if (cursor != null) { + cursor.close(); + } + // 结束Activity + finish(); + } + } + + /** + * 显示重置密码对话框 + */ + private void showResetPasswordDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Reset Password"); + builder.setMessage("Please set a new password"); + + final EditText passwordInput = new EditText(this); + passwordInput.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + passwordInput.setHint("Enter new password"); + + final EditText confirmInput = new EditText(this); + confirmInput.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + confirmInput.setHint("Confirm new password"); + + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.addView(passwordInput); + layout.addView(confirmInput); + layout.setPadding(40, 20, 40, 20); + builder.setView(layout); + + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String password = passwordInput.getText().toString(); + String confirmPassword = confirmInput.getText().toString(); + + if (!password.equals(confirmPassword)) { + Toast.makeText(NoteEditActivity.this, "Passwords do not match", Toast.LENGTH_SHORT).show(); + return; + } + + if (password.length() < 4) { + Toast.makeText(NoteEditActivity.this, "Password length must be at least 4 characters", Toast.LENGTH_SHORT).show(); + return; + } + + // 重置密码 + if (resetPassword(password)) { + mPassword = password; + mPasswordVerified = true; + // 密码重置成功,加载便签内容 + loadNoteContent(); + Toast.makeText(NoteEditActivity.this, "Password reset successfully", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(NoteEditActivity.this, "Failed to reset password", Toast.LENGTH_SHORT).show(); + // 结束Activity + finish(); + } + } + }); + + builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // 取消重置,结束Activity + finish(); + } + }); + + builder.setCancelable(false); // 不可取消 + builder.show(); + } + + /** + * 验证密码 + * @param password 待验证的密码 + * @return 密码是否正确 + */ + private boolean verifyPassword(String password) { + // 查询加密信息 + Cursor cursor = getContentResolver().query( + Notes.CONTENT_ENCRYPTION_URI, + null, + Notes.EncryptionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)}, + null + ); + + if (cursor != null && cursor.moveToFirst()) { + String passwordHash = cursor.getString(cursor.getColumnIndexOrThrow(Notes.EncryptionColumns.PASSWORD_HASH)); + String salt = cursor.getString(cursor.getColumnIndexOrThrow(Notes.EncryptionColumns.SALT)); + cursor.close(); + + Log.d(TAG, "Verify password: salt=" + salt + ", passwordHash length=" + (passwordHash != null ? passwordHash.length() : 0)); + + // 验证密码 + boolean result = EncryptionUtils.verifyPassword(password, salt, passwordHash); + Log.d(TAG, "Verify password result: " + result); + return result; + } else { + if (cursor != null) { + cursor.close(); + } + return false; + } + } + + /** + * 设置密码 + * @param password 密码 + */ + private void setPassword(String password) { + try { + // 生成盐值和初始化向量 + String salt = EncryptionUtils.generateSalt(); + String iv = EncryptionUtils.generateIv(); + + // 生成密码哈希 + String passwordHash = EncryptionUtils.generatePasswordHash(password, salt); + + Log.d(TAG, "Set password: salt=" + salt + ", passwordHash length=" + (passwordHash != null ? passwordHash.length() : 0)); + + // 获取当前编辑器内容 + String content = mNoteEditor.getText().toString(); + + // 确保内容不为空 + if (TextUtils.isEmpty(content)) { + Log.e(TAG, "Content is empty, cannot set password"); + Toast.makeText(this, "内容为空,无法设置密码", Toast.LENGTH_SHORT).show(); + return; + } + + // 加密便签内容 + String encryptedContent = EncryptionUtils.encrypt(content, password, salt, iv); + + // 确保加密后内容不为空 + if (TextUtils.isEmpty(encryptedContent)) { + Log.e(TAG, "Encrypted content is empty, cannot set password"); + Toast.makeText(this, "加密失败,无法设置密码", Toast.LENGTH_SHORT).show(); + return; + } + + // 保存加密信息 + ContentValues values = new ContentValues(); + values.put(Notes.EncryptionColumns.PASSWORD_HASH, passwordHash); + values.put(Notes.EncryptionColumns.SALT, salt); + values.put(Notes.EncryptionColumns.IV, iv); + values.put(Notes.EncryptionColumns.MODIFIED_DATE, System.currentTimeMillis()); + + // 检查是否已经存在加密信息 + Cursor cursor = getContentResolver().query( + Notes.CONTENT_ENCRYPTION_URI, + null, + Notes.EncryptionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)}, + null + ); + + if (cursor != null && cursor.moveToFirst()) { + // 存在加密信息,更新 + getContentResolver().update( + Notes.CONTENT_ENCRYPTION_URI, + values, + Notes.EncryptionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)} + ); + cursor.close(); + } else { + // 不存在加密信息,插入 + values.put(Notes.EncryptionColumns.NOTE_ID, mNoteId); + values.put(Notes.EncryptionColumns.CREATED_DATE, System.currentTimeMillis()); + getContentResolver().insert(Notes.CONTENT_ENCRYPTION_URI, values); + if (cursor != null) { + cursor.close(); + } + } + + // 更新加密状态 + mIsEncrypted = true; + mPassword = password; + mPasswordVerified = true; + + // 更新便签内容为加密后的内容 + mWorkingNote.setWorkingText(encryptedContent); + saveNote(); + } catch (Exception e) { + Log.e(TAG, "Set password failed: " + e.getMessage()); + Toast.makeText(this, "密码设置失败", Toast.LENGTH_SHORT).show(); + } + } + + /** + * 验证密保问题答案 + * @param answer 待验证的答案 + * @param storedHash 存储的哈希值 + * @return 答案是否正确 + */ + private boolean verifyAnswer(String answer, String storedHash) { + // 查询盐值 + Cursor cursor = getContentResolver().query( + Notes.CONTENT_ENCRYPTION_URI, + new String[]{Notes.EncryptionColumns.SALT}, + Notes.EncryptionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)}, + null + ); + + if (cursor != null && cursor.moveToFirst()) { + String salt = cursor.getString(cursor.getColumnIndexOrThrow(Notes.EncryptionColumns.SALT)); + cursor.close(); + + // 验证答案 + return EncryptionUtils.verifyAnswer(answer, salt, storedHash); + } else { + if (cursor != null) { + cursor.close(); + } + return false; + } + } + + + + /** + * 重置密码 + * @param newPassword 新密码 + * @return 重置是否成功 + */ + private boolean resetPassword(String newPassword) { + try { + // 查询加密信息 + Cursor cursor = getContentResolver().query( + Notes.CONTENT_ENCRYPTION_URI, + null, + Notes.EncryptionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)}, + null + ); + + if (cursor != null && cursor.moveToFirst()) { + String oldSalt = cursor.getString(cursor.getColumnIndexOrThrow(Notes.EncryptionColumns.SALT)); + String oldIv = cursor.getString(cursor.getColumnIndexOrThrow(Notes.EncryptionColumns.IV)); + cursor.close(); + + // 生成新的盐值和初始化向量 + String newSalt = EncryptionUtils.generateSalt(); + String newIv = EncryptionUtils.generateIv(); + + // 生成新的密码哈希 + String newPasswordHash = EncryptionUtils.generatePasswordHash(newPassword, newSalt); + + // 确保密码哈希不为空 + if (TextUtils.isEmpty(newPasswordHash)) { + Log.e(TAG, "Password hash is empty, cannot reset password"); + return false; + } + + // 更新当前密码变量 + mPassword = newPassword; + mPasswordVerified = true; + mIsEncrypted = true; + + // 更新加密信息 + ContentValues values = new ContentValues(); + values.put(Notes.EncryptionColumns.PASSWORD_HASH, newPasswordHash); + values.put(Notes.EncryptionColumns.SALT, newSalt); + values.put(Notes.EncryptionColumns.IV, newIv); + values.put(Notes.EncryptionColumns.MODIFIED_DATE, System.currentTimeMillis()); + + int updateCount = getContentResolver().update( + Notes.CONTENT_ENCRYPTION_URI, + values, + Notes.EncryptionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)} + ); + + if (updateCount == 0) { + Log.e(TAG, "Update encryption info failed"); + return false; + } + + // 重置后编辑器为空,用户需要重新输入内容 + mNoteEditor.setText(""); + mWorkingNote.setWorkingText(""); + boolean saveResult = saveNote(); + + if (!saveResult) { + Log.e(TAG, "Save note failed"); + return false; + } + + Log.d(TAG, "Reset password success"); + return true; + } else { + if (cursor != null) { + cursor.close(); + } + Log.e(TAG, "No encryption info found"); + return false; + } + } catch (Exception e) { + Log.e(TAG, "Reset password failed: " + e.getMessage()); + return false; + } + } + + /** + * 保存密保问题和答案 + * @param question 密保问题 + * @param answer 密保问题答案 + */ + private void saveSecurityQuestion(String question, String answer) { + try { + // 查询盐值 + Cursor cursor = getContentResolver().query( + Notes.CONTENT_ENCRYPTION_URI, + new String[]{Notes.EncryptionColumns.SALT}, + Notes.EncryptionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)}, + null + ); + + if (cursor != null && cursor.moveToFirst()) { + String salt = cursor.getString(cursor.getColumnIndexOrThrow(Notes.EncryptionColumns.SALT)); + cursor.close(); + + // 生成答案哈希 + String answerHash = EncryptionUtils.generateAnswerHash(answer, salt); + + // 保存密保问题 + ContentValues values = new ContentValues(); + values.put(Notes.SecurityQuestionColumns.NOTE_ID, mNoteId); + values.put(Notes.SecurityQuestionColumns.QUESTION, question); + values.put(Notes.SecurityQuestionColumns.ANSWER_HASH, answerHash); + values.put(Notes.SecurityQuestionColumns.CREATED_DATE, System.currentTimeMillis()); + values.put(Notes.SecurityQuestionColumns.MODIFIED_DATE, System.currentTimeMillis()); + + getContentResolver().insert(Notes.CONTENT_SECURITY_QUESTION_URI, values); + } else { + if (cursor != null) { + cursor.close(); + } + } + } catch (Exception e) { + Log.e(TAG, "Save security question failed: " + e.getMessage()); + } + } + + /** + * 加载便签内容(处理加密内容) + */ + private void loadNoteContent() { + try { + if (mIsEncrypted && mPasswordVerified && mPassword != null) { + // 查询加密信息 + Cursor cursor = getContentResolver().query( + Notes.CONTENT_ENCRYPTION_URI, + null, + Notes.EncryptionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)}, + null + ); + + if (cursor != null && cursor.moveToFirst()) { + String salt = cursor.getString(cursor.getColumnIndexOrThrow(Notes.EncryptionColumns.SALT)); + String iv = cursor.getString(cursor.getColumnIndexOrThrow(Notes.EncryptionColumns.IV)); + cursor.close(); + + // 解密便签内容 + String encryptedContent = mWorkingNote.getContent(); + + try { + String decryptedContent = EncryptionUtils.decrypt(encryptedContent, mPassword, salt, iv); + + // 显示解密后的内容 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + switchToListMode(decryptedContent); + } else { + mNoteEditor.setText(getHighlightQueryResult(decryptedContent, mUserQuery)); + } + } catch (Exception e) { + Log.e(TAG, "Decrypt failed: " + e.getMessage()); + // 解密失败,可能是密码错误或数据损坏 + Toast.makeText(this, "解密失败,请确认密码正确", Toast.LENGTH_SHORT).show(); + // 重新验证密码 + mPasswordVerified = false; + showPasswordVerificationDialog(); + } + } else { + if (cursor != null) { + cursor.close(); + } + // 加密信息不存在,直接显示内容 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + switchToListMode(mWorkingNote.getContent()); + } else { + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + } + } + } else { + // 未加密的便签,直接加载内容 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + switchToListMode(mWorkingNote.getContent()); + } else { + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + } + } + } catch (Exception e) { + Log.e(TAG, "Load note content failed: " + e.getMessage()); + Toast.makeText(this, "加载便签内容失败", Toast.LENGTH_SHORT).show(); + // 结束Activity + finish(); + } + } + + /** + * 保存便签(处理加密内容) + */ + private void saveNoteWithEncryption() { + if (mIsEncrypted && mPasswordVerified && mPassword != null) { + try { + // 获取当前编辑器内容 + String content = mNoteEditor.getText().toString(); + + // 确保内容不为空 + if (TextUtils.isEmpty(content)) { + Log.e(TAG, "Content is empty, cannot save"); + Toast.makeText(this, "内容为空,无法保存", Toast.LENGTH_SHORT).show(); + return; + } + + // 查询加密信息 + Cursor cursor = getContentResolver().query( + Notes.CONTENT_ENCRYPTION_URI, + null, + Notes.EncryptionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)}, + null + ); + + if (cursor != null && cursor.moveToFirst()) { + String salt = cursor.getString(cursor.getColumnIndexOrThrow(Notes.EncryptionColumns.SALT)); + String iv = cursor.getString(cursor.getColumnIndexOrThrow(Notes.EncryptionColumns.IV)); + cursor.close(); + + // 加密便签内容 + String encryptedContent = EncryptionUtils.encrypt(content, mPassword, salt, iv); + + // 确保加密后内容不为空 + if (TextUtils.isEmpty(encryptedContent)) { + Log.e(TAG, "Encrypted content is empty, cannot save"); + Toast.makeText(this, "加密失败,无法保存", Toast.LENGTH_SHORT).show(); + return; + } + + // 更新便签内容 + mWorkingNote.setWorkingText(encryptedContent); + saveNote(); + } else { + if (cursor != null) { + cursor.close(); + } + // 加密信息不存在,直接保存 + saveNote(); + } + } catch (Exception e) { + Log.e(TAG, "Save note with encryption failed: " + e.getMessage()); + // 保存失败,尝试直接保存 + saveNote(); + } + } else { + // 未加密的便签,直接保存 + saveNote(); + } + } + + /** + * 移除密码保护 + */ + private void removePasswordProtection() { + try { + if (mIsEncrypted && mPasswordVerified && mPassword != null) { + // 查询加密信息 + Cursor cursor = getContentResolver().query( + Notes.CONTENT_ENCRYPTION_URI, + null, + Notes.EncryptionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)}, + null + ); + + if (cursor != null && cursor.moveToFirst()) { + String salt = cursor.getString(cursor.getColumnIndexOrThrow(Notes.EncryptionColumns.SALT)); + String iv = cursor.getString(cursor.getColumnIndexOrThrow(Notes.EncryptionColumns.IV)); + cursor.close(); + + // 解密便签内容 + String encryptedContent = mWorkingNote.getContent(); + String decryptedContent = EncryptionUtils.decrypt(encryptedContent, mPassword, salt, iv); + + // 更新便签内容为解密后的内容 + mWorkingNote.setWorkingText(decryptedContent); + saveNote(); + + // 删除加密信息 + getContentResolver().delete( + Notes.CONTENT_ENCRYPTION_URI, + Notes.EncryptionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)} + ); + + // 删除密保问题 + getContentResolver().delete( + Notes.CONTENT_SECURITY_QUESTION_URI, + Notes.SecurityQuestionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mNoteId)} + ); + + mIsEncrypted = false; + mPassword = null; + mPasswordVerified = true; + + Toast.makeText(this, "密码保护已移除", Toast.LENGTH_SHORT).show(); + } else { + if (cursor != null) { + cursor.close(); + } + } + } + } catch (Exception e) { + Log.e(TAG, "Remove password protection failed: " + e.getMessage()); + Toast.makeText(this, "移除密码保护失败", Toast.LENGTH_SHORT).show(); + } + } } \ No newline at end of file diff --git a/src/java/net/micode/notes/ui/NoteItemData.java b/src/java/net/micode/notes/ui/NoteItemData.java index c3c56f2..810ff48 100644 --- a/src/java/net/micode/notes/ui/NoteItemData.java +++ b/src/java/net/micode/notes/ui/NoteItemData.java @@ -28,8 +28,13 @@ import android.text.TextUtils; // 文本工具 import net.micode.notes.data.Contact; // 联系人工具类 import net.micode.notes.data.Notes; // Notes主类 import net.micode.notes.data.Notes.NoteColumns; // 便签表列定义 +import net.micode.notes.data.Notes.TagColumns; // 标签表列定义 +import net.micode.notes.data.Notes.NoteTagRelationColumns; // 便签-标签关联表列定义 import net.micode.notes.tool.DataUtils; // 数据工具 +// Java集合 +import java.util.ArrayList; + // ======================= 便签项数据模型 ======================= /** * NoteItemData - 便签项数据模型类 @@ -105,6 +110,12 @@ public class NoteItemData { private boolean mIsOnlyOneItem; // 是否是唯一一项 private boolean mIsOneNoteFollowingFolder; // 是否是文件夹后的唯一便签 private boolean mIsMultiNotesFollowingFolder; // 是否是文件夹后的多个便签之一 + + // 标签相关 + private ArrayList mTags; // 便签的标签列表 + + // 上下文 + private Context mContext; // 上下文,用于数据库查询 // ======================= 构造函数 ======================= @@ -117,10 +128,11 @@ public class NoteItemData { * 3. 处理通话记录 * 4. 检查位置状态 * - * @param context 上下文,用于联系人查询 + * @param context 上下文,用于联系人查询和数据库操作 * @param cursor 数据库游标,必须包含PROJECTION指定的字段 */ public NoteItemData(Context context, Cursor cursor) { + mContext = context; // 1. 读取基本字段 mId = cursor.getLong(ID_COLUMN); mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); @@ -161,6 +173,9 @@ public class NoteItemData { mName = ""; } + // 5. 加载标签数据 + loadTags(context); + // 4. 检查位置状态 checkPostion(cursor); } @@ -406,6 +421,107 @@ public class NoteItemData { public boolean isCallRecord() { return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); } + + /** + * 加载便签的标签数据 + * @param context 上下文 + */ + private void loadTags(Context context) { + mTags = new ArrayList<>(); + + try { + // 查询便签关联的标签 + Cursor tagCursor = context.getContentResolver().query( + Notes.CONTENT_NOTE_TAG_RELATION_URI, + new String[]{Notes.NoteTagRelationColumns.TAG_ID}, + Notes.NoteTagRelationColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mId)}, + null); + + if (tagCursor != null) { + try { + while (tagCursor.moveToNext()) { + long tagId = tagCursor.getLong(0); + + // 查询标签详情 + Cursor tagDetailCursor = context.getContentResolver().query( + Notes.CONTENT_TAG_URI, + new String[]{TagColumns.NAME}, + TagColumns.ID + "=?", + new String[]{String.valueOf(tagId)}, + null); + + if (tagDetailCursor != null) { + try { + if (tagDetailCursor.moveToFirst()) { + String tagName = tagDetailCursor.getString(0); + mTags.add(tagName); + } + } finally { + tagDetailCursor.close(); + } + } + } + } finally { + tagCursor.close(); + } + } + } catch (Exception e) { + // 捕获数据库异常,如表不存在等情况 + // 此时标签功能可能尚未初始化,优雅处理 + mTags.clear(); + } + } + + /** + * 获取便签的标签列表 + * @return 标签列表 + */ + public ArrayList getTags() { + return mTags; + } + + /** + * 检查便签是否有标签 + * @return 是否有标签 + */ + public boolean hasTags() { + return mTags != null && !mTags.isEmpty(); + } + + /** + * 获取上下文 + * @return 上下文 + */ + private Context getContext() { + return mContext; + } + + /** + * 检查便签是否加密 + * @return 是否加密 + */ + public boolean isEncrypted() { + try { + // 查询加密信息 + Cursor cursor = getContext().getContentResolver().query( + Notes.CONTENT_ENCRYPTION_URI, + null, + Notes.EncryptionColumns.NOTE_ID + "=?", + new String[]{String.valueOf(mId)}, + null + ); + + boolean isEncrypted = cursor != null && cursor.moveToFirst(); + if (cursor != null) { + cursor.close(); + } + return isEncrypted; + } catch (Exception e) { + // 表不存在或其他异常,返回false + return false; + } + } // ======================= 静态工具方法 ======================= diff --git a/src/java/net/micode/notes/ui/NotesListActivity.java b/src/java/net/micode/notes/ui/NotesListActivity.java index a5349ff..8e95c78 100644 --- a/src/java/net/micode/notes/ui/NotesListActivity.java +++ b/src/java/net/micode/notes/ui/NotesListActivity.java @@ -95,6 +95,7 @@ import java.io.IOException; // IO异常 import java.io.InputStream; // 输入流 import java.io.InputStreamReader; // 输入流读取器 // Java集合 +import java.util.ArrayList; // 数组列表 import java.util.HashSet; // 哈希集合 import java.util.List; // 列表接口 import java.util.Map; // 映射接口 @@ -195,8 +196,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt private NoteItemData mFocusNoteDataItem; /** 当前选中的标签ID - 用于按标签筛选便签,-1表示不筛选 */ - - + private long mCurrentTagId = -1; + + /** 夜间模式状态 */ + private static final String PREF_NIGHT_MODE = "night_mode"; + private boolean mIsNightMode = false; + // ======================= 数据库查询条件常量 ======================= /** 普通文件夹查询条件 - 查询指定父文件夹下的便签 */ @@ -225,13 +230,19 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + // 加载夜间模式设置 + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + mIsNightMode = prefs.getBoolean(PREF_NIGHT_MODE, false); + applyNightMode(); + // 设置布局 setContentView(R.layout.note_list); // 初始化资源 initResources(); /** - * 用户首次使用应用时插入介绍便签 + * 用户首次使用应用时插入便签 */ setAppInfoFromRawRes(); } @@ -344,6 +355,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mState = ListEditState.NOTE_LIST; // 初始状态为便签列表 mModeCallBack = new ModeCallback(); // 创建多选模式回调 + // 设置列表背景色,根据当前夜间模式状态 + if (mIsNightMode) { + mNotesListView.setBackgroundColor(getResources().getColor(R.color.night_list_background)); + } else { + mNotesListView.setBackgroundColor(getResources().getColor(android.R.color.white)); + } + // 更新标题栏 updateTitleBar(); } @@ -583,13 +601,99 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt * 开始异步便签列表查询 */ private void startAsyncNotesListQuery() { - String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION - : NORMAL_SELECTION; - String[] selectionArgs = { String.valueOf(mCurrentFolderId) }; + // 基础查询条件 + StringBuilder selection = new StringBuilder(); + ArrayList selectionArgs = new ArrayList<>(); + + // 根据当前文件夹添加查询条件 + if (mCurrentFolderId == Notes.ID_ROOT_FOLDER) { + selection.append(ROOT_FOLDER_SELECTION); + } else { + selection.append(NORMAL_SELECTION); + } + selectionArgs.add(String.valueOf(mCurrentFolderId)); + + // 如果有标签筛选,添加标签关联条件 + if (mCurrentTagId != -1) { + Log.d("NotesListActivity", "开始标签筛选,标签ID:" + mCurrentTagId); + Log.d("NotesListActivity", "基础查询条件:" + selection.toString()); + + try { + // 查询带有该标签的便签ID + Log.d("NotesListActivity", "开始查询标签关联的便签ID"); + Cursor relationCursor = getContentResolver().query( + Notes.CONTENT_NOTE_TAG_RELATION_URI, + new String[]{Notes.NoteTagRelationColumns.NOTE_ID}, + Notes.NoteTagRelationColumns.TAG_ID + "=?", + new String[]{String.valueOf(mCurrentTagId)}, + null); + + if (relationCursor != null) { + int count = relationCursor.getCount(); + Log.d("NotesListActivity", "标签 " + mCurrentTagId + " 关联了 " + count + " 个便签"); + + if (count > 0) { + // 构建便签ID列表 + StringBuilder noteIdList = new StringBuilder(); + boolean first = true; + while (relationCursor.moveToNext()) { + long noteId = relationCursor.getLong(0); + if (first) { + first = false; + } else { + noteIdList.append(","); + } + noteIdList.append(noteId); + Log.d("NotesListActivity", "便签 " + noteId + " 关联了标签 " + mCurrentTagId); + } + relationCursor.close(); + + // 添加便签ID筛选条件 + selection.append(" AND ").append(NoteColumns.ID).append(" IN (").append(noteIdList).append(")"); + Log.d("NotesListActivity", "最终查询条件:" + selection.toString()); + } else { + // 没有关联的便签,添加一个永远为false的条件 + selection.append(" AND 1=0"); + Log.d("NotesListActivity", "标签 " + mCurrentTagId + " 没有关联的便签,添加 1=0 条件"); + relationCursor.close(); + } + } else { + Log.d("NotesListActivity", "relationCursor 为 null"); + } + + } catch (Exception e) { + Log.d("NotesListActivity", "标签筛选异常:" + e.getMessage()); + e.printStackTrace(); + // 表不存在或其他异常,忽略标签筛选 + } + } else { + Log.d("NotesListActivity", "mCurrentTagId 为 -1,不进行标签筛选"); + } + + // 生成排序字符串,确保置顶便签在最前面 + StringBuilder sortOrder = new StringBuilder(); + sortOrder.append(NoteColumns.PINNED).append(" DESC,"); + sortOrder.append(NoteColumns.TYPE).append(" DESC,"); + + // 根据当前排序模式添加相应的排序字段 + switch (mCurrentSortMode) { + case SORT_BY_NAME: + sortOrder.append(NoteColumns.SNIPPET).append(" ASC"); + break; + case SORT_BY_MODIFIED_DATE: + sortOrder.append(NoteColumns.MODIFIED_DATE).append(" DESC"); + break; + case SORT_BY_CREATED_DATE: + sortOrder.append(NoteColumns.CREATED_DATE).append(" DESC"); + break; + default: + sortOrder.append(NoteColumns.MODIFIED_DATE).append(" DESC"); + break; + } mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, - Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, selectionArgs, - NoteColumns.PINNED + " DESC," + NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); + Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection.toString(), + selectionArgs.toArray(new String[0]), sortOrder.toString()); } /** @@ -1158,6 +1262,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt /** * 选项菜单项选择 */ + // 排序方式常量 + private static final int SORT_BY_NAME = 0; + private static final int SORT_BY_MODIFIED_DATE = 1; + private static final int SORT_BY_CREATED_DATE = 2; + private int mCurrentSortMode = SORT_BY_MODIFIED_DATE; // 默认按修改时间排序 + @Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); @@ -1181,10 +1291,135 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt createNewNote(); } else if (itemId == R.id.menu_search) { onSearchRequested(); + } else if (itemId == R.id.menu_sort) { + showSortOptions(); + } else if (itemId == R.id.menu_tags) { + showTagFilterDialog(); + } else if (itemId == R.id.menu_night_mode) { + toggleNightMode(); } return true; } + /** + * 显示排序选项对话框 + */ + private void showSortOptions() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Sort By"); + final String[] options = {"Note Name", "Modified Date", "Created Date"}; + builder.setSingleChoiceItems(options, mCurrentSortMode, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mCurrentSortMode = which; + startAsyncNotesListQuery(); + dialog.dismiss(); + } + }); + builder.setNegativeButton("Cancel", null); + builder.show(); + } + + + + /** + * 显示标签筛选对话框 + */ + private void showTagFilterDialog() { + // 准备标签列表 + final List tagNames = new ArrayList<>(); + final List tagIds = new ArrayList<>(); + + // 添加"全部"选项 + tagNames.add("全部"); + tagIds.add(-1L); + + try { + // 查询所有标签 + Cursor tagCursor = getContentResolver().query( + Notes.CONTENT_TAG_URI, + new String[]{Notes.TagColumns.ID, Notes.TagColumns.NAME}, + null, null, Notes.TagColumns.NAME + " ASC"); + + if (tagCursor != null) { + try { + while (tagCursor.moveToNext()) { + tagNames.add(tagCursor.getString(tagCursor.getColumnIndex(Notes.TagColumns.NAME))); + tagIds.add(tagCursor.getLong(tagCursor.getColumnIndex(Notes.TagColumns.ID))); + } + } finally { + tagCursor.close(); + } + } + } catch (Exception e) { + // 表不存在或其他异常,忽略,继续显示对话框 + } + + // 显示标签选择对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("按标签筛选"); + + // 查找当前选中的标签索引 + int checkedItem = 0; + for (int i = 0; i < tagIds.size(); i++) { + if (tagIds.get(i) == mCurrentTagId) { + checkedItem = i; + break; + } + } + + builder.setSingleChoiceItems(tagNames.toArray(new String[0]), checkedItem, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mCurrentTagId = tagIds.get(which); + startAsyncNotesListQuery(); + dialog.dismiss(); + } + }); + builder.setNegativeButton("取消", null); + builder.show(); + } + + // ======================= 夜间模式 ======================= + + /** + * 切换夜间模式 + */ + private void toggleNightMode() { + mIsNightMode = !mIsNightMode; + + // 保存设置 + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.edit().putBoolean(PREF_NIGHT_MODE, mIsNightMode).apply(); + + // 显示提示 + Toast.makeText(this, + mIsNightMode ? R.string.night_mode_enabled : R.string.night_mode_disabled, + Toast.LENGTH_SHORT).show(); + + // 重启Activity以应用新主题 + recreate(); + } + + /** + * 应用夜间模式主题 + */ + private void applyNightMode() { + if (mIsNightMode) { + setTheme(R.style.NoteTheme_Night); + // 设置列表背景为深色 + if (mNotesListView != null) { + mNotesListView.setBackgroundColor(getResources().getColor(R.color.night_list_background)); + } + } else { + setTheme(R.style.NoteTheme); + // 设置列表背景为浅色 + if (mNotesListView != null) { + mNotesListView.setBackgroundColor(getResources().getColor(android.R.color.white)); + } + } + } + // ======================= 搜索请求 ======================= /** @@ -1355,4 +1590,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } return false; } + + /** + * onDestroy - Activity销毁 + * 注销主题变化广播接收器 + */ + @Override + protected void onDestroy() { + super.onDestroy(); + } } \ No newline at end of file diff --git a/src/java/net/micode/notes/ui/NotesListAdapter.java b/src/java/net/micode/notes/ui/NotesListAdapter.java index f9e6342..332e89a 100644 --- a/src/java/net/micode/notes/ui/NotesListAdapter.java +++ b/src/java/net/micode/notes/ui/NotesListAdapter.java @@ -67,6 +67,9 @@ public class NotesListAdapter extends CursorAdapter { /** 选择模式标志 - true: 多选模式; false: 普通模式 */ private boolean mChoiceMode; + + /** 夜间模式标志 - true: 夜间模式; false: 日间模式 */ + private boolean mIsNightMode; // ======================= 小部件属性内部类 ======================= @@ -91,6 +94,16 @@ public class NotesListAdapter extends CursorAdapter { mSelectedIndex = new HashMap(); mContext = context; mNotesCount = 0; // 初始便签数为0 + mIsNightMode = false; // 初始为日间模式 + } + + /** + * 设置夜间模式状态 + * @param isNightMode 是否为夜间模式 + */ + public void setNightMode(boolean isNightMode) { + mIsNightMode = isNightMode; + notifyDataSetChanged(); // 通知视图更新 } // ======================= 适配器核心方法 ======================= @@ -120,9 +133,9 @@ 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()), mIsNightMode); } } diff --git a/src/java/net/micode/notes/ui/NotesListItem.java b/src/java/net/micode/notes/ui/NotesListItem.java index ab1ba72..25b09e0 100644 --- a/src/java/net/micode/notes/ui/NotesListItem.java +++ b/src/java/net/micode/notes/ui/NotesListItem.java @@ -65,6 +65,12 @@ public class NotesListItem extends LinearLayout { /** 联系人姓名文本 - 通话记录专用,显示联系人姓名 */ private TextView mCallName; + /** 标签容器 - 显示便签的标签 */ + private LinearLayout mTagContainer; + + /** 加密图标 - 显示便签是否加密 */ + private ImageView mEncrypted; + /** 便签数据项 - 当前项绑定的数据 */ private NoteItemData mItemData; @@ -88,10 +94,13 @@ public class NotesListItem extends LinearLayout { // 2. 查找并保存子视图引用 mAlert = (ImageView) findViewById(R.id.iv_alert_icon); mPinned = (ImageView) findViewById(R.id.iv_pinned_icon); + mEncrypted = (ImageView) findViewById(R.id.iv_encrypted_icon); mTitle = (TextView) findViewById(R.id.tv_title); mTime = (TextView) findViewById(R.id.tv_time); mCallName = (TextView) findViewById(R.id.tv_name); mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); + // 查找标签容器 + mTagContainer = (LinearLayout) findViewById(R.id.ll_tags); } // ======================= 数据绑定方法 ======================= @@ -109,8 +118,9 @@ public class NotesListItem extends LinearLayout { * @param data 便签数据项 * @param choiceMode 是否在选择模式中 * @param checked 该项是否被选中 + * @param isNightMode 是否为夜间模式 */ - public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { + public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked, boolean isNightMode) { // 1. 处理选择模式复选框 if (choiceMode && data.getType() == Notes.TYPE_NOTE) { // 选择模式且是便签类型:显示复选框 @@ -127,20 +137,20 @@ public class NotesListItem extends LinearLayout { // 2. 根据便签类型设置不同显示 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { // 情况1:通话记录文件夹 - bindCallRecordFolder(context, data); + bindCallRecordFolder(context, data, isNightMode); } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { // 情况2:通话记录文件夹中的便签 - bindCallRecordNote(context, data); + bindCallRecordNote(context, data, isNightMode); } else { // 情况3:普通文件夹或便签 - bindNormalItem(context, data); + bindNormalItem(context, data, isNightMode); } // 3. 更新时间显示(所有类型都显示) mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); // 4. 设置背景 - setBackground(data); + setBackground(data, isNightMode); } // ======================= 通话记录文件夹绑定 ======================= @@ -150,8 +160,9 @@ public class NotesListItem extends LinearLayout { * 特殊显示:文件夹图标 + 便签数量 * @param context 上下文 * @param data 文件夹数据 + * @param isNightMode 是否为夜间模式 */ - private void bindCallRecordFolder(Context context, NoteItemData data) { + private void bindCallRecordFolder(Context context, NoteItemData data, boolean isNightMode) { // 隐藏联系人姓名 mCallName.setVisibility(View.GONE); // 显示提醒图标(文件夹图标) @@ -163,6 +174,12 @@ public class NotesListItem extends LinearLayout { + context.getString(R.string.format_folder_files_count, data.getNotesCount())); // 设置文件夹图标 mAlert.setImageResource(R.drawable.call_record); + + // 夜间模式:更新文本颜色 + if (isNightMode) { + mTitle.setTextColor(context.getResources().getColor(R.color.night_primary_text)); + mTime.setTextColor(context.getResources().getColor(R.color.night_secondary_text)); + } } // ======================= 通话记录便签绑定 ======================= @@ -172,8 +189,9 @@ public class NotesListItem extends LinearLayout { * 特殊显示:联系人姓名 + 提醒图标 * @param context 上下文 * @param data 便签数据 + * @param isNightMode 是否为夜间模式 */ - private void bindCallRecordNote(Context context, NoteItemData data) { + private void bindCallRecordNote(Context context, NoteItemData data, boolean isNightMode) { // 显示联系人姓名 mCallName.setVisibility(View.VISIBLE); mCallName.setText(data.getCallName()); @@ -188,6 +206,13 @@ public class NotesListItem extends LinearLayout { } else { mAlert.setVisibility(View.GONE); } + + // 夜间模式:更新文本颜色 + if (isNightMode) { + mCallName.setTextColor(context.getResources().getColor(R.color.night_primary_text)); + mTitle.setTextColor(context.getResources().getColor(R.color.night_primary_text)); + mTime.setTextColor(context.getResources().getColor(R.color.night_secondary_text)); + } } // ======================= 普通项绑定 ======================= @@ -197,8 +222,9 @@ public class NotesListItem extends LinearLayout { * 通用显示逻辑 * @param context 上下文 * @param data 便签/文件夹数据 + * @param isNightMode 是否为夜间模式 */ - private void bindNormalItem(Context context, NoteItemData data) { + private void bindNormalItem(Context context, NoteItemData data, boolean isNightMode) { // 隐藏联系人姓名 mCallName.setVisibility(View.GONE); // 设置主标题样式 @@ -210,6 +236,10 @@ public class NotesListItem extends LinearLayout { + context.getString(R.string.format_folder_files_count, data.getNotesCount())); mAlert.setVisibility(View.GONE); // 文件夹不显示提醒图标 + // 文件夹不显示标签 + if (mTagContainer != null) { + mTagContainer.setVisibility(View.GONE); + } } else { // 普通便签:显示格式化摘要 mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); @@ -226,6 +256,50 @@ public class NotesListItem extends LinearLayout { } else { mPinned.setVisibility(View.GONE); } + + // 根据是否加密设置加密图标 + if (data.isEncrypted()) { + mEncrypted.setVisibility(View.VISIBLE); + } else { + mEncrypted.setVisibility(View.GONE); + } + // 显示标签 + if (mTagContainer != null) { + if (data.hasTags()) { + // 清空现有标签 + mTagContainer.removeAllViews(); + // 添加新标签 + for (String tag : data.getTags()) { + TextView tagView = new TextView(context); + tagView.setText(tag); + tagView.setTextAppearance(context, R.style.TextAppearanceSecondaryItem); + tagView.setBackgroundResource(R.drawable.tag_background); + tagView.setPadding(8, 4, 8, 4); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + params.setMargins(4, 0, 4, 0); + tagView.setLayoutParams(params); + + // 夜间模式:更新标签颜色和背景 + if (isNightMode) { + tagView.setTextColor(context.getResources().getColor(R.color.night_tag_text)); + tagView.setBackgroundColor(context.getResources().getColor(R.color.night_tag_background)); + } + + mTagContainer.addView(tagView); + } + mTagContainer.setVisibility(View.VISIBLE); + } else { + mTagContainer.setVisibility(View.GONE); + } + } + } + + // 夜间模式:更新文本颜色 + if (isNightMode) { + mTitle.setTextColor(context.getResources().getColor(R.color.night_primary_text)); + mTime.setTextColor(context.getResources().getColor(R.color.night_secondary_text)); } } @@ -239,28 +313,35 @@ public class NotesListItem extends LinearLayout { * 2. 文件夹:统一使用文件夹背景 * * @param data 便签数据项 + * @param isNightMode 是否为夜间模式 */ - private void setBackground(NoteItemData data) { - int id = data.getBgColorId(); // 获取背景颜色ID - - if (data.getType() == Notes.TYPE_NOTE) { - // 便签类型:根据位置选择背景 - if (data.isSingle() || data.isOneFollowingFolder()) { - // 单独项或文件夹后的唯一便签:单独项背景 - setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); - } else if (data.isLast()) { - // 最后一项:底部圆角背景 - setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); - } else if (data.isFirst() || data.isMultiFollowingFolder()) { - // 第一项或文件夹后的多个便签之一:顶部圆角背景 - setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); + private void setBackground(NoteItemData data, boolean isNightMode) { + if (isNightMode) { + // 夜间模式:使用统一的深色背景 + setBackgroundColor(getContext().getResources().getColor(R.color.night_note_background)); + } else { + // 日间模式:根据便签类型和位置选择背景 + int id = data.getBgColorId(); // 获取背景颜色ID + + if (data.getType() == Notes.TYPE_NOTE) { + // 便签类型:根据位置选择背景 + if (data.isSingle() || data.isOneFollowingFolder()) { + // 单独项或文件夹后的唯一便签:单独项背景 + setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); + } else if (data.isLast()) { + // 最后一项:底部圆角背景 + setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); + } else if (data.isFirst() || data.isMultiFollowingFolder()) { + // 第一项或文件夹后的多个便签之一:顶部圆角背景 + setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); + } else { + // 中间项:普通直角背景 + setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); + } } else { - // 中间项:普通直角背景 - setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); + // 文件夹类型:统一使用文件夹背景 + setBackgroundResource(NoteItemBgResources.getFolderBgRes()); } - } else { - // 文件夹类型:统一使用文件夹背景 - setBackgroundResource(NoteItemBgResources.getFolderBgRes()); } } diff --git a/src/java/net/micode/notes/ui/NotesPreferenceActivity.java b/src/java/net/micode/notes/ui/NotesPreferenceActivity.java index effe600..06cca59 100644 --- a/src/java/net/micode/notes/ui/NotesPreferenceActivity.java +++ b/src/java/net/micode/notes/ui/NotesPreferenceActivity.java @@ -37,8 +37,10 @@ import android.content.IntentFilter; // 意图过滤器 import android.content.SharedPreferences; // 偏好设置 import android.os.Bundle; // 状态保存 // Android偏好设置 +import android.preference.CheckBoxPreference; // 复选框偏好设置 import android.preference.Preference; // 偏好设置项 import android.preference.Preference.OnPreferenceClickListener; // 偏好设置点击监听 +import android.preference.Preference.OnPreferenceChangeListener; // 偏好设置变化监听 import android.preference.PreferenceActivity; // 偏好设置Activity基类 import android.preference.PreferenceCategory; // 偏好设置分类 import android.preference.PreferenceManager; // 偏好设置管理器 @@ -141,7 +143,7 @@ public class NotesPreferenceActivity extends PreferenceActivity { mReceiver = new GTaskReceiver(); IntentFilter filter = new IntentFilter(); filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); - registerReceiver(mReceiver, filter,Context.RECEIVER_EXPORTED); + registerReceiver(mReceiver, filter, Context.RECEIVER_EXPORTED); mOriAccounts = null; // 初始化原始账户列表 // 添加设置界面头部 diff --git a/src/res/drawable/tag_background.xml b/src/res/drawable/tag_background.xml new file mode 100644 index 0000000..4de84b6 --- /dev/null +++ b/src/res/drawable/tag_background.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/src/res/layout/note_item.xml b/src/res/layout/note_item.xml index eb899ed..adb76c4 100644 --- a/src/res/layout/note_item.xml +++ b/src/res/layout/note_item.xml @@ -59,6 +59,15 @@ android:layout_height="wrap_content" android:textAppearance="@style/TextAppearanceSecondaryItem" /> + + + + + \ No newline at end of file diff --git a/src/res/menu/note_list.xml b/src/res/menu/note_list.xml index 42ea736..3c306fe 100644 --- a/src/res/menu/note_list.xml +++ b/src/res/menu/note_list.xml @@ -36,4 +36,14 @@ + + + + + + diff --git a/src/res/values/colors.xml b/src/res/values/colors.xml index 2b72ca2..b0e526a 100644 --- a/src/res/values/colors.xml +++ b/src/res/values/colors.xml @@ -47,13 +47,41 @@ #335b5b5b + ======================= 便签背景颜色 ======================= + 名称:note_background + 颜色值:#ffffff + 功能:便签的默认背景颜色 + 使用场景: + 1. 便签编辑界面背景 + 2. 便签列表项背景 +--> #ffffff + + + + #000000 + + + #ffffff + + + #d0d0d0 + + + #1a1a1a + + + #000000 + + + #2a2a2a + + + #3a3a3a + + + #b0b0b0 + + + #2196f3 \ No newline at end of file diff --git a/src/res/values/strings.xml b/src/res/values/strings.xml index 6cc899d..788ca25 100644 --- a/src/res/values/strings.xml +++ b/src/res/values/strings.xml @@ -132,4 +132,17 @@ Search History Set Cancel + + + Tag added + Failed to add tag + Note not saved, cannot add tag + Failed to add tag + Tags + Password Protection + + + Night Mode + Night mode enabled + Night mode disabled \ No newline at end of file diff --git a/src/res/values/styles.xml b/src/res/values/styles.xml index eebb6bc..6a69f78 100644 --- a/src/res/values/styles.xml +++ b/src/res/values/styles.xml @@ -305,6 +305,47 @@ @android:color/white @android:style/TextAppearance.Holo.Widget.ActionBar.Title + + + + + + + + + + + + \ No newline at end of file