From a6feecbe39c08b981aade698765337993124d70d Mon Sep 17 00:00:00 2001 From: mc19 <2716188191@qq.com> Date: Thu, 29 Jan 2026 20:36:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=9A=90=E7=A7=81=E9=94=81?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/notes/data/Notes.java | 62 +- src/notes/data/NotesDatabaseHelper.java | 37 +- src/notes/data/UserDatabaseHelper.java | 3 - src/notes/model/WorkingNote.java | 2 +- src/notes/tool/PrivacyLockManager.java | 353 +++ src/notes/tool/UserManager.java | 42 +- src/notes/ui/GestureLockView.java | 264 +++ src/notes/ui/LoginRegisterActivity.java | 42 +- src/notes/ui/NoteEditActivity.java | 2676 ++++++++++++----------- src/notes/ui/NotesListActivity.java | 358 ++- src/notes/ui/NotesListItem.java | 22 +- 11 files changed, 2572 insertions(+), 1289 deletions(-) create mode 100644 src/notes/tool/PrivacyLockManager.java create mode 100644 src/notes/ui/GestureLockView.java diff --git a/src/notes/data/Notes.java b/src/notes/data/Notes.java index 2958f09..414c31f 100644 --- a/src/notes/data/Notes.java +++ b/src/notes/data/Notes.java @@ -26,20 +26,17 @@ import android.net.Uri; /** *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"; //日志显示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 @@ -50,15 +47,16 @@ 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"; //桌面便签小部件 @@ -66,14 +64,16 @@ 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 在查询/插入/打开文件时会先问 URI 对应的 MIME: + * MIME是描述文件类型的一种标准 + * Android的ContentResolver在查询/插入/打开文件时会首先查询MIME类型 */ public static class DataConstants { public static final String NOTE = TextNote.CONTENT_ITEM_TYPE; //文本便签MIME类型 @@ -81,7 +81,6 @@ public class Notes { public static final String IMAGE = ImageData.CONTENT_ITEM_TYPE; //图片MIME类型 } - //七、基础URI /** * Uri to query all notes and folders */ @@ -93,10 +92,9 @@ public class Notes { */ public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); - //八、数据库列名接口 —— Note 表 /** - * NoteColumns是便签数据库列名接口,只定义/声明一些方法、变量 - * 此处声明了便签数据列名有ID,PARENT_ID,创建日期,修改日期,提醒日期等 + * NoteColumns是便签数据库列名接口 + * 声明便签信息列名,包括便签ID,父文件夹ID,创建日期,修改日期,提醒日期等。 */ public interface NoteColumns { /** @@ -227,13 +225,29 @@ public class Notes { *

Type : INTEGER (long)

*/ public static final String VERSION = "version"; + + /** + * Lock status: 0-unlocked, 1-locked + *

Type: INTEGER

+ */ + public static final String LOCKED = "locked"; + + /** + * Lock type: 0-none, 1-password, 2-gesture + *

Type: INTEGER

+ */ + public static final String LOCK_TYPE = "lock_type"; + + /** + * Encrypted password for privacy lock + *

Type: TEXT

+ */ + public static final String ENCRYPTED_PASSWORD = "encrypted_password"; } - //九、数据库列名接口 —— Data 表 - /** - * DataColumns是数据库列名接口 + * 数据库列名接口,声明与便签内容相关的字段 */ public interface DataColumns { /** @@ -322,8 +336,10 @@ public class Notes { } - //十、文本便签实体常量类 - //文本便签TextNote类实现DataColumns接口中声明的方法 + /** + * TextNote是文本便签实体常量类 + * 实现DataColumns接口中声明的方法 + */ public static final class TextNote implements DataColumns { /** * Mode to indicate the text in check list mode or not @@ -341,8 +357,8 @@ public class Notes { } /** - *通话记录便签实体常量类 - * 通话记录CallNote类也实现了DataColumns接口,体现了接口的多实现特性。 + * CallNote是通话记录便签实体常量类 + * 实现了DataColumns接口,与TextNote一起,体现了接口的多实现特性。 */ public static final class CallNote implements DataColumns { /** diff --git a/src/notes/data/NotesDatabaseHelper.java b/src/notes/data/NotesDatabaseHelper.java index b761c42..ee7df8c 100644 --- a/src/notes/data/NotesDatabaseHelper.java +++ b/src/notes/data/NotesDatabaseHelper.java @@ -54,7 +54,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { */ private static final String DB_NAME = "note.db"; - private static final int DB_VERSION = 6; + private static final int DB_VERSION = 7; /**表明常量接口*/ @@ -95,6 +95,10 @@ 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.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" + ")"; @@ -117,7 +121,9 @@ 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.DATA5 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.RICH_TEXT_FORMAT + " TEXT NOT NULL DEFAULT ''," + + DataColumns.IMAGE_PATH + " TEXT NOT NULL DEFAULT ''" + ")"; /** @@ -412,6 +418,16 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { oldVersion++; } + if (oldVersion == 6) { + upgradeToV7(db); + oldVersion++; + } + + if (oldVersion == 7) { + upgradeToV8(db); + oldVersion++; + } + if (reCreateTriggers) { reCreateNoteTableTriggers(db); reCreateDataTableTriggers(db); @@ -463,4 +479,21 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { // 为data表添加一个专门存储图片路径的列 db.execSQL("ALTER TABLE " + TABLE.DATA + " ADD COLUMN image_path TEXT NOT NULL DEFAULT ''"); } + + //v6到v7的升级,为note表新增位置提醒相关的列 + private void upgradeToV7(SQLiteDatabase db) { + // 为note表添加位置提醒相关的列 + 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 ''"); + } + + //v7到v8的升级,为note表新增隐私锁相关的列 + private void upgradeToV8(SQLiteDatabase db) { + // 为note表添加隐私锁相关列 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.LOCKED + " INTEGER NOT NULL DEFAULT 0"); // 锁定状态:0-未锁定,1-已锁定 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.LOCK_TYPE + " INTEGER NOT NULL DEFAULT 0"); // 锁类型:0-无锁,1-密码锁,2-手势锁 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.ENCRYPTED_PASSWORD + " TEXT NOT NULL DEFAULT ''"); // 加密后的密码 + } } diff --git a/src/notes/data/UserDatabaseHelper.java b/src/notes/data/UserDatabaseHelper.java index cb21d34..64d9c4c 100644 --- a/src/notes/data/UserDatabaseHelper.java +++ b/src/notes/data/UserDatabaseHelper.java @@ -108,10 +108,8 @@ public class UserDatabaseHelper extends SQLiteOpenHelper { boolean authenticated = cursor.getCount() > 0; cursor.close(); - return authenticated; } - /** * 检查用户名是否存在 * @param username 用户名 @@ -128,7 +126,6 @@ public class UserDatabaseHelper extends SQLiteOpenHelper { boolean exists = cursor.getCount() > 0; cursor.close(); - return exists; } diff --git a/src/notes/model/WorkingNote.java b/src/notes/model/WorkingNote.java index f3c37c8..cf00c34 100644 --- a/src/notes/model/WorkingNote.java +++ b/src/notes/model/WorkingNote.java @@ -82,7 +82,7 @@ public class WorkingNote { DataColumns.DATA4, DataColumns.DATA5, DataColumns.RICH_TEXT_FORMAT, - DataColumns.IMAGE_PATH, + DataColumns.IMAGE_PATH }; //指定查询Note表时需要返回的字段 diff --git a/src/notes/tool/PrivacyLockManager.java b/src/notes/tool/PrivacyLockManager.java new file mode 100644 index 0000000..be9de9d --- /dev/null +++ b/src/notes/tool/PrivacyLockManager.java @@ -0,0 +1,353 @@ +package net.micode.notes.tool; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.util.Log; + +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; + +/** + * 隐私锁管理类,用于处理便签和文件夹的隐私锁功能 + */ +public class PrivacyLockManager { + private static final String TAG = "PrivacyLockManager"; + + // 锁类型常量 + public static final int LOCK_TYPE_NONE = 0; + public static final int LOCK_TYPE_PASSWORD = 1; + public static final int LOCK_TYPE_GESTURE = 2; + + // 锁状态常量 + public static final int LOCK_STATUS_UNLOCKED = 0; + public static final int LOCK_STATUS_LOCKED = 1; + + private Context mContext; + + public PrivacyLockManager(Context context) { + this.mContext = context; + } + + /** + * SHA256加密密码 + * @param password 原始密码 + * @return 加密后的密码 + */ + public static String encryptPassword(String password) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(password.getBytes("UTF-8")); + + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + + return hexString.toString(); + } catch (Exception ex) { + Log.e(TAG, "Error encrypting password", ex); + return ""; + } + } + + /** + * 检查指定便签是否已加锁 + * @param noteId 便签ID + * @return 是否已加锁 + */ + public boolean isNoteLocked(long noteId) { + String[] projection = new String[]{NoteColumns.LOCKED}; + String selection = NoteColumns.ID + " = ?"; + String[] selectionArgs = new String[]{String.valueOf(noteId)}; + + Cursor cursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + projection, + selection, + selectionArgs, + null + ); + + boolean isLocked = false; + if (cursor != null) { + if (cursor.moveToFirst()) { + isLocked = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.LOCKED)) == LOCK_STATUS_LOCKED; + } + cursor.close(); + } + + return isLocked; + } + + /** + * 为指定便签添加隐私锁 + * @param noteId 便签ID + * @param lockType 锁类型 + * @param passwordOrGesture 加密后的密码或手势序列(如果使用密码锁或手势锁) + * @return 是否成功 + */ + public boolean addPrivacyLock(long noteId, int lockType, String passwordOrGesture) { + if (noteId <= 0) { + Log.e(TAG, "Invalid note ID: " + noteId); + return false; + } + + ContentValues values = new ContentValues(); + values.put(NoteColumns.LOCKED, LOCK_STATUS_LOCKED); + values.put(NoteColumns.LOCK_TYPE, lockType); + + if ((lockType == LOCK_TYPE_PASSWORD || lockType == LOCK_TYPE_GESTURE) && passwordOrGesture != null) { + values.put(NoteColumns.ENCRYPTED_PASSWORD, passwordOrGesture); + } else if (lockType == LOCK_TYPE_NONE) { + values.put(NoteColumns.ENCRYPTED_PASSWORD, ""); + } + + int rowsUpdated = mContext.getContentResolver().update( + Notes.CONTENT_NOTE_URI, + values, + NoteColumns.ID + " = ?", + new String[]{String.valueOf(noteId)} + ); + + return rowsUpdated > 0; + } + + /** + * 移除指定便签的隐私锁 + * @param noteId 便签ID + * @return 是否成功 + */ + public boolean removePrivacyLock(long noteId) { + if (noteId <= 0) { + Log.e(TAG, "Invalid note ID: " + noteId); + return false; + } + + ContentValues values = new ContentValues(); + values.put(NoteColumns.LOCKED, LOCK_STATUS_UNLOCKED); + values.put(NoteColumns.LOCK_TYPE, LOCK_TYPE_NONE); + values.put(NoteColumns.ENCRYPTED_PASSWORD, ""); + + int rowsUpdated = mContext.getContentResolver().update( + Notes.CONTENT_NOTE_URI, + values, + NoteColumns.ID + " = ?", + new String[]{String.valueOf(noteId)} + ); + + return rowsUpdated > 0; + } + + /** + * 验证密码是否正确 + * @param noteId 便签ID + * @param password 待验证的密码 + * @return 是否验证成功 + */ + public boolean verifyPassword(long noteId, String password) { + String[] projection = new String[]{NoteColumns.ENCRYPTED_PASSWORD}; + String selection = NoteColumns.ID + " = ?"; + String[] selectionArgs = new String[]{String.valueOf(noteId)}; + + Cursor cursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + projection, + selection, + selectionArgs, + null + ); + + boolean isValid = false; + if (cursor != null) { + if (cursor.moveToFirst()) { + String storedEncryptedPassword = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.ENCRYPTED_PASSWORD)); + String encryptedInput = encryptPassword(password); + + isValid = storedEncryptedPassword.equals(encryptedInput); + } + cursor.close(); + } + + return isValid; + } + + /** + * 获取便签的锁类型 + * @param noteId 便签ID + * @return 锁类型 + */ + public int getLockType(long noteId) { + String[] projection = new String[]{NoteColumns.LOCK_TYPE}; + String selection = NoteColumns.ID + " = ?"; + String[] selectionArgs = new String[]{String.valueOf(noteId)}; + + Cursor cursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + projection, + selection, + selectionArgs, + null + ); + + int lockType = LOCK_TYPE_NONE; + if (cursor != null) { + if (cursor.moveToFirst()) { + lockType = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.LOCK_TYPE)); + } + cursor.close(); + } + + return lockType; + } + + /** + * 为整个文件夹及其所有子便签添加隐私锁 + * @param folderId 文件夹ID + * @param lockType 锁类型 + * @param passwordOrGesture 加密后的密码或手势序列(如果使用密码锁或手势锁) + * @return 是否成功 + */ + public boolean addPrivacyLockToFolder(long folderId, int lockType, String passwordOrGesture) { + // 首先为文件夹本身添加锁 + boolean folderSuccess = addPrivacyLock(folderId, lockType, passwordOrGesture); + + // 然后为文件夹内的所有便签添加锁 + String[] projection = new String[]{NoteColumns.ID}; + String selection = NoteColumns.PARENT_ID + " = ? AND " + NoteColumns.TYPE + " = ?"; + String[] selectionArgs = new String[]{String.valueOf(folderId), String.valueOf(Notes.TYPE_NOTE)}; + + Cursor cursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + projection, + selection, + selectionArgs, + null + ); + + boolean allNotesSuccess = true; + if (cursor != null) { + while (cursor.moveToNext()) { + long noteId = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.ID)); + boolean noteSuccess = addPrivacyLock(noteId, lockType, passwordOrGesture); + if (!noteSuccess) { + allNotesSuccess = false; + } + } + cursor.close(); + } + + return folderSuccess && allNotesSuccess; + } + + /** + * 移除整个文件夹及其所有子便签的隐私锁 + * @param folderId 文件夹ID + * @return 是否成功 + */ + public boolean removePrivacyLockFromFolder(long folderId) { + // 首先移除文件夹本身的锁 + boolean folderSuccess = removePrivacyLock(folderId); + + // 然后移除文件夹内的所有便签的锁 + String[] projection = new String[]{NoteColumns.ID}; + String selection = NoteColumns.PARENT_ID + " = ? AND " + NoteColumns.TYPE + " = ?"; + String[] selectionArgs = new String[]{String.valueOf(folderId), String.valueOf(Notes.TYPE_NOTE)}; + + Cursor cursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + projection, + selection, + selectionArgs, + null + ); + + boolean allNotesSuccess = true; + if (cursor != null) { + while (cursor.moveToNext()) { + long noteId = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.ID)); + boolean noteSuccess = removePrivacyLock(noteId); + if (!noteSuccess) { + allNotesSuccess = false; + } + } + cursor.close(); + } + + return folderSuccess && allNotesSuccess; + } + + /** + * 验证手势是否正确 + * @param noteId 便签ID + * @param gestureSequence 手势序列 + * @return 是否验证成功 + */ + public boolean verifyGesture(long noteId, List gestureSequence) { + String[] projection = new String[]{NoteColumns.ENCRYPTED_PASSWORD}; + String selection = NoteColumns.ID + " = ?"; + String[] selectionArgs = new String[]{String.valueOf(noteId)}; + + Cursor cursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + projection, + selection, + selectionArgs, + null + ); + + boolean isValid = false; + if (cursor != null) { + if (cursor.moveToFirst()) { + String storedGestureSequence = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.ENCRYPTED_PASSWORD)); + + // 将手势序列转换为字符串进行比较 + StringBuilder gestureBuilder = new StringBuilder(); + for (int point : gestureSequence) { + gestureBuilder.append(point); + } + String inputGestureSequence = gestureBuilder.toString(); + + isValid = storedGestureSequence.equals(inputGestureSequence); + } + cursor.close(); + } + + return isValid; + } + + /** + * 将手势点列表转换为字符串 + * @param gesturePoints 手势点列表 + * @return 手势序列字符串 + */ + public static String gestureToString(List gesturePoints) { + StringBuilder sb = new StringBuilder(); + for (int point : gesturePoints) { + sb.append(point); + } + return sb.toString(); + } + + /** + * 将手势序列字符串转换为点列表 + * @param gestureString 手势序列字符串 + * @return 手势点列表 + */ + public static List stringToGesture(String gestureString) { + List gesturePoints = new ArrayList<>(); + for (char c : gestureString.toCharArray()) { + gesturePoints.add(Character.getNumericValue(c)); + } + return gesturePoints; + } +} \ No newline at end of file diff --git a/src/notes/tool/UserManager.java b/src/notes/tool/UserManager.java index 7154346..be6f5c3 100644 --- a/src/notes/tool/UserManager.java +++ b/src/notes/tool/UserManager.java @@ -11,17 +11,33 @@ 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 UserManager instance; + private static volatile UserManager instance; // 使用volatile关键字确保多线程环境下的可见性 private SharedPreferences sharedPreferences; + private Context applicationContext; private UserManager(Context context) { - sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + if (context != null) { + this.applicationContext = context.getApplicationContext(); + if (this.applicationContext != null) { + this.sharedPreferences = this.applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + } + } } - public static synchronized UserManager getInstance(Context context) { + public static UserManager getInstance(Context context) { + if (context == null) { + throw new IllegalArgumentException("Context cannot be null"); + } + + // 双重检查锁定模式,确保线程安全 if (instance == null) { - instance = new UserManager(context.getApplicationContext()); + synchronized (UserManager.class) { + if (instance == null) { + instance = new UserManager(context.getApplicationContext()); + } + } } return instance; } @@ -33,6 +49,7 @@ public class UserManager { sharedPreferences.edit() .putBoolean(KEY_IS_LOGGED_IN, true) .putString(KEY_CURRENT_USER, username) + .putLong(KEY_LOGIN_TIMESTAMP, System.currentTimeMillis()) // 保存当前登录时间 .apply(); } @@ -50,6 +67,22 @@ 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; + } + /** * 退出登录,清除登录状态 */ @@ -57,6 +90,7 @@ 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/GestureLockView.java b/src/notes/ui/GestureLockView.java new file mode 100644 index 0000000..d2d48f0 --- /dev/null +++ b/src/notes/ui/GestureLockView.java @@ -0,0 +1,264 @@ +package net.micode.notes.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +/** + * 手势锁自定义视图 + */ +public class GestureLockView extends View { + private static final int MATRIX_SIZE = 3; // 3x3矩阵 + + private Paint mPaint; // 画笔 + private List mPoints; // 手势点集合 + private List mSelectedPoints; // 已选择的点集合 + private List mLines; // 连接线集合 + private GesturePoint mCurrentPoint; // 当前手指位置点 + private boolean mIsDrawing; // 是否正在绘制 + + private OnGestureCompleteListener mListener; // 手势完成监听器 + + public interface OnGestureCompleteListener { + void onGestureComplete(List selectedPoints); + } + + public static class GesturePoint { + public float x; // X坐标 + public float y; // Y坐标 + public int index; // 点的索引 (0-8) + public boolean isSelected; // 是否被选中 + + public GesturePoint(float x, float y, int index) { + this.x = x; + this.y = y; + this.index = index; + this.isSelected = false; + } + } + + public static class GestureLine { + public GesturePoint startPoint; // 起始点 + public GesturePoint endPoint; // 结束点 + + public GestureLine(GesturePoint start, GesturePoint end) { + this.startPoint = start; + this.endPoint = end; + } + } + + public GestureLockView(Context context) { + super(context); + init(); + } + + public GestureLockView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public GestureLockView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + mPaint = new Paint(); + mPaint.setAntiAlias(true); // 抗锯齿 + mPaint.setStrokeWidth(4); // 线宽 + + mPoints = new ArrayList<>(); + mSelectedPoints = new ArrayList<>(); + mLines = new ArrayList<>(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + // 计算点的位置 + int padding = Math.min(w, h) / 10; // 边距 + int cellWidth = (w - 2 * padding) / (MATRIX_SIZE - 1); // 每个格子的宽度 + int cellHeight = (h - 2 * padding) / (MATRIX_SIZE - 1); // 每个格子的高度 + + mPoints.clear(); + for (int i = 0; i < MATRIX_SIZE; i++) { + for (int j = 0; j < MATRIX_SIZE; j++) { + float x = padding + j * cellWidth; + float y = padding + i * cellHeight; + mPoints.add(new GesturePoint(x, y, i * MATRIX_SIZE + j)); + } + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // 绘制所有点 + for (GesturePoint point : mPoints) { + drawPoint(canvas, point); + } + + // 绘制连接线 + for (GestureLine line : mLines) { + drawLine(canvas, line); + } + + // 如果正在绘制,绘制从最后一点到当前手指位置的连线 + if (mIsDrawing && mCurrentPoint != null && !mSelectedPoints.isEmpty()) { + GesturePoint lastPoint = mPoints.get(mSelectedPoints.get(mSelectedPoints.size() - 1)); + mPaint.setColor(Color.parseColor("#FFA500")); // 橙色 + mPaint.setStyle(Paint.Style.STROKE); + canvas.drawLine(lastPoint.x, lastPoint.y, mCurrentPoint.x, mCurrentPoint.y, mPaint); + } + } + + private void drawPoint(Canvas canvas, GesturePoint point) { + float radius = Math.min(getWidth(), getHeight()) / 15f; // 点的半径 + + if (point.isSelected) { + // 绘制选中的点(大圆圈) + mPaint.setColor(Color.parseColor("#FFA500")); // 橙色 + mPaint.setStyle(Paint.Style.FILL); + canvas.drawCircle(point.x, point.y, radius, mPaint); + + // 绘制内部小圆点 + mPaint.setColor(Color.WHITE); + canvas.drawCircle(point.x, point.y, radius / 2, mPaint); + } else { + // 绘制未选中的点(圆环) + mPaint.setColor(Color.GRAY); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(4); + canvas.drawCircle(point.x, point.y, radius, mPaint); + } + } + + private void drawLine(Canvas canvas, GestureLine line) { + mPaint.setColor(Color.parseColor("#FFA500")); // 橙色 + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(8); + canvas.drawLine(line.startPoint.x, line.startPoint.y, + line.endPoint.x, line.endPoint.y, mPaint); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + handleTouchDown(x, y); + break; + case MotionEvent.ACTION_MOVE: + handleTouchMove(x, y); + break; + case MotionEvent.ACTION_UP: + handleTouchUp(); + break; + } + + invalidate(); // 重绘 + return true; + } + + private void handleTouchDown(float x, float y) { + mSelectedPoints.clear(); + mLines.clear(); + mIsDrawing = true; + + // 检查是否点击了某个点 + for (GesturePoint point : mPoints) { + float distance = (float) Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2)); + if (distance < Math.min(getWidth(), getHeight()) / 10f) { // 如果在点的范围内 + selectPoint(point); + break; + } + } + } + + private void handleTouchMove(float x, float y) { + if (!mIsDrawing) return; + + // 更新当前手指位置 + mCurrentPoint = new GesturePoint(x, y, -1); + + // 检查是否有新的点被经过 + for (GesturePoint point : mPoints) { + if (point.isSelected) continue; // 已经选过的点不再处理 + + float distance = (float) Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2)); + if (distance < Math.min(getWidth(), getHeight()) / 10f) { // 如果在点的范围内 + selectPoint(point); + break; + } + } + } + + private void handleTouchUp() { + if (!mIsDrawing) return; + + mIsDrawing = false; + mCurrentPoint = null; + + // 通知手势完成 + if (mListener != null && !mSelectedPoints.isEmpty()) { + mListener.onGestureComplete(mSelectedPoints); + } + + // 重置状态 + reset(); + } + + private void selectPoint(GesturePoint point) { + if (!point.isSelected) { + point.isSelected = true; + mSelectedPoints.add(point.index); + + // 添加连接线(如果不是第一个点) + if (mSelectedPoints.size() > 1) { + int lastIndex = mSelectedPoints.get(mSelectedPoints.size() - 2); + GesturePoint lastPoint = mPoints.get(lastIndex); + mLines.add(new GestureLine(lastPoint, point)); + } + } + } + + private void reset() { + for (GesturePoint point : mPoints) { + point.isSelected = false; + } + mSelectedPoints.clear(); + mLines.clear(); + mCurrentPoint = null; + } + + public void setOnGestureCompleteListener(OnGestureCompleteListener listener) { + this.mListener = listener; + } + + /** + * 清除当前手势并重置视图 + */ + public void clearGesture() { + reset(); + invalidate(); + } + + /** + * 获取当前手势点的数量 + */ + public int getSelectedPointsCount() { + return mSelectedPoints.size(); + } +} \ No newline at end of file diff --git a/src/notes/ui/LoginRegisterActivity.java b/src/notes/ui/LoginRegisterActivity.java index 28d4516..b2ac984 100644 --- a/src/notes/ui/LoginRegisterActivity.java +++ b/src/notes/ui/LoginRegisterActivity.java @@ -29,10 +29,12 @@ public class LoginRegisterActivity extends AppCompatActivity { private View tabIndicator; private EditText etUsername; private EditText etPassword; + private EditText etConfirmPassword; private ImageView ivPasswordVisibility; private TextView tvErrorMessage; private Button btnAction; private TextView tvForgotPassword; + private LinearLayout confirmPasswordLayout; // 数据库帮助类 private UserDatabaseHelper dbHelper; @@ -51,8 +53,12 @@ public class LoginRegisterActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login_register); - // 检查是否已登录,如果已登录则直接跳转到主页面 - if (userManager.isLoggedIn()) { + //通过getInstance获取已初始化的UserManager实例 + UserManager userManager = UserManager.getInstance(this); + + //安全调用:先判空,再调用方法,避免null + // 检查是否已登录且在有效期内,如果已登录且在有效期内则直接跳转到主页面 + if (userManager != null && userManager.isLoginValid()) { startActivity(new Intent(this, NotesListActivity.class)); finish(); return; @@ -73,10 +79,12 @@ public class LoginRegisterActivity extends AppCompatActivity { 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); 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); @@ -147,11 +155,12 @@ public class LoginRegisterActivity extends AppCompatActivity { // 更新指示器位置 LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) tabIndicator.getLayoutParams(); - params.leftMargin = 40; // 左侧位置 + params.leftMargin = 40; // 左侧位置 (登录tab的位置) tabIndicator.setLayoutParams(params); btnAction.setText("登录"); tvForgotPassword.setVisibility(View.VISIBLE); + confirmPasswordLayout.setVisibility(View.GONE); // 隐藏确认密码输入框 } else { // 注册模式 tvRegisterTab.setTextColor(getResources().getColor(android.R.color.holo_orange_dark)); @@ -159,11 +168,12 @@ public class LoginRegisterActivity extends AppCompatActivity { // 更新指示器位置 LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) tabIndicator.getLayoutParams(); - params.leftMargin = 120; // 右侧位置 + params.leftMargin = 120; // 右侧位置 (注册tab的位置) tabIndicator.setLayoutParams(params); btnAction.setText("注册"); tvForgotPassword.setVisibility(View.GONE); + confirmPasswordLayout.setVisibility(View.VISIBLE); // 显示确认密码输入框 } // 清除错误消息 @@ -227,6 +237,7 @@ 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)) { @@ -248,6 +259,16 @@ public class LoginRegisterActivity extends AppCompatActivity { showError("密码长度不能少于6位"); return; } + + if (TextUtils.isEmpty(confirmPassword)) { + showError("请确认密码"); + return; + } + + if (!password.equals(confirmPassword)) { + showError("两次输入的密码不一致"); + return; + } // 检查用户名是否已存在 if (dbHelper.isUsernameExists(username)) { @@ -260,9 +281,10 @@ public class LoginRegisterActivity extends AppCompatActivity { if (success) { Toast.makeText(this, "注册成功", Toast.LENGTH_SHORT).show(); - // 自动切换到登录模式 + // 自动切换到登录模式并清空输入框 isLoginMode = true; updateUIForLoginMode(); + clearInputFields(); // 清空输入框 } else { showError("注册失败,请重试"); } @@ -275,6 +297,16 @@ 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); + } /** * 显示忘记密码对话框 diff --git a/src/notes/ui/NoteEditActivity.java b/src/notes/ui/NoteEditActivity.java index f3c30fc..d3e4097 100644 --- a/src/notes/ui/NoteEditActivity.java +++ b/src/notes/ui/NoteEditActivity.java @@ -64,6 +64,7 @@ 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.ui.DateTimePickerDialog.OnDateTimeSetListener; @@ -74,6 +75,8 @@ 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; @@ -154,15 +157,21 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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; // 选择系统图片 // 清单模式下的标记符号 - public static final String TAG_CHECKED = String.valueOf('\u221A'); // 对勾符号 √ - public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); // 方框符号 □ + 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; // 当前解锁尝试次数 private LinearLayout mEditTextList; // 清单模式下的编辑框列表容器 private String mUserQuery; // 用户搜索查询词(从搜索跳转过来时使用) @@ -191,6 +200,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, return; } initResources(); // 初始化视图资源 + mPrivacyLockManager = new PrivacyLockManager(this); // 初始化隐私锁管理器 + + // 检查是否需要解锁 + if (mWorkingNote != null && mPrivacyLockManager.isNoteLocked(mWorkingNote.getNoteId())) { + showUnlockDialog(); + return; // 暂停初始化,等待解锁 + } // 添加对OnBackPressedDispatcher的支持,以便处理手势返回 try { @@ -351,7 +367,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, // 普通文本模式,高亮搜索词 String content = mWorkingNote.getContent(); String formatInfo = mWorkingNote.getRichTextFormat(); - + // 恢复富文本格式 if (formatInfo != null && !formatInfo.isEmpty()) { // 使用序列化的格式信息恢复文本格式 @@ -361,7 +377,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, // 没有格式信息时,正常显示高亮文本 mNoteEditor.setText(getHighlightQueryResult(content, mUserQuery)); } - + mNoteEditor.setSelection(mNoteEditor.getText().length()); // 显示富文本格式工具栏 @@ -386,10 +402,27 @@ public class NoteEditActivity extends Activity implements OnClickListener, // 显示提醒信息 showAlertHeader(); + // 显示锁图标(如果便签被锁定) + showLockIcon(); + // 初始化时设置格式工具栏背景 updateFormatToolbarBackground(); } + /** + * 显示锁图标(如果便签被锁定) + */ + private void showLockIcon() { + if (mPrivacyLockManager.isNoteLocked(mWorkingNote.getNoteId())) { + // 在修改时间旁边添加锁图标 + if (mNoteHeaderHolder.tvModified != null) { + // 添加锁图标到修改时间文本末尾 + String originalText = mNoteHeaderHolder.tvModified.getText().toString(); + mNoteHeaderHolder.tvModified.setText(originalText + " 🔒"); + } + } + } + /** * 显示或隐藏提醒相关的UI组件 */ @@ -425,75 +458,75 @@ public class NoteEditActivity extends Activity implements OnClickListener, } 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 onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); +} - /** - * 保存活动状态 - */ - @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"); +/** + * 保存活动状态 + */ +@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"); +} - /** - * 处理从地图应用返回的位置数据 - */ - @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(); - - // 更新对话框中的输入框 - 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(); +/** + * 处理从地图应用返回的位置数据 + */ +@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(); + + // 更新对话框中的输入框 + 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(); } } - } 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 (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")中 @@ -507,1349 +540,1512 @@ public class NoteEditActivity extends Activity implements OnClickListener, } */ - if (imageUri != null) { - insertImageToEditor(imageUri); - } + if (imageUri != null) { + insertImageToEditor(imageUri); } } +} - /** - * 将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; - } +/** + * 将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; } +} - /** - * 在编辑器光标位置插入图片 - */ - 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); - } - - if (bitmap != null) { - android.text.style.ImageSpan imageSpan = new android.text.style.ImageSpan(this, bitmap); - - // 获取当前光标位置 - int cursorPos = mNoteEditor.getSelectionStart(); - - // 创建包含图片的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); - - // 更新编辑器内容 - mNoteEditor.setText(ssb); - mNoteEditor.setSelection(cursorPos + 2); // 将光标移到图片后面 - - // 保存图片信息到数据库 - saveImageToDatabase(imagePath); - } - } catch (Exception e) { - e.printStackTrace(); - Toast.makeText(this, "插入图片失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); - } - } - } - - /** - * 保存图片信息到数据库 - */ - 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()); - } - } - - /** - * 从URI获取真实路径 - */ - private String getRealPathFromURI(Uri contentUri) { - String[] proj = {android.provider.MediaStore.Images.Media.DATA}; +/** + * 在编辑器光标位置插入图片 + */ +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 { - 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; + // 获取图片的实际路径 + 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); + } + + if (bitmap != null) { + android.text.style.ImageSpan imageSpan = new android.text.style.ImageSpan(this, bitmap); + + // 获取当前光标位置 + int cursorPos = mNoteEditor.getSelectionStart(); + + // 创建包含图片的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); + + // 更新编辑器内容 + mNoteEditor.setText(ssb); + mNoteEditor.setSelection(cursorPos + 2); // 将光标移到图片后面 + + // 保存图片信息到数据库 + saveImageToDatabase(imagePath); } } catch (Exception e) { e.printStackTrace(); + Toast.makeText(this, "插入图片失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } - 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 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()); + } +} - if (mFontSizeSelector.getVisibility() == View.VISIBLE - && !inRangeOfView(mFontSizeSelector, ev)) { - mFontSizeSelector.setVisibility(View.GONE); - return true; +/** + * 从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; } - return super.dispatchTouchEvent(ev); + } catch (Exception e) { + e.printStackTrace(); } + return contentUri.toString(); +} - /** - * 判断触摸点是否在指定视图范围内 - */ - 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; - } +/** + * 触摸事件分发,用于点击选择器外部时关闭选择器 + */ +@Override +public boolean dispatchTouchEvent(MotionEvent ev) { + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mNoteBgColorSelector, ev)) { + mNoteBgColorSelector.setVisibility(View.GONE); 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(); + if (mFontSizeSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mFontSizeSelector, ev)) { + mFontSizeSelector.setVisibility(View.GONE); + return true; } + return super.dispatchTouchEvent(ev); +} - /** - * Activity暂停时保存笔记 - */ - @Override - protected void onPause() { - super.onPause(); - // 保存当前编辑的笔记 - if(saveNote()) { - Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); - } - clearSettingState(); // 清理设置面板状态 - - // 隐藏富文本格式工具栏 - if (mFormatToolbar != null) { - mFormatToolbar.setVisibility(View.GONE); - } +/** + * 判断触摸点是否在指定视图范围内 + */ +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; +} - /** - * 更新关联的桌面小部件 - */ - 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); // 设置活动结果 +/** + * 初始化视图资源和事件监听 + */ +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); } - /** - * 点击事件处理 - */ - 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)); - } - 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(); - } + 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); - /** - * 返回键按下事件处理 - */ - @Override - public void onBackPressedDispatcher() { - // 如果有设置面板打开,先关闭面板 - if(clearSettingState()) { - return; - } + // 设置图片删除处理 + setupImageDeletionHandling(); +} - // 保存笔记后执行默认返回操作 - saveNote(); - super.onBackPressed(); +/** + * Activity暂停时保存笔记 + */ +@Override +protected void onPause() { + super.onPause(); + // 保存当前编辑的笔记 + if(saveNote()) { + Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); } + clearSettingState(); // 清理设置面板状态 - /** - * 清理设置面板状态(关闭打开的面板) - * @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 false; + // 隐藏富文本格式工具栏 + if (mFormatToolbar != null) { + mFormatToolbar.setVisibility(View.GONE); } +} - /** - * 背景颜色改变回调(NoteSettingChangedListener接口方法) - */ - public void onBackgroundColorChanged() { - // 显示新选中颜色的标记 - findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( - View.VISIBLE); - // 更新编辑器面板和头部面板的背景 - mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); - mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); - - // 根据当前背景颜色调整富文本格式工具栏的背景 - updateFormatToolbarBackground(); +/** + * 更新关联的桌面小部件 + */ +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; } - /** - * 根据当前便签背景颜色更新格式工具栏的背景 - */ - private void updateFormatToolbarBackground() { - if (mFormatToolbar != null) { - // 获取当前背景颜色资源ID - int bgColorResId = mWorkingNote.getBgColorResId(); + // 设置要更新的小部件ID + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { + mWorkingNote.getWidgetId() + }); - // 根据背景颜色计算合适的工具栏背景色 - int toolbarBgColor = calculateToolbarBackgroundColor(bgColorResId); + sendBroadcast(intent); // 发送广播通知小部件更新 + setResult(RESULT_OK, intent); // 设置活动结果 +} - // 设置工具栏背景 - mFormatToolbar.setBackgroundColor(toolbarBgColor); +/** + * 点击事件处理 + */ +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)); } + 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(); } +} - /** - * 根据便签背景资源计算工具栏合适的背景颜色 - * @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); // 半透明白色 - } +/** + * 返回键按下事件处理 + */ +@Override +public void onBackPressedDispatcher() { + // 如果有设置面板打开,先关闭面板 + if(clearSettingState()) { + return; } - /** - * 组合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; + // 保存笔记后执行默认返回操作 + 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 false; +} - /** - * 准备选项菜单 - */ - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - if (isFinishing()) { - return true; - } - clearSettingState(); // 清理设置面板状态 - menu.clear(); // 清空菜单 +/** + * 背景颜色改变回调(NoteSettingChangedListener接口方法) + */ +public void onBackgroundColorChanged() { + // 显示新选中颜色的标记 + findViewById(NoteEditActivity.sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.VISIBLE); + // 更新编辑器面板和头部面板的背景 + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); + + // 根据当前背景颜色调整富文本格式工具栏的背景 + updateFormatToolbarBackground(); +} - // 根据笔记类型加载不同的菜单 - if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) { - getMenuInflater().inflate(R.menu.call_note_edit, menu); - } else { - getMenuInflater().inflate(R.menu.note_edit, menu); - } +/** + * 根据当前便签背景颜色更新格式工具栏的背景 + */ +private void updateFormatToolbarBackground() { + if (mFormatToolbar != null) { + // 获取当前背景颜色资源ID + int bgColorResId = mWorkingNote.getBgColorResId(); - // 设置清单模式/普通模式菜单项标题 - 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); - } + // 根据背景颜色计算合适的工具栏背景色 + int toolbarBgColor = calculateToolbarBackgroundColor(bgColorResId); - // 根据是否有提醒设置菜单项显示 - 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); - } + // 设置工具栏背景 + mFormatToolbar.setBackgroundColor(toolbarBgColor); + } +} - // 根据是否有位置提醒设置菜单项显示 - 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); - } +/** + * 根据便签背景资源计算工具栏合适的背景颜色 + * @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()) { 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 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; + // 设置清单模式/普通模式菜单项标题 + 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); + } - case R.id.menu_delete_location_remind: - // 删除位置提醒 - mWorkingNote.setAlertLocation(0, 0, 0, "", false); - break; - default: - break; - } - return true; + // 根据是否有提醒设置菜单项显示 + 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); } - /** - * 设置提醒时间 - */ - 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); - } - }); - d.show(); + // 根据是否有位置提醒设置菜单项显示 + 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); } - /** - * 设置位置提醒 - */ - 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; +} + +/** + * 菜单项点击处理 + */ +@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); + } + }); + 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); + + 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.setData(Uri.parse("geo:0,0?q=map")); - if (intent.resolveActivity(getPackageManager()) != null) { - startActivityForResult(intent, REQUEST_CODE_SELECT_LOCATION); + // 如果还是失败,尝试更通用的方式 + Intent chooserIntent = Intent.createChooser(intent, "选择地图应用"); + if (chooserIntent.resolveActivity(getPackageManager()) != null) { + startActivityForResult(chooserIntent, 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(); - } + Toast.makeText(NoteEditActivity.this, "没有找到地图应用", Toast.LENGTH_SHORT).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米 - } + builder.setView(layout); - // 设置位置提醒 - mWorkingNote.setAlertLocation(latitude, longitude, radius, locationName, true); - } catch (NumberFormatException e) { - Toast.makeText(NoteEditActivity.this, "请输入有效的位置信息", Toast.LENGTH_SHORT).show(); + // 确定按钮 + 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米 } - } - }); - // 取消按钮 - builder.setNegativeButton(android.R.string.cancel, null); + // 设置位置提醒 + mWorkingNote.setAlertLocation(latitude, longitude, radius, locationName, true); + } catch (NumberFormatException e) { + Toast.makeText(NoteEditActivity.this, "请输入有效的位置信息", Toast.LENGTH_SHORT).show(); + } + } + }); - // 显示对话框 - 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); // 启动分享选择器 - } + // 取消按钮 + builder.setNegativeButton(android.R.string.cancel, null); - /** - * 创建新笔记 - */ - private void createNewNote() { - // 先保存当前编辑的笔记 - saveNote(); + // 显示对话框 + 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); // 启动分享选择器 +} - // 结束当前活动并启动新的编辑活动 - 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 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); +} - /** - * 删除当前笔记 - */ - 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"); - } +/** + * 删除当前笔记 + */ +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"); + } - // 根据同步模式选择删除方式 - 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"); - } + // 根据同步模式选择删除方式 + 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); // 标记为已删除 } + mWorkingNote.markDeleted(true); // 标记为已删除 +} - /** - * 检查是否处于同步模式 - */ - private boolean isSyncMode() { - return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; - } +/** + * 检查是否处于同步模式 + */ +private boolean isSyncMode() { + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; +} - /** - * 提醒时间改变回调(NoteSettingChangedListener接口方法) - */ - public void onClockAlertChanged(long date, boolean set) { - // 对于未保存的笔记,先保存再设置提醒 - if (!mWorkingNote.existInDatabase()) { - saveNote(); - } +/** + * 提醒时间改变回调(NoteSettingChangedListener接口方法) + */ +public void onClockAlertChanged(long date, boolean set) { + // 对于未保存的笔记,先保存再设置提醒 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } - 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); - } + 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 { - // 笔记为空,无法设置提醒 - Log.e(TAG, "Clock alert setting error"); - showToast(R.string.error_note_empty_for_clock); + // 设置提醒 + alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); } + } 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(); - } +/** + * 位置提醒改变回调(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(); - } + if (mWorkingNote.getNoteId() > 0) { + showAlertHeader(); // 更新提醒显示 + // 位置提醒的地理围栏实现将在后续版本中添加 + if (set) { + Toast.makeText(this, "位置提醒已设置", Toast.LENGTH_SHORT).show(); } else { - // 笔记为空,无法设置提醒 - Log.e(TAG, "Location alert setting error"); - showToast(R.string.error_note_empty_for_clock); + Toast.makeText(this, "位置提醒已取消", Toast.LENGTH_SHORT).show(); } + } else { + // 笔记为空,无法设置提醒 + Log.e(TAG, "Location alert setting error"); + showToast(R.string.error_note_empty_for_clock); } +} - /** - * 小部件改变回调(NoteSettingChangedListener接口方法) - */ - public void onWidgetChanged() { - updateWidget(); // 更新关联的小部件 +/** + * 小部件改变回调(NoteSettingChangedListener接口方法) + */ +public void onWidgetChanged() { + updateWidget(); // 更新关联的小部件 +} + +/** + * 清单模式下删除编辑项(OnTextViewChangeListener接口方法) + */ +public void onEditTextDelete(int index, String text) { + int childCount = mEditTextList.getChildCount(); + if (childCount == 1) { + return; // 只有一个项时不删除 } - /** - * 清单模式下删除编辑项(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); + } - // 更新后面所有项的索引 - 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); +} - // 移除指定位置的视图 - 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"); } - /** - * 清单模式下插入新编辑项(OnTextViewChangeListener接口方法) - */ - public void onEditTextEnter(int index, String text) { - // 安全检查 - if(index > mEditTextList.getChildCount()) { - Log.e(TAG, "Index out of mEditTextList boundrary, should not happen"); - } + // 创建新列表项并插入到指定位置 + 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); + } +} - // 创建新列表项并插入到指定位置 - 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); +/** + * 切换到清单模式 + */ +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 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(); + // 切换显示模式 + mNoteEditor.setVisibility(View.GONE); + mEditTextList.setVisibility(View.VISIBLE); +} - // 切换显示模式 - 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(); + } } + return spannable; +} - /** - * 高亮显示搜索关键词 - */ - 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(); +/** + * 创建清单模式下的列表项视图 + */ +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); } } - return spannable; + }); + + // 解析清单标记符号 + 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(); } - /** - * 创建清单模式下的列表项视图 - */ - 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); - } - } - }); - - // 解析清单标记符号 - 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; +} - 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; + } + // 根据是否有文本显示或隐藏复选框 + 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); } +} - /** - * 文本变化回调(OnTextViewChangeListener接口方法) - */ - public void onTextChange(int index, boolean hasText) { - if (index >= mEditTextList.getChildCount()) { - Log.e(TAG, "Wrong index, should not happen"); - return; +/** + * 清单模式改变回调(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); } - // 根据是否有文本显示或隐藏复选框 - 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); + } else { + // 切换到普通模式 + if (!getWorkingText()) { + // 清理清单标记符号 + mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", + "")); } - } + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mEditTextList.setVisibility(View.GONE); + mNoteEditor.setVisibility(View.VISIBLE); - /** - * 清单模式改变回调(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); - - // 显示富文本格式工具栏 - if (mFormatToolbar != null) { - mFormatToolbar.setVisibility(View.VISIBLE); - } + // 显示富文本格式工具栏 + if (mFormatToolbar != null) { + mFormatToolbar.setVisibility(View.VISIBLE); } } +} - /** - * 从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; - } else { - // 未完成的项 - sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); - } +/** + * 从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; + } else { + // 未完成的项 + sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); } } - mWorkingNote.setWorkingText(sb.toString()); - } else { - // 普通模式:获取带格式的文本 - mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); - - // 保存格式化信息到工作文本 - String formatInfo = RichTextFormatUtils.serializeFormatInfo(mNoteEditor.getText()); - mWorkingNote.setRichTextFormat(formatInfo); - - // TODO: 保存图片信息到工作笔记 - 这需要更复杂的数据结构来存储图片位置和路径 - // 当前实现会在普通文本模式中保留图片,但在清单模式中会丢失图片 } - return hasChecked; - } + mWorkingNote.setWorkingText(sb.toString()); + } else { + // 普通模式:获取带格式的文本 + mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); - /** - * 保存笔记到数据库 - * @return 保存是否成功 - */ - private boolean saveNote() { - getWorkingText(); // 从UI获取最新文本 - boolean saved = mWorkingNote.saveNote(); // 保存到数据库 - if (saved) { - setResult(RESULT_OK); // 设置活动结果 - } - return saved; + // 保存格式化信息到工作文本 + String formatInfo = RichTextFormatUtils.serializeFormatInfo(mNoteEditor.getText()); + mWorkingNote.setRichTextFormat(formatInfo); + + // TODO: 保存图片信息到工作笔记 - 这需要更复杂的数据结构来存储图片位置和路径 + // 当前实现会在普通文本模式中保留图片,但在清单模式中会丢失图片 } + return hasChecked; +} - /** - * 发送笔记到桌面快捷方式 - */ - private void sendToDesktop() { - // 确保笔记已保存到数据库 - if (!mWorkingNote.existInDatabase()) { - saveNote(); - } +/** + * 保存笔记到数据库 + * @return 保存是否成功 + */ +private boolean saveNote() { + getWorkingText(); // 从UI获取最新文本 + boolean saved = mWorkingNote.saveNote(); // 保存到数据库 + if (saved) { + setResult(RESULT_OK); // 设置活动结果 + } + return saved; +} - 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); - } +/** + * 发送笔记到桌面快捷方式 + */ +private void sendToDesktop() { + // 确保笔记已保存到数据库 + if (!mWorkingNote.existInDatabase()) { + saveNote(); } - /** - * 生成桌面快捷方式的标题 - */ - 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; + 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); } +} - /** - * 处理富文本格式化 - * @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; - } - - 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 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; +} + +/** + * 处理富文本格式化 + * @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; } - - /** - * 显示图片选择对话框 - */ - 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(); - } - }); + int selectionStart = mNoteEditor.getSelectionStart(); + int selectionEnd = mNoteEditor.getSelectionEnd(); - // 添加按钮到布局 - layout.addView(albumBtn); - layout.addView(systemBtn); + // 确保有选中的文本 + if (selectionStart >= 0 && selectionEnd >= 0 && selectionStart != selectionEnd) { + Spannable str = mNoteEditor.getText(); - builder.setView(layout); - builder.setNegativeButton("取消", null); - builder.show(); + 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 void showSystemImagesDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("选择系统图片"); +/** + * 显示图片选择对话框 + */ +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(); + } + }); - // 创建水平排列的图片布局 - LinearLayout layout = new LinearLayout(this); - layout.setOrientation(LinearLayout.HORIZONTAL); - layout.setPadding(20, 20, 20, 20); + // 添加按钮到布局 + layout.addView(albumBtn); + layout.addView(systemBtn); - // 创建猫图片 - 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(); - } - } - }); + builder.setView(layout); + builder.setNegativeButton("取消", null); + builder.show(); +} - // 创建狗图片 - 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(); +/** + * 显示系统图片选择对话框 + */ +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(); + } } - 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(); - } + }); + + // 创建狗图片 + 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(); } - }); + } + }); - // 添加图片到布局 - layout.addView(catImageView); - layout.addView(dogImageView); + // 添加图片到布局 + layout.addView(catImageView); + layout.addView(dogImageView); - builder.setView(layout); - builder.setNegativeButton("取消", null); - builder.show(); + 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 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(); } +} - /** - * 打开系统图片选择器 - */ - 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(); +/** + * 切换样式(粗体、斜体) + */ +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; } } - /** - * 切换样式(粗体、斜体) - */ - 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; - } - } + // 如果没有对应样式,则添加 + 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); - // 如果没有对应样式,则添加 - if (!hasStyle) { - str.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + if (spans.length > 0) { + // 如果已存在下划线,则移除 + for (UnderlineSpan span : spans) { + str.removeSpan(span); } + } else { + // 如果不存在下划线,则添加 + str.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } +} - /** - * 切换下划线 - */ - private void toggleUnderlineSpan(Spannable str, int start, int end) { - UnderlineSpan[] spans = str.getSpans(start, end, UnderlineSpan.class); +/** + * 切换删除线 + */ +private void toggleStrikethroughSpan(Spannable str, int start, int end) { + StrikethroughSpan[] spans = str.getSpans(start, end, StrikethroughSpan.class); - if (spans.length > 0) { - // 如果已存在下划线,则移除 - for (UnderlineSpan span : spans) { - str.removeSpan(span); - } - } else { - // 如果不存在下划线,则添加 - str.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + if (spans.length > 0) { + // 如果已存在删除线,则移除 + for (StrikethroughSpan span : spans) { + str.removeSpan(span); } + } else { + // 如果不存在删除线,则添加 + str.setSpan(new StrikethroughSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } +} - /** - * 切换删除线 - */ - private void toggleStrikethroughSpan(Spannable str, int start, int end) { - StrikethroughSpan[] spans = str.getSpans(start, end, StrikethroughSpan.class); +/** + * 显示Toast提示 + */ +private void showToast(int resId) { + showToast(resId, Toast.LENGTH_SHORT); +} + +private void showToast(int resId, int duration) { + Toast.makeText(this, resId, duration).show(); +} - if (spans.length > 0) { - // 如果已存在删除线,则移除 - for (StrikethroughSpan span : spans) { - str.removeSpan(span); +/** + * 处理编辑器中的按键事件,包括删除图片 + */ +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); + } + + return true; // 消费此事件 + } + } } - } else { - // 如果不存在删除线,则添加 - str.setSpan(new StrikethroughSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + return false; } + }); +} + + /** + * 显示解锁对话框 + */ + 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); + }); + + // 手势解锁按钮 + btnUnlockGesture.setOnClickListener(v -> { + showGestureUnlockDialog(dialog); + }); + + dialog.show(); } /** - * 显示Toast提示 + * 显示密码解锁对话框 */ - private void showToast(int resId) { - showToast(resId, Toast.LENGTH_SHORT); - } + 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()); + }); + + // 确认按钮点击 + btnConfirm.setOnClickListener(v -> { + String password = etPassword.getText().toString().trim(); + + 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; + } + + tvErrorMessage.setText("密码错误 (" + mUnlockAttempts + "/" + MAX_UNLOCK_ATTEMPTS + ")"); + tvErrorMessage.setVisibility(View.VISIBLE); + etPassword.setText(""); // 清空密码框 + } + }); - private void showToast(int resId, int duration) { - Toast.makeText(this, resId, duration).show(); + // 取消按钮点击 + btnCancel.setOnClickListener(v -> dialog.dismiss()); + + dialog.show(); } - + /** - * 处理编辑器中的按键事件,包括删除图片 + * 显示手势解锁对话框 */ - 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); - } - - return true; // 消费此事件 - } - } + 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; + } + + // 设置手势完成监听器 + 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; } - return false; + + tvStatus.setText("验证失败 (" + mUnlockAttempts + "/" + MAX_UNLOCK_ATTEMPTS + ")"); + gestureLockView.clearGesture(); } }); + + // 重置按钮点击 + btnReset.setOnClickListener(v -> { + gestureLockView.clearGesture(); + tvStatus.setText(""); + }); + + dialog.show(); } } \ No newline at end of file diff --git a/src/notes/ui/NotesListActivity.java b/src/notes/ui/NotesListActivity.java index f1b1a47..2971194 100644 --- a/src/notes/ui/NotesListActivity.java +++ b/src/notes/ui/NotesListActivity.java @@ -32,6 +32,7 @@ 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; @@ -54,7 +55,9 @@ 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.ListView; import android.widget.PopupMenu; import android.widget.TextView; @@ -63,6 +66,7 @@ import android.widget.Toast; import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.UserDatabaseHelper; import net.micode.notes.gtask.remote.GTaskSyncService; import net.micode.notes.model.WorkingNote; import net.micode.notes.tool.BackupUtils; @@ -72,12 +76,15 @@ 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; /** * 笔记列表主活动,显示笔记和文件夹的列表,是应用的入口界面之一。 @@ -119,6 +126,7 @@ 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"; // 日志标签 @@ -994,6 +1002,10 @@ 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; @@ -1088,7 +1100,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt public void onClick(DialogInterface dialog, int which) { // 清除登录状态 UserManager userManager = UserManager.getInstance(NotesListActivity.this); - userManager.logout(); + if (userManager != null) { + userManager.logout(); + } // 跳转到登录界面 Intent intent = new Intent(NotesListActivity.this, LoginRegisterActivity.class); @@ -1103,7 +1117,101 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt /** * 启动设置活动 + * 显示添加用户对话框 */ + 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); @@ -1190,16 +1298,10 @@ 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()) { - if (mNotesListView.startActionMode(mModeCallBack) != null) { - // 启动成功,设置当前项为选中状态 - mModeCallBack.onItemCheckedStateChanged(null, position, id, true); - // 提供触觉反馈(震动) - mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - } else { - Log.e(TAG, "startActionMode fails"); - } + showPrivacyLockMenu(mFocusNoteDataItem.getId()); } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { // 如果长按的是文件夹,设置上下文菜单监听器 mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); @@ -1207,4 +1309,240 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } return false; // 返回false,让系统继续处理长按事件(显示上下文菜单等) } + + /** + * 显示隐私锁菜单 + * @param noteId 便签ID + */ + private void showPrivacyLockMenu(long noteId) { + boolean isLocked = mPrivacyLockManager.isNoteLocked(noteId); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(isLocked ? "移除隐私锁" : "添加隐私锁"); + + 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; // 不需要显示确认对话框 + } + + builder.setNegativeButton("取消", null); + builder.show(); + } + + /** + * 显示隐私锁类型选择对话框 + * @param noteId 便签ID + */ + private void showPrivacyLockTypeSelectionDialog(long noteId) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("添加隐私锁"); + + String[] options; + + // 检查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[]{"密码锁"}; + } + + builder.setItems(options, 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) { + // 手势锁 + showGestureLockDialog(noteId); + } + } + }); + + 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()); + + 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(""); + + 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(); + } 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(); + } + } + }); + + // 重置按钮点击 + btnReset.setOnClickListener(v -> { + gestureLockView.clearGesture(); + if (firstGesture[0] != null) { + firstGesture[0] = null; + tvInstruction.setText("请绘制连续手势(至少4个点)"); + } + tvStatus.setText(""); + }); + + dialog.setOnCancelListener(d -> { + // 取消时不做任何操作 + }); + + dialog.show(); + } } \ No newline at end of file diff --git a/src/notes/ui/NotesListItem.java b/src/notes/ui/NotesListItem.java index 52a530e..20037c9 100644 --- a/src/notes/ui/NotesListItem.java +++ b/src/notes/ui/NotesListItem.java @@ -27,6 +27,7 @@ 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; /** @@ -35,6 +36,7 @@ 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; // 通话记录联系人姓名 @@ -51,6 +53,7 @@ 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); @@ -129,12 +132,15 @@ public class NotesListItem extends LinearLayout { // 设置相对时间显示(如"2分钟前"、"昨天"等) mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + // 显示锁图标(如果便签被锁定) + showLockIcon(context, data); + // 根据位置和类型设置背景 setBackground(data); } /** - * 根据数据项的位置和类型设置不同的背景 + * 根据位置和类型设置不同的背景 * 实现列表项的分组视觉效果(第一个、最后一个、中间项等) * @param data 笔记数据项 */ @@ -161,6 +167,20 @@ 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对象