diff --git a/doc/小米便签泛读、标注和维护报告文档.docx b/doc/小米便签泛读、标注和维护报告文档.docx deleted file mode 100644 index 697a69e..0000000 Binary files a/doc/小米便签泛读、标注和维护报告文档.docx and /dev/null differ diff --git a/doc/小米便签的质量分析报告文档.docx b/doc/小米便签的质量分析报告文档.docx deleted file mode 100644 index 3496557..0000000 Binary files a/doc/小米便签的质量分析报告文档.docx and /dev/null differ diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 89fedba..08d803a 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -2,9 +2,12 @@ + + + 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 column for title + 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 { @@ -235,197 +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); + 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 db02826..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) { 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/NoteEditActivity.java b/src/main/java/net/micode/notes/ui/NoteEditActivity.java index bdebcbf..64adc87 100644 --- a/src/main/java/net/micode/notes/ui/NoteEditActivity.java +++ b/src/main/java/net/micode/notes/ui/NoteEditActivity.java @@ -33,12 +33,15 @@ 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; @@ -49,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; @@ -56,6 +60,7 @@ 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; @@ -67,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; @@ -75,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; @@ -143,6 +152,17 @@ public class NoteEditActivity extends Activity implements OnClickListener, private View mNoteEditorPanel; private WorkingNote mWorkingNote; + + public WorkingNote getWorkingNote() { + return mWorkingNote; + } + + /** + * 刷新便签内容 + */ + public void refreshNoteContent() { + initNoteScreen(); + } private SharedPreferences mSharedPrefs; private int mFontSizeId; @@ -167,20 +187,84 @@ 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); - // 请求通知权限 - requestNotificationPermission(); + // 请求通知权限 + requestNotificationPermission(); + + // 请求存储权限 + requestStoragePermission(); - if (savedInstanceState == null && !initActivityState(getIntent())) { + 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() { @@ -197,6 +281,49 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } } + + /** + * 请求存储权限 + */ + 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) { @@ -213,6 +340,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, } 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(); + } } } @@ -226,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; @@ -322,22 +461,48 @@ 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; } } @@ -347,52 +512,141 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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); } - // 强制显示提醒时间和图标 - mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE); - mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE); - // 调整布局,确保它们能显示出来 - mNoteHeaderHolder.tvAlertDate.setEllipsize(null); - mNoteHeaderHolder.tvAlertDate.setSingleLine(false); + + // 强制显示提醒图标 + if (mNoteHeaderHolder.ivAlertIcon != null) { + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE); + } + // 确保标题栏可见 - 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 @@ -413,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"); } @@ -457,6 +715,41 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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); @@ -489,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(); } @@ -529,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); @@ -550,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(); } } @@ -575,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) { @@ -586,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 @@ -757,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() { // 使用当前时间作为默认值 @@ -1023,7 +1559,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, } // 对于旧版本,使用 setExact - else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + 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+"); @@ -1031,9 +1567,8 @@ public class NoteEditActivity extends Activity implements OnClickListener, Log.e(TAG, "Error using setExact: " + e.getMessage()); // 失败后尝试其他方法 } - } - // 对于非常旧的版本,使用 set - else { + } else { + // 对于非常旧的版本,使用 set try { alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); Log.d(TAG, "Successfully set alarm using set for older APIs"); @@ -1041,39 +1576,41 @@ public class NoteEditActivity extends Activity implements OnClickListener, Log.e(TAG, "Error using set: " + e.getMessage()); } } + } // 最后再次尝试 setExact,作为最后的保障 try { - 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"); + 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"); - } + + // 显示设置成功的提示 + 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); @@ -1099,494 +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); } - // 保存标题 - mWorkingNote.setTitle(mNoteTitleEditor.getText().toString()); - 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() { - String text = mNoteEditor.getText().toString(); - String[] lines = text.split("\n"); - StringBuilder sb = new StringBuilder(); + private void applyUnorderedList() { + if (mNoteEditor == null) return; - 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 start = mNoteEditor.getSelectionStart(); + int end = mNoteEditor.getSelectionEnd(); + String text = mNoteEditor.getText().toString(); - mNoteEditor.setText(sb.toString()); - mNoteEditor.setSelection(sb.length()); - } - - private boolean saveNote() { - getWorkingText(); + // 获取当前行的起始位置 + int lineStart = text.lastIndexOf('\n', start - 1) + 1; - // 如果是新便签,添加创建时间到内容右下角 - if (!mWorkingNote.existInDatabase()) { - String content = mWorkingNote.getContent(); - // 格式化当前时间为年月日 - java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy年MM月dd日", java.util.Locale.getDefault()); - String createTime = sdf.format(new java.util.Date()); - // 添加创建时间到内容右下角 - if (!TextUtils.isEmpty(content)) { - content += "\n\n"; - } - content += "\n—— 创建于 " + createTime; - mWorkingNote.setWorkingText(content); - } + // 构建无序列表文本 + StringBuilder sb = new StringBuilder(text); + sb.insert(lineStart, "• "); - 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.setText(sb.toString()); + 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(" 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; @@ -126,21 +169,13 @@ 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 ImageView mSearchByTimeButton; private String mSearchQuery = ""; private long mSearchStartTime = 0; private long mSearchEndTime = 0; @@ -164,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); } @@ -197,7 +233,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt try { in.close(); } catch (IOException e) { - // TODO Auto-generated catch block e.printStackTrace(); } } @@ -226,346 +261,410 @@ 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); - mSearchByTimeButton = (ImageView) findViewById(R.id.btn_search_by_time); - - // 添加搜索文本变化监听器 - 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); - } - }); - - // 添加时间搜索按钮点击监听器 - mSearchByTimeButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - showTimeSearchDialog(); - } - }); - // 初始隐藏清除按钮 - 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 { + } 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); - mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, timeSearchUri, NoteItemData.PROJECTION, null, null, NoteColumns.PINNED + " DESC," + NoteColumns.CREATED_DATE + " DESC"); - Log.d(TAG, "Time searching from: " + mSearchStartTime + " to: " + mSearchEndTime + " in folder: " + 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; - } - // 检查是否有搜索关键词 - else if (!TextUtils.isEmpty(mSearchQuery)) { - // 有搜索关键词,使用新的搜索 URI 来同时搜索标题和内容 - Uri searchUri = Uri.parse(Notes.CONTENT_NOTE_URI + "/list_search?pattern=" + Uri.encode(mSearchQuery) + "&folder_id=" + mCurrentFolderId); - mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, searchUri, NoteItemData.PROJECTION, null, null, NoteColumns.PINNED + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); - Log.d(TAG, "Searching for: " + mSearchQuery + " in folder: " + mCurrentFolderId); + } + // 待办事项空间,只查询待办事项文件夹中的内容 + selection = NoteColumns.PARENT_ID + "=" + Notes.ID_TODO_FOLDER + " AND " + NoteColumns.DELETED + "=0"; + selectionArgs = null; + // 使用用户选择的排序方式,保持置顶优先级 + sortOrder = NoteColumns.PINNED + " DESC," + getSortOrderString(); + } else { + // 检查是否有时间搜索 + 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 { // 无搜索关键词,正常查询 @@ -574,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(); } @@ -614,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); @@ -630,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"); } @@ -668,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(); } @@ -712,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(); @@ -746,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) { @@ -754,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) { @@ -780,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(); } } @@ -949,7 +1090,519 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt 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); @@ -1016,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 } @@ -1029,7 +1681,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } public void afterTextChanged(Editable s) { - // TODO Auto-generated method stub } }); @@ -1042,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: @@ -1086,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(); - } - return true; - } + + // 隐藏新建便签按钮 + 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; + } - @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 fcd399f..5473778 100644 --- a/src/main/java/net/micode/notes/ui/NotesListAdapter.java +++ b/src/main/java/net/micode/notes/ui/NotesListAdapter.java @@ -75,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; } @@ -98,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 d274948..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); @@ -82,47 +198,86 @@ public class NotesListItem extends LinearLayout { } mTitle.setText(title); } else { - mCallName.setVisibility(View.GONE); - mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + mCallName.setVisibility(View.GONE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); - if (data.getType() == Notes.TYPE_FOLDER) { - mTitle.setText(data.getSnippet() - + context.getString(R.string.format_folder_files_count, - data.getNotesCount())); - mAlert.setVisibility(View.GONE); - } else { - // 优先显示标题,如果没有标题则显示内容摘要 - String title = data.getTitle(); - if (title == null || title.isEmpty()) { + if (data.getType() == Notes.TYPE_FOLDER) { + mTitle.setText(data.getSnippet() + + context.getString(R.string.format_folder_files_count, + data.getNotesCount())); + mAlert.setVisibility(View.GONE); + } else { + // 优先显示标题,如果没有标题则显示内容摘要 + 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()); - if (!reminderTime.isEmpty()) { - title += " " + reminderTime; - } + } + // 如果有提醒,添加剩余提醒时间 + if (data.hasAlert()) { + String reminderTime = getRemainingReminderTime(context, data.getAlertDate()); + if (!reminderTime.isEmpty()) { + title += " " + reminderTime; } - mTitle.setText(title); + } + 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); } } } - // 格式化创建时间为年月日格式显示 - java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy年MM月dd日", java.util.Locale.getDefault()); - mTime.setText(sdf.format(new java.util.Date(data.getCreatedDate()))); + // 格式化创建时间为年月日 上午/下午 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); } @@ -159,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/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 854751e..f9a2716 100644 --- a/src/main/res/layout/note_edit.xml +++ b/src/main/res/layout/note_edit.xml @@ -1,68 +1,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="56dp" + android:orientation="horizontal" + android:gravity="center_vertical" + android:paddingHorizontal="16dp"> + - - - + android:id="@+id/btn_back" + android:layout_width="40dp" + android:layout_height="40dp" + android:background="@android:color/transparent" + android:src="@android:drawable/ic_menu_revert" + android:tint="#000000" /> - + + - + + - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + 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" /> + + - - + + - + + + + + + + + + + + + + + @@ -712,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" /> @@ -733,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" /> @@ -753,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" /> + @@ -876,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" /> @@ -914,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 240d80c..850b9e3 100644 --- a/src/main/res/layout/note_list.xml +++ b/src/main/res/layout/note_list.xml @@ -15,108 +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"> + android:layout_width="16dp" + android:layout_height="16dp" + android:src="@android:drawable/ic_menu_search" + android:layout_marginRight="8dp" + android:tint="#999999" /> + + + + + + + - + + + +