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) {