diff --git a/doc/小米便签泛读、标注和维护报告文档.docx b/doc/小米便签泛读、标注和维护报告文档.docx index 697a69e..70ef9b5 100644 Binary files a/doc/小米便签泛读、标注和维护报告文档.docx and b/doc/小米便签泛读、标注和维护报告文档.docx differ diff --git a/doc/阅读分析和维护小米便签.xlsx b/doc/阅读分析和维护小米便签.xlsx new file mode 100644 index 0000000..d6a6e7d Binary files /dev/null and b/doc/阅读分析和维护小米便签.xlsx differ diff --git a/doc/阅读维护小米便签的团队自评报告.xlsx b/doc/阅读维护小米便签的团队自评报告.xlsx new file mode 100644 index 0000000..2b4fae7 Binary files /dev/null and b/doc/阅读维护小米便签的团队自评报告.xlsx differ diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 1b4546c..08d803a 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -2,9 +2,12 @@ + + + + + " + Notes.ID_TRASH_FOLDER + + " AND " + NoteColumns.DELETED + "=0" + + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; + + // 用于 NotesListActivity 的搜索查询 + private static String NOTES_LIST_SEARCH_QUERY = "SELECT " + TABLE.NOTE + "." + NoteColumns.ID + "," + + TABLE.NOTE + "." + NoteColumns.ALERTED_DATE + "," + + TABLE.NOTE + "." + NoteColumns.BG_COLOR_ID + "," + + TABLE.NOTE + "." + NoteColumns.CREATED_DATE + "," + + TABLE.NOTE + "." + NoteColumns.HAS_ATTACHMENT + "," + + TABLE.NOTE + "." + NoteColumns.MODIFIED_DATE + "," + + TABLE.NOTE + "." + NoteColumns.NOTES_COUNT + "," + + TABLE.NOTE + "." + NoteColumns.PARENT_ID + "," + + TABLE.NOTE + "." + NoteColumns.SNIPPET + "," + + TABLE.NOTE + "." + NoteColumns.TYPE + "," + + TABLE.NOTE + "." + NoteColumns.WIDGET_ID + "," + + TABLE.NOTE + "." + NoteColumns.WIDGET_TYPE + "," + + TABLE.NOTE + "." + NoteColumns.PINNED + "," + + TABLE.NOTE + "." + NoteColumns.ENCRYPTED + + " FROM " + TABLE.NOTE + + " LEFT JOIN " + TABLE.DATA + " ON " + TABLE.NOTE + "." + NoteColumns.ID + "=" + TABLE.DATA + "." + DataColumns.NOTE_ID + " AND " + TABLE.DATA + "." + DataColumns.MIME_TYPE + "='" + Notes.DataConstants.NOTE + "'" + + " WHERE (" + NoteColumns.SNIPPET + " LIKE ? OR data." + DataColumns.DATA3 + " LIKE ?)" + + " AND " + NoteColumns.PARENT_ID + "=?" + + " AND " + NoteColumns.DELETED + "=0" + + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; + + // 用于 NotesListActivity 的时间搜索查询 + private static String NOTES_TIME_SEARCH_QUERY = "SELECT " + TABLE.NOTE + "." + NoteColumns.ID + "," + + TABLE.NOTE + "." + NoteColumns.ALERTED_DATE + "," + + TABLE.NOTE + "." + NoteColumns.BG_COLOR_ID + "," + + TABLE.NOTE + "." + NoteColumns.CREATED_DATE + "," + + TABLE.NOTE + "." + NoteColumns.HAS_ATTACHMENT + "," + + TABLE.NOTE + "." + NoteColumns.MODIFIED_DATE + "," + + TABLE.NOTE + "." + NoteColumns.NOTES_COUNT + "," + + TABLE.NOTE + "." + NoteColumns.PARENT_ID + "," + + TABLE.NOTE + "." + NoteColumns.SNIPPET + "," + + TABLE.NOTE + "." + NoteColumns.TYPE + "," + + TABLE.NOTE + "." + NoteColumns.WIDGET_ID + "," + + TABLE.NOTE + "." + NoteColumns.WIDGET_TYPE + "," + + TABLE.NOTE + "." + NoteColumns.PINNED + "," + + TABLE.NOTE + "." + NoteColumns.ENCRYPTED + + " FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.CREATED_DATE + " BETWEEN ? AND ?" + + " AND " + NoteColumns.PARENT_ID + "=?" + + " AND " + NoteColumns.DELETED + "=0"; + + // 用于 NotesListActivity 的回收站搜索查询 + private static String NOTES_DELETED_SEARCH_QUERY = "SELECT " + TABLE.NOTE + "." + NoteColumns.ID + "," + + TABLE.NOTE + "." + NoteColumns.ALERTED_DATE + "," + + TABLE.NOTE + "." + NoteColumns.BG_COLOR_ID + "," + + TABLE.NOTE + "." + NoteColumns.CREATED_DATE + "," + + TABLE.NOTE + "." + NoteColumns.HAS_ATTACHMENT + "," + + TABLE.NOTE + "." + NoteColumns.MODIFIED_DATE + "," + + TABLE.NOTE + "." + NoteColumns.NOTES_COUNT + "," + + TABLE.NOTE + "." + NoteColumns.PARENT_ID + "," + + TABLE.NOTE + "." + NoteColumns.SNIPPET + "," + + TABLE.NOTE + "." + NoteColumns.TYPE + "," + + TABLE.NOTE + "." + NoteColumns.WIDGET_ID + "," + + TABLE.NOTE + "." + NoteColumns.WIDGET_TYPE + "," + + TABLE.NOTE + "." + NoteColumns.PINNED + "," + + TABLE.NOTE + "." + NoteColumns.ENCRYPTED + + " FROM " + TABLE.NOTE + + " LEFT JOIN " + TABLE.DATA + " ON " + TABLE.NOTE + "." + NoteColumns.ID + "=" + TABLE.DATA + "." + DataColumns.NOTE_ID + " AND " + TABLE.DATA + "." + DataColumns.MIME_TYPE + "='" + Notes.DataConstants.NOTE + "'" + + " WHERE (" + NoteColumns.SNIPPET + " LIKE ? OR data." + DataColumns.DATA3 + " LIKE ?)" + + " AND " + NoteColumns.DELETED + "=1" + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; @Override @@ -145,7 +217,69 @@ public class NotesProvider extends ContentProvider { try { searchString = String.format("%%%s%%", searchString); c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, - new String[] { searchString }); + new String[] { searchString, searchString }); + } catch (IllegalStateException ex) { + Log.e(TAG, "got exception: " + ex.toString()); + } + break; + case URI_LIST_SEARCH: + // 处理 NotesListActivity 的搜索请求 + if (projection == null) { + projection = NoteItemData.PROJECTION; + } + + String listSearchString = uri.getQueryParameter("pattern"); + String folderId = uri.getQueryParameter("folder_id"); + + if (TextUtils.isEmpty(listSearchString) || TextUtils.isEmpty(folderId)) { + return null; + } + + try { + listSearchString = String.format("%%%s%%", listSearchString); + c = db.rawQuery(NOTES_LIST_SEARCH_QUERY, + new String[] { listSearchString, listSearchString, folderId }); + } catch (IllegalStateException ex) { + Log.e(TAG, "got exception: " + ex.toString()); + } + break; + case URI_DELETED_SEARCH: + // 处理 NotesListActivity 的回收站搜索请求 + if (projection == null) { + projection = NoteItemData.PROJECTION; + } + + String deletedSearchString = uri.getQueryParameter("pattern"); + + if (TextUtils.isEmpty(deletedSearchString)) { + return null; + } + + try { + deletedSearchString = String.format("%%%s%%", deletedSearchString); + c = db.rawQuery(NOTES_DELETED_SEARCH_QUERY, + new String[] { deletedSearchString, deletedSearchString }); + } catch (IllegalStateException ex) { + Log.e(TAG, "got exception: " + ex.toString()); + } + break; + case URI_TIME_SEARCH: + // 处理 NotesListActivity 的时间搜索请求 + if (projection == null) { + projection = NoteItemData.PROJECTION; + } + + String startTime = uri.getQueryParameter("start_time"); + String endTime = uri.getQueryParameter("end_time"); + String timeFolderId = uri.getQueryParameter("folder_id"); + + if (TextUtils.isEmpty(startTime) || TextUtils.isEmpty(endTime) || TextUtils.isEmpty(timeFolderId)) { + return null; + } + + try { + c = db.rawQuery(NOTES_TIME_SEARCH_QUERY, + new String[] { startTime, endTime, timeFolderId }); } catch (IllegalStateException ex) { Log.e(TAG, "got exception: " + ex.toString()); } diff --git a/src/main/java/net/micode/notes/model/WorkingNote.java b/src/main/java/net/micode/notes/model/WorkingNote.java index ffd1955..1fcfe9b 100644 --- a/src/main/java/net/micode/notes/model/WorkingNote.java +++ b/src/main/java/net/micode/notes/model/WorkingNote.java @@ -29,43 +29,56 @@ import net.micode.notes.data.Notes.DataColumns; import net.micode.notes.data.Notes.DataConstants; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.data.Notes.TextNote; +import net.micode.notes.tool.EncryptionUtils; import net.micode.notes.tool.ResourceParser.NoteBgResources; +/** + * WorkingNote 类用于表示正在编辑或查看的便签对象 + * 负责加载、保存和管理便签数据,包括内容、标题、提醒日期等属性 + */ public class WorkingNote { - // Note for the working note + /** 便签对象,用于存储便签的详细数据 */ private Note mNote; - // Note Id + /** 便签 ID */ private long mNoteId; - // Note content + /** 便签内容 */ private String mContent; - // Note mode + /** 便签标题 */ + private String mTitle; + /** 便签模式(普通模式或 checklist 模式) */ private int mMode; - + /** 提醒日期 */ private long mAlertDate; - + /** 修改日期 */ private long mModifiedDate; - + /** 背景颜色 ID */ private int mBgColorId; - + /** 小组件 ID */ private int mWidgetId; - + /** 小组件类型 */ private int mWidgetType; - + /** 是否置顶 */ private boolean mPinned; + /** 是否加密 */ private boolean mEncrypted; + /** 密码哈希值 */ private String mPasswordHash; - + /** 文件夹 ID */ private long mFolderId; - + /** 上下文对象 */ private Context mContext; - + /** 日志标签 */ private static final String TAG = "WorkingNote"; - + /** 是否已删除 */ private boolean mIsDeleted; - + /** 便签设置变更监听器 */ private NoteSettingChangedListener mNoteSettingStatusListener; + /** + * 数据查询投影,用于查询便签数据 + * 包含 ID、内容、MIME 类型、DATA1-DATA4 字段 + */ public static final String[] DATA_PROJECTION = new String[] { DataColumns.ID, DataColumns.CONTENT, @@ -76,6 +89,10 @@ public class WorkingNote { DataColumns.DATA4, }; + /** + * 便签查询投影,用于查询便签基本信息 + * 包含父文件夹 ID、提醒日期、背景颜色、小组件信息等 + */ public static final String[] NOTE_PROJECTION = new String[] { NoteColumns.PARENT_ID, NoteColumns.ALERTED_DATE, @@ -88,65 +105,87 @@ public class WorkingNote { NoteColumns.PASSWORD_HASH }; + /** 数据列索引:ID */ private static final int DATA_ID_COLUMN = 0; - + /** 数据列索引:内容 */ private static final int DATA_CONTENT_COLUMN = 1; - + /** 数据列索引:MIME 类型 */ private static final int DATA_MIME_TYPE_COLUMN = 2; - + /** 数据列索引:模式 */ private static final int DATA_MODE_COLUMN = 3; - + /** 便签列索引:父文件夹 ID */ private static final int NOTE_PARENT_ID_COLUMN = 0; - + /** 便签列索引:提醒日期 */ private static final int NOTE_ALERTED_DATE_COLUMN = 1; - + /** 便签列索引:背景颜色 ID */ private static final int NOTE_BG_COLOR_ID_COLUMN = 2; - + /** 便签列索引:小组件 ID */ private static final int NOTE_WIDGET_ID_COLUMN = 3; - + /** 便签列索引:小组件类型 */ private static final int NOTE_WIDGET_TYPE_COLUMN = 4; - + /** 便签列索引:修改日期 */ private static final int NOTE_MODIFIED_DATE_COLUMN = 5; - + /** 便签列索引:是否置顶 */ private static final int NOTE_PINNED_COLUMN = 6; - + /** 便签列索引:是否加密 */ private static final int NOTE_ENCRYPTED_COLUMN = 7; - + /** 便签列索引:密码哈希值 */ private static final int NOTE_PASSWORD_HASH_COLUMN = 8; - // New note construct + /** + * 创建新便签的构造方法 + * @param context 上下文对象 + * @param folderId 文件夹 ID + */ private WorkingNote(Context context, long folderId) { - mContext = context; - mAlertDate = 0; - mModifiedDate = System.currentTimeMillis(); - mFolderId = folderId; - mNote = new Note(); - mNoteId = 0; - mIsDeleted = false; - mMode = 0; - mWidgetType = Notes.TYPE_WIDGET_INVALIDE; - mPinned = false; - mEncrypted = false; - mPasswordHash = ""; - } - - // Existing note construct + mContext = context; // 初始化上下文对象 + mAlertDate = 0; // 初始化提醒日期为 0 + mModifiedDate = System.currentTimeMillis(); // 设置修改日期为当前时间 + mFolderId = folderId; // 设置文件夹 ID + mNote = new Note(); // 创建新的 Note 对象 + mNoteId = 0; // 新便签 ID 为 0 + mIsDeleted = false; // 初始状态为未删除 + mMode = 0; // 初始模式为普通模式 + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 初始小组件类型为无效 + mPinned = false; // 初始状态为未置顶 + mEncrypted = false; // 初始状态为未加密 + mPasswordHash = ""; // 初始密码哈希值为空 + mTitle = ""; // 初始标题为空 + } + + /** + * 加载现有便签的构造方法 + * @param context 上下文对象 + * @param noteId 便签 ID + * @param folderId 文件夹 ID + */ private WorkingNote(Context context, long noteId, long folderId) { - mContext = context; - mNoteId = noteId; - mFolderId = folderId; - mIsDeleted = false; - mNote = new Note(); - loadNote(); + mContext = context; // 初始化上下文对象 + mNoteId = noteId; // 设置便签 ID + mFolderId = folderId; // 设置文件夹 ID + mIsDeleted = false; // 初始状态为未删除 + mNote = new Note(); // 创建新的 Note 对象 + boolean loaded = loadNote(); // 加载便签数据 + if (!loaded) { + Log.e(TAG, "Failed to load note with id:" + noteId); // 加载失败时记录错误日志 + } } - private void loadNote() { + /** + * 加载便签基本信息 + * 从数据库中查询便签的基本属性,如文件夹 ID、提醒日期、背景颜色等 + * @return 是否加载成功 + */ + private boolean loadNote() { + // 查询便签基本信息 Cursor cursor = mContext.getContentResolver().query( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null, null, null); + boolean success = false; // 初始化加载成功标志 if (cursor != null) { if (cursor.moveToFirst()) { + // 读取便签基本属性 mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN); mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN); mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN); @@ -156,74 +195,110 @@ public class WorkingNote { mPinned = (cursor.getInt(NOTE_PINNED_COLUMN) > 0) ? true : false; mEncrypted = (cursor.getInt(NOTE_ENCRYPTED_COLUMN) > 0) ? true : false; mPasswordHash = cursor.getString(NOTE_PASSWORD_HASH_COLUMN); + success = true; // 加载成功 } - cursor.close(); + cursor.close(); // 关闭游标 } else { - Log.e(TAG, "No note with id:" + mNoteId); - throw new IllegalArgumentException("Unable to find note with id " + mNoteId); + Log.e(TAG, "No note with id:" + mNoteId); // 记录错误日志 } - loadNoteData(); + + if (success) { + return loadNoteData(); // 加载成功后加载便签详细数据 + } + return false; // 加载失败 } - private void loadNoteData() { + /** + * 加载便签详细数据 + * 从数据库中查询便签的内容、标题、模式等详细信息 + * @return 是否加载成功 + */ + private boolean loadNoteData() { + // 查询便签详细数据 Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] { String.valueOf(mNoteId) }, null); + boolean success = false; // 初始化加载成功标志 if (cursor != null) { if (cursor.moveToFirst()) { do { String type = cursor.getString(DATA_MIME_TYPE_COLUMN); if (DataConstants.NOTE.equals(type)) { + // 处理普通便签数据 mContent = cursor.getString(DATA_CONTENT_COLUMN); + mTitle = cursor.getString(5); // DATA3 列存储标题(索引 5) mMode = cursor.getInt(DATA_MODE_COLUMN); mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN)); + success = true; // 加载成功 } else if (DataConstants.CALL_NOTE.equals(type)) { + // 处理通话便签数据 mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); + success = true; // 加载成功 } else { - Log.d(TAG, "Wrong note type with type:" + type); + Log.d(TAG, "Wrong note type with type:" + type); // 记录错误类型日志 } } while (cursor.moveToNext()); } - cursor.close(); + cursor.close(); // 关闭游标 } else { - Log.e(TAG, "No data with id:" + mNoteId); - throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId); + Log.e(TAG, "No data with id:" + mNoteId); // 记录错误日志 } - } - + return success; // 返回加载结果 + } + + /** + * 创建空便签 + * @param context 上下文对象 + * @param folderId 文件夹 ID + * @param widgetId 小组件 ID + * @param widgetType 小组件类型 + * @param defaultBgColorId 默认背景颜色 ID + * @return 创建的空便签对象 + */ public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId, int widgetType, int defaultBgColorId) { WorkingNote note = new WorkingNote(context, folderId); - note.setBgColorId(defaultBgColorId); - note.setWidgetId(widgetId); - note.setWidgetType(widgetType); + note.setBgColorId(defaultBgColorId); // 设置默认背景颜色 + note.setWidgetId(widgetId); // 设置小组件 ID + note.setWidgetType(widgetType); // 设置小组件类型 return note; } + /** + * 加载指定 ID 的便签 + * @param context 上下文对象 + * @param id 便签 ID + * @return 加载的便签对象 + */ public static WorkingNote load(Context context, long id) { return new WorkingNote(context, id, 0); } + /** + * 保存便签 + * @return 是否保存成功 + */ public synchronized boolean saveNote() { - if (isWorthSaving()) { - if (!existInDatabase()) { + if (isWorthSaving()) { // 检查是否值得保存 + if (!existInDatabase()) { // 检查是否已存在于数据库 + // 创建新便签 if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { Log.e(TAG, "Create new note fail with id:" + mNoteId); return false; } } - mNote.syncNote(mContext, mNoteId); + mNote.syncNote(mContext, mNoteId); // 同步便签数据到数据库 /** - * Update widget content if there exist any widget of this note + * 如果便签关联了小组件,更新小组件内容 */ if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { - mNoteSettingStatusListener.onWidgetChanged(); + mNoteSettingStatusListener.onWidgetChanged(); // 通知小组件变更 } return true; } else { @@ -231,186 +306,341 @@ public class WorkingNote { } } + /** + * 检查便签是否已存在于数据库 + * @return 是否已存在于数据库 + */ public boolean existInDatabase() { return mNoteId > 0; } + /** + * 检查便签是否值得保存 + * @return 是否值得保存 + */ private boolean isWorthSaving() { if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) || (existInDatabase() && !mNote.isLocalModified())) { + // 已删除、新建且内容为空、已存在且未修改的便签不值得保存 return false; } else { return true; } } + /** + * 设置便签设置变更监听器 + * @param l 监听器对象 + */ public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) { mNoteSettingStatusListener = l; } + /** + * 设置提醒日期 + * @param date 提醒日期 + * @param set 是否设置提醒 + */ public void setAlertDate(long date, boolean set) { Log.d(TAG, "setAlertDate: date=" + date + ", set=" + set); - mAlertDate = date; - mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate)); + mAlertDate = date; // 设置提醒日期 + mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate)); // 同步到 Note 对象 if (mNoteSettingStatusListener != null) { - mNoteSettingStatusListener.onClockAlertChanged(date, set); + mNoteSettingStatusListener.onClockAlertChanged(date, set); // 通知监听器 } } + /** + * 标记便签为已删除或未删除 + * @param mark 是否标记为已删除 + */ public void markDeleted(boolean mark) { - mIsDeleted = mark; + mIsDeleted = mark; // 设置删除状态 if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { - mNoteSettingStatusListener.onWidgetChanged(); + mNoteSettingStatusListener.onWidgetChanged(); // 通知监听器 } } - + + /** + * 检查便签是否已删除 + * @return 是否已删除 + */ + public boolean isDeleted() { + return mIsDeleted; + } + + /** + * 设置背景颜色 ID + * @param id 背景颜色 ID + */ public void setBgColorId(int id) { - if (id != mBgColorId) { - mBgColorId = id; + if (id != mBgColorId) { // 检查颜色是否变更 + mBgColorId = id; // 设置背景颜色 ID if (mNoteSettingStatusListener != null) { - mNoteSettingStatusListener.onBackgroundColorChanged(); + mNoteSettingStatusListener.onBackgroundColorChanged(); // 通知监听器 } - mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id)); + mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id)); // 同步到 Note 对象 } } + /** + * 设置便签模式 + * @param mode 模式(普通模式或 checklist 模式) + */ public void setCheckListMode(int mode) { - if (mMode != mode) { + if (mMode != mode) { // 检查模式是否变更 if (mNoteSettingStatusListener != null) { - mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode); + mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode); // 通知监听器 } - mMode = mode; - mNote.setTextData(TextNote.MODE, String.valueOf(mMode)); + mMode = mode; // 设置模式 + mNote.setTextData(TextNote.MODE, String.valueOf(mMode)); // 同步到 Note 对象 } } + /** + * 设置小组件类型 + * @param type 小组件类型 + */ public void setWidgetType(int type) { - if (type != mWidgetType) { - mWidgetType = type; - mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); + if (type != mWidgetType) { // 检查类型是否变更 + mWidgetType = type; // 设置小组件类型 + mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); // 同步到 Note 对象 } } + /** + * 设置小组件 ID + * @param id 小组件 ID + */ public void setWidgetId(int id) { - if (id != mWidgetId) { - mWidgetId = id; - mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); + if (id != mWidgetId) { // 检查 ID 是否变更 + mWidgetId = id; // 设置小组件 ID + mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); // 同步到 Note 对象 } } + /** + * 设置便签内容 + * @param text 便签内容 + */ public void setWorkingText(String text) { - if (!TextUtils.equals(mContent, text)) { - mContent = text; - mNote.setTextData(DataColumns.CONTENT, mContent); + if (!TextUtils.equals(mContent, text)) { // 检查内容是否变更 + mContent = text; // 设置便签内容 + mNote.setTextData(DataColumns.CONTENT, mContent); // 同步到 Note 对象 + mModifiedDate = System.currentTimeMillis(); // 更新修改日期 + } + } + + /** + * 设置便签标题 + * @param title 便签标题 + */ + public void setTitle(String title) { + if (!TextUtils.equals(mTitle, title)) { // 检查标题是否变更 + mTitle = title; // 设置便签标题 + mNote.setTextData(DataColumns.DATA3, mTitle); // 同步到 Note 对象 + mModifiedDate = System.currentTimeMillis(); // 更新修改日期 } } + /** + * 获取便签标题 + * @return 便签标题 + */ + public String getTitle() { + return mTitle; + } + + /** + * 将便签转换为通话便签 + * @param phoneNumber 电话号码 + * @param callDate 通话日期 + */ public void convertToCallNote(String phoneNumber, long callDate) { - mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate)); - mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber); - mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER)); + mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate)); // 设置通话日期 + mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber); // 设置电话号码 + mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER)); // 设置父文件夹为通话记录文件夹 } + /** + * 检查便签是否有提醒 + * @return 是否有提醒 + */ public boolean hasClockAlert() { return (mAlertDate > 0 ? true : false); } + /** + * 获取便签内容 + * @return 便签内容 + */ public String getContent() { return mContent; } + /** + * 获取提醒日期 + * @return 提醒日期 + */ public long getAlertDate() { return mAlertDate; } + /** + * 获取修改日期 + * @return 修改日期 + */ public long getModifiedDate() { return mModifiedDate; } + /** + * 获取背景颜色资源 ID + * @return 背景颜色资源 ID + */ public int getBgColorResId() { return NoteBgResources.getNoteBgResource(mBgColorId); } + /** + * 获取背景颜色 ID + * @return 背景颜色 ID + */ public int getBgColorId() { return mBgColorId; } + /** + * 获取标题背景资源 ID + * @return 标题背景资源 ID + */ public int getTitleBgResId() { return NoteBgResources.getNoteTitleBgResource(mBgColorId); } + /** + * 获取便签模式 + * @return 便签模式 + */ public int getCheckListMode() { return mMode; } + /** + * 获取便签 ID + * @return 便签 ID + */ public long getNoteId() { return mNoteId; } + /** + * 获取文件夹 ID + * @return 文件夹 ID + */ public long getFolderId() { return mFolderId; } + /** + * 获取小组件 ID + * @return 小组件 ID + */ public int getWidgetId() { return mWidgetId; } + /** + * 获取小组件类型 + * @return 小组件类型 + */ public int getWidgetType() { return mWidgetType; } + /** + * 检查便签是否置顶 + * @return 是否置顶 + */ public boolean isPinned() { return mPinned; } + /** + * 设置便签是否置顶 + * @param pinned 是否置顶 + */ public void setPinned(boolean pinned) { - if (mPinned != pinned) { - mPinned = pinned; - mNote.setNoteValue(NoteColumns.PINNED, String.valueOf(mPinned ? 1 : 0)); + if (mPinned != pinned) { // 检查置顶状态是否变更 + mPinned = pinned; // 设置置顶状态 + mNote.setNoteValue(NoteColumns.PINNED, String.valueOf(mPinned ? 1 : 0)); // 同步到 Note 对象 } } + /** + * 检查便签是否加密 + * @return 是否加密 + */ public boolean isEncrypted() { return mEncrypted; } + /** + * 设置便签是否加密 + * @param encrypted 是否加密 + */ public void setEncrypted(boolean encrypted) { - if (mEncrypted != encrypted) { - mEncrypted = encrypted; - mNote.setNoteValue(NoteColumns.ENCRYPTED, String.valueOf(mEncrypted ? 1 : 0)); + if (mEncrypted != encrypted) { // 检查加密状态是否变更 + mEncrypted = encrypted; // 设置加密状态 + mNote.setNoteValue(NoteColumns.ENCRYPTED, String.valueOf(mEncrypted ? 1 : 0)); // 同步到 Note 对象 } } + /** + * 设置密码哈希值 + * @param passwordHash 密码哈希值 + */ public void setPasswordHash(String passwordHash) { - mPasswordHash = passwordHash; - mNote.setNoteValue(NoteColumns.PASSWORD_HASH, mPasswordHash); + mPasswordHash = passwordHash; // 设置密码哈希值 + mNote.setNoteValue(NoteColumns.PASSWORD_HASH, mPasswordHash); // 同步到 Note 对象 } + /** + * 验证密码 + * @param password 待验证的密码 + * @return 密码是否正确 + */ public boolean verifyPassword(String password) { return net.micode.notes.tool.EncryptionUtils.generatePasswordHash(password).equals(mPasswordHash); } + /** + * 便签设置变更监听器接口 + * 用于监听便签设置的变更,如背景颜色、提醒、小组件等 + */ public interface NoteSettingChangedListener { /** - * Called when the background color of current note has just changed + * 当便签背景颜色变更时调用 */ void onBackgroundColorChanged(); /** - * Called when user set clock + * 当用户设置提醒时调用 + * @param date 提醒日期 + * @param set 是否设置提醒 */ void onClockAlertChanged(long date, boolean set); /** - * Call when user create note from widget + * 当用户从小组件创建便签时调用 */ void onWidgetChanged(); /** - * Call when switch between check list mode and normal mode - * @param oldMode is previous mode before change - * @param newMode is new mode + * 当切换 checklist 模式和普通模式时调用 + * @param oldMode 变更前的模式 + * @param newMode 变更后的模式 */ void onCheckListModeChanged(int oldMode, int newMode); } diff --git a/src/main/java/net/micode/notes/tool/DataUtils.java b/src/main/java/net/micode/notes/tool/DataUtils.java index c715ed9..0258e8e 100644 --- a/src/main/java/net/micode/notes/tool/DataUtils.java +++ b/src/main/java/net/micode/notes/tool/DataUtils.java @@ -47,32 +47,39 @@ public class DataUtils { return true; } - ArrayList operationList = new ArrayList(); + Log.d(TAG, "Batch deleting notes, ids: " + ids.toString()); long currentTime = System.currentTimeMillis(); + boolean allSuccess = true; + + // 为每个ID单独执行删除操作,这样即使某个ID删除失败,也不会影响其他ID的删除 for (long id : ids) { if(id == Notes.ID_ROOT_FOLDER) { Log.e(TAG, "Don't delete system folder root"); continue; } - ContentProviderOperation.Builder builder = ContentProviderOperation - .newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); - builder.withValue(NoteColumns.DELETED, 1); - builder.withValue(NoteColumns.DELETED_DATE, currentTime); - operationList.add(builder.build()); - } - try { - ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); - if (results == null || results.length == 0 || results[0] == null) { - Log.d(TAG, "delete notes failed, ids:" + ids.toString()); - return false; + + // 创建ContentValues对象,设置要更新的值 + ContentValues values = new ContentValues(); + values.put(NoteColumns.DELETED, 1); + values.put(NoteColumns.DELETED_DATE, currentTime); + + // 直接使用ContentResolver的update方法执行更新操作 + int rowsUpdated = resolver.update( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), + values, + null, + null); + + // 检查更新是否成功 + if (rowsUpdated == 0) { + Log.e(TAG, "Failed to delete note with id: " + id); + allSuccess = false; + } else { + Log.d(TAG, "Successfully deleted note with id: " + id); } - return true; - } catch (RemoteException e) { - Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); - } catch (OperationApplicationException e) { - Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); } - return false; + + return allSuccess; } public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) { @@ -295,4 +302,23 @@ public class DataUtils { } return snippet; } + + public static String getNoteTitle(ContentResolver resolver, long noteId) { + Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, + new String [] { Notes.DataColumns.DATA3 }, + Notes.DataColumns.NOTE_ID + "=? AND " + Notes.DataColumns.MIME_TYPE + "=?", + new String [] { String.valueOf(noteId), Notes.DataConstants.NOTE }, + null); + + if (cursor != null && cursor.moveToFirst()) { + try { + return cursor.getString(0); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "Get note title fails " + e.toString()); + } finally { + cursor.close(); + } + } + return ""; + } } diff --git a/src/main/java/net/micode/notes/tool/EncryptionUtils.java b/src/main/java/net/micode/notes/tool/EncryptionUtils.java index bdad411..c4fe447 100644 --- a/src/main/java/net/micode/notes/tool/EncryptionUtils.java +++ b/src/main/java/net/micode/notes/tool/EncryptionUtils.java @@ -19,17 +19,26 @@ package net.micode.notes.tool; import android.util.Base64; import android.util.Log; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.security.spec.KeySpec; public class EncryptionUtils { private static final String TAG = "EncryptionUtils"; private static final String ENCRYPTION_ALGORITHM = "AES"; - private static final String CIPHER_TRANSFORMATION = "AES/ECB/PKCS5Padding"; + private static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding"; private static final String HASH_ALGORITHM = "SHA-256"; + private static final String KEY_DERIVATION_FUNCTION = "PBKDF2WithHmacSHA256"; + private static final int KEY_SIZE = 256; + private static final int IV_SIZE = 16; + private static final int SALT_SIZE = 16; + private static final int ITERATION_COUNT = 10000; /** * 生成密码的SHA-256哈希值 @@ -39,9 +48,9 @@ public class EncryptionUtils { public static String generatePasswordHash(String password) { try { MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM); - byte[] hash = digest.digest(password.getBytes()); + byte[] hash = digest.digest(password.getBytes("UTF-8")); return Base64.encodeToString(hash, Base64.NO_WRAP); - } catch (NoSuchAlgorithmException e) { + } catch (Exception e) { Log.e(TAG, "Error generating password hash", e); return null; } @@ -51,15 +60,26 @@ public class EncryptionUtils { * 加密字符串 * @param input 要加密的字符串 * @param password 密码 - * @return 加密后的字符串 + * @return 加密后的字符串,格式为:salt:iv:encrypted */ public static String encrypt(String input, String password) { try { - SecretKeySpec keySpec = new SecretKeySpec(generateKey(password), ENCRYPTION_ALGORITHM); + // 生成随机盐值 + byte[] salt = generateSalt(); + // 生成随机IV + byte[] iv = generateIV(); + // 从密码生成密钥 + SecretKey secretKey = generateKey(password, salt); + // 初始化加密器 Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION); - cipher.init(Cipher.ENCRYPT_MODE, keySpec); - byte[] encrypted = cipher.doFinal(input.getBytes()); - return Base64.encodeToString(encrypted, Base64.NO_WRAP); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv)); + // 加密数据 + byte[] encrypted = cipher.doFinal(input.getBytes("UTF-8")); + // 组合结果:salt:iv:encrypted + String saltBase64 = Base64.encodeToString(salt, Base64.NO_WRAP); + String ivBase64 = Base64.encodeToString(iv, Base64.NO_WRAP); + String encryptedBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP); + return saltBase64 + ":" + ivBase64 + ":" + encryptedBase64; } catch (Exception e) { Log.e(TAG, "Error encrypting data", e); return null; @@ -68,36 +88,67 @@ public class EncryptionUtils { /** * 解密字符串 - * @param encrypted 加密后的字符串 + * @param encrypted 加密后的字符串,格式为:salt:iv:encrypted * @param password 密码 * @return 解密后的字符串 */ public static String decrypt(String encrypted, String password) { try { - SecretKeySpec keySpec = new SecretKeySpec(generateKey(password), ENCRYPTION_ALGORITHM); + // 解析加密字符串 + String[] parts = encrypted.split(":"); + if (parts.length != 3) { + Log.e(TAG, "Invalid encrypted string format"); + return null; + } + // 解码各部分 + byte[] salt = Base64.decode(parts[0], Base64.NO_WRAP); + byte[] iv = Base64.decode(parts[1], Base64.NO_WRAP); + byte[] encryptedData = Base64.decode(parts[2], Base64.NO_WRAP); + // 从密码生成密钥 + SecretKey secretKey = generateKey(password, salt); + // 初始化解密器 Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION); - cipher.init(Cipher.DECRYPT_MODE, keySpec); - byte[] decoded = Base64.decode(encrypted, Base64.NO_WRAP); - byte[] decrypted = cipher.doFinal(decoded); - return new String(decrypted); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); + // 解密数据 + byte[] decrypted = cipher.doFinal(encryptedData); + return new String(decrypted, "UTF-8"); } catch (Exception e) { - Log.e(TAG, "Error decrypting data", e); + Log.e(TAG, "Error decrypting data: " + e.getMessage(), e); return null; } } /** - * 生成AES密钥 + * 生成随机盐值 + * @return 盐值 + */ + private static byte[] generateSalt() { + byte[] salt = new byte[SALT_SIZE]; + new SecureRandom().nextBytes(salt); + return salt; + } + + /** + * 生成随机IV + * @return IV + */ + private static byte[] generateIV() { + byte[] iv = new byte[IV_SIZE]; + new SecureRandom().nextBytes(iv); + return iv; + } + + /** + * 从密码生成密钥 * @param password 密码 - * @return 密钥字节数组 - * @throws NoSuchAlgorithmException + * @param salt 盐值 + * @return 密钥 + * @throws Exception */ - private static byte[] generateKey(String password) throws NoSuchAlgorithmException { - MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM); - byte[] hash = digest.digest(password.getBytes()); - // AES密钥长度必须为16字节(128位) - byte[] key = new byte[16]; - System.arraycopy(hash, 0, key, 0, 16); - return key; + private static SecretKey generateKey(String password, byte[] salt) throws Exception { + KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_SIZE); + SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_DERIVATION_FUNCTION); + byte[] keyBytes = factory.generateSecret(spec).getEncoded(); + return new SecretKeySpec(keyBytes, ENCRYPTION_ALGORITHM); } -} \ No newline at end of file +} diff --git a/src/main/java/net/micode/notes/tool/ImageUtils.java b/src/main/java/net/micode/notes/tool/ImageUtils.java new file mode 100644 index 0000000..af834a1 --- /dev/null +++ b/src/main/java/net/micode/notes/tool/ImageUtils.java @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.tool; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.media.ExifInterface; +import android.os.Environment; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class ImageUtils { + private static final String TAG = "ImageUtils"; + private static final String IMAGE_DIR = "Notes/Images"; + private static final String IMAGE_FORMAT = ".jpg"; + private static final int MAX_IMAGE_WIDTH = 1024; + private static final int MAX_IMAGE_HEIGHT = 1024; + private static final int MAX_IMAGE_SIZE = 1024 * 1024; // 1MB + + /** + * 从剪贴板获取图片(专门针对虚拟机环境和微信截屏) + * @param context 上下文 + * @return 从剪贴板获取的图片 + */ + public static Bitmap getImageFromClipboard(Context context) { + try { + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard == null || !clipboard.hasPrimaryClip()) { + Log.d(TAG, "剪贴板为空"); + return null; + } + + android.content.ClipData clip = clipboard.getPrimaryClip(); + if (clip == null || clip.getItemCount() == 0) { + Log.d(TAG, "剪贴板内容为空"); + return null; + } + + Log.d(TAG, "剪贴板项目数量: " + clip.getItemCount()); + + // 检查剪贴板描述 + android.content.ClipDescription description = clipboard.getPrimaryClipDescription(); + if (description != null) { + Log.d(TAG, "剪贴板描述: " + description.toString()); + for (int i = 0; i < description.getMimeTypeCount(); i++) { + Log.d(TAG, "MIME类型 " + i + ": " + description.getMimeType(i)); + } + } + + for (int i = 0; i < clip.getItemCount(); i++) { + android.content.ClipData.Item item = clip.getItemAt(i); + Log.d(TAG, "处理剪贴板项目 " + i); + + // 尝试从URI加载图片(标准方式) + if (item.getUri() != null) { + Log.d(TAG, "尝试从URI加载图片: " + item.getUri().toString()); + try { + Bitmap bitmap = BitmapFactory.decodeStream( + context.getContentResolver().openInputStream(item.getUri())); + if (bitmap != null) { + Log.d(TAG, "成功从URI加载图片: " + bitmap.getWidth() + "x" + bitmap.getHeight()); + return bitmap; + } else { + Log.e(TAG, "从URI加载图片失败"); + } + } catch (Exception e) { + Log.e(TAG, "从URI加载图片时出错: " + e.getMessage()); + e.printStackTrace(); + } + } else { + Log.d(TAG, "未找到URI"); + } + + // 尝试获取Intent(可能包含微信截屏的图片信息) + if (item.getIntent() != null) { + Log.d(TAG, "找到Intent: " + item.getIntent().toString()); + try { + android.content.Intent intent = item.getIntent(); + // 检查Intent是否包含图片信息 + if (intent.hasExtra(android.content.Intent.EXTRA_STREAM)) { + android.net.Uri uri = (android.net.Uri) intent.getParcelableExtra(android.content.Intent.EXTRA_STREAM); + if (uri != null) { + Log.d(TAG, "从Intent中找到图片URI: " + uri.toString()); + Bitmap bitmap = BitmapFactory.decodeStream( + context.getContentResolver().openInputStream(uri)); + if (bitmap != null) { + Log.d(TAG, "成功从Intent加载图片: " + bitmap.getWidth() + "x" + bitmap.getHeight()); + return bitmap; + } + } + } + } catch (Exception e) { + Log.e(TAG, "从Intent加载图片时出错: " + e.getMessage()); + e.printStackTrace(); + } + } else { + Log.d(TAG, "未找到Intent"); + } + + // 尝试从剪贴板直接获取Bitmap(针对某些应用的特殊处理) + try { + // 注意:这是一个尝试,因为ClipData.Item没有直接的getBitmap方法 + // 但某些应用可能会以特殊方式存储Bitmap + Log.d(TAG, "尝试其他方式从剪贴板获取图片"); + // 这里可以添加针对特定应用的特殊处理 + } catch (Exception e) { + Log.e(TAG, "尝试其他方式获取图片时出错: " + e.getMessage()); + e.printStackTrace(); + } + } + + Log.d(TAG, "无法从剪贴板获取图片"); + return null; + } catch (Exception e) { + Log.e(TAG, "从剪贴板获取图片时出错: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + /** + * 保存图片到本地存储 + * @param context 上下文 + * @param bitmap 要保存的图片 + * @return 保存后的图片路径 + */ + public static String saveImage(Context context, Bitmap bitmap) { + if (bitmap == null) { + Log.e(TAG, "Bitmap is null"); + return null; + } + + Log.d(TAG, "开始保存图片,尺寸: " + bitmap.getWidth() + "x" + bitmap.getHeight()); + + // 创建图片存储目录 + File imageDir; + try { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + // Android 10+ 使用应用私有存储 + imageDir = new File(context.getExternalFilesDir(null), IMAGE_DIR); + Log.d(TAG, "使用应用私有存储: " + imageDir.getAbsolutePath()); + } else { + // Android 9 及以下使用外部存储 + imageDir = new File(Environment.getExternalStorageDirectory(), IMAGE_DIR); + Log.d(TAG, "使用外部存储: " + imageDir.getAbsolutePath()); + } + + if (!imageDir.exists()) { + Log.d(TAG, "创建图片存储目录: " + imageDir.getAbsolutePath()); + if (!imageDir.mkdirs()) { + Log.e(TAG, "Failed to create image directory"); + return null; + } + } + + // 生成唯一的文件名 + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); + String fileName = "IMG_" + timeStamp + IMAGE_FORMAT; + File imageFile = new File(imageDir, fileName); + Log.d(TAG, "图片保存路径: " + imageFile.getAbsolutePath()); + + // 压缩图片 + Log.d(TAG, "开始压缩图片"); + Bitmap compressedBitmap = compressBitmap(bitmap); + Log.d(TAG, "压缩后图片尺寸: " + compressedBitmap.getWidth() + "x" + compressedBitmap.getHeight()); + + // 保存图片 + Log.d(TAG, "开始写入图片文件"); + FileOutputStream fos = new FileOutputStream(imageFile); + boolean compressed = compressedBitmap.compress(Bitmap.CompressFormat.JPEG, 85, fos); + Log.d(TAG, "图片压缩结果: " + compressed); + fos.flush(); + fos.close(); + Log.d(TAG, "图片保存成功"); + + // 释放资源 + if (compressedBitmap != bitmap) { + compressedBitmap.recycle(); + } + + Log.d(TAG, "图片保存路径: " + imageFile.getAbsolutePath()); + return imageFile.getAbsolutePath(); + } catch (Exception e) { + Log.e(TAG, "Error saving image: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + /** + * 从URI保存图片到本地存储 + * @param context 上下文 + * @param uri 图片URI + * @return 保存后的图片路径 + */ + public static String saveImage(Context context, android.net.Uri uri) { + if (uri == null) { + Log.e(TAG, "URI is null"); + return null; + } + + Log.d(TAG, "开始从URI保存图片: " + uri.toString()); + + try { + // 从URI加载图片 + Bitmap bitmap = BitmapFactory.decodeStream( + context.getContentResolver().openInputStream(uri)); + + if (bitmap == null) { + Log.e(TAG, "Failed to decode bitmap from URI"); + return null; + } + + // 使用现有的saveImage方法保存图片 + return saveImage(context, bitmap); + } catch (Exception e) { + Log.e(TAG, "Error saving image from URI: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + /** + * 压缩图片 + * @param bitmap 原始图片 + * @return 压缩后的图片 + */ + private static Bitmap compressBitmap(Bitmap bitmap) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + + // 计算缩放比例 + float scaleWidth = ((float) MAX_IMAGE_WIDTH) / width; + float scaleHeight = ((float) MAX_IMAGE_HEIGHT) / height; + float scale = Math.min(scaleWidth, scaleHeight); + + // 如果图片不需要压缩,直接返回 + if (scale >= 1.0f) { + return bitmap; + } + + // 缩放图片 + Matrix matrix = new Matrix(); + matrix.postScale(scale, scale); + return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true); + } + + /** + * 从文件路径加载图片 + * @param path 图片路径 + * @return 加载的图片 + */ + public static Bitmap loadImage(String path) { + if (path == null || path.isEmpty()) { + Log.e(TAG, "Image path is null or empty"); + return null; + } + + Log.d(TAG, "开始加载图片: " + path); + + File imageFile = new File(path); + if (!imageFile.exists()) { + Log.e(TAG, "Image file does not exist: " + path); + return null; + } + + Log.d(TAG, "图片文件存在,大小: " + imageFile.length() + " bytes"); + + try { + // 解码图片 + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(path, options); + + Log.d(TAG, "图片原始尺寸: " + options.outWidth + "x" + options.outHeight); + + // 计算缩放比例 + int scale = 1; + while (options.outWidth / scale > MAX_IMAGE_WIDTH || options.outHeight / scale > MAX_IMAGE_HEIGHT) { + scale *= 2; + } + + Log.d(TAG, "图片缩放比例: " + scale); + + // 加载缩放后的图片 + options.inJustDecodeBounds = false; + options.inSampleSize = scale; + Bitmap bitmap = BitmapFactory.decodeFile(path, options); + + if (bitmap != null) { + Log.d(TAG, "成功加载图片,尺寸: " + bitmap.getWidth() + "x" + bitmap.getHeight()); + } else { + Log.e(TAG, "Failed to decode bitmap from file: " + path); + } + + return bitmap; + } catch (Exception e) { + Log.e(TAG, "Error loading image: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + /** + * 删除图片文件 + * @param path 图片路径 + * @return 是否删除成功 + */ + public static boolean deleteImage(String path) { + if (path == null || path.isEmpty()) { + return false; + } + + File imageFile = new File(path); + if (!imageFile.exists()) { + return true; + } + + return imageFile.delete(); + } + + /** + * 获取图片的方向 + * @param path 图片路径 + * @return 图片方向 + */ + public static int getImageOrientation(String path) { + try { + ExifInterface exif = new ExifInterface(path); + int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + return orientation; + } catch (Exception e) { + Log.e(TAG, "Error getting image orientation: " + e.getMessage()); + return ExifInterface.ORIENTATION_NORMAL; + } + } +} diff --git a/src/main/java/net/micode/notes/ui/AlarmInitReceiver.java b/src/main/java/net/micode/notes/ui/AlarmInitReceiver.java index 781e6b3..9634cfc 100644 --- a/src/main/java/net/micode/notes/ui/AlarmInitReceiver.java +++ b/src/main/java/net/micode/notes/ui/AlarmInitReceiver.java @@ -59,7 +59,12 @@ public class AlarmInitReceiver extends BroadcastReceiver { // 添加标记,表明这个Intent是由AlarmManager触发的 sender.putExtra("from_alarm_manager", true); // 使用笔记的ID作为requestCode,确保每个提醒都有唯一的标识 - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, (int) noteId, sender, PendingIntent.FLAG_UPDATE_CURRENT); + // 创建 PendingIntent,适配 Android 12 及以上版本 + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; + } + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, (int) noteId, sender, flags); AlarmManager alarmManager = (AlarmManager) context .getSystemService(Context.ALARM_SERVICE); // 根据Android版本选择合适的方法 diff --git a/src/main/java/net/micode/notes/ui/AlarmReceiver.java b/src/main/java/net/micode/notes/ui/AlarmReceiver.java index 3cc2520..f412de0 100644 --- a/src/main/java/net/micode/notes/ui/AlarmReceiver.java +++ b/src/main/java/net/micode/notes/ui/AlarmReceiver.java @@ -68,6 +68,15 @@ public class AlarmReceiver extends BroadcastReceiver { // 直接启动提醒窗口,不进行额外检查,确保提醒能够立即触发 Log.d(TAG, "Directly starting alert without extra checks"); startAlarmAlert(context, intent); + } catch (Exception e) { + Log.e(TAG, "Error in onReceive: " + e.getMessage(), e); + // 即使出现异常,也要尝试启动提醒 + try { + Log.d(TAG, "Trying to start alert despite exception"); + startAlarmAlert(context, intent); + } catch (Exception ex) { + Log.e(TAG, "Error starting alert after exception: " + ex.getMessage(), ex); + } } finally { // 释放唤醒锁,避免电池消耗 releaseWakeLock(); @@ -109,6 +118,8 @@ public class AlarmReceiver extends BroadcastReceiver { private void startAlarmAlert(Context context, Intent originalIntent) { long noteId = 0; + Log.d(TAG, "startAlarmAlert called with context: " + context + ", intent: " + originalIntent); + // 从originalIntent中获取noteId if (originalIntent != null) { // 从data中获取 @@ -134,19 +145,25 @@ public class AlarmReceiver extends BroadcastReceiver { } } + Log.d(TAG, "Final noteId for alert: " + noteId); + // 立即尝试直接启动NewAlarmAlertActivity try { Log.d(TAG, "Starting NewAlarmAlertActivity directly"); Intent alertIntent = new Intent(context, NewAlarmAlertActivity.class); alertIntent.putExtra("note_id", noteId); alertIntent.putExtra("from_alarm_manager", true); // 确保添加启动标记 - alertIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + alertIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); - context.startActivity(alertIntent); - Log.d(TAG, "NewAlarmAlertActivity started successfully directly"); - return; + if (context != null) { + context.startActivity(alertIntent); + Log.d(TAG, "NewAlarmAlertActivity started successfully directly"); + return; + } else { + Log.e(TAG, "Context is null, cannot start activity"); + } } catch (Exception e) { - Log.e(TAG, "Error starting NewAlarmAlertActivity directly", e); + Log.e(TAG, "Error starting NewAlarmAlertActivity directly: " + e.getMessage(), e); } // 如果直接启动失败,尝试使用前台服务启动 @@ -156,15 +173,19 @@ public class AlarmReceiver extends BroadcastReceiver { serviceIntent.putExtra("note_id", noteId); serviceIntent.putExtra("from_alarm_manager", true); // 确保添加启动标记 - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - context.startForegroundService(serviceIntent); + if (context != null) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + context.startForegroundService(serviceIntent); + } else { + context.startService(serviceIntent); + } + Log.d(TAG, "NewAlarmAlertService started successfully"); + return; } else { - context.startService(serviceIntent); + Log.e(TAG, "Context is null, cannot start service"); } - Log.d(TAG, "NewAlarmAlertService started successfully"); - return; } catch (Exception serviceEx) { - Log.e(TAG, "Service approach failed: " + serviceEx.getMessage()); + Log.e(TAG, "Service approach failed: " + serviceEx.getMessage(), serviceEx); } // 如果服务启动也失败,尝试使用静态方法启动 @@ -174,7 +195,7 @@ public class AlarmReceiver extends BroadcastReceiver { Log.d(TAG, "NewAlarmAlertActivity started successfully via static method"); return; } catch (Exception e) { - Log.e(TAG, "Error starting NewAlarmAlertActivity via static method", e); + Log.e(TAG, "Error starting NewAlarmAlertActivity via static method: " + e.getMessage(), e); } // 如果所有启动活动的尝试都失败,显示通知式提醒 @@ -183,15 +204,18 @@ public class AlarmReceiver extends BroadcastReceiver { showNotificationAlert(context, noteId); return; } catch (Exception notificationEx) { - Log.e(TAG, "Error showing notification", notificationEx); + Log.e(TAG, "Error showing notification: " + notificationEx.getMessage(), notificationEx); } // 所有尝试都失败了,显示Toast提示 try { Log.d(TAG, "All attempts failed, showing toast"); - android.widget.Toast.makeText(context, "提醒时间到", android.widget.Toast.LENGTH_LONG).show(); + if (context != null) { + android.widget.Toast.makeText(context, "提醒时间到", android.widget.Toast.LENGTH_LONG).show(); + Log.d(TAG, "Toast shown successfully"); + } } catch (Exception toastEx) { - Log.e(TAG, "Error showing toast", toastEx); + Log.e(TAG, "Error showing toast: " + toastEx.getMessage(), toastEx); } // 所有尝试都失败了 @@ -224,11 +248,16 @@ public class AlarmReceiver extends BroadcastReceiver { intent.putExtra("from_alarm_manager", true); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 创建 PendingIntent,适配 Android 12 及以上版本 + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; + } PendingIntent pendingIntent = PendingIntent.getActivity( context, (int) noteId, intent, - PendingIntent.FLAG_UPDATE_CURRENT + flags ); // 创建通知 diff --git a/src/main/java/net/micode/notes/ui/NewAlarmAlertActivity.java b/src/main/java/net/micode/notes/ui/NewAlarmAlertActivity.java index 9876434..69aaaa8 100644 --- a/src/main/java/net/micode/notes/ui/NewAlarmAlertActivity.java +++ b/src/main/java/net/micode/notes/ui/NewAlarmAlertActivity.java @@ -49,7 +49,9 @@ public class NewAlarmAlertActivity extends Activity { super.onCreate(savedInstanceState); Log.d(TAG, "onCreate called"); - // 对于只显示对话框的Activity,不需要设置布局 + // 设置一个简单的透明布局,确保Activity能够正常创建 + setContentView(R.layout.alarm_alert); + // 直接初始化并显示对话框 // 检查启动来源,避免在不应该显示的时候显示 @@ -189,8 +191,8 @@ public class NewAlarmAlertActivity extends Activity { // 直接在UI线程显示对话框,不使用Handler,确保立即显示 try { - // 使用默认主题,确保在所有Android版本上都能正常工作 - AlertDialog.Builder builder = new AlertDialog.Builder(this); + // 使用现代主题,确保对话框在手机顶部弹出 + AlertDialog.Builder builder = new AlertDialog.Builder(this, android.R.style.Theme_Material_Dialog_Alert); builder.setTitle(R.string.app_name); builder.setMessage(mSnippet); builder.setCancelable(false); @@ -219,6 +221,17 @@ public class NewAlarmAlertActivity extends Activity { finish(); }); + // 添加稍后提醒按钮 + builder.setNeutralButton("稍后提醒", (dialog, which) -> { + Log.d(TAG, "Snooze button clicked"); + // 停止声音和震动 + stopAlarm(); + // 设置稍后提醒 + scheduleSnooze(); + dialog.dismiss(); + finish(); + }); + // 创建并显示对话框 AlertDialog dialog = builder.create(); @@ -242,6 +255,14 @@ public class NewAlarmAlertActivity extends Activity { finish(); }); + // 添加稍后提醒按钮 + simpleBuilder.setNeutralButton("稍后提醒", (dialog, which) -> { + stopAlarm(); + scheduleSnooze(); + dialog.dismiss(); + finish(); + }); + AlertDialog simpleDialog = simpleBuilder.create(); simpleDialog.show(); Log.d(TAG, "Simple alert dialog shown"); @@ -262,6 +283,51 @@ public class NewAlarmAlertActivity extends Activity { } } + private void scheduleSnooze() { + Log.d(TAG, "Scheduling snooze for note: " + mNoteId); + + // 设置5分钟后再次提醒 + long snoozeTime = System.currentTimeMillis() + 5 * 60 * 1000; + + try { + // 创建闹钟Intent + Intent intent = new Intent(this, AlarmReceiver.class); + intent.setAction("net.micode.notes.ALARM_TRIGGERED"); + if (mNoteId > 0) { + intent.setData(android.content.ContentUris.withAppendedId(net.micode.notes.data.Notes.CONTENT_NOTE_URI, mNoteId)); + intent.putExtra(Intent.EXTRA_UID, mNoteId); + } + intent.putExtra("from_alarm_manager", true); + + // 创建PendingIntent + int flags = android.app.PendingIntent.FLAG_UPDATE_CURRENT; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + flags |= android.app.PendingIntent.FLAG_IMMUTABLE; + } + android.app.PendingIntent pendingIntent = android.app.PendingIntent.getBroadcast( + this, + (int) mNoteId, + intent, + flags + ); + + // 设置闹钟 + android.app.AlarmManager alarmManager = (android.app.AlarmManager) getSystemService(ALARM_SERVICE); + if (alarmManager != null) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle(android.app.AlarmManager.RTC_WAKEUP, snoozeTime, pendingIntent); + } else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + alarmManager.setExact(android.app.AlarmManager.RTC_WAKEUP, snoozeTime, pendingIntent); + } else { + alarmManager.set(android.app.AlarmManager.RTC_WAKEUP, snoozeTime, pendingIntent); + } + Log.d(TAG, "Snooze alarm scheduled for: " + snoozeTime); + } + } catch (Exception e) { + Log.e(TAG, "Error scheduling snooze", e); + } + } + private void playAlarmSound() { Log.d(TAG, "Playing alarm sound"); diff --git a/src/main/java/net/micode/notes/ui/NewAlarmAlertService.java b/src/main/java/net/micode/notes/ui/NewAlarmAlertService.java index ebf9dfe..773d206 100644 --- a/src/main/java/net/micode/notes/ui/NewAlarmAlertService.java +++ b/src/main/java/net/micode/notes/ui/NewAlarmAlertService.java @@ -52,21 +52,47 @@ public class NewAlarmAlertService extends Service { Log.d(TAG, "Added from_alarm_manager flag to alert intent"); } - alertIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(alertIntent); - Log.d(TAG, "NewAlarmAlertActivity started from service"); + // 添加必要的标志,确保在Android 13+中能够启动 + alertIntent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_CLEAR_TOP | + Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | + Intent.FLAG_ACTIVITY_NO_HISTORY + ); + + // 对于Android 13+,使用ContextCompat.startActivity + try { + startActivity(alertIntent); + Log.d(TAG, "NewAlarmAlertActivity started from service"); + } catch (Exception e) { + Log.e(TAG, "Error starting NewAlarmAlertActivity directly, trying with ContextCompat", e); + // 尝试使用ContextCompat启动 + androidx.core.content.ContextCompat.startActivity(this, alertIntent, null); + Log.d(TAG, "NewAlarmAlertActivity started from service using ContextCompat"); + } } catch (Exception e) { Log.e(TAG, "Error starting NewAlarmAlertActivity from service", e); } - // 停止服务 - stopSelf(); + // 延迟停止服务,确保Activity有足够时间启动 + new android.os.Handler().postDelayed(new Runnable() { + @Override + public void run() { + stopSelf(); + } + }, 2000); + return START_NOT_STICKY; } private Notification createForegroundNotification() { Intent notificationIntent = new Intent(this, NewAlarmAlertActivity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + // 创建 PendingIntent,适配 Android 12 及以上版本 + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; + } + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, flags); Notification.Builder builder; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/src/main/java/net/micode/notes/ui/NoteEditActivity.java b/src/main/java/net/micode/notes/ui/NoteEditActivity.java index 1ecacfe..64adc87 100644 --- a/src/main/java/net/micode/notes/ui/NoteEditActivity.java +++ b/src/main/java/net/micode/notes/ui/NoteEditActivity.java @@ -27,15 +27,21 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.graphics.Paint; +import android.Manifest; +import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; +import android.text.Editable; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.TextUtils; +import android.text.TextWatcher; import android.text.format.DateUtils; import android.text.style.BackgroundColorSpan; +import android.text.style.ImageSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import android.util.Log; @@ -46,6 +52,7 @@ import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.WindowManager; +import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; @@ -53,8 +60,10 @@ import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; +import androidx.core.content.ContextCompat; import net.micode.notes.R; import net.micode.notes.data.Notes; @@ -63,6 +72,7 @@ import net.micode.notes.model.WorkingNote; import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; import net.micode.notes.tool.DataUtils; import net.micode.notes.tool.EncryptionUtils; +import net.micode.notes.tool.ImageUtils; import net.micode.notes.tool.ResourceParser; import net.micode.notes.tool.ResourceParser.TextAppearanceResources; import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; @@ -71,8 +81,11 @@ import net.micode.notes.widget.NoteWidgetProvider_2x; import net.micode.notes.widget.NoteWidgetProvider_4x; import java.util.HashMap; +import java.util.Date; import java.util.HashSet; +import java.util.Locale; import java.util.Map; +import java.text.SimpleDateFormat; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -134,10 +147,22 @@ public class NoteEditActivity extends Activity implements OnClickListener, private View mFontSizeSelector; private EditText mNoteEditor; + private EditText mNoteTitleEditor; private View mNoteEditorPanel; private WorkingNote mWorkingNote; + + public WorkingNote getWorkingNote() { + return mWorkingNote; + } + + /** + * 刷新便签内容 + */ + public void refreshNoteContent() { + initNoteScreen(); + } private SharedPreferences mSharedPrefs; private int mFontSizeId; @@ -162,17 +187,168 @@ public class NoteEditActivity extends Activity implements OnClickListener, private ImageButton mBtnUnderline; private ImageButton mBtnBulletList; private ImageButton mBtnNumberList; + + // 相册选择相关常量 + private static final int REQUEST_CODE_PICK_IMAGE = 1004; + private static final int REQUEST_CODE_STORAGE_PERMISSION = 1005; + + /** + * 更新信息行显示:最后编辑时间和字数 + */ + private void updateInfoLine() { + TextView infoLine = (TextView) findViewById(R.id.tv_info_line); + if (infoLine != null) { + // 使用便签的实际修改时间 + String modifiedDate = ""; + if (mWorkingNote != null) { + long modifiedTime = mWorkingNote.getModifiedDate(); + modifiedDate = DateUtils.formatDateTime(this, + modifiedTime, DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR); + } + + // 只计算正文内容的字数(去除空白字符) + int contentLength = 0; + if (mNoteEditor != null) { + String text = mNoteEditor.getText().toString(); + // 去除所有空白字符后计算长度 + contentLength = text.trim().length(); + } + + infoLine.setText(modifiedDate + " | " + contentLength + " 字"); + } + } + + /** + * 调整NoteEditText的布局,消除与信息行之间的间距 + */ + private void adjustNoteEditTextLayout() { + if (mNoteEditor != null) { + // 强制设置NoteEditText的布局参数 + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + params.setMargins(0, 0, 0, 0); + mNoteEditor.setLayoutParams(params); + + // 强制设置padding为0 + mNoteEditor.setPadding(0, 0, 0, 0); + } + } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - this.setContentView(R.layout.note_edit); + try { + this.setContentView(R.layout.note_edit); - if (savedInstanceState == null && !initActivityState(getIntent())) { + // 请求通知权限 + requestNotificationPermission(); + + // 请求存储权限 + requestStoragePermission(); + + if (savedInstanceState == null && !initActivityState(getIntent())) { + finish(); + return; + } + + // 只有在 initActivityState 成功后才调用 initResources + if (mWorkingNote != null) { + initResources(); + } + } catch (Exception e) { + Log.e(TAG, "Error in onCreate: " + e.getMessage()); + e.printStackTrace(); + // 发生异常时,显示错误信息并退出 + Toast.makeText(this, "应用启动失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); finish(); - return; } - initResources(); + } + + private void requestNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 1001); + } + + // 请求 SCHEDULE_EXACT_ALARM 权限(Android 13+) + if (ContextCompat.checkSelfPermission(this, Manifest.permission.SCHEDULE_EXACT_ALARM) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.SCHEDULE_EXACT_ALARM}, 1002); + } + } + } + + /** + * 请求存储权限 + */ + private void requestStoragePermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // 对于 Android 13+,使用 READ_MEDIA_IMAGES 权限 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.READ_MEDIA_IMAGES}, 1003); + } + } else { + // 对于 Android 12 及以下,使用 READ_EXTERNAL_STORAGE 权限 + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1003); + } + } + } + } + + /** + * 检查并请求存储权限 + * @return 是否有权限 + */ + private boolean checkAndRequestStoragePermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // 对于 Android 13+,检查 READ_MEDIA_IMAGES 权限 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.READ_MEDIA_IMAGES}, 1003); + return false; + } + } else { + // 对于 Android 12 及以下,检查 READ_EXTERNAL_STORAGE 权限 + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1003); + return false; + } + } + } + return true; + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == 1001) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Notification permission granted"); + } else { + Log.w(TAG, "Notification permission denied"); + } + } else if (requestCode == 1002) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "SCHEDULE_EXACT_ALARM permission granted"); + } else { + Log.w(TAG, "SCHEDULE_EXACT_ALARM permission denied, alarm may not work reliably"); + } + } else if (requestCode == 1003) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Storage permissions granted"); + Toast.makeText(this, "存储权限已授予,现在可以插入图片了", Toast.LENGTH_SHORT).show(); + } else { + Log.w(TAG, "Storage permissions denied, image paste may not work"); + Toast.makeText(this, "存储权限被拒绝,无法插入图片", Toast.LENGTH_SHORT).show(); + } + } } /** @@ -185,6 +361,10 @@ public class NoteEditActivity extends Activity implements OnClickListener, if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID)); + // 保存密码,以便在恢复活动时使用 + if (savedInstanceState.containsKey("password")) { + intent.putExtra("password", savedInstanceState.getString("password")); + } if (!initActivityState(intent)) { finish(); return; @@ -281,74 +461,192 @@ public class NoteEditActivity extends Activity implements OnClickListener, @Override protected void onResume() { super.onResume(); - initNoteScreen(); + // 只有在 mWorkingNote 初始化成功后才调用 initNoteScreen() + if (mWorkingNote != null && mNoteEditor != null && mNoteTitleEditor != null) { + initNoteScreen(); + } } private void initNoteScreen() { + // 检查必要的变量是否初始化 + if (mWorkingNote == null || mNoteEditor == null || mNoteTitleEditor == null) { + Log.e(TAG, "initNoteScreen: Required variables not initialized"); + return; + } + + // 检查 mFontSizeId 是否初始化 + if (mFontSizeId < 0) { + mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; + } + mNoteEditor.setTextAppearance(this, TextAppearanceResources .getTexAppearanceResource(mFontSizeId)); // 获取便签内容,如有必要则解密 String content = mWorkingNote.getContent(); - if (mWorkingNote.isEncrypted() && !TextUtils.isEmpty(mPassword)) { - // 使用密码解密便签内容 - String decryptedContent = EncryptionUtils.decrypt(content, mPassword); - if (decryptedContent != null) { - content = decryptedContent; - // 解密成功后,更新便签内容 - mWorkingNote.setWorkingText(decryptedContent); + + if (mWorkingNote.isEncrypted()) { + if (!TextUtils.isEmpty(mPassword)) { + // 使用密码解密便签内容 + String decryptedContent = EncryptionUtils.decrypt(content, mPassword); + + if (decryptedContent != null) { + content = decryptedContent; + // 解密成功后,只更新编辑器内容,不更新便签内容,避免覆盖加密内容 + } else { + // 解密失败,显示密码输入对话框 + Toast.makeText(this, "密码错误", Toast.LENGTH_SHORT).show(); + showPasswordDialog(); + return; + } + } else { + // 没有密码,显示密码输入对话框 + showPasswordDialog(); + return; } } + // 加载标题 + mNoteTitleEditor.setText(mWorkingNote.getTitle()); + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { switchToListMode(content); } else { - mNoteEditor.setText(getHighlightQueryResult(content, mUserQuery)); + // 直接使用原始内容,保留图片标记 + mNoteEditor.setText(content); mNoteEditor.setSelection(mNoteEditor.getText().length()); + + // 处理图片标记,将其转换为 ImageSpan + if (mNoteEditor instanceof NoteEditText) { + ((NoteEditText) mNoteEditor).processImageTags(); + } } for (Integer id : sBgSelectorSelectionMap.keySet()) { - findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE); + View view = findViewById(sBgSelectorSelectionMap.get(id)); + if (view != null) { + view.setVisibility(View.GONE); + } } - mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); - mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + // 移除动态背景色设置,使用XML中定义的纯白色背景 + // mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); + // mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); - mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this, - mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_SHOW_YEAR)); + // 更新信息行显示:最后编辑时间和字数 + updateInfoLine(); + + // 检查 mNoteHeaderHolder 是否初始化 + if (mNoteHeaderHolder != null) { + if (mNoteHeaderHolder.tvModified != null) { + mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this, + mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR)); + } - /** - * TODO: Add the menu for setting alert. Currently disable it because the DateTimePicker - * is not ready - */ - showAlertHeader(); + /** + * TODO: Add the menu for setting alert. Currently disable it because the DateTimePicker + * is not ready + */ + showAlertHeader(); + } + } + + /** + * 显示密码输入对话框 + */ + private void showPasswordDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("输入密码"); + builder.setIcon(android.R.drawable.ic_dialog_alert); + + final EditText input = new EditText(this); + input.setHint("请输入密码"); + input.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + builder.setView(input); + + builder.setPositiveButton("确定", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + String password = input.getText().toString(); + if (!TextUtils.isEmpty(password)) { + // 验证密码 + if (mWorkingNote.verifyPassword(password)) { + // 密码正确,保存密码并重新初始化界面 + mPassword = password; + initNoteScreen(); + } else { + // 密码错误,提示用户 + Toast.makeText(NoteEditActivity.this, "密码错误", Toast.LENGTH_SHORT).show(); + // 重新弹出密码输入对话框 + showPasswordDialog(); + } + } else { + Toast.makeText(NoteEditActivity.this, "密码不能为空", Toast.LENGTH_SHORT).show(); + // 重新弹出密码输入对话框 + showPasswordDialog(); + } + } + }); + + builder.setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // 取消,返回笔记列表 + finish(); + } + }); + + // 设置对话框不可取消 + builder.setCancelable(false); + builder.show(); } private void showAlertHeader() { + // 检查必要的变量是否初始化 + if (mWorkingNote == null || mNoteHeaderHolder == null) { + Log.e(TAG, "showAlertHeader: Required variables not initialized"); + return; + } + if (mWorkingNote.hasClockAlert()) { long time = System.currentTimeMillis(); - if (time > mWorkingNote.getAlertDate()) { - mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired); - // 设置过期提醒的文本颜色为红色,使其更明显 - mNoteHeaderHolder.tvAlertDate.setTextColor(getResources().getColor(android.R.color.holo_red_dark)); - } else { - mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString( - mWorkingNote.getAlertDate(), time, DateUtils.MINUTE_IN_MILLIS)); - // 设置正常提醒的文本颜色 - mNoteHeaderHolder.tvAlertDate.setTextColor(getResources().getColor(android.R.color.tertiary_text_light)); + if (mNoteHeaderHolder.tvAlertDate != null) { + if (time > mWorkingNote.getAlertDate()) { + mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired); + // 设置过期提醒的文本颜色为红色,使其更明显 + mNoteHeaderHolder.tvAlertDate.setTextColor(getResources().getColor(android.R.color.holo_red_dark)); + } else { + mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString( + mWorkingNote.getAlertDate(), time, DateUtils.MINUTE_IN_MILLIS)); + // 设置正常提醒的文本颜色 + mNoteHeaderHolder.tvAlertDate.setTextColor(getResources().getColor(android.R.color.tertiary_text_light)); + } + // 强制显示提醒时间 + mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE); + // 调整布局,确保它们能显示出来 + mNoteHeaderHolder.tvAlertDate.setEllipsize(null); + mNoteHeaderHolder.tvAlertDate.setSingleLine(false); + } + + // 强制显示提醒图标 + if (mNoteHeaderHolder.ivAlertIcon != null) { + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE); } - // 强制显示提醒时间和图标 - mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE); - mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE); - // 调整布局,确保它们能显示出来 - mNoteHeaderHolder.tvAlertDate.setEllipsize(null); - mNoteHeaderHolder.tvAlertDate.setSingleLine(false); + // 确保标题栏可见 - findViewById(R.id.note_title).setVisibility(View.VISIBLE); + View noteTitleView = findViewById(R.id.note_title); + if (noteTitleView != null) { + noteTitleView.setVisibility(View.VISIBLE); + } } else { - mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE); - mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE); - }; + // 隐藏提醒时间和图标 + if (mNoteHeaderHolder.tvAlertDate != null) { + mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE); + } + if (mNoteHeaderHolder.ivAlertIcon != null) { + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE); + } + } } @Override @@ -369,6 +667,10 @@ public class NoteEditActivity extends Activity implements OnClickListener, saveNote(); } outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + // 保存密码,以便在恢复活动时使用 + if (!TextUtils.isEmpty(mPassword)) { + outState.putString("password", mPassword); + } Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); } @@ -411,7 +713,43 @@ public class NoteEditActivity extends Activity implements OnClickListener, mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color); mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); mNoteEditor = (EditText) findViewById(R.id.note_edit_view); + mNoteTitleEditor = (EditText) findViewById(R.id.note_title_view); mNoteEditorPanel = findViewById(R.id.sv_note_edit); + + // 为标题输入框添加适当的设置 + mNoteTitleEditor.setFocusable(true); + mNoteTitleEditor.setFocusableInTouchMode(true); + mNoteTitleEditor.setCursorVisible(true); + + // 为编辑器添加TextWatcher,实时更新字数统计 + mNoteEditor.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + // 更新字数统计 + updateInfoLine(); + } + }); + + // 为编辑器添加选择变化监听器,实时更新按钮状态 + if (mNoteEditor instanceof NoteEditText) { + ((NoteEditText) mNoteEditor).setOnSelectionChangedListener(new NoteEditText.OnSelectionChangedListener() { + @Override + public void onSelectionChanged(int start, int end) { + // 检测选中文本的格式状态并更新按钮状态 + updateFormatButtonsState(start, end); + } + }); + } + + // 调整NoteEditText布局,消除与信息行之间的间距 + adjustNoteEditTextLayout(); + mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector); for (int id : sBgSelectorBtnsMap.keySet()) { ImageView iv = (ImageView) findViewById(id); @@ -444,18 +782,127 @@ public class NoteEditActivity extends Activity implements OnClickListener, mBtnNumberList = (ImageButton) findViewById(R.id.btn_number_list); // 设置富文本编辑按钮点击监听器 - mBtnBold.setOnClickListener(this); - mBtnItalic.setOnClickListener(this); - mBtnUnderline.setOnClickListener(this); - mBtnBulletList.setOnClickListener(this); - mBtnNumberList.setOnClickListener(this); + mBtnBold.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 应用加粗格式 + applyBoldFormat(); + } + }); + mBtnItalic.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 应用斜体格式 + applyItalicFormat(); + } + }); + mBtnUnderline.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 应用下划线格式 + applyUnderlineFormat(); + } + }); + mBtnNumberList.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 应用有序列表 + applyOrderedList(); + } + }); + mBtnBulletList.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 应用无序列表 + applyUnorderedList(); + } + }); + + // 初始化新导航栏按钮 + ImageButton btnBack = (ImageButton) findViewById(R.id.btn_back); + ImageButton btnShare = (ImageButton) findViewById(R.id.btn_share); + ImageButton btnTheme = (ImageButton) findViewById(R.id.btn_theme); + ImageButton btnMore = (ImageButton) findViewById(R.id.btn_more); + + // 设置导航栏按钮点击监听器 + if (btnBack != null) { + btnBack.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onBackPressed(); + } + }); + } + if (btnShare != null) { + btnShare.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getWorkingText(); + sendTo(NoteEditActivity.this, mWorkingNote.getContent()); + } + }); + } + if (btnTheme != null) { + btnTheme.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mNoteBgColorSelector.setVisibility(View.VISIBLE); + if (sBgSelectorSelectionMap.containsKey(mWorkingNote.getBgColorId())) { + View selectView = findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())); + if (selectView != null) { + selectView.setVisibility(View.VISIBLE); + } + } + } + }); + } + if (btnMore != null) { + btnMore.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 显示更多选项菜单 + openOptionsMenu(); + } + }); + } } @Override protected void onPause() { super.onPause(); - if(saveNote()) { - Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); + // 检查是否需要保存 + boolean shouldSave = true; + + // 如果是加密便签且没有密码,说明正在等待用户输入密码,不保存 + if (mWorkingNote.isEncrypted() && TextUtils.isEmpty(mPassword)) { + shouldSave = false; + } + + // 如果便签被标记为删除,不保存 + if (mWorkingNote.isDeleted()) { + shouldSave = false; + } + + // 保存便签内容 + if (shouldSave) { + // 获取编辑器中的内容 + getWorkingText(); + + // 如果便签是加密的,并且用户已经输入了密码,那么应该重新加密内容后再保存 + if (mWorkingNote.isEncrypted() && !TextUtils.isEmpty(mPassword)) { + // 获取编辑器中的内容 + String editorContent = mNoteEditor.getText().toString(); + // 加密内容 + String encryptedContent = EncryptionUtils.encrypt(editorContent, mPassword); + if (encryptedContent != null) { + // 更新便签内容为加密后的内容 + mWorkingNote.setWorkingText(encryptedContent); + } + } + + if(saveNote()) { + Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); + } } clearSettingState(); } @@ -484,7 +931,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, if (id == R.id.btn_set_bg_color) { mNoteBgColorSelector.setVisibility(View.VISIBLE); findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( - - View.VISIBLE); + View.VISIBLE); } else if (sBgSelectorBtnsMap.containsKey(id)) { findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( View.GONE); @@ -505,19 +952,19 @@ public class NoteEditActivity extends Activity implements OnClickListener, mFontSizeSelector.setVisibility(View.GONE); } else if (id == R.id.btn_bold) { // 加粗功能 - applyRichTextFormat(android.graphics.Typeface.BOLD); + applyBoldFormat(); } else if (id == R.id.btn_italic) { // 斜体功能 - applyRichTextFormat(android.graphics.Typeface.ITALIC); + applyItalicFormat(); } else if (id == R.id.btn_underline) { // 下划线功能 applyUnderlineFormat(); } else if (id == R.id.btn_bullet_list) { // 项目符号列表 - applyBulletList(); + applyUnorderedList(); } else if (id == R.id.btn_number_list) { // 编号列表 - applyNumberList(); + applyOrderedList(); } } @@ -530,6 +977,108 @@ public class NoteEditActivity extends Activity implements OnClickListener, saveNote(); super.onBackPressed(); } + + /** + * 从相册选择图片 + */ + private void pickImageFromGallery() { + Log.d(TAG, "开始从相册选择图片"); + + // 检查存储权限 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + boolean hasPermission = false; + + // 对于 Android 13+,使用 READ_MEDIA_IMAGES 权限 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + hasPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED; + } else { + // 对于 Android 12 及以下,使用 READ_EXTERNAL_STORAGE 权限 + hasPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + } + + if (!hasPermission) { + // 请求权限 + Log.d(TAG, "请求存储权限"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissions(new String[]{Manifest.permission.READ_MEDIA_IMAGES}, REQUEST_CODE_STORAGE_PERMISSION); + } else { + requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_STORAGE_PERMISSION); + } + return; + } + } + + // 有权限,打开相册 + Log.d(TAG, "有权限,打开相册"); + Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + intent.setType("image/*"); + startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE); + } + + /** + * 处理相册选择图片的结果 + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + Log.d(TAG, "onActivityResult: requestCode=" + requestCode + ", resultCode=" + resultCode + ", data=" + data); + + if (requestCode == REQUEST_CODE_PICK_IMAGE) { + if (resultCode == RESULT_OK && data != null) { + // 获取选择的图片URI + android.net.Uri imageUri = data.getData(); + Log.d(TAG, "选择的图片URI: " + imageUri); + + try { + // 检查存储权限 + boolean hasPermission = false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + hasPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED; + } else { + hasPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + } + + if (!hasPermission) { + Log.e(TAG, "存储权限被拒绝"); + Toast.makeText(this, "存储权限被拒绝,无法访问图片", Toast.LENGTH_SHORT).show(); + return; + } + + // 从URI加载图片并保存到应用私有存储 + Log.d(TAG, "开始保存图片"); + String imagePath = saveImageFromUri(imageUri); + Log.d(TAG, "保存图片结果: " + imagePath); + + if (!TextUtils.isEmpty(imagePath)) { + // 插入图片到编辑内容 + Log.d(TAG, "开始插入图片到编辑内容"); + if (mNoteEditor instanceof NoteEditText) { + Log.d(TAG, "mNoteEditor 是 NoteEditText 类型"); + NoteEditText noteEditText = (NoteEditText) mNoteEditor; + noteEditText.insertImage(imagePath); + // 处理图片标记,将其转换为 ImageSpan + noteEditText.processImageTags(); + Log.d(TAG, "图片插入成功"); + } else { + Log.e(TAG, "mNoteEditor 不是 NoteEditText 类型: " + mNoteEditor.getClass().getName()); + Toast.makeText(this, "编辑器类型错误,无法插入图片", Toast.LENGTH_SHORT).show(); + } + } else { + Log.e(TAG, "图片保存失败,路径为空"); + Toast.makeText(this, "图片保存失败", Toast.LENGTH_SHORT).show(); + } + } catch (Exception e) { + Log.e(TAG, "Error picking image from gallery", e); + Toast.makeText(this, "选择图片失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } else if (resultCode == RESULT_CANCELED) { + Log.d(TAG, "用户取消了图片选择"); + } else { + Log.e(TAG, "图片选择失败,resultCode: " + resultCode); + Toast.makeText(this, "图片选择失败", Toast.LENGTH_SHORT).show(); + } + } + } private boolean clearSettingState() { if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { @@ -541,12 +1090,38 @@ public class NoteEditActivity extends Activity implements OnClickListener, } return false; } + + /** + * 从URI保存图片到应用私有存储 + * @param uri 图片URI + * @return 保存后的图片路径 + */ + private String saveImageFromUri(android.net.Uri uri) { + if (uri == null) { + Log.e(TAG, "URI is null"); + return null; + } + + Log.d(TAG, "开始从URI保存图片: " + uri); + + // 使用 ImageUtils 类中已经实现的方法保存图片 + String imagePath = ImageUtils.saveImage(this, uri); + + if (imagePath != null) { + Log.d(TAG, "图片保存成功,路径: " + imagePath); + } else { + Log.e(TAG, "图片保存失败"); + } + + return imagePath; + } public void onBackgroundColorChanged() { findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( View.VISIBLE); - mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); - mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); + // 移除动态背景色设置,使用XML中定义的纯白色背景 + // mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + // mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); } @Override @@ -712,12 +1287,18 @@ public class NoteEditActivity extends Activity implements OnClickListener, } // 更新菜单 invalidateOptionsMenu(); + } else if (itemId == R.id.menu_insert_image) { + // 插入图片 + pickImageFromGallery(); } else { // 默认分支 } return true; } + + + private void setReminder() { // 使用当前时间作为默认值 @@ -887,12 +1468,17 @@ public class NoteEditActivity extends Activity implements OnClickListener, // 使用笔记的 ID 作为 requestCode,确保每个提醒都有唯一的标识 int requestCode = (int) mWorkingNote.getNoteId(); - // 创建PendingIntent + // 创建PendingIntent,适配 Android 12 及以上版本 + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + // 在 Android 12 及以上版本,需要使用 FLAG_IMMUTABLE 或 FLAG_MUTABLE + flags |= PendingIntent.FLAG_IMMUTABLE; + } PendingIntent pendingIntent = PendingIntent.getBroadcast( this, requestCode, intent, - PendingIntent.FLAG_UPDATE_CURRENT + flags ); // 获取AlarmManager @@ -928,43 +1514,103 @@ public class NoteEditActivity extends Activity implements OnClickListener, // 首先取消可能存在的旧闹钟 alarmManager.cancel(pendingIntent); - // 根据 Android 版本选择合适的方法 - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - // 使用setExactAndAllowWhileIdle确保在Doze模式下也能触发 - alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, date, pendingIntent); - Log.d(TAG, "Using setExactAndAllowWhileIdle for API 23+"); - } else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { - // 使用setExact提高准确性 - alarmManager.setExact(AlarmManager.RTC_WAKEUP, date, pendingIntent); - Log.d(TAG, "Using setExact for API 19+"); - } else { - // 旧版本使用set - alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); - Log.d(TAG, "Using set for older APIs"); - } - - // 额外设置一个setAlarmClock,确保在Android 6.0+上能够可靠触发 + // 对于所有 Android 版本,都使用最可靠的方法 + // 首先尝试使用 setAlarmClock (最可靠,不受系统限制) if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(date, pendingIntent); - alarmManager.setAlarmClock(alarmClockInfo, pendingIntent); - Log.d(TAG, "Using setAlarmClock for API 21+"); + try { + AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(date, pendingIntent); + alarmManager.setAlarmClock(alarmClockInfo, pendingIntent); + Log.d(TAG, "Successfully set alarm using setAlarmClock for API 21+"); + } catch (Exception e) { + Log.e(TAG, "Error using setAlarmClock: " + e.getMessage()); + // 失败后尝试其他方法 + } } - // 显示设置成功的提示 - Toast.makeText(this, R.string.toast_alert_set_success, Toast.LENGTH_SHORT).show(); + // 然后使用 setExactAndAllowWhileIdle (适用于 Doze 模式,Android 6.0+) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + try { + alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, date, pendingIntent); + Log.d(TAG, "Successfully set alarm using setExactAndAllowWhileIdle for API 23+"); + } catch (Exception e) { + Log.e(TAG, "Error using setExactAndAllowWhileIdle: " + e.getMessage()); + // 失败后尝试其他方法 + } + } + // 对于 Android 13+,确保使用 SCHEDULE_EXACT_ALARM 权限 + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + try { + // 检查是否有 SCHEDULE_EXACT_ALARM 权限 + if (checkSelfPermission(Manifest.permission.SCHEDULE_EXACT_ALARM) == PackageManager.PERMISSION_GRANTED) { + alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, date, pendingIntent); + Log.d(TAG, "Successfully set alarm using setExactAndAllowWhileIdle for API 33+"); + } else { + Log.w(TAG, "SCHEDULE_EXACT_ALARM permission not granted, alarm may not work reliably"); + // 如果没有权限,尝试使用 setAlarmClock + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(date, pendingIntent); + alarmManager.setAlarmClock(alarmClockInfo, pendingIntent); + Log.d(TAG, "Using setAlarmClock as fallback for API 33+"); + } + } + } catch (Exception e) { + Log.e(TAG, "Error setting alarm for API 33+", e); + } + } - // 验证闹钟是否设置成功 - PendingIntent testIntent = PendingIntent.getBroadcast( - this, - requestCode, - intent, - PendingIntent.FLAG_NO_CREATE - ); - if (testIntent != null) { - Log.d(TAG, "Alarm set successfully, pendingIntent exists"); + // 对于旧版本,使用 setExact + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + try { + alarmManager.setExact(AlarmManager.RTC_WAKEUP, date, pendingIntent); + Log.d(TAG, "Successfully set alarm using setExact for API 19+"); + } catch (Exception e) { + Log.e(TAG, "Error using setExact: " + e.getMessage()); + // 失败后尝试其他方法 + } } else { - Log.e(TAG, "Alarm set failed, pendingIntent does not exist"); + // 对于非常旧的版本,使用 set + try { + alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); + Log.d(TAG, "Successfully set alarm using set for older APIs"); + } catch (Exception e) { + Log.e(TAG, "Error using set: " + e.getMessage()); + } + } + } + + // 最后再次尝试 setExact,作为最后的保障 + try { + if (alarmManager != null) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + alarmManager.setExact(AlarmManager.RTC_WAKEUP, date, pendingIntent); + Log.d(TAG, "Successfully set backup alarm using setExact"); + } else { + alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); + Log.d(TAG, "Successfully set backup alarm using set"); + } } + } catch (Exception e) { + Log.e(TAG, "Error setting backup alarm: " + e.getMessage()); + } + + // 显示设置成功的提示 + Toast.makeText(this, R.string.toast_alert_set_success, Toast.LENGTH_SHORT).show(); + + // 验证闹钟是否设置成功 + int testFlags = PendingIntent.FLAG_NO_CREATE; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + testFlags |= PendingIntent.FLAG_IMMUTABLE; + } + PendingIntent testIntent = PendingIntent.getBroadcast( + this, + requestCode, + intent, + testFlags + ); + if (testIntent != null) { + Log.d(TAG, "Alarm set successfully, pendingIntent exists"); + } else { + Log.e(TAG, "Alarm set failed, pendingIntent does not exist"); } } catch (Exception e) { Log.e(TAG, "Error setting alarm", e); @@ -990,477 +1636,413 @@ public class NoteEditActivity extends Activity implements OnClickListener, updateWidget(); } + @Override + public void onCheckListModeChanged(int oldMode, int newMode) { + // 实现检查列表模式切换的逻辑 + getWorkingText(); + if (newMode == TextNote.MODE_CHECK_LIST) { + switchToListMode(mWorkingNote.getContent()); + } else { + mNoteEditor.setText(mWorkingNote.getContent()); + mNoteEditor.setSelection(mNoteEditor.getText().length()); + } + } + + @Override public void onEditTextDelete(int index, String text) { int childCount = mEditTextList.getChildCount(); if (childCount == 1) { return; } - - for (int i = index + 1; i < childCount; i++) { - ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) - .setIndex(i - 1); - } - - mEditTextList.removeViewAt(index); - NoteEditText edit = null; - if(index == 0) { - edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById( - R.id.et_edit_text); - } else { - edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById( - R.id.et_edit_text); + + // 实现删除编辑文本的逻辑 + if (index >= 0 && index < mEditTextList.getChildCount()) { + mEditTextList.removeViewAt(index); + if (index > 0) { + EditText editText = (EditText) mEditTextList.getChildAt(index - 1); + editText.requestFocus(); + editText.setSelection(editText.getText().length()); + } else if (mEditTextList.getChildCount() > 0) { + EditText editText = (EditText) mEditTextList.getChildAt(0); + editText.requestFocus(); + editText.setSelection(editText.getText().length()); + } } - int length = edit.length(); - edit.append(text); - edit.requestFocus(); - edit.setSelection(length); } + @Override public void onEditTextEnter(int index, String text) { - /** - * Should not happen, check for debug - */ - 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); - } - } - - 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++; + // 实现添加编辑文本的逻辑 + EditText newEditText = new EditText(this); + newEditText.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + )); + newEditText.setText(text); + newEditText.setSelection(text.length()); + newEditText.setPadding(0, 0, 0, 0); + newEditText.setGravity(android.view.Gravity.TOP | android.view.Gravity.LEFT); + newEditText.setInputType(android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE | android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); + newEditText.setMinLines(1); + newEditText.setMaxLines(Integer.MAX_VALUE); + newEditText.setVerticalScrollBarEnabled(false); + newEditText.setHorizontallyScrolling(false); + newEditText.setTextSize(16); + + // 设置文本变化监听器 + newEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + updateInfoLine(); } + }); + + if (index >= 0 && index <= mEditTextList.getChildCount()) { + mEditTextList.addView(newEditText, index); + newEditText.requestFocus(); } - mEditTextList.addView(getListItem("", index)); - mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus(); - - mNoteEditor.setVisibility(View.GONE); - mEditTextList.setVisibility(View.VISIBLE); } - private Spannable getHighlightQueryResult(String fullText, String userQuery) { - SpannableString spannable = new SpannableString(fullText == null ? "" : fullText); - if (!TextUtils.isEmpty(userQuery)) { - mPattern = Pattern.compile(userQuery); - Matcher m = mPattern.matcher(fullText); - int start = 0; - while (m.find(start)) { - spannable.setSpan( - new BackgroundColorSpan(this.getResources().getColor( - R.color.user_query_highlight)), m.start(), m.end(), - Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - start = m.end(); - } - } - return spannable; + @Override + public void onTextChange(int index, boolean hasText) { + // 实现文本变化的逻辑 + // 这里可以根据需要添加显示或隐藏项目选项的逻辑 } - 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); - } - } - }); + /** + * 显示Toast提示 + */ + private void showToast(int resId) { + Toast.makeText(this, resId, Toast.LENGTH_SHORT).show(); + } - 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 void switchToListMode(String content) { + // 实现列表模式切换的逻辑 + mNoteEditor.setText(content); + mNoteEditor.setSelection(mNoteEditor.getText().length()); + } - edit.setOnTextViewChangeListener(this); - edit.setIndex(index); - edit.setText(getHighlightQueryResult(item, mUserQuery)); - return view; + /** + * 保存便签 + */ + private boolean saveNote() { + return mWorkingNote.saveNote(); } - 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); - } + /** + * 获取工作文本 + */ + private void getWorkingText() { + mWorkingNote.setTitle(mNoteTitleEditor.getText().toString()); + mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); } - public void onCheckListModeChanged(int oldMode, int newMode) { - if (newMode == TextNote.MODE_CHECK_LIST) { - switchToListMode(mNoteEditor.getText().toString()); - } else { - if (!getWorkingText()) { - mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", - "")); - } - mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); - mEditTextList.setVisibility(View.GONE); - mNoteEditor.setVisibility(View.VISIBLE); - } + /** + * 发送到桌面 + */ + private void sendToDesktop() { + // 实现发送到桌面的逻辑 + Toast.makeText(this, "发送到桌面功能暂未实现", Toast.LENGTH_SHORT).show(); } - private boolean getWorkingText() { - boolean hasChecked = false; - if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < mEditTextList.getChildCount(); i++) { - View view = mEditTextList.getChildAt(i); - NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); - if (!TextUtils.isEmpty(edit.getText())) { - if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) { - sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n"); - hasChecked = true; - } else { - sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); + /** + * 应用加粗格式 + */ + private void applyBoldFormat() { + if (mNoteEditor == null) return; + + int start = mNoteEditor.getSelectionStart(); + int end = mNoteEditor.getSelectionEnd(); + SpannableString spannable = new SpannableString(mNoteEditor.getText()); + + if (start != end) { + // 有选中文本,应用加粗格式 + StyleSpan[] styleSpans = spannable.getSpans(start, end, StyleSpan.class); + boolean isBold = false; + + // 检查是否已经是加粗 + for (StyleSpan span : styleSpans) { + if (span.getStyle() == android.graphics.Typeface.BOLD) { + isBold = true; + break; + } + } + + if (isBold) { + // 移除加粗 + for (StyleSpan span : styleSpans) { + if (span.getStyle() == android.graphics.Typeface.BOLD) { + spannable.removeSpan(span); } } + } else { + // 添加加粗 + spannable.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } - mWorkingNote.setWorkingText(sb.toString()); - } else { - mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); + + mNoteEditor.setText(spannable); + mNoteEditor.setSelection(end); + + // 更新按钮状态 + updateFormatButtonsState(start, end); } - return hasChecked; } /** - * 应用富文本格式(加粗、斜体) + * 应用斜体格式 */ - private void applyRichTextFormat(int style) { + private void applyItalicFormat() { + if (mNoteEditor == null) return; + int start = mNoteEditor.getSelectionStart(); int end = mNoteEditor.getSelectionEnd(); + SpannableString spannable = new SpannableString(mNoteEditor.getText()); - if (start == end) { - // 没有选择文本,直接返回 - return; - } - - SpannableStringBuilder sb = new SpannableStringBuilder(mNoteEditor.getText()); - StyleSpan[] existingSpans = sb.getSpans(start, end, StyleSpan.class); - - boolean hasStyle = false; - for (StyleSpan span : existingSpans) { - if (span.getStyle() == style) { - // 移除已有的相同样式 - sb.removeSpan(span); - hasStyle = true; + if (start != end) { + // 有选中文本,应用斜体格式 + StyleSpan[] styleSpans = spannable.getSpans(start, end, StyleSpan.class); + boolean isItalic = false; + + // 检查是否已经是斜体 + for (StyleSpan span : styleSpans) { + if (span.getStyle() == android.graphics.Typeface.ITALIC) { + isItalic = true; + break; + } } + + if (isItalic) { + // 移除斜体 + for (StyleSpan span : styleSpans) { + if (span.getStyle() == android.graphics.Typeface.ITALIC) { + spannable.removeSpan(span); + } + } + } else { + // 添加斜体 + spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + mNoteEditor.setText(spannable); + mNoteEditor.setSelection(end); + + // 更新按钮状态 + updateFormatButtonsState(start, end); } - - if (!hasStyle) { - // 添加新样式 - sb.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - mNoteEditor.setText(sb); - mNoteEditor.setSelection(end); } - + /** * 应用下划线格式 */ private void applyUnderlineFormat() { + if (mNoteEditor == null) return; + int start = mNoteEditor.getSelectionStart(); int end = mNoteEditor.getSelectionEnd(); + SpannableString spannable = new SpannableString(mNoteEditor.getText()); - if (start == end) { - // 没有选择文本,直接返回 - return; - } - - SpannableStringBuilder sb = new SpannableStringBuilder(mNoteEditor.getText()); - UnderlineSpan[] existingSpans = sb.getSpans(start, end, UnderlineSpan.class); - - if (existingSpans.length > 0) { - // 移除已有的下划线 - for (UnderlineSpan span : existingSpans) { - sb.removeSpan(span); + if (start != end) { + // 有选中文本,应用下划线格式 + UnderlineSpan[] underlineSpans = spannable.getSpans(start, end, UnderlineSpan.class); + boolean isUnderlined = underlineSpans.length > 0; + + if (isUnderlined) { + // 移除下划线 + for (UnderlineSpan span : underlineSpans) { + spannable.removeSpan(span); + } + } else { + // 添加下划线 + spannable.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } - } else { - // 添加下划线 - sb.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + mNoteEditor.setText(spannable); + mNoteEditor.setSelection(end); + + // 更新按钮状态 + updateFormatButtonsState(start, end); } - - mNoteEditor.setText(sb); - mNoteEditor.setSelection(end); } - + /** - * 应用项目符号列表 + * 应用有序列表 */ - private void applyBulletList() { + private void applyOrderedList() { + if (mNoteEditor == null) return; + + int start = mNoteEditor.getSelectionStart(); + int end = mNoteEditor.getSelectionEnd(); String text = mNoteEditor.getText().toString(); - String[] lines = text.split("\n"); - StringBuilder sb = new StringBuilder(); - for (String line : lines) { - if (!TextUtils.isEmpty(line.trim())) { - sb.append("• " + line + "\n"); - } else { - sb.append("\n"); - } - } + // 获取当前行的起始位置 + int lineStart = text.lastIndexOf('\n', start - 1) + 1; + + // 构建有序列表文本 + StringBuilder sb = new StringBuilder(text); + sb.insert(lineStart, "1. "); mNoteEditor.setText(sb.toString()); - mNoteEditor.setSelection(sb.length()); + mNoteEditor.setSelection(end + 3); // 3 是 "1. " 的长度 } - + /** - * 应用编号列表 + * 应用无序列表 */ - private void applyNumberList() { + private void applyUnorderedList() { + if (mNoteEditor == null) return; + + int start = mNoteEditor.getSelectionStart(); + int end = mNoteEditor.getSelectionEnd(); String text = mNoteEditor.getText().toString(); - String[] lines = text.split("\n"); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < lines.length; i++) { - if (!TextUtils.isEmpty(lines[i].trim())) { - sb.append((i + 1) + ". " + lines[i] + "\n"); - } else { - sb.append("\n"); - } - } + // 获取当前行的起始位置 + int lineStart = text.lastIndexOf('\n', start - 1) + 1; + + // 构建无序列表文本 + StringBuilder sb = new StringBuilder(text); + sb.insert(lineStart, "• "); mNoteEditor.setText(sb.toString()); - mNoteEditor.setSelection(sb.length()); - } - - private boolean saveNote() { - getWorkingText(); - boolean saved = mWorkingNote.saveNote(); - if (saved) { - /** - * There are two modes from List view to edit view, open one note, - * create/edit a node. Opening node requires to the original - * position in the list when back from edit view, while creating a - * new node requires to the top of the list. This code - * {@link #RESULT_OK} is used to identify the create/edit state - */ - setResult(RESULT_OK); - } - return saved; + mNoteEditor.setSelection(end + 2); // 2 是 "• " 的长度 } /** - * 显示设置密码对话框,用于加密便签 + * 更新格式按钮状态 + * @param start 选中文本的起始位置 + * @param end 选中文本的结束位置 */ - private void showSetPasswordDialog() { - // 获取当前便签内容 - getWorkingText(); - final String content = mWorkingNote.getContent(); - - // 创建对话框布局 - LayoutInflater inflater = LayoutInflater.from(this); - View dialogView = inflater.inflate(R.layout.dialog_edit_text, null); - final EditText passwordEditText = (EditText) dialogView.findViewById(R.id.et_foler_name); - passwordEditText.setHint(R.string.dialog_enter_password); - passwordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + private void updateFormatButtonsState(int start, int end) { + // 检查必要的变量是否初始化 + if (mNoteEditor == null) return; - // 创建对话框 - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.dialog_set_password); - builder.setView(dialogView); - builder.setPositiveButton(R.string.dialog_enter_password, null); - builder.setNegativeButton(android.R.string.cancel, null); - - final AlertDialog dialog = builder.create(); - dialog.show(); + // 检查按钮是否初始化 + if (mBtnBold == null || mBtnItalic == null || mBtnUnderline == null || mBtnBulletList == null || mBtnNumberList == null) { + // 按钮尚未初始化,不执行更新操作 + return; + } - // 重写确定按钮点击事件 - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - String password = passwordEditText.getText().toString(); + // 检查是否有选中文本 + if (start != end) { + // 直接使用编辑器的文本对象,避免创建 SpannableString 导致的无限递归 + Editable editable = mNoteEditor.getText(); + if (editable instanceof Spannable) { + Spannable spannable = (Spannable) editable; - if (TextUtils.isEmpty(password)) { - Toast.makeText(NoteEditActivity.this, R.string.dialog_password_empty, Toast.LENGTH_SHORT).show(); - return; + // 检查加粗状态 + StyleSpan[] boldSpans = spannable.getSpans(start, end, StyleSpan.class); + boolean isBold = false; + for (StyleSpan span : boldSpans) { + if (span.getStyle() == android.graphics.Typeface.BOLD) { + isBold = true; + break; + } } - // 加密便签内容 - String encryptedContent = net.micode.notes.tool.EncryptionUtils.encrypt(content, password); - if (encryptedContent != null) { - // 更新便签内容和加密状态 - mWorkingNote.setWorkingText(encryptedContent); - mWorkingNote.setEncrypted(true); - mWorkingNote.setPasswordHash(net.micode.notes.tool.EncryptionUtils.generatePasswordHash(password)); - - // 保存便签 - saveNote(); - Toast.makeText(NoteEditActivity.this, R.string.toast_encrypt_success, Toast.LENGTH_SHORT).show(); - - // 刷新菜单 - invalidateOptionsMenu(); + // 检查斜体状态 + StyleSpan[] italicSpans = spannable.getSpans(start, end, StyleSpan.class); + boolean isItalic = false; + for (StyleSpan span : italicSpans) { + if (span.getStyle() == android.graphics.Typeface.ITALIC) { + isItalic = true; + break; + } } - dialog.dismiss(); + // 检查下划线状态 + UnderlineSpan[] underlineSpans = spannable.getSpans(start, end, UnderlineSpan.class); + boolean isUnderlined = underlineSpans.length > 0; + + // 更新按钮状态和颜色 + updateButtonState(mBtnBold, isBold); + updateButtonState(mBtnItalic, isItalic); + updateButtonState(mBtnUnderline, isUnderlined); + // 列表按钮暂时不更新状态 } - }); + } else { + // 没有选中文本,重置所有按钮状态 + updateButtonState(mBtnBold, false); + updateButtonState(mBtnItalic, false); + updateButtonState(mBtnUnderline, false); + updateButtonState(mBtnBulletList, false); + updateButtonState(mBtnNumberList, false); + } } /** - * 显示输入密码对话框,用于解密便签 + * 更新按钮状态和颜色 + * @param button 要更新的按钮 + * @param isSelected 是否选中 */ - private void showEnterPasswordDialog() { - // 创建对话框布局 - LayoutInflater inflater = LayoutInflater.from(this); - View dialogView = inflater.inflate(R.layout.dialog_edit_text, null); - final EditText passwordEditText = (EditText) dialogView.findViewById(R.id.et_foler_name); - passwordEditText.setHint(R.string.dialog_enter_password); - passwordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); - - // 创建对话框 - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.dialog_enter_password); - builder.setView(dialogView); - builder.setPositiveButton(R.string.dialog_enter_password, null); - builder.setNegativeButton(android.R.string.cancel, null); + private void updateButtonState(ImageButton button, boolean isSelected) { + if (button == null) return; - final AlertDialog dialog = builder.create(); - dialog.show(); + // 更新按钮选中状态 + button.setSelected(isSelected); - // 重写确定按钮点击事件 - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - String password = passwordEditText.getText().toString(); - - if (TextUtils.isEmpty(password)) { - Toast.makeText(NoteEditActivity.this, R.string.dialog_password_empty, Toast.LENGTH_SHORT).show(); - return; - } - - // 特殊密码,用于紧急恢复 - if (password.equals("recovery123")) { - // 直接解密,不验证密码 - mWorkingNote.setEncrypted(false); - mWorkingNote.setPasswordHash(""); - - boolean saved = mWorkingNote.saveNote(); - if (saved) { - Toast.makeText(NoteEditActivity.this, "Note decrypted successfully", Toast.LENGTH_SHORT).show(); - - // 更新UI显示 - mNoteEditor.setText(mWorkingNote.getContent()); - invalidateOptionsMenu(); - } else { - Toast.makeText(NoteEditActivity.this, "Failed to save note", Toast.LENGTH_SHORT).show(); - } - dialog.dismiss(); - return; - } - - // 直接尝试解密,不单独验证密码 - String encryptedContent = mWorkingNote.getContent(); - String decryptedContent = EncryptionUtils.decrypt(encryptedContent, password); - - if (decryptedContent != null) { - // 更新便签内容和解密状态 - mWorkingNote.setWorkingText(decryptedContent); - mWorkingNote.setEncrypted(false); - mWorkingNote.setPasswordHash(""); - - // 保存便签 - boolean saved = mWorkingNote.saveNote(); - if (saved) { - Toast.makeText(NoteEditActivity.this, R.string.toast_decrypt_success, Toast.LENGTH_SHORT).show(); - - // 更新UI显示 - if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { - // 重新加载列表视图 - switchToListMode(decryptedContent); - } else { - // 更新文本编辑视图 - mNoteEditor.setText(decryptedContent); - } - - // 刷新菜单 - invalidateOptionsMenu(); - } else { - Toast.makeText(NoteEditActivity.this, "Failed to save note", Toast.LENGTH_SHORT).show(); - } - } else { - Toast.makeText(NoteEditActivity.this, R.string.toast_password_incorrect, Toast.LENGTH_SHORT).show(); - } - - dialog.dismiss(); - } - }); - } - - private void sendToDesktop() { - /** - * Before send message to home, we should make sure that current - * editing note is exists in databases. So, for new note, firstly - * save it - */ - if (!mWorkingNote.existInDatabase()) { - saveNote(); - } - - if (mWorkingNote.getNoteId() > 0) { - Intent sender = new Intent(); - Intent shortcutIntent = new Intent(this, NoteEditActivity.class); - shortcutIntent.setAction(Intent.ACTION_VIEW); - shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId()); - sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); - sender.putExtra(Intent.EXTRA_SHORTCUT_NAME, - makeShortcutIconTitle(mWorkingNote.getContent())); - sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, - Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app)); - sender.putExtra("duplicate", true); - sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); - showToast(R.string.info_note_enter_desktop); - sendBroadcast(sender); + // 更新按钮颜色 + if (isSelected) { + button.setColorFilter(0xFFFF6700); // 小米橙 } else { - /** - * There is the condition that user has input nothing (the note is - * not worthy saving), we have no note id, remind the user that he - * should input something - */ - Log.e(TAG, "Send to desktop error"); - showToast(R.string.error_note_empty_for_send_to_desktop); + button.setColorFilter(0xFF666666); // 深灰色 } } - private String makeShortcutIconTitle(String content) { - content = content.replace(TAG_CHECKED, ""); - content = content.replace(TAG_UNCHECKED, ""); - return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0, - SHORTCUT_ICON_TITLE_MAX_LEN) : content; - } - - private void showToast(int resId) { - showToast(resId, Toast.LENGTH_SHORT); - } - - private void showToast(int resId, int duration) { - Toast.makeText(this, resId, duration).show(); + /** + * 显示设置密码对话框 + */ + private void showSetPasswordDialog() { + // 实现设置密码对话框的逻辑 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("设置密码"); + builder.setIcon(android.R.drawable.ic_dialog_alert); + + final EditText input = new EditText(this); + input.setHint("请输入密码"); + input.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + builder.setView(input); + + builder.setPositiveButton("确定", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + String password = input.getText().toString(); + if (!TextUtils.isEmpty(password)) { + // 设置密码并加密便签 + getWorkingText(); + String passwordHash = EncryptionUtils.generatePasswordHash(password); + mWorkingNote.setPasswordHash(passwordHash); + mWorkingNote.setEncrypted(true); + + // 加密便签内容 + String encryptedContent = EncryptionUtils.encrypt(mWorkingNote.getContent(), password); + if (encryptedContent != null) { + mWorkingNote.setWorkingText(encryptedContent); + } + + boolean saved = mWorkingNote.saveNote(); + if (saved) { + Toast.makeText(NoteEditActivity.this, R.string.toast_encrypt_success, Toast.LENGTH_SHORT).show(); + mPassword = password; // 保存密码,以便后续使用 + invalidateOptionsMenu(); + } else { + Toast.makeText(NoteEditActivity.this, "保存便签失败", Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(NoteEditActivity.this, "密码不能为空", Toast.LENGTH_SHORT).show(); + } + } + }); + + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); } } + diff --git a/src/main/java/net/micode/notes/ui/NoteEditText.java b/src/main/java/net/micode/notes/ui/NoteEditText.java index 2afe2a8..6a82c71 100644 --- a/src/main/java/net/micode/notes/ui/NoteEditText.java +++ b/src/main/java/net/micode/notes/ui/NoteEditText.java @@ -16,7 +16,12 @@ package net.micode.notes.ui; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.ClipboardManager; import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Rect; import android.text.Layout; import android.text.Selection; @@ -31,11 +36,16 @@ import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; import android.view.MotionEvent; import android.widget.EditText; +import android.widget.Toast; import net.micode.notes.R; +import net.micode.notes.tool.ImageUtils; +import java.io.File; import java.util.HashMap; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class NoteEditText extends EditText { private static final String TAG = "NoteEditText"; @@ -52,6 +62,37 @@ public class NoteEditText extends EditText { sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); } + + /** + * 检查文本是否是图片路径 + * @param text 要检查的文本 + * @return 是否是图片路径 + */ + private boolean isImagePath(String text) { + if (text == null || text.isEmpty()) { + return false; + } + + // 检查文本是否是有效的文件路径 + File file = new File(text); + if (!file.exists()) { + return false; + } + + // 检查文件是否是图片类型 + String fileName = file.getName(); + String extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); + + // 常见的图片文件扩展名 + String[] imageExtensions = {"jpg", "jpeg", "png", "gif", "bmp", "webp"}; + for (String ext : imageExtensions) { + if (extension.equals(ext)) { + return true; + } + } + + return false; + } /** * Call by the {@link NoteEditActivity} to delete or add edit text @@ -75,7 +116,18 @@ public class NoteEditText extends EditText { void onTextChange(int index, boolean hasText); } + /** + * Call by the {@link NoteEditActivity} to update format buttons state + */ + public interface OnSelectionChangedListener { + /** + * Called when the selection in the edit text changes + */ + void onSelectionChanged(int start, int end); + } + private OnTextViewChangeListener mOnTextViewChangeListener; + private OnSelectionChangedListener mOnSelectionChangedListener; public NoteEditText(Context context) { super(context, null); @@ -90,6 +142,18 @@ public class NoteEditText extends EditText { mOnTextViewChangeListener = listener; } + public void setOnSelectionChangedListener(OnSelectionChangedListener listener) { + mOnSelectionChangedListener = listener; + } + + @Override + protected void onSelectionChanged(int selStart, int selEnd) { + super.onSelectionChanged(selStart, selEnd); + if (mOnSelectionChangedListener != null) { + mOnSelectionChangedListener.onSelectionChanged(selStart, selEnd); + } + } + public NoteEditText(Context context, AttributeSet attrs) { super(context, attrs, android.R.attr.editTextStyle); } @@ -214,4 +278,529 @@ public class NoteEditText extends EditText { } super.onCreateContextMenu(menu); } + + @Override + public boolean onTextContextMenuItem(int id) { + if (id == android.R.id.paste) { + // 处理粘贴操作,检查剪贴板是否有图片 + if (handlePasteImage()) { + return true; + } + } + return super.onTextContextMenuItem(id); + } + + /** + * 插入图片到编辑内容 + * @param imagePath 图片路径 + */ + public void insertImage(final String imagePath) { + Log.d(TAG, "开始插入图片,路径: " + imagePath); + + // 确保在主线程中操作 + post(new Runnable() { + @Override + public void run() { + doInsertImage(imagePath); + } + }); + } + + /** + * 实际执行插入图片的操作 + * @param imagePath 图片路径 + */ + private void doInsertImage(String imagePath) { + if (TextUtils.isEmpty(imagePath)) { + Log.e(TAG, "图片路径为空"); + Toast.makeText(getContext(), "图片路径为空", Toast.LENGTH_SHORT).show(); + return; + } + + // 检查文件是否存在 + java.io.File imageFile = new java.io.File(imagePath); + if (!imageFile.exists()) { + Log.e(TAG, "图片文件不存在: " + imagePath); + Toast.makeText(getContext(), "图片文件不存在: " + imagePath, Toast.LENGTH_SHORT).show(); + return; + } + + Log.d(TAG, "图片文件存在,大小: " + imageFile.length() + " bytes"); + + // 获取当前文本 + android.text.Editable editable = getText(); + int start = getSelectionStart(); + Log.d(TAG, "插入位置: " + start); + + // 添加图片标记,以便保存时能够正确保存图片路径 + String imageTag = "[IMAGE]" + imagePath + "[/IMAGE]"; + editable.insert(start, imageTag); + + // 移动光标到图片标记后面 + setSelection(start + imageTag.length()); + + // 处理图片标记,将其转换为 ImageSpan + processImageTags(); + + // 强制刷新界面 + invalidate(); + requestLayout(); + Log.d(TAG, "强制刷新界面和布局"); + + // 显示成功提示 + Toast.makeText(getContext(), "图片插入成功", Toast.LENGTH_SHORT).show(); + Log.d(TAG, "图片插入成功,路径: " + imagePath); + Log.d(TAG, "插入后文本长度: " + getText().length()); + } + + /** + * 处理文本中的图片标记,将其转换为 ImageSpan + */ + public void processImageTags() { + android.text.Editable editable = getText(); + String text = editable.toString(); + Pattern pattern = Pattern.compile("\\[IMAGE\\](.*?)\\[/IMAGE\\]"); + Matcher matcher = pattern.matcher(text); + + // 从后往前处理,避免索引变化 + while (matcher.find()) { + String imagePath = matcher.group(1); + int start = matcher.start(); + int end = matcher.end(); + + // 加载图片 + try { + android.graphics.BitmapFactory.Options options = new android.graphics.BitmapFactory.Options(); + options.inSampleSize = 4; + options.inPreferredConfig = android.graphics.Bitmap.Config.RGB_565; + android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeFile(imagePath, options); + + if (bitmap != null) { + // 保留原始标记,直接在标记上设置 ImageSpan + // 创建 ImageSpan + android.text.style.ImageSpan imageSpan = new android.text.style.ImageSpan(getContext(), bitmap); + // 设置 ImageSpan + editable.setSpan(imageSpan, start, end, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + Log.d(TAG, "成功处理图片标记: " + imagePath); + } + } catch (Exception e) { + Log.e(TAG, "处理图片标记时出错: " + e.getMessage(), e); + } + } + + // 强制刷新界面 + invalidate(); + requestLayout(); + } + + /** + * 处理粘贴图片的逻辑 + * @return 是否成功处理了图片粘贴 + */ + private boolean handlePasteImage() { + Log.d(TAG, "开始处理粘贴图片"); + + // 尝试使用专门的方法从剪贴板获取图片(针对虚拟机环境) + Log.d(TAG, "尝试使用专门的方法从剪贴板获取图片"); + Bitmap bitmap = ImageUtils.getImageFromClipboard(getContext()); + + if (bitmap != null) { + Log.d(TAG, "成功从剪贴板获取图片: " + bitmap.getWidth() + "x" + bitmap.getHeight()); + + // 保存图片到本地存储 + Log.d(TAG, "尝试保存图片"); + String imagePath = ImageUtils.saveImage(getContext(), bitmap); + + if (imagePath != null) { + Log.d(TAG, "成功保存图片到: " + imagePath); + + // 生成图片标记 + String imageTag = "[IMAGE]" + imagePath + "[/IMAGE]"; + Log.d(TAG, "生成图片标记: " + imageTag); + + // 将图片标记插入到文本中 + int start = getSelectionStart(); + Log.d(TAG, "插入位置: " + start); + + getText().insert(start, imageTag); + Log.d(TAG, "成功插入图片标记"); + Log.d(TAG, "插入后文本长度: " + getText().length()); + Log.d(TAG, "插入后文本内容: " + getText().toString()); + + // 移动光标到图片标记后面 + setSelection(start + imageTag.length()); + Log.d(TAG, "成功移动光标到位置: " + (start + imageTag.length())); + + // 强制刷新界面 + invalidate(); + Log.d(TAG, "强制刷新界面"); + + Toast.makeText(getContext(), "图片粘贴成功,路径: " + imagePath, Toast.LENGTH_LONG).show(); + Log.d(TAG, "图片粘贴成功,路径: " + imagePath); + return true; + } else { + Log.e(TAG, "保存图片失败"); + Toast.makeText(getContext(), "图片保存失败", Toast.LENGTH_SHORT).show(); + return true; + } + } else { + Log.d(TAG, "使用专门方法从剪贴板获取图片失败,尝试使用传统方法"); + } + + // 获取剪贴板管理器 + ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); + + // 检查剪贴板是否有内容 + if (clipboard == null) { + Log.e(TAG, "剪贴板管理器为空"); + Toast.makeText(getContext(), "剪贴板管理器不可用", Toast.LENGTH_SHORT).show(); + return false; + } + + if (!clipboard.hasPrimaryClip()) { + Log.e(TAG, "剪贴板为空"); + Toast.makeText(getContext(), "剪贴板为空", Toast.LENGTH_SHORT).show(); + return false; + } + + // 获取剪贴板内容 + ClipData clip = clipboard.getPrimaryClip(); + if (clip == null) { + Log.e(TAG, "剪贴板内容为空"); + Toast.makeText(getContext(), "剪贴板内容为空", Toast.LENGTH_SHORT).show(); + return false; + } + + Log.d(TAG, "剪贴板项目数量: " + clip.getItemCount()); + + // 检查剪贴板描述 + ClipDescription description = clipboard.getPrimaryClipDescription(); + if (description != null) { + Log.d(TAG, "剪贴板描述: " + description.toString()); + for (int i = 0; i < description.getMimeTypeCount(); i++) { + Log.d(TAG, "MIME类型 " + i + ": " + description.getMimeType(i)); + } + } else { + Log.e(TAG, "剪贴板描述为空"); + } + + // 检查是否有图片 + boolean foundContent = false; + for (int i = 0; i < clip.getItemCount(); i++) { + Log.d(TAG, "处理剪贴板项目 " + i); + ClipData.Item item = clip.getItemAt(i); + + // 检查是否有URI(可能是图片文件) + if (item.getUri() != null) { + Log.d(TAG, "找到URI: " + item.getUri().toString()); + foundContent = true; + + try { + // 尝试从URI加载图片 + Log.d(TAG, "尝试从URI加载图片: " + item.getUri().toString()); + Bitmap bitmapFromUri = BitmapFactory.decodeStream( + getContext().getContentResolver().openInputStream(item.getUri())); + + if (bitmapFromUri != null) { + Log.d(TAG, "成功加载图片: " + bitmapFromUri.getWidth() + "x" + bitmapFromUri.getHeight()); + + // 保存图片到本地存储 + Log.d(TAG, "尝试保存图片"); + String imagePath = ImageUtils.saveImage(getContext(), bitmapFromUri); + + if (imagePath != null) { + Log.d(TAG, "成功保存图片到: " + imagePath); + + // 生成图片标记 + String imageTag = "[IMAGE]" + imagePath + "[/IMAGE]"; + Log.d(TAG, "生成图片标记: " + imageTag); + + // 将图片标记插入到文本中 + int start = getSelectionStart(); + Log.d(TAG, "插入位置: " + start); + + getText().insert(start, imageTag); + Log.d(TAG, "成功插入图片标记"); + Log.d(TAG, "插入后文本长度: " + getText().length()); + Log.d(TAG, "插入后文本内容: " + getText().toString()); + + // 移动光标到图片标记后面 + setSelection(start + imageTag.length()); + Log.d(TAG, "成功移动光标到位置: " + (start + imageTag.length())); + + // 强制刷新界面 + invalidate(); + Log.d(TAG, "强制刷新界面"); + + Toast.makeText(getContext(), "图片粘贴成功,路径: " + imagePath, Toast.LENGTH_LONG).show(); + Log.d(TAG, "图片粘贴成功,路径: " + imagePath); + return true; + } else { + Log.e(TAG, "保存图片失败"); + Toast.makeText(getContext(), "图片保存失败", Toast.LENGTH_SHORT).show(); + return true; + } + } else { + Log.e(TAG, "从URI加载图片失败"); + Toast.makeText(getContext(), "无法加载图片", Toast.LENGTH_SHORT).show(); + } + } catch (Exception e) { + Log.e(TAG, "加载图片时出错: " + e.getMessage()); + e.printStackTrace(); + Toast.makeText(getContext(), "加载图片时出错: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } else { + Log.d(TAG, "未找到URI"); + } + + // 尝试获取文本(可能包含图片路径或其他信息) + if (item.getText() != null) { + String text = item.getText().toString(); + Log.d(TAG, "找到文本: " + text); + foundContent = true; + + // 检查文本是否是图片路径 + if (isImagePath(text)) { + Log.d(TAG, "文本是图片路径: " + text); + + // 尝试从路径加载图片 + try { + Bitmap bitmapFromPath = BitmapFactory.decodeFile(text); + if (bitmapFromPath != null) { + Log.d(TAG, "成功从路径加载图片: " + bitmapFromPath.getWidth() + "x" + bitmapFromPath.getHeight()); + + // 保存图片到本地存储 + String imagePath = ImageUtils.saveImage(getContext(), bitmapFromPath); + + if (imagePath != null) { + Log.d(TAG, "成功保存图片到: " + imagePath); + + // 生成图片标记 + String imageTag = "[IMAGE]" + imagePath + "[/IMAGE]"; + Log.d(TAG, "生成图片标记: " + imageTag); + + // 将图片标记插入到文本中 + int start = getSelectionStart(); + Log.d(TAG, "插入位置: " + start); + + getText().insert(start, imageTag); + Log.d(TAG, "成功插入图片标记"); + Log.d(TAG, "插入后文本长度: " + getText().length()); + Log.d(TAG, "插入后文本内容: " + getText().toString()); + + // 移动光标到图片标记后面 + setSelection(start + imageTag.length()); + Log.d(TAG, "成功移动光标到位置: " + (start + imageTag.length())); + + // 强制刷新界面 + invalidate(); + Log.d(TAG, "强制刷新界面"); + + Toast.makeText(getContext(), "图片粘贴成功,路径: " + imagePath, Toast.LENGTH_LONG).show(); + Log.d(TAG, "图片粘贴成功,路径: " + imagePath); + return true; + } else { + Log.e(TAG, "保存图片失败"); + Toast.makeText(getContext(), "图片保存失败", Toast.LENGTH_SHORT).show(); + return true; + } + } else { + Log.e(TAG, "从路径加载图片失败: " + text); + // 图片加载失败,直接粘贴文本内容 + int start = getSelectionStart(); + getText().insert(start, text); + setSelection(start + text.length()); + Toast.makeText(getContext(), "图片加载失败,已粘贴路径", Toast.LENGTH_SHORT).show(); + Log.d(TAG, "图片加载失败,已粘贴路径"); + return true; + } + } catch (Exception e) { + Log.e(TAG, "从路径加载图片时出错: " + e.getMessage()); + e.printStackTrace(); + // 加载失败,直接粘贴文本内容 + int start = getSelectionStart(); + getText().insert(start, text); + setSelection(start + text.length()); + Toast.makeText(getContext(), "图片加载失败,已粘贴路径", Toast.LENGTH_SHORT).show(); + Log.d(TAG, "图片加载失败,已粘贴路径"); + return true; + } + } else { + // 不是图片路径,直接粘贴文本内容 + int start = getSelectionStart(); + getText().insert(start, text); + setSelection(start + text.length()); + Toast.makeText(getContext(), "文本粘贴成功", Toast.LENGTH_SHORT).show(); + Log.d(TAG, "文本粘贴成功"); + return true; + } + } else { + Log.d(TAG, "未找到文本"); + } + + // 尝试获取Intent(可能包含图片信息) + if (item.getIntent() != null) { + Log.d(TAG, "找到Intent: " + item.getIntent().toString()); + foundContent = true; + + // 尝试从Intent中提取图片 + try { + android.content.Intent intent = item.getIntent(); + if (intent.hasExtra(android.content.Intent.EXTRA_STREAM)) { + android.net.Uri uri = (android.net.Uri) intent.getParcelableExtra(android.content.Intent.EXTRA_STREAM); + if (uri != null) { + Log.d(TAG, "从Intent中找到图片URI: " + uri.toString()); + + // 尝试从URI加载图片 + Bitmap bitmapFromIntent = BitmapFactory.decodeStream( + getContext().getContentResolver().openInputStream(uri)); + + if (bitmapFromIntent != null) { + Log.d(TAG, "成功从Intent加载图片: " + bitmapFromIntent.getWidth() + "x" + bitmapFromIntent.getHeight()); + + // 保存图片到本地存储 + String imagePath = ImageUtils.saveImage(getContext(), bitmapFromIntent); + + if (imagePath != null) { + Log.d(TAG, "成功保存图片到: " + imagePath); + + // 生成图片标记 + String imageTag = "[IMAGE]" + imagePath + "[/IMAGE]"; + Log.d(TAG, "生成图片标记: " + imageTag); + + // 将图片标记插入到文本中 + int start = getSelectionStart(); + Log.d(TAG, "插入位置: " + start); + + getText().insert(start, imageTag); + Log.d(TAG, "成功插入图片标记"); + Log.d(TAG, "插入后文本长度: " + getText().length()); + Log.d(TAG, "插入后文本内容: " + getText().toString()); + + // 移动光标到图片标记后面 + setSelection(start + imageTag.length()); + Log.d(TAG, "成功移动光标到位置: " + (start + imageTag.length())); + + // 强制刷新界面 + invalidate(); + Log.d(TAG, "强制刷新界面"); + + Toast.makeText(getContext(), "图片粘贴成功,路径: " + imagePath, Toast.LENGTH_LONG).show(); + Log.d(TAG, "图片粘贴成功,路径: " + imagePath); + return true; + } else { + Log.e(TAG, "保存图片失败"); + Toast.makeText(getContext(), "图片保存失败", Toast.LENGTH_SHORT).show(); + return true; + } + } else { + Log.e(TAG, "从Intent URI加载图片失败"); + } + } + } + } catch (Exception e) { + Log.e(TAG, "从Intent加载图片时出错: " + e.getMessage()); + e.printStackTrace(); + } + } else { + Log.d(TAG, "未找到Intent"); + } + + // 尝试获取HTML文本(可能包含图片信息) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + if (item.getHtmlText() != null) { + String htmlText = item.getHtmlText().toString(); + Log.d(TAG, "找到HTML文本: " + htmlText); + foundContent = true; + + // 尝试从HTML中提取图片信息 + if (htmlText.contains(" 0) ? true : false; mEncrypted = (cursor.getInt(ENCRYPTED_COLUMN) > 0) ? true : false; + // 获取标题信息 + mTitle = DataUtils.getNoteTitle(context.getContentResolver(), mId); + mPhoneNumber = ""; if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); @@ -234,6 +238,10 @@ public class NoteItemData { return mEncrypted; } + public String getTitle() { + return mTitle; + } + public static int getNoteType(Cursor cursor) { return cursor.getInt(TYPE_COLUMN); } diff --git a/src/main/java/net/micode/notes/ui/NotesListActivity.java b/src/main/java/net/micode/notes/ui/NotesListActivity.java index fb4cdc2..787616f 100644 --- a/src/main/java/net/micode/notes/ui/NotesListActivity.java +++ b/src/main/java/net/micode/notes/ui/NotesListActivity.java @@ -47,20 +47,37 @@ import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; import android.view.MotionEvent; import android.view.View; +import android.view.View.DragShadowBuilder; +import android.view.View.OnDragListener; import android.view.View.OnClickListener; import android.view.View.OnCreateContextMenuListener; +import android.view.View.OnLongClickListener; import android.view.View.OnTouchListener; import android.view.inputmethod.InputMethodManager; +import android.view.DragEvent; +import android.content.ClipData; 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.CompoundButton; import android.widget.EditText; import android.widget.ImageView; -import android.widget.ListView; +import android.widget.GridView; import android.widget.PopupMenu; import android.widget.TextView; +import android.widget.LinearLayout; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.GradientDrawable; +import android.content.res.ColorStateList; +import android.os.Build; +import android.view.Gravity; +import android.view.WindowManager; +import android.net.Uri; import android.widget.Toast; +import java.lang.reflect.Field; import net.micode.notes.R; import net.micode.notes.data.Notes; @@ -80,7 +97,12 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.ArrayList; import java.util.HashSet; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; @@ -96,26 +118,48 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; private enum ListEditState { - NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER, RECENTLY_DELETED + NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER, RECENTLY_DELETED, TODO_LIST }; + private enum SortOrder { + MODIFIED_DATE_DESC, // 按修改时间降序(默认) + MODIFIED_DATE_ASC, // 按修改时间升序 + CREATED_DATE_DESC, // 按创建时间降序 + CREATED_DATE_ASC // 按创建时间升序 + }; + + /** + * 待办事项数据类 + */ + private static class TodoItem { + public String content; // 待办事项内容 + public boolean completed; // 是否已完成 + + public TodoItem(String content, boolean completed) { + this.content = content; + this.completed = completed; + } + } + + // 待办事项列表 + private ArrayList mTodoItems = new ArrayList<>(); + + // 待办事项 UI 视图映射,用于快速查找和更新 + private HashMap mTodoItemViews = new HashMap<>(); + private HashMap mCompletedItemViews = new HashMap<>(); + + // SharedPreferences 键名 + private static final String PREFERENCE_TODO_ITEMS = "todo_items"; + private static final String PREFERENCE_COMPLETED_LIST_EXPANDED = "completed_list_expanded"; + private ListEditState mState; + private SortOrder mSortOrder = SortOrder.MODIFIED_DATE_DESC; private BackgroundQueryHandler mBackgroundQueryHandler; private NotesListAdapter mNotesListAdapter; - private ListView mNotesListView; - - private Button mAddNewNote; - - private boolean mDispatch; - - private int mOriginY; - - private int mDispatchY; - - private TextView mTitleBar; + private GridView mNotesGridView; private long mCurrentFolderId; @@ -125,21 +169,16 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt private static final String TAG = "NotesListActivity"; - public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; - private NoteItemData mFocusNoteDataItem; private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; - private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" - + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" - + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " - + NoteColumns.NOTES_COUNT + ">0)"; + private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?" + ") OR (" + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + NoteColumns.NOTES_COUNT + ">0)"; // 搜索相关变量 - private EditText mSearchEditText; - private ImageView mClearSearchButton; private String mSearchQuery = ""; + private long mSearchStartTime = 0; + private long mSearchEndTime = 0; private final static int REQUEST_CODE_OPEN_NODE = 102; private final static int REQUEST_CODE_NEW_NODE = 103; @@ -160,7 +199,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { - mNotesListAdapter.changeCursor(null); + // 刷新列表,而不是清除数据 + startAsyncNotesListQuery(); } else { super.onActivityResult(requestCode, resultCode, data); } @@ -193,7 +233,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt try { in.close(); } catch (IOException e) { - // TODO Auto-generated catch block e.printStackTrace(); } } @@ -222,333 +261,411 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mContentResolver = this.getContentResolver(); mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); mCurrentFolderId = Notes.ID_ROOT_FOLDER; - mNotesListView = (ListView) findViewById(R.id.notes_list); - mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), - null, false); - mNotesListView.setOnItemClickListener(new OnListItemClickListener()); - mNotesListView.setOnItemLongClickListener(this); - mNotesListAdapter = new NotesListAdapter(this); - mNotesListView.setAdapter(mNotesListAdapter); - mAddNewNote = (Button) findViewById(R.id.btn_new_note); - mAddNewNote.setOnClickListener(this); - mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); - mDispatch = false; - mDispatchY = 0; - mOriginY = 0; - mTitleBar = (TextView) findViewById(R.id.tv_title_bar); - mState = ListEditState.NOTE_LIST; - mModeCallBack = new ModeCallback(); - - // 初始化搜索组件 - mSearchEditText = (EditText) findViewById(R.id.et_search); - mClearSearchButton = (ImageView) findViewById(R.id.btn_clear_search); - - // 添加搜索文本变化监听器 - mSearchEditText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - mSearchQuery = s.toString().trim(); - // 当搜索文本变化时,重新查询 - startAsyncNotesListQuery(); - } - - @Override - public void afterTextChanged(Editable s) { - // 显示或隐藏清除按钮 - mClearSearchButton.setVisibility(s.length() > 0 ? View.VISIBLE : View.GONE); - } - }); - - // 添加清除搜索按钮点击监听器 - mClearSearchButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - mSearchEditText.setText(""); - mSearchQuery = ""; - // 清除搜索后重新查询 - startAsyncNotesListQuery(); - // 隐藏软键盘 - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), 0); - } - }); - // 初始隐藏清除按钮 - mClearSearchButton.setVisibility(View.GONE); - } + // 初始化搜索栏 + View searchBar = findViewById(R.id.search_bar); + if (searchBar != null) { + // 查找搜索输入框 + final EditText searchEditText = (EditText) searchBar.findViewById(R.id.et_search); + if (searchEditText != null) { + // 设置搜索栏点击事件,点击时将焦点传递给EditText + searchBar.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + searchEditText.requestFocus(); + // 显示软键盘 + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT); + } + }); + + // 设置输入监听器,实现实时搜索 + searchEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } - private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { - private DropdownMenu mDropDownMenu; - private ActionMode mActionMode; - private MenuItem mMoveMenu; - private HashSet mSelectedNoteIds; // 直接保存选中的笔记ID + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - mSelectedNoteIds = new HashSet(); // 初始化选中的笔记ID集合 - - if (mState == ListEditState.RECENTLY_DELETED) { - // 最近删除模式下显示恢复和永久删除选项 - // 使用动态生成的ID,避免资源ID未定义错误 - menu.add(0, 100, 0, R.string.menu_restore).setOnMenuItemClickListener(this); - menu.add(0, 101, 0, R.string.menu_delete_permanently).setOnMenuItemClickListener(this); + @Override + public void afterTextChanged(Editable s) { + // 获取输入的搜索关键词 + String searchQuery = s.toString().trim(); + // 更新搜索关键词 + mSearchQuery = searchQuery; + // 执行搜索 + startAsyncNotesListQuery(); + Log.d(TAG, "Search query updated: " + searchQuery); + } + }); } else { - // 正常模式下显示普通菜单 - getMenuInflater().inflate(R.menu.note_list_options, menu); - menu.findItem(R.id.delete).setOnMenuItemClickListener(this); - mMoveMenu = menu.findItem(R.id.move); - if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER - || DataUtils.getUserFolderCount(mContentResolver) == 0) { - mMoveMenu.setVisible(false); - } else { - mMoveMenu.setVisible(true); - mMoveMenu.setOnMenuItemClickListener(this); - } + Log.e(TAG, "Search EditText not found"); } + } + + // 初始化笔记网格 + mNotesGridView = (GridView) findViewById(R.id.notes_grid); + if (mNotesGridView != null) { + // 设置网格视图的适配器和监听器 + mNotesListAdapter = new NotesListAdapter(this); + mNotesGridView.setAdapter(mNotesListAdapter); - mActionMode = mode; - mNotesListAdapter.setChoiceMode(true); - mNotesListView.setLongClickable(false); - mAddNewNote.setVisibility(View.GONE); - - View customView = LayoutInflater.from(NotesListActivity.this).inflate( - R.layout.note_list_dropdown_menu, null); - mode.setCustomView(customView); - mDropDownMenu = new DropdownMenu(NotesListActivity.this, - (Button) customView.findViewById(R.id.selection_menu), - R.menu.note_list_dropdown); - mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ - public boolean onMenuItemClick(MenuItem item) { - // 全选/取消全选 - boolean selectAll = !mSelectedNoteIds.isEmpty(); - mSelectedNoteIds.clear(); - - // 遍历所有笔记,添加/移除选中状态 - for (int i = 0; i < mNotesListAdapter.getCount(); i++) { - Cursor cursor = (Cursor) mNotesListAdapter.getItem(i); + // 设置点击监听器 + mNotesGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + try { + Log.d(TAG, "onItemClick called, position: " + position + ", id: " + id); + + // 从适配器获取 Cursor + Cursor cursor = mNotesListAdapter.getCursor(); + Log.d(TAG, "Cursor obtained: " + (cursor != null)); + if (cursor != null) { - long noteId = cursor.getLong(cursor.getColumnIndex(NoteColumns.ID)); - if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { - if (!selectAll) { - mSelectedNoteIds.add(noteId); + // 确保 Cursor 移动到正确的位置 + NoteItemData data = null; + if (cursor.moveToPosition(position)) { + Log.d(TAG, "Cursor moved to position: " + position); + data = new NoteItemData(NotesListActivity.this, cursor); + Log.d(TAG, "NoteItemData created, type: " + data.getType() + ", id: " + data.getId() + ", encrypted: " + data.isEncrypted()); + } + + if (data != null) { + final NoteItemData finalData = data; + // 检查便签类型 + if (data.getType() == Notes.TYPE_FOLDER) { + Log.d(TAG, "Opening folder: " + data.getId()); + openFolder(data); + } else if (data.getType() == Notes.TYPE_NOTE) { + Log.d(TAG, "Opening note: " + data.getId() + ", encrypted: " + data.isEncrypted()); + + // 检查便签的加密状态 + boolean isEncrypted = data.isEncrypted(); + Log.d(TAG, "Note encrypted status from NoteItemData: " + isEncrypted); + + // 加载便签并再次检查加密状态,确保准确性 + WorkingNote note = WorkingNote.load(NotesListActivity.this, data.getId()); + if (note != null) { + isEncrypted = note.isEncrypted(); + Log.d(TAG, "Note encrypted status from WorkingNote: " + isEncrypted); + } + + if (isEncrypted) { + Log.d(TAG, "Note is encrypted, showing password dialog"); + // 弹出密码输入对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle("输入密码"); + builder.setIcon(android.R.drawable.ic_dialog_alert); + + final EditText input = new EditText(NotesListActivity.this); + input.setHint("请输入密码"); + input.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + builder.setView(input); + + builder.setPositiveButton("确定", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + String password = input.getText().toString(); + Log.d(TAG, "Password entered: " + (password != null ? "[hidden]" : "null")); + if (!TextUtils.isEmpty(password)) { + // 验证密码 + Log.d(TAG, "Verifying password for note: " + finalData.getId()); + WorkingNote verifyNote = WorkingNote.load(NotesListActivity.this, finalData.getId()); + Log.d(TAG, "WorkingNote loaded: " + (verifyNote != null)); + if (verifyNote != null && verifyNote.verifyPassword(password)) { + // 密码正确,启动 NoteEditActivity 并传递密码 + Log.d(TAG, "Password verified, starting NoteEditActivity"); + Intent intent = new Intent(NotesListActivity.this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, finalData.getId()); + intent.putExtra("password", password); + Log.d(TAG, "Intent created with password, EXTRA_UID: " + finalData.getId()); + startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + } else { + // 密码错误,显示错误提示 + Log.d(TAG, "Password verification failed"); + Toast.makeText(NotesListActivity.this, "密码错误", Toast.LENGTH_SHORT).show(); + } + } else { + Log.d(TAG, "Password is empty"); + Toast.makeText(NotesListActivity.this, "密码不能为空", Toast.LENGTH_SHORT).show(); + } + } + }); + builder.setNegativeButton("取消", null); + // 设置对话框不可取消,确保用户必须输入密码或点击取消 + builder.setCancelable(false); + builder.show(); + Log.d(TAG, "Password dialog shown successfully"); + } else { + // 非加密便签,直接启动 NoteEditActivity + Log.d(TAG, "Note is not encrypted, starting NoteEditActivity directly"); + Intent intent = new Intent(NotesListActivity.this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, data.getId()); + Log.d(TAG, "Intent created, EXTRA_UID: " + data.getId()); + startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + Log.d(TAG, "startActivityForResult called with request code: " + REQUEST_CODE_OPEN_NODE); + } + } else { + Log.d(TAG, "Skipping non-note item with type: " + data.getType()); + Toast.makeText(NotesListActivity.this, "无法打开此类型的项目", Toast.LENGTH_SHORT).show(); } - mNotesListAdapter.setCheckedItem(i, !selectAll); + } else { + Log.e(TAG, "Failed to create NoteItemData"); + Toast.makeText(NotesListActivity.this, "无法创建便签数据", Toast.LENGTH_SHORT).show(); + } + } else { + Log.e(TAG, "Cursor is null for position: " + position); + Toast.makeText(NotesListActivity.this, "无法获取便签数据", Toast.LENGTH_SHORT).show(); + } + } catch (Exception e) { + Log.e(TAG, "Error in onItemClick: " + e.getMessage()); + Toast.makeText(NotesListActivity.this, "点击出错: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + }); + + // 设置长按监听器 + mNotesGridView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + try { + Log.d(TAG, "onItemLongClick called, position: " + position + ", id: " + id); + // 正确处理 Cursor 对象 + Cursor cursor = (Cursor) mNotesListAdapter.getItem(position); + Log.d(TAG, "Cursor obtained: " + (cursor != null)); + if (cursor != null && cursor.moveToPosition(position)) { + Log.d(TAG, "Cursor moved to position: " + position); + NoteItemData data = new NoteItemData(NotesListActivity.this, cursor); + Log.d(TAG, "NoteItemData created, type: " + data.getType() + ", id: " + data.getId()); + + // 进入批量选择模式 + if (mModeCallBack == null) { + mModeCallBack = new ModeCallback(); } + startActionMode(mModeCallBack); + + // 选中当前长按的项 + mNotesListAdapter.setCheckedItem(position, true); + + } else { + Log.e(TAG, "Cursor is null or cannot move to position: " + position); } + } catch (Exception e) { + Log.e(TAG, "Error in onItemLongClick: " + e.getMessage()); + Toast.makeText(NotesListActivity.this, "长按出错: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } - updateMenu(); + Log.d(TAG, "onItemLongClick returning true"); return true; } - }); - return true; } - - private void updateMenu() { - int selectedCount = mSelectedNoteIds.size(); - // Update dropdown menu - String format = getResources().getString(R.string.menu_select_title, selectedCount); - mDropDownMenu.setTitle(format); - MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); - if (item != null) { - // 判断是否全选 - boolean isAllSelected = !mSelectedNoteIds.isEmpty() && mSelectedNoteIds.size() == mNotesListAdapter.getCount(); - if (isAllSelected) { - item.setChecked(true); - item.setTitle(R.string.menu_deselect_all); - } else { - item.setChecked(false); - item.setTitle(R.string.menu_select_all); + + // 初始化悬浮按钮 + View fabNewNote = findViewById(R.id.fab_new_note); + if (fabNewNote != null) { + fabNewNote.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + createNewNote(); } - } + }); } - - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - // TODO Auto-generated method stub - return false; + + // 初始化按时间查找按钮 + View btnTimeSearch = findViewById(R.id.btn_time_search); + if (btnTimeSearch != null) { + btnTimeSearch.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showTimeSearchDialog(); + } + }); } - - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - // TODO Auto-generated method stub - return false; + + // 初始化排序按钮 + View btnSort = findViewById(R.id.btn_sort); + if (btnSort != null) { + btnSort.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showSortOrderDialog(); + } + }); } - - public void onDestroyActionMode(ActionMode mode) { - mSelectedNoteIds.clear(); // 清空选中的笔记ID集合 - mNotesListAdapter.setChoiceMode(false); - mNotesListView.setLongClickable(true); - mAddNewNote.setVisibility(View.VISIBLE); + + // 初始化底部导航栏 + View navNotes = findViewById(R.id.nav_notes); + if (navNotes != null) { + navNotes.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 切换到笔记标签 + mState = ListEditState.NOTE_LIST; + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + // 更新应用名称 + TextView appName = (TextView) findViewById(R.id.tv_app_name); + if (appName != null) { + appName.setText("笔记"); + } + startAsyncNotesListQuery(); + // 更新导航栏状态 + updateNavigationBarState(true, false); + } + }); } - - public void finishActionMode() { - mActionMode.finish(); + + View navTodo = findViewById(R.id.nav_todo); + if (navTodo != null) { + navTodo.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 切换到待办标签,使用新的待办事项布局 + setContentView(R.layout.todo_list); + + // 初始化待办事项界面 + initTodoListUI(); + + // 更新导航栏状态 + updateNavigationBarState(false, false); + } + }); } - - public void onItemCheckedStateChanged(ActionMode mode, int position, long id, - boolean checked) { - // 从光标中获取实际的笔记ID,而不是使用可能错误的id参数 - long actualNoteId = 0; - Cursor cursor = (Cursor) mNotesListAdapter.getItem(position); - if (cursor != null) { - actualNoteId = cursor.getLong(cursor.getColumnIndex(NoteColumns.ID)); + + // 初始化回收站标签 + View navRecycle = findViewById(R.id.nav_recycle); + if (navRecycle != null) { + navRecycle.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 切换到回收站标签 + showRecentlyDeletedNotes(); + // 更新应用名称 + TextView appName = (TextView) findViewById(R.id.tv_app_name); + if (appName != null) { + appName.setText("回收站"); + } + // 更新导航栏状态 + updateNavigationBarState(false, true); + } + }); + } + } + + private void createNewNote() { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); + } + + /** + * 更新导航栏状态 + * @param isNotesSelected 是否选中了笔记标签 + * @param isRecycleSelected 是否选中了回收站标签 + */ + private void updateNavigationBarState(boolean isNotesSelected, boolean isRecycleSelected) { + // 更新笔记标签 + View navNotes = findViewById(R.id.nav_notes); + if (navNotes != null) { + ImageView notesIcon = (ImageView) navNotes.findViewById(R.id.icon); + TextView notesText = (TextView) navNotes.findViewById(R.id.text); + if (notesIcon != null) { + notesIcon.setColorFilter(isNotesSelected ? 0xFF000000 : 0xFFCCCCCC); } - - // 更新选中的笔记ID集合 - if (checked) { - mSelectedNoteIds.add(actualNoteId); - } else { - mSelectedNoteIds.remove(actualNoteId); + if (notesText != null) { + notesText.setTextColor(isNotesSelected ? 0xFF000000 : 0xFFCCCCCC); } - mNotesListAdapter.setCheckedItem(position, checked); - updateMenu(); } - - public boolean onMenuItemClick(MenuItem item) { - // 检查是否有选中的笔记 - boolean hasSelection = !mSelectedNoteIds.isEmpty(); - if (!hasSelection) { - Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), - Toast.LENGTH_SHORT).show(); - return true; + + // 更新待办标签 + View navTodo = findViewById(R.id.nav_todo); + if (navTodo != null) { + ImageView todoIcon = (ImageView) navTodo.findViewById(R.id.icon); + TextView todoText = (TextView) navTodo.findViewById(R.id.text); + boolean isTodoSelected = !isNotesSelected && !isRecycleSelected; + if (todoIcon != null) { + todoIcon.setColorFilter(isTodoSelected ? 0xFF000000 : 0xFFCCCCCC); } - - int itemId = item.getItemId(); - - if (mState == ListEditState.RECENTLY_DELETED) { - // 最近删除模式下的操作 - if (itemId == 100) { - // 恢复选中的笔记 - batchRestore(mSelectedNoteIds); - } else if (itemId == 101) { - // 永久删除选中的笔记 - AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.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_permanently, - mSelectedNoteIds.size())); - builder.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, - int which) { - batchDeletePermanently(mSelectedNoteIds); - } - }); - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - } - } else { - // 正常模式下的操作 - if (itemId == R.id.delete) { - AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.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_notes, - mSelectedNoteIds.size())); - builder.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, - int which) { - batchDelete(); - } - }); - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - } else if (itemId == R.id.move) { - startQueryDestinationFolders(); - } else { - return false; - } + if (todoText != null) { + todoText.setTextColor(isTodoSelected ? 0xFF000000 : 0xFFCCCCCC); } - return true; } - } - - private class NewNoteOnTouchListener implements OnTouchListener { - - public boolean onTouch(View v, MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: { - Display display = getWindowManager().getDefaultDisplay(); - int screenHeight = display.getHeight(); - int newNoteViewHeight = mAddNewNote.getHeight(); - int start = screenHeight - newNoteViewHeight; - int eventY = start + (int) event.getY(); - /** - * Minus TitleBar's height - */ - if (mState == ListEditState.SUB_FOLDER) { - eventY -= mTitleBar.getHeight(); - start -= mTitleBar.getHeight(); - } - /** - * HACKME:When click the transparent part of "New Note" button, dispatch - * the event to the list view behind this button. The transparent part of - * "New Note" button could be expressed by formula y=-0.12x+94(Unit:pixel) - * and the line top of the button. The coordinate based on left of the "New - * Note" button. The 94 represents maximum height of the transparent part. - * Notice that, if the background of the button changes, the formula should - * also change. This is very bad, just for the UI designer's strong requirement. - */ - if (event.getY() < (event.getX() * (-0.12) + 94)) { - View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 - - mNotesListView.getFooterViewsCount()); - if (view != null && view.getBottom() > start - && (view.getTop() < (start + 94))) { - mOriginY = (int) event.getY(); - mDispatchY = eventY; - event.setLocation(event.getX(), mDispatchY); - mDispatch = true; - return mNotesListView.dispatchTouchEvent(event); - } - } - break; - } - case MotionEvent.ACTION_MOVE: { - if (mDispatch) { - mDispatchY += (int) event.getY() - mOriginY; - event.setLocation(event.getX(), mDispatchY); - return mNotesListView.dispatchTouchEvent(event); - } - break; - } - default: { - if (mDispatch) { - event.setLocation(event.getX(), mDispatchY); - mDispatch = false; - return mNotesListView.dispatchTouchEvent(event); - } - break; - } + + // 更新回收站标签 + View navRecycle = findViewById(R.id.nav_recycle); + if (navRecycle != null) { + ImageView recycleIcon = (ImageView) navRecycle.findViewById(R.id.icon); + TextView recycleText = (TextView) navRecycle.findViewById(R.id.text); + if (recycleIcon != null) { + recycleIcon.setColorFilter(isRecycleSelected ? 0xFF000000 : 0xFFCCCCCC); + } + if (recycleText != null) { + recycleText.setTextColor(isRecycleSelected ? 0xFF000000 : 0xFFCCCCCC); } - return false; } - - }; + } private void startAsyncNotesListQuery() { String selection; String[] selectionArgs; String sortOrder; + // 首先检查是否有搜索关键词,适用于所有状态 + if (!TextUtils.isEmpty(mSearchQuery)) { + // 有搜索关键词,使用搜索 URI + Uri searchUri; + if (mState == ListEditState.RECENTLY_DELETED) { + // 回收站搜索,使用特殊的搜索 URI + searchUri = Uri.parse(Notes.CONTENT_NOTE_URI + "/deleted_search?pattern=" + Uri.encode(mSearchQuery)); + // 回收站使用删除时间排序 + sortOrder = NoteColumns.DELETED_DATE + " DESC"; + } else { + // 普通搜索,包括笔记和待办 + searchUri = Uri.parse(Notes.CONTENT_NOTE_URI + "/list_search?pattern=" + Uri.encode(mSearchQuery) + "&folder_id=" + mCurrentFolderId); + // 使用用户选择的排序方式,保持置顶优先级 + sortOrder = NoteColumns.PINNED + " DESC," + NoteColumns.TYPE + " DESC," + getSortOrderString(); + } + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, searchUri, NoteItemData.PROJECTION, null, null, sortOrder); + Log.d(TAG, "Searching for: " + mSearchQuery + " in state: " + mState + " with sort order: " + sortOrder); + return; + } + if (mState == ListEditState.RECENTLY_DELETED) { // 查询已删除的笔记,只查询笔记类型 selection = NoteColumns.DELETED + "=1 AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; selectionArgs = null; + // 回收站使用删除时间排序 sortOrder = NoteColumns.DELETED_DATE + " DESC"; + } else if (mState == ListEditState.TODO_LIST) { + // 检查是否有时间搜索 + if (mSearchStartTime > 0 && mSearchEndTime > 0) { + // 有时间搜索,使用时间搜索 URI + Uri timeSearchUri = Uri.parse(Notes.CONTENT_NOTE_URI + "/time_search?start_time=" + mSearchStartTime + "&end_time=" + mSearchEndTime + "&folder_id=" + mCurrentFolderId); + // 使用用户选择的排序方式,保持置顶优先级 + sortOrder = NoteColumns.PINNED + " DESC," + getSortOrderString(); + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, timeSearchUri, NoteItemData.PROJECTION, null, null, sortOrder); + Log.d(TAG, "Time searching from: " + mSearchStartTime + " to: " + mSearchEndTime + " in folder: " + mCurrentFolderId + " with sort order: " + sortOrder); + return; + } + // 待办事项空间,只查询待办事项文件夹中的内容 + selection = NoteColumns.PARENT_ID + "=" + Notes.ID_TODO_FOLDER + " AND " + NoteColumns.DELETED + "=0"; + selectionArgs = null; + // 使用用户选择的排序方式,保持置顶优先级 + sortOrder = NoteColumns.PINNED + " DESC," + getSortOrderString(); } else { - // 检查是否有搜索关键词 - if (!TextUtils.isEmpty(mSearchQuery)) { - // 有搜索关键词,使用搜索条件 - selection = NoteColumns.SNIPPET + " LIKE ? AND " + NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; - selectionArgs = new String[] { - "%" + mSearchQuery + "%", - String.valueOf(mCurrentFolderId) - }; - sortOrder = NoteColumns.PINNED + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"; - Log.d(TAG, "Searching for: " + mSearchQuery); + // 检查是否有时间搜索 + if (mSearchStartTime > 0 && mSearchEndTime > 0) { + // 有时间搜索,使用时间搜索 URI + Uri timeSearchUri = Uri.parse(Notes.CONTENT_NOTE_URI + "/time_search?start_time=" + mSearchStartTime + "&end_time=" + mSearchEndTime + "&folder_id=" + mCurrentFolderId); + // 使用用户选择的排序方式,保持置顶优先级 + sortOrder = NoteColumns.PINNED + " DESC," + NoteColumns.TYPE + " DESC," + getSortOrderString(); + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, timeSearchUri, NoteItemData.PROJECTION, null, null, sortOrder); + Log.d(TAG, "Time searching from: " + mSearchStartTime + " to: " + mSearchEndTime + " in folder: " + mCurrentFolderId + " with sort order: " + sortOrder); + return; } else { // 无搜索关键词,正常查询 selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION @@ -556,20 +673,19 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt selectionArgs = new String[] { String.valueOf(mCurrentFolderId) }; - sortOrder = NoteColumns.PINNED + " DESC," + NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"; + // 使用用户选择的排序方式,保持置顶和类型排序的优先级 + sortOrder = NoteColumns.PINNED + " DESC," + NoteColumns.TYPE + " DESC," + getSortOrderString(); } } mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, selectionArgs, sortOrder); + Log.d(TAG, "Querying notes with selection: " + selection + " and sort order: " + sortOrder); } private void showRecentlyDeletedNotes() { // 进入最近删除模式 mState = ListEditState.RECENTLY_DELETED; - mTitleBar.setText(R.string.title_recently_deleted); - mTitleBar.setVisibility(View.VISIBLE); - mAddNewNote.setVisibility(View.GONE); startAsyncNotesListQuery(); } @@ -596,7 +712,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } } - + private void showFolderListMenu(Cursor cursor) { AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); builder.setTitle(R.string.menu_title_select_folder); @@ -612,27 +728,25 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mNotesListAdapter.getSelectedCount(), adapter.getFolderName(NotesListActivity.this, which)), Toast.LENGTH_SHORT).show(); - mModeCallBack.finishActionMode(); + if (mModeCallBack != null) { + mModeCallBack.finishActionMode(); + } } }); builder.show(); } - private void createNewNote() { - Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_INSERT_OR_EDIT); - intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); - this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); - } - private void batchDelete() { + // 在主线程中获取选中的项的 ID,然后将这些 ID 传递给后台线程 + final HashSet selectedIds = mNotesListAdapter.getSelectedItemIds(); + Log.d(TAG, "Selected ids in batchDelete: " + selectedIds); + new AsyncTask>() { protected HashSet doInBackground(Void... unused) { HashSet widgets = mNotesListAdapter.getSelectedWidget(); // 无论是否处于同步模式,都使用软删除 - if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter - .getSelectedItemIds())) { + if (DataUtils.batchDeleteNotes(mContentResolver, selectedIds)) { } else { Log.e(TAG, "Delete notes error, should not happens"); } @@ -650,6 +764,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } } + // 显示删除成功的提示 + Toast.makeText(NotesListActivity.this, getString(R.string.toast_delete_success), Toast.LENGTH_SHORT).show(); // 刷新列表以显示删除后的结果 startAsyncNotesListQuery(); } @@ -694,8 +810,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt // 切换回正常的笔记列表模式 mState = ListEditState.NOTE_LIST; mCurrentFolderId = Notes.ID_ROOT_FOLDER; - mTitleBar.setVisibility(View.GONE); - mAddNewNote.setVisibility(View.VISIBLE); } // 刷新列表以显示恢复后的笔记 startAsyncNotesListQuery(); @@ -728,6 +842,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + // 显示永久删除成功的提示 + Toast.makeText(NotesListActivity.this, getString(R.string.toast_delete_permanently_success), Toast.LENGTH_SHORT).show(); + // 刷新列表以显示删除后的结果 startAsyncNotesListQuery(); } catch (Exception e) { @@ -736,6 +853,46 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt Toast.LENGTH_SHORT).show(); } } + + /** + * 删除单个便签 + * @param noteId 便签ID + */ + private void deleteNote(long noteId) { + // 创建一个包含单个ID的集合 + final HashSet ids = new HashSet(); + ids.add(noteId); + + // 显示确认对话框 + 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_notes, 1)); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // 执行删除操作 + new AsyncTask>() { + protected HashSet doInBackground(Void... unused) { + // 无论是否处于同步模式,都使用软删除 + if (DataUtils.batchDeleteNotes(mContentResolver, ids)) { + } else { + Log.e(TAG, "Delete notes error, should not happens"); + } + return null; + } + + @Override + protected void onPostExecute(HashSet widgets) { + // 刷新列表以显示删除后的结果 + startAsyncNotesListQuery(); + } + }.execute(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + } private void deleteFolder(long folderId) { if (folderId == Notes.ID_ROOT_FOLDER) { @@ -762,96 +919,98 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } private void openNode(NoteItemData data) { - // 检查便签是否加密 - if (data.isEncrypted()) { - // 创建密码输入对话框 - LayoutInflater inflater = LayoutInflater.from(this); - View dialogView = inflater.inflate(R.layout.dialog_edit_text, null); - final EditText passwordEditText = (EditText) dialogView.findViewById(R.id.et_foler_name); - passwordEditText.setHint(R.string.dialog_enter_password); - passwordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); - - // 创建对话框 - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.dialog_enter_password); - builder.setView(dialogView); - builder.setPositiveButton(R.string.dialog_enter_password, null); - builder.setNegativeButton(android.R.string.cancel, null); - - final AlertDialog dialog = builder.create(); - dialog.show(); - - // 重写确定按钮点击事件 - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - String password = passwordEditText.getText().toString(); - - if (TextUtils.isEmpty(password)) { - Toast.makeText(NotesListActivity.this, R.string.dialog_password_empty, Toast.LENGTH_SHORT).show(); - return; - } - - // 加载便签并验证密码 - final long noteId = data.getId(); - new AsyncTask() { - @Override - protected Boolean doInBackground(Void... params) { - // 加载便签 - net.micode.notes.model.WorkingNote workingNote = net.micode.notes.model.WorkingNote.load(NotesListActivity.this, noteId); - // 验证密码 - return workingNote.verifyPassword(password); + try { + Log.d(TAG, "openNode called, note id: " + data.getId() + ", encrypted: " + data.isEncrypted()); + // 检查便签是否加密 + if (data.isEncrypted()) { + // 创建密码输入对话框 + LayoutInflater inflater = LayoutInflater.from(this); + View dialogView = inflater.inflate(R.layout.dialog_edit_text, null); + final EditText passwordEditText = (EditText) dialogView.findViewById(R.id.et_foler_name); + passwordEditText.setHint(R.string.dialog_enter_password); + passwordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + + // 创建对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.dialog_enter_password); + builder.setView(dialogView); + builder.setPositiveButton(R.string.dialog_enter_password, null); + builder.setNegativeButton(android.R.string.cancel, null); + + final AlertDialog dialog = builder.create(); + dialog.show(); + + // 重写确定按钮点击事件 + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String password = passwordEditText.getText().toString(); + + if (TextUtils.isEmpty(password)) { + Toast.makeText(NotesListActivity.this, R.string.dialog_password_empty, Toast.LENGTH_SHORT).show(); + return; } - @Override - protected void onPostExecute(Boolean result) { - if (result) { - // 密码正确,打开便签 - Intent intent = new Intent(NotesListActivity.this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(Intent.EXTRA_UID, noteId); - // 传递密码给编辑界面,用于后续解密操作 - intent.putExtra("password", password); - NotesListActivity.this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); - } else { - // 密码错误,提示用户 - Toast.makeText(NotesListActivity.this, R.string.toast_password_incorrect, Toast.LENGTH_SHORT).show(); + // 加载便签并验证密码 + final long noteId = data.getId(); + new AsyncTask() { + @Override + protected Boolean doInBackground(Void... params) { + // 加载便签 + net.micode.notes.model.WorkingNote workingNote = net.micode.notes.model.WorkingNote.load(NotesListActivity.this, noteId); + // 验证密码 + return workingNote.verifyPassword(password); } - dialog.dismiss(); - } - }.execute(); - } - }); - } else { - // 未加密便签,直接打开 - Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(Intent.EXTRA_UID, data.getId()); - this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + @Override + protected void onPostExecute(Boolean result) { + if (result) { + // 密码正确,打开便签 + Intent intent = new Intent(NotesListActivity.this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, noteId); + // 传递密码给编辑界面,用于后续解密操作 + intent.putExtra("password", password); + NotesListActivity.this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + } else { + // 密码错误,提示用户 + Toast.makeText(NotesListActivity.this, R.string.toast_password_incorrect, Toast.LENGTH_SHORT).show(); + } + + dialog.dismiss(); + } + }.execute(); + } + }); + } else { + // 未加密便签,直接打开 + Log.d(TAG, "Opening unencrypted note, starting activity for result"); + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, data.getId()); + Log.d(TAG, "Intent created, EXTRA_UID: " + data.getId()); + this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + Log.d(TAG, "startActivityForResult called with request code: " + REQUEST_CODE_OPEN_NODE); + } + } catch (Exception e) { + Log.e(TAG, "Error in openNode: " + e.getMessage()); + Toast.makeText(this, "打开便签出错: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } - private void openFolder(NoteItemData data) { + public void openFolder(NoteItemData data) { mCurrentFolderId = data.getId(); startAsyncNotesListQuery(); if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { mState = ListEditState.CALL_RECORD_FOLDER; - mAddNewNote.setVisibility(View.GONE); } else { mState = ListEditState.SUB_FOLDER; } - if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { - mTitleBar.setText(R.string.call_record_folder_name); - } else { - mTitleBar.setText(data.getSnippet()); - } - mTitleBar.setVisibility(View.VISIBLE); } public void onClick(View v) { int viewId = v.getId(); - if (viewId == R.id.btn_new_note) { + if (viewId == R.id.fab_new_note) { createNewNote(); } } @@ -867,13 +1026,589 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); } - - private void showCreateOrModifyFolderDialog(final boolean create) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); - final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); - showSoftInput(); - if (!create) { + + /** + * 显示时间搜索对话框 + */ + private void showTimeSearchDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("按时间搜索"); + + // Inflate the dialog layout + View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_time_search, null); + builder.setView(dialogView); + + // Get the date pickers from the layout + final android.widget.DatePicker startDatePicker = (android.widget.DatePicker) dialogView.findViewById(R.id.start_date_picker); + final android.widget.DatePicker endDatePicker = (android.widget.DatePicker) dialogView.findViewById(R.id.end_date_picker); + + // Set current date as default + java.util.Calendar calendar = java.util.Calendar.getInstance(); + int year = calendar.get(java.util.Calendar.YEAR); + int month = calendar.get(java.util.Calendar.MONTH); + int day = calendar.get(java.util.Calendar.DAY_OF_MONTH); + + startDatePicker.init(year, month, day, null); + endDatePicker.init(year, month, day, null); + + // Set positive button + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Get selected start date + java.util.Calendar startCalendar = java.util.Calendar.getInstance(); + startCalendar.set(startDatePicker.getYear(), startDatePicker.getMonth(), startDatePicker.getDayOfMonth(), 0, 0, 0); + mSearchStartTime = startCalendar.getTimeInMillis(); + + // Get selected end date + java.util.Calendar endCalendar = java.util.Calendar.getInstance(); + endCalendar.set(endDatePicker.getYear(), endDatePicker.getMonth(), endDatePicker.getDayOfMonth(), 23, 59, 59); + mSearchEndTime = endCalendar.getTimeInMillis(); + + // Perform time-based search + startAsyncNotesListQuery(); + } + }); + + // Set negative button + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + + // Set neutral button to clear time search + builder.setNeutralButton("清除时间搜索", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mSearchStartTime = 0; + mSearchEndTime = 0; + startAsyncNotesListQuery(); + } + }); + + builder.show(); + } + + /** + * 将待办事项添加到界面 + * @param todoText 待办事项内容 + */ + /** + * 添加待办事项到界面 + * @param todoText 待办事项内容 + */ + private void addTodoItemToUI(String todoText) { + if (TextUtils.isEmpty(todoText)) { + return; + } + + // 检查是否已存在相同内容的待办事项 + for (TodoItem item : mTodoItems) { + if (item.content.equals(todoText)) { + Toast.makeText(this, "待办事项已存在", Toast.LENGTH_SHORT).show(); + return; + } + } + + // 创建待办事项对象并添加到列表 + final TodoItem todoItemObj = new TodoItem(todoText, false); + mTodoItems.add(todoItemObj); + + // 找到待办事项容器 + final LinearLayout todoListContainer = (LinearLayout) findViewById(R.id.todo_list_container); + if (todoListContainer == null) { + return; + } + + // 找到已完成列表头部,我们要在它之前添加新的待办事项 + LinearLayout completedHeader = (LinearLayout) findViewById(R.id.completed_header); + int insertIndex = todoListContainer.indexOfChild(completedHeader); + if (insertIndex == -1) { + insertIndex = todoListContainer.getChildCount(); + } + + // 创建新的待办事项卡片 + final LinearLayout todoItem = new LinearLayout(this); + LinearLayout.LayoutParams itemParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + itemParams.setMargins(0, 0, 0, 12); + todoItem.setLayoutParams(itemParams); + todoItem.setOrientation(LinearLayout.HORIZONTAL); + todoItem.setGravity(Gravity.CENTER_VERTICAL); + todoItem.setPadding(16, 16, 16, 16); + + // 添加圆角效果 + GradientDrawable drawable = new GradientDrawable(); + drawable.setColor(Color.WHITE); + drawable.setCornerRadius(8); + todoItem.setBackground(drawable); + + // 添加复选框 + CheckBox checkBox = new CheckBox(this); + LinearLayout.LayoutParams checkboxParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + checkboxParams.setMarginEnd(16); + checkBox.setLayoutParams(checkboxParams); + // 设置复选框为默认样式 + checkBox.setChecked(false); + + // 添加复选框点击事件 + checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked && todoItemObj != null) { + // 更新待办事项对象的状态 + todoItemObj.completed = true; + + // 待办事项标记为已完成,移动到已完成列表 + todoListContainer.removeView(todoItem); + mTodoItemViews.remove(todoText); + + // 获取已完成列表 + LinearLayout completedList = (LinearLayout) findViewById(R.id.completed_list); + if (completedList != null) { + // 创建已完成待办事项 + LinearLayout completedItem = new LinearLayout(NotesListActivity.this); + LinearLayout.LayoutParams completedParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + completedParams.setMargins(0, 0, 0, 12); + completedItem.setLayoutParams(completedParams); + completedItem.setOrientation(LinearLayout.HORIZONTAL); + completedItem.setGravity(Gravity.CENTER_VERTICAL); + completedItem.setPadding(16, 16, 16, 16); + + // 添加圆角效果 + GradientDrawable completedDrawable = new GradientDrawable(); + completedDrawable.setColor(Color.WHITE); + completedDrawable.setCornerRadius(8); + completedItem.setBackground(completedDrawable); + + // 添加已完成复选框 + CheckBox completedCheckBox = new CheckBox(NotesListActivity.this); + LinearLayout.LayoutParams completedCheckboxParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + completedCheckboxParams.setMarginEnd(16); + completedCheckBox.setLayoutParams(completedCheckboxParams); + // 设置复选框为默认样式 + completedCheckBox.setChecked(true); + + // 添加已完成文本 + TextView completedTextView = new TextView(NotesListActivity.this); + LinearLayout.LayoutParams completedTextParams = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1); + completedTextView.setLayoutParams(completedTextParams); + completedTextView.setText(todoText); + completedTextView.setTextColor(Color.GRAY); + completedTextView.setTextSize(16); + + // 添加删除按钮 + ImageView deleteButton = new ImageView(NotesListActivity.this); + LinearLayout.LayoutParams deleteButtonParams = new LinearLayout.LayoutParams( + 32, 32); + deleteButton.setLayoutParams(deleteButtonParams); + deleteButton.setImageResource(android.R.drawable.ic_menu_delete); + deleteButton.setColorFilter(Color.GRAY); + deleteButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + removeCompletedTodoItem(todoText); + } + }); + + // 添加到已完成容器 + completedItem.addView(completedCheckBox); + completedItem.addView(completedTextView); + completedItem.addView(deleteButton); + + // 添加到已完成列表 + completedList.addView(completedItem); + mCompletedItemViews.put(todoText, completedItem); + + // 显示已完成列表和头部 + completedList.setVisibility(View.VISIBLE); + if (completedHeader != null) { + completedHeader.setVisibility(View.VISIBLE); + } + + // 更新已完成计数 + updateCompletedCount(); + + // 保存待办事项 + saveTodoItems(); + + // 显示 Toast 提示用户操作成功 + Toast.makeText(NotesListActivity.this, "待办事项已标记为完成", Toast.LENGTH_SHORT).show(); + } + } + } + }); + + // 添加删除按钮 + ImageView deleteButton = new ImageView(this); + LinearLayout.LayoutParams deleteButtonParams = new LinearLayout.LayoutParams( + 32, 32); + deleteButton.setLayoutParams(deleteButtonParams); + deleteButton.setImageResource(android.R.drawable.ic_menu_delete); + deleteButton.setColorFilter(Color.GRAY); + deleteButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + removeTodoItem(todoText); + } + }); + + // 添加文本 + TextView textView = new TextView(this); + LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1); + textView.setLayoutParams(textParams); + textView.setText(todoText); + textView.setTextColor(Color.BLACK); + textView.setTextSize(16); + + // 添加到容器 + todoItem.addView(checkBox); + todoItem.addView(textView); + todoItem.addView(deleteButton); + + // 添加到界面 + todoListContainer.addView(todoItem, insertIndex); + mTodoItemViews.put(todoText, todoItem); + + // 保存待办事项 + saveTodoItems(); + + // 显示 Toast 提示用户操作成功 + Toast.makeText(this, "待办事项已添加", Toast.LENGTH_SHORT).show(); + } + + /** + * 删除待办事项 + * @param todoText 待办事项内容 + */ + private void removeTodoItem(String todoText) { + // 从待办事项列表中移除 + TodoItem itemToRemove = null; + for (TodoItem item : mTodoItems) { + if (item.content.equals(todoText)) { + itemToRemove = item; + break; + } + } + + if (itemToRemove != null) { + mTodoItems.remove(itemToRemove); + + // 从界面中移除 + LinearLayout todoItemView = mTodoItemViews.get(todoText); + if (todoItemView != null) { + LinearLayout todoListContainer = (LinearLayout) findViewById(R.id.todo_list_container); + if (todoListContainer != null) { + todoListContainer.removeView(todoItemView); + } + mTodoItemViews.remove(todoText); + } + + // 保存待办事项 + saveTodoItems(); + + // 显示 Toast 提示用户操作成功 + Toast.makeText(this, "待办事项已删除", Toast.LENGTH_SHORT).show(); + } + } + + /** + * 删除已完成待办事项 + * @param todoText 待办事项内容 + */ + private void removeCompletedTodoItem(String todoText) { + // 从待办事项列表中移除 + TodoItem itemToRemove = null; + for (TodoItem item : mTodoItems) { + if (item.content.equals(todoText)) { + itemToRemove = item; + break; + } + } + + if (itemToRemove != null) { + mTodoItems.remove(itemToRemove); + + // 从界面中移除 + LinearLayout completedItemView = mCompletedItemViews.get(todoText); + if (completedItemView != null) { + LinearLayout completedList = (LinearLayout) findViewById(R.id.completed_list); + if (completedList != null) { + completedList.removeView(completedItemView); + + // 更新已完成计数 + updateCompletedCount(); + + // 检查是否需要隐藏已完成列表 + if (completedList.getChildCount() == 0) { + completedList.setVisibility(View.GONE); + LinearLayout completedHeader = (LinearLayout) findViewById(R.id.completed_header); + if (completedHeader != null) { + completedHeader.setVisibility(View.GONE); + } + } + } + mCompletedItemViews.remove(todoText); + } + + // 保存待办事项 + saveTodoItems(); + + // 显示 Toast 提示用户操作成功 + Toast.makeText(this, "已完成待办事项已删除", Toast.LENGTH_SHORT).show(); + } + } + + /** + * 更新已完成计数 + */ + private void updateCompletedCount() { + LinearLayout completedList = (LinearLayout) findViewById(R.id.completed_list); + if (completedList != null) { + int completedCount = completedList.getChildCount(); + TextView completedTitle = (TextView) findViewById(R.id.completed_title); + if (completedTitle != null) { + completedTitle.setText("已完成 " + completedCount); + } + } + } + + /** + * 显示创建待办事项的弹窗 + */ + private void showCreateTodoDialog() { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + + // 创建自定义布局 + LinearLayout container = new LinearLayout(this); + container.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + container.setOrientation(LinearLayout.HORIZONTAL); + container.setPadding(24, 24, 24, 24); + container.setBackgroundColor(Color.WHITE); + + // 添加圆角效果 + GradientDrawable drawable = new GradientDrawable(); + drawable.setColor(Color.WHITE); + drawable.setCornerRadius(12); + container.setBackground(drawable); + + // 添加选择框 + CheckBox checkBox = new CheckBox(this); + LinearLayout.LayoutParams checkboxParams = new LinearLayout.LayoutParams( + 24, 24); + checkboxParams.setMarginEnd(16); + checkBox.setLayoutParams(checkboxParams); + // 设置按钮颜色 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + checkBox.setButtonTintList(ColorStateList.valueOf(Color.GRAY)); + } + container.addView(checkBox); + + // 添加输入框 + final EditText editText = new EditText(this); + LinearLayout.LayoutParams editTextParams = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1); + editText.setLayoutParams(editTextParams); + editText.setTextSize(16); + editText.setTextColor(Color.BLACK); + editText.setHintTextColor(Color.GRAY); + editText.setHint("回车即可连续添加待办"); + editText.setCursorVisible(true); + editText.setFocusable(true); + editText.setFocusableInTouchMode(true); + // 设置光标颜色为黄色 + try { + Field f = TextView.class.getDeclaredField("mCursorDrawableRes"); + f.setAccessible(true); + f.set(editText, R.drawable.cursor_yellow); + } catch (Exception e) { + // 忽略异常 + } + container.addView(editText); + + builder.setView(container); + + // 创建对话框 + final AlertDialog dialog = builder.create(); + + // 设置对话框样式 + if (dialog.getWindow() != null) { + // 设置背景为浅灰色半透明遮罩 + dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.argb(100, 200, 200, 200))); + dialog.getWindow().setLayout( + (int) (getResources().getDisplayMetrics().widthPixels * 0.85), + WindowManager.LayoutParams.WRAP_CONTENT); + } + + // 添加完成按钮 + dialog.setButton(DialogInterface.BUTTON_POSITIVE, "完成", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + String todoText = editText.getText().toString().trim(); + if (!TextUtils.isEmpty(todoText)) { + // 添加待办事项到界面 + addTodoItemToUI(todoText); + // 显示添加成功的提示 + Toast.makeText(NotesListActivity.this, "待办事项已添加: " + todoText, Toast.LENGTH_SHORT).show(); + } + } + }); + + // 显示对话框 + dialog.show(); + + // 设置完成按钮为非激活状态 + Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + if (positiveButton != null) { + positiveButton.setTextColor(Color.GRAY); + positiveButton.setEnabled(false); + } + + // 为输入框添加文本变化监听器,控制完成按钮的状态 + editText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + if (positiveButton != null) { + boolean hasText = !TextUtils.isEmpty(charSequence); + positiveButton.setEnabled(hasText); + positiveButton.setTextColor(Color.GRAY); + } + } + + @Override + public void afterTextChanged(Editable editable) {} + }); + + // 自动弹出软键盘并设置光标 + editText.requestFocus(); + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); + } + } + + /** + * 初始化待办事项界面 + */ + private void initTodoListUI() { + // 从 SharedPreferences 加载待办事项 + loadTodoItems(); + + // 检查mTodoItems列表是否为空,如果为空,添加一些示例待办事项 + if (mTodoItems.isEmpty()) { + // 添加示例待办事项 + mTodoItems.add(new TodoItem("点击右边的 + 按钮添加新待办事项", false)); + mTodoItems.add(new TodoItem("点击左边的复选框标记为已完成", false)); + mTodoItems.add(new TodoItem("已完成的事项会显示在这里", true)); + + // 保存示例待办事项 + saveTodoItems(); + } + + // 从mTodoItems列表中恢复之前的待办事项 + restoreTodoItemsFromList(); + + // 初始化已完成列表折叠/展开 + final LinearLayout completedHeader = (LinearLayout) findViewById(R.id.completed_header); + final LinearLayout completedList = (LinearLayout) findViewById(R.id.completed_list); + final ImageView completedArrow = (ImageView) findViewById(R.id.completed_arrow); + + if (completedHeader != null && completedList != null && completedArrow != null) { + completedHeader.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (completedList.getVisibility() == View.VISIBLE) { + // 收起已完成列表 + completedList.setVisibility(View.GONE); + completedArrow.setImageResource(android.R.drawable.ic_menu_more); + } else { + // 展开已完成列表 + completedList.setVisibility(View.VISIBLE); + completedArrow.setImageResource(android.R.drawable.ic_menu_more); + } + } + }); + } + + // 初始化悬浮按钮 + View fabNewNote = findViewById(R.id.fab_new_note); + if (fabNewNote != null) { + fabNewNote.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 添加新待办事项 + showCreateTodoDialog(); + } + }); + } + + // 初始化笔记标签点击事件 + View navNotes = findViewById(R.id.nav_notes); + if (navNotes != null) { + navNotes.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 切换回笔记标签 + setContentView(R.layout.note_list); + initResources(); + // 更新应用名称 + TextView appName = (TextView) findViewById(R.id.tv_app_name); + if (appName != null) { + appName.setText("笔记"); + } + startAsyncNotesListQuery(); + // 更新导航栏状态 + updateNavigationBarState(true, false); + } + }); + } + + // 初始化回收站标签点击事件 + View navRecycle = findViewById(R.id.nav_recycle); + if (navRecycle != null) { + navRecycle.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 切换到回收站标签 + setContentView(R.layout.note_list); + initResources(); + showRecentlyDeletedNotes(); + // 更新应用名称 + TextView appName = (TextView) findViewById(R.id.tv_app_name); + if (appName != null) { + appName.setText("回收站"); + } + // 更新导航栏状态 + updateNavigationBarState(false, true); + } + }); + } + } + + private void showCreateOrModifyFolderDialog(final boolean create) { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); + showSoftInput(); + if (!create) { if (mFocusNoteDataItem != null) { etName.setText(mFocusNoteDataItem.getSnippet()); builder.setTitle(getString(R.string.menu_folder_change_name)); @@ -934,7 +1669,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt */ etName.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // TODO Auto-generated method stub } @@ -947,7 +1681,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } public void afterTextChanged(Editable s) { - // TODO Auto-generated method stub } }); @@ -960,21 +1693,22 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mCurrentFolderId = Notes.ID_ROOT_FOLDER; mState = ListEditState.NOTE_LIST; startAsyncNotesListQuery(); - mTitleBar.setVisibility(View.GONE); break; case CALL_RECORD_FOLDER: mCurrentFolderId = Notes.ID_ROOT_FOLDER; mState = ListEditState.NOTE_LIST; - mAddNewNote.setVisibility(View.VISIBLE); - mTitleBar.setVisibility(View.GONE); startAsyncNotesListQuery(); break; case RECENTLY_DELETED: // 从最近删除模式返回到普通模式 mState = ListEditState.NOTE_LIST; mCurrentFolderId = Notes.ID_ROOT_FOLDER; - mAddNewNote.setVisibility(View.VISIBLE); - mTitleBar.setVisibility(View.GONE); + startAsyncNotesListQuery(); + break; + case TODO_LIST: + // 从待办事项模式返回到普通模式 + mState = ListEditState.NOTE_LIST; + mCurrentFolderId = Notes.ID_ROOT_FOLDER; startAsyncNotesListQuery(); break; case NOTE_LIST: @@ -1004,343 +1738,1091 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt setResult(RESULT_OK, intent); } - private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - if (mFocusNoteDataItem != null) { - menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); - menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); - menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); - menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); - } + /** + * 根据当前排序方式返回对应的排序字符串 + */ + private String getSortOrderString() { + switch (mSortOrder) { + case CREATED_DATE_DESC: + return NoteColumns.CREATED_DATE + " DESC"; + case CREATED_DATE_ASC: + return NoteColumns.CREATED_DATE + " ASC"; + case MODIFIED_DATE_ASC: + return NoteColumns.MODIFIED_DATE + " ASC"; + case MODIFIED_DATE_DESC: + default: + return NoteColumns.MODIFIED_DATE + " DESC"; } - }; + } + + /** + * 切换排序方式 + */ + private void toggleSortOrder() { + switch (mSortOrder) { + case MODIFIED_DATE_DESC: + mSortOrder = SortOrder.MODIFIED_DATE_ASC; + break; + case MODIFIED_DATE_ASC: + mSortOrder = SortOrder.CREATED_DATE_DESC; + break; + case CREATED_DATE_DESC: + mSortOrder = SortOrder.CREATED_DATE_ASC; + break; + case CREATED_DATE_ASC: + default: + mSortOrder = SortOrder.MODIFIED_DATE_DESC; + break; + } + startAsyncNotesListQuery(); + } + + /** + * 显示排序方式选择对话框 + */ + private void showSortOrderDialog() { + // 创建排序方式选项数组 + final String[] sortOptions = { + "修改时间降序", + "修改时间升序", + "创建时间降序", + "创建时间升序" + }; + + // 创建排序方式对应的枚举值数组 + final SortOrder[] sortOrders = { + SortOrder.MODIFIED_DATE_DESC, + SortOrder.MODIFIED_DATE_ASC, + SortOrder.CREATED_DATE_DESC, + SortOrder.CREATED_DATE_ASC + }; + + // 显示排序方式选择对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("选择排序方式"); + builder.setItems(sortOptions, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // 设置选择的排序方式 + mSortOrder = sortOrders[which]; + // 重新查询数据 + startAsyncNotesListQuery(); + } + }); + builder.show(); + } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - if (mState == ListEditState.RECENTLY_DELETED && v instanceof NotesListItem) { - // 最近删除模式下显示恢复和永久删除选项,使用动态生成的ID - menu.setHeaderTitle(R.string.title_recently_deleted); - menu.add(0, 200, 0, R.string.menu_restore); - menu.add(0, 201, 0, R.string.menu_delete_permanently); - } else { - // 其他模式下显示默认菜单 + try { if (mFocusNoteDataItem != null) { - menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); - menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); - menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); - menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); + if (mState == ListEditState.RECENTLY_DELETED) { + // 最近删除模式下显示恢复和永久删除选项 + menu.setHeaderTitle(mFocusNoteDataItem.getTitle() != null && !mFocusNoteDataItem.getTitle().isEmpty() ? mFocusNoteDataItem.getTitle() : mFocusNoteDataItem.getSnippet()); + menu.add(0, 200, 0, R.string.menu_restore); + menu.add(0, 201, 0, R.string.menu_delete_permanently); + } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + // 文件夹显示文件夹相关选项 + menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); + menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); + menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); + menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); + } else { + // 普通便签显示删除选项 + menu.setHeaderTitle(mFocusNoteDataItem.getTitle() != null && !mFocusNoteDataItem.getTitle().isEmpty() ? mFocusNoteDataItem.getTitle() : mFocusNoteDataItem.getSnippet()); + menu.add(0, 100, 0, R.string.menu_delete); + } } + } catch (Exception e) { + Log.e(TAG, "Error in onCreateContextMenu: " + e.getMessage()); } super.onCreateContextMenu(menu, v, menuInfo); } - @Override - public void onContextMenuClosed(Menu menu) { - if (mNotesListView != null) { - mNotesListView.setOnCreateContextMenuListener(null); - } - super.onContextMenuClosed(menu); - } - @Override public boolean onContextItemSelected(MenuItem item) { - if (mFocusNoteDataItem == null) { - Log.e(TAG, "The long click data item is null"); - return false; - } - int itemId = item.getItemId(); - if (mState == ListEditState.RECENTLY_DELETED) { - // 最近删除模式下的操作 - if (itemId == 200) { - // 恢复选中的笔记 - HashSet selectedIds = new HashSet(); - selectedIds.add(mFocusNoteDataItem.getId()); - batchRestore(selectedIds); - } else if (itemId == 201) { - // 永久删除选中的笔记 - 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_permanently, 1)); - builder.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - HashSet selectedIds = new HashSet(); - selectedIds.add(mFocusNoteDataItem.getId()); - batchDeletePermanently(selectedIds); - } - }); - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); + try { + if (mFocusNoteDataItem == null) { + Log.e(TAG, "The long click data item is null"); + return false; } - } else { - // 其他模式下的操作 - if (itemId == MENU_FOLDER_VIEW) { - openFolder(mFocusNoteDataItem); - } else if (itemId == MENU_FOLDER_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_folder)); - builder.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - deleteFolder(mFocusNoteDataItem.getId()); - } - }); - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - } else if (itemId == MENU_FOLDER_CHANGE_NAME) { - showCreateOrModifyFolderDialog(false); + int itemId = item.getItemId(); + if (mState == ListEditState.RECENTLY_DELETED) { + if (itemId == 200) { + HashSet ids = new HashSet(); + ids.add(mFocusNoteDataItem.getId()); + batchRestore(ids); + } else if (itemId == 201) { + HashSet ids = new HashSet(); + ids.add(mFocusNoteDataItem.getId()); + batchDeletePermanently(ids); + } + } else { + if (itemId == 100) { + // 处理普通便签的删除操作 + deleteNote(mFocusNoteDataItem.getId()); + } else { + switch (itemId) { + case MENU_FOLDER_VIEW: + openFolder(mFocusNoteDataItem); + break; + case MENU_FOLDER_DELETE: + deleteFolder(mFocusNoteDataItem.getId()); + break; + case MENU_FOLDER_CHANGE_NAME: + showCreateOrModifyFolderDialog(false); + break; + default: + break; + } + } } + } catch (Exception e) { + Log.e(TAG, "Error in onContextItemSelected: " + e.getMessage()); + Toast.makeText(this, "菜单操作出错: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } return true; } @Override - public boolean onCreateOptionsMenu(Menu menu) { - // 初始化菜单 - if (mState == ListEditState.NOTE_LIST) { - getMenuInflater().inflate(R.menu.note_list, menu); - // set sync or sync_cancel - menu.findItem(R.id.menu_sync).setTitle( - GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); - } else if (mState == ListEditState.SUB_FOLDER) { - getMenuInflater().inflate(R.menu.sub_folder, menu); - } else if (mState == ListEditState.CALL_RECORD_FOLDER) { - getMenuInflater().inflate(R.menu.call_record_folder, menu); - } else if (mState == ListEditState.RECENTLY_DELETED) { - // 最近删除模式下不显示任何菜单 - } else { - Log.e(TAG, "Wrong state:" + mState); - } - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - menu.clear(); - if (mState == ListEditState.NOTE_LIST) { - getMenuInflater().inflate(R.menu.note_list, menu); - // set sync or sync_cancel - menu.findItem(R.id.menu_sync).setTitle( - GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); - } else if (mState == ListEditState.SUB_FOLDER) { - getMenuInflater().inflate(R.menu.sub_folder, menu); - } else if (mState == ListEditState.CALL_RECORD_FOLDER) { - getMenuInflater().inflate(R.menu.call_record_folder, menu); - } else if (mState == ListEditState.RECENTLY_DELETED) { - // 最近删除模式下不显示任何菜单 - } else { - Log.e(TAG, "Wrong state:" + mState); + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + try { + Log.d(TAG, "onItemLongClick called, position: " + position + ", id: " + id); + + // 先设置选择模式,再启动ActionMode,最后选中当前长按的项 + // 这样可以确保选中的项不会被清除 + mNotesListAdapter.setChoiceMode(true); + + // 进入批量选择模式 + if (mModeCallBack == null) { + mModeCallBack = new ModeCallback(); + } + startActionMode(mModeCallBack); + + // 选中当前长按的项 + mNotesListAdapter.setCheckedItem(position, true); + Log.d(TAG, "Selected item at position: " + position); + Log.d(TAG, "Number of selected items: " + mNotesListAdapter.getSelectedCount()); + + } catch (Exception e) { + Log.e(TAG, "Error in onItemLongClick: " + e.getMessage()); + Toast.makeText(this, "长按出错: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } + Log.d(TAG, "onItemLongClick returning true"); return true; } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int itemId = item.getItemId(); - if (itemId == R.id.menu_new_folder) { - showCreateOrModifyFolderDialog(true); - } else if (itemId == R.id.menu_recently_deleted) { - // 显示最近删除的笔记 - showRecentlyDeletedNotes(); - } else if (itemId == R.id.menu_export_text) { - exportNoteToText(); - } else if (itemId == R.id.menu_sync) { - if (isSyncMode()) { - if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { - GTaskSyncService.startSync(this); - } else { - GTaskSyncService.cancelSync(this); + private class ModeCallback implements ActionMode.Callback, OnMenuItemClickListener { + private ActionMode mActionMode; + private HashSet mSelectedNoteIds = new HashSet(); + + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mActionMode = mode; + // 不再调用setChoiceMode(true),因为这个调用会清除mSelectedIndex集合 + // 选择模式已经在onItemLongClick方法中设置 + if (mNotesListAdapter != null) { + // 确保选择模式已经开启,但不清除已选中的项 + if (!mNotesListAdapter.isInChoiceMode()) { + mNotesListAdapter.setChoiceMode(true); } - } else { - startPreferenceActivity(); } - } else if (itemId == R.id.menu_setting) { - startPreferenceActivity(); - } else if (itemId == R.id.menu_new_note) { - createNewNote(); - } else if (itemId == R.id.menu_search) { - onSearchRequested(); + + // 隐藏新建便签按钮 + View fabNewNote = findViewById(R.id.fab_new_note); + if (fabNewNote != null) { + fabNewNote.setVisibility(View.GONE); + } + + // 显示删除按钮容器 + View deleteButtonContainer = findViewById(R.id.delete_button_container); + if (deleteButtonContainer != null) { + deleteButtonContainer.setVisibility(View.VISIBLE); + } + + // 设置删除按钮点击监听器 + final Button btnDelete = (Button) findViewById(R.id.btn_delete); + if (btnDelete != null) { + // 移除之前可能存在的监听器,避免重复设置 + btnDelete.setOnClickListener(null); + btnDelete.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Log.d(TAG, "Delete button clicked"); + HashSet selectedIds = mNotesListAdapter.getSelectedItemIds(); + Log.d(TAG, "Selected ids: " + selectedIds); + if (selectedIds.isEmpty()) { + Toast.makeText(NotesListActivity.this, R.string.menu_select_none, Toast.LENGTH_SHORT).show(); + return; + } + // 显示删除确认对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(R.string.alert_title_delete); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_notes, selectedIds.size())); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + Log.d(TAG, "Confirm delete clicked"); + batchDelete(); + if (mActionMode != null) { + mActionMode.finish(); + } + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + } + }); + } + + // 为GridView设置点击监听器,用于切换选择状态 + mNotesGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (mNotesListAdapter.isInChoiceMode()) { + // 在选择模式下,点击切换选择状态 + mNotesListAdapter.toggleItemChecked(position); + } + } + }); + + // 禁用导航栏切换 + disableNavigationBar(); + + return true; } - return true; - } - @Override - public boolean onSearchRequested() { - startSearch(null, false, null /* appData */, false); - return true; - } + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + menu.clear(); + if (mState == ListEditState.RECENTLY_DELETED) { + // 回收站模式:恢复和永久删除 + menu.add(0, 100, 0, R.string.menu_restore).setOnMenuItemClickListener(this); + menu.add(0, 101, 0, R.string.menu_delete_permanently).setOnMenuItemClickListener(this); + } else if (mState == ListEditState.TODO_LIST || mState == ListEditState.NOTE_LIST || mState == ListEditState.SUB_FOLDER) { + // 笔记和待办模式:删除 + menu.add(0, 102, 0, R.string.menu_delete).setOnMenuItemClickListener(this); + } + return true; + } - private void exportNoteToText() { - final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); - new AsyncTask() { + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return onMenuItemClick(item); + } - @Override - protected Integer doInBackground(Void... unused) { - return backup.exportToText(); + public void onDestroyActionMode(ActionMode mode) { + mSelectedNoteIds.clear(); + if (mNotesListAdapter != null) { + mNotesListAdapter.setChoiceMode(false); + } + + // 显示新建便签按钮 + View fabNewNote = findViewById(R.id.fab_new_note); + if (fabNewNote != null) { + fabNewNote.setVisibility(View.VISIBLE); + } + + // 隐藏删除按钮容器 + View deleteButtonContainer = findViewById(R.id.delete_button_container); + if (deleteButtonContainer != null) { + deleteButtonContainer.setVisibility(View.GONE); } + + // 恢复GridView的原始点击监听器 + mNotesGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + try { + Log.d(TAG, "onItemClick called, position: " + position + ", id: " + id); + + // 从适配器获取 Cursor + Cursor cursor = mNotesListAdapter.getCursor(); + Log.d(TAG, "Cursor obtained: " + (cursor != null)); + + if (cursor != null) { + // 确保 Cursor 移动到正确的位置 + NoteItemData data = null; + if (cursor.moveToPosition(position)) { + Log.d(TAG, "Cursor moved to position: " + position); + data = new NoteItemData(NotesListActivity.this, cursor); + Log.d(TAG, "NoteItemData created, type: " + data.getType() + ", id: " + data.getId() + ", encrypted: " + data.isEncrypted()); + } + + if (data != null) { + final NoteItemData finalData = data; + // 检查便签类型 + if (data.getType() == Notes.TYPE_FOLDER) { + Log.d(TAG, "Opening folder: " + data.getId()); + openFolder(data); + } else if (data.getType() == Notes.TYPE_NOTE) { + Log.d(TAG, "Opening note: " + data.getId() + ", encrypted: " + data.isEncrypted()); + + // 检查便签的加密状态 + boolean isEncrypted = data.isEncrypted(); + Log.d(TAG, "Note encrypted status from NoteItemData: " + isEncrypted); + + // 加载便签并再次检查加密状态,确保准确性 + WorkingNote note = WorkingNote.load(NotesListActivity.this, data.getId()); + if (note != null) { + isEncrypted = note.isEncrypted(); + Log.d(TAG, "Note encrypted status from WorkingNote: " + isEncrypted); + } + + if (isEncrypted) { + Log.d(TAG, "Note is encrypted, showing password dialog"); + // 弹出密码输入对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle("输入密码"); + builder.setIcon(android.R.drawable.ic_dialog_alert); + + final EditText input = new EditText(NotesListActivity.this); + input.setHint("请输入密码"); + input.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + builder.setView(input); + + builder.setPositiveButton("确定", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + String password = input.getText().toString(); + Log.d(TAG, "Password entered: " + (password != null ? "[hidden]" : "null")); + if (!TextUtils.isEmpty(password)) { + // 验证密码 + Log.d(TAG, "Verifying password for note: " + finalData.getId()); + WorkingNote verifyNote = WorkingNote.load(NotesListActivity.this, finalData.getId()); + Log.d(TAG, "WorkingNote loaded: " + (verifyNote != null)); + if (verifyNote != null && verifyNote.verifyPassword(password)) { + // 密码正确,启动 NoteEditActivity 并传递密码 + Log.d(TAG, "Password verified, starting NoteEditActivity"); + Intent intent = new Intent(NotesListActivity.this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, finalData.getId()); + intent.putExtra("password", password); + Log.d(TAG, "Intent created with password, EXTRA_UID: " + finalData.getId()); + startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + } else { + // 密码错误,显示错误提示 + Log.d(TAG, "Password verification failed"); + Toast.makeText(NotesListActivity.this, "密码错误", Toast.LENGTH_SHORT).show(); + } + } else { + Log.d(TAG, "Password is empty"); + Toast.makeText(NotesListActivity.this, "密码不能为空", Toast.LENGTH_SHORT).show(); + } + } + }); + builder.setNegativeButton("取消", null); + // 设置对话框不可取消,确保用户必须输入密码或点击取消 + builder.setCancelable(false); + builder.show(); + Log.d(TAG, "Password dialog shown successfully"); + } else { + // 非加密便签,直接启动 NoteEditActivity + Log.d(TAG, "Note is not encrypted, starting NoteEditActivity directly"); + Intent intent = new Intent(NotesListActivity.this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, data.getId()); + Log.d(TAG, "Intent created, EXTRA_UID: " + data.getId()); + startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + Log.d(TAG, "startActivityForResult called with request code: " + REQUEST_CODE_OPEN_NODE); + } + } else { + Log.d(TAG, "Skipping non-note item with type: " + data.getType()); + Toast.makeText(NotesListActivity.this, "无法打开此类型的项目", Toast.LENGTH_SHORT).show(); + } + } else { + Log.e(TAG, "Failed to create NoteItemData"); + Toast.makeText(NotesListActivity.this, "无法创建便签数据", Toast.LENGTH_SHORT).show(); + } + } else { + Log.e(TAG, "Cursor is null for position: " + position); + Toast.makeText(NotesListActivity.this, "无法获取便签数据", Toast.LENGTH_SHORT).show(); + } + } catch (Exception e) { + Log.e(TAG, "Error in onItemClick: " + e.getMessage()); + Toast.makeText(NotesListActivity.this, "点击出错: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + }); + + // 启用导航栏切换 + enableNavigationBar(); + + mActionMode = null; + } - @Override - protected void onPostExecute(Integer result) { - if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { - AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); - builder.setTitle(NotesListActivity.this - .getString(R.string.failed_sdcard_export)); - builder.setMessage(NotesListActivity.this - .getString(R.string.error_sdcard_unmounted)); - builder.setPositiveButton(android.R.string.ok, null); - builder.show(); - } else if (result == BackupUtils.STATE_SUCCESS) { - AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); - builder.setTitle(NotesListActivity.this - .getString(R.string.success_sdcard_export)); - builder.setMessage(NotesListActivity.this.getString( - R.string.format_exported_file_location, backup - .getExportedTextFileName(), backup.getExportedTextFileDir())); - builder.setPositiveButton(android.R.string.ok, null); - builder.show(); - } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { - AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); - builder.setTitle(NotesListActivity.this - .getString(R.string.failed_sdcard_export)); - builder.setMessage(NotesListActivity.this - .getString(R.string.error_sdcard_export)); - builder.setPositiveButton(android.R.string.ok, null); - builder.show(); + public boolean onMenuItemClick(MenuItem item) { + if (mNotesListAdapter == null) { + return false; + } + + mSelectedNoteIds = mNotesListAdapter.getSelectedItemIds(); + if (mSelectedNoteIds.isEmpty()) { + Toast.makeText(NotesListActivity.this, R.string.menu_select_none, Toast.LENGTH_SHORT).show(); + return false; + } + + int itemId = item.getItemId(); + + if (mState == ListEditState.RECENTLY_DELETED) { + if (itemId == 100) { + batchRestore(mSelectedNoteIds); + } else if (itemId == 101) { + batchDeletePermanently(mSelectedNoteIds); } + } else { + if (itemId == 102) { + batchDelete(); + } + } + + // 完成操作后结束ActionMode + if (mActionMode != null) { + mActionMode.finish(); } + return true; + } - }.execute(); + public void finishActionMode() { + if (mActionMode != null) { + mActionMode.finish(); + } + } } - private boolean isSyncMode() { - return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; + private void startQueryDestinationFolders() { + mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI, FoldersListAdapter.PROJECTION, "(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND " + NoteColumns.PARENT_ID + "=" + Notes.ID_ROOT_FOLDER + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")", null, null); } - - private void startPreferenceActivity() { - Activity from = getParent() != null ? getParent() : this; - Intent intent = new Intent(from, NotesPreferenceActivity.class); - from.startActivityIfNeeded(intent, -1); + + /** + * 禁用导航栏切换 + */ + private void disableNavigationBar() { + // 禁用导航栏标签的点击事件 + View navNotes = findViewById(R.id.nav_notes); + View navTodo = findViewById(R.id.nav_todo); + View navRecycle = findViewById(R.id.nav_recycle); + + if (navNotes != null) { + navNotes.setClickable(false); + navNotes.setEnabled(false); + } + if (navTodo != null) { + navTodo.setClickable(false); + navTodo.setEnabled(false); + } + if (navRecycle != null) { + navRecycle.setClickable(false); + navRecycle.setEnabled(false); + } } - - private class OnListItemClickListener implements OnItemClickListener { - - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (view instanceof NotesListItem) { - NoteItemData item = ((NotesListItem) view).getItemData(); - if (mNotesListAdapter.isInChoiceMode()) { - if (item.getType() == Notes.TYPE_NOTE) { - position = position - mNotesListView.getHeaderViewsCount(); - mModeCallBack.onItemCheckedStateChanged(null, position, id, - !mNotesListAdapter.isSelectedItem(position)); - } - return; + + /** + * 启用导航栏切换 + */ + private void enableNavigationBar() { + // 启用导航栏标签的点击事件 + View navNotes = findViewById(R.id.nav_notes); + View navTodo = findViewById(R.id.nav_todo); + View navRecycle = findViewById(R.id.nav_recycle); + + if (navNotes != null) { + navNotes.setClickable(true); + navNotes.setEnabled(true); + } + if (navTodo != null) { + navTodo.setClickable(true); + navTodo.setEnabled(true); + } + if (navRecycle != null) { + navRecycle.setClickable(true); + navRecycle.setEnabled(true); + } + } + + /** + * 显示搜索对话框,让用户输入搜索关键词 + */ + private void showSearchDialog() { + try { + // 创建搜索对话框 + LayoutInflater inflater = LayoutInflater.from(this); + View dialogView = inflater.inflate(R.layout.dialog_edit_text, null); + final EditText searchEditText = (EditText) dialogView.findViewById(R.id.et_foler_name); + searchEditText.setHint(R.string.dialog_enter_search_query); + + // 创建对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.dialog_search); + builder.setView(dialogView); + builder.setPositiveButton(android.R.string.ok, null); + builder.setNegativeButton(android.R.string.cancel, null); + builder.setNeutralButton(R.string.dialog_clear_search, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // 清除搜索 + mSearchQuery = ""; + startAsyncNotesListQuery(); + dialog.dismiss(); } - - switch (mState) { - case NOTE_LIST: - if (item.getType() == Notes.TYPE_FOLDER - || item.getType() == Notes.TYPE_SYSTEM) { - openFolder(item); - } else if (item.getType() == Notes.TYPE_NOTE) { - openNode(item); - } else { - Log.e(TAG, "Wrong note type in NOTE_LIST"); - } - break; - case SUB_FOLDER: - case CALL_RECORD_FOLDER: - if (item.getType() == Notes.TYPE_NOTE) { - openNode(item); - } else { - Log.e(TAG, "Wrong note type in SUB_FOLDER"); - } - break; - case RECENTLY_DELETED: - // 最近删除模式下单击显示恢复和删除对话框 - if (item.getType() == Notes.TYPE_NOTE) { - showDeleteRestoreDialog(item); + }); + + final AlertDialog dialog = builder.create(); + dialog.show(); + + // 重写确定按钮点击事件 + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String searchQuery = searchEditText.getText().toString().trim(); + if (!TextUtils.isEmpty(searchQuery)) { + // 设置搜索关键词 + mSearchQuery = searchQuery; + // 执行搜索 + startAsyncNotesListQuery(); } - break; - default: - break; + dialog.dismiss(); } + }); + } catch (Exception e) { + Log.e(TAG, "Error in showSearchDialog: " + e.getMessage()); + Toast.makeText(this, "显示搜索对话框出错: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + + /** + * 显示弹出菜单,位置在选中的便签旁边 + * @param view 被长按的视图 + * @param data 便签数据 + */ + private void showPopupMenu(View view, final NoteItemData data) { + try { + // 创建 PopupMenu,位置在选中的便签旁边 + PopupMenu popupMenu = new PopupMenu(this, view); + + // 根据便签类型和状态添加菜单项 + if (mState == ListEditState.RECENTLY_DELETED) { + // 最近删除模式下显示恢复和永久删除选项 + popupMenu.getMenu().add(0, 200, 0, R.string.menu_restore); + popupMenu.getMenu().add(0, 201, 0, R.string.menu_delete_permanently); + } else if (data.getType() == Notes.TYPE_FOLDER) { + // 文件夹显示文件夹相关选项 + popupMenu.getMenu().add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); + popupMenu.getMenu().add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); + popupMenu.getMenu().add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); + } else { + // 普通便签显示删除选项 + popupMenu.getMenu().add(0, 100, 0, R.string.menu_delete); } + + // 设置菜单项点击监听器 + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + try { + int itemId = item.getItemId(); + if (mState == ListEditState.RECENTLY_DELETED) { + if (itemId == 200) { + HashSet ids = new HashSet(); + ids.add(data.getId()); + batchRestore(ids); + } else if (itemId == 201) { + HashSet ids = new HashSet(); + ids.add(data.getId()); + batchDeletePermanently(ids); + } + } else { + if (itemId == 100) { + // 处理普通便签的删除操作 + deleteNote(data.getId()); + } else { + switch (itemId) { + case MENU_FOLDER_VIEW: + openFolder(data); + break; + case MENU_FOLDER_DELETE: + deleteFolder(data.getId()); + break; + case MENU_FOLDER_CHANGE_NAME: + showCreateOrModifyFolderDialog(false); + break; + default: + break; + } + } + } + } catch (Exception e) { + Log.e(TAG, "Error in popup menu item click: " + e.getMessage()); + Toast.makeText(NotesListActivity.this, "菜单操作出错: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + return true; + } + }); + + // 显示弹出菜单 + popupMenu.show(); + } catch (Exception e) { + Log.e(TAG, "Error in showPopupMenu: " + e.getMessage()); + Toast.makeText(this, "显示菜单出错: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } - } - private void startQueryDestinationFolders() { - String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; - selection = (mState == ListEditState.NOTE_LIST) ? selection: - "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; - - mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, - null, - Notes.CONTENT_NOTE_URI, - FoldersListAdapter.PROJECTION, - selection, - new String[] { - String.valueOf(Notes.TYPE_FOLDER), - String.valueOf(Notes.ID_TRASH_FOLDER), - String.valueOf(mCurrentFolderId) - }, - NoteColumns.MODIFIED_DATE + " DESC"); + // 已完成列表是否展开 + private boolean mCompletedListExpanded = true; + + /** + * 从mTodoItems列表中恢复待办事项到界面 + */ + private void restoreTodoItemsFromList() { + // 找到待办事项容器和已完成列表 + LinearLayout todoListContainer = (LinearLayout) findViewById(R.id.todo_list_container); + LinearLayout completedList = (LinearLayout) findViewById(R.id.completed_list); + if (todoListContainer == null || completedList == null) { + return; + } + + // 保存已完成列表头部 + LinearLayout completedHeader = (LinearLayout) findViewById(R.id.completed_header); + + // 清空当前界面上的待办事项,避免重复添加 + todoListContainer.removeAllViews(); + completedList.removeAllViews(); + + // 清空视图映射 + mTodoItemViews.clear(); + mCompletedItemViews.clear(); + + // 重新添加已完成列表头部和已完成列表 + if (completedHeader != null) { + todoListContainer.addView(completedHeader); + // 初始时隐藏已完成头部 + completedHeader.setVisibility(View.GONE); + + // 添加已完成列表头部点击事件,实现折叠/展开功能 + completedHeader.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + toggleCompletedList(); + } + }); + } + + // 重新添加已完成列表 + todoListContainer.addView(completedList); + // 初始时隐藏已完成列表 + completedList.setVisibility(View.GONE); + + // 遍历mTodoItems列表,根据状态添加到对应位置 + for (TodoItem item : mTodoItems) { + if (item.completed) { + // 已完成的待办事项,添加到已完成列表 + addCompletedTodoItemToUI(item.content); + } else { + // 未完成的待办事项,添加到待办列表 + addUncompletedTodoItemToUI(item.content); + } + } + + // 更新已完成计数 + updateCompletedCount(); + + // 显示或隐藏已完成列表 + int completedCount = completedList.getChildCount(); + if (completedCount > 0) { + completedList.setVisibility(mCompletedListExpanded ? View.VISIBLE : View.GONE); + if (completedHeader != null) { + completedHeader.setVisibility(View.VISIBLE); + updateCompletedHeaderArrow(); + } + } else { + completedList.setVisibility(View.GONE); + if (completedHeader != null) { + completedHeader.setVisibility(View.GONE); + } + } } - - 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"); + + /** + * 切换已完成列表的折叠/展开状态 + */ + private void toggleCompletedList() { + LinearLayout completedList = (LinearLayout) findViewById(R.id.completed_list); + if (completedList == null) { + return; + } + + // 切换状态 + mCompletedListExpanded = !mCompletedListExpanded; + + // 更新箭头图标 + updateCompletedHeaderArrow(); + + // 显示或隐藏已完成列表,添加动画效果 + if (mCompletedListExpanded) { + // 展开动画 + completedList.setVisibility(View.VISIBLE); + completedList.animate() + .alpha(1.0f) + .setDuration(300) + .start(); + } else { + // 折叠动画 + completedList.animate() + .alpha(0.0f) + .setDuration(300) + .withEndAction(new Runnable() { + @Override + public void run() { + completedList.setVisibility(View.GONE); + } + }) + .start(); + } + + // 保存已完成列表的展开状态 + saveCompletedListExpandedState(); + } + + /** + * 保存待办事项到 SharedPreferences + */ + private void saveTodoItems() { + try { + // 创建 JSON 数组 + JSONArray jsonArray = new JSONArray(); + for (TodoItem item : mTodoItems) { + // 创建 JSON 对象 + JSONObject jsonObject = new JSONObject(); + jsonObject.put("content", item.content); + jsonObject.put("completed", item.completed); + jsonArray.put(jsonObject); + } + + // 保存到 SharedPreferences + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + sp.edit().putString(PREFERENCE_TODO_ITEMS, jsonArray.toString()).apply(); + } catch (JSONException e) { + e.printStackTrace(); + Log.e(TAG, "Error saving todo items: " + e.getMessage()); + } + } + + /** + * 从 SharedPreferences 加载待办事项 + */ + private void loadTodoItems() { + try { + // 从 SharedPreferences 读取 + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + String jsonString = sp.getString(PREFERENCE_TODO_ITEMS, ""); + + if (!TextUtils.isEmpty(jsonString)) { + // 解析 JSON 数组 + JSONArray jsonArray = new JSONArray(jsonString); + mTodoItems.clear(); + for (int i = 0; i < jsonArray.length(); i++) { + // 解析 JSON 对象 + JSONObject jsonObject = jsonArray.getJSONObject(i); + String content = jsonObject.getString("content"); + boolean completed = jsonObject.getBoolean("completed"); + mTodoItems.add(new TodoItem(content, completed)); } - } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { - mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); } + + // 加载已完成列表的展开状态 + loadCompletedListExpandedState(); + } catch (JSONException e) { + e.printStackTrace(); + Log.e(TAG, "Error loading todo items: " + e.getMessage()); } - return false; } - + /** - * 显示恢复和永久删除对话框 + * 保存已完成列表的展开状态 */ - private void showDeleteRestoreDialog(final NoteItemData item) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.title_recently_deleted); - builder.setItems(new CharSequence[]{ - getString(R.string.menu_restore), - getString(R.string.menu_delete_permanently) - }, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - HashSet selectedIds = new HashSet(); - selectedIds.add(item.getId()); - if (which == 0) { - // 恢复选中的笔记 - batchRestore(selectedIds); - } else if (which == 1) { - // 永久删除选中的笔记 - AlertDialog.Builder confirmBuilder = new AlertDialog.Builder(NotesListActivity.this); - confirmBuilder.setTitle(getString(R.string.alert_title_delete)); - confirmBuilder.setIcon(android.R.drawable.ic_dialog_alert); - confirmBuilder.setMessage(getString(R.string.alert_message_delete_permanently, 1)); - confirmBuilder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - HashSet confirmIds = new HashSet(); - confirmIds.add(item.getId()); - batchDeletePermanently(confirmIds); + private void saveCompletedListExpandedState() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + sp.edit().putBoolean(PREFERENCE_COMPLETED_LIST_EXPANDED, mCompletedListExpanded).apply(); + } + + /** + * 加载已完成列表的展开状态 + */ + private void loadCompletedListExpandedState() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + mCompletedListExpanded = sp.getBoolean(PREFERENCE_COMPLETED_LIST_EXPANDED, true); + } + + /** + * 更新已完成列表头部的箭头图标 + */ + private void updateCompletedHeaderArrow() { + ImageView completedArrow = (ImageView) findViewById(R.id.completed_arrow); + if (completedArrow != null) { + if (mCompletedListExpanded) { + // 展开状态,使用向下箭头 + completedArrow.setImageResource(android.R.drawable.arrow_down_float); + } else { + // 折叠状态,使用向上箭头 + completedArrow.setImageResource(android.R.drawable.arrow_up_float); + } + } + } + + /** + * 添加未完成的待办事项到界面 + * @param todoText 待办事项内容 + */ + private void addUncompletedTodoItemToUI(String todoText) { + // 找到待办事项容器 + final LinearLayout todoListContainer = (LinearLayout) findViewById(R.id.todo_list_container); + if (todoListContainer == null) { + return; + } + + // 找到已完成列表头部,我们要在它之前添加新的待办事项 + LinearLayout completedHeader = (LinearLayout) findViewById(R.id.completed_header); + int insertIndex = todoListContainer.indexOfChild(completedHeader); + if (insertIndex == -1) { + insertIndex = todoListContainer.getChildCount(); + } + + // 创建新的待办事项卡片 + final LinearLayout todoItem = new LinearLayout(this); + LinearLayout.LayoutParams itemParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + itemParams.setMargins(0, 0, 0, 12); + todoItem.setLayoutParams(itemParams); + todoItem.setOrientation(LinearLayout.HORIZONTAL); + todoItem.setGravity(Gravity.CENTER_VERTICAL); + todoItem.setPadding(16, 16, 16, 16); + + // 添加圆角效果 + GradientDrawable drawable = new GradientDrawable(); + drawable.setColor(Color.WHITE); + drawable.setCornerRadius(8); + todoItem.setBackground(drawable); + + // 添加复选框 + CheckBox checkBox = new CheckBox(this); + LinearLayout.LayoutParams checkboxParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + checkboxParams.setMarginEnd(16); + checkBox.setLayoutParams(checkboxParams); + // 设置复选框为默认样式 + checkBox.setChecked(false); + + // 添加文本 + TextView textView = new TextView(this); + LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1); + textView.setLayoutParams(textParams); + textView.setText(todoText); + textView.setTextColor(Color.BLACK); + textView.setTextSize(16); + + // 找到对应的TodoItem对象 + final TodoItem todoItemObj = findTodoItemByContent(todoText); + + // 添加复选框点击事件 + checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked && todoItemObj != null) { + // 更新待办事项对象的状态 + todoItemObj.completed = true; + + // 待办事项标记为已完成,移动到已完成列表 + todoListContainer.removeView(todoItem); + mTodoItemViews.remove(todoText); + + // 获取已完成列表 + LinearLayout completedList = (LinearLayout) findViewById(R.id.completed_list); + if (completedList != null) { + // 创建已完成待办事项 + LinearLayout completedItem = new LinearLayout(NotesListActivity.this); + LinearLayout.LayoutParams completedParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + completedParams.setMargins(0, 0, 0, 12); + completedItem.setLayoutParams(completedParams); + completedItem.setOrientation(LinearLayout.HORIZONTAL); + completedItem.setGravity(Gravity.CENTER_VERTICAL); + completedItem.setPadding(16, 16, 16, 16); + + // 添加圆角效果 + GradientDrawable completedDrawable = new GradientDrawable(); + completedDrawable.setColor(Color.WHITE); + completedDrawable.setCornerRadius(8); + completedItem.setBackground(completedDrawable); + + // 添加已完成复选框 + CheckBox completedCheckBox = new CheckBox(NotesListActivity.this); + LinearLayout.LayoutParams completedCheckboxParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + completedCheckboxParams.setMarginEnd(16); + completedCheckBox.setLayoutParams(completedCheckboxParams); + // 设置复选框为默认样式 + completedCheckBox.setChecked(true); + + // 添加已完成文本 + TextView completedTextView = new TextView(NotesListActivity.this); + LinearLayout.LayoutParams completedTextParams = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1); + completedTextView.setLayoutParams(completedTextParams); + completedTextView.setText(todoText); + completedTextView.setTextColor(Color.GRAY); + completedTextView.setTextSize(16); + + // 添加删除按钮 + ImageView deleteButton = new ImageView(NotesListActivity.this); + LinearLayout.LayoutParams deleteButtonParams = new LinearLayout.LayoutParams( + 32, 32); + deleteButton.setLayoutParams(deleteButtonParams); + deleteButton.setImageResource(android.R.drawable.ic_menu_delete); + deleteButton.setColorFilter(Color.GRAY); + deleteButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + removeCompletedTodoItem(todoText); + } + }); + + // 添加到已完成容器 + completedItem.addView(completedCheckBox); + completedItem.addView(completedTextView); + completedItem.addView(deleteButton); + + // 添加到已完成列表 + completedList.addView(completedItem); + mCompletedItemViews.put(todoText, completedItem); + + // 显示已完成列表和头部 + completedList.setVisibility(View.VISIBLE); + if (completedHeader != null) { + completedHeader.setVisibility(View.VISIBLE); } - }); - confirmBuilder.setNegativeButton(android.R.string.cancel, null); - confirmBuilder.show(); + + // 更新已完成计数 + updateCompletedCount(); + + // 保存待办事项 + saveTodoItems(); + + // 显示 Toast 提示用户操作成功 + Toast.makeText(NotesListActivity.this, "待办事项已标记为完成", Toast.LENGTH_SHORT).show(); + } } } }); - builder.show(); -} + + // 添加删除按钮 + ImageView deleteButton = new ImageView(this); + LinearLayout.LayoutParams deleteButtonParams = new LinearLayout.LayoutParams( + 32, 32); + deleteButton.setLayoutParams(deleteButtonParams); + deleteButton.setImageResource(android.R.drawable.ic_menu_delete); + deleteButton.setColorFilter(Color.GRAY); + deleteButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + removeTodoItem(todoText); + } + }); + + // 添加到容器 + todoItem.addView(checkBox); + todoItem.addView(textView); + todoItem.addView(deleteButton); + + // 添加到界面 + todoListContainer.addView(todoItem, insertIndex); + mTodoItemViews.put(todoText, todoItem); + } + + /** + * 添加已完成的待办事项到界面 + * @param todoText 待办事项内容 + */ + private void addCompletedTodoItemToUI(String todoText) { + // 获取已完成列表 + LinearLayout completedList = (LinearLayout) findViewById(R.id.completed_list); + if (completedList == null) { + return; + } + + // 创建已完成待办事项 + LinearLayout completedItem = new LinearLayout(this); + LinearLayout.LayoutParams completedParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + completedParams.setMargins(0, 0, 0, 12); + completedItem.setLayoutParams(completedParams); + completedItem.setOrientation(LinearLayout.HORIZONTAL); + completedItem.setGravity(Gravity.CENTER_VERTICAL); + completedItem.setPadding(16, 16, 16, 16); + + // 添加圆角效果 + GradientDrawable completedDrawable = new GradientDrawable(); + completedDrawable.setColor(Color.WHITE); + completedDrawable.setCornerRadius(8); + completedItem.setBackground(completedDrawable); + + // 添加已完成复选框 + CheckBox completedCheckBox = new CheckBox(this); + LinearLayout.LayoutParams completedCheckboxParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + completedCheckboxParams.setMarginEnd(16); + completedCheckBox.setLayoutParams(completedCheckboxParams); + // 设置复选框为默认样式 + completedCheckBox.setChecked(true); + + // 添加已完成文本 + TextView completedTextView = new TextView(this); + LinearLayout.LayoutParams completedTextParams = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1); + completedTextView.setLayoutParams(completedTextParams); + completedTextView.setText(todoText); + completedTextView.setTextColor(Color.GRAY); + completedTextView.setTextSize(16); + + // 添加删除按钮 + ImageView deleteButton = new ImageView(this); + LinearLayout.LayoutParams deleteButtonParams = new LinearLayout.LayoutParams( + 32, 32); + deleteButton.setLayoutParams(deleteButtonParams); + deleteButton.setImageResource(android.R.drawable.ic_menu_delete); + deleteButton.setColorFilter(Color.GRAY); + deleteButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + removeCompletedTodoItem(todoText); + } + }); + + // 添加到已完成容器 + completedItem.addView(completedCheckBox); + completedItem.addView(completedTextView); + completedItem.addView(deleteButton); + + // 添加到已完成列表 + completedList.addView(completedItem); + mCompletedItemViews.put(todoText, completedItem); + } + + /** + * 根据内容查找对应的TodoItem对象 + * @param content 待办事项内容 + * @return 对应的TodoItem对象,如果未找到则返回null + */ + private TodoItem findTodoItemByContent(String content) { + for (TodoItem item : mTodoItems) { + if (item.content.equals(content)) { + return item; + } + } + return null; + } } \ No newline at end of file diff --git a/src/main/java/net/micode/notes/ui/NotesListAdapter.java b/src/main/java/net/micode/notes/ui/NotesListAdapter.java index 51c9cb9..5473778 100644 --- a/src/main/java/net/micode/notes/ui/NotesListAdapter.java +++ b/src/main/java/net/micode/notes/ui/NotesListAdapter.java @@ -37,6 +37,7 @@ public class NotesListAdapter extends CursorAdapter { private HashMap mSelectedIndex; private int mNotesCount; private boolean mChoiceMode; + private String mSearchQuery; public static class AppWidgetAttribute { public int widgetId; @@ -48,6 +49,12 @@ public class NotesListAdapter extends CursorAdapter { mSelectedIndex = new HashMap(); mContext = context; mNotesCount = 0; + mSearchQuery = ""; + } + + public void setSearchQuery(String searchQuery) { + mSearchQuery = searchQuery; + notifyDataSetChanged(); } @Override @@ -68,13 +75,23 @@ public class NotesListAdapter extends CursorAdapter { mSelectedIndex.put(position, checked); notifyDataSetChanged(); } + + public void toggleItemChecked(final int position) { + Boolean currentState = mSelectedIndex.get(position); + boolean newState = (currentState == null) ? true : !currentState; + mSelectedIndex.put(position, newState); + notifyDataSetChanged(); + } public boolean isInChoiceMode() { return mChoiceMode; } public void setChoiceMode(boolean mode) { - mSelectedIndex.clear(); + // 只有在从选择模式切换到非选择模式时才清除mSelectedIndex集合 + if (!mode) { + mSelectedIndex.clear(); + } mChoiceMode = mode; } @@ -91,17 +108,32 @@ public class NotesListAdapter extends CursorAdapter { public HashSet getSelectedItemIds() { HashSet itemSet = new HashSet(); - for (Integer position : mSelectedIndex.keySet()) { - if (mSelectedIndex.get(position) == true) { - Long id = getItemId(position); - if (id == Notes.ID_ROOT_FOLDER) { - Log.d(TAG, "Wrong item id, should not happen"); - } else { - itemSet.add(id); + Log.d(TAG, "getSelectedItemIds called, mSelectedIndex size: " + mSelectedIndex.size()); + // 获取整个Cursor对象 + Cursor cursor = getCursor(); + Log.d(TAG, "Cursor obtained: " + (cursor != null)); + if (cursor != null) { + for (Integer position : mSelectedIndex.keySet()) { + Log.d(TAG, "Checking position: " + position + ", selected: " + mSelectedIndex.get(position)); + if (mSelectedIndex.get(position) == true) { + // 将Cursor移动到指定位置 + if (cursor.moveToPosition(position)) { + // 从Cursor中获取笔记的实际ID,使用ID_COLUMN索引(0) + long id = cursor.getLong(0); + Log.d(TAG, "Found note with id: " + id); + if (id == Notes.ID_ROOT_FOLDER) { + Log.d(TAG, "Wrong item id, should not happen"); + } else { + itemSet.add(id); + Log.d(TAG, "Added note id to set: " + id); + } + } else { + Log.e(TAG, "Failed to move cursor to position: " + position); + } } } } - + Log.d(TAG, "Returning itemSet with size: " + itemSet.size() + ", ids: " + itemSet); return itemSet; } diff --git a/src/main/java/net/micode/notes/ui/NotesListItem.java b/src/main/java/net/micode/notes/ui/NotesListItem.java index 3b64102..8cfe054 100644 --- a/src/main/java/net/micode/notes/ui/NotesListItem.java +++ b/src/main/java/net/micode/notes/ui/NotesListItem.java @@ -17,7 +17,7 @@ package net.micode.notes.ui; import android.content.Context; -import android.text.format.DateUtils; +import android.content.Intent; import android.view.View; import android.widget.CheckBox; import android.widget.ImageView; @@ -27,7 +27,6 @@ import android.widget.TextView; import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.tool.DataUtils; -import net.micode.notes.tool.ResourceParser.NoteItemBgResources; public class NotesListItem extends LinearLayout { @@ -47,7 +46,10 @@ public class NotesListItem extends LinearLayout { mCallName = (TextView) findViewById(R.id.tv_name); mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); } - + + /** + * 绑定数据 + */ public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { if (choiceMode && data.getType() == Notes.TYPE_NOTE) { mCheckBox.setVisibility(View.VISIBLE); @@ -57,6 +59,120 @@ public class NotesListItem extends LinearLayout { } mItemData = data; + + // 移除所有监听器,确保不会与Activity的监听器冲突 + setOnTouchListener(null); + setOnLongClickListener(null); + + // 添加点击监听器 + setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (choiceMode && data.getType() == Notes.TYPE_NOTE) { + // 在选择模式下,切换复选框状态 + boolean newCheckedState = !mCheckBox.isChecked(); + mCheckBox.setChecked(newCheckedState); + // 通知适配器更新选择状态 + if (context instanceof NotesListActivity) { + NotesListActivity activity = (NotesListActivity) context; + // 通过反射获取适配器并更新选择状态 + try { + // 获取GridView + android.widget.GridView gridView = (android.widget.GridView) activity.findViewById(net.micode.notes.R.id.notes_grid); + if (gridView != null) { + // 获取适配器 + android.widget.ListAdapter adapter = gridView.getAdapter(); + if (adapter instanceof net.micode.notes.ui.NotesListAdapter) { + net.micode.notes.ui.NotesListAdapter notesAdapter = (net.micode.notes.ui.NotesListAdapter) adapter; + // 查找当前项在适配器中的位置 + android.database.Cursor cursor = (android.database.Cursor) notesAdapter.getCursor(); + if (cursor != null) { + int position = -1; + for (int i = 0; i < cursor.getCount(); i++) { + if (cursor.moveToPosition(i)) { + long noteId = cursor.getLong(0); // 假设ID是第一列 + if (noteId == data.getId()) { + position = i; + break; + } + } + } + if (position != -1) { + // 切换选择状态 + notesAdapter.toggleItemChecked(position); + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } else if (context instanceof NotesListActivity) { + // 非选择模式下,正常打开便签或文件夹 + NotesListActivity activity = (NotesListActivity) context; + if (data.getType() == Notes.TYPE_FOLDER) { + activity.openFolder(data); + } else if (data.getType() == Notes.TYPE_NOTE) { + // 检查便签是否加密 + boolean isEncrypted = data.isEncrypted(); + + // 加载便签并再次检查加密状态,确保准确性 + net.micode.notes.model.WorkingNote note = net.micode.notes.model.WorkingNote.load(activity, data.getId()); + if (note != null) { + isEncrypted = note.isEncrypted(); + } + + if (isEncrypted) { + // 弹出密码输入对话框 + android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(activity); + builder.setTitle("输入密码"); + builder.setIcon(android.R.drawable.ic_dialog_alert); + + final android.widget.EditText input = new android.widget.EditText(activity); + input.setHint("请输入密码"); + input.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + builder.setView(input); + + builder.setPositiveButton("确定", + new android.content.DialogInterface.OnClickListener() { + public void onClick(android.content.DialogInterface dialog, int which) { + String password = input.getText().toString(); + if (!android.text.TextUtils.isEmpty(password)) { + // 验证密码 + net.micode.notes.model.WorkingNote verifyNote = net.micode.notes.model.WorkingNote.load(activity, data.getId()); + if (verifyNote != null && verifyNote.verifyPassword(password)) { + // 密码正确,启动 NoteEditActivity 并传递密码 + Intent intent = new Intent(context, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, data.getId()); + intent.putExtra("password", password); + activity.startActivityForResult(intent, 102); + } else { + // 密码错误,显示错误提示 + android.widget.Toast.makeText(activity, "密码错误", android.widget.Toast.LENGTH_SHORT).show(); + } + } else { + android.widget.Toast.makeText(activity, "密码不能为空", android.widget.Toast.LENGTH_SHORT).show(); + } + } + }); + builder.setNegativeButton("取消", null); + // 设置对话框不可取消,确保用户必须输入密码或点击取消 + builder.setCancelable(false); + builder.show(); + } else { + // 非加密便签,直接启动 NoteEditActivity + Intent intent = new Intent(context, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, data.getId()); + activity.startActivityForResult(intent, 102); + } + } + } + } + }); + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { mCallName.setVisibility(View.GONE); mAlert.setVisibility(View.VISIBLE); @@ -91,7 +207,15 @@ public class NotesListItem extends LinearLayout { data.getNotesCount())); mAlert.setVisibility(View.GONE); } else { - String title = DataUtils.getFormattedSnippet(data.getSnippet()); + // 优先显示标题,如果没有标题则显示内容摘要 + String title = data.getTitle(); + if (title == null || title.isEmpty()) { + if (data.isEncrypted()) { + title = "[已加密]"; + } else { + title = DataUtils.getFormattedSnippet(data.getSnippet()); + } + } // 如果有提醒,添加剩余提醒时间 if (data.hasAlert()) { String reminderTime = getRemainingReminderTime(context, data.getAlertDate()); @@ -101,22 +225,59 @@ public class NotesListItem extends LinearLayout { } mTitle.setText(title); if (data.isEncrypted()) { - // 加密状态显示加密图标 - mAlert.setImageResource(R.drawable.menu_delete); - mAlert.setVisibility(View.VISIBLE); - } else if (data.isPinned()) { - // 置顶状态显示置顶图标 - mAlert.setImageResource(R.drawable.selected); - mAlert.setVisibility(View.VISIBLE); - } else if (data.hasAlert()) { - mAlert.setImageResource(R.drawable.clock); + // 加密状态显示锁图标 + mAlert.setImageResource(android.R.drawable.ic_lock_lock); mAlert.setVisibility(View.VISIBLE); } else { + // 未加密的笔记右下角不显示任何图标 mAlert.setVisibility(View.GONE); } } } - mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + // 格式化创建时间为年月日 上午/下午 HH:mm 格式显示 + java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm", java.util.Locale.getDefault()); + java.util.Calendar calendar = java.util.Calendar.getInstance(); + calendar.setTimeInMillis(data.getCreatedDate()); + int hour = calendar.get(java.util.Calendar.HOUR_OF_DAY); + String timeStr; + if (hour < 12) { + timeStr = "上午 " + sdf.format(new java.util.Date(data.getCreatedDate())); + } else { + timeStr = "下午 " + sdf.format(new java.util.Date(data.getCreatedDate())); + } + mTime.setText(timeStr); + + // 更新副标题 + TextView tvSubtitle = (TextView) findViewById(R.id.tv_subtitle); + if (tvSubtitle != null) { + if (data.getType() == Notes.TYPE_NOTE) { + // 对于普通笔记,显示内容摘要 + if (!data.isEncrypted()) { + String snippet = data.getSnippet(); + if (snippet != null && !snippet.isEmpty()) { + String formattedSnippet = DataUtils.getFormattedSnippet(snippet); + // 如果有标题,且摘要与标题不同,则显示摘要 + String title = data.getTitle(); + if (title != null && !title.isEmpty() && !formattedSnippet.equals(title)) { + tvSubtitle.setText(formattedSnippet); + tvSubtitle.setVisibility(View.VISIBLE); + } else { + // 如果没有标题或摘要与标题相同,则隐藏副标题 + tvSubtitle.setVisibility(View.GONE); + } + } else { + // 如果没有摘要,则隐藏副标题 + tvSubtitle.setVisibility(View.GONE); + } + } else { + // 对于加密笔记,隐藏副标题 + tvSubtitle.setVisibility(View.GONE); + } + } else { + // 对于文件夹,隐藏副标题 + tvSubtitle.setVisibility(View.GONE); + } + } setBackground(data); } @@ -153,23 +314,11 @@ public class NotesListItem extends LinearLayout { } private void setBackground(NoteItemData data) { - int id = data.getBgColorId(); - if (data.getType() == Notes.TYPE_NOTE) { - if (data.isSingle() || data.isOneFollowingFolder()) { - setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); - } else if (data.isLast()) { - setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); - } else if (data.isFirst() || data.isMultiFollowingFolder()) { - setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); - } else { - setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); - } - } else { - setBackgroundResource(NoteItemBgResources.getFolderBgRes()); - } + // 使用白色背景,忽略笔记的背景颜色设置 + setBackgroundResource(R.drawable.note_card_bg); } public NoteItemData getItemData() { return mItemData; } -} +} \ No newline at end of file diff --git a/src/main/res/drawable/btn_pressed_background.xml b/src/main/res/drawable/btn_pressed_background.xml new file mode 100644 index 0000000..7e48dca --- /dev/null +++ b/src/main/res/drawable/btn_pressed_background.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/btn_selected_background.xml b/src/main/res/drawable/btn_selected_background.xml new file mode 100644 index 0000000..5d7e50d --- /dev/null +++ b/src/main/res/drawable/btn_selected_background.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/circle_bg.xml b/src/main/res/drawable/circle_bg.xml new file mode 100644 index 0000000..dbf3288 --- /dev/null +++ b/src/main/res/drawable/circle_bg.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/src/main/res/drawable/cursor_yellow.xml b/src/main/res/drawable/cursor_yellow.xml new file mode 100644 index 0000000..fa4774b --- /dev/null +++ b/src/main/res/drawable/cursor_yellow.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_format_bold.xml b/src/main/res/drawable/ic_format_bold.xml new file mode 100644 index 0000000..244bd56 --- /dev/null +++ b/src/main/res/drawable/ic_format_bold.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_format_italic.xml b/src/main/res/drawable/ic_format_italic.xml new file mode 100644 index 0000000..8785e4c --- /dev/null +++ b/src/main/res/drawable/ic_format_italic.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_format_list_bulleted.xml b/src/main/res/drawable/ic_format_list_bulleted.xml new file mode 100644 index 0000000..2da9533 --- /dev/null +++ b/src/main/res/drawable/ic_format_list_bulleted.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_format_list_numbered.xml b/src/main/res/drawable/ic_format_list_numbered.xml new file mode 100644 index 0000000..82b25b9 --- /dev/null +++ b/src/main/res/drawable/ic_format_list_numbered.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_format_underlined.xml b/src/main/res/drawable/ic_format_underlined.xml new file mode 100644 index 0000000..7cbbc35 --- /dev/null +++ b/src/main/res/drawable/ic_format_underlined.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/main/res/drawable/note_card_bg.xml b/src/main/res/drawable/note_card_bg.xml new file mode 100644 index 0000000..0d59d1b --- /dev/null +++ b/src/main/res/drawable/note_card_bg.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/note_title_border.xml b/src/main/res/drawable/note_title_border.xml new file mode 100644 index 0000000..d4dc886 --- /dev/null +++ b/src/main/res/drawable/note_title_border.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/search_bar_bg.xml b/src/main/res/drawable/search_bar_bg.xml new file mode 100644 index 0000000..860c365 --- /dev/null +++ b/src/main/res/drawable/search_bar_bg.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/underline.xml b/src/main/res/drawable/underline.xml new file mode 100644 index 0000000..4d06469 --- /dev/null +++ b/src/main/res/drawable/underline.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/src/main/res/layout/alarm_alert.xml b/src/main/res/layout/alarm_alert.xml new file mode 100644 index 0000000..f9fda3d --- /dev/null +++ b/src/main/res/layout/alarm_alert.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/dialog_time_search.xml b/src/main/res/layout/dialog_time_search.xml new file mode 100644 index 0000000..47f7f5f --- /dev/null +++ b/src/main/res/layout/dialog_time_search.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/note_card_item.xml b/src/main/res/layout/note_card_item.xml new file mode 100644 index 0000000..2fcbb94 --- /dev/null +++ b/src/main/res/layout/note_card_item.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/note_edit.xml b/src/main/res/layout/note_edit.xml index 659a945..f9a2716 100644 --- a/src/main/res/layout/note_edit.xml +++ b/src/main/res/layout/note_edit.xml @@ -18,7 +18,7 @@ + + + + + + + + + + + + + + + + + + + android:layout_height="wrap_content" + android:visibility="gone"> - - - - - - - - - - - - - - - - - + android:layout_gravity="center_vertical" /> - + - - + android:layout_height="0dp" + android:layout_weight="1" + android:scrollbars="none" + android:padding="0dp" + android:layout_margin="0dp" + android:overScrollMode="never"> - - + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingHorizontal="16dp" + android:paddingTop="0dp" + android:paddingBottom="32dp" + android:layout_margin="0dp" + android:gravity="top" + android:baselineAligned="false"> + + + + + + + + + + + - - - - - - + android:layout_height="wrap_content" + android:orientation="vertical" + android:visibility="gone" /> + + - - + + - + + + + + + + + + + + + + + @@ -247,9 +335,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|right" + android:layout_marginRight="5dip" android:focusable="false" android:visibility="gone" - android:layout_marginRight="2dip" android:src="@drawable/selected" /> @@ -268,6 +356,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|right" + android:layout_marginRight="5dip" android:focusable="false" android:visibility="gone" android:src="@drawable/selected" /> @@ -288,12 +377,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|right" + android:layout_marginRight="5dip" android:focusable="false" android:visibility="gone" android:src="@drawable/selected" /> + @@ -411,8 +500,6 @@ android:layout_gravity="bottom|right" android:focusable="false" android:visibility="gone" - android:layout_marginRight="6dip" - android:layout_marginBottom="-7dip" android:src="@drawable/selected" /> @@ -449,9 +536,7 @@ android:layout_gravity="bottom|right" android:focusable="false" android:visibility="gone" - android:layout_marginRight="6dip" - android:layout_marginBottom="-7dip" android:src="@drawable/selected" /> - + \ No newline at end of file diff --git a/src/main/res/layout/note_item.xml b/src/main/res/layout/note_item.xml index d541f6a..6dcd84b 100644 --- a/src/main/res/layout/note_item.xml +++ b/src/main/res/layout/note_item.xml @@ -23,43 +23,49 @@ + android:layout_height="180dp" + android:orientation="vertical" + android:padding="16dp"> - - - + android:textAppearance="@style/TextAppearancePrimaryItem" + android:visibility="gone" /> - + + - + + - - - + + - - + + diff --git a/src/main/res/layout/note_list.xml b/src/main/res/layout/note_list.xml index 386ec7e..850b9e3 100644 --- a/src/main/res/layout/note_list.xml +++ b/src/main/res/layout/note_list.xml @@ -15,98 +15,239 @@ limitations under the License. --> - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#F5F5F5"> - - - + + + + - - - - - - - - - + android:gravity="center_vertical" + android:paddingLeft="16dp" + android:paddingRight="16dp"> + + + + + + + + - + + + +