From d4024054a69e8f95422eaf8367dc73e983dd6e7b Mon Sep 17 00:00:00 2001 From: mc19 <2716188191@qq.com> Date: Thu, 5 Feb 2026 19:05:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/~$软件的质量分析报告文档.docx | Bin 0 -> 162 bytes src/notes/MainActivity.java | 24 + src/notes/data/Contact.java | 32 +- src/notes/data/Notes.java | 103 +- src/notes/data/NotesDatabaseHelper.java | 205 +- src/notes/data/UserDatabaseHelper.java | 3 + src/notes/gtask/remote/GTaskSyncService.java | 2 +- src/notes/model/Note.java | 74 +- src/notes/model/WorkingNote.java | 124 +- src/notes/tool/AIService.java | 624 ++++ src/notes/tool/ImageSpanUtils.java | 9 +- src/notes/tool/PrivacyLockManager.java | 1 - src/notes/tool/SmartReminderManager.java | 503 +++ src/notes/tool/UserManager.java | 42 +- src/notes/ui/AlarmInitReceiver.java | 7 +- src/notes/ui/LoginRegisterActivity.java | 149 +- src/notes/ui/NoteEditActivity.java | 2948 +++++++++--------- src/notes/ui/NoteEditText.java | 4 +- src/notes/ui/NoteItemData.java | 61 +- src/notes/ui/NotesListActivity.java | 968 +++--- src/notes/ui/NotesListItem.java | 61 +- src/notes/ui/NotesPreferenceActivity.java | 6 +- src/notes/ui/RichEditor.java | 277 ++ src/notes/ui/SplashActivity.java | 53 +- 24 files changed, 3912 insertions(+), 2368 deletions(-) create mode 100644 doc/~$软件的质量分析报告文档.docx create mode 100644 src/notes/MainActivity.java create mode 100644 src/notes/tool/AIService.java create mode 100644 src/notes/tool/SmartReminderManager.java create mode 100644 src/notes/ui/RichEditor.java diff --git a/doc/~$软件的质量分析报告文档.docx b/doc/~$软件的质量分析报告文档.docx new file mode 100644 index 0000000000000000000000000000000000000000..88c902712d2a62b4bf892ae1388370eec2a85be3 GIT binary patch literal 162 fcmZRzFf+6;VjvN)GFUK}F&F|#lC_d(69WSP6hZ { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + } +} \ No newline at end of file diff --git a/src/notes/data/Contact.java b/src/notes/data/Contact.java index 13c9564..206eaf3 100644 --- a/src/notes/data/Contact.java +++ b/src/notes/data/Contact.java @@ -32,10 +32,15 @@ import android.util.Log; import java.util.HashMap; /** - * Contact类是联系人工具类 - * 根据提供的电话号码反向查询联系人姓名 - * 本类所有方法均为静态,无需实例化。 - */ +*Contact类是联系人工具类 +* +*

根据“提供的电话号码反向查询联系人姓名”的能力,并带内存缓存,避免重复查库。

+* 本类所有方法均为静态,无需实例化。 +* +* @author 蒙程 + * @version 1.0 + * @since [起始版本] +*/ public class Contact { //HashMap表示联系人和电话号码之间的映射关系。只声明,不new,是延迟初始化的表现 private static HashMap sContactCache; @@ -51,13 +56,15 @@ public class Contact { /** * 根据电话号码获取联系人姓名 - * 优先读取内存缓存,未命中时查询系统联系人数据库,并将结果写入缓存 + *

优先读取内存缓存,未命中时查询系统联系人数据库,并将结果写入缓存。

* @param context 上下文,用于访问ContentResolver * @param phoneNumber 待查询的电话号码(可为国际格式、带前缀 0 等) - * @return 联系人姓名;(未匹配到或者发生异常时,返回@code null) + * @return 联系人姓名;未匹配到或者发生异常时,返回@code null + * @throws + * @see android.provider.ContactsContract.CommonDataKinds */ public static String getContact(Context context, String phoneNumber) { - //不到真正要用的时候,不给对象分配内存,第一次被调用时才new + //不到真正要用的时候,不给对象分配内存。第一次被调用时才new。 if(sContactCache == null) { sContactCache = new HashMap(); } @@ -67,11 +74,11 @@ public class Contact { return sContactCache.get(phoneNumber); } - //构造匹配串,提取电话号码数字核心部分 + //构造匹配串,提取数字核心部分 String selection = CALLER_ID_SELECTION.replace("+", PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); - //getcontentresolver.query()方法查询数据库,根据URI和电话号码匹配 + //cursor是数据库游标,初始位置是-1;query把SQL拼装好后交给系统联系人Provider Cursor cursor = context.getContentResolver().query( Data.CONTENT_URI, new String [] { Phone.DISPLAY_NAME }, @@ -79,17 +86,16 @@ public class Contact { new String[] { phoneNumber }, null); - //利用cursor获取联系人姓名 + //指针移到第一行,成功返回true if (cursor != null && cursor.moveToFirst()) { try { String name = cursor.getString(0); sContactCache.put(phoneNumber, name); //将号码-姓名键值对放进内存缓存 return name; - } catch (IndexOutOfBoundsException e) { - //捕获索引越界异常 + } catch (IndexOutOfBoundsException e) { //捕获异常,索引超出列范围的情况,返回错误日志 Log.e(TAG, " Cursor get string error " + e.toString()); return null; - } finally { + } finally { //无论是否发生异常都必须释放cursor。 cursor.close(); } } else { //查询失败,输出日志信息 diff --git a/src/notes/data/Notes.java b/src/notes/data/Notes.java index 34c26dd..b871c3a 100644 --- a/src/notes/data/Notes.java +++ b/src/notes/data/Notes.java @@ -25,18 +25,21 @@ package net.micode.notes.data; import android.net.Uri; /** -* Notes是便签数据库类 -*

定义了URI常量、便签/文件夹类型常量、Intent扩展字段名、数据库接口、两种业务实体

-* 所有字段均用static final修饰,静态变量,静态常量,静态方法,静态内部类。 +*Notes是便签数据库类 + +*

定义了URI常量、便签/文件夹类型常量、Intent扩展字段名、数据库接口、两种与业务实体

+* 本类所有字段均用static final修饰,说明字段不会变,可以直接用 + +* @author 蒙程 +* @since [起始版本] */ public class Notes { + //一、ContentProvider权威域名 public static final String AUTHORITY = "micode_notes"; //拼接URI的前缀 - public static final String TAG = "Notes"; //日志显示的标记 + public static final String TAG = "Notes"; //日志显示Notes - /** - * 定义便签/文件夹/系统文件夹类型常量 - */ + //二、便签/文件夹/系统文件夹 类型常量 public static final int TYPE_NOTE = 0; //普通文本便签类型常量为0 public static final int TYPE_FOLDER = 1; //文件夹类型常量为1 public static final int TYPE_SYSTEM = 2; //系统文件夹(如回收站)类型常量为2 @@ -47,15 +50,15 @@ public class Notes { * {@link Notes#ID_TEMPORARY_FOLDER } is for notes belonging no folder * {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records */ + + //三、系统保留文件夹 ID public static final int ID_ROOT_FOLDER = 0; //默认文件夹ID为0 public static final int ID_TEMPORARY_FOLDER = -1; //临时文件夹 public static final int ID_CALL_RECORD_FOLDER = -2; //通话记录文件夹ID,保存由通话备忘功能生成的便签 public static final int ID_TRASH_FOLDER = -3; - /** - * Intent实现各个组件(界面)之间数据的传递 - */ + //四、Intent是什么? public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; //提醒时间戳 public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; //便签背景颜色 public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id"; //桌面便签小部件 @@ -63,23 +66,21 @@ public class Notes { public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id"; //便签父文件夹ID public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date"; //通话记录时间戳 - /** - * 定义桌面小部件类型常量 - */ + //五、桌面小部件类型常量 public static final int TYPE_WIDGET_INVALIDE = -1; public static final int TYPE_WIDGET_2X = 0; public static final int TYPE_WIDGET_4X = 1; /** - * MIME是描述文件类型的一种标准 - * Android的ContentResolver在查询/插入/打开文件时会首先查询MIME类型 + *六、便签数据MIME汇总常量类 + * Android 的 ContentResolver 在查询/插入/打开文件时会先问 URI 对应的 MIME: */ public static class DataConstants { public static final String NOTE = TextNote.CONTENT_ITEM_TYPE; //文本便签MIME类型 public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE; //通话备忘MIME类型 - public static final String IMAGE = ImageData.CONTENT_ITEM_TYPE; //图片MIME类型 } + //七、基础URI /** * Uri to query all notes and folders */ @@ -91,9 +92,10 @@ public class Notes { */ public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); + //八、数据库列名接口 —— Note 表 /** - * NoteColumns是便签数据库列名接口 - * 声明便签信息列名,包括便签ID,父文件夹ID,创建日期,修改日期,提醒日期等 + * NoteColumns是便签数据库列名接口,只定义/声明一些方法、变量 + * 此处声明了便签数据列名有ID,PARENT_ID,创建日期,修改日期,提醒日期等 */ public interface NoteColumns { /** @@ -193,31 +195,36 @@ public class Notes { *

Type : TEXT

*/ public static final String GTASK_ID = "gtask_id"; - + /** * Alert location latitude *

Type: REAL

*/ public static final String ALERT_LATITUDE = "alert_latitude"; - + /** * Alert location longitude *

Type: REAL

*/ public static final String ALERT_LONGITUDE = "alert_longitude"; - + /** * Alert location radius *

Type: REAL

*/ public static final String ALERT_RADIUS = "alert_radius"; - + /** * Alert location name *

Type: TEXT

*/ public static final String ALERT_LOCATION_NAME = "alert_location_name"; + /** + * Note tag + *

Type: TEXT

+ */ + public static final String TAG = "tag"; /** * The version code @@ -245,8 +252,10 @@ public class Notes { } + //九、数据库列名接口 —— Data 表 + /** - * 数据库列名接口,声明与便签具体内容相关的字段 + * DataColumns是数据库列名接口 */ public interface DataColumns { /** @@ -320,51 +329,37 @@ public class Notes { *

Type: TEXT

*/ public static final String DATA5 = "data5"; - - /** - * Rich text format information for storing span information - *

Type: TEXT

- */ - public static final String RICH_TEXT_FORMAT = "rich_text_format"; - - /** - * Path to the image file for image data - *

Type: TEXT

- */ - public static final String IMAGE_PATH = "image_path"; } - /** - * TextNote是文本便签实体常量类 - * 实现DataColumns接口中声明的方法 - */ + //十、文本便签实体常量类 + //文本便签TextNote类实现DataColumns接口中声明的方法 public static final class TextNote implements DataColumns { /** * Mode to indicate the text in check list mode or not *

Type: Integer 1:check list mode 0: normal mode

*/ - public static final String MODE = DATA1; //存储文本的清单模式或普通文本模式 + public static final String MODE = DATA1; //清单模式or普通文本模式? public static final int MODE_CHECK_LIST = 1; - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note"; //MIME类型为批量笔记 + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note"; //MIME类型为集合(多条) - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; //MIME类型为单条笔记 + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; //MIME类型为单条 public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); } /** - * CallNote是通话记录便签实体常量类 - * TextNote和Callote实现DataColumns接口,体现接口的多实现特性 + *通话记录便签实体常量类 + * 通话记录CallNote类也实现了DataColumns接口,体现了接口的多实现特性。 */ public static final class CallNote implements DataColumns { /** * Call date for this record *

Type: INTEGER (long)

*/ - public static final String CALL_DATE = DATA1; //DATA1存储通话的时间戳 + public static final String CALL_DATE = DATA1; //DATA1是数据库中的真实列名,将通话的时间戳存入DATA1列中 /** * Phone number for this record @@ -378,22 +373,4 @@ public class Notes { public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); } - - /** - * 图片数据实体常量类 - * 增加原因是新增了便签插入图片功能 - */ - public static final class ImageData implements DataColumns { - /** - * Path to the image file - *

Type: TEXT

- */ - public static final String PATH = DATA3; - - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/image_data"; - - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/image_data"; - - public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/image_data"); - } } diff --git a/src/notes/data/NotesDatabaseHelper.java b/src/notes/data/NotesDatabaseHelper.java index 1445fd7..7b9fdf8 100644 --- a/src/notes/data/NotesDatabaseHelper.java +++ b/src/notes/data/NotesDatabaseHelper.java @@ -32,29 +32,31 @@ import net.micode.notes.data.Notes.DataConstants; import net.micode.notes.data.Notes.NoteColumns; /** -*便签数据库辅助类: +*便签数据库辅助类 +

+ *负责以下功能: *

    - *
  • 版本:7
  • - *
  • 数据库名:note.db
  • - *
  • 创建/升级note与data表
  • - *
  • 建立更新和删除触发器维护文件夹便签数量和内容
  • - *
  • 插入系统文件夹(根、临时、通话、回收站)
  • - *
  • 提供单例实例
  • + *
  • 创建/升级 note 与 data 表
  • + *
  • 建立触发器维护文件夹便签数量、便签摘要
  • + *
  • 插入系统文件夹(根、临时、通话、回收站)
  • + *
  • 提供单例实例
  • *
+

* * @author 蒙程 - * @version 1.0 - * @since 2025-12-13 + * @version {@link #DB_VERSION} 4 + * @since 2010-2011 */ public class NotesDatabaseHelper extends SQLiteOpenHelper { /** - *一、声明数据库基础常量 + *一、数据库基础常量 */ private static final String DB_NAME = "note.db"; - private static final int DB_VERSION = 7; + private static final int DB_VERSION = 8; + /**表明常量接口*/ public interface TABLE { public static final String NOTE = "note"; @@ -63,17 +65,16 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { private static final String TAG = "NotesDatabaseHelper"; - //类加载时不创建实例单例,真正需要使用时才调用getInstance初始化,节省应用启动时的内存和资源开销。 + //类加载时不创建NotesDatabaseHelper实例(mInstance),真正需要使用数据库时才调用getInstance初始化,节省应用启动时的内存和资源开销。 private static NotesDatabaseHelper mInstance; + + //二、建表SQL,初始化note和data表,同时添加note_id_index索引列 /** - * 二、初始化Note和Data表中各列 - * NoteColumns是 Notes类便签数据库列名接口,ID、PARENT_ID等都是列名 - *

- * NOT NULL约束强制列不接受NULL值,DEFAULT 0 表示默认值为0, - * 如果字段设定 NOT NULL,没有输入值时,用DEFAULT后的默认值填充。 - *

- * strftime是 SQLite内置的时间、日期格式化函数,参数为输出格式(%s表示秒级Unix时间戳)、时间源(‘now'表示现在) + * 结合在Notes中定义的NoteColumns初始化 + * 在Notes类里定义了NoteColumns,是便签数据库列名接口,ID、PARENT_ID等都是列名,表示把Notes数据库里声明的列名拿来用 + * NOT NULL约束强制列不接受NULL值,DEFAULT 0 表示默认值为0,如果字段设定NOT NULL,没有输入值时,DEFAULT后跟的默认值来填充 + * 这里的strftime是SQLite内置的时间、日期格式化函数,参数为输出格式(%s表示秒级Unix时间戳)、时间源(‘now'表示现在) */ private static final String CREATE_NOTE_TABLE_SQL = "CREATE TABLE " + TABLE.NOTE + "(" + //开始建表,表名是TABLE.NOTE(note) @@ -93,18 +94,22 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.TAG + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0," + NoteColumns.ALERT_LATITUDE + " REAL NOT NULL DEFAULT 0," + NoteColumns.ALERT_LONGITUDE + " REAL NOT NULL DEFAULT 0," + NoteColumns.ALERT_RADIUS + " REAL NOT NULL DEFAULT 0," + NoteColumns.ALERT_LOCATION_NAME + " TEXT NOT NULL DEFAULT ''," + - NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + + NoteColumns.LOCKED + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.LOCK_TYPE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ENCRYPTED_PASSWORD + " TEXT NOT NULL DEFAULT ''" + ")"; /** * 创建 Data 表 SQL,结合在Notes中定义的DataColumns初始化 *

- * 外键:{@link DataColumns#NOTE_ID} 指向 note.id
- * 通用列 data1~data5 含义由 {@link DataColumns#MIME_TYPE} 决定。 + * 外键:{@link DataColumns#NOTE_ID} 指向 note.id
+ * 通用列 data1~data5 含义由 {@link DataColumns#MIME_TYPE} 决定。 *

*/ private static final String CREATE_DATA_TABLE_SQL = @@ -119,25 +124,24 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { DataColumns.DATA2 + " INTEGER," + DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + - DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''," + - DataColumns.RICH_TEXT_FORMAT + " TEXT NOT NULL DEFAULT ''," + - DataColumns.IMAGE_PATH + " TEXT NOT NULL DEFAULT ''" + + DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + ")"; /** - *

在data表中如果不存在 note_id索引列,则创建

- *

便于执行"SELECT * FROM data WHERE note_id=?"之类的查询语句

+ *在data(NOTE_ID)这一列上创建名为note_id_index的索引(如果该索引不存在) + * 便于执行SELECT * FROM data WHERE note_id=?之类的查询语句 */ private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = "CREATE INDEX IF NOT EXISTS note_id_index ON " + TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; + + //三、创建Note表触发器,在文件夹下便签数量变化时自动增减便签数量(notes_count)的值 /** - * 三、创建 Note表触发器,文件夹下便签数量变化时自动增减便签数量的值 * Increase folder's note count when move note to the folder - * 创建触发器 increase_folder_count_on_update - * 在 note表的 parent_id列更新后 - * 执行触发器体,更新 Note表,设置 notes_count=notes_count+1,在 id=new.parent_id的地方 + * 创建触发器,名字叫increase_folder_count_on_update + * 在note表的parent_id列更新后 + * 执行触发器体,更新Note表,设置notes_count=notes_count+1,在id=new.parent_id的地方 * 此处用于自动更新父文件夹下的便签数量 */ private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = @@ -187,11 +191,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { " AND " + NoteColumns.NOTES_COUNT + ">0;" + " END"; + //四、Data 表触发器:自动同步便签摘要(snippet) /** - * 四、Data 表触发器:自动同步便签摘要(snippet) * Update note's content when insert data with type {@link DataConstants#NOTE} - * 启动触发器:新插入的记录 mime_type字段值为note类型(其他数据类型,比如图片,则不触发) - * 执行触发器:根据 note_id将 Note表里相应的snippet列值更新为 DataColumn表中的content列值 + * 仅当新插入的DATA记录的mime_type字段值为note(笔记)类型时,触发器才会执行后续逻辑,若插入的是其他数据类型,比如图片,则不触发 + * 执行触发器,将note表里的snippet列值更新为新纪录的content值,且仅更新note表里主键id=新纪录note_id的行 */ private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER = "CREATE TRIGGER update_note_content_on_insert " + @@ -231,8 +235,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { /** * Delete data belong to note which has been deleted - * 当从 note表删除一条笔记记录后,通过 note表的id与 data表的 note_id关联 - * 自动删除data表中所有与这条被删笔记关联的记录 + * 当从note表删除一条笔记记录后,通过data表的note_id与note表的id关联 + * 自动删除data表中所有与这条被删笔记关联的记录,笔记-笔记内容 */ private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER = "CREATE TRIGGER delete_data_on_delete " + @@ -244,8 +248,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { /** * Delete notes belong to folder which has been deleted - * 从note表里删除一条笔记,自动级联删除note表中所有parent_id为该记录id的子记录 - * 比如:删除一个文件夹,该文件夹下的所有笔记都会被删除 + * 当从note表里删除一条记录后,自动级联删除note表中所有parent_id等于被删记录id的子记录 */ private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER = "CREATE TRIGGER folder_delete_notes_on_delete " + @@ -269,7 +272,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { " END"; /** - * Android开发中自定义的数据库辅助类的构造方法,作用是初始化要创建/管理的数据库名称、版本号等核心参数 + * Android开发中自定义的数据库辅助类的构造方法,作用是初始化,指定要创建/管理的数据库名称、版本号等核心参数 * @param context 上下文,提供应用运行的环境信息,如访问资源、数据库文件路径、系统服务等。 */ public NotesDatabaseHelper(Context context) { @@ -277,15 +280,13 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { /** super是调用的父类(SQLiteOpenHelper)构造方法 * @param context 父类通过context确定数据库文件的存储位置 * @param DB_NAME 数据库文件名 - * @param null 创建数据库查询的游标(cursor),null表示默认(默认值为 1,表示只读) - * @param DB_VERSION 自定义常量,数据库版本号 + * @param null 创建数据库查询的游标(cursor),传null表示默认 + * @param DB_VERSION自定义常量,数据库版本号 */ super(context, DB_NAME, null, DB_VERSION); } - /** - * 创建Note表 - */ + //完成note表的全量初始化 public void createNoteTable(SQLiteDatabase db) { db.execSQL(CREATE_NOTE_TABLE_SQL); //执行SQL语句创建note表 reCreateNoteTableTriggers(db); //重建该表关联的触发器 @@ -293,9 +294,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { Log.d(TAG, "note table has been created"); //打印日志,确认note表成功创建 } - /** - * 批量重建触发器,先删除旧的,再建立新的 - */ + //批量重建触发器,先删除旧的,再建立新的 private void reCreateNoteTableTriggers(SQLiteDatabase db) { db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update"); db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update"); @@ -314,11 +313,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER); } - /** - * 创建系统文件夹 - * 向note表插入四个系统默认文件夹:通话记录、根、临时、回收站.保证应用首次启动时有基础的文件夹结构 - * @param db - */ + //向note表插入四个系统默认文件夹:通话记录、根、临时、回收站。保证应用首次启动时有基础的文件夹结构。 private void createSystemFolder(SQLiteDatabase db) { //ContentValues类用于存储键值对 ContentValues values = new ContentValues(); @@ -355,9 +350,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { db.insert(TABLE.NOTE, null, values); } - /** - * 创建 Data 表 - */ public void createDataTable(SQLiteDatabase db) { db.execSQL(CREATE_DATA_TABLE_SQL); reCreateDataTableTriggers(db); @@ -376,9 +368,10 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { } /** - * synchronized同步关键字,保证多线程安全,避免多个线程同时调用该方法时创建多个实例 - * getInstance方法用于获取唯一实例 + * synchronized同步关键字,保证多线程安全,避免多个线程同时调用该方法时,进入if分支,创建多个实例 + * getInstance函数用于获取唯一实例 */ + static synchronized NotesDatabaseHelper getInstance(Context context) { //首次调用该方法时,才创建实例;若已创建,则直接复用 if (mInstance == null) { @@ -387,24 +380,13 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { return mInstance; } - /** - * onCreate方法在数据库第一次创建时调用 - * "@override"表示子类重写父类方法,实现多态性 - */ + //@override是Java中的一种注解,用于明确表示子类的方法是对父类方法的重写,实现多态性 @Override public void onCreate(SQLiteDatabase db) { - createNoteTable(db); //初始化笔记表Note - createDataTable(db); //初始化数据表Data + createNoteTable(db); //初始化笔记表 + createDataTable(db); //初始化数据表 } - /** - * onUpgrade方法在数据库升级时调用 - *

- * 什么时候需要更新数据库版本? - *

  • 数据库结构发生改变,比如添加字段 rich_text_format
  • - *
  • 数据库结构没有改变,但数据结构发生改变, 比如增加了富文本样式
  • - *

    - */ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { boolean reCreateTriggers = false; @@ -428,24 +410,24 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { } if (oldVersion == 4) { - upgradeToV5(db); - oldVersion++; - } + upgradeToV5(db); + oldVersion++; + } if (oldVersion == 5) { - upgradeToV6(db); - oldVersion++; - } + upgradeToV6(db); + oldVersion++; + } if (oldVersion == 6) { - upgradeToV7(db); - oldVersion++; - } + upgradeToV7(db); + oldVersion++; + } if (oldVersion == 7) { - upgradeToV8(db); - oldVersion++; - } + upgradeToV8(db); + oldVersion++; + } if (reCreateTriggers) { reCreateNoteTableTriggers(db); @@ -454,13 +436,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { if (oldVersion != newVersion) { throw new IllegalStateException("Upgrade notes database to version " + newVersion - + "fails"); + + " fails"); } } - /** - * v1到v2的升级,删除旧的note data表,清空所有用户数据,重建新的 - */ + //v1到v2的升级,删除旧的note data表,清空所有用户数据,重建新的 private void upgradeToV2(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); //执行具体的SQL语句,删除旧的note表 db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); //删除旧的data表 @@ -468,9 +448,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { createDataTable(db); } - /** - * v2到v3的升级,清除无用触发器,Note表新增 gtask_id列,新增回收站文件夹 - */ + //v2到v3的升级,清除无用触发器,新增gtask_id列,新增回收站文件夹 private void upgradeToV3(SQLiteDatabase db) { // drop unused triggers db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert"); @@ -486,49 +464,46 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { db.insert(TABLE.NOTE, null, values); } - /** - *v3到 v4的升级,为 note表新增 version列,且约束非空,默认为0 - */ + //v3到v4的升级,为note表新增version列,且约束非空,默认为0 private void upgradeToV4(SQLiteDatabase db) { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0"); } - /** - * v4到v5的升级,为 data表新增 rich_text_format列,用于存储富文本格式信息 - */ + //v4到v5的升级,为note表新增tag列,且约束非空,默认为空字符串 private void upgradeToV5(SQLiteDatabase db) { - db.execSQL("ALTER TABLE " + TABLE.DATA + " ADD COLUMN rich_text_format TEXT NOT NULL DEFAULT ''"); + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.TAG + + " TEXT NOT NULL DEFAULT ''"); } - /** - * v5到v6的升级,为 data表新增用于存储图片路径的列 - */ + //v5到v6的升级,为note表新增位置提醒相关的列 private void upgradeToV6(SQLiteDatabase db) { - db.execSQL("ALTER TABLE " + TABLE.DATA + " ADD COLUMN image_path TEXT NOT NULL DEFAULT ''"); + // 添加位置提醒相关的列 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.ALERT_LATITUDE + + " REAL NOT NULL DEFAULT 0"); + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.ALERT_LONGITUDE + + " REAL NOT NULL DEFAULT 0"); + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.ALERT_RADIUS + + " REAL NOT NULL DEFAULT 0"); + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.ALERT_LOCATION_NAME + + " TEXT NOT NULL DEFAULT ''"); } - - /** - * v6到v7的升级,为 note表新增位置提醒相关的列 - */ + + //v6到v7的升级 private void upgradeToV7(SQLiteDatabase db) { - db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.ALERT_LATITUDE + " REAL NOT NULL DEFAULT 0"); //纬度 - db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.ALERT_LONGITUDE + " REAL NOT NULL DEFAULT 0"); //经度 - db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.ALERT_RADIUS + " REAL NOT NULL DEFAULT 0"); //半径 - db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.ALERT_LOCATION_NAME + " TEXT NOT NULL DEFAULT ''"); //地点名称 + // 移除重复添加的位置提醒列 + // 这些列已经在 v6 版本中添加过了 } - /** - * v7到v8的升级,为 note表新增隐私锁相关的列 - */ + //v7到v8的升级,为 note表新增隐私锁相关的列 private void upgradeToV8(SQLiteDatabase db) { // 锁定状态:0-未锁定,1-已锁定 db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.LOCKED + " INTEGER NOT NULL DEFAULT 0"); - + // 锁类型:0-无锁,1-密码锁,2-手势锁 db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.LOCK_TYPE + " INTEGER NOT NULL DEFAULT 0"); - + // 加密后的密码 db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.ENCRYPTED_PASSWORD + " TEXT NOT NULL DEFAULT ''"); } -} \ No newline at end of file +} diff --git a/src/notes/data/UserDatabaseHelper.java b/src/notes/data/UserDatabaseHelper.java index 64d9c4c..cb21d34 100644 --- a/src/notes/data/UserDatabaseHelper.java +++ b/src/notes/data/UserDatabaseHelper.java @@ -108,8 +108,10 @@ public class UserDatabaseHelper extends SQLiteOpenHelper { boolean authenticated = cursor.getCount() > 0; cursor.close(); + return authenticated; } + /** * 检查用户名是否存在 * @param username 用户名 @@ -126,6 +128,7 @@ public class UserDatabaseHelper extends SQLiteOpenHelper { boolean exists = cursor.getCount() > 0; cursor.close(); + return exists; } diff --git a/src/notes/gtask/remote/GTaskSyncService.java b/src/notes/gtask/remote/GTaskSyncService.java index a34a8b4..ee3650b 100644 --- a/src/notes/gtask/remote/GTaskSyncService.java +++ b/src/notes/gtask/remote/GTaskSyncService.java @@ -81,7 +81,7 @@ public class GTaskSyncService extends Service { /** * 处理启动服务的意图 * 解析意图中的动作类型,执行对应的启动/取消同步操作 - * @param intent The Intent supplied to {@link android.content.Context#startService}, + * @param intent The Intent supplied to {@link Context#startService}, * as given. This may be null if the service is being restarted after * its process has gone away, and it had previously returned anything * except {@link #START_STICKY_COMPATIBILITY}. diff --git a/src/notes/model/Note.java b/src/notes/model/Note.java index eb68f95..ecf9c21 100644 --- a/src/notes/model/Note.java +++ b/src/notes/model/Note.java @@ -110,15 +110,6 @@ public class Note { mNoteData.setCallData(key, value); } - public void setRichTextFormat(String formatInfo) { - mNoteData.setRichTextFormat(formatInfo); - } - - // 设置图片数据 - public void setImageData(String key, String value) { - mNoteData.setImageData(key, value); - } - //是否有本地修改 public boolean isLocalModified() { return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified(); @@ -166,10 +157,6 @@ public class Note { private ContentValues mTextDataValues; // 存储文本数据的变化 - private ContentValues mRichTextFormatValues; // 存储富文本格式信息 - - private ContentValues mImageDataValues; // 存储图片数据的变化 - private long mCallDataId; // 通话记录数据在数据库中的ID(0表示未创建) private ContentValues mCallDataValues; // 存储通话数据的变化(如号码更新) @@ -178,15 +165,13 @@ public class Note { public NoteData() { mTextDataValues = new ContentValues(); - mRichTextFormatValues = new ContentValues(); - mImageDataValues = new ContentValues(); mCallDataValues = new ContentValues(); mTextDataId = 0; mCallDataId = 0; } boolean isLocalModified() { - return mTextDataValues.size() > 0 || mCallDataValues.size() > 0 || mImageDataValues.size() > 0; + return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; } void setTextDataId(long id) { @@ -215,18 +200,6 @@ public class Note { mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); } - void setRichTextFormat(String formatInfo) { - mRichTextFormatValues.put(DataColumns.RICH_TEXT_FORMAT, formatInfo); - mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); - mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); - } - - void setImageData(String key, String value) { - mImageDataValues.put(key, value); - mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); - mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); - } - /** * 将文本/通话数据的变化同步到数据库 * 若数据未创建(ID=0)则插入新记录;若已创建(ID>0)则更新记录 @@ -248,27 +221,22 @@ public class Note { ContentProviderOperation.Builder builder = null; //处理文本数据同步 - if(mTextDataValues.size() > 0 || mRichTextFormatValues.size() > 0) { + if(mTextDataValues.size() > 0) { // 关联文本数据到当前笔记 mTextDataValues.put(DataColumns.NOTE_ID, noteId); - - //合并文本数据和富文本格式信息 - ContentValues combinedValues = new ContentValues(mTextDataValues); - combinedValues.putAll(mRichTextFormatValues); //文本数据未创建 if (mTextDataId == 0) { // 插入数据到ContentProvider,获取返回的Uri - combinedValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); + mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, - combinedValues); + mTextDataValues); try { // 从Uri解析新插入的文本数据ID并存储 setTextDataId(Long.valueOf(uri.getPathSegments().get(1))); } catch (NumberFormatException e) { Log.e(TAG, "Insert new text data fail with noteId" + noteId); mTextDataValues.clear(); - mRichTextFormatValues.clear(); return null; } } else { @@ -276,42 +244,10 @@ public class Note { // 构建更新操作(指定数据ID对应的Uri) builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( Notes.CONTENT_DATA_URI, mTextDataId)); - builder.withValues(combinedValues); // 设置要更新的内容 + builder.withValues(mTextDataValues); // 设置要更新的内容 operationList.add(builder.build()); // 添加到批量操作列表 } mTextDataValues.clear(); - mRichTextFormatValues.clear(); - } - - //处理图片数据同步 - if(mImageDataValues.size() > 0) { - mImageDataValues.put(DataColumns.NOTE_ID, noteId); - if (mTextDataId == 0) { // 使用文本数据ID作为参考,因为图片通常与文本数据一起存储 - mImageDataValues.put(DataColumns.MIME_TYPE, Notes.DataConstants.IMAGE); - Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, - mImageDataValues); - try { - // 图片数据ID可能需要单独处理,这里简化处理 - Log.d(TAG, "Inserting image data"); - } catch (Exception e) { - Log.e(TAG, "Insert new image data fail with noteId" + noteId); - mImageDataValues.clear(); - return null; - } - } else { - // 如果文本数据已存在,我们需要为图片数据创建新的记录 - mImageDataValues.put(DataColumns.MIME_TYPE, Notes.DataConstants.IMAGE); - Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, - mImageDataValues); - try { - Log.d(TAG, "Inserting additional image data"); - } catch (Exception e) { - Log.e(TAG, "Insert additional image data fail with noteId" + noteId); - mImageDataValues.clear(); - return null; - } - } - mImageDataValues.clear(); } //处理通话数据同步(逻辑同处理文本数据同步) diff --git a/src/notes/model/WorkingNote.java b/src/notes/model/WorkingNote.java index cf00c34..dad51b7 100644 --- a/src/notes/model/WorkingNote.java +++ b/src/notes/model/WorkingNote.java @@ -28,7 +28,6 @@ import net.micode.notes.data.Notes.CallNote; import net.micode.notes.data.Notes.DataColumns; import net.micode.notes.data.Notes.DataConstants; import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.TextNote; import net.micode.notes.tool.ResourceParser.NoteBgResources; @@ -56,6 +55,8 @@ public class WorkingNote { private int mBgColorId; + private String mTag; + private int mWidgetId; private int mWidgetType; @@ -69,7 +70,6 @@ public class WorkingNote { private boolean mIsDeleted; private NoteSettingChangedListener mNoteSettingStatusListener; - private String mRichTextFormatInfo; //指定查询Data表时需要返回的字段 public static final String[] DATA_PROJECTION = new String[] { @@ -80,9 +80,6 @@ public class WorkingNote { DataColumns.DATA2, DataColumns.DATA3, DataColumns.DATA4, - DataColumns.DATA5, - DataColumns.RICH_TEXT_FORMAT, - DataColumns.IMAGE_PATH }; //指定查询Note表时需要返回的字段 @@ -96,7 +93,8 @@ public class WorkingNote { NoteColumns.ALERT_LATITUDE, NoteColumns.ALERT_LONGITUDE, NoteColumns.ALERT_RADIUS, - NoteColumns.ALERT_LOCATION_NAME + NoteColumns.ALERT_LOCATION_NAME, + NoteColumns.TAG }; private static final int DATA_ID_COLUMN = 0; @@ -107,10 +105,6 @@ public class WorkingNote { private static final int DATA_MODE_COLUMN = 3; - private static final int DATA_RICH_TEXT_FORMAT_COLUMN = 8; - - private static final int DATA_IMAGE_PATH_COLUMN = 9; - private static final int NOTE_PARENT_ID_COLUMN = 0; private static final int NOTE_ALERTED_DATE_COLUMN = 1; @@ -126,6 +120,7 @@ public class WorkingNote { private static final int NOTE_ALERT_LONGITUDE_COLUMN = 7; private static final int NOTE_ALERT_RADIUS_COLUMN = 8; private static final int NOTE_ALERT_LOCATION_NAME_COLUMN = 9; + private static final int NOTE_TAG_COLUMN = 10; // New note construct private WorkingNote(Context context, long folderId) { @@ -172,14 +167,11 @@ public class WorkingNote { mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN); mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN); mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN); - - // 经纬度和提醒位置 mAlertLatitude = cursor.getDouble(NOTE_ALERT_LATITUDE_COLUMN); mAlertLongitude = cursor.getDouble(NOTE_ALERT_LONGITUDE_COLUMN); mAlertRadius = cursor.getFloat(NOTE_ALERT_RADIUS_COLUMN); mAlertLocationName = cursor.getString(NOTE_ALERT_LOCATION_NAME_COLUMN); - // 加载富文本格式信息 - mRichTextFormatInfo = cursor.getString(DATA_RICH_TEXT_FORMAT_COLUMN); + mTag = cursor.getString(NOTE_TAG_COLUMN); } cursor.close(); } else { @@ -206,15 +198,8 @@ public class WorkingNote { mContent = cursor.getString(DATA_CONTENT_COLUMN); mMode = cursor.getInt(DATA_MODE_COLUMN); mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN)); - // 加载富文本格式信息 - mRichTextFormatInfo = cursor.getString(DATA_RICH_TEXT_FORMAT_COLUMN); - } - //如果是图片类型 - else if (DataConstants.IMAGE.equals(type)) { - // 图片数据,目前我们只是加载数据,实际图片显示在UI层处理 - String imagePath = cursor.getString(DATA_IMAGE_PATH_COLUMN); - Log.d(TAG, "Loaded image data: " + imagePath); } + //如果是通话记录类型 else if (DataConstants.CALL_NOTE.equals(type)) { mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); @@ -355,6 +340,61 @@ public class WorkingNote { } } + //获取笔记标题(第一行) + public String getTitle() { + if (TextUtils.isEmpty(mContent)) { + return ""; + } + int firstLineEnd = mContent.indexOf('\n'); + if (firstLineEnd == -1) { + // 如果只有一行,全部作为标题 + return mContent; + } + return mContent.substring(0, firstLineEnd); + } + + //获取去掉标题后的正文内容 + public String getContentWithoutTitle() { + if (TextUtils.isEmpty(mContent)) { + return ""; + } + int firstLineEnd = mContent.indexOf('\n'); + if (firstLineEnd == -1) { + // 如果只有一行,正文为空 + return ""; + } + return mContent.substring(firstLineEnd + 1); + } + + //智能推荐标题:根据正文内容生成推荐标题 + public String generateSmartTitle(String content) { + if (TextUtils.isEmpty(content)) { + return ""; + } + + // 去掉换行符,取前50个字符作为推荐标题 + String cleanContent = content.replace('\n', ' ').trim(); + if (cleanContent.length() <= 50) { + return cleanContent; + } + return cleanContent.substring(0, 50) + "..."; + } + + //设置标题和正文 + public void setTitleAndContent(String title, String content) { + String fullContent; + if (TextUtils.isEmpty(title) && TextUtils.isEmpty(content)) { + fullContent = ""; + } else if (TextUtils.isEmpty(content)) { + fullContent = title; + } else if (TextUtils.isEmpty(title)) { + fullContent = content; + } else { + fullContent = title + "\n" + content; + } + setWorkingText(fullContent); + } + //设置笔记内容 public void setWorkingText(String text) { if (!TextUtils.equals(mContent, text)) { @@ -363,18 +403,6 @@ public class WorkingNote { } } - //设置富文本格式 - public void setRichTextFormat(String formatInfo) { - if (!TextUtils.equals(mRichTextFormatInfo, formatInfo)) { - mRichTextFormatInfo = formatInfo; - mNote.setRichTextFormat(formatInfo); - } - } - - public String getRichTextFormat() { - return mRichTextFormatInfo; - } - // 将笔记转换为通话笔记(设置通话相关数据) public void convertToCallNote(String phoneNumber, long callDate) { mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate)); @@ -385,12 +413,11 @@ public class WorkingNote { public boolean hasClockAlert() { return (mAlertDate > 0 ? true : false); } - - + public boolean hasLocationAlert() { return (mAlertLatitude != 0 && mAlertLongitude != 0); } - + public void setAlertLocation(double latitude, double longitude, float radius, String locationName, boolean set) { // 有位置变化才更新 if (latitude != mAlertLatitude || longitude != mAlertLongitude || radius != mAlertRadius || !locationName.equals(mAlertLocationName)) { @@ -398,31 +425,31 @@ public class WorkingNote { mAlertLongitude = longitude; mAlertRadius = radius; mAlertLocationName = locationName; - + mNote.setNoteValue(NoteColumns.ALERT_LATITUDE, String.valueOf(mAlertLatitude)); mNote.setNoteValue(NoteColumns.ALERT_LONGITUDE, String.valueOf(mAlertLongitude)); mNote.setNoteValue(NoteColumns.ALERT_RADIUS, String.valueOf(mAlertRadius)); mNote.setNoteValue(NoteColumns.ALERT_LOCATION_NAME, mAlertLocationName); } - + // 若监听器不为空,触发位置提醒变更回调 if (mNoteSettingStatusListener != null) { mNoteSettingStatusListener.onLocationAlertChanged(latitude, longitude, radius, locationName, set); } } - + public double getAlertLatitude() { return mAlertLatitude; } - + public double getAlertLongitude() { return mAlertLongitude; } - + public float getAlertRadius() { return mAlertRadius; } - + public String getAlertLocationName() { return mAlertLocationName; } @@ -471,10 +498,14 @@ public class WorkingNote { return mWidgetType; } - public Note getNote() { - return mNote; + public String getTag() { + return mTag; } + public void setTag(String tag) { + mTag = tag; + mNote.setNoteValue(NoteColumns.TAG, tag); + } // 笔记设置变更监听器接口(定义属性变化时的回调方法) public interface NoteSettingChangedListener { @@ -487,13 +518,12 @@ public class WorkingNote { * Called when user set clock */ void onClockAlertChanged(long date, boolean set); - + /** * Called when user set location alert */ void onLocationAlertChanged(double latitude, double longitude, float radius, String locationName, boolean set); - /** * Call when user create note from widget */ diff --git a/src/notes/tool/AIService.java b/src/notes/tool/AIService.java new file mode 100644 index 0000000..4a46782 --- /dev/null +++ b/src/notes/tool/AIService.java @@ -0,0 +1,624 @@ +package net.micode.notes.tool; + +import android.content.Context; +import android.os.AsyncTask; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * AI服务类,用于智能生成标题 + * 这里使用模拟实现,实际项目中可以替换为真实的AI API调用 + */ +public class AIService { + private Context mContext; + private OnAITitleGeneratedListener mListener; + + public interface OnAITitleGeneratedListener { + void onTitleGenerated(String title); + } + + public AIService(Context context) { + mContext = context; + } + + public void setOnAITitleGeneratedListener(OnAITitleGeneratedListener listener) { + mListener = listener; + } + + /** + * 智能生成标题 + * @param content 正文内容 + */ + public void generateSmartTitle(String content) { + if (TextUtils.isEmpty(content)) { + if (mListener != null) { + mListener.onTitleGenerated(""); + } + return; + } + + // 使用异步任务模拟AI调用 + new GenerateTitleTask().execute(content); + } + + /** + * 模拟AI生成标题的异步任务 + */ + private class GenerateTitleTask extends AsyncTask { + @Override + protected String doInBackground(String... params) { + String content = params[0]; + + // 模拟AI处理延迟 + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // 模拟AI生成标题逻辑 + // 这里可以替换为真实的AI API调用,如GPT、百度文心一言等 + return generateMockAITitle(content); + } + + @Override + protected void onPostExecute(String title) { + if (mListener != null) { + mListener.onTitleGenerated(title); + } + } + } + + /** + * 模拟AI生成标题 + * @param content 正文内容 + * @return 生成的标题 + */ + private String generateMockAITitle(String content) { + // 清理内容,去掉特殊字符 + String cleanContent = content.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5 \\t\\n\\r\\f\\v]", "").trim(); + + // 分析内容主题 + String theme = analyzeTheme(cleanContent); + + // 分析内容类型 + String contentType = analyzeContentType(cleanContent); + + // 提取核心关键词 + List coreKeywords = extractCoreKeywords(cleanContent, theme); + + // 根据主题、类型和核心关键词生成标题 + return generateTitleByThemeTypeAndKeywords(cleanContent, theme, contentType, coreKeywords); + } + + /** + * 分析内容主题 + * @param content 正文内容 + * @return 主题关键词 + */ + private String analyzeTheme(String content) { + // 简洁的主题关键词库,只保留通用主题词汇,去掉重复和具体课程名称 + String[] themes = { + "学习", "工作", "会议", "计划", "总结", "任务", "想法", + "灵感", "生活", "旅行", "美食", "健康", "运动", "训练", + "购物", "阅读", "写作", "思考", "讨论", "研究", "开发", "设计", + "项目", "方案", "报告", "文档", "邮件", "电话", "客户", + "产品", "市场", "销售", "运营", "财务", "人事", "管理", + "技术", "代码", "测试", "上线", "维护", "优化", "创新", + "家庭", "朋友", "聚会", "电影", "音乐", "游戏", "动漫", + "书籍", "文章", "新闻", "资讯", "医疗", "养生", + "烹饪", "烘焙", "餐厅", "外卖", "食材", "食谱", + "景点", "酒店", "交通", "攻略", "行程", "机票", + "商品", "价格", "优惠", "促销", "订单", "物流", + "健身", "锻炼", "瑜伽", "跑步", "游泳", "篮球", "足球" + }; + + // 主题相关词汇映射,为每个主题添加丰富的相关词汇 + Map> themeRelatedWords = new HashMap<>(); + + // 学习相关词汇 + themeRelatedWords.put("学习", Arrays.asList("学习", "课程", "考试", "作业", "论文", "复习", "预习", + "毛概", "系统工程", "数据结构", "数学", "英语", "物理", "化学", + "语文", "历史", "地理", "政治", "计算机", "编程", "算法")); + + // 工作相关词汇 + themeRelatedWords.put("工作", Arrays.asList("工作", "会议", "报告", "项目", "计划", "总结", "方案", + "文档", "邮件", "电话", "客户", "产品", "市场", "销售", + "运营", "财务", "人事", "管理", "技术", "代码", "测试", + "上线", "维护", "优化", "创新")); + + // 会议相关词汇 + themeRelatedWords.put("会议", Arrays.asList("会议", "讨论", "议题", "议程", "记录", "结论", "决策", + "参会", "主持", "发言", "汇报", "演示", "PPT", "视频会议")); + + // 计划相关词汇 + themeRelatedWords.put("计划", Arrays.asList("计划", "安排", "日程", "时间", "任务", "目标", "步骤", + "进度", "时间表", "规划", "策划", "方案", "预算", "资源")); + + // 总结相关词汇 + themeRelatedWords.put("总结", Arrays.asList("总结", "回顾", "反思", "收获", "体会", "感悟", "经验", + "教训", "成果", "不足", "改进", "建议", "报告", "汇报")); + + // 任务相关词汇 + themeRelatedWords.put("任务", Arrays.asList("任务", "工作", "项目", "目标", "责任", "分工", "协作", + "完成", "交付", "验收", "评估", "绩效", "效率", "质量")); + + // 生活相关词汇 + themeRelatedWords.put("生活", Arrays.asList("生活", "家庭", "朋友", "聚会", "电影", "音乐", "游戏", + "动漫", "书籍", "文章", "新闻", "资讯", "健康", "医疗", + "养生", "烹饪", "烘焙", "美食", "餐厅", "外卖", "食材")); + + // 旅行相关词汇 + themeRelatedWords.put("旅行", Arrays.asList("旅行", "景点", "酒店", "交通", "攻略", "行程", "机票", + "车票", "住宿", "美食", "购物", "拍照", "打卡", "纪念品")); + + // 美食相关词汇 + themeRelatedWords.put("美食", Arrays.asList("美食", "烹饪", "烘焙", "餐厅", "外卖", "食材", "食谱", + "菜品", "口味", "味道", "营养", "健康", "养生", "减肥")); + + // 健康相关词汇 + themeRelatedWords.put("健康", Arrays.asList("健康", "医疗", "养生", "健身", "锻炼", "饮食", "睡眠", + "休息", "放松", "压力", "心理", "情绪", "疾病", "治疗")); + + // 运动相关词汇 + themeRelatedWords.put("运动", Arrays.asList("运动", "健身", "锻炼", "瑜伽", "跑步", "游泳", "篮球", + "足球", "羽毛球", "乒乓球", "网球", "排球", "健身操", "舞蹈")); + + // 训练相关词汇 + themeRelatedWords.put("训练", Arrays.asList("训练", "上肢", "下肢", "核心", "拉伸", "力量", "耐力", + "有氧", "无氧", "器械", "自由重量", "俯卧撑", "仰卧起坐", "深蹲")); + + // 购物相关词汇 + themeRelatedWords.put("购物", Arrays.asList("购物", "商品", "价格", "优惠", "促销", "订单", "物流", + "快递", "收货", "退货", "换货", "评价", "评分", "客服")); + + // 阅读相关词汇 + themeRelatedWords.put("阅读", Arrays.asList("阅读", "书籍", "文章", "小说", "散文", "诗歌", "传记", + "历史", "哲学", "科学", "技术", "杂志", "报纸", "电子书")); + + // 写作相关词汇 + themeRelatedWords.put("写作", Arrays.asList("写作", "文章", "小说", "散文", "诗歌", "传记", "历史", + "论文", "报告", "文档", "策划", "方案", "总结", "日记")); + + // 统计每个主题相关词汇出现的次数 + int[] counts = new int[themes.length]; + for (int i = 0; i < themes.length; i++) { + String theme = themes[i]; + // 检查主题词本身 + if (content.contains(theme)) { + counts[i] += 3; // 主题词本身权重最高 + } + // 检查主题相关词汇 + List relatedWords = themeRelatedWords.get(theme); + if (relatedWords != null) { + for (String word : relatedWords) { + if (content.contains(word)) { + counts[i]++; + } + } + } + } + + // 找到出现次数最多的主题 + int maxIndex = 0; + for (int i = 1; i < counts.length; i++) { + if (counts[i] > counts[maxIndex]) { + maxIndex = i; + } + } + + // 如果没有找到主题,返回通用主题 + if (counts[maxIndex] == 0) { + return "笔记"; + } + + return themes[maxIndex]; + } + + /** + * 分析内容类型 + * @param content 正文内容 + * @return 内容类型 + */ + private String analyzeContentType(String content) { + // 检查是否包含时间安排相关词汇,添加"上午"关键词 + if (content.contains("上午") || content.contains("早上") || content.contains("下午") || content.contains("晚上") || + content.contains("明天") || content.contains("今天") || content.contains("后天") || + content.contains("周一") || content.contains("周二") || content.contains("周三") || + content.contains("周四") || content.contains("周五") || content.contains("周六") || + content.contains("周日")) { + return "schedule"; + } + + // 检查是否包含任务列表相关词汇 + if (content.contains("需要") || content.contains("要") || content.contains("必须") || + content.contains("应该") || content.contains("完成") || content.contains("做")) { + return "task"; + } + + // 检查是否包含总结相关词汇 + if (content.contains("总结") || content.contains("回顾") || content.contains("反思") || + content.contains("收获") || content.contains("体会") || content.contains("感悟")) { + return "summary"; + } + + // 默认类型 + return "normal"; + } + + /** + * 根据主题和类型生成标题 + * @param content 正文内容 + * @param theme 主题 + * @param contentType 内容类型 + * @return 生成的标题 + */ + private String generateTitleByThemeAndType(String content, String theme, String contentType) { + switch (contentType) { + case "schedule": + return generateScheduleTitle(content, theme); + case "task": + return generateTaskTitle(content, theme); + case "summary": + return generateSummaryTitle(content, theme); + default: + return generateNormalTitle(content, theme); + } + } + + /** + * 生成日程安排类型标题 + * @param content 正文内容 + * @param theme 主题 + * @return 生成的标题 + */ + private String generateScheduleTitle(String content, String theme) { + // 提取时间关键词 + String timeKeywords = extractTimeKeywords(content); + + // 优化标题生成逻辑 + if (!TextUtils.isEmpty(timeKeywords)) { + // 生成更自然的标题格式:时间 + 主题 + 安排 + return timeKeywords + theme + "安排"; + } else { + // 没有时间关键词,使用默认格式 + return theme + "安排"; + } + } + + /** + * 生成任务类型标题 + * @param content 正文内容 + * @param theme 主题 + * @return 生成的标题 + */ + private String generateTaskTitle(String content, String theme) { + // 提取任务数量 + int taskCount = countTasks(content); + // 提取主要任务 + String mainTask = extractMainActivity(content, theme); + + if (taskCount > 0 && !TextUtils.isEmpty(mainTask)) { + return theme + ": " + mainTask + "等" + taskCount + "项任务"; + } else if (taskCount > 0) { + return theme + ": " + taskCount + "项任务"; + } else if (!TextUtils.isEmpty(mainTask)) { + return theme + ": " + mainTask; + } else { + return theme + "任务"; + } + } + + /** + * 生成总结类型标题 + * @param content 正文内容 + * @param theme 主题 + * @return 生成的标题 + */ + private String generateSummaryTitle(String content, String theme) { + // 提取时间关键词 + String timeKeywords = extractTimeKeywords(content); + + if (!TextUtils.isEmpty(timeKeywords)) { + return theme + ": " + timeKeywords + "总结"; + } else { + return theme + "总结"; + } + } + + /** + * 生成普通类型标题 + * @param content 正文内容 + * @param theme 主题 + * @return 生成的标题 + */ + private String generateNormalTitle(String content, String theme) { + // 提取主要活动 + String mainActivity = extractMainActivity(content, theme); + + if (!TextUtils.isEmpty(mainActivity)) { + return theme + ": " + mainActivity; + } else { + return theme + "笔记"; + } + } + + /** + * 提取时间关键词 + * @param content 正文内容 + * @return 时间关键词 + */ + private String extractTimeKeywords(String content) { + // 时间关键词优先级排序 + String[] timeWords = { + "今天", "明天", "后天", "昨天", "上周", "本周", "下周", + "本月", "下月", "今年", "明年", + "周一", "周二", "周三", "周四", "周五", "周六", "周日" + }; + + // 查找第一个时间关键词 + for (String timeWord : timeWords) { + if (content.contains(timeWord)) { + return timeWord; + } + } + + // 检查是否包含早上、下午、晚上等时间词汇 + if (content.contains("早上") || content.contains("下午") || content.contains("晚上")) { + return "今日"; + } + + return ""; + } + + /** + * 提取主要活动 + * @param content 正文内容 + * @param theme 主题 + * @return 主要活动 + */ + private String extractMainActivity(String content, String theme) { + // 去掉主题词 + String contentWithoutTheme = content.replace(theme, "").trim(); + + // 按换行符分割 + String[] lines = contentWithoutTheme.split("\\n"); + + // 查找第一个非空行 + for (String line : lines) { + String trimmedLine = line.trim(); + if (!TextUtils.isEmpty(trimmedLine)) { + // 提取活动关键词 + String activity = extractActivityFromLine(trimmedLine); + if (!TextUtils.isEmpty(activity)) { + return activity; + } + } + } + + return ""; + } + + /** + * 从行中提取活动关键词 + * @param line 行内容 + * @return 活动关键词 + */ + private String extractActivityFromLine(String line) { + // 常见活动动词 + String[] verbs = { + "学习", "工作", "会议", "讨论", "研究", "开发", "设计", + "阅读", "写作", "思考", "运动", "锻炼", "跑步", "游泳", + "吃饭", "睡觉", "休息", "旅行", "购物", "参观", "访问" + }; + + // 查找第一个动词 + for (String verb : verbs) { + if (line.contains(verb)) { + // 提取动词后面的内容,最多10个字符 + int verbIndex = line.indexOf(verb); + String activity = line.substring(verbIndex); + if (activity.length() > 10) { + activity = activity.substring(0, 10); + } + return activity; + } + } + + // 如果没有找到动词,返回行的前10个字符 + if (line.length() > 10) { + return line.substring(0, 10); + } + + return line; + } + + /** + * 统计任务数量 + * @param content 正文内容 + * @return 任务数量 + */ + private int countTasks(String content) { + // 按换行符分割 + String[] lines = content.split("\\n"); + int count = 0; + + // 统计非空行数量 + for (String line : lines) { + if (!TextUtils.isEmpty(line.trim())) { + count++; + } + } + + return count; + } + + /** + * 提取核心关键词 + * @param content 正文内容 + * @param theme 主题 + * @return 核心关键词列表 + */ + private List extractCoreKeywords(String content, String theme) { + List keywords = new ArrayList<>(); + + // 常见关键词库,按领域分类 + Map> keywordLibrary = new HashMap<>(); + keywordLibrary.put("学习", new ArrayList() {{ + add("毛概"); add("系统工程"); add("数据结构"); add("数学"); add("英语"); + add("物理"); add("化学"); add("生物"); add("历史"); add("地理"); + add("政治"); add("语文"); add("计算机"); add("编程"); add("算法"); + }}); + keywordLibrary.put("工作", new ArrayList() {{ + add("会议"); add("报告"); add("项目"); add("计划"); add("总结"); + add("方案"); add("设计"); add("开发"); add("测试"); add("上线"); + }}); + keywordLibrary.put("生活", new ArrayList() {{ + add("吃饭"); add("睡觉"); add("运动"); add("购物"); add("旅行"); + add("电影"); add("音乐"); add("阅读"); add("游戏"); add("社交"); + }}); + + // 获取当前主题相关的关键词库 + List themeKeywords = keywordLibrary.getOrDefault(theme, new ArrayList<>()); + + // 查找内容中包含的主题相关关键词 + for (String keyword : themeKeywords) { + if (content.contains(keyword) && !keywords.contains(keyword)) { + keywords.add(keyword); + } + } + + // 如果没有找到主题相关关键词,提取内容中的高频词 + if (keywords.isEmpty()) { + // 简单的高频词提取逻辑 + String[] words = content.split("\\s+"); + Map wordCount = new HashMap<>(); + + // 统计词频 + for (String word : words) { + if (!TextUtils.isEmpty(word) && word.length() > 1 && !word.equals(theme)) { + wordCount.put(word, wordCount.getOrDefault(word, 0) + 1); + } + } + + // 按词频排序,取前3个 + wordCount.entrySet().stream() + .sorted((a, b) -> b.getValue().compareTo(a.getValue())) + .limit(3) + .forEach(entry -> keywords.add(entry.getKey())); + } + + return keywords; + } + + /** + * 根据主题、类型和核心关键词生成标题 + * @param content 正文内容 + * @param theme 主题 + * @param contentType 内容类型 + * @param coreKeywords 核心关键词 + * @return 生成的标题 + */ + private String generateTitleByThemeTypeAndKeywords(String content, String theme, String contentType, List coreKeywords) { + switch (contentType) { + case "schedule": + return generateEnhancedScheduleTitle(content, theme, coreKeywords); + case "task": + return generateEnhancedTaskTitle(content, theme, coreKeywords); + case "summary": + return generateEnhancedSummaryTitle(content, theme, coreKeywords); + default: + return generateEnhancedNormalTitle(content, theme, coreKeywords); + } + } + + /** + * 生成增强版日程安排标题 + * @param content 正文内容 + * @param theme 主题 + * @param coreKeywords 核心关键词 + * @return 生成的标题 + */ + private String generateEnhancedScheduleTitle(String content, String theme, List coreKeywords) { + String timeKeywords = extractTimeKeywords(content); + + if (!TextUtils.isEmpty(timeKeywords)) { + // 生成简洁的标题:时间 + 主题 + 安排 + return timeKeywords + theme + "安排"; + } else { + // 没有时间关键词,生成:主题 + 安排 + return theme + "安排"; + } + } + + /** + * 生成增强版任务标题 + * @param content 正文内容 + * @param theme 主题 + * @param coreKeywords 核心关键词 + * @return 生成的标题 + */ + private String generateEnhancedTaskTitle(String content, String theme, List coreKeywords) { + // 生成简洁的标题:主题 + 任务 + return theme + "任务"; + } + + /** + * 生成增强版总结标题 + * @param content 正文内容 + * @param theme 主题 + * @param coreKeywords 核心关键词 + * @return 生成的标题 + */ + private String generateEnhancedSummaryTitle(String content, String theme, List coreKeywords) { + String timeKeywords = extractTimeKeywords(content); + + if (!TextUtils.isEmpty(timeKeywords)) { + // 生成简洁的标题:时间 + 主题 + 总结 + return timeKeywords + theme + "总结"; + } else { + // 没有时间关键词,生成:主题 + 总结 + return theme + "总结"; + } + } + + /** + * 生成增强版普通标题 + * @param content 正文内容 + * @param theme 主题 + * @param coreKeywords 核心关键词 + * @return 生成的标题 + */ + private String generateEnhancedNormalTitle(String content, String theme, List coreKeywords) { + // 生成简洁的标题:主题 + 笔记 + return theme + "笔记"; + } + + /** + * 获取内容摘要 + * @param content 正文内容 + * @param maxLength 最大长度 + * @return 内容摘要 + */ + private String getContentSummary(String content, int maxLength) { + if (content.length() <= maxLength) { + return content; + } + + // 尝试在句子边界截断 + for (int i = maxLength; i > 0; i--) { + char c = content.charAt(i); + if (c == '。' || c == '!' || c == '?' || c == '.' || c == '!' || c == '?') { + return content.substring(0, i + 1); + } + } + + // 找不到合适的边界,直接截断 + return content.substring(0, maxLength) + "..."; + } +} \ No newline at end of file diff --git a/src/notes/tool/ImageSpanUtils.java b/src/notes/tool/ImageSpanUtils.java index 9c27cde..be66219 100644 --- a/src/notes/tool/ImageSpanUtils.java +++ b/src/notes/tool/ImageSpanUtils.java @@ -17,9 +17,7 @@ package net.micode.notes.tool; import android.content.Context; -import android.graphics.Bitmap; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.style.ImageSpan; @@ -28,9 +26,6 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import java.util.ArrayList; -import java.util.List; - /** * 图片Span工具类,用于处理图片在Spannable中的序列化和反序列化 */ @@ -55,11 +50,9 @@ public class ImageSpanUtils { int start = spannable.getSpanStart(span); int end = spannable.getSpanEnd(span); - // 获取图片URI(如果有的话) + // 获取图片URI Drawable drawable = span.getDrawable(); if (drawable != null) { - // 这里我们假设图片信息已经在其他地方存储,只需记录位置和路径 - // 在实际实现中,你可能需要将图片保存到特定位置并记录路径 spanObj.put("type", "image"); spanObj.put("start", start); spanObj.put("end", end); diff --git a/src/notes/tool/PrivacyLockManager.java b/src/notes/tool/PrivacyLockManager.java index be9de9d..a71b1a3 100644 --- a/src/notes/tool/PrivacyLockManager.java +++ b/src/notes/tool/PrivacyLockManager.java @@ -9,7 +9,6 @@ import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; diff --git a/src/notes/tool/SmartReminderManager.java b/src/notes/tool/SmartReminderManager.java new file mode 100644 index 0000000..cc07284 --- /dev/null +++ b/src/notes/tool/SmartReminderManager.java @@ -0,0 +1,503 @@ +package net.micode.notes.tool; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 智能提醒管理器,基于规则实现智能提醒功能 + * 包括上下文提醒、智能重复提醒和关联提醒 + */ +public class SmartReminderManager { + private static final String TAG = "SmartReminderManager"; + private Context mContext; + + // 日期时间格式 + private SimpleDateFormat mDateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + private SimpleDateFormat mDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + + // 时间相关的正则表达式 + private Pattern mTimePattern = Pattern.compile( + "(\\d{1,2})[::]\\d{1,2}|" + // 如 3:30 或 14:30 + "(\\d{1,2})[点时]|" + // 如 3点 或 14时 + "上午(\\d{1,2})[点时]|" + // 如 上午3点 或 上午10时 + "下午(\\d{1,2})[点时]|" + // 如 下午3点 或 下午10时 + "明天|今天|后天|" + // 如 明天 + "(\\d{1,2})月(\\d{1,2})[日号]|" + // 如 12月31日 或 1月1号 + "下[周月年]|本[周月年]|上[周月年]"); // 如 下周 或 本月 + + // 重复相关的正则表达式 + private Pattern mRepeatPattern = Pattern.compile( + "每[天周月年]|" + // 如 每天 或 每周 + "周[一二三四五六日]|" + // 如 周一 或 周三 + "每月(\\d{1,2})[日号]|" + // 如 每月15日 + "每年(\\d{1,2})月(\\d{1,2})[日号]"); // 如 每年12月31日 + + // 关联关键词 + private List mRelatedKeywords = new ArrayList<>(); + + public SmartReminderManager(Context context) { + mContext = context; + + // 初始化关联关键词 + mRelatedKeywords.add("会议"); + mRelatedKeywords.add("面试"); + mRelatedKeywords.add("任务"); + mRelatedKeywords.add("计划"); + mRelatedKeywords.add("安排"); + mRelatedKeywords.add("项目"); + mRelatedKeywords.add("工作"); + mRelatedKeywords.add("进度"); + mRelatedKeywords.add("报告"); + mRelatedKeywords.add("讨论"); + mRelatedKeywords.add("准备"); + mRelatedKeywords.add("总结"); + mRelatedKeywords.add("方案"); + mRelatedKeywords.add("设计"); + mRelatedKeywords.add("开发"); + mRelatedKeywords.add("测试"); + mRelatedKeywords.add("上线"); + mRelatedKeywords.add(" deadline"); + mRelatedKeywords.add("截止"); + } + + /** + * 根据便签内容智能建议提醒时间 + * @param content 便签内容 + * @return 建议的提醒时间戳,0表示没有建议 + */ + public long suggestReminderTime(String content) { + if (TextUtils.isEmpty(content)) { + return 0; + } + + // 先查找日期关键词(明天、后天、x月x日等) + Matcher dateMatcher = Pattern.compile("明天|今天|后天|(\\d{1,2})月(\\d{1,2})[日号]|下[周月年]|本[周月年]|上[周月年]").matcher(content); + String dateStr = null; + if (dateMatcher.find()) { + dateStr = dateMatcher.group(); + } + + // 再查找时间关键词(3:30、3点、上午3点等) + Matcher timeMatcher = Pattern.compile("(\\d{1,2})[::]\\d{1,2}|(\\d{1,2})[点时]|上午(\\d{1,2})[点时]|下午(\\d{1,2})[点时]").matcher(content); + String timeStr = null; + if (timeMatcher.find()) { + timeStr = timeMatcher.group(); + } + + // 结合日期和时间进行解析,生成更准确的提醒时间 + if (dateStr != null && timeStr != null) { + // 先解析日期,获取基准日期 + long baseTime = parseTimeString(dateStr); + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(baseTime); + + // 再解析时间,设置具体时分 + if (timeStr.contains(":") || timeStr.contains(":")) { + // 处理时间格式,如 3:30 或 14:30 + String[] timeParts = timeStr.replace(":", ":").split(":"); + int hour = Integer.parseInt(timeParts[0]); + int minute = Integer.parseInt(timeParts[1]); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + } else if (timeStr.matches("\\d{1,2}[点时]")) { + // 处理时间格式,如 3点 或 14时 + int hour = Integer.parseInt(timeStr.replaceAll("[点时]", "")); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, 0); + } else if (timeStr.matches("上午\\d{1,2}[点时]")) { + // 处理上午时间,如 上午3点 或 上午10时 + int hour = Integer.parseInt(timeStr.replaceAll("[上午点时]", "")); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, 0); + } else if (timeStr.matches("下午\\d{1,2}[点时]")) { + // 处理下午时间,如 下午3点 或 下午10时 + int hour = Integer.parseInt(timeStr.replaceAll("[下午点时]", "")); + // 转换为24小时制 + hour += 12; + // 特殊处理下午12点 + if (hour == 24) { + hour = 12; + } + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, 0); + } + + calendar.set(Calendar.SECOND, 0); + return calendar.getTimeInMillis(); + } + // 如果只有日期或只有时间,使用原有逻辑 + else { + Matcher matcher = mTimePattern.matcher(content); + if (matcher.find()) { + String singleTimeStr = matcher.group(); + return parseTimeString(singleTimeStr); + } + } + + return 0; + } + + /** + * 根据便签内容智能建议重复频率 + * @param content 便签内容 + * @return 建议的重复类型,如 "daily", "weekly", "monthly", "yearly",空字符串表示不重复 + */ + public String suggestRepeatType(String content) { + if (TextUtils.isEmpty(content)) { + return ""; + } + + // 查找重复相关关键词 + Matcher repeatMatcher = mRepeatPattern.matcher(content); + if (repeatMatcher.find()) { + String repeatStr = repeatMatcher.group(); + return parseRepeatString(repeatStr); + } + + return ""; + } + + /** + * 判断两个便签是否相关,用于关联提醒 + * @param content1 第一个便签内容 + * @param content2 第二个便签内容 + * @return 如果相关返回true,否则返回false + */ + public boolean areNotesRelated(String content1, String content2) { + if (TextUtils.isEmpty(content1) || TextUtils.isEmpty(content2)) { + return false; + } + + // 转换为小写,不区分大小写 + String lowerContent1 = content1.toLowerCase(); + String lowerContent2 = content2.toLowerCase(); + + // 1. 检查是否包含相同的关联关键词 + for (String keyword : mRelatedKeywords) { + if (lowerContent1.contains(keyword) && lowerContent2.contains(keyword)) { + return true; + } + } + + // 2. 检查是否包含相似的项目名称或主题 + // 查找可能的项目名称(如"项目A"、"任务1"等) + Pattern projectPattern = Pattern.compile("[项目任务计划][A-Za-z0-9_]+|[A-Za-z0-9_]+[项目任务计划]"); + Matcher projectMatcher1 = projectPattern.matcher(lowerContent1); + Matcher projectMatcher2 = projectPattern.matcher(lowerContent2); + + List projects1 = new ArrayList<>(); + while (projectMatcher1.find()) { + projects1.add(projectMatcher1.group()); + } + + while (projectMatcher2.find()) { + if (projects1.contains(projectMatcher2.group())) { + return true; + } + } + + // 3. 检查是否包含相同或相似的时间信息 + // 先提取所有时间相关的关键词 + Matcher timeMatcher1 = mTimePattern.matcher(content1); + Matcher timeMatcher2 = mTimePattern.matcher(content2); + + List timeStrs1 = new ArrayList<>(); + while (timeMatcher1.find()) { + timeStrs1.add(timeMatcher1.group()); + } + + List timeStrs2 = new ArrayList<>(); + while (timeMatcher2.find()) { + timeStrs2.add(timeMatcher2.group()); + } + + // 检查是否有完全相同的时间关键词 + for (String timeStr : timeStrs1) { + if (timeStrs2.contains(timeStr)) { + return true; + } + } + + // 检查是否有相关的时间关键词(如"明天"和"明天3点") + for (String time1 : timeStrs1) { + for (String time2 : timeStrs2) { + if (areTimesRelated(time1, time2)) { + return true; + } + } + } + + // 4. 检查内容相似度(如果内容较短,相似度要求较高) + int minLength = Math.min(content1.length(), content2.length()); + if (minLength > 5) { // 只有当内容足够长时才检查相似度 + int commonWords = countCommonWords(content1, content2); + double similarity = (double) commonWords / Math.sqrt(content1.length() * content2.length()); + if (similarity > 0.3) { // 相似度阈值 + return true; + } + } + + return false; + } + + /** + * 检查两个时间字符串是否相关 + * @param time1 第一个时间字符串 + * @param time2 第二个时间字符串 + * @return 如果相关返回true,否则返回false + */ + private boolean areTimesRelated(String time1, String time2) { + // 检查是否有包含关系(如"明天"包含在"明天3点"中) + if (time1.contains(time2) || time2.contains(time1)) { + return true; + } + + // 检查是否都是日期或都是时间 + boolean isDate1 = time1.matches(".*[明天后天今天月日号周].*"); + boolean isDate2 = time2.matches(".*[明天后天今天月日号周].*"); + boolean isTime1 = time1.matches(".*[0-9::点时上午下午].*"); + boolean isTime2 = time2.matches(".*[0-9::点时上午下午].*"); + + // 如果一个是日期,一个是时间,认为它们可能相关 + if ((isDate1 && isTime2) || (isDate2 && isTime1)) { + return true; + } + + return false; + } + + /** + * 计算两个字符串中相同的词语数量 + * @param str1 第一个字符串 + * @param str2 第二个字符串 + * @return 相同词语的数量 + */ + private int countCommonWords(String str1, String str2) { + // 简单的词语分割,按空格分割 + String[] words1 = str1.split("\\s+"); + String[] words2 = str2.split("\\s+"); + + Set wordSet1 = new HashSet<>(); + for (String word : words1) { + // 移除常见标点符号 + String cleanedWord = word.replaceAll("[\\p{Punct}]", "").toLowerCase(); + if (cleanedWord.length() > 1) { // 只考虑长度大于1的词语 + wordSet1.add(cleanedWord); + } + } + + int commonCount = 0; + for (String word : words2) { + // 移除常见标点符号 + String cleanedWord = word.replaceAll("[\\p{Punct}]", "").toLowerCase(); + if (cleanedWord.length() > 1 && wordSet1.contains(cleanedWord)) { + commonCount++; + } + } + + return commonCount; + } + + /** + * 解析时间字符串,返回时间戳 + * @param timeStr 时间字符串 + * @return 时间戳 + */ + private long parseTimeString(String timeStr) { + Calendar calendar = Calendar.getInstance(); + + try { + if (timeStr.contains(":") || timeStr.contains(":")) { + // 处理时间格式,如 3:30 或 14:30 + String[] timeParts = timeStr.replace(":", ":").split(":"); + int hour = Integer.parseInt(timeParts[0]); + int minute = Integer.parseInt(timeParts[1]); + + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, 0); + + // 如果时间已过,设置为明天 + if (calendar.getTimeInMillis() < System.currentTimeMillis()) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + } else if (timeStr.matches("\\d{1,2}[点时]")) { + // 处理时间格式,如 3点 或 14时 + int hour = Integer.parseInt(timeStr.replaceAll("[点时]", "")); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + + // 如果时间已过,设置为明天 + if (calendar.getTimeInMillis() < System.currentTimeMillis()) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + } else if (timeStr.matches("上午\\d{1,2}[点时]")) { + // 处理上午时间,如 上午3点 或 上午10时 + int hour = Integer.parseInt(timeStr.replaceAll("[上午点时]", "")); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + + // 如果时间已过,设置为明天 + if (calendar.getTimeInMillis() < System.currentTimeMillis()) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + } else if (timeStr.matches("下午\\d{1,2}[点时]")) { + // 处理下午时间,如 下午3点 或 下午10时 + int hour = Integer.parseInt(timeStr.replaceAll("[下午点时]", "")); + // 转换为24小时制 + hour += 12; + // 特殊处理下午12点 + if (hour == 24) { + hour = 12; + } + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + + // 如果时间已过,设置为明天 + if (calendar.getTimeInMillis() < System.currentTimeMillis()) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + } else if (timeStr.contains("明天")) { + // 明天 + calendar.add(Calendar.DAY_OF_MONTH, 1); + } else if (timeStr.contains("后天")) { + // 后天 + calendar.add(Calendar.DAY_OF_MONTH, 2); + } else if (timeStr.contains("今天")) { + // 今天(保持不变) + } else if (timeStr.contains("周")) { + // 处理星期,如 下周 或 本周 + int dayOfWeek = getDayOfWeek(timeStr); + if (dayOfWeek != -1) { + int currentDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); + int daysToAdd = dayOfWeek - currentDayOfWeek; + if (daysToAdd <= 0) { + daysToAdd += 7; + } + calendar.add(Calendar.DAY_OF_MONTH, daysToAdd); + } + } else if (timeStr.contains("月")) { + // 处理日期,如 12月31日 + String[] dateParts = timeStr.replace("月", "-").replace("日", "").replace("号", "").split("-"); + int month = Integer.parseInt(dateParts[0]) - 1; // Calendar月份从0开始 + int day = Integer.parseInt(dateParts[1]); + + calendar.set(Calendar.MONTH, month); + calendar.set(Calendar.DAY_OF_MONTH, day); + + // 如果日期已过,设置为明年 + if (calendar.getTimeInMillis() < System.currentTimeMillis()) { + calendar.add(Calendar.YEAR, 1); + } + } + + return calendar.getTimeInMillis(); + } catch (Exception e) { + Log.e(TAG, "Failed to parse time string: " + timeStr, e); + return 0; + } + } + + /** + * 解析重复字符串,返回重复类型 + * @param repeatStr 重复字符串 + * @return 重复类型描述 + */ + private String parseRepeatString(String repeatStr) { + if (repeatStr.contains("每天")) { + return "每天"; + } else if (repeatStr.contains("每周")) { + return "每周"; + } else if (repeatStr.contains("每月")) { + return "每月"; + } else if (repeatStr.contains("每年")) { + return "每年"; + } else if (repeatStr.contains("周一")) { + return "每周一"; + } else if (repeatStr.contains("周二")) { + return "每周二"; + } else if (repeatStr.contains("周三")) { + return "每周三"; + } else if (repeatStr.contains("周四")) { + return "每周四"; + } else if (repeatStr.contains("周五")) { + return "每周五"; + } else if (repeatStr.contains("周六")) { + return "每周六"; + } else if (repeatStr.contains("周日")) { + return "每周日"; + } else if (repeatStr.contains("周")) { + return "每周"; + } + return ""; + } + + /** + * 获取星期对应的Calendar.DAY_OF_WEEK值 + * @param dayStr 星期字符串 + * @return Calendar.DAY_OF_WEEK值,-1表示解析失败 + */ + private int getDayOfWeek(String dayStr) { + if (dayStr.contains("周一") || dayStr.contains("星期一")) { + return Calendar.MONDAY; + } else if (dayStr.contains("周二") || dayStr.contains("星期二")) { + return Calendar.TUESDAY; + } else if (dayStr.contains("周三") || dayStr.contains("星期三")) { + return Calendar.WEDNESDAY; + } else if (dayStr.contains("周四") || dayStr.contains("星期四")) { + return Calendar.THURSDAY; + } else if (dayStr.contains("周五") || dayStr.contains("星期五")) { + return Calendar.FRIDAY; + } else if (dayStr.contains("周六") || dayStr.contains("星期六")) { + return Calendar.SATURDAY; + } else if (dayStr.contains("周日") || dayStr.contains("星期日")) { + return Calendar.SUNDAY; + } + return -1; + } + + /** + * 根据重复类型计算下一次提醒时间 + * @param currentTime 当前提醒时间 + * @param repeatType 重复类型 + * @return 下一次提醒时间戳 + */ + public long getNextRepeatTime(long currentTime, String repeatType) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(currentTime); + + switch (repeatType) { + case "daily": + calendar.add(Calendar.DAY_OF_MONTH, 1); + break; + case "weekly": + calendar.add(Calendar.WEEK_OF_MONTH, 1); + break; + case "monthly": + calendar.add(Calendar.MONTH, 1); + break; + case "yearly": + calendar.add(Calendar.YEAR, 1); + break; + default: + return 0; + } + + return calendar.getTimeInMillis(); + } +} \ No newline at end of file diff --git a/src/notes/tool/UserManager.java b/src/notes/tool/UserManager.java index be6f5c3..7154346 100644 --- a/src/notes/tool/UserManager.java +++ b/src/notes/tool/UserManager.java @@ -11,33 +11,17 @@ public class UserManager { private static final String PREF_NAME = "user_prefs"; private static final String KEY_IS_LOGGED_IN = "is_logged_in"; private static final String KEY_CURRENT_USER = "current_user"; - private static final String KEY_LOGIN_TIMESTAMP = "login_timestamp"; - private static volatile UserManager instance; // 使用volatile关键字确保多线程环境下的可见性 + private static UserManager instance; private SharedPreferences sharedPreferences; - private Context applicationContext; private UserManager(Context context) { - if (context != null) { - this.applicationContext = context.getApplicationContext(); - if (this.applicationContext != null) { - this.sharedPreferences = this.applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - } - } + sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); } - public static UserManager getInstance(Context context) { - if (context == null) { - throw new IllegalArgumentException("Context cannot be null"); - } - - // 双重检查锁定模式,确保线程安全 + public static synchronized UserManager getInstance(Context context) { if (instance == null) { - synchronized (UserManager.class) { - if (instance == null) { - instance = new UserManager(context.getApplicationContext()); - } - } + instance = new UserManager(context.getApplicationContext()); } return instance; } @@ -49,7 +33,6 @@ public class UserManager { sharedPreferences.edit() .putBoolean(KEY_IS_LOGGED_IN, true) .putString(KEY_CURRENT_USER, username) - .putLong(KEY_LOGIN_TIMESTAMP, System.currentTimeMillis()) // 保存当前登录时间 .apply(); } @@ -67,22 +50,6 @@ public class UserManager { return sharedPreferences.getString(KEY_CURRENT_USER, ""); } - /** - * 检查登录状态是否仍然有效(3天内) - */ - public boolean isLoginValid() { - if (!isLoggedIn()) { - return false; - } - - long loginTime = sharedPreferences.getLong(KEY_LOGIN_TIMESTAMP, 0); - long currentTime = System.currentTimeMillis(); - // 3天 = 3 * 24 * 60 * 60 * 1000 毫秒 - long threeDaysInMillis = 3L * 24 * 60 * 60 * 1000; - - return (currentTime - loginTime) < threeDaysInMillis; - } - /** * 退出登录,清除登录状态 */ @@ -90,7 +57,6 @@ public class UserManager { sharedPreferences.edit() .putBoolean(KEY_IS_LOGGED_IN, false) .remove(KEY_CURRENT_USER) - .remove(KEY_LOGIN_TIMESTAMP) .apply(); } } \ No newline at end of file diff --git a/src/notes/ui/AlarmInitReceiver.java b/src/notes/ui/AlarmInitReceiver.java index 6020082..ee8b486 100644 --- a/src/notes/ui/AlarmInitReceiver.java +++ b/src/notes/ui/AlarmInitReceiver.java @@ -73,7 +73,12 @@ public class AlarmInitReceiver extends BroadcastReceiver { sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId)); // 创建待定意图,用于闹钟管理器 - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + // 在 Android 12+ 上必须指定 FLAG_IMMUTABLE 或 FLAG_MUTABLE + int flags = 0; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + flags = PendingIntent.FLAG_IMMUTABLE; + } + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, flags); // 获取闹钟管理器服务 AlarmManager alarmManager = (AlarmManager) context diff --git a/src/notes/ui/LoginRegisterActivity.java b/src/notes/ui/LoginRegisterActivity.java index d797bd8..72801e0 100644 --- a/src/notes/ui/LoginRegisterActivity.java +++ b/src/notes/ui/LoginRegisterActivity.java @@ -3,13 +3,17 @@ package net.micode.notes.ui; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; +import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.widget.Button; +import android.widget.CheckBox; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; @@ -22,18 +26,16 @@ import net.micode.notes.tool.UserManager; public class LoginRegisterActivity extends AppCompatActivity { // UI组件 - private ImageView ivLogo; private TextView tvLoginTab; private TextView tvRegisterTab; private View tabIndicator; private EditText etUsername; private EditText etPassword; - private EditText etConfirmPassword; private ImageView ivPasswordVisibility; + private CheckBox cbRememberPassword; private TextView tvErrorMessage; private Button btnAction; private TextView tvForgotPassword; - private LinearLayout confirmPasswordLayout; // 数据库帮助类 private UserDatabaseHelper dbHelper; @@ -46,25 +48,16 @@ public class LoginRegisterActivity extends AppCompatActivity { // 密码可见性状态 private boolean isPasswordVisible = false; - - private static final int ROOT_AUTH_REQUEST_CODE = 1001; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login_register); - //通过getInstance获取已初始化的UserManager实例 - UserManager userManager = UserManager.getInstance(this); - - //安全调用:先判空,再调用方法,避免null - // 检查是否已登录且在有效期内,如果已登录且在有效期内则直接跳转到主页面 - if (userManager != null && userManager.isLoginValid()) { - startActivity(new Intent(this, NotesListActivity.class)); - finish(); - return; - } + // 初始化用户管理器 + userManager = UserManager.getInstance(this); + // 每次都显示登录/注册界面,不自动登录 initViews(); initListeners(); updateUIForLoginMode(); @@ -74,24 +67,22 @@ public class LoginRegisterActivity extends AppCompatActivity { * 初始化视图组件 */ private void initViews() { - ivLogo = findViewById(R.id.iv_logo); tvLoginTab = findViewById(R.id.tv_login_tab); tvRegisterTab = findViewById(R.id.tv_register_tab); tabIndicator = findViewById(R.id.tab_indicator); etUsername = findViewById(R.id.et_username); etPassword = findViewById(R.id.et_password); - etConfirmPassword = findViewById(R.id.et_confirm_password); ivPasswordVisibility = findViewById(R.id.iv_password_visibility); + cbRememberPassword = findViewById(R.id.cb_remember_password); tvErrorMessage = findViewById(R.id.tv_error_message); btnAction = findViewById(R.id.btn_action); tvForgotPassword = findViewById(R.id.tv_forgot_password); - confirmPasswordLayout = findViewById(R.id.confirm_password_layout); // 初始化数据库帮助类 dbHelper = UserDatabaseHelper.getInstance(this); - // 初始化用户管理器 - userManager = UserManager.getInstance(this); + // 加载记住的密码 + loadRememberedPassword(); } /** @@ -131,8 +122,7 @@ public class LoginRegisterActivity extends AppCompatActivity { if (isLoginMode) { performLogin(); } else { - // 注册前需要验证根用户权限 - showRootAuthDialog(); + performRegister(); } } }); @@ -146,15 +136,6 @@ public class LoginRegisterActivity extends AppCompatActivity { }); } - /** - * 显示根用户权限验证对话框 - */ - private void showRootAuthDialog() { - Intent intent = new Intent(this, SplashActivity.class); - intent.putExtra("SHOW_ROOT_AUTH_DIALOG", true); - startActivityForResult(intent, ROOT_AUTH_REQUEST_CODE); - } - /** * 更新UI以适应当前模式(登录或注册) */ @@ -165,26 +146,24 @@ public class LoginRegisterActivity extends AppCompatActivity { tvRegisterTab.setTextColor(getResources().getColor(android.R.color.darker_gray)); // 更新指示器位置 - LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) tabIndicator.getLayoutParams(); - params.leftMargin = 40; // 左侧位置 (登录tab的位置) + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) tabIndicator.getLayoutParams(); + params.leftMargin = 40; // 左侧位置 tabIndicator.setLayoutParams(params); btnAction.setText("登录"); tvForgotPassword.setVisibility(View.VISIBLE); - confirmPasswordLayout.setVisibility(View.GONE); // 隐藏确认密码输入框 } else { // 注册模式 tvRegisterTab.setTextColor(getResources().getColor(android.R.color.holo_orange_dark)); tvLoginTab.setTextColor(getResources().getColor(android.R.color.darker_gray)); // 更新指示器位置 - LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) tabIndicator.getLayoutParams(); - params.leftMargin = 120; // 右侧位置 (注册tab的位置) + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) tabIndicator.getLayoutParams(); + params.leftMargin = 120; // 右侧位置 tabIndicator.setLayoutParams(params); btnAction.setText("注册"); tvForgotPassword.setVisibility(View.GONE); - confirmPasswordLayout.setVisibility(View.VISIBLE); // 显示确认密码输入框 } // 清除错误消息 @@ -198,10 +177,10 @@ public class LoginRegisterActivity extends AppCompatActivity { isPasswordVisible = !isPasswordVisible; if (isPasswordVisible) { etPassword.setInputType(android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); - ivPasswordVisibility.setImageResource(R.drawable.ic_eye_open); + ivPasswordVisibility.setImageResource(android.R.drawable.ic_secure); } else { etPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); - ivPasswordVisibility.setImageResource(R.drawable.ic_eye_closed); + ivPasswordVisibility.setImageResource(android.R.drawable.ic_lock_lock); } // 将光标移到末尾 @@ -231,6 +210,13 @@ public class LoginRegisterActivity extends AppCompatActivity { // 登录成功,保存登录状态 userManager.saveLoginStatus(username); + // 保存记住的密码 + if (cbRememberPassword.isChecked()) { + saveRememberedPassword(username, password); + } else { + clearRememberedPassword(); + } + // 跳转到主页面 Intent intent = new Intent(this, NotesListActivity.class); startActivity(intent); @@ -241,6 +227,45 @@ public class LoginRegisterActivity extends AppCompatActivity { showError("用户名或密码错误,请重新输入"); } } + + /** + * 保存记住的密码 + */ + private void saveRememberedPassword(String username, String password) { + SharedPreferences sharedPreferences = getSharedPreferences("login_prefs", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean("remember_password", true); + editor.putString("username", username); + editor.putString("password", password); + editor.apply(); + } + + /** + * 加载记住的密码 + */ + private void loadRememberedPassword() { + SharedPreferences sharedPreferences = getSharedPreferences("login_prefs", Context.MODE_PRIVATE); + boolean rememberPassword = sharedPreferences.getBoolean("remember_password", false); + if (rememberPassword) { + String username = sharedPreferences.getString("username", ""); + String password = sharedPreferences.getString("password", ""); + etUsername.setText(username); + etPassword.setText(password); + cbRememberPassword.setChecked(true); + } + } + + /** + * 清除记住的密码 + */ + private void clearRememberedPassword() { + SharedPreferences sharedPreferences = getSharedPreferences("login_prefs", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean("remember_password", false); + editor.remove("username"); + editor.remove("password"); + editor.apply(); + } /** * 执行注册操作 @@ -248,7 +273,6 @@ public class LoginRegisterActivity extends AppCompatActivity { private void performRegister() { String username = etUsername.getText().toString().trim(); String password = etPassword.getText().toString().trim(); - String confirmPassword = etConfirmPassword.getText().toString().trim(); // 验证输入 if (TextUtils.isEmpty(username)) { @@ -270,16 +294,6 @@ public class LoginRegisterActivity extends AppCompatActivity { showError("密码长度不能少于6位"); return; } - - if (TextUtils.isEmpty(confirmPassword)) { - showError("请确认密码"); - return; - } - - if (!password.equals(confirmPassword)) { - showError("两次输入的密码不一致"); - return; - } // 检查用户名是否已存在 if (dbHelper.isUsernameExists(username)) { @@ -292,10 +306,9 @@ public class LoginRegisterActivity extends AppCompatActivity { if (success) { Toast.makeText(this, "注册成功", Toast.LENGTH_SHORT).show(); - // 自动切换到登录模式并清空输入框 + // 自动切换到登录模式 isLoginMode = true; updateUIForLoginMode(); - clearInputFields(); // 清空输入框 } else { showError("注册失败,请重试"); } @@ -308,16 +321,6 @@ public class LoginRegisterActivity extends AppCompatActivity { tvErrorMessage.setText(message); tvErrorMessage.setVisibility(View.VISIBLE); } - - /** - * 清空输入框 - */ - private void clearInputFields() { - etUsername.setText(""); - etPassword.setText(""); - etConfirmPassword.setText(""); - tvErrorMessage.setVisibility(View.GONE); - } /** * 显示忘记密码对话框 @@ -334,24 +337,4 @@ public class LoginRegisterActivity extends AppCompatActivity { }) .show(); } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (requestCode == ROOT_AUTH_REQUEST_CODE) { - if (resultCode == RESULT_OK && data != null) { - boolean authResult = data.getBooleanExtra("ROOT_AUTH_RESULT", false); - if (authResult) { - // 根用户验证成功,执行注册 - performRegister(); - } else { - Toast.makeText(this, "根用户验证失败,无法注册新用户", Toast.LENGTH_SHORT).show(); - } - } else if (resultCode == RESULT_CANCELED) { - // 用户取消了验证 - Toast.makeText(this, "已取消根用户验证", Toast.LENGTH_SHORT).show(); - } - } - } -} \ No newline at end of file +} diff --git a/src/notes/ui/NoteEditActivity.java b/src/notes/ui/NoteEditActivity.java index d3e4097..47cd7c7 100644 --- a/src/notes/ui/NoteEditActivity.java +++ b/src/notes/ui/NoteEditActivity.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package net.micode.notes.ui; import android.app.Activity; @@ -35,9 +36,6 @@ import android.text.InputType; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.style.BackgroundColorSpan; -import android.text.style.StyleSpan; -import android.text.style.UnderlineSpan; -import android.text.style.StrikethroughSpan; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -46,17 +44,20 @@ import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.WindowManager; -import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.EditText; import android.net.Uri; -import android.widget.ImageButton; +import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import android.widget.ArrayAdapter; +import android.database.Cursor; +import java.util.ArrayList; +import java.util.List; import net.micode.notes.R; import net.micode.notes.data.Notes; @@ -64,19 +65,17 @@ import net.micode.notes.data.Notes.TextNote; import net.micode.notes.model.WorkingNote; import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; import net.micode.notes.tool.DataUtils; -import net.micode.notes.tool.PrivacyLockManager; import net.micode.notes.tool.ResourceParser; import net.micode.notes.tool.ResourceParser.TextAppearanceResources; +import net.micode.notes.tool.SmartReminderManager; +import net.micode.notes.tool.AIService; import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; -import net.micode.notes.tool.RichTextFormatUtils; import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener; import net.micode.notes.widget.NoteWidgetProvider_2x; import net.micode.notes.widget.NoteWidgetProvider_4x; import java.util.HashMap; import java.util.HashSet; -import java.util.List; -import java.util.ArrayList; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -96,6 +95,8 @@ public class NoteEditActivity extends Activity implements OnClickListener, public ImageView ivAlertIcon; // 提醒图标 public TextView tvAlertDate; // 提醒日期文本 public ImageView ibSetBgColor; // 设置背景颜色的按钮 + public ImageView ibSetTag; // 设置标签的按钮 + public TextView tvTag; // 显示标签的文本视图 } // 背景颜色选择按钮与颜色ID的映射 @@ -136,18 +137,44 @@ public class NoteEditActivity extends Activity implements OnClickListener, sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select); } + // 标签常量 + public static final String TAG_LIFE = "life"; + public static final String TAG_STUDY = "study"; + public static final String TAG_WORK = "work"; + public static final String TAG_CUSTOM = "custom"; + + // 标签选择按钮与标签值的映射 + private static final Map sTagSelectorBtnsMap = new HashMap(); + static { + sTagSelectorBtnsMap.put(R.id.iv_tag_life, TAG_LIFE); + sTagSelectorBtnsMap.put(R.id.iv_tag_study, TAG_STUDY); + sTagSelectorBtnsMap.put(R.id.iv_tag_work, TAG_WORK); + sTagSelectorBtnsMap.put(R.id.iv_tag_custom, TAG_CUSTOM); + } + + // 标签值与选中状态视图的映射 + private static final Map sTagSelectorSelectionMap = new HashMap(); + static { + sTagSelectorSelectionMap.put(TAG_LIFE, R.id.iv_tag_life_select); + sTagSelectorSelectionMap.put(TAG_STUDY, R.id.iv_tag_study_select); + sTagSelectorSelectionMap.put(TAG_WORK, R.id.iv_tag_work_select); + sTagSelectorSelectionMap.put(TAG_CUSTOM, R.id.iv_tag_custom_select); + } + private static final String TAG = "NoteEditActivity"; private HeadViewHolder mNoteHeaderHolder; // 头部视图持有者实例 private View mHeadViewPanel; // 头部面板视图 private View mNoteBgColorSelector; // 背景颜色选择器面板 + private View mNoteTagSelector; // 标签选择器面板 private View mFontSizeSelector; // 字体大小选择器面板 + private EditText mNoteTitle; // 笔记标题编辑器 private EditText mNoteEditor; // 笔记内容编辑器(普通模式) private View mNoteEditorPanel; // 编辑器面板 private WorkingNote mWorkingNote; // 当前正在编辑的笔记数据模型 private SharedPreferences mSharedPrefs; // 偏好设置 private int mFontSizeId; // 当前字体大小ID - + // 位置提醒对话框相关控件 private EditText mLocationNameEditText; // 位置名称输入框 private EditText mLatitudeEditText; // 纬度输入框 @@ -155,36 +182,31 @@ public class NoteEditActivity extends Activity implements OnClickListener, private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; // 字体大小偏好设置键 private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; // 桌面快捷方式标题最大长度 - private static final int REQUEST_CODE_SELECT_LOCATION = 1001; // 选择位置请求码 - // 图片选择请求码 - private static final int REQUEST_CODE_PICK_IMAGE = 1002; // 从相册选择图片 - private static final int REQUEST_CODE_CAPTURE_IMAGE = 1003; // 拍照 - 保留以避免编译错误,但不使用 - private static final int REQUEST_CODE_CHOOSE_IMAGE = 1004; // 选择系统图片 - // 清单模式下的标记符号 - static final String TAG_CHECKED = String.valueOf('\u221A'); // 对勾符号 √ - static final String TAG_UNCHECKED = String.valueOf('\u25A1'); // 方框符号 □ - - // 隐私锁相关常量 - private static final int MAX_UNLOCK_ATTEMPTS = 3; // 最大解锁尝试次数 - - private PrivacyLockManager mPrivacyLockManager; // 隐私锁管理器 - private int mUnlockAttempts = 0; // 当前解锁尝试次数 + public static final String TAG_CHECKED = String.valueOf('\u221A'); // 对勾符号 √ + public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); // 方框符号 □ private LinearLayout mEditTextList; // 清单模式下的编辑框列表容器 private String mUserQuery; // 用户搜索查询词(从搜索跳转过来时使用) private Pattern mPattern; // 用于高亮搜索词的正则表达式模式 - + private SmartReminderManager mSmartReminderManager; // 智能提醒管理器实例 + private AIService mAIService; // AI服务实例,用于智能生成标题 + + // 图片选择相关常量 + private static final int REQUEST_CODE_PICK_IMAGE = 100; // 相册选择请求码 + private static final int REQUEST_CODE_CHOOSE_IMAGE = 101; // 系统图片选择请求码 + // 富文本编辑相关控件 private LinearLayout mFormatToolbar; // 格式工具栏 - private ImageButton mBtnBold; // 粗体按钮 - private ImageButton mBtnItalic; // 斜体按钮 - private ImageButton mBtnUnderline; // 下划线按钮 - private ImageButton mBtnStrikethrough; // 删除线按钮 - private ImageButton mBtnAddImage; // 添加图片按钮 - + private Button mBtnBold; // 粗体按钮 + private Button mBtnItalic; // 斜体按钮 + private Button mBtnUnderline; // 下划线按钮 + private Button mBtnStrikethrough; // 删除线按钮 + + // 图片选择相关UI组件 + private ImageView mBtnAddImage; // 添加图片按钮 /** * Activity创建时的初始化方法 @@ -200,14 +222,22 @@ public class NoteEditActivity extends Activity implements OnClickListener, return; } initResources(); // 初始化视图资源 - mPrivacyLockManager = new PrivacyLockManager(this); // 初始化隐私锁管理器 - - // 检查是否需要解锁 - if (mWorkingNote != null && mPrivacyLockManager.isNoteLocked(mWorkingNote.getNoteId())) { - showUnlockDialog(); - return; // 暂停初始化,等待解锁 - } - + + // 初始化智能提醒管理器 + mSmartReminderManager = new SmartReminderManager(this); + + // 初始化AI服务 + mAIService = new AIService(this); + mAIService.setOnAITitleGeneratedListener(new AIService.OnAITitleGeneratedListener() { + @Override + public void onTitleGenerated(String title) { + // 只有当标题为空时,才自动填充AI生成的标题 + if (TextUtils.isEmpty(mNoteTitle.getText())) { + mNoteTitle.setText(title); + } + } + }); + // 添加对OnBackPressedDispatcher的支持,以便处理手势返回 try { // 使用反射调用getOnBackPressedDispatcher方法,添加一个空回调 @@ -217,16 +247,16 @@ public class NoteEditActivity extends Activity implements OnClickListener, Object callback = callbackClass.getConstructor(boolean.class).newInstance(true); Class callbackInterface = Class.forName("androidx.activity.OnBackPressedCallback$OnBackPressedListener"); Object listener = java.lang.reflect.Proxy.newProxyInstance( - callbackInterface.getClassLoader(), - new Class[]{callbackInterface}, - (proxy, method, args) -> { - // 调用原有的onBackPressed方法 - onBackPressed(); - return null; - } + callbackInterface.getClassLoader(), + new Class[]{callbackInterface}, + (proxy, method, args) -> { + // 调用原有的onBackPressed方法 + onBackPressed(); + return null; + } ); callbackClass.getMethod("addOnBackPressedListener", callbackInterface).invoke(callback, listener); - dispatcher.getClass().getMethod("addCallback", android.app.Activity.class, callbackClass).invoke(dispatcher, this, callback); + dispatcher.getClass().getMethod("addCallback", Activity.class, callbackClass).invoke(dispatcher, this, callback); } catch (Exception e) { // 如果反射调用失败,不影响原有功能 Log.d("NoteEditActivity", "OnBackPressedDispatcher not available, using legacy onBackPressed"); @@ -358,30 +388,35 @@ public class NoteEditActivity extends Activity implements OnClickListener, // 设置编辑器字体大小 mNoteEditor.setTextAppearance(this, TextAppearanceResources .getTexAppearanceResource(mFontSizeId)); + mNoteTitle.setTextAppearance(this, TextAppearanceResources + .getTexAppearanceResource(mFontSizeId)); // 根据笔记模式初始化界面 if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { - // 清单模式,只显示文本原有内容 + // 清单模式 switchToListMode(mWorkingNote.getContent()); + // 隐藏标题和普通编辑器,显示清单编辑器 + mNoteTitle.setVisibility(View.GONE); + mNoteEditor.setVisibility(View.GONE); + mEditTextList.setVisibility(View.VISIBLE); } else { - // 普通文本模式,高亮搜索词 - String content = mWorkingNote.getContent(); - String formatInfo = mWorkingNote.getRichTextFormat(); - - // 恢复富文本格式 - if (formatInfo != null && !formatInfo.isEmpty()) { - // 使用序列化的格式信息恢复文本格式 - android.text.SpannableStringBuilder formattedText = RichTextFormatUtils.deserializeFormatInfo(content, formatInfo); - mNoteEditor.setText(formattedText); - } else { - // 没有格式信息时,正常显示高亮文本 - mNoteEditor.setText(getHighlightQueryResult(content, mUserQuery)); - } - - mNoteEditor.setSelection(mNoteEditor.getText().length()); - - // 显示富文本格式工具栏 + // 普通文本模式,显示标题和正文 + mNoteTitle.setVisibility(View.VISIBLE); + mNoteEditor.setVisibility(View.VISIBLE); + mEditTextList.setVisibility(View.GONE); + // 显示富文本工具栏 mFormatToolbar.setVisibility(View.VISIBLE); + + // 拆分标题和正文 + String title = mWorkingNote.getTitle(); + String content = mWorkingNote.getContentWithoutTitle(); + + // 设置标题和正文 + mNoteTitle.setText(title); + mNoteEditor.setText(getHighlightQueryResult(content, mUserQuery)); + // 保存当前编辑状态 + mWorkingNote.setTitleAndContent(title, content); + mNoteEditor.setSelection(mNoteEditor.getText().length()); } // 隐藏所有背景颜色选中标记 @@ -399,29 +434,101 @@ public class NoteEditActivity extends Activity implements OnClickListener, | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_YEAR)); + // 设置标签显示 + String currentTag = mWorkingNote.getTag(); + if (currentTag != null && !currentTag.isEmpty()) { + String tagText = ""; + if (TAG_LIFE.equals(currentTag)) { + tagText = "生活"; + } else if (TAG_STUDY.equals(currentTag)) { + tagText = "学习"; + } else if (TAG_WORK.equals(currentTag)) { + tagText = "工作"; + } else { + tagText = currentTag; // 直接显示自定义标签值 + } + mNoteHeaderHolder.tvTag.setText("标签:" + tagText); + } else { + mNoteHeaderHolder.tvTag.setText(""); + } + // 显示提醒信息 showAlertHeader(); - - // 显示锁图标(如果便签被锁定) - showLockIcon(); - - // 初始化时设置格式工具栏背景 - updateFormatToolbarBackground(); + + // 检查关联提醒 + checkRelatedNotes(); } - + /** - * 显示锁图标(如果便签被锁定) + * 检查是否有关联的笔记,并显示提醒 */ - private void showLockIcon() { - if (mPrivacyLockManager.isNoteLocked(mWorkingNote.getNoteId())) { - // 在修改时间旁边添加锁图标 - if (mNoteHeaderHolder.tvModified != null) { - // 添加锁图标到修改时间文本末尾 - String originalText = mNoteHeaderHolder.tvModified.getText().toString(); - mNoteHeaderHolder.tvModified.setText(originalText + " 🔒"); + private void checkRelatedNotes() { + // 只有当笔记已存在于数据库时才检查关联 + if (mWorkingNote.existInDatabase()) { + // 获取当前笔记内容 + String currentContent = mWorkingNote.getContent(); + + // 查询所有可见的笔记 + Cursor cursor = getContentResolver().query( + Notes.CONTENT_NOTE_URI, + new String[]{Notes.NoteColumns.ID, Notes.NoteColumns.SNIPPET}, // 只查询ID和内容片段 + Notes.NoteColumns.TYPE + "=? AND " + Notes.NoteColumns.ID + "!=?", // 只查询笔记类型,排除当前笔记 + new String[]{String.valueOf(Notes.TYPE_NOTE), String.valueOf(mWorkingNote.getNoteId())}, + null + ); + + if (cursor != null && cursor.moveToFirst()) { + List relatedNoteIds = new ArrayList<>(); + List relatedNoteSnippets = new ArrayList<>(); + + do { + long noteId = cursor.getLong(0); + String snippet = cursor.getString(1); + + // 检查是否相关 + if (mSmartReminderManager.areNotesRelated(currentContent, snippet)) { + relatedNoteIds.add(noteId); + relatedNoteSnippets.add(snippet); + } + } while (cursor.moveToNext()); + + cursor.close(); + + // 如果找到相关笔记,显示提醒 + if (!relatedNoteIds.isEmpty()) { + showRelatedNotesAlert(relatedNoteIds, relatedNoteSnippets); + } + } else if (cursor != null) { + cursor.close(); } } } + + /** + * 显示关联笔记提醒对话框 + */ + private void showRelatedNotesAlert(List relatedNoteIds, List relatedNoteSnippets) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("关联笔记提醒"); + + // 创建列表适配器 + ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, relatedNoteSnippets); + + builder.setAdapter(adapter, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // 打开选中的关联笔记 + long noteId = relatedNoteIds.get(which); + Intent intent = new Intent(NoteEditActivity.this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, noteId); + startActivity(intent); + } + }); + + builder.setNegativeButton("取消", null); + builder.show(); + } /** * 显示或隐藏提醒相关的UI组件 @@ -429,10 +536,10 @@ public class NoteEditActivity extends Activity implements OnClickListener, private void showAlertHeader() { boolean hasClockAlert = mWorkingNote.hasClockAlert(); boolean hasLocationAlert = mWorkingNote.hasLocationAlert(); - + if (hasClockAlert || hasLocationAlert) { StringBuilder alertText = new StringBuilder(); - + // 添加时间提醒信息 if (hasClockAlert) { long time = System.currentTimeMillis(); @@ -443,7 +550,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, mWorkingNote.getAlertDate(), time, DateUtils.MINUTE_IN_MILLIS)); } } - + // 添加位置提醒信息 if (hasLocationAlert) { if (hasClockAlert) { @@ -451,1601 +558,1388 @@ public class NoteEditActivity extends Activity implements OnClickListener, } alertText.append("位置: ").append(mWorkingNote.getAlertLocationName()); } - + mNoteHeaderHolder.tvAlertDate.setText(alertText.toString()); mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE); mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE); } else { mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE); mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE); - } + }; } /** * 处理新的Intent(活动被重新启动时) */ -@Override -protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - initActivityState(intent); -} - -/** - * 保存活动状态 - */ -@Override -protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - // 保存新笔记的ID - if (!mWorkingNote.existInDatabase()) { - saveNote(); + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); } - outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); - Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); -} -/** - * 处理从地图应用返回的位置数据 - */ -@Override -protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_SELECT_LOCATION) { - if (data != null && data.getData() != null) { - Uri uri = data.getData(); - String geoString = uri.toString(); - - // 解析地图应用返回的位置数据 - // 格式通常为: geo:latitude,longitude?q=address - Pattern pattern = Pattern.compile("geo:([-0-9.]+),([-0-9.]+)"); - Matcher matcher = pattern.matcher(geoString); - if (matcher.find()) { - try { - double latitude = Double.parseDouble(matcher.group(1)); - double longitude = Double.parseDouble(matcher.group(2)); - - // 更新UI显示选择的位置 - Toast.makeText(this, "位置已选择: " + latitude + ", " + longitude, Toast.LENGTH_SHORT).show(); + /** + * 保存活动状态 + */ + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + // 保存新笔记的ID + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); + } - // 更新对话框中的输入框 - if (mLatitudeEditText != null) { - mLatitudeEditText.setText(String.valueOf(latitude)); - } - if (mLongitudeEditText != null) { - mLongitudeEditText.setText(String.valueOf(longitude)); + /** + * 处理从地图应用返回的位置数据和图片选择结果 + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_CODE_SELECT_LOCATION) { + if (data != null && data.getData() != null) { + Uri uri = data.getData(); + String geoString = uri.toString(); + + // 解析地图应用返回的位置数据 + // 格式通常为: geo:latitude,longitude?q=address + Pattern pattern = Pattern.compile("geo:([-0-9.]+),([-0-9.]+)"); + Matcher matcher = pattern.matcher(geoString); + if (matcher.find()) { + try { + double latitude = Double.parseDouble(matcher.group(1)); + double longitude = Double.parseDouble(matcher.group(2)); + + // 更新UI显示选择的位置 + Toast.makeText(this, "位置已选择: " + latitude + ", " + longitude, Toast.LENGTH_SHORT).show(); + + // 更新对话框中的输入框 + if (mLatitudeEditText != null) { + mLatitudeEditText.setText(String.valueOf(latitude)); + } + if (mLongitudeEditText != null) { + mLongitudeEditText.setText(String.valueOf(longitude)); + } + } catch (NumberFormatException e) { + Toast.makeText(this, "解析位置数据失败", Toast.LENGTH_SHORT).show(); + } } - } catch (NumberFormatException e) { - Toast.makeText(this, "解析位置数据失败", Toast.LENGTH_SHORT).show(); } - } - } - } else if (resultCode == RESULT_OK) { - // 处理图片选择结果 - Uri imageUri = null; - if (requestCode == REQUEST_CODE_PICK_IMAGE || requestCode == REQUEST_CODE_CHOOSE_IMAGE) { - if (data != null) { - imageUri = data.getData(); - } - } /* 不再处理相机拍照请求 - else if (requestCode == REQUEST_CODE_CAPTURE_IMAGE) { - if (data != null && data.getExtras() != null) { - // 拍照返回的图片通常在data.getExtras().get("data")中 - Bundle extras = data.getExtras(); - android.graphics.Bitmap bitmap = (android.graphics.Bitmap) extras.get("data"); - if (bitmap != null) { - // 将Bitmap转换为Uri - imageUri = getImageUriFromBitmap(bitmap); - } + } else if (requestCode == REQUEST_CODE_PICK_IMAGE || requestCode == REQUEST_CODE_CHOOSE_IMAGE) { + // 处理图片选择结果 + if (data != null && data.getData() != null) { + Uri imageUri = data.getData(); + insertImageToEditor(imageUri); } } - */ - - if (imageUri != null) { - insertImageToEditor(imageUri); } } -} + + /** + * 触摸事件分发,用于点击选择器外部时关闭选择器 + */ + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mNoteBgColorSelector, ev)) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } -/** - * 将Bitmap转换为Uri - */ -private Uri getImageUriFromBitmap(android.graphics.Bitmap bitmap) { - // 将Bitmap保存到临时文件,然后返回Uri - try { - java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream(); - bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 100, bytes); - String path = android.provider.MediaStore.Images.Media.insertImage( - getContentResolver(), bitmap, "Title", null); - return android.net.Uri.parse(path); - } catch (Exception e) { - e.printStackTrace(); - return null; + if (mFontSizeSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mFontSizeSelector, ev)) { + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + return super.dispatchTouchEvent(ev); } -} - -/** - * 在编辑器光标位置插入图片 - */ -private void insertImageToEditor(Uri imageUri) { - if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { - // 清单模式下,显示图片路径 - int cursorPos = mNoteEditor.getSelectionStart(); - String imagePath = imageUri.toString(); - mNoteEditor.getText().insert(cursorPos, "[图片: " + imagePath + "]"); - } else { - // 普通模式下,插入图片 - try { - // 获取图片的实际路径 - String imagePath = getRealPathFromURI(imageUri); - - // 创建图片Span - android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri)); - // 缩放图片以适应编辑器 - int maxWidth = mNoteEditor.getWidth() - 40; // 留一些边距 - if (bitmap != null && bitmap.getWidth() > maxWidth) { - float scale = (float) maxWidth / bitmap.getWidth(); - int newHeight = (int) (bitmap.getHeight() * scale); - bitmap = android.graphics.Bitmap.createScaledBitmap(bitmap, maxWidth, newHeight, true); - } + /** + * 判断触摸点是否在指定视图范围内 + */ + private boolean inRangeOfView(View view, MotionEvent ev) { + int []location = new int[2]; + view.getLocationOnScreen(location); + int x = location[0]; + int y = location[1]; + if (ev.getX() < x + || ev.getX() > (x + view.getWidth()) + || ev.getY() < y + || ev.getY() > (y + view.getHeight())) { + return false; + } + return true; + } - if (bitmap != null) { - android.text.style.ImageSpan imageSpan = new android.text.style.ImageSpan(this, bitmap); + /** + * 初始化视图资源和事件监听 + */ + private void initResources() { + mHeadViewPanel = findViewById(R.id.note_title); + mNoteHeaderHolder = new HeadViewHolder(); + mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date); + mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon); + mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date); + mNoteHeaderHolder.tvTag = (TextView) findViewById(R.id.tv_tag); + mNoteTitle = (EditText) findViewById(R.id.note_title_view); + mNoteEditor = (EditText) findViewById(R.id.note_edit_view); + mNoteEditorPanel = findViewById(R.id.sv_note_edit); + mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector); + mNoteTagSelector = findViewById(R.id.note_tag_selector); + + // 设置背景颜色选择按钮的点击监听 + for (int id : sBgSelectorBtnsMap.keySet()) { + ImageView iv = (ImageView) findViewById(id); + iv.setOnClickListener(this); + } - // 获取当前光标位置 - int cursorPos = mNoteEditor.getSelectionStart(); + // 设置标签选择按钮的点击监听 + for (int id : sTagSelectorBtnsMap.keySet()) { + ImageView iv = (ImageView) findViewById(id); + iv.setOnClickListener(this); + } - // 创建包含图片的SpannableString - android.text.SpannableStringBuilder ssb = new android.text.SpannableStringBuilder(mNoteEditor.getText()); - ssb.insert(cursorPos, "🖼️"); // 使用占位符文本 - ssb.setSpan(imageSpan, cursorPos, cursorPos + 2, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + mFontSizeSelector = findViewById(R.id.font_size_selector); + // 设置字体大小选择项的点击监听 + for (int id : sFontSizeBtnsMap.keySet()) { + View view = findViewById(id); + view.setOnClickListener(this); + }; - // 更新编辑器内容 - mNoteEditor.setText(ssb); - mNoteEditor.setSelection(cursorPos + 2); // 将光标移到图片后面 + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE); - // 保存图片信息到数据库 - saveImageToDatabase(imagePath); - } - } catch (Exception e) { - e.printStackTrace(); - Toast.makeText(this, "插入图片失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + // 修复字体大小ID可能超出范围的问题 + if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) { + mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; } + mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); + + // 初始化富文本编辑工具栏 + mFormatToolbar = findViewById(R.id.format_toolbar); + mBtnBold = findViewById(R.id.btn_bold); + mBtnItalic = findViewById(R.id.btn_italic); + mBtnUnderline = findViewById(R.id.btn_underline); + mBtnStrikethrough = findViewById(R.id.btn_strikethrough); + + // 设置富文本按钮点击监听 + mBtnBold.setOnClickListener(this); + mBtnItalic.setOnClickListener(this); + mBtnUnderline.setOnClickListener(this); + mBtnStrikethrough.setOnClickListener(this); + + // 添加正文变化监听,实现智能标题推荐 + mNoteEditor.addTextChangedListener(new android.text.TextWatcher() { + private long mLastTextChangeTime = 0; + private static final long DEBOUNCE_DELAY = 1000; // 防抖延迟1秒 + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(android.text.Editable s) { + // 只有当标题为空时,才自动推荐标题 + if (TextUtils.isEmpty(mNoteTitle.getText())) { + long currentTime = System.currentTimeMillis(); + // 防抖处理,避免频繁调用AI服务 + if (currentTime - mLastTextChangeTime > DEBOUNCE_DELAY) { + mLastTextChangeTime = currentTime; + // 使用AI服务生成智能标题 + mAIService.generateSmartTitle(s.toString()); + } + } + } + }); + + // 初始化图片相关功能 + setupImageFunctionality(); } -} - -/** - * 保存图片信息到数据库 - */ -private void saveImageToDatabase(String imagePath) { - try { - // 通过Note对象保存图片数据 - mWorkingNote.getNote().setImageData(net.micode.notes.data.Notes.DataColumns.IMAGE_PATH, imagePath); - } catch (Exception e) { - e.printStackTrace(); - Log.e(TAG, "Failed to save image data to database: " + e.getMessage()); + + /** + * 初始化图片相关功能 + */ + private void setupImageFunctionality() { + // 设置图片删除处理 + setupImageDeletionHandling(); + } + + /** + * 设置图片删除处理 + */ + private void setupImageDeletionHandling() { + // 监听编辑器内容变化,处理图片删除 + mNoteEditor.addTextChangedListener(new android.text.TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(android.text.Editable s) { + // 检查是否有图片被删除 + android.text.style.ImageSpan[] imageSpans = + s.getSpans(0, s.length(), android.text.style.ImageSpan.class); + if (imageSpans.length > 0) { + // 图片存在,不需要特殊处理 + } + } + }); } -} -/** - * 从URI获取真实路径 - */ -private String getRealPathFromURI(Uri contentUri) { - String[] proj = {android.provider.MediaStore.Images.Media.DATA}; - try { - android.database.Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null); - if (cursor != null) { - int column_index = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA); - cursor.moveToFirst(); - String result = cursor.getString(column_index); - cursor.close(); - return result; + /** + * Activity暂停时保存笔记 + */ + @Override + protected void onPause() { + super.onPause(); + // 保存当前编辑的笔记 + if(saveNote()) { + Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); } - } catch (Exception e) { - e.printStackTrace(); + clearSettingState(); // 清理设置面板状态 } - return contentUri.toString(); -} -/** - * 触摸事件分发,用于点击选择器外部时关闭选择器 - */ -@Override -public boolean dispatchTouchEvent(MotionEvent ev) { - if (mNoteBgColorSelector.getVisibility() == View.VISIBLE - && !inRangeOfView(mNoteBgColorSelector, ev)) { - mNoteBgColorSelector.setVisibility(View.GONE); - return true; - } + /** + * 更新关联的桌面小部件 + */ + private void updateWidget() { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + // 根据小部件类型设置对应的广播接收器 + if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { + intent.setClass(this, NoteWidgetProvider_2x.class); + } else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) { + intent.setClass(this, NoteWidgetProvider_4x.class); + } else { + Log.e(TAG, "Unspported widget type"); + return; + } - if (mFontSizeSelector.getVisibility() == View.VISIBLE - && !inRangeOfView(mFontSizeSelector, ev)) { - mFontSizeSelector.setVisibility(View.GONE); - return true; - } - return super.dispatchTouchEvent(ev); -} + // 设置要更新的小部件ID + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { + mWorkingNote.getWidgetId() + }); -/** - * 判断触摸点是否在指定视图范围内 - */ -private boolean inRangeOfView(View view, MotionEvent ev) { - int []location = new int[2]; - view.getLocationOnScreen(location); - int x = location[0]; - int y = location[1]; - if (ev.getX() < x - || ev.getX() > (x + view.getWidth()) - || ev.getY() < y - || ev.getY() > (y + view.getHeight())) { - return false; + sendBroadcast(intent); // 发送广播通知小部件更新 + setResult(RESULT_OK, intent); // 设置活动结果 } - return true; -} - -/** - * 初始化视图资源和事件监听 - */ -private void initResources() { - mHeadViewPanel = findViewById(R.id.note_title); - mNoteHeaderHolder = new HeadViewHolder(); - mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date); - mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon); - mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date); - mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color); - mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); - mNoteEditor = (EditText) findViewById(R.id.note_edit_view); - mNoteEditorPanel = findViewById(R.id.sv_note_edit); - mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector); - - // 设置背景颜色选择按钮的点击监听 - for (int id : sBgSelectorBtnsMap.keySet()) { - ImageView iv = (ImageView) findViewById(id); - iv.setOnClickListener(this); - } - - mFontSizeSelector = findViewById(R.id.font_size_selector); - // 设置字体大小选择项的点击监听 - for (int id : sFontSizeBtnsMap.keySet()) { - View view = findViewById(id); - view.setOnClickListener(this); - }; - - // 初始化富文本编辑工具栏 - mFormatToolbar = findViewById(R.id.format_toolbar); - mBtnBold = findViewById(R.id.btn_bold); - mBtnItalic = findViewById(R.id.btn_italic); - mBtnUnderline = findViewById(R.id.btn_underline); - mBtnStrikethrough = findViewById(R.id.btn_strikethrough); - - // 设置富文本编辑按钮点击监听器 - mBtnBold.setOnClickListener(this); - mBtnItalic.setOnClickListener(this); - mBtnUnderline.setOnClickListener(this); - mBtnStrikethrough.setOnClickListener(this); - - // 初始化并设置添加图片按钮 - mBtnAddImage = findViewById(R.id.add_img_btn); - mBtnAddImage.setOnClickListener(this); - - mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); - mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE); - - // 修复字体大小ID可能超出范围的问题 - if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) { - mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; - } - mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); - - // 设置图片删除处理 - setupImageDeletionHandling(); -} -/** - * Activity暂停时保存笔记 - */ -@Override -protected void onPause() { - super.onPause(); - // 保存当前编辑的笔记 - if(saveNote()) { - Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); - } - clearSettingState(); // 清理设置面板状态 + /** + * 点击事件处理 + */ + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.btn_set_color) { + // 点击设置按钮,显示选择对话框 + mNoteBgColorSelector.setVisibility(View.VISIBLE); + mNoteTagSelector.setVisibility(View.VISIBLE); + } else if (sBgSelectorBtnsMap.containsKey(id)) { + // 点击了某个背景颜色按钮 + // 隐藏之前选中颜色的标记 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.GONE); + // 更新笔记的背景颜色ID + mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); + mNoteBgColorSelector.setVisibility(View.GONE); // 隐藏选择器 + } else if (sTagSelectorBtnsMap.containsKey(id)) { + // 点击了某个标签按钮 + // 隐藏之前选中标签的标记 + String currentTag = mWorkingNote.getTag(); + if (currentTag != null || currentTag.isEmpty()) { + currentTag = ""; + } + Integer selectId = sTagSelectorSelectionMap.get(currentTag); + if (selectId != null) { + findViewById(selectId).setVisibility(View.GONE); + } + } else if (id == R.id.btn_bold || id == R.id.btn_italic || + id == R.id.btn_underline || id == R.id.btn_strikethrough) { + // 处理富文本编辑按钮点击 + handleRichTextFormatting(v); + } else if (sFontSizeBtnsMap.containsKey(id)) { + // 点击了字体大小选项 + // 隐藏之前选中字体的标记 + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); + // 更新字体大小ID并保存到偏好设置 + mFontSizeId = sFontSizeBtnsMap.get(id); + mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); + // 显示新选中字体的标记 + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); - // 隐藏富文本格式工具栏 - if (mFormatToolbar != null) { - mFormatToolbar.setVisibility(View.GONE); + // 应用新的字体大小 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 清单模式:重新加载文本并切换到列表模式 + getWorkingText(); + switchToListMode(mWorkingNote.getContent()); + } else { + // 普通模式:直接设置编辑器的文本外观 + mNoteEditor.setTextAppearance(this, + TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + } + mFontSizeSelector.setVisibility(View.GONE); // 隐藏字体选择器 + } } -} -/** - * 更新关联的桌面小部件 - */ -private void updateWidget() { - Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - // 根据小部件类型设置对应的广播接收器 - if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { - intent.setClass(this, NoteWidgetProvider_2x.class); - } else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) { - intent.setClass(this, NoteWidgetProvider_4x.class); - } else { - Log.e(TAG, "Unspported widget type"); - return; - } - - // 设置要更新的小部件ID - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { - mWorkingNote.getWidgetId() - }); - - sendBroadcast(intent); // 发送广播通知小部件更新 - setResult(RESULT_OK, intent); // 设置活动结果 -} - -/** - * 点击事件处理 - */ -public void onClick(View v) { - int id = v.getId(); - if (id == R.id.btn_set_bg_color) { - // 点击背景颜色设置按钮,显示颜色选择器 - mNoteBgColorSelector.setVisibility(View.VISIBLE); - // 显示当前选中颜色的标记 - findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( - View.VISIBLE); - } else if (sBgSelectorBtnsMap.containsKey(id)) { - // 点击了某个背景颜色按钮 - // 隐藏之前选中颜色的标记 - findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( - View.GONE); - // 更新笔记的背景颜色ID - mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); - mNoteBgColorSelector.setVisibility(View.GONE); // 隐藏选择器 - } else if (sFontSizeBtnsMap.containsKey(id)) { - // 点击了字体大小选项 - // 隐藏之前选中字体的标记 - findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); - // 更新字体大小ID并保存到偏好设置 - mFontSizeId = sFontSizeBtnsMap.get(id); - mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); - // 显示新选中字体的标记 - findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); - - // 应用新的字体大小 - if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { - // 清单模式:重新加载文本并切换到列表模式 - getWorkingText(); - switchToListMode(mWorkingNote.getContent()); - } else { - // 普通模式:直接设置编辑器的文本外观 - mNoteEditor.setTextAppearance(this, - TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + /** + * 返回键按下事件处理 + */ + @Override + public void onBackPressed() { + // 如果有设置面板打开,先关闭面板 + if(clearSettingState()) { + return; } - mFontSizeSelector.setVisibility(View.GONE); // 隐藏字体选择器 - } else if (id == R.id.btn_bold || id == R.id.btn_italic || - id == R.id.btn_underline || id == R.id.btn_strikethrough) { - // 处理富文本编辑按钮点击 - handleRichTextFormatting(v); - } else if (id == R.id.add_img_btn) { - // 处理添加图片按钮点击 - showImageSelectionDialog(); - } -} -/** - * 返回键按下事件处理 - */ -@Override -public void onBackPressedDispatcher() { - // 如果有设置面板打开,先关闭面板 - if(clearSettingState()) { - return; + // 保存笔记后执行默认返回操作 + saveNote(); + super.onBackPressed(); } - // 保存笔记后执行默认返回操作 - saveNote(); - super.onBackPressed(); -} - -/** - * 清理设置面板状态(关闭打开的面板) - * @return 是否有面板被关闭 - */ -private boolean clearSettingState() { - if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { - mNoteBgColorSelector.setVisibility(View.GONE); - return true; - } else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { - mFontSizeSelector.setVisibility(View.GONE); - return true; + /** + * 清理设置面板状态(关闭打开的面板) + * @return 是否有面板被关闭 + */ + private boolean clearSettingState() { + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } else if (mNoteTagSelector.getVisibility() == View.VISIBLE) { + mNoteTagSelector.setVisibility(View.GONE); + return true; + } else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + return false; } - return false; -} - -/** - * 背景颜色改变回调(NoteSettingChangedListener接口方法) - */ -public void onBackgroundColorChanged() { - // 显示新选中颜色的标记 - findViewById(NoteEditActivity.sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( - View.VISIBLE); - // 更新编辑器面板和头部面板的背景 - mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); - mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); - - // 根据当前背景颜色调整富文本格式工具栏的背景 - updateFormatToolbarBackground(); -} - -/** - * 根据当前便签背景颜色更新格式工具栏的背景 - */ -private void updateFormatToolbarBackground() { - if (mFormatToolbar != null) { - // 获取当前背景颜色资源ID - int bgColorResId = mWorkingNote.getBgColorResId(); - // 根据背景颜色计算合适的工具栏背景色 - int toolbarBgColor = calculateToolbarBackgroundColor(bgColorResId); - - // 设置工具栏背景 - mFormatToolbar.setBackgroundColor(toolbarBgColor); + /** + * 背景颜色改变回调(NoteSettingChangedListener接口方法) + */ + public void onBackgroundColorChanged() { + // 显示新选中颜色的标记 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.VISIBLE); + // 更新编辑器面板和头部面板的背景 + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); } -} - -/** - * 根据便签背景资源计算工具栏合适的背景颜色 - * @param bgColorResId 便签背景资源ID - * @return 计算出的工具栏背景颜色 - */ -private int calculateToolbarBackgroundColor(int bgColorResId) { - // 获取当前背景颜色ID - int bgColorId = mWorkingNote.getBgColorId(); - - // 根据不同背景颜色ID返回相应的半透明工具栏背景 - switch (bgColorId) { - case ResourceParser.YELLOW: // 黄色背景 - // 黄色背景较亮,使用浅灰色半透明背景 - return getColorWithAlpha(0x4D, 0xDD, 0xDD, 0xDD); // 浅灰半透明 - case ResourceParser.BLUE: // 蓝色背景 - // 蓝色背景,使用浅蓝白半透明背景 - return getColorWithAlpha(0x66, 0xF0, 0xF8, 0xFF); // 浅蓝白半透明 - case ResourceParser.WHITE: // 白色背景 - // 白色背景,使用浅灰半透明背景 - return getColorWithAlpha(0x4D, 0xCC, 0xCC, 0xCC); // 中灰半透明 - case ResourceParser.GREEN: // 绿色背景 - // 绿色背景,使用浅绿白半透明背景 - return getColorWithAlpha(0x66, 0xE8, 0xF5, 0xE8); // 浅绿白半透明 - case ResourceParser.RED: // 红色背景 - // 红色背景,使用浅红白半透明背景 - return getColorWithAlpha(0x66, 0xFA, 0xE0, 0xE0); // 浅红白半透明 - default: - // 默认使用半透明白色背景 - return getColorWithAlpha(0x66, 0xFF, 0xFF, 0xFF); // 半透明白色 - } -} -/** - * 组合ARGB值为颜色整数 - * @param alpha 透明度 (0-255) - * @param red 红色值 (0-255) - * @param green 绿色值 (0-255) - * @param blue 蓝色值 (0-255) - * @return ARGB颜色整数 - */ -private int getColorWithAlpha(int alpha, int red, int green, int blue) { - return (alpha << 24) | (red << 16) | (green << 8) | blue; -} - -/** - * 准备选项菜单 - */ -@Override -public boolean onPrepareOptionsMenu(Menu menu) { - if (isFinishing()) { + /** + * 创建选项菜单 + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // 根据笔记类型加载不同的菜单 + if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) { + getMenuInflater().inflate(R.menu.call_note_edit, menu); + } else { + getMenuInflater().inflate(R.menu.note_edit, menu); + } return true; } - clearSettingState(); // 清理设置面板状态 - menu.clear(); // 清空菜单 - // 根据笔记类型加载不同的菜单 - if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) { - getMenuInflater().inflate(R.menu.call_note_edit, menu); - } else { - getMenuInflater().inflate(R.menu.note_edit, menu); - } + /** + * 准备选项菜单 + */ + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + if (isFinishing()) { + return true; + } + clearSettingState(); // 清理设置面板状态 + menu.clear(); // 清空菜单 - // 设置清单模式/普通模式菜单项标题 - if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { - menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode); - } else { - menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode); - } + // 根据笔记类型加载不同的菜单 + if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) { + getMenuInflater().inflate(R.menu.call_note_edit, menu); + } else { + getMenuInflater().inflate(R.menu.note_edit, menu); + } - // 根据是否有提醒设置菜单项显示 - if (mWorkingNote.hasClockAlert()) { - menu.findItem(R.id.menu_alert).setVisible(false); - menu.findItem(R.id.menu_delete_remind).setVisible(true); - } else { - menu.findItem(R.id.menu_alert).setVisible(true); - menu.findItem(R.id.menu_delete_remind).setVisible(false); - } + // 设置清单模式/普通模式菜单项标题 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode); + } else { + menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode); + } - // 根据是否有位置提醒设置菜单项显示 - if (mWorkingNote.hasLocationAlert()) { - menu.findItem(R.id.menu_location_alert).setVisible(false); - menu.findItem(R.id.menu_delete_location_remind).setVisible(true); - } else { - menu.findItem(R.id.menu_location_alert).setVisible(true); - menu.findItem(R.id.menu_delete_location_remind).setVisible(false); + // 根据是否有提醒设置菜单项显示 + if (mWorkingNote.hasClockAlert()) { + menu.findItem(R.id.menu_alert).setVisible(false); + menu.findItem(R.id.menu_delete_remind).setVisible(true); + } else { + menu.findItem(R.id.menu_alert).setVisible(true); + menu.findItem(R.id.menu_delete_remind).setVisible(false); + } + + // 根据是否有位置提醒设置菜单项显示 + if (mWorkingNote.hasLocationAlert()) { + menu.findItem(R.id.menu_location_alert).setVisible(false); + menu.findItem(R.id.menu_delete_location_remind).setVisible(true); + } else { + menu.findItem(R.id.menu_location_alert).setVisible(true); + menu.findItem(R.id.menu_delete_location_remind).setVisible(false); + } + return true; } - return true; -} - -/** - * 菜单项点击处理 - */ -@Override -public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_new_note: - // 创建新笔记 - createNewNote(); - break; - case R.id.menu_delete: - // 删除笔记,弹出确认对话框 - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.alert_title_delete)); - builder.setIcon(android.R.drawable.ic_dialog_alert); - builder.setMessage(getString(R.string.alert_message_delete_note)); - builder.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - deleteCurrentNote(); // 删除当前笔记 - finish(); // 结束活动 - } - }); - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - break; - case R.id.menu_font_size: - // 显示字体大小选择器 - mFontSizeSelector.setVisibility(View.VISIBLE); - findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); - break; - case R.id.menu_list_mode: - // 切换清单模式/普通模式 - mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ? - TextNote.MODE_CHECK_LIST : 0); - break; - case R.id.menu_share: - // 分享笔记内容 - getWorkingText(); - sendTo(this, mWorkingNote.getContent()); - break; - case R.id.menu_send_to_desktop: - // 发送到桌面快捷方式 - sendToDesktop(); - break; - case R.id.menu_alert: - // 设置提醒 - setReminder(); - break; - case R.id.menu_delete_remind: - // 删除提醒 - mWorkingNote.setAlertDate(0, false); - break; - case R.id.menu_location_alert: - // 设置位置提醒 - setLocationReminder(); - break; - - case R.id.menu_delete_location_remind: - // 删除位置提醒 - mWorkingNote.setAlertLocation(0, 0, 0, "", false); - break; - default: - break; - } - return true; -} - -/** - * 设置提醒时间 - */ -private void setReminder() { - // 创建日期时间选择对话框 - DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis()); - d.setOnDateTimeSetListener(new OnDateTimeSetListener() { - public void OnDateTimeSet(AlertDialog dialog, long date) { - // 设置提醒日期 - mWorkingNote.setAlertDate(date, true); + /** + * 菜单项点击处理 + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_new_note: + // 创建新笔记 + createNewNote(); + break; + case R.id.menu_delete: + // 删除笔记,弹出确认对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_note)); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteCurrentNote(); // 删除当前笔记 + finish(); // 结束活动 + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case R.id.menu_font_size: + // 显示字体大小选择器 + mFontSizeSelector.setVisibility(View.VISIBLE); + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + break; + case R.id.menu_list_mode: + // 切换清单模式/普通模式 + mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ? + TextNote.MODE_CHECK_LIST : 0); + break; + case R.id.menu_share: + // 分享笔记内容 + getWorkingText(); + sendTo(this, mWorkingNote.getContent()); + break; + case R.id.menu_send_to_desktop: + // 发送到桌面快捷方式 + sendToDesktop(); + break; + case R.id.menu_alert: + // 设置提醒 + setReminder(); + break; + case R.id.menu_delete_remind: + // 删除提醒 + mWorkingNote.setAlertDate(0, false); + break; + + case R.id.menu_location_alert: + // 设置位置提醒 + setLocationReminder(); + break; + + case R.id.menu_delete_location_remind: + // 删除位置提醒 + mWorkingNote.setAlertLocation(0, 0, 0, "", false); + break; + case R.id.menu_tag: + // 显示标签选择对话框 + showTagSelectorDialog(); + break; + case R.id.menu_add_image: + // 显示图片选择对话框 + showImageSelectionDialog(); + break; + default: + break; } - }); - d.show(); -} - -/** - * 设置位置提醒 - */ -private void setLocationReminder() { - // 创建位置提醒对话框 - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.location_reminder_dialog_title); - - // 创建布局 - LinearLayout layout = new LinearLayout(this); - layout.setOrientation(LinearLayout.VERTICAL); - layout.setPadding(50, 30, 50, 30); - - // 位置名称输入框 - mLocationNameEditText = new EditText(this); - mLocationNameEditText.setHint(R.string.location_reminder_enter_name); - LinearLayout.LayoutParams nameParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - nameParams.setMargins(0, 0, 0, 20); - mLocationNameEditText.setLayoutParams(nameParams); - layout.addView(mLocationNameEditText); - - // 纬度输入框 - mLatitudeEditText = new EditText(this); - mLatitudeEditText.setHint("纬度 (Latitude)"); - mLatitudeEditText.setInputType(InputType.TYPE_NUMBER_FLAG_DECIMAL | InputType.TYPE_NUMBER_FLAG_SIGNED); - LinearLayout.LayoutParams latParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - latParams.setMargins(0, 0, 0, 20); - mLatitudeEditText.setLayoutParams(latParams); - layout.addView(mLatitudeEditText); - - // 经度输入框 - mLongitudeEditText = new EditText(this); - mLongitudeEditText.setHint("经度 (Longitude)"); - mLongitudeEditText.setInputType(InputType.TYPE_NUMBER_FLAG_DECIMAL | InputType.TYPE_NUMBER_FLAG_SIGNED); - LinearLayout.LayoutParams lonParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - lonParams.setMargins(0, 0, 0, 20); - mLongitudeEditText.setLayoutParams(lonParams); - layout.addView(mLongitudeEditText); - - // 选择位置按钮 - final Button selectLocationButton = new Button(this); - selectLocationButton.setText(R.string.location_reminder_select_location); - LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - buttonParams.setMargins(0, 0, 0, 20); - selectLocationButton.setLayoutParams(buttonParams); - layout.addView(selectLocationButton); - - // 提醒半径输入框 - final EditText radiusEditText = new EditText(this); - radiusEditText.setHint(R.string.location_reminder_radius); - radiusEditText.setInputType(InputType.TYPE_CLASS_NUMBER); - radiusEditText.setText("100"); // 默认100米 - LinearLayout.LayoutParams radiusParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - radiusEditText.setLayoutParams(radiusParams); - layout.addView(radiusEditText); - - // 选择位置按钮点击事件 - selectLocationButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // 调用地图应用选择位置 - Intent intent = new Intent(Intent.ACTION_VIEW); - - // 尝试不同的URI格式 - Uri geoUri = Uri.parse("geo:0,0"); - intent.setData(geoUri); - - // 不限制特定地图应用,让系统选择默认的地图应用 - intent.setPackage(null); + return true; + } - if (intent.resolveActivity(getPackageManager()) != null) { - startActivityForResult(intent, REQUEST_CODE_SELECT_LOCATION); - } else { - // 如果第一种格式失败,尝试另一种格式 - intent.setData(Uri.parse("geo:0,0?q=map")); - if (intent.resolveActivity(getPackageManager()) != null) { - startActivityForResult(intent, REQUEST_CODE_SELECT_LOCATION); - } else { - // 如果还是失败,尝试更通用的方式 - Intent chooserIntent = Intent.createChooser(intent, "选择地图应用"); - if (chooserIntent.resolveActivity(getPackageManager()) != null) { - startActivityForResult(chooserIntent, REQUEST_CODE_SELECT_LOCATION); - } else { - Toast.makeText(NoteEditActivity.this, "没有找到地图应用", Toast.LENGTH_SHORT).show(); - } + /** + * 设置提醒时间 + */ + private void setReminder() { + // 获取当前笔记内容到mWorkingNote对象 + getWorkingText(); + String content = mWorkingNote.getContent(); + + // 使用智能提醒管理器获取建议的提醒时间 + long suggestedTime = mSmartReminderManager.suggestReminderTime(content); + String suggestedRepeatType = mSmartReminderManager.suggestRepeatType(content); + + // 如果没有获取到建议时间,使用当前时间 + long initialTime = suggestedTime > 0 ? suggestedTime : System.currentTimeMillis(); + + // 创建日期时间选择对话框 + DateTimePickerDialog d = new DateTimePickerDialog(this, initialTime); + d.setOnDateTimeSetListener(new OnDateTimeSetListener() { + public void OnDateTimeSet(AlertDialog dialog, long date) { + // 设置提醒日期 + mWorkingNote.setAlertDate(date, true); + + // 显示智能建议的重复频率 + if (!TextUtils.isEmpty(suggestedRepeatType)) { + new AlertDialog.Builder(NoteEditActivity.this) + .setTitle("智能重复建议") + .setMessage("根据笔记内容,建议设置为" + suggestedRepeatType + "重复") + .setPositiveButton("设置", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // 这里可以添加设置重复提醒的逻辑 + // 目前应用只支持单次提醒,所以只做提示 + Toast.makeText(NoteEditActivity.this, "智能建议已显示,当前版本暂不支持重复提醒", Toast.LENGTH_SHORT).show(); + } + }) + .setNegativeButton("取消", null) + .show(); } } - } - }); - - builder.setView(layout); - - // 确定按钮 - builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - try { - String locationName = mLocationNameEditText.getText().toString().trim(); - double latitude = Double.parseDouble(mLatitudeEditText.getText().toString()); - double longitude = Double.parseDouble(mLongitudeEditText.getText().toString()); - float radius = Float.parseFloat(radiusEditText.getText().toString()); - - // 验证输入 - if (TextUtils.isEmpty(locationName)) { - locationName = "未知位置"; - } - if (radius <= 0) { - radius = 100; // 默认100米 - } - - // 设置位置提醒 - mWorkingNote.setAlertLocation(latitude, longitude, radius, locationName, true); - } catch (NumberFormatException e) { - Toast.makeText(NoteEditActivity.this, "请输入有效的位置信息", Toast.LENGTH_SHORT).show(); + }); + d.show(); + } + + /** + * 显示图片选择对话框 + */ + private void showImageSelectionDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("选择图片"); // 标题 + + // 创建垂直排列的按钮布局 + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(50, 30, 50, 30); + + // 相册按钮 + Button albumBtn = new Button(this); + albumBtn.setText("相册"); + albumBtn.setAllCaps(false); // 不要大写 + albumBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + openGallery(); } - } - }); - - // 取消按钮 - builder.setNegativeButton(android.R.string.cancel, null); - - // 显示对话框 - builder.show(); -} -/** - * 分享笔记内容到其他应用 - */ -private void sendTo(Context context, String info) { - Intent intent = new Intent(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_TEXT, info); // 设置分享文本 - intent.setType("text/plain"); // 设置MIME类型 - context.startActivity(intent); // 启动分享选择器 -} + }); -/** - * 创建新笔记 - */ -private void createNewNote() { - // 先保存当前编辑的笔记 - saveNote(); - - // 结束当前活动并启动新的编辑活动 - finish(); - Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_INSERT_OR_EDIT); - intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId()); - startActivity(intent); -} + layout.addView(albumBtn); -/** - * 删除当前笔记 - */ -private void deleteCurrentNote() { - if (mWorkingNote.existInDatabase()) { - HashSet ids = new HashSet(); - long id = mWorkingNote.getNoteId(); - if (id != Notes.ID_ROOT_FOLDER) { - ids.add(id); + builder.setView(layout); + builder.setNegativeButton("取消", null); + builder.show(); + } + + /** + * 打开相册选择图片 + */ + private void openGallery() { + // 使用更通用的ACTION_GET_CONTENT,避免直接访问存储 + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("image/*"); + intent.addCategory(Intent.CATEGORY_OPENABLE); + + // 使用Intent.createChooser确保能打开选择器 + Intent chooserIntent = Intent.createChooser(intent, "选择图片"); + if (chooserIntent.resolveActivity(getPackageManager()) != null) { + // 对于ACTION_GET_CONTENT,系统会自动处理权限,不需要手动请求READ_EXTERNAL_STORAGE + startActivityForResult(chooserIntent, REQUEST_CODE_PICK_IMAGE); } else { - Log.d(TAG, "Wrong note id, should not happen"); + Toast.makeText(this, "设备不支持图片选择功能", Toast.LENGTH_SHORT).show(); } - - // 根据同步模式选择删除方式 - if (!isSyncMode()) { - // 非同步模式:直接删除 - if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) { - Log.e(TAG, "Delete Note error"); - } - } else { - // 同步模式:移动到垃圾箱 - if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLDER)) { - Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + + /** + * 处理权限请求结果 + */ + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == 100) { + if (grantResults.length > 0 && grantResults[0] == android.content.pm.PackageManager.PERMISSION_GRANTED) { + // 权限被授予,重新打开相册 + openGallery(); + } else { + Toast.makeText(this, "需要存储权限才能选择图片", Toast.LENGTH_SHORT).show(); } } } - mWorkingNote.markDeleted(true); // 标记为已删除 -} - -/** - * 检查是否处于同步模式 - */ -private boolean isSyncMode() { - return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; -} - -/** - * 提醒时间改变回调(NoteSettingChangedListener接口方法) - */ -public void onClockAlertChanged(long date, boolean set) { - // 对于未保存的笔记,先保存再设置提醒 - if (!mWorkingNote.existInDatabase()) { - saveNote(); + + /** + * 处理富文本格式设置 + * @param v 点击的视图 + */ + private void handleRichTextFormatting(View v) { + int id = v.getId(); + android.text.Editable editable = mNoteEditor.getText(); + int start = mNoteEditor.getSelectionStart(); + int end = mNoteEditor.getSelectionEnd(); + + // 如果没有选中文本,不进行格式设置 + if (start == end) { + Toast.makeText(this, "请先选中文本", Toast.LENGTH_SHORT).show(); + return; + } + + // 应用相应的格式 + if (id == R.id.btn_bold) { + // 应用或移除粗体格式 + applyOrRemoveStyleSpan(editable, start, end, android.graphics.Typeface.BOLD); + } else if (id == R.id.btn_italic) { + // 应用或移除斜体格式 + applyOrRemoveStyleSpan(editable, start, end, android.graphics.Typeface.ITALIC); + } else if (id == R.id.btn_underline) { + // 应用或移除下划线格式 + applyOrRemoveUnderlineSpan(editable, start, end); + } else if (id == R.id.btn_strikethrough) { + // 应用或移除删除线格式 + applyOrRemoveStrikethroughSpan(editable, start, end); + } } - - if (mWorkingNote.getNoteId() > 0) { - // 创建闹钟意图 - Intent intent = new Intent(this, AlarmReceiver.class); - intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId())); - PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_IMMUTABLE); - AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE)); - - showAlertHeader(); // 更新提醒显示 - if(!set) { - // 取消提醒 - alarmManager.cancel(pendingIntent); - } else { - // 设置提醒 - alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); + + /** + * 应用或移除StyleSpan(粗体、斜体等) + */ + private void applyOrRemoveStyleSpan(android.text.Editable editable, int start, int end, int style) { + android.text.style.StyleSpan[] spans = editable.getSpans(start, end, android.text.style.StyleSpan.class); + boolean hasStyle = false; + + // 检查是否已应用该样式 + for (android.text.style.StyleSpan span : spans) { + if (span.getStyle() == style) { + hasStyle = true; + editable.removeSpan(span); + } + } + + // 如果没有应用该样式,则添加 + if (!hasStyle) { + editable.setSpan(new android.text.style.StyleSpan(style), start, end, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } - } else { - // 笔记为空,无法设置提醒 - Log.e(TAG, "Clock alert setting error"); - showToast(R.string.error_note_empty_for_clock); } -} - -/** - * 位置提醒改变回调(NoteSettingChangedListener接口方法) - */ -public void onLocationAlertChanged(double latitude, double longitude, float radius, String locationName, boolean set) { - // 对于未保存的笔记,先保存再设置提醒 - if (!mWorkingNote.existInDatabase()) { - saveNote(); + + /** + * 应用或移除下划线格式 + */ + private void applyOrRemoveUnderlineSpan(android.text.Editable editable, int start, int end) { + android.text.style.UnderlineSpan[] spans = editable.getSpans(start, end, android.text.style.UnderlineSpan.class); + + // 如果已应用下划线,则移除 + if (spans.length > 0) { + for (android.text.style.UnderlineSpan span : spans) { + editable.removeSpan(span); + } + } else { + // 否则添加下划线 + editable.setSpan(new android.text.style.UnderlineSpan(), start, end, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } } - - if (mWorkingNote.getNoteId() > 0) { - showAlertHeader(); // 更新提醒显示 - // 位置提醒的地理围栏实现将在后续版本中添加 - if (set) { - Toast.makeText(this, "位置提醒已设置", Toast.LENGTH_SHORT).show(); + + /** + * 应用或移除删除线格式 + */ + private void applyOrRemoveStrikethroughSpan(android.text.Editable editable, int start, int end) { + android.text.style.StrikethroughSpan[] spans = editable.getSpans(start, end, android.text.style.StrikethroughSpan.class); + + // 如果已应用删除线,则移除 + if (spans.length > 0) { + for (android.text.style.StrikethroughSpan span : spans) { + editable.removeSpan(span); + } } else { - Toast.makeText(this, "位置提醒已取消", Toast.LENGTH_SHORT).show(); + // 否则添加删除线 + editable.setSpan(new android.text.style.StrikethroughSpan(), start, end, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } - } else { - // 笔记为空,无法设置提醒 - Log.e(TAG, "Location alert setting error"); - showToast(R.string.error_note_empty_for_clock); } -} - -/** - * 小部件改变回调(NoteSettingChangedListener接口方法) - */ -public void onWidgetChanged() { - updateWidget(); // 更新关联的小部件 -} - -/** - * 清单模式下删除编辑项(OnTextViewChangeListener接口方法) - */ -public void onEditTextDelete(int index, String text) { - int childCount = mEditTextList.getChildCount(); - if (childCount == 1) { - return; // 只有一个项时不删除 - } - - // 更新后面所有项的索引 - for (int i = index + 1; i < childCount; i++) { - ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) - .setIndex(i - 1); - } - - // 移除指定位置的视图 - mEditTextList.removeViewAt(index); - NoteEditText edit = null; - // 决定焦点给哪个编辑框 - if(index == 0) { - edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById(R.id.et_edit_text); - } else { - edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById(R.id.et_edit_text); - } - // 将删除的文本追加到获得焦点的编辑框 - int length = edit.length(); - edit.append(text); - edit.requestFocus(); - edit.setSelection(length); -} - -/** - * 清单模式下插入新编辑项(OnTextViewChangeListener接口方法) - */ -public void onEditTextEnter(int index, String text) { - // 安全检查 - if(index > mEditTextList.getChildCount()) { - Log.e(TAG, "Index out of mEditTextList boundrary, should not happen"); + + /** + * 将图片插入到编辑器中 + * @param imageUri 图片Uri + */ + private void insertImageToEditor(Uri imageUri) { + try { + // 获取当前光标位置 + int cursorPos = mNoteEditor.getSelectionStart(); + + // 从Uri获取图片Bitmap + android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri)); + if (bitmap == null) { + Toast.makeText(this, "无法加载图片", Toast.LENGTH_SHORT).show(); + return; + } + + // 缩放图片以适应编辑器 + int maxWidth = mNoteEditor.getWidth() - mNoteEditor.getPaddingLeft() - mNoteEditor.getPaddingRight(); + int maxHeight = 500; // 设置最大高度 + float scale = Math.min((float) maxWidth / bitmap.getWidth(), (float) maxHeight / bitmap.getHeight()); + android.graphics.Matrix matrix = new android.graphics.Matrix(); + matrix.postScale(scale, scale); + android.graphics.Bitmap scaledBitmap = android.graphics.Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + + // 创建ImageSpan并插入到编辑器 + android.text.SpannableStringBuilder ssb = new android.text.SpannableStringBuilder(mNoteEditor.getText()); + ssb.insert(cursorPos, " "); // 插入一个空格作为图片占位符 + android.text.style.ImageSpan imageSpan = new android.text.style.ImageSpan(this, scaledBitmap); + ssb.setSpan(imageSpan, cursorPos, cursorPos + 1, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + // 更新编辑器内容 + mNoteEditor.setText(ssb); + mNoteEditor.setSelection(cursorPos + 1); + + // 保存图片路径到数据库 + String imagePath = getRealPathFromURI(imageUri); + if (imagePath != null) { + saveImageToDatabase(imagePath); + } + } catch (Exception e) { + e.printStackTrace(); + Toast.makeText(this, "插入图片失败", Toast.LENGTH_SHORT).show(); + } } - - // 创建新列表项并插入到指定位置 - View view = getListItem(text, index); - mEditTextList.addView(view, index); - NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); - edit.requestFocus(); - edit.setSelection(0); - - // 更新后面所有项的索引 - for (int i = index + 1; i < mEditTextList.getChildCount(); i++) { - ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) - .setIndex(i); + + /** + * 将Bitmap转换为Uri + * @param bitmap 图片Bitmap + * @return 图片Uri + */ + private Uri getImageUriFromBitmap(android.graphics.Bitmap bitmap) { + String path = android.provider.MediaStore.Images.Media.insertImage( + getContentResolver(), bitmap, "NoteImage", null); + return Uri.parse(path); } -} - -/** - * 切换到清单模式 - */ -private void switchToListMode(String text) { - mEditTextList.removeAllViews(); // 清空现有视图 - String[] items = text.split("\n"); // 按换行符分割文本 - int index = 0; - for (String item : items) { - if(!TextUtils.isEmpty(item)) { - // 为每个非空段落创建列表项 - mEditTextList.addView(getListItem(item, index)); - index++; + + /** + * 从Uri获取真实路径 + * @param contentUri 内容Uri + * @return 真实文件路径 + */ + private String getRealPathFromURI(Uri contentUri) { + String[] proj = {android.provider.MediaStore.Images.Media.DATA}; + Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null); + if (cursor != null) { + int column_index = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA); + cursor.moveToFirst(); + String path = cursor.getString(column_index); + cursor.close(); + return path; } + return null; } - // 添加一个空的列表项用于输入新内容 - mEditTextList.addView(getListItem("", index)); - mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus(); - - // 切换显示模式 - mNoteEditor.setVisibility(View.GONE); - mEditTextList.setVisibility(View.VISIBLE); -} - -/** - * 高亮显示搜索关键词 - */ -private Spannable getHighlightQueryResult(String fullText, String userQuery) { - SpannableString spannable = new SpannableString(fullText == null ? "" : fullText); - if (!TextUtils.isEmpty(userQuery)) { - mPattern = Pattern.compile(userQuery); - Matcher m = mPattern.matcher(fullText); - int start = 0; - // 为所有匹配的文本设置背景色 - while (m.find(start)) { - spannable.setSpan( - new BackgroundColorSpan(this.getResources().getColor( - R.color.user_query_highlight)), m.start(), m.end(), - Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - start = m.end(); - } + + /** + * 保存图片到数据库 + * @param imagePath 图片路径 + */ + private void saveImageToDatabase(String imagePath) { + // 这里可以添加保存图片路径到数据库的逻辑 + // 目前暂时只做日志记录 + Log.d(TAG, "Saved image path: " + imagePath); } - return spannable; -} -/** - * 创建清单模式下的列表项视图 - */ -private View getListItem(String item, int index) { - View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null); - 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() { - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - // 选中时添加删除线 - edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - } else { - // 取消选中时恢复正常样式 - edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); + /** + * 显示标签选择对话框 + */ + private void showTagSelectorDialog() { + // 创建标签选择对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.menu_tag); + + // 创建标签选择布局 + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(30, 30, 30, 30); + + // 预设标签选项 + String[] tags = {"生活", "学习", "工作", "自定义"}; + builder.setItems(tags, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case 0: + // 生活标签 + mWorkingNote.setTag(TAG_LIFE); + break; + case 1: + // 学习标签 + mWorkingNote.setTag(TAG_STUDY); + break; + case 2: + // 工作标签 + mWorkingNote.setTag(TAG_WORK); + break; + case 3: + // 自定义标签,显示输入框 + showCustomTagInputDialog(); + break; + } } - } - }); - - // 解析清单标记符号 - if (item.startsWith(TAG_CHECKED)) { - cb.setChecked(true); // 已完成的项 - edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - item = item.substring(TAG_CHECKED.length(), item.length()).trim(); - } else if (item.startsWith(TAG_UNCHECKED)) { - cb.setChecked(false); // 未完成的项 - edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); - item = item.substring(TAG_UNCHECKED.length(), item.length()).trim(); - } - - edit.setOnTextViewChangeListener(this); - edit.setIndex(index); - edit.setText(getHighlightQueryResult(item, mUserQuery)); - return view; -} + }); -/** - * 文本变化回调(OnTextViewChangeListener接口方法) - */ -public void onTextChange(int index, boolean hasText) { - if (index >= mEditTextList.getChildCount()) { - Log.e(TAG, "Wrong index, should not happen"); - return; + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); } - // 根据是否有文本显示或隐藏复选框 - if(hasText) { - mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.VISIBLE); - } else { - mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE); - } -} -/** - * 清单模式改变回调(NoteSettingChangedListener接口方法) - */ -public void onCheckListModeChanged(int oldMode, int newMode) { - if (newMode == TextNote.MODE_CHECK_LIST) { - // 切换到清单模式 - switchToListMode(mNoteEditor.getText().toString()); - - // 隐藏富文本格式工具栏 - if (mFormatToolbar != null) { - mFormatToolbar.setVisibility(View.GONE); - } - } else { - // 切换到普通模式 - if (!getWorkingText()) { - // 清理清单标记符号 - mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", - "")); - } - mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); - mEditTextList.setVisibility(View.GONE); - mNoteEditor.setVisibility(View.VISIBLE); + /** + * 显示自定义标签输入对话框 + */ + private void showCustomTagInputDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("自定义标签"); + + // 创建输入框 + EditText input = new EditText(this); + input.setHint("输入标签名称"); + builder.setView(input); + + builder.setPositiveButton("保存", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + String customTag = input.getText().toString().trim(); + if (!customTag.isEmpty()) { + mWorkingNote.setTag(customTag); + } + } + }); - // 显示富文本格式工具栏 - if (mFormatToolbar != null) { - mFormatToolbar.setVisibility(View.VISIBLE); - } + builder.setNegativeButton("取消", null); + builder.show(); } -} - -/** - * 从UI获取当前编辑的文本内容 - * @return 是否存在已完成的项 - */ -boolean getWorkingText() { - boolean hasChecked = false; - if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { - // 清单模式:构建格式化的文本 - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < mEditTextList.getChildCount(); i++) { - View view = mEditTextList.getChildAt(i); - NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); - if (!TextUtils.isEmpty(edit.getText())) { - if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) { - // 已完成的项 - sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n"); - hasChecked = true; + + /** + * 设置位置提醒 + */ + private void setLocationReminder() { + // 创建位置提醒对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.location_reminder_dialog_title); + + // 创建布局 + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(50, 30, 50, 30); + + // 位置名称输入框 + mLocationNameEditText = new EditText(this); + mLocationNameEditText.setHint(R.string.location_reminder_enter_name); + LinearLayout.LayoutParams nameParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + nameParams.setMargins(0, 0, 0, 20); + mLocationNameEditText.setLayoutParams(nameParams); + layout.addView(mLocationNameEditText); + + // 纬度输入框 + mLatitudeEditText = new EditText(this); + mLatitudeEditText.setHint("纬度 (Latitude)"); + mLatitudeEditText.setInputType(InputType.TYPE_NUMBER_FLAG_DECIMAL | InputType.TYPE_NUMBER_FLAG_SIGNED); + LinearLayout.LayoutParams latParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + latParams.setMargins(0, 0, 0, 20); + mLatitudeEditText.setLayoutParams(latParams); + layout.addView(mLatitudeEditText); + + // 经度输入框 + mLongitudeEditText = new EditText(this); + mLongitudeEditText.setHint("经度 (Longitude)"); + mLongitudeEditText.setInputType(InputType.TYPE_NUMBER_FLAG_DECIMAL | InputType.TYPE_NUMBER_FLAG_SIGNED); + LinearLayout.LayoutParams lonParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + lonParams.setMargins(0, 0, 0, 20); + mLongitudeEditText.setLayoutParams(lonParams); + layout.addView(mLongitudeEditText); + + // 选择位置按钮 + final Button selectLocationButton = new Button(this); + selectLocationButton.setText(R.string.location_reminder_select_location); + LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + buttonParams.setMargins(0, 0, 0, 20); + selectLocationButton.setLayoutParams(buttonParams); + layout.addView(selectLocationButton); + + // 提醒半径输入框 + final EditText radiusEditText = new EditText(this); + radiusEditText.setHint(R.string.location_reminder_radius); + radiusEditText.setInputType(InputType.TYPE_CLASS_NUMBER); + radiusEditText.setText("100"); // 默认100米 + LinearLayout.LayoutParams radiusParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + radiusEditText.setLayoutParams(radiusParams); + layout.addView(radiusEditText); + + // 选择位置按钮点击事件 + selectLocationButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + // 调用地图应用选择位置 + Intent intent = new Intent(Intent.ACTION_VIEW); + + // 尝试不同的URI格式 + Uri geoUri = Uri.parse("geo:0,0"); + intent.setData(geoUri); + + // 不限制特定地图应用,让系统选择默认的地图应用 + intent.setPackage(null); + + if (intent.resolveActivity(getPackageManager()) != null) { + startActivityForResult(intent, REQUEST_CODE_SELECT_LOCATION); } else { - // 未完成的项 - sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); + // 如果第一种格式失败,尝试另一种格式 + intent.setData(Uri.parse("geo:0,0?q=map")); + if (intent.resolveActivity(getPackageManager()) != null) { + startActivityForResult(intent, REQUEST_CODE_SELECT_LOCATION); + } else { + // 如果还是失败,尝试更通用的方式 + Intent chooserIntent = Intent.createChooser(intent, "选择地图应用"); + if (chooserIntent.resolveActivity(getPackageManager()) != null) { + startActivityForResult(chooserIntent, REQUEST_CODE_SELECT_LOCATION); + } else { + Toast.makeText(NoteEditActivity.this, "没有找到地图应用", Toast.LENGTH_SHORT).show(); + } + } } } - } - mWorkingNote.setWorkingText(sb.toString()); - } else { - // 普通模式:获取带格式的文本 - mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); - - // 保存格式化信息到工作文本 - String formatInfo = RichTextFormatUtils.serializeFormatInfo(mNoteEditor.getText()); - mWorkingNote.setRichTextFormat(formatInfo); - - // TODO: 保存图片信息到工作笔记 - 这需要更复杂的数据结构来存储图片位置和路径 - // 当前实现会在普通文本模式中保留图片,但在清单模式中会丢失图片 + }); + + builder.setView(layout); + + // 确定按钮 + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + String locationName = mLocationNameEditText.getText().toString().trim(); + double latitude = Double.parseDouble(mLatitudeEditText.getText().toString()); + double longitude = Double.parseDouble(mLongitudeEditText.getText().toString()); + float radius = Float.parseFloat(radiusEditText.getText().toString()); + + // 验证输入 + if (TextUtils.isEmpty(locationName)) { + locationName = "未知位置"; + } + if (radius <= 0) { + radius = 100; // 默认100米 + } + + // 设置位置提醒 + mWorkingNote.setAlertLocation(latitude, longitude, radius, locationName, true); + } catch (NumberFormatException e) { + Toast.makeText(NoteEditActivity.this, "请输入有效的位置信息", Toast.LENGTH_SHORT).show(); + } + } + }); + + // 取消按钮 + builder.setNegativeButton(android.R.string.cancel, null); + + // 显示对话框 + builder.show(); } - return hasChecked; -} -/** - * 保存笔记到数据库 - * @return 保存是否成功 - */ -private boolean saveNote() { - getWorkingText(); // 从UI获取最新文本 - boolean saved = mWorkingNote.saveNote(); // 保存到数据库 - if (saved) { - setResult(RESULT_OK); // 设置活动结果 + /** + * 分享笔记内容到其他应用 + */ + private void sendTo(Context context, String info) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, info); // 设置分享文本 + intent.setType("text/plain"); // 设置MIME类型 + context.startActivity(intent); // 启动分享选择器 } - return saved; -} -/** - * 发送笔记到桌面快捷方式 - */ -private void sendToDesktop() { - // 确保笔记已保存到数据库 - if (!mWorkingNote.existInDatabase()) { + /** + * 创建新笔记 + */ + private void createNewNote() { + // 先保存当前编辑的笔记 saveNote(); - } - - if (mWorkingNote.getNoteId() > 0) { - Intent sender = new Intent(); - // 创建快捷方式意图 - Intent shortcutIntent = new Intent(this, NoteEditActivity.class); - shortcutIntent.setAction(Intent.ACTION_VIEW); - shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId()); - sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); - sender.putExtra(Intent.EXTRA_SHORTCUT_NAME, - makeShortcutIconTitle(mWorkingNote.getContent())); - sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, - Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app)); - sender.putExtra("duplicate", true); // 允许重复创建 - sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); - - showToast(R.string.info_note_enter_desktop); - sendBroadcast(sender); - } else { - // 笔记为空,无法创建快捷方式 - Log.e(TAG, "Send to desktop error"); - showToast(R.string.error_note_empty_for_send_to_desktop); + // 结束当前活动并启动新的编辑活动 + finish(); + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId()); + startActivity(intent); } -} -/** - * 生成桌面快捷方式的标题 - */ -private String makeShortcutIconTitle(String content) { - // 清理清单标记符号 - content = content.replace(TAG_CHECKED, ""); - content = content.replace(TAG_UNCHECKED, ""); - // 截取标题长度 - return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0, - SHORTCUT_ICON_TITLE_MAX_LEN) : content; -} + /** + * 删除当前笔记 + */ + private void deleteCurrentNote() { + if (mWorkingNote.existInDatabase()) { + HashSet ids = new HashSet(); + long id = mWorkingNote.getNoteId(); + if (id != Notes.ID_ROOT_FOLDER) { + ids.add(id); + } else { + Log.d(TAG, "Wrong note id, should not happen"); + } -/** - * 处理富文本格式化 - * @param v 点击的按钮 - */ -private void handleRichTextFormatting(View v) { - if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { - // 清单模式下暂不支持富文本编辑 - Toast.makeText(this, "Rich text formatting is not supported in checklist mode", Toast.LENGTH_SHORT).show(); - return; + // 根据同步模式选择删除方式 + if (!isSyncMode()) { + // 非同步模式:直接删除 + if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) { + Log.e(TAG, "Delete Note error"); + } + } else { + // 同步模式:移动到垃圾箱 + if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLDER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + } + mWorkingNote.markDeleted(true); // 标记为已删除 } - int selectionStart = mNoteEditor.getSelectionStart(); - int selectionEnd = mNoteEditor.getSelectionEnd(); - - // 确保有选中的文本 - if (selectionStart >= 0 && selectionEnd >= 0 && selectionStart != selectionEnd) { - Spannable str = mNoteEditor.getText(); - - switch (v.getId()) { - case R.id.btn_bold: - toggleStyleSpan(str, selectionStart, selectionEnd, android.graphics.Typeface.BOLD); - break; - case R.id.btn_italic: - toggleStyleSpan(str, selectionStart, selectionEnd, android.graphics.Typeface.ITALIC); - break; - case R.id.btn_underline: - toggleUnderlineSpan(str, selectionStart, selectionEnd); - break; - case R.id.btn_strikethrough: - toggleStrikethroughSpan(str, selectionStart, selectionEnd); - break; - } - } else { - Toast.makeText(this, "Please select text first", Toast.LENGTH_SHORT).show(); + /** + * 检查是否处于同步模式 + */ + private boolean isSyncMode() { + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; } -} -/** - * 显示图片选择对话框 - */ -private void showImageSelectionDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("选择图片"); // 标题 - - // 创建垂直排列的按钮布局 - LinearLayout layout = new LinearLayout(this); - layout.setOrientation(LinearLayout.VERTICAL); - layout.setPadding(50, 30, 50, 30); - - // 相册按钮 - Button albumBtn = new Button(this); - albumBtn.setText("相册"); - albumBtn.setAllCaps(false); // 不要大写 - albumBtn.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - openGallery(); - } - }); - - // 系统图片按钮 - Button systemBtn = new Button(this); - systemBtn.setText("系统图片"); - systemBtn.setAllCaps(false); - systemBtn.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - showSystemImagesDialog(); + /** + * 提醒时间改变回调(NoteSettingChangedListener接口方法) + */ + public void onClockAlertChanged(long date, boolean set) { + // 对于未保存的笔记,先保存再设置提醒 + if (!mWorkingNote.existInDatabase()) { + saveNote(); } - }); - // 添加按钮到布局 - layout.addView(albumBtn); - layout.addView(systemBtn); - - builder.setView(layout); - builder.setNegativeButton("取消", null); - builder.show(); -} - -/** - * 显示系统图片选择对话框 - */ -private void showSystemImagesDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("选择系统图片"); - - // 创建水平排列的图片布局 - LinearLayout layout = new LinearLayout(this); - layout.setOrientation(LinearLayout.HORIZONTAL); - layout.setPadding(20, 20, 20, 20); - - // 创建猫图片 - ImageView catImageView = new ImageView(this); - try { - // 检查资源是否存在 - catImageView.setImageResource(R.drawable.cat); // 引用res/drawable-hdpi/cat.png - } catch (Exception e) { - // 如果资源不存在,显示占位符或提示信息 - catImageView.setImageResource(android.R.drawable.ic_menu_gallery); // 使用系统默认图标作为占位符 - Toast.makeText(this, "cat.png 图片资源未找到", Toast.LENGTH_SHORT).show(); - } - catImageView.setLayoutParams(new LinearLayout.LayoutParams( - 300, 300, 1.0f)); // 设置尺寸和权重 - catImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); - catImageView.setPadding(10, 10, 10, 10); - catImageView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // 使用猫图片的资源ID创建Uri - try { - Uri catUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.drawable.cat); - insertImageToEditor(catUri); - } catch (Exception e) { - Toast.makeText(NoteEditActivity.this, "无法加载cat.png图片", Toast.LENGTH_SHORT).show(); + if (mWorkingNote.getNoteId() > 0) { + // 创建闹钟意图 + Intent intent = new Intent(this, AlarmReceiver.class); + intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId())); + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); + AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE)); + + showAlertHeader(); // 更新提醒显示 + if(!set) { + // 取消提醒 + alarmManager.cancel(pendingIntent); + } else { + // 设置提醒 + alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); } + } else { + // 笔记为空,无法设置提醒 + Log.e(TAG, "Clock alert setting error"); + showToast(R.string.error_note_empty_for_clock); } - }); - - // 创建狗图片 - ImageView dogImageView = new ImageView(this); - try { - // 检查资源是否存在 - dogImageView.setImageResource(R.drawable.dog); // 引用res/drawable-hdpi/dog.png - } catch (Exception e) { - // 如果资源不存在,显示占位符或提示信息 - dogImageView.setImageResource(android.R.drawable.ic_menu_gallery); // 使用系统默认图标作为占位符 - Toast.makeText(this, "dog.png 图片资源未找到", Toast.LENGTH_SHORT).show(); - } - dogImageView.setLayoutParams(new LinearLayout.LayoutParams( - 300, 300, 1.0f)); // 设置尺寸和权重 - dogImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); - dogImageView.setPadding(10, 10, 10, 10); - dogImageView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // 使用狗图片的资源ID创建Uri - try { - Uri dogUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.drawable.dog); - insertImageToEditor(dogUri); - } catch (Exception e) { - Toast.makeText(NoteEditActivity.this, "无法加载dog.png图片", Toast.LENGTH_SHORT).show(); + } + + /** + * 位置提醒改变回调(NoteSettingChangedListener接口方法) + */ + public void onLocationAlertChanged(double latitude, double longitude, float radius, String locationName, boolean set) { + // 对于未保存的笔记,先保存再设置提醒 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + + if (mWorkingNote.getNoteId() > 0) { + showAlertHeader(); // 更新提醒显示 + // 位置提醒的地理围栏实现将在后续版本中添加 + if (set) { + Toast.makeText(this, "位置提醒已设置", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "位置提醒已取消", Toast.LENGTH_SHORT).show(); } + } else { + // 笔记为空,无法设置提醒 + Log.e(TAG, "Location alert setting error"); + showToast(R.string.error_note_empty_for_clock); } - }); - - // 添加图片到布局 - layout.addView(catImageView); - layout.addView(dogImageView); - - builder.setView(layout); - builder.setNegativeButton("取消", null); - builder.show(); -} - -/** - * 打开相册选择图片 - */ -private void openGallery() { - Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); - intent.setType("image/*"); - if (intent.resolveActivity(getPackageManager()) != null) { - startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE); - } else { - Toast.makeText(this, "设备不支持图片选择功能", Toast.LENGTH_SHORT).show(); } -} -/** - * 打开系统图片选择器 - */ -private void openSystemImagePicker() { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("image/*"); - if (intent.resolveActivity(getPackageManager()) != null) { - startActivityForResult(intent, REQUEST_CODE_CHOOSE_IMAGE); - } else { - Toast.makeText(this, "设备不支持图片选择功能", Toast.LENGTH_SHORT).show(); + /** + * 小部件改变回调(NoteSettingChangedListener接口方法) + */ + public void onWidgetChanged() { + updateWidget(); // 更新关联的小部件 } -} -/** - * 切换样式(粗体、斜体) - */ -private void toggleStyleSpan(Spannable str, int start, int end, int style) { - // 检查选中区域内是否已有相同的样式 - StyleSpan[] spans = str.getSpans(start, end, StyleSpan.class); - boolean hasStyle = false; - - for (StyleSpan span : spans) { - if (span.getStyle() == style) { - str.removeSpan(span); - hasStyle = true; + /** + * 清单模式下删除编辑项(OnTextViewChangeListener接口方法) + */ + public void onEditTextDelete(int index, String text) { + int childCount = mEditTextList.getChildCount(); + if (childCount == 1) { + return; // 只有一个项时不删除 } - } - - // 如果没有对应样式,则添加 - if (!hasStyle) { - str.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } -} -/** - * 切换下划线 - */ -private void toggleUnderlineSpan(Spannable str, int start, int end) { - UnderlineSpan[] spans = str.getSpans(start, end, UnderlineSpan.class); + // 更新后面所有项的索引 + for (int i = index + 1; i < childCount; i++) { + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i - 1); + } - if (spans.length > 0) { - // 如果已存在下划线,则移除 - for (UnderlineSpan span : spans) { - str.removeSpan(span); + // 移除指定位置的视图 + mEditTextList.removeViewAt(index); + NoteEditText edit = null; + // 决定焦点给哪个编辑框 + if(index == 0) { + edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById(R.id.et_edit_text); + } else { + edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById(R.id.et_edit_text); } - } else { - // 如果不存在下划线,则添加 - str.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + // 将删除的文本追加到获得焦点的编辑框 + int length = edit.length(); + edit.append(text); + edit.requestFocus(); + edit.setSelection(length); } -} -/** - * 切换删除线 - */ -private void toggleStrikethroughSpan(Spannable str, int start, int end) { - StrikethroughSpan[] spans = str.getSpans(start, end, StrikethroughSpan.class); + /** + * 清单模式下插入新编辑项(OnTextViewChangeListener接口方法) + */ + public void onEditTextEnter(int index, String text) { + // 安全检查 + if(index > mEditTextList.getChildCount()) { + Log.e(TAG, "Index out of mEditTextList boundrary, should not happen"); + } - if (spans.length > 0) { - // 如果已存在删除线,则移除 - for (StrikethroughSpan span : spans) { - str.removeSpan(span); + // 创建新列表项并插入到指定位置 + View view = getListItem(text, index); + mEditTextList.addView(view, index); + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + edit.requestFocus(); + edit.setSelection(0); + + // 更新后面所有项的索引 + for (int i = index + 1; i < mEditTextList.getChildCount(); i++) { + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i); } - } else { - // 如果不存在删除线,则添加 - str.setSpan(new StrikethroughSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } -} - -/** - * 显示Toast提示 - */ -private void showToast(int resId) { - showToast(resId, Toast.LENGTH_SHORT); -} -private void showToast(int resId, int duration) { - Toast.makeText(this, resId, duration).show(); -} + /** + * 切换到清单模式 + */ + private void switchToListMode(String text) { + mEditTextList.removeAllViews(); // 清空现有视图 + String[] items = text.split("\n"); // 按换行符分割文本 + int index = 0; + for (String item : items) { + if(!TextUtils.isEmpty(item)) { + // 为每个非空段落创建列表项 + mEditTextList.addView(getListItem(item, index)); + index++; + } + } + // 添加一个空的列表项用于输入新内容 + mEditTextList.addView(getListItem("", index)); + mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus(); -/** - * 处理编辑器中的按键事件,包括删除图片 - */ -private void setupImageDeletionHandling() { - mNoteEditor.setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, android.view.KeyEvent event) { - if (event.getAction() == android.view.KeyEvent.ACTION_DOWN && - keyCode == android.view.KeyEvent.KEYCODE_DEL) { - - // 检查光标前是否是图片 - int cursorPos = mNoteEditor.getSelectionStart(); - if (cursorPos > 0) { - // 这里可以添加删除图片的逻辑 - // 检查当前位置是否有图片Span - android.text.style.ImageSpan[] imageSpans = - mNoteEditor.getText().getSpans(cursorPos - 1, cursorPos, - android.text.style.ImageSpan.class); - - if (imageSpans.length > 0) { - // 删除图片Span - for (android.text.style.ImageSpan span : imageSpans) { - int start = mNoteEditor.getText().getSpanStart(span); - int end = mNoteEditor.getText().getSpanEnd(span); - - // 移除图片Span - mNoteEditor.getText().removeSpan(span); - - // 删除图片占位符文本 - mNoteEditor.getText().delete(start, end); - } + // 切换显示模式 + mNoteEditor.setVisibility(View.GONE); + mEditTextList.setVisibility(View.VISIBLE); + } - return true; // 消费此事件 - } - } + /** + * 高亮显示搜索关键词 + */ + private Spannable getHighlightQueryResult(String fullText, String userQuery) { + SpannableString spannable = new SpannableString(fullText == null ? "" : fullText); + if (!TextUtils.isEmpty(userQuery)) { + mPattern = Pattern.compile(userQuery); + Matcher m = mPattern.matcher(fullText); + int start = 0; + // 为所有匹配的文本设置背景色 + while (m.find(start)) { + spannable.setSpan( + new BackgroundColorSpan(this.getResources().getColor( + R.color.user_query_highlight)), m.start(), m.end(), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + start = m.end(); } - return false; } - }); -} - + return spannable; + } + /** - * 显示解锁对话框 + * 创建清单模式下的列表项视图 */ - private void showUnlockDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_unlock, null); - builder.setView(dialogView); - - TextView tvTitle = dialogView.findViewById(R.id.tv_unlock_title); - TextView tvInstruction = dialogView.findViewById(R.id.tv_unlock_instruction); - Button btnUnlockPassword = dialogView.findViewById(R.id.btn_unlock_password); - Button btnUnlockGesture = dialogView.findViewById(R.id.btn_unlock_gesture); - TextView tvAttempts = dialogView.findViewById(R.id.tv_unlock_attempts); - TextView tvError = dialogView.findViewById(R.id.tv_unlock_error); - - tvTitle.setText("解锁"); - tvInstruction.setText("便签已被锁定,请解锁"); - - // 更新尝试次数显示 - tvAttempts.setText("尝试次数: " + mUnlockAttempts + "/" + MAX_UNLOCK_ATTEMPTS); - - AlertDialog dialog = builder.create(); - - // 密码解锁按钮 - btnUnlockPassword.setOnClickListener(v -> { - showPasswordUnlockDialog(dialog); + private View getListItem(String item, int index) { + View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null); + 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() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + // 选中时添加删除线 + edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } else { + // 取消选中时恢复正常样式 + edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); + } + } }); - // 手势解锁按钮 - btnUnlockGesture.setOnClickListener(v -> { - showGestureUnlockDialog(dialog); - }); + // 解析清单标记符号 + if (item.startsWith(TAG_CHECKED)) { + cb.setChecked(true); // 已完成的项 + edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + item = item.substring(TAG_CHECKED.length(), item.length()).trim(); + } else if (item.startsWith(TAG_UNCHECKED)) { + cb.setChecked(false); // 未完成的项 + edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); + item = item.substring(TAG_UNCHECKED.length(), item.length()).trim(); + } - dialog.show(); + edit.setOnTextViewChangeListener(this); + edit.setIndex(index); + edit.setText(getHighlightQueryResult(item, mUserQuery)); + return view; } /** - * 显示密码解锁对话框 + * 文本变化回调(OnTextViewChangeListener接口方法) */ - private void showPasswordUnlockDialog(AlertDialog parentDialog) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_password_input, null); - builder.setView(dialogView); - - TextView tvTitle = dialogView.findViewById(R.id.tv_password_title); - EditText etPassword = dialogView.findViewById(R.id.et_password); - EditText etConfirmPassword = dialogView.findViewById(R.id.et_confirm_password); - CheckBox cbShowPassword = dialogView.findViewById(R.id.cb_show_password); - TextView tvHint = dialogView.findViewById(R.id.tv_password_hint); - TextView tvErrorMessage = dialogView.findViewById(R.id.tv_error_message); - Button btnCancel = dialogView.findViewById(R.id.btn_cancel); - Button btnConfirm = dialogView.findViewById(R.id.btn_confirm); - - // 隐藏确认密码框和相关元素,因为我们只需要输入一次密码 - etConfirmPassword.setVisibility(View.GONE); - tvHint.setText("输入解锁密码"); - tvTitle.setText("密码解锁"); - - AlertDialog dialog = builder.create(); - - // 显示/隐藏密码 - cbShowPassword.setOnCheckedChangeListener((buttonView, isChecked) -> { - int inputType = isChecked ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD - : InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD; - etPassword.setInputType(inputType); - etPassword.setSelection(etPassword.getText().length()); - }); + public void onTextChange(int index, boolean hasText) { + if (index >= mEditTextList.getChildCount()) { + Log.e(TAG, "Wrong index, should not happen"); + return; + } + // 根据是否有文本显示或隐藏复选框 + if(hasText) { + mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.VISIBLE); + } else { + mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE); + } + } - // 确认按钮点击 - btnConfirm.setOnClickListener(v -> { - String password = etPassword.getText().toString().trim(); + /** + * 清单模式改变回调(NoteSettingChangedListener接口方法) + */ + public void onCheckListModeChanged(int oldMode, int newMode) { + if (newMode == TextNote.MODE_CHECK_LIST) { + // 切换到清单模式 + switchToListMode(mNoteEditor.getText().toString()); + } else { + // 切换到普通模式 + if (!getWorkingText()) { + // 清理清单标记符号 + mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", + "")); + } + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mEditTextList.setVisibility(View.GONE); + mNoteEditor.setVisibility(View.VISIBLE); + } + } - if (mPrivacyLockManager.verifyPassword(mWorkingNote.getNoteId(), password)) { - // 密码正确,解锁成功 - dialog.dismiss(); - parentDialog.dismiss(); - - // 重新初始化界面 - initResources(); - initNoteScreen(); - } else { - // 密码错误 - mUnlockAttempts++; - if (mUnlockAttempts >= MAX_UNLOCK_ATTEMPTS) { - Toast.makeText(this, "解锁失败次数过多,无法继续解锁", Toast.LENGTH_SHORT).show(); - finish(); // 关闭活动 - return; + /** + * 从UI获取当前编辑的文本内容 + * @return 是否存在已完成的项 + */ + private boolean getWorkingText() { + boolean hasChecked = false; + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 清单模式:构建格式化的文本 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mEditTextList.getChildCount(); i++) { + View view = mEditTextList.getChildAt(i); + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + if (!TextUtils.isEmpty(edit.getText())) { + if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) { + // 已完成的项 + sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n"); + hasChecked = true; + } else { + // 未完成的项 + sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); + } } - - tvErrorMessage.setText("密码错误 (" + mUnlockAttempts + "/" + MAX_UNLOCK_ATTEMPTS + ")"); - tvErrorMessage.setVisibility(View.VISIBLE); - etPassword.setText(""); // 清空密码框 } - }); - - // 取消按钮点击 - btnCancel.setOnClickListener(v -> dialog.dismiss()); + mWorkingNote.setWorkingText(sb.toString()); + } else { + // 普通模式:合并标题和正文 + String title = mNoteTitle.getText().toString(); + String content = mNoteEditor.getText().toString(); + mWorkingNote.setTitleAndContent(title, content); + } + return hasChecked; + } - dialog.show(); + /** + * 保存笔记到数据库 + * @return 保存是否成功 + */ + private boolean saveNote() { + getWorkingText(); // 从UI获取最新文本 + boolean saved = mWorkingNote.saveNote(); // 保存到数据库 + if (saved) { + setResult(RESULT_OK); // 设置活动结果 + } + return saved; } /** - * 显示手势解锁对话框 + * 发送笔记到桌面快捷方式 */ - private void showGestureUnlockDialog(AlertDialog parentDialog) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_gesture_lock, null); - builder.setView(dialogView); - - TextView tvTitle = dialogView.findViewById(R.id.tv_gesture_title); - TextView tvInstruction = dialogView.findViewById(R.id.tv_gesture_instruction); - GestureLockView gestureLockView = dialogView.findViewById(R.id.gesture_lock_view); - TextView tvStatus = dialogView.findViewById(R.id.tv_gesture_status); - Button btnReset = dialogView.findViewById(R.id.btn_gesture_reset); - - tvTitle.setText("手势解锁"); - tvInstruction.setText("请绘制解锁手势"); - tvStatus.setText(""); - - AlertDialog dialog = builder.create(); - - // 获取存储的手势锁类型 - int lockType = mPrivacyLockManager.getLockType(mWorkingNote.getNoteId()); - if (lockType != PrivacyLockManager.LOCK_TYPE_GESTURE) { - Toast.makeText(this, "该便签不是手势锁", Toast.LENGTH_SHORT).show(); - dialog.dismiss(); - return; + private void sendToDesktop() { + // 确保笔记已保存到数据库 + if (!mWorkingNote.existInDatabase()) { + saveNote(); } - // 设置手势完成监听器 - gestureLockView.setOnGestureCompleteListener(selectedPoints -> { - // 验证手势 - if (mPrivacyLockManager.verifyGesture(mWorkingNote.getNoteId(), selectedPoints)) { - // 手势正确,解锁成功 - dialog.dismiss(); - parentDialog.dismiss(); - - // 重新初始化界面 - initResources(); - initNoteScreen(); - } else { - // 手势错误 - mUnlockAttempts++; - if (mUnlockAttempts >= MAX_UNLOCK_ATTEMPTS) { - Toast.makeText(this, "解锁失败次数过多,无法继续解锁", Toast.LENGTH_SHORT).show(); - finish(); // 关闭活动 - return; - } + if (mWorkingNote.getNoteId() > 0) { + Intent sender = new Intent(); + // 创建快捷方式意图 + Intent shortcutIntent = new Intent(this, NoteEditActivity.class); + shortcutIntent.setAction(Intent.ACTION_VIEW); + shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + + sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); + sender.putExtra(Intent.EXTRA_SHORTCUT_NAME, + makeShortcutIconTitle(mWorkingNote.getContent())); + sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, + Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app)); + sender.putExtra("duplicate", true); // 允许重复创建 + sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); + + showToast(R.string.info_note_enter_desktop); + sendBroadcast(sender); + } else { + // 笔记为空,无法创建快捷方式 + Log.e(TAG, "Send to desktop error"); + showToast(R.string.error_note_empty_for_send_to_desktop); + } + } - tvStatus.setText("验证失败 (" + mUnlockAttempts + "/" + MAX_UNLOCK_ATTEMPTS + ")"); - gestureLockView.clearGesture(); - } - }); + /** + * 生成桌面快捷方式的标题 + */ + private String makeShortcutIconTitle(String content) { + // 清理清单标记符号 + content = content.replace(TAG_CHECKED, ""); + content = content.replace(TAG_UNCHECKED, ""); + // 截取标题长度 + return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0, + SHORTCUT_ICON_TITLE_MAX_LEN) : content; + } - // 重置按钮点击 - btnReset.setOnClickListener(v -> { - gestureLockView.clearGesture(); - tvStatus.setText(""); - }); + /** + * 显示Toast提示 + */ + private void showToast(int resId) { + showToast(resId, Toast.LENGTH_SHORT); + } - dialog.show(); + private void showToast(int resId, int duration) { + Toast.makeText(this, resId, duration).show(); } } \ No newline at end of file diff --git a/src/notes/ui/NoteEditText.java b/src/notes/ui/NoteEditText.java index d27fdda..8122993 100644 --- a/src/notes/ui/NoteEditText.java +++ b/src/notes/ui/NoteEditText.java @@ -41,7 +41,7 @@ import java.util.Map; * 自定义编辑文本框,用于笔记应用的清单模式 * 支持特殊的删除和回车逻辑,以及链接处理功能 */ -public class NoteEditText extends androidx.appcompat.widget.AppCompatEditText { +public class NoteEditText extends EditText { private static final String TAG = "NoteEditText"; private int mIndex; // 当前编辑框在清单列表中的索引位置 private int mSelectionStartBeforeDelete; // 记录删除操作前光标起始位置 @@ -64,8 +64,6 @@ public class NoteEditText extends androidx.appcompat.widget.AppCompatEditText { * 由NoteEditActivity实现,用于处理清单模式下的特殊编辑逻辑 */ public interface OnTextViewChangeListener { - void onBackPressedDispatcher(); - /** * 当在行首按删除键且文本为空时,删除当前编辑框 * @param index 被删除的编辑框索引 diff --git a/src/notes/ui/NoteItemData.java b/src/notes/ui/NoteItemData.java index c1601e9..513a3e2 100644 --- a/src/notes/ui/NoteItemData.java +++ b/src/notes/ui/NoteItemData.java @@ -48,7 +48,8 @@ public class NoteItemData { NoteColumns.SNIPPET, // 内容摘要/片段 NoteColumns.TYPE, // 类型(笔记、文件夹、系统文件夹等) NoteColumns.WIDGET_ID, // 关联的小部件ID - NoteColumns.WIDGET_TYPE, // 小部件类型 + NoteColumns.WIDGET_TYPE, // 小部件的类型 + NoteColumns.TAG, // 标签 }; /** @@ -67,6 +68,7 @@ public class NoteItemData { private static final int TYPE_COLUMN = 9; // 类型列的索引 private static final int WIDGET_ID_COLUMN = 10; // 小部件ID列的索引 private static final int WIDGET_TYPE_COLUMN = 11; // 小部件类型列的索引 + private static final int TAG_COLUMN = 12; // 标签列的索引 // 成员变量,对应数据库中的各个字段 private long mId; // 笔记/文件夹的唯一标识ID @@ -81,6 +83,8 @@ public class NoteItemData { private int mType; // 类型(笔记、文件夹、系统文件夹等) private int mWidgetId; // 关联的桌面小部件ID private int mWidgetType; // 桌面小部件的类型 + private String mTag; // 标签 + private boolean mIsCompleted; // 便签是否已完成 // 额外添加的业务逻辑相关字段(不直接来自数据库) private String mName; // 联系人姓名(如果是通话记录笔记) @@ -110,6 +114,9 @@ public class NoteItemData { mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); mParentId = cursor.getLong(PARENT_ID_COLUMN); mSnippet = cursor.getString(SNIPPET_COLUMN); + mTag = cursor.getString(TAG_COLUMN); + // 保存原始的完成状态 + mIsCompleted = mSnippet.startsWith(NoteEditActivity.TAG_CHECKED); // 清理摘要文本中的清单标记符号(对勾和方框),让显示更干净 mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( NoteEditActivity.TAG_UNCHECKED, ""); @@ -331,8 +338,40 @@ public class NoteItemData { } /** - * 获取内容摘要/片段(已清理清单标记) - * @return 清理后的内容摘要 + * 获取笔记标题(第一行) + * @return 标题 + */ + public String getTitle() { + if (TextUtils.isEmpty(mSnippet)) { + return ""; + } + int firstLineEnd = mSnippet.indexOf('\n'); + if (firstLineEnd == -1) { + // 如果只有一行,全部作为标题 + return mSnippet; + } + return mSnippet.substring(0, firstLineEnd); + } + + /** + * 获取去掉标题后的内容摘要 + * @return 内容摘要 + */ + public String getContentSnippet() { + if (TextUtils.isEmpty(mSnippet)) { + return ""; + } + int firstLineEnd = mSnippet.indexOf('\n'); + if (firstLineEnd == -1) { + // 如果只有一行,内容为空 + return ""; + } + return mSnippet.substring(firstLineEnd + 1); + } + + /** + * 获取完整内容(已清理清单标记) + * @return 完整内容 */ public String getSnippet() { return mSnippet; @@ -346,6 +385,14 @@ public class NoteItemData { return (mAlertDate > 0); } + /** + * 获取标签 + * @return 标签 + */ + public String getTag() { + return mTag; + } + /** * 判断是否是通话记录笔记 * 条件:属于通话记录文件夹 且 有有效的电话号码 @@ -354,6 +401,14 @@ public class NoteItemData { public boolean isCallRecord() { return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); } + + /** + * 判断便签是否已完成 + * @return 如果便签内容以完成标记开头,返回true + */ + public boolean isCompleted() { + return mIsCompleted; + } /** * 静态方法:直接从游标中获取笔记类型 diff --git a/src/notes/ui/NotesListActivity.java b/src/notes/ui/NotesListActivity.java index 2971194..db8a987 100644 --- a/src/notes/ui/NotesListActivity.java +++ b/src/notes/ui/NotesListActivity.java @@ -28,11 +28,16 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; +import android.net.Uri; +import android.net.Uri.Builder; +import android.content.ContentUris; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import android.os.AsyncTask; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.Editable; -import android.text.InputType; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; @@ -40,7 +45,10 @@ import android.view.ActionMode; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Display; +import android.view.GestureDetector; +import android.view.GestureDetector.SimpleOnGestureListener; import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -49,42 +57,44 @@ import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnCreateContextMenuListener; +import android.view.View.OnKeyListener; import android.view.View.OnTouchListener; +import android.view.VelocityTracker; import android.view.inputmethod.InputMethodManager; +import android.text.InputType; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.Button; -import android.widget.CheckBox; import android.widget.EditText; -import android.widget.LinearLayout; +import android.widget.ImageButton; import android.widget.ListView; import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + import net.micode.notes.R; import net.micode.notes.data.Notes; +import net.micode.notes.tool.SmartReminderManager; import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.data.UserDatabaseHelper; import net.micode.notes.gtask.remote.GTaskSyncService; import net.micode.notes.model.WorkingNote; import net.micode.notes.tool.BackupUtils; import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.PrivacyLockManager; import net.micode.notes.tool.ResourceParser; import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; import net.micode.notes.widget.NoteWidgetProvider_2x; import net.micode.notes.widget.NoteWidgetProvider_4x; -import net.micode.notes.tool.UserManager; -import net.micode.notes.tool.PrivacyLockManager; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashSet; -import java.util.List; -import java.util.ArrayList; /** * 笔记列表主活动,显示笔记和文件夹的列表,是应用的入口界面之一。 @@ -92,6 +102,9 @@ import java.util.ArrayList; */ public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { + // NOTE_ID常量 + public static final String NOTE_ID = "note_id"; + // 异步查询的令牌,用于区分不同类型的查询 private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; // 查询文件夹下的笔记列表 private static final int FOLDER_LIST_QUERY_TOKEN = 1; // 查询可用的文件夹列表(用于移动操作) @@ -126,7 +139,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt private long mCurrentFolderId; // 当前显示的文件夹ID private ContentResolver mContentResolver; // 内容解析器 private ModeCallback mModeCallBack; // 多选模式的回调(动作模式) - private PrivacyLockManager mPrivacyLockManager; // 隐私锁管理器 private static final String TAG = "NotesListActivity"; // 日志标签 @@ -134,6 +146,17 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; private NoteItemData mFocusNoteDataItem; // 当前获得焦点的数据项(长按时记录) + private EditText mSearchEditText; // 搜索输入框 + private ImageButton mSearchClearButton; // 搜索清除按钮 + private String mSearchQuery = ""; // 当前搜索关键词 + private String mCurrentTagFilter = ""; // 当前标签筛选 + private SmartReminderManager mSmartReminderManager; // 智能提醒管理器 + + // 手势检测相关成员变量 + private GestureDetector mGestureDetector; // 手势检测器 + private static final int SWIPE_MIN_DISTANCE = 120; // 最小滑动距离 + private static final int SWIPE_MAX_OFF_PATH = 250; // 最大偏离路径 + private static final int SWIPE_THRESHOLD_VELOCITY = 200; // 最小滑动速度 // 数据库查询条件(Selection) private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; // 普通查询:父文件夹ID=? @@ -146,7 +169,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt // 请求码,用于startActivityForResult private final static int REQUEST_CODE_OPEN_NODE = 102; // 打开已有笔记 private final static int REQUEST_CODE_NEW_NODE = 103; // 创建新笔记 - private final static int REQUEST_CODE_LOGOUT = 104; // 退出登录 /** * Activity生命周期方法:创建时调用 @@ -163,6 +185,30 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt * 当用户第一次使用此应用时,插入一个介绍性的笔记 */ setAppInfoFromRawRes(); + + // 添加对OnBackPressedDispatcher的支持,以便处理手势返回 + try { + // 使用反射调用getOnBackPressedDispatcher方法,添加一个空回调 + // 这样可以确保手势返回功能正常工作,同时保持原有代码不变 + Object dispatcher = getClass().getMethod("getOnBackPressedDispatcher").invoke(this); + Class callbackClass = Class.forName("androidx.activity.OnBackPressedCallback"); + Object callback = callbackClass.getConstructor(boolean.class).newInstance(true); + Class callbackInterface = Class.forName("androidx.activity.OnBackPressedCallback$OnBackPressedListener"); + Object listener = java.lang.reflect.Proxy.newProxyInstance( + callbackInterface.getClassLoader(), + new Class[]{callbackInterface}, + (proxy, method, args) -> { + // 调用原有的onBackPressed方法 + onBackPressed(); + return null; + } + ); + callbackClass.getMethod("addOnBackPressedListener", callbackInterface).invoke(callback, listener); + dispatcher.getClass().getMethod("addCallback", Activity.class, callbackClass).invoke(dispatcher, this, callback); + } catch (Exception e) { + // 如果反射调用失败,不影响原有功能 + Log.d(TAG, "OnBackPressedDispatcher not available, using legacy onBackPressed"); + } } /** @@ -255,6 +301,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); // 创建后台查询处理器 mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 初始当前文件夹为根目录 mNotesListView = (ListView) findViewById(R.id.notes_list); // 查找列表视图 + mSmartReminderManager = new SmartReminderManager(this); // 初始化智能提醒管理器 // 为列表添加脚部视图(可能用于显示空白区域或加载更多) mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), null, false); @@ -273,8 +320,38 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mDispatchY = 0; mOriginY = 0; mTitleBar = (TextView) findViewById(R.id.tv_title_bar); // 标题栏 + + // 初始化搜索相关UI元素 + mSearchEditText = (EditText) findViewById(R.id.search_edit_text); + mSearchClearButton = (ImageButton) findViewById(R.id.search_clear_button); + + // 初始化标签筛选按钮 + Button tagFilterButton = (Button) findViewById(R.id.btn_tag_filter); + tagFilterButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + showTagFilterDialog(); + } + }); + + // 设置搜索输入框监听器 + mSearchEditText.addTextChangedListener(new SearchTextWatcher()); + mSearchEditText.setOnKeyListener(new SearchOnKeyListener()); + + // 设置搜索清除按钮监听器 + mSearchClearButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mSearchEditText.setText(""); + clearSearch(); + } + }); + mState = ListEditState.NOTE_LIST; // 初始状态为根目录笔记列表 mModeCallBack = new ModeCallback(); // 创建多选模式回调实例 + + // 初始化手势检测器,支持滑动操作 + initSwipeGestureDetector(); } /** @@ -510,11 +587,173 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt // 根据当前文件夹ID选择查询条件 String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION : NORMAL_SELECTION; + + // 设置查询参数 + String[] selectionArgs = new String[] { String.valueOf(mCurrentFolderId) }; + + // 如果有搜索关键词,添加搜索条件 + if (!TextUtils.isEmpty(mSearchQuery)) { + selection = "(" + selection + ") AND (" + NoteColumns.SNIPPET + " LIKE ?)"; + selectionArgs = new String[] { + selectionArgs[0], + "%" + mSearchQuery + "%" + }; + } + + // 如果有标签筛选,添加标签条件 + if (!TextUtils.isEmpty(mCurrentTagFilter)) { + String tagSelection = NoteColumns.TAG + "=?"; + // 确保标签筛选条件被正确应用到整个查询条件中 + selection = "(" + selection + ") AND " + tagSelection; + String[] newArgs = new String[selectionArgs.length + 1]; + System.arraycopy(selectionArgs, 0, newArgs, 0, selectionArgs.length); + newArgs[selectionArgs.length] = mCurrentTagFilter; + selectionArgs = newArgs; + } + // 执行异步查询 mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, - Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] { - String.valueOf(mCurrentFolderId) // 参数:当前文件夹ID - }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); // 排序:类型降序,修改日期降序 + Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, selectionArgs, + NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); // 排序:类型降序,修改日期降序 + } + + /** + * 搜索文本变化监听器 + */ + private class SearchTextWatcher implements TextWatcher { + @Override + public void afterTextChanged(Editable s) { + mSearchQuery = s.toString().trim(); + mSearchClearButton.setVisibility(TextUtils.isEmpty(mSearchQuery) ? View.GONE : View.VISIBLE); + startAsyncNotesListQuery(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + } + + /** + * 搜索按键监听器 + */ + private class SearchOnKeyListener implements OnKeyListener { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) { + // 隐藏软键盘 + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + return true; + } + return false; + } + } + + /** + * 清除搜索 + */ + private void clearSearch() { + mSearchQuery = ""; + mSearchClearButton.setVisibility(View.GONE); + startAsyncNotesListQuery(); + } + + /** + * 显示标签筛选对话框 + */ + private void showTagFilterDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("选择标签"); + + // 查询数据库获取所有使用过的标签 + List tagList = new ArrayList<>(); + // 预设标签 + List defaultTags = new ArrayList<>(); + defaultTags.add(NoteEditActivity.TAG_LIFE); + defaultTags.add(NoteEditActivity.TAG_STUDY); + defaultTags.add(NoteEditActivity.TAG_WORK); + + // 自定义标签 + List customTags = new ArrayList<>(); + + // 查询所有标签 + Cursor cursor = getContentResolver().query( + Notes.CONTENT_NOTE_URI, + new String[]{Notes.NoteColumns.TAG}, + Notes.NoteColumns.TAG + " IS NOT NULL AND " + Notes.NoteColumns.TAG + " != ''", + null, + null); + + if (cursor != null) { + while (cursor.moveToNext()) { + String tag = cursor.getString(0); + if (tag != null && !tag.isEmpty()) { + tag = tag.trim(); + if (defaultTags.contains(tag)) { + // 预设标签,不重复添加 + continue; + } else if (NoteEditActivity.TAG_CUSTOM.equals(tag)) { + // 旧版自定义标签,跳过 + continue; + } else { + // 自定义标签,添加到列表 + if (!customTags.contains(tag)) { + customTags.add(tag); + } + } + } + } + cursor.close(); + } + + // 排序自定义标签 + Collections.sort(customTags); + + // 构建最终标签列表 + List finalTags = new ArrayList<>(); + List finalTagValues = new ArrayList<>(); + + // 添加"全部"选项 + finalTags.add("全部"); + finalTagValues.add(""); + + // 添加预设标签 + finalTags.add("生活"); + finalTagValues.add(NoteEditActivity.TAG_LIFE); + finalTags.add("学习"); + finalTagValues.add(NoteEditActivity.TAG_STUDY); + finalTags.add("工作"); + finalTagValues.add(NoteEditActivity.TAG_WORK); + + // 添加自定义标签 + for (String customTag : customTags) { + finalTags.add(customTag); + finalTagValues.add(customTag); + } + + // 转换为数组 + final String[] tags = finalTags.toArray(new String[0]); + final String[] tagValues = finalTagValues.toArray(new String[0]); + + builder.setItems(tags, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + // 全部 + mCurrentTagFilter = ""; + ((Button) findViewById(R.id.btn_tag_filter)).setText("全部"); + } else { + // 选择标签 + mCurrentTagFilter = tagValues[which]; + ((Button) findViewById(R.id.btn_tag_filter)).setText(tags[which]); + } + startAsyncNotesListQuery(); + } + }); + + builder.show(); } /** @@ -740,7 +979,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt final AlertDialog.Builder builder = new AlertDialog.Builder(this); // 加载对话框布局 View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); - final EditText etName = (EditText) view.findViewById(R.id.et_folder_name); + final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); showSoftInput(); // 显示软键盘 // 根据是创建还是修改设置不同的初始文本和标题 if (!create) { @@ -830,6 +1069,175 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }); } + /** + * 初始化滑动手势检测器 + */ + private void initSwipeGestureDetector() { + mGestureDetector = new GestureDetector(this, new SwipeGestureListener()); + + // 为ListView设置触摸监听器,将触摸事件传递给手势检测器 + mNotesListView.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + // 将触摸事件传递给手势检测器 + return mGestureDetector.onTouchEvent(event); + } + }); + } + + /** + * 滑动手势监听器,处理各种滑动事件 + */ + private class SwipeGestureListener extends SimpleOnGestureListener { + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + try { + // 检查是否是水平滑动且速度足够 + if (Math.abs(e1.getY() - e2.getY()) > SWIPE_MAX_OFF_PATH) { + return false; // 垂直方向滑动过大,忽略 + } + + // 获取滑动位置对应的项索引 + int position = mNotesListView.pointToPosition((int) e1.getX(), (int) e1.getY()); + if (position < 0 || position >= mNotesListAdapter.getCount()) { + return false; // 位置无效 + } + + // 获取当前项的数据 + Cursor cursor = (Cursor) mNotesListAdapter.getItem(position); + if (cursor == null) { + return false; + } + + NoteItemData noteData = new NoteItemData(NotesListActivity.this, cursor); + + // 左滑:删除笔记 + if (e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) { + showDeleteConfirmation(noteData, position); + return true; + } + // 右滑:标记完成(仅对清单模式有效) + else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) { + toggleNoteCompletion(noteData); + return true; + } + } catch (Exception e) { + Log.e(TAG, "Swipe gesture error: " + e.getMessage()); + } + return false; + } + + @Override + public boolean onDown(MotionEvent e) { + return true; // 必须返回true,否则不会触发后续的手势事件 + } + } + + /** + * 显示删除确认对话框 + */ + private void showDeleteConfirmation(final NoteItemData noteData, final int position) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_note)); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteNote(noteData); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + } + + /** + * 删除单个笔记 + */ + private void deleteNote(NoteItemData noteData) { + HashSet ids = new HashSet(); + ids.add(noteData.getId()); + + if (!isSyncMode()) { + // 非同步模式:直接删除笔记 + if (DataUtils.batchDeleteNotes(mContentResolver, ids)) { + Toast.makeText(this, "笔记已删除", Toast.LENGTH_SHORT).show(); + } else { + Log.e(TAG, "Delete note error"); + } + } else { + // 同步模式:将笔记移动到垃圾箱文件夹 + if (DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLDER)) { + Toast.makeText(this, "笔记已移至垃圾箱", Toast.LENGTH_SHORT).show(); + } else { + Log.e(TAG, "Move note to trash error"); + } + } + + // 更新关联的小部件 + if (noteData.getWidgetId() != AppWidgetManager.INVALID_APPWIDGET_ID && + noteData.getWidgetType() != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(noteData.getWidgetId(), noteData.getWidgetType()); + } + + // 刷新列表 + startAsyncNotesListQuery(); + } + + /** + * 切换便签的完成状态 + * @param noteData 要切换状态的便签数据 + */ + private void toggleNoteCompletion(NoteItemData noteData) { + try { + // 加载便签内容 + WorkingNote workingNote = WorkingNote.load(this, noteData.getId()); + if (workingNote == null) { + Toast.makeText(this, "无法加载便签", Toast.LENGTH_SHORT).show(); + return; + } + + String content = workingNote.getContent(); + if (TextUtils.isEmpty(content)) { + Toast.makeText(this, "便签内容为空", Toast.LENGTH_SHORT).show(); + return; + } + + // 检查当前便签是否已经标记为完成 + if (content.startsWith(NoteEditActivity.TAG_CHECKED)) { + // 已完成状态,切换为未完成 + content = content.replaceFirst(NoteEditActivity.TAG_CHECKED, NoteEditActivity.TAG_UNCHECKED); + Toast.makeText(this, "已标记为未完成", Toast.LENGTH_SHORT).show(); + } else if (content.startsWith(NoteEditActivity.TAG_UNCHECKED)) { + // 未完成状态,切换为已完成 + content = content.replaceFirst(NoteEditActivity.TAG_UNCHECKED, NoteEditActivity.TAG_CHECKED); + Toast.makeText(this, "已标记为完成", Toast.LENGTH_SHORT).show(); + } else { + // 没有状态标记,默认标记为已完成 + content = NoteEditActivity.TAG_CHECKED + content; + Toast.makeText(this, "已标记为完成", Toast.LENGTH_SHORT).show(); + } + + // 更新便签内容 + workingNote.setWorkingText(content); + if (workingNote.saveNote()) { + // 刷新列表 + startAsyncNotesListQuery(); + + // 更新关联的小部件 + if (noteData.getWidgetId() != AppWidgetManager.INVALID_APPWIDGET_ID && + noteData.getWidgetType() != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(noteData.getWidgetId(), noteData.getWidgetType()); + } + } else { + Toast.makeText(this, "保存便签失败", Toast.LENGTH_SHORT).show(); + } + } catch (Exception e) { + Log.e(TAG, "Toggle completion error: " + e.getMessage()); + Toast.makeText(this, "操作失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + /** * 处理返回键按下事件 */ @@ -859,7 +1267,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt break; } } - + /** * 更新桌面小部件 * @param appWidgetId 小部件ID @@ -920,6 +1328,37 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt Log.e(TAG, "The long click data item is null"); return false; } + + // 处理便签的上下文菜单 + if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE) { + switch (item.getItemId()) { + case R.id.edit: + editNote(mFocusNoteDataItem.getId()); + return true; + case R.id.share: + shareNote(mFocusNoteDataItem.getId()); + return true; + case R.id.copy: + copyNote(mFocusNoteDataItem.getId()); + return true; + case R.id.move: + // 原有的移动逻辑 + break; + case R.id.add_lock: + showAddLockDialog(mFocusNoteDataItem.getId()); + return true; + case R.id.remove_lock: + showRemoveLockDialog(mFocusNoteDataItem.getId()); + return true; + case R.id.delete: + // 原有的删除逻辑 + break; + default: + return super.onContextItemSelected(item); + } + } + + // 处理文件夹的上下文菜单 switch (item.getItemId()) { case MENU_FOLDER_VIEW: openFolder(mFocusNoteDataItem); // 打开文件夹 @@ -975,6 +1414,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt */ @Override public boolean onOptionsItemSelected(MenuItem item) { + Log.d(TAG, "onOptionsItemSelected called: " + item.getItemId()); switch (item.getItemId()) { case R.id.menu_new_folder: { showCreateOrModifyFolderDialog(true); // 创建新文件夹 @@ -1002,14 +1442,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt startPreferenceActivity(); // 跳转到设置界面 break; } - case R.id.menu_add_user: { - showAddUserDialog(); // 添加用户 - break; - } - case R.id.menu_logout: { - logout(); // 退出登录 - break; - } case R.id.menu_new_note: { createNewNote(); // 新建笔记(在子文件夹菜单中) break; @@ -1087,131 +1519,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; } - /** - * 退出登录 - */ - private void logout() { - // 显示确认对话框 - new AlertDialog.Builder(this) - .setTitle(R.string.menu_logout) - .setMessage("确定要退出登录吗?") - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // 清除登录状态 - UserManager userManager = UserManager.getInstance(NotesListActivity.this); - if (userManager != null) { - userManager.logout(); - } - - // 跳转到登录界面 - Intent intent = new Intent(NotesListActivity.this, LoginRegisterActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - finish(); - } - }) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } - /** * 启动设置活动 - * 显示添加用户对话框 */ - private void showAddUserDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - LayoutInflater inflater = LayoutInflater.from(this); - View dialogView = inflater.inflate(R.layout.dialog_add_user, null); - - final EditText etNewUsername = dialogView.findViewById(R.id.et_new_username); - final EditText etNewPassword = dialogView.findViewById(R.id.et_new_password); - final EditText etConfirmPassword = dialogView.findViewById(R.id.et_confirm_password); - final TextView tvError = dialogView.findViewById(R.id.tv_error_message); - - builder.setView(dialogView); - builder.setTitle("添加用户"); - - final AlertDialog dialog = builder.create(); - - // 取消按钮 - Button btnCancel = dialogView.findViewById(R.id.btn_cancel); - btnCancel.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dialog.dismiss(); - } - }); - - // 确认按钮 - Button btnConfirm = dialogView.findViewById(R.id.btn_confirm); - btnConfirm.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - String username = etNewUsername.getText().toString().trim(); - String password = etNewPassword.getText().toString().trim(); - String confirmPassword = etConfirmPassword.getText().toString().trim(); - - // 验证输入 - if (TextUtils.isEmpty(username)) { - tvError.setText("请输入用户名"); - tvError.setVisibility(View.VISIBLE); - return; - } - - if (username.length() < 3) { - tvError.setText("用户名长度不能少于3位"); - tvError.setVisibility(View.VISIBLE); - return; - } - - if (TextUtils.isEmpty(password)) { - tvError.setText("请输入密码"); - tvError.setVisibility(View.VISIBLE); - return; - } - - if (password.length() < 6) { - tvError.setText("密码长度不能少于6位"); - tvError.setVisibility(View.VISIBLE); - return; - } - - if (TextUtils.isEmpty(confirmPassword)) { - tvError.setText("请确认密码"); - tvError.setVisibility(View.VISIBLE); - return; - } - - if (!password.equals(confirmPassword)) { - tvError.setText("两次输入的密码不一致"); - tvError.setVisibility(View.VISIBLE); - return; - } - - // 检查用户名是否已存在 - UserDatabaseHelper dbHelper = UserDatabaseHelper.getInstance(NotesListActivity.this); - if (dbHelper.isUsernameExists(username)) { - tvError.setText("该用户名已存在,请更换用户名"); - tvError.setVisibility(View.VISIBLE); - return; - } - - // 注册用户 - boolean success = dbHelper.registerUser(username, password); - if (success) { - Toast.makeText(NotesListActivity.this, "添加用户成功", Toast.LENGTH_SHORT).show(); - dialog.dismiss(); - } else { - tvError.setText("添加用户失败,请重试"); - tvError.setVisibility(View.VISIBLE); - } - } - }); - - dialog.show(); - } - private void startPreferenceActivity() { Activity from = getParent() != null ? getParent() : this; Intent intent = new Intent(from, NotesPreferenceActivity.class); @@ -1219,7 +1529,16 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 列表项点击监听器 + * 创建选项菜单 + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.note_list, menu); + return true; + } + + /** + * 列表项点击监听器,处理笔记和文件夹的点击事件 */ private class OnListItemClickListener implements OnItemClickListener { public void onItemClick(AdapterView parent, View view, int position, long id) { @@ -1298,10 +1617,16 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { if (view instanceof NotesListItem) { mFocusNoteDataItem = ((NotesListItem) view).getItemData(); // 记录长按的数据项 - - // 如果长按的是笔记且不在多选模式下,显示隐私锁菜单 + // 如果长按的是笔记且不在多选模式下,启动多选模式 if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) { - showPrivacyLockMenu(mFocusNoteDataItem.getId()); + if (mNotesListView.startActionMode(mModeCallBack) != null) { + // 启动成功,设置当前项为选中状态 + mModeCallBack.onItemCheckedStateChanged(null, position, id, true); + // 提供触觉反馈(震动) + mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } else { + Log.e(TAG, "startActionMode fails"); + } } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { // 如果长按的是文件夹,设置上下文菜单监听器 mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); @@ -1311,238 +1636,185 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 显示隐私锁菜单 - * @param noteId 便签ID + * 编辑便签 */ - private void showPrivacyLockMenu(long noteId) { - boolean isLocked = mPrivacyLockManager.isNoteLocked(noteId); - - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(isLocked ? "移除隐私锁" : "添加隐私锁"); + private void editNote(long noteId) { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.putExtra(NotesListActivity.NOTE_ID, noteId); + startActivity(intent); + } + + /** + * 分享便签 + */ + private void shareNote(long noteId) { + // 获取便签内容 + Cursor cursor = getContentResolver().query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + new String[]{Notes.NoteColumns.SNIPPET}, + null, null, null); - if (isLocked) { - builder.setMessage("确定要移除隐私锁吗?"); - builder.setPositiveButton("移除", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // 直接移除隐私锁 - if (mPrivacyLockManager.removePrivacyLock(noteId)) { - Toast.makeText(NotesListActivity.this, "成功移除隐私锁", Toast.LENGTH_SHORT).show(); - // 刷新列表 - mNotesListAdapter.changeCursor(null); - startAsyncNotesListQuery(); - } else { - Toast.makeText(NotesListActivity.this, "移除隐私锁失败", Toast.LENGTH_SHORT).show(); - } - } - }); - } else { - // 显示隐私锁类型选择对话框 - showPrivacyLockTypeSelectionDialog(noteId); - return; // 不需要显示确认对话框 + if (cursor != null && cursor.moveToFirst()) { + String content = cursor.getString(cursor.getColumnIndexOrThrow(Notes.NoteColumns.SNIPPET)); + + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, "便签"); + shareIntent.putExtra(Intent.EXTRA_TEXT, content); + startActivity(Intent.createChooser(shareIntent, "分享便签")); + + cursor.close(); } - - builder.setNegativeButton("取消", null); - builder.show(); } /** - * 显示隐私锁类型选择对话框 - * @param noteId 便签ID + * 复制便签 */ - private void showPrivacyLockTypeSelectionDialog(long noteId) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("添加隐私锁"); - - String[] options; + private void copyNote(long noteId) { + // 获取原便签内容 + Cursor cursor = getContentResolver().query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + new String[]{Notes.NoteColumns.SNIPPET}, + null, null, null); - // 检查Android版本是否支持手势锁 - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { - // Android 9.0 (API 28)及以上版本,支持手势锁 - options = new String[]{"密码锁", "手势锁"}; - } else { - // 版本低于Android 9.0 (API 28),不支持手势锁,显示提示 - Toast.makeText(this, "当前设备系统版本过低,不支持手势锁功能", Toast.LENGTH_SHORT).show(); - options = new String[]{"密码锁"}; + if (cursor != null && cursor.moveToFirst()) { + String content = cursor.getString(cursor.getColumnIndexOrThrow(Notes.NoteColumns.SNIPPET)); + + // 创建新便签,复制内容 + ContentValues values = new ContentValues(); + values.put(Notes.NoteColumns.SNIPPET, content + " (复制)"); + values.put(Notes.NoteColumns.CREATED_DATE, System.currentTimeMillis()); + values.put(Notes.NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + values.put(Notes.NoteColumns.PARENT_ID, mCurrentFolderId); + values.put(Notes.NoteColumns.TYPE, Notes.TYPE_NOTE); + + // 插入新便签 + Uri newNoteUri = getContentResolver().insert(Notes.CONTENT_NOTE_URI, values); + if (newNoteUri != null) { + Toast.makeText(this, "复制成功", Toast.LENGTH_SHORT).show(); + // 刷新列表 + refresh(); + } + + cursor.close(); } - - builder.setItems(options, new DialogInterface.OnClickListener() { + } + + /** + * 显示添加隐私锁对话框 + */ + private void showAddLockDialog(final long noteId) { + // 显示锁类型选择对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("选择锁类型"); + builder.setItems(new String[]{"密码锁", "手势锁"}, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (which == 0) { // 密码锁 - showPasswordInputDialog(noteId); - } else if (which == 1 && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + showPasswordInputDialog(noteId, true); + } else { // 手势锁 - showGestureLockDialog(noteId); + showGestureInputDialog(noteId, true); } } }); - - builder.setNegativeButton("取消", null); builder.show(); } /** * 显示密码输入对话框 - * @param noteId 便签ID */ - private void showPasswordInputDialog(long noteId) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_password_input, null); - builder.setView(dialogView); - - TextView tvTitle = dialogView.findViewById(R.id.tv_password_title); - EditText etPassword = dialogView.findViewById(R.id.et_password); - EditText etConfirmPassword = dialogView.findViewById(R.id.et_confirm_password); - CheckBox cbShowPassword = dialogView.findViewById(R.id.cb_show_password); - TextView tvHint = dialogView.findViewById(R.id.tv_password_hint); - TextView tvErrorMessage = dialogView.findViewById(R.id.tv_error_message); - Button btnCancel = dialogView.findViewById(R.id.btn_cancel); - Button btnConfirm = dialogView.findViewById(R.id.btn_confirm); - - tvTitle.setText("设置密码锁"); - - AlertDialog dialog = builder.create(); - - // 显示/隐藏密码 - cbShowPassword.setOnCheckedChangeListener((buttonView, isChecked) -> { - int inputType = isChecked ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD - : InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD; - etPassword.setInputType(inputType); - etConfirmPassword.setInputType(inputType); - etPassword.setSelection(etPassword.getText().length()); - etConfirmPassword.setSelection(etConfirmPassword.getText().length()); - }); - - // 确认按钮点击 - btnConfirm.setOnClickListener(v -> { - String password = etPassword.getText().toString().trim(); - String confirmPassword = etConfirmPassword.getText().toString().trim(); - - // 验证密码 - if (password.length() < 8) { - tvErrorMessage.setText("输入密码不得少于8位字母、数字"); - tvErrorMessage.setVisibility(View.VISIBLE); - return; - } - - if (!password.matches("^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]{8,}$")) { - tvErrorMessage.setText("密码必须包含至少8位字母和数字"); - tvErrorMessage.setVisibility(View.VISIBLE); - return; - } - - if (!password.equals(confirmPassword)) { - tvErrorMessage.setText("两次输入密码不一致"); - tvErrorMessage.setVisibility(View.VISIBLE); - return; - } - - // 加密密码并保存 - String encryptedPassword = PrivacyLockManager.encryptPassword(password); - if (mPrivacyLockManager.addPrivacyLock(noteId, PrivacyLockManager.LOCK_TYPE_PASSWORD, encryptedPassword)) { - Toast.makeText(NotesListActivity.this, "成功添加隐私锁", Toast.LENGTH_SHORT).show(); - dialog.dismiss(); - // 刷新列表 - mNotesListAdapter.changeCursor(null); - startAsyncNotesListQuery(); - } else { - Toast.makeText(NotesListActivity.this, "添加隐私锁失败", Toast.LENGTH_SHORT).show(); - } - }); - - // 取消按钮点击 - btnCancel.setOnClickListener(v -> dialog.dismiss()); + private void showPasswordInputDialog(final long noteId, final boolean isAddLock) { + final EditText passwordEditText = new EditText(this); + passwordEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + passwordEditText.setHint("请输入密码"); - dialog.show(); - } - - /** - * 显示手势锁对话框 - * @param noteId 便签ID - */ - private void showGestureLockDialog(long noteId) { AlertDialog.Builder builder = new AlertDialog.Builder(this); - View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_gesture_lock, null); - builder.setView(dialogView); - - TextView tvTitle = dialogView.findViewById(R.id.tv_gesture_title); - TextView tvInstruction = dialogView.findViewById(R.id.tv_gesture_instruction); - GestureLockView gestureLockView = dialogView.findViewById(R.id.gesture_lock_view); - TextView tvStatus = dialogView.findViewById(R.id.tv_gesture_status); - Button btnReset = dialogView.findViewById(R.id.btn_gesture_reset); - - tvTitle.setText("设置手势锁"); - tvInstruction.setText("请绘制连续手势(至少4个点)"); - tvStatus.setText(""); + builder.setTitle(isAddLock ? "设置密码" : "验证密码"); + builder.setView(passwordEditText); - AlertDialog dialog = builder.create(); - - // 记录当前绘制状态 - final List[] firstGesture = new List[]{null}; - final int[] attemptCount = {0}; - final int MAX_ATTEMPTS = 3; - - // 设置手势完成监听器 - gestureLockView.setOnGestureCompleteListener(selectedPoints -> { - if (firstGesture[0] == null) { - // 首次绘制 - if (selectedPoints.size() < 4) { - tvStatus.setText("手势需包含至少4个连续点,请重新绘制"); - gestureLockView.clearGesture(); - return; - } - - firstGesture[0] = new ArrayList<>(selectedPoints); - tvInstruction.setText("再次绘制手势确认"); - tvStatus.setText(""); - gestureLockView.clearGesture(); - } else { - // 二次绘制验证 - if (selectedPoints.equals(firstGesture[0])) { - // 手势一致,设置成功 - String gestureString = PrivacyLockManager.gestureToString(selectedPoints); - if (mPrivacyLockManager.addPrivacyLock(noteId, PrivacyLockManager.LOCK_TYPE_GESTURE, gestureString)) { - Toast.makeText(NotesListActivity.this, "成功添加隐私锁", Toast.LENGTH_SHORT).show(); - dialog.dismiss(); - // 刷新列表 - mNotesListAdapter.changeCursor(null); - startAsyncNotesListQuery(); + builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String password = passwordEditText.getText().toString().trim(); + if (!TextUtils.isEmpty(password)) { + if (isAddLock) { + // 添加密码锁 + PrivacyLockManager lockManager = new PrivacyLockManager(NotesListActivity.this); + String encryptedPassword = PrivacyLockManager.encryptPassword(password); + boolean success = lockManager.addPrivacyLock(noteId, PrivacyLockManager.LOCK_TYPE_PASSWORD, encryptedPassword); + if (success) { + Toast.makeText(NotesListActivity.this, "密码锁添加成功", Toast.LENGTH_SHORT).show(); + refresh(); + } else { + Toast.makeText(NotesListActivity.this, "密码锁添加失败", Toast.LENGTH_SHORT).show(); + } } else { - Toast.makeText(NotesListActivity.this, "添加隐私锁失败", Toast.LENGTH_SHORT).show(); + // 验证密码 + PrivacyLockManager lockManager = new PrivacyLockManager(NotesListActivity.this); + boolean isValid = lockManager.verifyPassword(noteId, password); + if (isValid) { + // 密码正确,移除锁 + boolean success = lockManager.removePrivacyLock(noteId); + if (success) { + Toast.makeText(NotesListActivity.this, "隐私锁已移除", Toast.LENGTH_SHORT).show(); + refresh(); + } else { + Toast.makeText(NotesListActivity.this, "隐私锁移除失败", Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(NotesListActivity.this, "密码错误", Toast.LENGTH_SHORT).show(); + } } } else { - // 手势不一致 - attemptCount[0]++; - if (attemptCount[0] >= MAX_ATTEMPTS) { - Toast.makeText(NotesListActivity.this, "3次绘制手势不一致,已取消设置", Toast.LENGTH_SHORT).show(); - dialog.dismiss(); - return; - } - - tvStatus.setText("两次绘制手势不一致,请重试 (" + attemptCount[0] + "/" + MAX_ATTEMPTS + ")"); - firstGesture[0] = null; - tvInstruction.setText("请绘制连续手势(至少4个点)"); - gestureLockView.clearGesture(); + Toast.makeText(NotesListActivity.this, "密码不能为空", Toast.LENGTH_SHORT).show(); } } }); - // 重置按钮点击 - btnReset.setOnClickListener(v -> { - gestureLockView.clearGesture(); - if (firstGesture[0] != null) { - firstGesture[0] = null; - tvInstruction.setText("请绘制连续手势(至少4个点)"); - } - tvStatus.setText(""); - }); - - dialog.setOnCancelListener(d -> { - // 取消时不做任何操作 - }); + builder.setNegativeButton("取消", null); + builder.show(); + } + + /** + * 显示手势输入对话框 + */ + private void showGestureInputDialog(final long noteId, final boolean isAddLock) { + // 这里需要实现手势输入对话框 + // 由于需要使用GestureLockView,这里简化处理,实际项目中需要创建包含GestureLockView的对话框 + Toast.makeText(this, "手势锁功能需要在实际设备上测试", Toast.LENGTH_SHORT).show(); + } + + /** + * 显示移除隐私锁对话框 + */ + private void showRemoveLockDialog(final long noteId) { + PrivacyLockManager lockManager = new PrivacyLockManager(this); + int lockType = lockManager.getLockType(noteId); - dialog.show(); + if (lockType == PrivacyLockManager.LOCK_TYPE_PASSWORD) { + showPasswordInputDialog(noteId, false); + } else if (lockType == PrivacyLockManager.LOCK_TYPE_GESTURE) { + showGestureInputDialog(noteId, false); + } + } + + /** + * 刷新便签列表 + */ + private void refresh() { + // 重新查询数据 + if (mBackgroundQueryHandler != null) { + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, + Notes.CONTENT_NOTE_URI, + null, + Notes.NoteColumns.PARENT_ID + "=? AND " + Notes.NoteColumns.TYPE + "!=?", + new String[]{ + String.valueOf(mCurrentFolderId), + String.valueOf(Notes.TYPE_SYSTEM) + }, + Notes.NoteColumns.MODIFIED_DATE + " DESC"); + } } } \ No newline at end of file diff --git a/src/notes/ui/NotesListItem.java b/src/notes/ui/NotesListItem.java index 20037c9..4450ab7 100644 --- a/src/notes/ui/NotesListItem.java +++ b/src/notes/ui/NotesListItem.java @@ -17,6 +17,7 @@ package net.micode.notes.ui; import android.content.Context; +import android.text.TextUtils; import android.text.format.DateUtils; import android.view.View; import android.widget.CheckBox; @@ -27,7 +28,6 @@ import android.widget.TextView; import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.tool.DataUtils; -import net.micode.notes.tool.PrivacyLockManager; import net.micode.notes.tool.ResourceParser.NoteItemBgResources; /** @@ -36,10 +36,10 @@ import net.micode.notes.tool.ResourceParser.NoteItemBgResources; */ public class NotesListItem extends LinearLayout { private ImageView mAlert; // 提醒图标(时钟或通话记录图标) - private ImageView mLock; // 锁图标 private TextView mTitle; // 标题/内容文本 private TextView mTime; // 修改时间文本 private TextView mCallName; // 通话记录联系人姓名 + private TextView mTag; // 标签文本 private NoteItemData mItemData; // 绑定的数据对象 private CheckBox mCheckBox; // 多选模式下的复选框 @@ -53,10 +53,10 @@ public class NotesListItem extends LinearLayout { inflate(context, R.layout.note_item, this); // 初始化各个子视图组件 mAlert = (ImageView) findViewById(R.id.iv_alert_icon); - mLock = (ImageView) findViewById(R.id.iv_lock_icon); mTitle = (TextView) findViewById(R.id.tv_title); mTime = (TextView) findViewById(R.id.tv_time); mCallName = (TextView) findViewById(R.id.tv_name); + mTag = (TextView) findViewById(R.id.tv_tag); // 使用系统预定义的checkbox ID mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); } @@ -117,12 +117,26 @@ public class NotesListItem extends LinearLayout { data.getNotesCount())); mAlert.setVisibility(View.GONE); // 文件夹不显示提醒图标 } else { - // 普通笔记显示:内容摘要 - mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + // 普通笔记显示:标题 + String title = data.getTitle(); + String content = data.getContentSnippet(); + + if (TextUtils.isEmpty(title)) { + // 如果没有标题,显示内容摘要 + mTitle.setText(DataUtils.getFormattedSnippet(content)); + } else { + // 如果有标题,显示标题 + mTitle.setText(title); + } + // 根据是否有提醒设置提醒图标 if (data.hasAlert()) { mAlert.setImageResource(R.drawable.clock); // 时钟图标 mAlert.setVisibility(View.VISIBLE); + } else if (data.isCompleted()) { + // 如果是已完成的便签,显示已完成图标(使用系统内置图标) + mAlert.setImageResource(android.R.drawable.checkbox_on_background); // 已完成图标 + mAlert.setVisibility(View.VISIBLE); } else { mAlert.setVisibility(View.GONE); } @@ -132,15 +146,32 @@ public class NotesListItem extends LinearLayout { // 设置相对时间显示(如"2分钟前"、"昨天"等) mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); - // 显示锁图标(如果便签被锁定) - showLockIcon(context, data); + // 设置标签显示 + String tag = data.getTag(); + if (tag != null && !tag.trim().isEmpty()) { + String tagText = ""; + tag = tag.trim(); // 去除前后空格 + if (NoteEditActivity.TAG_LIFE.equals(tag)) { + tagText = "生活"; + } else if (NoteEditActivity.TAG_STUDY.equals(tag)) { + tagText = "学习"; + } else if (NoteEditActivity.TAG_WORK.equals(tag)) { + tagText = "工作"; + } else { + tagText = tag; // 直接显示标签值,包括自定义标签 + } + mTag.setText("标签:" + tagText); + mTag.setVisibility(View.VISIBLE); + } else { + mTag.setVisibility(View.GONE); + } // 根据位置和类型设置背景 setBackground(data); } /** - * 根据位置和类型设置不同的背景 + * 根据数据项的位置和类型设置不同的背景 * 实现列表项的分组视觉效果(第一个、最后一个、中间项等) * @param data 笔记数据项 */ @@ -167,20 +198,6 @@ public class NotesListItem extends LinearLayout { } } - /** - * 显示锁图标(如果便签被锁定) - * @param context 上下文环境 - * @param data 笔记数据项 - */ - private void showLockIcon(Context context, NoteItemData data) { - PrivacyLockManager privacyLockManager = new PrivacyLockManager(context); - if (privacyLockManager.isNoteLocked(data.getId())) { - mLock.setVisibility(View.VISIBLE); - } else { - mLock.setVisibility(View.GONE); - } - } - /** * 获取当前绑定的数据对象 * @return NoteItemData对象 diff --git a/src/notes/ui/NotesPreferenceActivity.java b/src/notes/ui/NotesPreferenceActivity.java index 820fb69..f67f37b 100644 --- a/src/notes/ui/NotesPreferenceActivity.java +++ b/src/notes/ui/NotesPreferenceActivity.java @@ -27,7 +27,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; -import android.os.Build; import android.os.Bundle; import android.preference.Preference; import android.preference.Preference.OnPreferenceClickListener; @@ -43,8 +42,6 @@ import android.widget.Button; import android.widget.TextView; import android.widget.Toast; -import androidx.annotation.RequiresApi; - import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; @@ -77,7 +74,6 @@ public class NotesPreferenceActivity extends PreferenceActivity { /** * Activity创建时的初始化方法 */ - @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -93,7 +89,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); mOriAccounts = null; // 加载设置页面的自定义头部布局 diff --git a/src/notes/ui/RichEditor.java b/src/notes/ui/RichEditor.java new file mode 100644 index 0000000..029a8bf --- /dev/null +++ b/src/notes/ui/RichEditor.java @@ -0,0 +1,277 @@ +package net.micode.notes.ui; + +import android.content.Context; +import android.os.Build; +import android.text.Editable; +import android.text.TextWatcher; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; +import android.util.AttributeSet; +import android.widget.EditText; +import android.graphics.Typeface; +import android.text.Spannable; + +public class RichEditor extends EditText { + + public interface OnTextChangeListener { + void onTextChange(String text); + } + + private OnTextChangeListener onTextChangeListener; + + public RichEditor(Context context) { + super(context); + init(); + } + + public RichEditor(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public RichEditor(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + // 初始化逻辑 + } + + public void setOnTextChangeListener(OnTextChangeListener listener) { + this.onTextChangeListener = listener; + + // 监听文本变化 + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (onTextChangeListener != null) { + onTextChangeListener.onTextChange(s.toString()); + } + } + + @Override + public void afterTextChanged(Editable s) {} + }); + } + + // 重写获取文本的方法 + @Override + public Editable getText() { + return super.getText(); + } + + // 重写设置文本的方法,包括设置文本颜色、文本样式等 + @Override + public void setText(CharSequence text, BufferType type) { + super.setText(text, type); + } + + // 重写设置光标位置的方法 + @Override + public void setSelection(int index) { + super.setSelection(index); + } + + // 重写设置选中文本的方法 + @Override + public void setSelection(int start, int stop) { + super.setSelection(start, stop); + } + + // 重写获取选中文本起始位置的方法 + @Override + public int getSelectionStart() { + return super.getSelectionStart(); + } + + // 重写获取选中文本结束位置的方法 + @Override + public int getSelectionEnd() { + return super.getSelectionEnd(); + } + + // 重写设置文本外观的方法,文本外观包括字体、颜色等 + @Override + public void setTextAppearance(Context context, int resid) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + super.setTextAppearance(resid); + } else { + // 对旧版本的兼容处理 + setTextAppearance(context, resid); + } + } + + // 设置编辑器高度 + public void setEditorHeight(int pixels) { + this.setHeight(pixels); + } + + + //设置编辑器字体大小 + public void setEditorFontSize(int size) { + this.setTextSize(size); + } + + //设置编辑器字体颜色 + public void setEditorFontColor(int color) { + this.setTextColor(color); + } + + //设置占位符文本 + public void setPlaceholder(String placeholder) { + this.setHint(placeholder); + } + + // 设置编辑器启用或禁用输入 + public void setInputEnabled(boolean enabled) { + this.setEnabled(enabled); + } + + //切换粗体样式 + public void toggleBold() { + toggleStyleSpan(Typeface.BOLD); + } + + //切换斜体样式 + public void toggleItalic() { + toggleStyleSpan(Typeface.ITALIC); + } + + //切换删除线样式 + public void toggleStrikeThrough() { + toggleSpan(StrikethroughSpan.class, new StrikethroughSpan()); + } + + //切换下划线样式 + public void toggleUnderline() { + toggleSpan(UnderlineSpan.class, new UnderlineSpan()); + } + + // 粗体、斜体样式切换方法 + private void toggleStyleSpan(int style) { + int start = getSelectionStart(); + int end = getSelectionEnd(); + if (start < 0 || end < 0 || start >= end) { + return; // 没有选中文本时不处理 + } + + Spannable str = getText(); + StyleSpan[] spans = str.getSpans(start, end, StyleSpan.class); + + boolean hasStyle = false; + // 检查是否已存在相同样式 + for (StyleSpan span : spans) { + if (span.getStyle() == style) { + str.removeSpan(span); + hasStyle = true; + } + } + + // 如果不存在对应样式,则添加 + if (!hasStyle) { + str.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + + //通用的Span切换方法,下划线和删除线 + private void toggleSpan(Class spanType, T newSpan) { + int start = getSelectionStart(); + int end = getSelectionEnd(); + if (start < 0 || end < 0 || start >= end) { + return; + } + + Spannable str = getText(); + @SuppressWarnings("unchecked") + T[] spans = (T[]) str.getSpans(start, end, spanType); + + if (spans.length > 0) { + // 如果已存在样式,则移除 + for (T span : spans) { + str.removeSpan(span); + } + } else { + // 如果不存在样式,则添加 + str.setSpan(newSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + + // 应用粗体样式 + public void applyBold() { + int start = getSelectionStart(); + int end = getSelectionEnd(); + + if (start >= 0 && end >= 0 && start != end) { + Spannable str = getText(); + StyleSpan[] spans = str.getSpans(start, end, StyleSpan.class); + + for (StyleSpan span : spans) { + if (span.getStyle() == Typeface.BOLD) { + str.removeSpan(span); + } + } + + str.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + // 应用斜体样式 + public void applyItalic() { + int start = getSelectionStart(); + int end = getSelectionEnd(); + + if (start >= 0 && end >= 0 && start != end) { + Spannable str = getText(); + StyleSpan[] spans = str.getSpans(start, end, StyleSpan.class); + + for (StyleSpan span : spans) { + if (span.getStyle() == Typeface.ITALIC) { + str.removeSpan(span); + } + } + + str.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + // 应用删除线样式 + public void applyStrikeThrough() { + int start = getSelectionStart(); + int end = getSelectionEnd(); + + if (start >= 0 && end >= 0 && start != end) { + Spannable str = getText(); + StrikethroughSpan[] spans = str.getSpans(start, end, StrikethroughSpan.class); + + for (StrikethroughSpan span : spans) { + str.removeSpan(span); + } + + str.setSpan(new StrikethroughSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + // 应用下划线样式 + public void applyUnderline() { + int start = getSelectionStart(); + int end = getSelectionEnd(); + + if (start >= 0 && end >= 0 && start != end) { + Spannable str = getText(); + UnderlineSpan[] spans = str.getSpans(start, end, UnderlineSpan.class); + + for (UnderlineSpan span : spans) { + str.removeSpan(span); + } + + str.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } +} \ No newline at end of file diff --git a/src/notes/ui/SplashActivity.java b/src/notes/ui/SplashActivity.java index 9b72136..34dc3b1 100644 --- a/src/notes/ui/SplashActivity.java +++ b/src/notes/ui/SplashActivity.java @@ -3,47 +3,58 @@ package net.micode.notes.ui; import android.content.Intent; import android.os.Bundle; import android.os.Handler; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.ImageView; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import net.micode.notes.R; -import net.micode.notes.tool.UserManager; public class SplashActivity extends AppCompatActivity { private static final int SPLASH_DURATION = 3000; // 3秒 - + private static final int TEXT_FADE_IN_DELAY = 2000; // 2秒后文字淡入 + // 启动页创建,初始化动画界面和跳转逻辑 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); + ImageView logo = findViewById(R.id.splash_logo); TextView text = findViewById(R.id.splash_text); - // 开始淡入动画 - WaveAnimation.applyFadeInAnimation(text); - - // 3秒后根据登录状态跳转 - new Handler().postDelayed(() -> { - UserManager userManager = UserManager.getInstance(this); - boolean isLoggedIn = userManager.isLoggedIn(); - - if (isLoggedIn) { - // 已登录,直接进入主界面 - Intent intent = new Intent(SplashActivity.this, NotesListActivity.class); - startActivity(intent); - } else { - // 未登录,跳转到注册登录界面 - Intent intent = new Intent(SplashActivity.this, LoginRegisterActivity.class); - startActivity(intent); + // 加载文字滑动动画 + Animation slideUpAnimation = AnimationUtils.loadAnimation(this, R.anim.text_slide_up); + + // 2秒后显示文字动画 + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + text.setVisibility(android.view.View.VISIBLE); + text.startAnimation(slideUpAnimation); + } + }, TEXT_FADE_IN_DELAY); + + // 3秒后跳转到主界面 + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + onSplashComplete(); } - finish(); - overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); }, SPLASH_DURATION); } - + + // 动画完成回调方法 + private void onSplashComplete() { + Intent intent = new Intent(SplashActivity.this, LoginRegisterActivity.class); + startActivity(intent); + finish(); // 结束启动页,防止用户返回到此页面 + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); + } + // 处理屏幕旋转等配置变更 @Override public void onConfigurationChanged(android.content.res.Configuration newConfig) {