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