扩展功能 #15

Merged
p7tupf26b merged 4 commits from jiangtianxiang_branch into master 1 month ago

@ -4,14 +4,6 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-01-19T06:52:55.276577900Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\啊?\.android\avd\Pixel_4a_API_31.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -0,0 +1,18 @@
# Fix NullPointerException in NoteInfoAdapter
The crash on startup is caused by a `NullPointerException` in `NoteInfoAdapter.java`. The `typeIcon` field in the `ViewHolder` is declared but never initialized in the `getView` method, leading to a crash when the code attempts to access `holder.typeIcon.setVisibility(...)` or `holder.typeIcon.setImageResource(...)`.
## Changes to `app/src/main/java/net/micode/notes/ui/NoteInfoAdapter.java`
1. **Initialize `typeIcon`**: In the `getView` method, inside the `if (convertView == null)` block, add the initialization for `holder.typeIcon`.
```java
// Inside getView method, if (convertView == null) block:
holder.title = convertView.findViewById(R.id.tv_title);
holder.time = convertView.findViewById(R.id.tv_time);
holder.typeIcon = convertView.findViewById(R.id.iv_type_icon); // Add this line
holder.checkBox = convertView.findViewById(android.R.id.checkbox);
holder.pinnedIcon = convertView.findViewById(R.id.iv_pinned_icon);
```
This simple fix will resolve the runtime crash and allow the application to start correctly, displaying the folder/note icons as intended.

@ -0,0 +1,36 @@
I have identified the issue: `PasswordActivity` does not properly handle the "Setup Password" flow. When the user selects a password type (PIN or Pattern), `NotesPreferenceActivity` launches `PasswordActivity`, but `PasswordActivity` might be crashing or finishing immediately due to logic errors, causing the app to fall back to the main activity.
Here is the plan to fix the issue and fully implement the password setup flow:
1. **Modify** **`PasswordActivity.java`**:
* Ensure `onCreate` correctly initializes the UI based on `ACTION_SETUP_PASSWORD` or `ACTION_CHECK_PASSWORD`.
* In `setupViews`, verify that the correct input view (`EditText` for PIN, `LockPatternView` for Pattern) is made visible and others are hidden.
* Review the `handleInput` logic for the setup phase (`mMode == 1`). It needs to correctly handle the two-step confirmation (first input -> prompt confirmation -> second input -> save).
* Add proper error handling and user feedback (Toast messages) if inputs don't match.
2. **Modify** **`NotesPreferenceActivity.java`**:
* Verify the `showSetPasswordDialog` correctly passes the `EXTRA_PASSWORD_TYPE` intent extra.
* Ensure `onActivityResult` correctly handles the result from `PasswordActivity` (e.g., refreshing the UI or showing a success message).
3. **Resource Check**:
* Ensure `activity_password.xml` contains all necessary views with correct IDs (`et_pin`, `lock_pattern_view`, `tv_prompt`, `tv_error`, `btn_cancel`).
4. **Verification**:
* Compile and run the app.
* Go to Settings -> Security Settings.
* Select "Digital Lock" (PIN). Verify the PIN entry screen appears. Enter a PIN twice. Verify success.
* Select "Gesture Lock" (Pattern). Verify the pattern view appears. Draw a pattern twice. Verify success.
* After setting a password, click Security Settings again. Verify it prompts for the old password before allowing changes.

@ -0,0 +1,28 @@
I have identified the causes for the "Security Settings" unresponsiveness and the incomplete implementation of the Privacy Lock feature. Here is the plan to fix these issues and fully implement the feature.
### 1. Fix "Security Settings" Unresponsiveness
- **File**: `app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java`
- **Issue**: The `loadSecurityPreference()` method is defined but never called.
- **Action**: Add a call to `loadSecurityPreference()` inside the `onCreate()` method.
### 2. Implement Lock Icon Display in List
- **File**: `app/src/main/java/net/micode/notes/ui/NoteInfoAdapter.java`
- **Issue**: The lock icon (`lockIcon`) is defined in the ViewHolder but its visibility is never set in `getView()`.
- **Action**: Add logic in `getView()` to set `holder.lockIcon` visibility to `VISIBLE` if `note.isLocked` is true, otherwise `GONE`.
### 3. Implement Password Protection Logic
- **File**: `app/src/main/java/net/micode/notes/ui/NotesListActivity.java`
- **Issue**: Clicking on a locked note or folder currently opens it directly, bypassing any security check.
- **Action**:
- Modify `handleItemClick` (and/or `onNoteItemClick`) to check `note.isLocked`.
- If locked, launch `PasswordActivity` with `ACTION_CHECK_PASSWORD`.
- Store the target note in `mPendingNoteToOpen`.
- Implement `onActivityResult` to handle the success result from `PasswordActivity` and proceed to open the note/folder.
### 4. Verification
- **Compile**: Ensure all changes compile without errors.
- **Test**:
- Verify clicking "Security Settings" now opens the password setup dialog.
- Verify locking a note shows the lock icon.
- Verify clicking a locked note prompts for a password.
- Verify entering the correct password opens the note.

@ -0,0 +1,30 @@
# Update Toolbar Title and Menu Handling in NotesListActivity
To address the user's request of changing the toolbar title to "Trash" when entering the Trash folder and back to "Notes" when returning, I will modify `NotesListActivity.java`.
The current implementation only updates the toolbar title during initialization and mode switching (Multi-select/Normal), but not when navigating between folders.
## Proposed Changes
### `app/src/main/java/net/micode/notes/ui/NotesListActivity.java`
1. **Modify** **`updateToolbarForNormalMode()`**:
* Update the title setting logic to use `R.string.menu_trash` for the Trash folder instead of the hardcoded "回收站".
* Add logic to re-inflate `R.menu.note_list` when in Normal mode, as `toolbar.getMenu().clear()` removes the menu items. This ensures the menu remains visible after folder navigation.
2. **Update** **`observeViewModel()`**:
* In the `notesLiveData` observer, add a call to `updateToolbarForNormalMode()`. This ensures that whenever the note list is refreshed (which happens on folder navigation), the toolbar title and menu are updated to reflect the current state.
## Verification
After applying these changes:
* Navigate to "Trash" from the sidebar -> Title should update to "Trash" (from `strings.xml`).
* Navigate back to "Notes" -> Title should update to "Notes".
* The options menu (Search, New Folder, etc.) should remain visible in "Notes" mode and be hidden (or appropriate) in "Trash" mode.

@ -0,0 +1,59 @@
# 便签置顶功能 UML 类图重绘计划
## 1. 目标
根据用户提供的UML类图规范图片强调类名、属性、方法的标准表示以及类间关系的正确符号使用 PlantUML 语言重新绘制“便签置顶功能”的类图。
## 2. 规范要求分析
* **类表示**:
* **类名**: 顶部居中,粗体。
* **属性**: `[可见性] 名称 [: 类型] [= 初值]`
* **方法**: `[可见性] 名称([参数表]) [: 返回类型]`
* **可见性符号**: `+` (public), `-` (private), `#` (protected)。
* **接口表示**: 使用 `<<interface>>` 构造型或棒棒糖图(此处选用构造型以展示方法细节)。
* **关系表示**:
* **依赖 (Dependency)**: 虚线箭头 (`..>`),表示使用关系。
* **关联 (Association)**: 实线箭头 (`-->`),表示拥有关系。
* **实现 (Realization)**: 虚线空心三角箭头 (`..|>`)。
* **泛化/继承 (Generalization)**: 实线空心三角箭头 (`--|>`)。
* **聚合 (Aggregation)**: 空心菱形 (`o--`)。
* **组合 (Composition)**: 实心菱形 (`*--`)。
## 3. 涉及的类与接口
我们将重点展示置顶功能相关的类及其核心成员。
1. **NotesListActivity** (UI层)
* 属性: `viewModel: NotesListViewModel`, `adapter: NoteInfoAdapter`
* 方法: `onOptionsItemSelected(item: MenuItem): boolean`, `updateToolbarForMultiSelectMode()`
2. **NotesListViewModel** (ViewModel层)
* 属性: `repository: NotesRepository`
* 方法: `toggleSelectedNotesPin()`, `isAllSelectedPinned(): boolean`
3. **NotesRepository** (Data层)
* 方法: `batchTogglePin(noteIds: List<Long>, isPinned: boolean)`, `queryNotes(folderId: long): List<NoteInfo>`
4. **NoteInfoAdapter** (UI适配器)
* 方法: `bindView(view: View, context: Context, cursor: Cursor)`
5. **Notes** (常量/契约类)
* 包含内部接口 `NoteColumns`
6. **NoteColumns** (接口)
* 属性: `TOP: String = "top"`
7. **NoteInfo** (实体类)
* 属性: `isPinned: boolean`
## 4. 关系设计
* `NotesListActivity` **关联** (`-->`) `NotesListViewModel` (持有ViewModel实例)。
* `NotesListActivity` **关联** (`-->`) `NoteInfoAdapter` (持有Adapter实例)。
* `NotesListViewModel` **关联** (`-->`) `NotesRepository` (持有Repository实例)。
* `NotesRepository` **依赖** (`..>`) `NoteInfo` (作为返回类型)。
* `NotesRepository` **依赖** (`..>`) `NoteColumns` (使用其常量)。
* `NoteInfoAdapter` **依赖** (`..>`) `NoteInfo` (绑定数据时使用)。
## 5. PlantUML 代码生成计划
编写 PlantUML 代码,精确定义上述类、属性、方法及关系,并添加必要的注释或构造型以符合规范。
## 6. 交付物
一段完整的 PlantUML 代码。

@ -0,0 +1,43 @@
# 便签置顶功能 UML 顺序图重绘计划
## 1. 目标
根据用户提供的UML顺序图规范图片强调对象、生命线、活跃期、消息传递的正确表示使用 PlantUML 语言重新绘制“便签置顶功能”的顺序图。
## 2. 规范要求分析
* **对象 (Object)**: 使用 `[对象名] : [类名]` 格式,例如 `viewModel : NotesListViewModel`
* **生命线 (Lifeline)**: 虚线表示。
* **活跃期 (Activation Bar)**: 细长矩形,表示对象正在执行操作。
* **消息 (Message)**:
* **同步消息**: 实线实心箭头 (`->`)。
* **异步消息**: 实线箭头 (`->>`),虽然通常用开放箭头,但在 PlantUML 中可以用不同箭头区分,这里主要涉及数据库操作,我们假设 Repository 操作是异步提交或回调模式,但在顺序图中为了清晰表达逻辑流,常表现为调用-返回结构。
* **返回消息**: 虚线开放箭头 (`-->>`)。
* **自消息**: 指向自身的实线箭头。
* **格式**: 严格遵循 UML 2.0 标准。
## 3. 参与对象
1. `User` (用户)
2. `activity : NotesListActivity` (界面层)
3. `viewModel : NotesListViewModel` (业务逻辑层)
4. `repository : NotesRepository` (数据仓库层)
5. `db : SQLiteDatabase` (底层数据库)
## 4. 交互流程设计
1. 用户点击“置顶”按钮。
2. `activity` 调用 `viewModel.toggleSelectedNotesPin()`
3. `viewModel` 计算新的置顶状态。
4. `viewModel` 调用 `repository.batchTogglePin()`
5. `repository` 执行 `db.update()` 更新数据库。
6. 数据库返回更新行数。
7. `repository` 通过回调通知 `viewModel` 成功。
8. `viewModel` 随即调用 `repository.queryNotes()` 刷新数据。
9. `repository` 查询数据库(包含排序逻辑)。
10. `db` 返回游标结果。
11. `repository` 将结果转换为列表并返回给 `viewModel`
12. `viewModel` 更新 LiveData通知 `activity`
13. `activity` 显示 Toast 并刷新列表 UI。
## 5. PlantUML 代码生成计划
我将编写一段 PlantUML 代码,精确描述上述流程,并包含所有必要的 UML 元素(生命线、激活条、消息类型)。
## 6. 交付物
一段完整的 PlantUML 代码,用户可以将其复制到在线编辑器(如 PlantText 或 PlantUML Server中生成符合规范的图片。

@ -0,0 +1,71 @@
# 回收站功能实现计划书
## 1. 现状分析
* **后端支持**: `NotesRepository.deleteNote` 已实现将笔记 `PARENT_ID` 设置为 `Notes.ID_TRASH_FOLER`,即后端已支持“软删除”。
* **UI 缺失**: `NotesListActivity.onTrashSelected` 目前为空实现 (`TODO`),点击 Trash 按钮没有任何反应或直接关闭侧栏。
* **逻辑缺失**: 缺少查询回收站笔记、恢复笔记、永久删除笔记的专用接口和 UI 交互。
## 2. 详细设计
### 2.1 数据层 (`NotesRepository`)
虽然现有的 `queryNotes` 可以查询指定 `folderId` 的笔记,但回收站的查询逻辑可能略有不同(不需要区分文件夹层级,直接列出所有在 Trash 中的项)。
* **新增方法**: `getTrashNotes(Callback<List<NoteInfo>> callback)`
* 查询 `PARENT_ID = Notes.ID_TRASH_FOLER` 的所有记录。
* **新增方法**: `restoreNotes(List<Long> noteIds, Callback<Integer> callback)`
* 将笔记的 `PARENT_ID` 恢复为 `Notes.ID_ROOT_FOLDER`(简化处理,恢复到根目录,避免原文件夹已删除的情况)。
* **新增方法**: `emptyTrash(Callback<Integer> callback)`
* 永久删除回收站中的所有笔记。
* **新增方法**: `deleteNotesForever(List<Long> noteIds, Callback<Integer> callback)`
* 从数据库中物理删除记录 (`contentResolver.delete`)。
### 2.2 界面层 (`NotesListActivity` & `NotesListViewModel`)
为了复用现有的列表 UI我们可以在 `NotesListActivity` 中通过加载不同的数据源来展示回收站内容,或者创建一个新的 `TrashActivity`。鉴于保持架构一致性,建议**复用 `NotesListActivity`**,通过进入特殊的 `Notes.ID_TRASH_FOLER` 文件夹来实现。
* **进入回收站**:
* 修改 `onTrashSelected`,调用 `viewModel.loadNotes(Notes.ID_TRASH_FOLER)`
* 更新 Toolbar 标题为“回收站”。
* 更新菜单,隐藏“新建”按钮,显示“清空回收站”选项。
* **列表项点击**:
* 在回收站模式下,点击笔记不应打开编辑器,而应弹出 **“恢复/删除”选择对话框**(根据用户需求)。
* **恢复/删除交互**:
* 用户点击 Trash 按钮 -> 进入回收站模式。
* 列表显示已删除项。
* 长按或点击某项 -> 弹出对话框:
* 标题:操作确认
* 内容:要恢复还是永久删除此笔记?
* 按钮1**恢复** (Neutral/Positive) -> 移回根目录。
* 按钮2**永久删除** (Negative, 深色/红色警告) -> 物理删除。
* 提供“清空回收站”菜单项。
### 2.3 决策点补充
1. **恢复位置**: 用户提到“返回到原来删除的位置”。
* **难点**: 如果原文件夹也被删除了怎么办?
* **解决方案**: 尝试恢复到 `ORIGIN_PARENT_ID`(如果数据库有记录),如果原父文件夹不存在,则回退到根目录。当前数据库结构似乎没有 `ORIGIN_PARENT_ID` 字段。
* **折衷方案**: 暂时恢复到根目录或者在删除时记录原父ID需要修改数据库结构
* **本次实现**: 为了不修改过多底层结构,**默认恢复到根目录**,或者如果技术允许,检查 `parentId` 是否在回收站中,如果不在则保持原 `parentId`(但目前删除是修改 `parentId` 为 trash_id所以原 ID 丢失了)。
* **改进**: 鉴于 `deleteNote` 已经把 `PARENT_ID` 改写了,我们需要在删除前保存原 ID或者接受恢复到根目录的设定。**决定:本次恢复到根目录 (ID_ROOT_FOLDER)**。
## 3. 实施步骤
### Step 1: 完善数据层
1. 在 `NotesRepository` 中添加 `restoreNotes``deleteNotesForever` 方法。
2. 修改 `queryNotes`,确保能正确查询 `ID_TRASH_FOLER` 下的内容(现有逻辑可能排除了系统文件夹,需检查 `NORMAL_SELECTION`)。
### Step 2: 完善 ViewModel
1. 添加 `restoreSelectedNotes()``deleteSelectedNotesForever()` 方法。
2. 添加 `isTrashMode()` 状态判断,用于 UI 区分展示。
### Step 3: 更新 UI (`NotesListActivity`)
1. 实现 `onTrashSelected`:调用 `viewModel.enterFolder(Notes.ID_TRASH_FOLER)`
2. 修改 `updateToolbar`:如果当前文件夹是 Trash标题设为“回收站”隐藏 FAB菜单变为“清空”。
3. 修改 `onNoteItemClick`:如果在 Trash 模式,点击弹出恢复/删除对话框。
4. 实现对话框逻辑:
* **恢复**: 调用 ViewModel 恢复方法 -> 刷新列表 -> Toast "已恢复"。
* **永久删除**: 弹出二次确认(“确定”深色按钮) -> 调用 ViewModel 物理删除 -> 刷新列表。
## 4. 验证计划
1. 删除一条笔记,确认在普通列表消失。
2. 进入侧栏 -> Trash确认能看到该笔记。
3. 点击该笔记,弹出对话框。
4. 点击“恢复”,确认笔记回到普通列表。
5. 再次删除并进入 Trash点击“永久删除”确认笔记彻底消失。

@ -0,0 +1,62 @@
# 图标不显示问题修复与方案改进计划
## 1. 问题分析
根据用户反馈和截图:
1. **图标未显示**: 文件夹左侧没有出现预期的图标。
2. **时间显示异常**: 普通便签的时间也没有正确显示(可能是因为字体颜色、大小问题,或者被隐藏了)。
3. **截图观察**: 截图显示列表项背景色正常,有标题,右侧有时间(如 "2026-01-20 09:52"),但所有项(包括置顶的“软件工程”)左侧都没有图标。置顶图标(向上箭头)出现在了右下角。
**潜在原因**:
1. **`R.drawable.folder` 不存在**: 我之前假设系统资源中有 `folder`,但实际上可能没有,导致 `setImageResource` 失败或者抛出异常被捕获(虽然 `setImageResource` 不会抛异常,但会留空)。应该使用系统内置的 `android.R.drawable.ic_menu_view` 或者确保项目中有 `folder.png/xml`
2. **`View` 复用问题**: `ViewHolder` 模式下,如果状态重置不彻底,可能导致图标在不需要的时候显示,或者在需要的时候被隐藏。
3. **布局属性**: `iv_type_icon``visibility` 默认为 `gone`,可能在代码中没有正确设置为 `VISIBLE`
4. **数据类型判断错误**: `note.type` 的值可能不是预期的 `Notes.TYPE_FOLDER`。需要确认 `Notes.TYPE_FOLDER` 的常量值。
## 2. 改进方案
### 2.1 资源确保
* 不依赖系统不确定的 `R.drawable.folder`
* 使用 Android 系统自带的 **`android.R.drawable.ic_menu_view`** (通常是一个网格图标,不太像文件夹) 或者 **`android.R.drawable.ic_menu_more`**。
* **最佳方案**: 既然是 Android Studio 环境,我们可以直接使用 Vector Asset 创建一个标准的文件夹图标 `ic_folder.xml`。如果没有,暂时使用 `android.R.drawable.ic_menu_manage` (通常是文件夹样式) 或 `android.R.drawable.star_on` (临时测试用)。为了稳妥,我将检查 `res/drawable` 目录。
### 2.2 代码逻辑增强
* 在 `getView` 中增加日志,打印 `note.type``position`,确认是否进入了文件夹判断逻辑。
* 强制设置 `holder.typeIcon.setVisibility(View.VISIBLE)` 当检测到文件夹时。
* 对于普通便签,确保 `holder.time.setVisibility(View.VISIBLE)`
### 2.3 视觉优化
* 截图显示置顶图标在右下角,这可能是之前的修改。现在的需求是左侧区分图标。
* **文件夹**: 左侧显示图标,右侧时间显示为 "文件夹" 或包含项数。
* **便签**: 左侧无图标(或显示文档图标),右侧显示时间。
## 3. 实施步骤
### Step 1: 检查资源
1. 列出 `res/drawable` 目录,看是否有合适的图标。
2. 如果没有,我将创建一个简单的 Vector Drawable `ic_folder.xml`
### Step 2: 修改 `NoteInfoAdapter.java`
1. 修改图标资源引用:确保引用存在的资源。
2. 增加调试日志。
3. 修正 `getView` 逻辑:
```java
if (note.type == Notes.TYPE_FOLDER) {
holder.typeIcon.setVisibility(View.VISIBLE);
holder.typeIcon.setImageResource(R.drawable.ic_folder); // 确保此资源存在
holder.time.setText(""); // 或者显示包含数量
holder.time.setVisibility(View.GONE);
} else {
holder.typeIcon.setVisibility(View.GONE);
holder.time.setVisibility(View.VISIBLE);
holder.time.setText(formatDate(note.modifiedDate));
}
```
### Step 3: 验证便签时间显示
* 截图中其实**显示了时间**(右侧灰色小字),用户说“没有显示最后修改时间”可能是指**文件夹**没有显示时间(这是符合设计的),或者是**普通便签**的时间显示不符合他的预期(比如格式不对,或者因为复用问题导致有时不显示)。
* 我会检查 `formatDate` 方法,并确保在 `else` 分支中显式 `setVisible(VISIBLE)`
## 4. 立即行动
1. 检查 `res/drawable`
2. 创建 `ic_folder.xml` (如果需要)。
3. 更新 Adapter 代码。

@ -0,0 +1,69 @@
# 小米便签 (MiNotes) 项目拓展计划书
## 1. 项目背景与目标
本项目旨在对开源项目“小米便签”进行现代化重构与功能拓展。当前版本虽然基础功能完备,但在架构设计、用户体验及系统兼容性方面存在滞后。本计划致力于打造一款符合现代 Android 开发标准、具备高扩展性且功能丰富的笔记应用。
## 2. 现状分析
### 2.1 架构分析
* **混合架构**: 目前处于 Legacy (Activity + God Class) 向 MVVM 过渡的阶段。`WorkingNote` 类承担了过多的职责UI 回调、数据存储、同步状态),导致维护困难。
* **数据层**: 依赖 `ContentProvider` 和原生 SQLite虽然利于跨应用共享但缺乏 ORM 的便捷性SQL 拼接容易出错。
* **同步机制**: `GTaskManager` 高度耦合 Google Tasks API难以扩展其他云服务。
### 2.2 现有功能
* 基础笔记(文本/清单)、文件夹管理、桌面小部件、定时提醒。
* 特色功能通话记录关联笔记、Google Task 同步。
### 2.3 兼容性风险
* 使用已废弃的 `AsyncTask`
* 未适配 Android 12+ 的精确闹钟权限 (`SCHEDULE_EXACT_ALARM`)。
* 未适配 Android 10+ 的分区存储 (Scoped Storage)。
## 3. 功能拓展规划
### Phase 1: 现代化重构与兼容性适配 (基础夯实)
**目标**: 确保应用在最新 Android 系统上稳定运行,并降低技术债务。
1. **移除过时 API**: 将 `AsyncTask` 替换为 **Kotlin Coroutines**`java.util.concurrent`
2. **权限适配**:
* 适配 Android 13+ 通知权限。
* 适配 Android 12 精确闹钟权限。
* 适配分区存储 (Scoped Storage),优化备份/恢复功能。
3. **架构解耦**:
* 将 `WorkingNote` 的 UI 逻辑剥离,下沉为纯数据模型或 Repository 方法。
* 为 `NoteEditActivity` 引入 `ViewModel`
### Phase 2: 核心功能增强 (实用性提升)
**目标**: 提升笔记的编辑体验和组织效率。
1. **富文本/Markdown 支持**:
* 扩展数据库 `data` 表的 `mime_type` 字段,支持 Markdown 语法。
* 集成 Markdown 渲染引擎(如 Markwon
2. **多媒体附件**:
* 支持插入图片、录音。
* 设计新的附件表 `attachments`,与 `note` 表通过 ID 关联。
3. **标签系统 (Tags)**:
* 突破单一文件夹的限制,实现多对多标签管理。
* 新增 `tags` 表和 `note_tags` 关联表。
### Phase 3: 高级特性与云同步 (生态扩展)
**目标**: 打破数据孤岛,提供灵活的同步选项。
1. **同步层抽象**:
* 定义 `SyncProvider` 接口,解耦 `GTaskManager`
* 实现 **WebDAV** 协议支持可连接坚果云、Nextcloud
* 实现本地自动备份策略。
2. **全局搜索增强**:
* 利用 SQLite FTS4/5 模块实现高性能全文检索。
* 支持按标签、时间、类型的组合搜索。
### Phase 4: UI/UX 全面升级 (视觉焕新)
**目标**: 符合 Material Design 3 设计规范。
1. **Material You**: 支持动态取色主题。
2. **深色模式**: 完善的 Dark Mode 适配。
3. **交互优化**: 引入侧滑删除、拖拽排序等现代交互手势。
## 4. 技术选型建议
* **语言**: 逐步迁移至 **Kotlin**(推荐)或继续优化 Java。
* **架构**: MVVM (Jetpack ViewModel + LiveData/Flow)。
* **数据库**: 考虑引入 **Room** 逐步接管原生 SQLite 操作。
* **构建工具**: 升级 Gradle 插件,统一依赖管理 (Version Catalog)。
## 5. 决策点 (需确认)
* **富文本方案**: 选择 Markdown适合极客/程序员)还是富文本编辑器(适合普通用户)?建议优先 Markdown。
* **同步策略**: 是否保留 Google Tasks考虑到国内访问问题建议将 **WebDAV** 设为首选同步方式。

@ -0,0 +1,116 @@
# 小米便签 (MiNotes) 功能拓展详细规划书 (v3.0 - 全面增强版)
基于市场调研和对主流笔记应用Notion, Obsidian, Flomo, Heptabase 等)的深度分析,本规划书在 v2.0 的基础上大幅扩充了功能池,旨在打造一款**全能型、离线优先、极具差异化**的笔记应用。
我们将新增功能分为四个维度:**效率与工作流**、**知识管理与可视化**、**创意与多媒体**、**隐私与安全**。
---
## 1. 效率与工作流 (Productivity & Workflow)
*定位:让笔记不仅是记录,更是行动的开始。*
### 1.1 专注模式 (Focus Mode / Pomodoro)
* **功能**: 在编辑笔记时,提供一个倒计时番茄钟。开启后,屏蔽应用内干扰,全屏显示编辑区和倒计时。
* **场景**: 写作、深度思考、复习笔记。
* **开发难度**: 低。
### 1.2 悬浮窗速记 (Floating Quick Note)
* **功能**: 在其他应用之上显示一个半透明悬浮球,点击后展开一个小窗口快速输入文字或语音,无需切换应用。
* **场景**: 阅读文章、看视频、通话时快速记录灵感。
* **开发难度**: 中(需处理 WindowManager 权限)。
### 1.3 每日回顾 (Daily Review / Journaling)
* **功能**: 自动创建一个以“日期”命名的笔记,聚合当天的所有操作(新建笔记、完成待办、通话记录)。提供“那年今日”回顾功能。
* **场景**: 写日记、工作日报、复盘。
* **开发难度**: 中。
### 1.4 模板中心 (Template Center)
* **功能**: 提供预设模板如康奈尔笔记法、SWOT 分析、会议纪要),并允许用户自定义模板。
* **场景**: 快速开始标准化记录。
* **开发难度**: 低。
---
## 2. 知识管理与可视化 (Knowledge & Visualization)
*定位:将碎片化信息重组为结构化知识。*
### 2.1 看板视图 (Kanban View)
* **功能**: 将笔记或待办事项以卡片形式展示在“未开始”、“进行中”、“已完成”等列中,支持拖拽流转。
* **场景**: 项目管理、任务进度追踪。
* **开发难度**: 高。
### 2.2 日历视图 (Calendar View)
* **功能**: 在日历上展示笔记(按创建/修改时间或提醒时间),支持在日历上直接点击日期创建笔记。
* **场景**: 行程规划、回顾历史记录。
* **开发难度**: 中。
### 2.3 知识图谱 (Knowledge Graph)
* **功能**: 基于双向链接(`[[Link]]`),用节点和连线可视化展示笔记之间的引用关系。支持缩放、拖拽节点。
* **场景**: 发现知识盲区,寻找灵感连接。
* **开发难度**: 极高(需引入图形渲染库)。
### 2.4 标签墙与嵌套标签 (Tag Hierarchy)
* **功能**: 支持多级标签(如 `工作/项目A/会议`),并提供一个标签云或标签墙视图进行筛选。
* **场景**: 多维度管理笔记,突破文件夹限制。
* **开发难度**: 中。
---
## 3. 创意与多媒体 (Creativity & Media)
*定位:释放表达欲,记录不设限。*
### 3.1 手写与绘图 (Handwriting & Sketch)
* **功能**: 提供画笔工具,支持在笔记中插入手写区域或涂鸦。支持压感(如果设备支持)。
* **场景**: 绘制草图、数学公式、手写签名。
* **开发难度**: 高。
### 3.2 语音笔记与时间戳 (Voice Memo & Timestamp)
* **功能**: 录音的同时可以打字记录。回放录音时,点击文字可跳转到对应的录音进度(类似 Notability
* **场景**: 会议记录、课堂录音。
* **开发难度**: 高。
### 3.3 闪念胶囊 (Flash Card / Flomo-like)
* **功能**: 类似 Flomo 的卡片式记录,强调短内容、高频次。支持随机漫游回顾。
* **场景**: 捕捉转瞬即逝的灵感。
* **开发难度**: 低。
---
## 4. 隐私与安全 (Privacy & Security)
*定位:数据主权,绝对安全。*
### 4.1 生物识别锁 (Biometric Lock)
* **功能**: 支持指纹或面部解锁进入应用,或锁定特定文件夹/笔记。
* **场景**: 保护日记、账号密码等敏感信息。
* **开发难度**: 低(使用 Android BiometricPrompt API
### 4.2 本地加密存储 (Local Encryption)
* **功能**: 使用 AES-256 对数据库和附件文件进行加密存储。即使手机丢失,导出文件也无法被破解。
* **场景**: 极高安全需求。
* **开发难度**: 中(需引入 SQLCipher
### 4.3 隐身模式 (Stealth Mode)
* **功能**: 在多任务界面模糊显示应用内容;提供一个“伪装密码”,输入后进入一个空的或预设的伪装空间。
* **场景**: 极致隐私保护。
* **开发难度**: 中。
---
## 5. 推荐实施路线 (Roadmap v3.0)
**Phase 1: 基础体验增强 (Foundation)**
* **回收站** (现有计划)
* **深色模式** (现有计划)
* **生物识别锁**: 增加安全感,实现简单,价值高。
* **模板功能**: 提升记录效率。
**Phase 2: 核心生产力 (Core Productivity)**
* **本地备份与导出** (现有计划)
* **Markdown 支持** (现有计划)
* **悬浮窗速记**: 扩展记录场景。
* **日历视图**: 提供新的时间维度视角。
**Phase 3: 高级与创新 (Innovation)**
* **离线 OCR** (现有计划)
* **看板视图**: 引入项目管理能力。
* **双向链接 & 知识图谱**: 打造知识库神器的核心。

@ -0,0 +1,119 @@
# 小米便签 (MiNotes) 功能拓展详细规划书 (v2.0 - 离线优先版)
本规划书针对 Android Studio 部署环境Pixel 4a, Android 12, API 31进行了深度优化特别强调**离线可用性**。所有功能设计均优先考虑本地处理能力,仅在必要时使用网络功能,确保在无网络环境下依然具备完整的核心体验。
## 1. 基础功能拓展 (Basic Features)
*定位:补齐短板,优化现有体验,利用现有架构快速实现,完全本地化。*
### 1.1 回收站 (Recycle Bin) - **[高优先级]**
* **功能描述**: 提供一个专门的“回收站”文件夹或页面,展示所有被用户删除的笔记。用户可以在此选择“恢复”笔记到原文件夹,或“彻底删除”以释放空间。
* **离线适配**: 完全基于本地 SQLite 数据库操作,无需网络。
* **技术现状**: 后端 `NotesRepository.deleteNote` 已实现将笔记移动到系统文件夹 `Notes.ID_TRASH_FOLER` 的逻辑。目前缺失的是前端展示界面及恢复/清除逻辑。
* **用户价值**: 防止误删,提供后悔药。
* **开发难度**: 低UI 开发为主)。
### 1.2 Markdown 编辑支持
* **功能描述**: 将编辑器升级为支持 Markdown 语法的富文本编辑器。支持 `# 标题`、`**粗体**`、`- 列表`、`> 引用` 等基础格式,并提供实时预览或编辑/预览切换模式。
* **离线适配**: 使用本地 Markdown 解析库(如 Markwon所有渲染在设备端完成。
* **技术现状**: 现有 `NoteEditActivity` 仅支持纯文本和基础 CheckList。需引入 Markdown 解析与渲染库。
* **用户价值**: 提升排版效率,满足极客与程序员群体的需求。
* **开发难度**: 中。
### 1.3 深色模式 (Dark Mode) 完善
* **功能描述**: 全面适配 Android 系统的深色模式DayNight theme。确保应用在夜间使用时不刺眼且符合系统统一的视觉规范。
* **离线适配**: 基于 Android 原生资源系统 (`res/values-night`),完全本地运行。
* **技术现状**: 资源目录中已有 `values-night`,但需全面检查自定义 View、图标颜色和背景色在深色模式下的表现。
* **用户价值**: 护眼省电OLED屏幕提升夜间使用体验。
* **开发难度**: 低。
### 1.4 增强清单模式 (Enhanced Checklist)
* **功能描述**: 优化待办事项清单的交互。支持长按拖拽排序、完成项自动下沉到底部、一键清除已完成项。
* **离线适配**: 纯本地交互逻辑。
* **技术现状**: 现有清单模式较为基础,交互单一。
* **用户价值**: 提升任务管理效率。
* **开发难度**: 中。
---
## 2. 高阶功能拓展 (Advanced Features)
*定位:提升生产力,增加用户粘性,侧重于本地数据的高级处理。*
### 2.1 本地备份与导出 (Local Backup & Export)
* **功能描述**:
* **自动备份**: 定时将所有笔记打包备份到手机本地存储(如 `/Documents/MiNotes/Backup`)。
* **格式导出**: 支持将笔记导出为 `.txt`, `.md` (Markdown), 或 `.pdf` 文件,方便用户通过 USB 或蓝牙传输。
* **离线适配**: 替代依赖网络的云同步,确保数据在无网环境下也能安全备份和迁移。
* **技术实现**: 使用 Android `Storage Access Framework` (SAF) 进行文件读写。
* **用户价值**: 数据绝对安全,完全掌握在用户手中,无惧断网。
* **开发难度**: 中。
### 2.2 离线 OCR 文字识别
* **功能描述**: 集成离线光学字符识别功能。用户拍摄文档或插入图片后,应用自动提取图片中的文字并转换为可编辑的笔记内容。
* **离线适配**: 使用 **Google ML Kit (On-device API)**,模型包随应用下发或首次安装时下载,识别过程完全不需要网络连接。
* **技术实现**: 集成 `com.google.mlkit:text-recognition`
* **用户价值**: 快速数字化纸质文档、板书、名片,且保护隐私。
* **开发难度**: 中。
### 2.3 双向链接 (Bi-directional Linking)
* **功能描述**: 支持类似 Obsidian 的双链功能。用户输入 `[[` 即可触发联想,引用另一篇笔记。笔记底部显示“反向链接”列表。
* **离线适配**: 链接解析和索引建立完全在本地数据库中进行。
* **技术实现**: 需在保存笔记时解析内容链接,维护一张本地引用关系表。
* **用户价值**: 构建个人知识库,让笔记不再孤立。
* **开发难度**: 高。
### 2.4 桌面小部件 (Widget) 高级定制
* **功能描述**: 提供高度可定制的 Widget。支持调整透明度、背景色、字体大小支持滚动查看长列表支持在桌面直接勾选完成待办事项。
* **离线适配**: Android 原生 AppWidget 机制。
* **技术现状**: 现有 Widget 功能较简单。
* **用户价值**: 提升信息触达效率,美化桌面。
* **开发难度**: 中。
---
## 3. 创新功能拓展 (Innovative Features)
*定位:差异化竞争,利用设备端算力和传感器技术打造亮点。*
### 3.1 离线语义搜索 (Offline Semantic Search)
* **功能描述**: 不仅支持关键词匹配,还能理解语义。例如搜索“旅游计划”,能搜出包含“订酒店”、“买机票”但没有“旅游”二字的笔记。
* **离线适配**: 引入轻量级本地向量数据库(如 ObjectBox 或 SQLite VSS和端侧嵌入模型如 TensorFlow Lite 模型)。
* **亮点**: 在无网环境下也能获得接近在线搜索的智能体验。
* **开发难度**: 极高。
### 3.2 无限画布 (Infinite Canvas)
* **功能描述**: 提供一种非线性的“白板”视图。用户可以在一张无限大的画布上自由摆放便签、图片、手绘线条,并建立连线关系。
* **离线适配**: 所有绘图数据存储在本地。
* **技术实现**: 自定义 View 或 SurfaceView 绘图引擎。
* **用户价值**: 适合头脑风暴、思维导图绘制、复杂项目规划。
* **开发难度**: 极高。
### 3.3 上下文感知提醒 (Contextual Reminders)
* **功能描述**: 超越传统的时间提醒。
* **位置提醒**: 当用户到达“超市”时,自动弹出“购物清单”笔记(基于 GPS/Geofencing
* **Wi-Fi 提醒**: 当手机连接“公司 Wi-Fi”时弹出“工作周报”笔记。
* **离线适配**: 位置围栏和 Wi-Fi 状态检测均由 Android 系统服务在本地提供,无需连接服务器。
* **技术实现**: 利用 Android Geofencing API 和 ConnectivityManager。
* **用户价值**: 在最合适的时间和地点提供最需要的信息。
* **开发难度**: 中。
---
## 4. 推荐实施路线 (Roadmap) - 离线优先策略
### Phase 1: 基础建设 (Current Focus)
* **目标**: 修复遗留问题,补全缺失的基础 UI确保核心功能闭环。
* **任务**:
1. 实现 **回收站 (Recycle Bin)** 的前端界面。
2. 完成 **深色模式** 的全面适配。
3. 重构异步任务代码 (AsyncTask -> Coroutines) 以适配 Android 12。
### Phase 2: 核心体验升级
* **目标**: 提升编辑和数据安全体验。
* **任务**:
1. 实现 **本地备份与导出** (替代网络同步)。
2. 实现 **Markdown** 编辑支持。
### Phase 3: 智能化与创新
* **目标**: 利用端侧能力打造差异化。
* **任务**:
1. 集成 **离线 OCR** 功能。
2. 探索 **双向链接** 或 **无限画布**。

@ -0,0 +1,77 @@
# 小米便签 (MiNotes) 功能拓展详细规划书
根据对现代主流笔记应用Notion, Obsidian, Google Keep 等)的调研,结合小米便签当前的代码现状(基础架构完备,但技术栈较老旧),我为您设计了以下三个层次的拓展计划。
## 1. 基础功能拓展 (Essential Features)
*定位:补齐短板,优化现有体验,利用现有架构快速实现。*
### 1.1 回收站 (Recycle Bin) - **[高优先级]**
* **描述**: 目前代码中后端已支持将笔记移动到 `ID_TRASH_FOLER`,但缺少前端 UI。需要实现一个专门的回收站页面展示已删除笔记支持“彻底删除”和“恢复”。
* **现状**: `NotesRepository.deleteNote` 已实现逻辑UI 入口 `onTrashSelected` 为 TODO。
* **工作量**: 中等(主要为 UI 开发)。
### 1.2 Markdown 编辑支持
* **描述**: 将纯文本编辑升级为 Markdown 编辑器,支持标题、粗体、列表、代码块等基础语法高亮和预览。
* **现状**: `NoteEditActivity` 仅支持纯文本。需引入 Markdown 解析库(如 Markwon
* **扩展性**: 数据库 `snippet` 字段可直接存储 Markdown 文本,兼容性好。
### 1.3 深色模式 (Dark Mode) 完善
* **描述**: 跟随系统或手动切换深色/浅色主题,适配 OLED 屏幕省电需求。
* **现状**: `res/values-night` 资源目录已存在,但可能不完整。需要全面检查自定义 View 的颜色适配。
### 1.4 增强清单模式 (Advanced Checklist)
* **描述**: 增加清单项的拖拽排序、已完成项自动下沉/划线功能。
* **现状**: 现有清单模式较为基础。
## 2. 高阶功能拓展 (Advanced Features)
*定位:提升生产力,增加用户粘性,需要引入新的技术栈。*
### 2.1 WebDAV 云同步
* **描述**: 替代过时的 Google Tasks 同步,支持标准的 WebDAV 协议坚果云、Nextcloud、NAS实现跨设备数据备份与同步。
* **现状**: 现有 `GTaskSyncService` 依赖废弃的 `AsyncTask` 且仅支持 GTask。需重构同步模块。
### 2.2 OCR 文字识别
* **描述**: 拍摄或插入图片后,自动识别图片中的文字并转换为笔记内容。
* **实现**: 可集成 Google ML Kit (离线/免费) 实现端侧 OCR。
### 2.3 双向链接 (Bi-directional Linking)
* **描述**: 支持使用 `[[笔记标题]]` 语法引用其他笔记,构建知识网络。
* **实现**: 需要在保存笔记时解析链接,建立引用索引表。
### 2.4 桌面小部件 (Widget) 高级定制
* **描述**: 提供更多尺寸、透明度调节、字体颜色选择的 Widget。支持在 Widget 上直接勾选清单。
* **现状**: 仅有 2x2 和 4x4 两种固定样式。
## 3. 创新功能拓展 (Innovative Features)
*定位:差异化竞争,利用 AI 和传感器技术。*
### 3.1 AI 智能摘要与生成
* **描述**: 接入大模型 API如 Gemini/DeepSeek一键为长笔记生成摘要、提取待办事项或根据关键词扩写内容。
* **亮点**: 生产力倍增器,将“记录”变为“创作”。
### 3.2 无限画布 (Infinite Canvas)
* **描述**: 打破线性列表限制,提供一个可以自由摆放便签、图片、手绘的白板模式。
* **亮点**: 适合头脑风暴和复杂项目规划。
### 3.3 上下文感知提醒 (Contextual Reminders)
* **描述**: 不仅支持时间提醒,还支持**位置提醒**(到达超市时提醒买菜)或**Wi-Fi提醒**连接公司Wi-Fi时提醒打卡
* **亮点**: 更加智能和贴心的生活助手。
---
## 推荐实施路线图 (Roadmap)
鉴于资源有限,建议按照以下顺序进行迭代:
**Phase 1: 基础夯实 (Current Focus)**
1. **完善回收站 UI**: 补全缺失的 UI 逻辑,闭环删除/恢复流程。
2. **WebDAV 同步**: 解决数据安全和跨端问题,替换老旧同步代码。
**Phase 2: 体验升级**
1. **Markdown 支持**: 提升编辑体验。
2. **OCR 集成**: 增强多媒体记录能力。
**Phase 3: 智能化**
1. **AI 辅助写作**: 引入智能化功能。
您目前已批准的计划书主要覆盖了 **Phase 1** 的部分内容。如果您同意,我们可以先从 **“完善回收站 UI”** 开始,因为这是目前代码中明确标记为 TODO 且逻辑已就绪的功能。

@ -0,0 +1,49 @@
# 置顶功能实现计划书
## 1. 现状分析
* **数据层**:
* 数据库已添加 `top` 字段 (Version 5)。
* `NotesRepository.NoteInfo` 已包含 `isPinned` 字段。
* `NotesRepository.queryNotes` 已经包含了按照 `isPinned` 排序的逻辑 (Line 250-251)。
* **缺失**: `NotesRepository` 缺少更新置顶状态的方法 (`togglePin`)。
* **UI 层**:
* `NotesListActivity` 在多选模式下 (`updateToolbarForMultiSelectMode`) 仅添加了“删除”和“移动”按钮,**缺少“置顶”按钮**。
* `NotesListViewModel` 缺少处理置顶逻辑的方法。
* 列表项 UI (`note_item.xml` 或类似) 可能需要显示置顶图标。
## 2. 实施步骤
### 第一步:数据层增强 (`NotesRepository`)
1. 在 `NotesRepository` 中添加 `toggleNotePin(long noteId, boolean pin, Callback<Integer> callback)` 方法,用于更新数据库中的 `top` 字段。
2. 在 `NotesRepository` 中添加批量操作方法 `batchTogglePin(List<Long> noteIds, boolean pin, Callback<Integer> callback)`
### 第二步ViewModel 增强 (`NotesListViewModel`)
1. 添加 `toggleSelectionPin()` 方法:
* 检查选中的笔记。
* 确定新的置顶状态(如果选中的笔记中有未置顶的,则全部置顶;如果全部已置顶,则全部取消置顶)。
* 调用 Repository 的批量更新方法。
* 操作成功后刷新列表。
### 第三步UI 层增强 (`NotesListActivity`)
1. **修改多选菜单**:
* 在 `updateToolbarForMultiSelectMode` 方法中,向 Toolbar 添加“置顶”按钮。
* 按钮图标可以使用 `android.R.drawable.ic_menu_upload` (暂代) 或导入一个新的置顶图标。
* 动态更新按钮图标/文字:如果选中的都是置顶的,显示“取消置顶”;否则显示“置顶”。
2. **处理点击事件**:
* 在 `updateToolbarForMultiSelectMode` 中为新按钮设置点击监听器,调用 ViewModel 的 `toggleSelectionPin`
### 第四步:列表项 UI 增强 (可选但推荐)
1. 修改 `NotesListAdapter` (或 `NoteInfoAdapter`),在 `bindView` 时检查 `note.isPinned`
2. 如果置顶,显示一个小图标(例如回形针或图钉)在列表项的角落,以区分置顶笔记。
## 3. 具体修改计划
1. **`NotesRepository.java`**: 添加 `updateNotePin` 方法。
2. **`NotesListViewModel.java`**: 添加 `toggleSelectedNotesPin` 方法。
3. **`NotesListActivity.java`**: 更新 `updateToolbarForMultiSelectMode`,添加置顶 Action。
## 4. 验证计划
1. 编译运行。
2. 长按笔记进入多选模式。
3. 观察 Toolbar 是否出现置顶按钮。
4. 点击置顶按钮,观察列表排序是否变化(置顶项应跳到顶部)。
5. 再次选中置顶项,点击取消置顶,观察是否恢复原序。

@ -0,0 +1,83 @@
# 小米便签 "隐私锁" 功能拓展项目计划书
本计划书旨在为小米便签MiNotes添加隐私保护功能允许用户为特定的便签或文件夹设置密码锁数字密码或手势密码
## 1. 核心需求分析
* **对象**: 便签、文件夹。
* **入口**:
* **上锁**: 长按选中 -> 菜单栏 "锁" 按钮 -> 确认对话框 -> 上锁。
* **解锁/访问**: 点击已上锁条目 -> 输入密码 -> 验证通过 -> 进入。
* **设置**: 侧边栏 -> Settings -> Safe -> 设置/修改/取消密码。
* **密码类型**: 数字密码 (PIN)、手势密码 (Pattern)。
* **视觉反馈**: 上锁条目显示 "锁" 图标。
* **环境**: Android 12 (API 31), 离线优先。
## 2. 详细设计方案
### 2.1 数据库与存储设计 (Phase 1)
* **数据库变更 (`note.db`)**:
* 升级数据库版本 `DB_VERSION` 到 6。
* 在 `note` 表中新增列 `locked` (INTEGER, 0=未锁, 1=已锁)。
* **偏好设置 (`SharedPreferences`)**:
* 存储密码哈希 (SHA-256),不存储明文。
* 字段:
* `security_password_type`: 0 (无), 1 (数字), 2 (手势)。
* `security_password_hash`: 密码的哈希值。
### 2.2 安全模块设计 (`SecurityManager`)
创建一个单例工具类 `SecurityManager`,负责:
* 密码的设置、验证、清除。
* 密码哈希计算。
* 判断是否已设置密码。
### 2.3 UI/UX 设计
#### 2.3.1 密码交互界面 (`PasswordActivity`)
创建一个通用的密码输入/设置 Activity支持多种模式
* **模式**:
* `MODE_SETUP`: 设置新密码(需输入两次确认)。
* `MODE_CHECK`: 验证密码(用于打开便签或修改设置前)。
* `MODE_MODIFY`: 修改密码流程。
* **组件**:
* **数字锁**: 使用 `EditText` (InputType=numberPassword)。
* **手势锁**: 自定义 View `LockPatternView` (3x3 网格)。
#### 2.3.2 设置页面扩展 (`NotesPreferenceActivity`)
* 在 `res/xml/preferences.xml` 中添加 "安全与隐私" (`Safe`) 分类。
* 添加点击事件:检查是否已设置密码 -> 验证原密码 -> 进入密码管理界面 (修改/取消)。
#### 2.3.3 主列表交互 (`NotesListActivity` & `NoteInfoAdapter`)
* **多选菜单**: 在 `updateToolbarForMultiSelectMode` 中添加 "上锁" (`R.id.menu_lock`) 按钮。
* **列表显示**: `NoteInfoAdapter` 根据 `locked` 字段显示 `ic_lock` 图标。
* **点击拦截**: `onNoteItemClick` 中检测 `locked` 状态,若已锁则跳转 `PasswordActivity` (Check Mode),回调成功后再打开便签。
## 3. 实施步骤 (Roadmap)
### 阶段一:基础设施 (Infrastructure)
1. **数据库升级**: 修改 `NotesDatabaseHelper`,实现 `upgradeToV6`,添加 `locked` 字段。
2. **数据模型更新**: 更新 `Notes.java` (Contract) 和 `NotesRepository` 以支持锁定状态的读写。
3. **安全工具类**: 实现 `SecurityManager`
### 阶段二:密码组件 (Security Components)
4. **手势控件**: 实现自定义 View `LockPatternView` (绘制 9 个点,处理触摸事件生成密码字符串)。
5. **密码页面**: 实现 `PasswordActivity`,处理 PIN 和 Pattern 的输入与逻辑流程 (设置、验证)。
### 阶段三:设置集成 (Settings Integration)
6. **设置入口**: 修改 `NotesPreferenceActivity`,集成密码管理入口。
7. **流程联调**: 确保用户可以顺利设置、修改和删除密码。
### 阶段四:核心功能实现 (Core Feature)
8. **列表适配**: 修改 `note_item.xml` 添加锁图标,更新 `NoteInfoAdapter`
9. **上锁逻辑**: 在 `NotesListActivity` 实现多选上锁逻辑及确认对话框。
10. **解锁逻辑**: 在打开便签/文件夹时拦截并调用密码验证。
### 阶段五:测试与优化 (Verification)
11. **功能测试**: 覆盖所有用例(设置、验证、修改、取消、上锁、解锁)。
12. **UI/UX 调整**: 确保图标美观,交互流畅。
## 4. 关键决策
* **手势锁实现**: 鉴于 Android 原生 `LockPatternView` 为私有 API我们将实现一个轻量级的自定义 View仅依赖 Canvas 绘制,保证兼容性 (Android 12+)。
* **加密**: 使用 SHA-256 对密码进行单向哈希存储,确保安全性。
* **取消密码**: 取消密码时,逻辑上会清除所有便签的 `locked` 状态,防止出现无法解锁的“死便签”。
该计划涵盖了从底层数据到上层 UI 的完整实现路径,确保功能的健壮性和扩展性。

@ -0,0 +1,53 @@
# 文件夹与便签区分优化方案
## 1. 问题分析
当前应用在列表中展示便签和文件夹时,可能仅通过标题和背景色区分,导致视觉上混淆,尤其当它们同名时。现有的 `note_item.xml` 布局对所有类型的项使用相同的结构,缺乏针对文件夹的特有标识。
## 2. 解决方案设计
为了清晰区分文件夹和便签,我们采取以下多维度的视觉区分策略:
### 2.1 图标区分 (Iconography)
* **文件夹**: 在列表项左侧添加一个明显的黄色文件夹图标。
* **便签**: 保持现状,或者添加一个默认的文档图标(可选,保持简洁则不加)。
### 2.2 辅助文本 (Metadata)
* **文件夹**: 显示该文件夹包含的笔记数量(例如 "(5)" 或 "5项")。
* **便签**: 显示最后修改时间(现有逻辑)。
### 2.3 视觉样式 (Visual Style)
* **文件夹**: 标题字体加粗,颜色略深。
* **便签**: 标题保持常规。
## 3. 详细实施计划
### Step 1: 准备资源
* 需要一个文件夹图标资源。可以使用系统自带的 `android.R.drawable.ic_menu_view` 或者导入一个新的 `ic_folder.xml` 矢量图。为简便起见,优先尝试系统资源,效果不佳则绘制。
### Step 2: 修改布局文件 (`note_item.xml`)
* 在标题左侧添加一个 `ImageView` (`iv_type_icon`),用于显示类型图标。默认设为 `GONE` 或显示默认图标。
* 或者复用现有的布局结构,在 `NoteInfoAdapter` 中动态控制。
### Step 3: 修改适配器 (`NoteInfoAdapter.java`)
* 在 `getView` 方法中,检查 `note.type`
* **如果是文件夹 (`Notes.TYPE_FOLDER`)**:
* 显示文件夹图标。
* 在标题后追加笔记数量信息(需要 `NotesRepository` 在查询时带出 `NOTES_COUNT`,或者简单点,仅仅在视觉上加图标)。
* 隐藏或修改时间显示(文件夹通常关注创建时间或不显示时间,显示包含数量更有用)。
* **如果是便签 (`Notes.TYPE_NOTE`)**:
* 隐藏文件夹图标(或显示便签图标)。
* 显示修改时间。
## 4. 补充细节
* **系统文件夹**: 像“通话记录”这样的系统文件夹,可以给予特殊的图标。
* **数据层支持**: 检查 `NoteInfo` 类是否包含 `notesCount` 字段。如果 `NotesRepository` 的查询逻辑中没有 join 统计数量,可能需要调整 SQL 查询。
* *快速检查*: `NotesRepository.queryNotes` 目前没有进行 join 查询,添加实时计数可能影响性能。
* *替代方案*: 仅在视觉上区分(图标 + 样式),暂不显示具体数量,或者只显示 "(文件夹)" 后缀。
## 5. 最终方案
为了最小化对数据层的侵入并快速见效:
1. **修改 `note_item.xml`**: 增加一个用于显示类型图标的 `ImageView`
2. **修改 `NoteInfoAdapter.java`**:
* 如果 `note.type == Notes.TYPE_FOLDER`: 设置图标为文件夹,隐藏时间(或显示为 "文件夹")。
* 如果 `note.type == Notes.TYPE_NOTE`: 隐藏图标,显示时间。
这样可以直观地通过图标区分两者。

@ -0,0 +1,61 @@
# 置顶功能问题修复与完善计划
## 1. 问题分析
根据用户提供的截图和代码分析,目前存在以下问题:
1. **置顶按钮缺失**: 在多选模式的 Toolbar 中,虽然代码逻辑已经添加,但截图显示只有“删除”和“全选”等按钮,可能因为 `menu_pin` 资源未正确加载或 `ifRoom` 空间不足被折叠到溢出菜单中。
2. **置顶状态不可见**: 截图中的笔记列表没有明显的置顶标识(如置顶图标或背景色区分),用户无法直观判断哪些笔记已置顶。
3. **排序逻辑确认**: 需要确认置顶操作后,列表是否正确重新排序(置顶项在最前)。
## 2. 修改计划
### 2.1 UI 层修复与增强 (`NotesListActivity` & `NoteInfoAdapter`)
1. **确保置顶按钮可见**:
* 检查 `note_list_multi_select.xml`,确保 `multi_select_pin``showAsAction` 属性设置为 `always`,强制显示在 Toolbar 上。
* 如果空间不足,考虑调整按钮顺序或图标。
2. **添加置顶视觉标识**:
* 修改 `note_item.xml` (需要查看该文件),添加一个用于显示置顶状态的 `ImageView` (例如 `iv_pinned`)。
* 在 `NoteInfoAdapter.getView` 中,根据 `note.isPinned` 状态控制该图标的可见性。
* 或者,可以通过改变置顶项的背景色来区分。
### 2.2 交互逻辑完善 (`NotesListViewModel` & `NotesListActivity`)
1. **操作反馈**:
* 在 `NotesListActivity` 中观察 ViewModel 的操作结果,成功置顶/取消置顶后显示 `Toast` 提示(例如“已置顶”、“已取消置顶”)。
2. **实时刷新**:
* 确认 `NotesRepository.batchTogglePin` 完成后是否触发了数据重新加载。目前 ViewModel 中的回调已经调用了 `refreshNotes()`,这部分逻辑应该是正确的。
### 2.3 数据层确认 (`NotesRepository`)
1. **排序逻辑**:
* 再次检查 `queryNotes` 方法中的排序逻辑,确保 `isPinned` 优先级高于 `modifiedDate`。代码中已经实现了 `if (a.isPinned != b.isPinned) return a.isPinned ? -1 : 1;`,逻辑正确。
## 3. 实施步骤
1. **修改 Menu 资源**: 将 `note_list_multi_select.xml` 中置顶按钮的 `showAsAction` 改为 `always`
2. **修改 Item 布局**: 在 `note_item.xml` 中添加置顶图标。
3. **更新 Adapter**: 在 `NoteInfoAdapter` 中绑定置顶图标数据。
4. **添加 Toast 反馈**: 在 `NotesListActivity` 中监听置顶操作完成事件(可能需要 ViewModel 添加一个新的 LiveData 用于通知 UI 操作结果)。
## 4. 验证方案
1. 长按进入多选模式,确认右上角出现置顶按钮。
2. 选中一个普通笔记,点击置顶,确认:
* 弹出“已置顶”提示。
* 笔记移动到列表顶部。
* 笔记上出现置顶图标。
3. 选中一个已置顶笔记,点击取消置顶,确认恢复原位且图标消失。

@ -0,0 +1,57 @@
# 置顶功能失效问题修复计划
## 1. 问题诊断
经过对代码的分析,发现导致置顶功能在 UI 上没有正确体现的原因有两点:
1. **数据层排序逻辑问题**:
`NotesRepository.java``queryNotes` 方法中(第 246-255 行),虽然有排序逻辑,但它是在 `noteFromCursor` 方法读取数据之后执行的。关键在于 `noteFromCursor` 方法(第 130-149 行)**没有读取** **`top`** **字段**。
* `NoteInfo` 类定义了 `isPinned` 字段。
* 但在 `noteFromCursor` 中,并没有从 `Cursor` 中读取 `NoteColumns.TOP` 列的值赋给 `isPinned`
* 这导致所有加载的笔记 `isPinned` 默认为 `false`因此排序时置顶逻辑失效UI 上的置顶图标也不会显示。
2. **ViewModel 刷新机制**:
虽然 `batchTogglePin` 成功更新了数据库(所以 Toast 显示成功),但由于读取时丢失了状态,导致刷新列表后看起来依然没有变化。
## 2. 修改计划
### 2.1 修复数据读取 (`NotesRepository.java`)
1. **修改** **`noteFromCursor`** **方法**:
* 添加读取 `NoteColumns.TOP` 列的逻辑。
* 将读取到的整数值0 或 1转换为布尔值赋给 `noteInfo.isPinned`
### 2.2 验证排序逻辑
* `NotesRepository` 中的排序逻辑 `if (a.isPinned != b.isPinned) return a.isPinned ? -1 : 1;` 是正确的,只要 `isPinned` 被正确赋值,排序就会生效。
## 3. 实施步骤
1. **修改** **`NotesRepository.java`**:
* 找到 `noteFromCursor` 方法。
* 在读取其他字段后,添加:
```java
int topIndex = cursor.getColumnIndex(NoteColumns.TOP);
if (topIndex != -1) {
noteInfo.isPinned = cursor.getInt(topIndex) > 0;
}
```
## 4. 预期效果
* 修改后,重新进入应用或执行置顶操作。
* `queryNotes` 将能正确读取置顶状态。
* `Collections.sort` 将根据 `isPinned` 将置顶笔记排在前面。
* `NotesListActivity` 的 Adapter 将根据 `isPinned` 显示置顶图标。

@ -0,0 +1,30 @@
# 解决小米便签编译报错及数据库升级方案
## 问题分析
用户遇到的编译报错 `找不到符号 NoteColumns.TOP` 是因为在 `NoteItemData.java` 中使用了 `NoteColumns.TOP` 常量,但该常量在 `Notes.java``NoteColumns` 接口中未定义。这通常是因为代码中引入了“置顶”功能的相关逻辑,但缺少了底层数据定义。
此外,仅修复编译错误是不够的。如果数据库中没有对应的 `top` 列,应用在运行时查询该字段可能会崩溃或行为异常。因此,需要同步更新数据库架构。
## 解决方案
我们将分两步解决此问题:
### 1. 修复编译错误
`app/src/main/java/net/micode/notes/data/Notes.java` 文件中,向 `NoteColumns` 接口添加 `TOP` 常量定义。
### 2. 升级数据库架构
`app/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java` 文件中执行以下操作:
1. **升级版本号**:将 `DB_VERSION``4` 增加到 `5`
2. **更新建表语句**:在 `CREATE_NOTE_TABLE_SQL` 中添加 `top` 字段的定义,确保新安装的应用包含该字段。
3. **实现升级逻辑**:添加 `upgradeToV5` 方法,使用 `ALTER TABLE` 语句为旧版本数据库添加 `top` 字段。
4. **注册升级路径**:在 `onUpgrade` 方法中调用 `upgradeToV5`
## 详细步骤
1. **编辑 `Notes.java`**:
* 添加 `public static final String TOP = "top";`
2. **编辑 `NotesDatabaseHelper.java`**:
* `DB_VERSION` -> `5`
* `CREATE_NOTE_TABLE_SQL`: 添加 `NoteColumns.TOP + " INTEGER NOT NULL DEFAULT 0"`
* 添加 `upgradeToV5(SQLiteDatabase db)` 方法。
* 在 `onUpgrade` 中处理 `oldVersion == 4` 的情况。
通过以上步骤,既能解决当前的编译错误,又能保证应用在部署到设备上后能正常读写新增的“置顶”字段。

@ -0,0 +1,117 @@
# 小米便签 (MiNotes) 项目深度分析与拓展计划书
## 1. 项目概览与架构分析 (Project Overview & Architecture)
### 1.1 总体架构
本项目采用了 **混合架构 (Hybrid Architecture)**,处于从传统的 Android 开发模式向现代 MVVM 模式过渡的阶段。
* **Legacy 部分**: 核心编辑页面 `NoteEditActivity` 及其对应的逻辑类 `WorkingNote` 采用了一种类似于 MVP/MVC 的变体,其中 `WorkingNote` 充当了“上帝类”的角色承担了数据加载、状态管理、UI 回调等过多职责,导致耦合度较高。
* **Modern 部分**: 列表页面 `NotesListActivity` 引入了 `ViewModel` (`NotesListViewModel`) 和 `Repository` (`NotesRepository`),利用 `LiveData` 进行数据驱动的 UI 更新,体现了较好的分层设计思想。
### 1.2 数据流向
* **核心数据源**: `NotesProvider` (继承自 `ContentProvider`) 是应用的数据中枢。它不仅为应用内部提供数据(通过 `ContentResolver`),还设计用于跨进程/跨应用的数据共享。
* **数据访问**:
* **读操作**: 大部分通过 `NotesRepository` -> `ContentResolver` -> `NotesProvider` -> `SQLiteDatabase` 的链路。
* **写操作**: 编辑页直接通过 `WorkingNote` 操作 `ContentResolver`,绕过了 Repository 层。
* **同步机制**: `GTaskManager` 负责与 Google Tasks 的同步,它直接操作本地数据库并进行网络请求。
### 1.3 关键组件
* **WorkingNote**: 负责单条笔记的生命周期管理(加载、保存、转换模式)。
* **NotesDatabaseHelper**: 管理 SQLite 数据库创建、升级及复杂的触发器Trigger逻辑。
* **GTaskSyncService**: 后台同步服务。
---
## 2. 现有功能分析 (Feature Analysis)
### 2.1 核心功能
* **笔记编辑**: 支持普通文本模式和清单模式Checklist
* **文件夹管理**: 支持多层级文件夹(虽然 UI 上主要体现为单层),包含系统预置文件夹(通话记录、回收站)。
* **通话记录笔记**: 特色功能,能够根据来电号码自动关联或创建笔记。
### 2.2 辅助功能
* **桌面小部件 (Widgets)**: 提供 2x2 和 4x4 两种规格,通过 `RemoteViews` 更新。
* **定时提醒**: 基于 `AlarmManager` 实现。
* **云同步**: 仅支持 Google Tasks 同步。
---
## 3. 可扩展性与兼容性评估 (Scalability & Compatibility)
### 3.1 可扩展性 (Scalability)
* **优势**:
* **ContentProvider**: 数据结构标准,易于被外部模块访问。
* **MVVM 雏形**: 新增模块若遵循 MVVM 模式,将具有良好的测试性和维护性。
* **劣势 (瓶颈)**:
* **数据库耦合**: 大量硬编码的 SQL 语句和触发器逻辑,难以迁移到 Room 等现代 ORM 框架。
* **WorkingNote 臃肿**: 修改编辑逻辑牵一发而动全身。
* **同步接口缺失**: 缺乏通用的同步接口抽象,难以扩展其他云服务(如 WebDAV
### 3.2 兼容性 (Compatibility)
当前代码库存在显著的兼容性风险,尤其是在 Android 10+ 设备上:
* **AsyncTask**: `GTaskASyncTask` 使用了已废弃的 API可能导致内存泄漏或崩溃。
* **存储权限**: 未适配 Android 10+ 的分区存储 (Scoped Storage),直接文件路径访问将失效。
* **精确闹钟**: Android 12+ 需要 `SCHEDULE_EXACT_ALARM` 权限。
* **通知渠道**: Android 8.0+ 必须适配 Notification Channels。
---
## 4. 项目拓展计划 (Extension Plan)
本计划旨在将小米便签打造为一款现代化、高效率、高颜值的笔记应用。
### Phase 1: 现代化重构与基石搭建 (Modernization)
**目标**: 消除技术债务,确保在 Android 14/15 上稳定运行。
1. **并发模型升级**:
* 废弃 `AsyncTask`
* 引入 **Kotlin Coroutines** (协程) 处理异步任务(数据库读写、网络请求)。
2. **系统适配**:
* **权限**: 适配动态权限申请,特别是闹钟和通知权限。
* **存储**: 使用 `MediaStore` API 或 SAF (Storage Access Framework) 替代直接文件操作。
* **服务**: 将后台同步服务适配为 WorkManager 或 Foreground Service。
3. **代码解耦**:
* 重构 `NoteEditActivity`,引入 `NoteEditViewModel`
* 拆分 `WorkingNote`,将其纯化为数据模型,逻辑移入 ViewModel/UseCase。
### Phase 2: 核心体验升级 (Core Experience)
**目标**: 提升笔记的编辑与组织能力。
1. **富文本支持 (Markdown)**:
* 数据库 `data` 表新增 `mime_type` 类型支持。
* 集成 Markdown 解析器(如 Markwon支持标题、列表、代码块渲染。
2. **多媒体附件**:
* 新增 `attachments` 数据表。
* 支持插入图片(压缩存储或引用 URI、音频文件。
3. **标签系统 (Tagging)**:
* 实现笔记与标签的多对多关系。
* UI 上支持按标签筛选、颜色标记。
### Phase 3: 云服务与互联 (Cloud & Connectivity)
**目标**: 打破数据孤岛,提供数据安全保障。
1. **同步架构抽象**:
* 定义 `ISyncProvider` 接口。
* 将 `GTaskManager` 降级为一种实现。
2. **WebDAV 支持**:
* 实现通用的 WebDAV 协议允许用户连接坚果云、Nextcloud、NAS 等私有云。
3. **本地备份**:
* 支持导出为 JSON/Zip 格式。
* 实现自动定时本地备份。
### Phase 4: UI/UX 焕新 (Design Refresh)
**目标**: 拥抱 Material Design 3 (Material You)。
1. **动态取色**: 支持 Android 12+ 的动态主题色。
2. **深色模式**: 完善的 Dark Mode 资源适配。
3. **现代交互**:
* 列表页支持侧滑删除/归档。
* 笔记拖拽排序。
* 沉浸式状态栏与导航栏。
## 5. 关键决策与建议
* **开发语言**: 建议新功能完全使用 **Kotlin** 开发,旧 Java 代码按需迁移。
* **数据库**: 虽然迁移到 Room 工作量大,但建议在 Phase 2 开始尝试引入 Room与现有 Helper 共存,逐步替换。
* **同步策略**: 鉴于网络环境,建议将 **WebDAV** 作为首推的同步方式Google Tasks 作为可选插件。
---
*Generated by Trae AI Assistant*

@ -0,0 +1,122 @@
# 小米便签 (MiNotes) 功能拓展详细规划书 (v3.0 - 全面增强版)
本规划书在 v2.0 离线优先的基础上进一步汲取了市场主流笔记应用Notion, Obsidian, Flomo 等)的精华,旨在打造一款**全能型、离线优先、极具差异化**的笔记应用。我们从效率、知识管理、创意、安全四个维度进行了深度发散和规划。
## 1. 效率与工作流 (Productivity & Workflow)
*定位:让笔记不仅是记录,更是行动的开始。*
### 1.1 专注模式 (Focus Mode / Pomodoro)
* **功能描述**: 在编辑笔记时,提供一个沉浸式的“专注模式”。开启后,隐藏所有无关 UI状态栏、工具栏并可叠加一个倒计时番茄钟。
* **用户价值**: 帮助用户进入心流状态,适合深度写作、思考或复习。
* **离线适配**: 纯本地逻辑。
* **开发难度**: 低。
### 1.2 悬浮窗速记 (Floating Quick Note)
* **功能描述**: 在其他应用之上显示一个半透明悬浮球。点击后展开一个小窗口,支持快速输入文字、语音或粘贴剪贴板内容,无需切换应用。
* **用户价值**: 随时随地捕捉灵感,阅读/看视频时的绝佳伴侣。
* **离线适配**: 本地 WindowManager 实现。
* **开发难度**: 中(需处理悬浮窗权限适配)。
### 1.3 每日回顾 (Daily Review / Journaling)
* **功能描述**:
* **自动日志**: 每天自动创建一个以“日期”命名的笔记,聚合当天的所有操作(新建笔记、完成待办、通话记录)。
* **那年今日**: 首页顶部展示往年今天的笔记回顾。
* **用户价值**: 自动化的生活记录与复盘,无需手动整理。
* **开发难度**: 中。
### 1.4 模板中心 (Template Center)
* **功能描述**: 提供预设模板库如康奈尔笔记法、SWOT 分析、会议纪要、周报),并允许用户将现有笔记保存为自定义模板。
* **用户价值**: 快速开始标准化记录,降低启动门槛。
* **开发难度**: 低。
---
## 2. 知识管理与可视化 (Knowledge & Visualization)
*定位:将碎片化信息重组为结构化知识。*
### 2.1 看板视图 (Kanban View)
* **功能描述**: 将文件夹内的笔记或待办事项以卡片形式展示在“未开始”、“进行中”、“已完成”等列中,支持拖拽流转状态。
* **用户价值**: 轻量级的项目管理工具,直观掌控任务进度。
* **开发难度**: 高。
### 2.2 日历视图 (Calendar View)
* **功能描述**: 在日历上以圆点或标题形式展示笔记(按创建/修改时间或提醒时间分布)。支持点击日期直接创建笔记。
* **用户价值**: 从时间维度回顾历史,规划未来。
* **开发难度**: 中。
### 2.3 知识图谱 (Knowledge Graph)
* **功能描述**: 基于双向链接(`[[Link]]`),用节点和连线可视化展示笔记之间的引用关系。支持缩放、拖拽节点,点击节点跳转。
* **用户价值**: 上帝视角俯瞰知识结构,发现知识盲区与隐性联系。
* **开发难度**: 极高(需引入图形渲染库如 MPAndroidChart 修改版或自定义 View
### 2.4 标签墙与嵌套标签 (Tag Hierarchy)
* **功能描述**: 支持多级标签系统(如 `#工作/项目A/会议`),并提供一个动态的标签云或标签墙视图进行筛选。
* **用户价值**: 突破文件夹的单一维度限制,实现多维度灵活管理。
* **开发难度**: 中。
---
## 3. 创意与多媒体 (Creativity & Media)
*定位:释放表达欲,记录不设限。*
### 3.1 手写与绘图 (Handwriting & Sketch)
* **功能描述**: 提供画笔工具,支持在笔记中插入手写区域或涂鸦。支持压感(如果设备支持),支持橡皮擦、套索工具。
* **用户价值**: 还原纸笔体验,适合绘制草图、数学公式、手写签名。
* **开发难度**: 高。
### 3.2 语音笔记与时间戳 (Audio Note with Timestamp)
* **功能描述**: 录音的同时可以打字记录。回放录音时,点击文字可跳转到对应的录音进度(类似 Notability 的功能)。
* **用户价值**: 会议记录、课堂录音的神器,不再错过任何细节。
* **开发难度**: 高。
### 3.3 闪念胶囊 (Flash Card / Flomo-like)
* **功能描述**: 一个类似于聊天界面的输入框,发送即保存为一张卡片。支持随机漫游回顾,支持标签分类。
* **用户价值**: 极速捕捉转瞬即逝的灵感,无压力记录。
* **开发难度**: 低。
---
## 4. 隐私与安全 (Privacy & Security)
*定位:数据主权,绝对安全。*
### 4.1 生物识别锁 (Biometric Lock)
* **功能描述**: 支持指纹或面部解锁进入应用,或锁定特定私密文件夹/笔记。
* **用户价值**: 保护日记、账号密码等敏感信息,防止他人窥探。
* **开发难度**: 低(使用 Android BiometricPrompt API
### 4.2 本地加密存储 (Local Encryption)
* **功能描述**: 使用 AES-256 算法对数据库文件和附件进行加密存储。即使手机丢失,导出文件也无法被暴力破解。
* **用户价值**: 满足极高安全需求场景(如商业机密、个人隐私)。
* **开发难度**: 中(需引入 SQLCipher
### 4.3 隐身模式 (Stealth Mode)
* **功能描述**: 在多任务界面模糊显示应用内容;提供一个“伪装密码”,输入后进入一个空的或预设的伪装空间。
* **用户价值**: 极致的隐私保护。
* **开发难度**: 中。
---
## 5. 推荐实施路线 (Roadmap v3.0) - 离线优先策略
### Phase 1: 基础体验增强 (Foundation)
* **目标**: 夯实基础,提升易用性和安全性。
* **任务**:
1. **回收站** (现有计划)
2. **深色模式** (现有计划)
3. **生物识别锁**: 增加安全感,实现简单,价值高。
4. **模板功能**: 提升记录效率。
### Phase 2: 核心生产力 (Core Productivity)
* **目标**: 丰富记录形式,提升编辑效率。
* **任务**:
1. **本地备份与导出** (现有计划)
2. **Markdown 支持** (现有计划)
3. **悬浮窗速记**: 扩展记录场景。
4. **日历视图**: 提供新的时间维度视角。
### Phase 3: 高级与创新 (Innovation)
* **目标**: 打造差异化壁垒。
* **任务**:
1. **离线 OCR** (现有计划)
2. **看板视图**: 引入项目管理能力。
3. **双向链接 & 知识图谱**: 打造知识库神器的核心。

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>app</name>
<comment>Project app created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
<filteredResources>
<filter>
<id>1768954653382</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

@ -0,0 +1,57 @@
# ListView迁移至RecyclerView实施方案
## 实施概述
将小米便签的ListView组件迁移至RecyclerView实现性能提升、功能增强和代码可维护性改进。
## 主要工作内容
### 1. 依赖配置
* 修改`build.gradle.kts`添加RecyclerView和CursorAdapter依赖
### 2. 核心类创建
* 创建`NoteViewHolder.java`实现ViewHolder模式
* 创建`NoteItemDecoration.java`:实现分隔线
* 创建`NoteItemTouchCallback.java`:实现手势操作
* 创建`list_divider.xml`:分隔线资源
### 3. 适配器重构
* 重写`NotesListAdapter.java`为`NotesRecyclerViewAdapter`
* 继承`CursorRecyclerViewAdapter`保持Cursor数据源
* 实现ViewHolder模式使用SparseArray优化选中状态
### 4. 布局修改
* 修改`note_list.xml`将ListView替换为RecyclerView
* 修改`NotesListActivity.java`:更新初始化和事件处理逻辑
### 5. 功能增强
* 添加DefaultItemAnimator实现增删改动画
* 实现ItemTouchHelper支持滑动删除和拖拽
* 实现局部刷新机制
## 预期效果
* 内存占用降低30-40%
* 滚动帧率提升至55-60 FPS
* 支持局部刷新和内置动画
* 代码可维护性显著提升
## 实施周期
预计10-15个工作日完成全部迁移和测试。

@ -4,15 +4,13 @@ plugins {
android {
namespace = "net.micode.notes"
compileSdk = 36
buildFeatures {
viewBinding = true
}
compileSdk = 34
buildToolsVersion = "34.0.0"
defaultConfig {
applicationId = "net.micode.notes"
minSdk = 24
targetSdk = 36
targetSdk = 31
versionCode = 1
versionName = "1.0"
@ -28,13 +26,6 @@ android {
)
}
}
testOptions {
unitTests {
isIncludeAndroidResources = false
isReturnDefaultValues = true
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
@ -53,13 +44,22 @@ dependencies {
implementation(libs.material)
implementation(libs.activity)
implementation(libs.constraintlayout)
implementation(files("D:\\ke\\software_enginering\\httpcomponents-client-4.5.14-bin\\lib\\httpclient-osgi-4.5.14.jar"))
implementation(files("D:\\ke\\software_enginering\\httpcomponents-client-4.5.14-bin\\lib\\httpclient-win-4.5.14.jar"))
implementation(files("D:\\ke\\software_enginering\\httpcomponents-client-4.5.14-bin\\lib\\httpcore-4.4.16.jar"))
// RecyclerView依赖
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.cursoradapter:cursoradapter:1.0.0")
// 部分需要重新修改
// implementation(fileTree(mapOf(
// "dir" to "D:\\Code\\AndroidCode\\Notesmaster\\httpcomponents-client-4.5.14-bin\\lib",
// "include" to listOf("*.aar", "*.jar"),
// "exclude" to listOf("")
// )))
//修改为如下代码:
implementation(files("D:\\college\\studying\\studying\\2025.09\\SE\\android\\client\\lib\\httpclient-osgi-4.5.14.jar"))
implementation(files("D:\\college\\studying\\studying\\2025.09\\SE\\android\\client\\lib\\httpclient-win-4.5.14.jar"))
implementation(files("D:\\college\\studying\\studying\\2025.09\\SE\\android\\client\\lib\\httpcore-4.4.16.jar"))
testImplementation(libs.junit)
testImplementation(libs.mockito.core)
testImplementation(libs.mockito.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
}

@ -174,6 +174,14 @@
android:theme="@android:style/Theme.Holo.Light" >
</activity>
<!-- ==================== 密码设置/验证活动 ==================== -->
<activity
android:name=".ui.PasswordActivity"
android:label="Password"
android:theme="@android:style/Theme.Holo.Light.NoActionBar"
android:windowSoftInputMode="stateVisible|adjustResize">
</activity>
<!-- ==================== 同步服务 ==================== -->
<!-- Google任务同步服务用于与Google Tasks同步数据 -->
<service

@ -241,6 +241,12 @@ public class Notes {
* <P> Type : INTEGER </P>
*/
public static final String TOP = "top";
/**
* Sign to indicate the note is locked or not
* <P> Type : INTEGER </P>
*/
public static final String LOCKED = "locked";
}
public interface DataColumns {

@ -153,7 +153,9 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.TOP + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.LOCKED + " INTEGER NOT NULL DEFAULT 0" +
")";
/**
@ -529,6 +531,18 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
oldVersion++;
}
// 从V4升级到V5
if (oldVersion == 4) {
upgradeToV5(db);
oldVersion++;
}
// 从V5升级到V6
if (oldVersion == 5) {
upgradeToV6(db);
oldVersion++;
}
// 如果需要,重新创建触发器
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
@ -542,6 +556,31 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
}
}
@Override
public void onOpen(SQLiteDatabase db) {
super.onOpen(db);
// 强制检查并修复缺失的列,解决版本升级可能失效的问题
// 这是一层额外的保险确保无论升级逻辑是否被触发LOCKED列都会存在
try {
android.database.Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE.NOTE + " LIMIT 0", null);
boolean hasLockedColumn = false;
if (cursor != null) {
if (cursor.getColumnIndex(NoteColumns.LOCKED) != -1) {
hasLockedColumn = true;
}
cursor.close();
}
if (!hasLockedColumn) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.LOCKED
+ " INTEGER NOT NULL DEFAULT 0");
Log.i(TAG, "Fixed: Added missing LOCKED column in onOpen");
}
} catch (Exception e) {
Log.e(TAG, "Failed to fix database in onOpen", e);
}
}
/**
* V2
* <p>
@ -608,4 +647,42 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.TOP
+ " INTEGER NOT NULL DEFAULT 0");
}
/**
* V6
* <p>
* LOCKEDnote
* </p>
*
* @param db SQLiteDatabase
*/
private void upgradeToV6(SQLiteDatabase db) {
// V6 upgrade logic
// Try adding the LOCKED column if it doesn't exist
try {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.LOCKED
+ " INTEGER NOT NULL DEFAULT 0");
} catch (Exception e) {
Log.e(TAG, "Failed to add LOCKED column in V6 upgrade (it might already exist)", e);
}
}
/**
* V7
* <p>
* LOCKED
* </p>
*
* @param db SQLiteDatabase
*/
private void upgradeToV7(SQLiteDatabase db) {
// V7 upgrade logic: Ensure LOCKED column exists
// This is a safety net for cases where V6 upgrade might have been skipped or failed silently
try {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.LOCKED
+ " INTEGER NOT NULL DEFAULT 0");
} catch (Exception e) {
Log.e(TAG, "Failed to add LOCKED column in V7 upgrade (it probably already exists)", e);
}
}
}

@ -69,6 +69,7 @@ public class NotesRepository {
public int localModified;
public int bgColorId;
public boolean isPinned; // 新增置顶字段
public boolean isLocked; // 新增锁定字段
public NoteInfo() {}
@ -149,6 +150,13 @@ public class NotesRepository {
if (topIndex != -1) {
noteInfo.isPinned = cursor.getInt(topIndex) > 0;
}
int lockedIndex = cursor.getColumnIndex(NoteColumns.LOCKED);
if (lockedIndex != -1) {
noteInfo.isLocked = cursor.getInt(lockedIndex) > 0;
} else {
noteInfo.isLocked = false;
}
return noteInfo;
}
@ -587,10 +595,7 @@ public class NotesRepository {
}
/**
*
* <p>
*
* </p>
*
*
* @param noteIds ID
* @param callback
@ -627,6 +632,81 @@ public class NotesRepository {
});
}
/**
*
*
* @param noteIds ID
* @param callback
*/
public void restoreNotes(List<Long> noteIds, Callback<Integer> callback) {
executor.execute(() -> {
try {
if (noteIds == null || noteIds.isEmpty()) {
callback.onError(new IllegalArgumentException("Note IDs list is empty"));
return;
}
int totalRows = 0;
// 恢复到根目录
long targetFolderId = Notes.ID_ROOT_FOLDER;
for (Long noteId : noteIds) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, targetFolderId);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
int rows = contentResolver.update(uri, values, null, null);
totalRows += rows;
}
if (totalRows > 0) {
callback.onSuccess(totalRows);
Log.d(TAG, "Successfully restored " + totalRows + " notes");
} else {
callback.onError(new RuntimeException("No notes were restored"));
}
} catch (Exception e) {
Log.e(TAG, "Failed to restore notes", e);
callback.onError(e);
}
});
}
/**
*
*
* @param noteIds ID
* @param callback
*/
public void deleteNotesForever(List<Long> noteIds, Callback<Integer> callback) {
executor.execute(() -> {
try {
if (noteIds == null || noteIds.isEmpty()) {
callback.onError(new IllegalArgumentException("Note IDs list is empty"));
return;
}
int totalRows = 0;
for (Long noteId : noteIds) {
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
int rows = contentResolver.delete(uri, null, null);
totalRows += rows;
}
if (totalRows > 0) {
callback.onSuccess(totalRows);
Log.d(TAG, "Successfully permanently deleted " + totalRows + " notes");
} else {
callback.onError(new RuntimeException("No notes were deleted"));
}
} catch (Exception e) {
Log.e(TAG, "Failed to delete notes forever", e);
callback.onError(e);
}
});
}
/**
*
* <p>
@ -859,6 +939,45 @@ public class NotesRepository {
});
}
/**
*
*
* @param noteIds ID
* @param isLocked
* @param callback
*/
public void batchLock(List<Long> noteIds, boolean isLocked, Callback<Integer> callback) {
executor.execute(() -> {
try {
if (noteIds == null || noteIds.isEmpty()) {
callback.onError(new IllegalArgumentException("Note IDs list is empty"));
return;
}
int totalRows = 0;
ContentValues values = new ContentValues();
values.put(NoteColumns.LOCKED, isLocked ? 1 : 0);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
for (Long noteId : noteIds) {
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
int rows = contentResolver.update(uri, values, null, null);
totalRows += rows;
}
if (totalRows > 0) {
callback.onSuccess(totalRows);
Log.d(TAG, "Successfully updated lock state for " + totalRows + " notes");
} else {
callback.onError(new RuntimeException("No notes were updated"));
}
} catch (Exception e) {
Log.e(TAG, "Failed to update lock state", e);
callback.onError(e);
}
});
}
/**
*
*

@ -0,0 +1,120 @@
package net.micode.notes.tool;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Base64;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
*
* <p>
*
* 使SHA-256
* </p>
*/
public class SecurityManager {
private static SecurityManager sInstance;
private Context mContext;
private static final String PREFERENCE_NAME = "notes_preferences";
private static final String PREF_PASSWORD_TYPE = "security_password_type";
private static final String PREF_PASSWORD_HASH = "security_password_hash";
/** 无密码 */
public static final int TYPE_NONE = 0;
/** 数字密码 (PIN) */
public static final int TYPE_PIN = 1;
/** 手势密码 (Pattern) */
public static final int TYPE_PATTERN = 2;
private SecurityManager(Context context) {
mContext = context.getApplicationContext();
}
/**
*
* @param context
* @return SecurityManager
*/
public static synchronized SecurityManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new SecurityManager(context);
}
return sInstance;
}
/**
*
* @return true
*/
public boolean isPasswordSet() {
return getPasswordType() != TYPE_NONE;
}
/**
*
* @return (TYPE_NONE, TYPE_PIN, TYPE_PATTERN)
*/
public int getPasswordType() {
SharedPreferences prefs = mContext.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
return prefs.getInt(PREF_PASSWORD_TYPE, TYPE_NONE);
}
/**
*
* @param input
* @return true
*/
public boolean checkPassword(String input) {
if (!isPasswordSet()) return true;
if (input == null) return false;
String hash = getHash(input);
SharedPreferences prefs = mContext.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
String savedHash = prefs.getString(PREF_PASSWORD_HASH, "");
return TextUtils.equals(hash, savedHash);
}
/**
*
* @param input
* @param type
*/
public void setPassword(String input, int type) {
SharedPreferences prefs = mContext.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(PREF_PASSWORD_TYPE, type);
editor.putString(PREF_PASSWORD_HASH, getHash(input));
editor.commit();
}
/**
*
*/
public void removePassword() {
SharedPreferences prefs = mContext.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(PREF_PASSWORD_TYPE, TYPE_NONE);
editor.remove(PREF_PASSWORD_HASH);
editor.commit();
}
/**
* SHA-256
* @param input
* @return Base64
*/
private String getHash(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes());
return Base64.encodeToString(hash, Base64.NO_WRAP);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return input; // 理论上不会发生
}
}
}

@ -0,0 +1,256 @@
package net.micode.notes.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
/**
*
* <p>
* 3x3
*
* </p>
*/
public class LockPatternView extends View {
private Paint mPaintNormal;
private Paint mPaintSelected;
private Paint mPaintError;
private Paint mPaintPath;
private Cell[][] mCells = new Cell[3][3];
private List<Cell> mSelectedCells = new ArrayList<>();
private float mRadius;
private boolean mInputEnabled = true;
private DisplayMode mDisplayMode = DisplayMode.Correct;
private OnPatternListener mOnPatternListener;
public enum DisplayMode {
Correct, Animate, Wrong
}
public interface OnPatternListener {
void onPatternStart();
void onPatternCleared();
void onPatternCellAdded(List<Cell> pattern);
void onPatternDetected(List<Cell> pattern);
}
public static class Cell {
int row;
int column;
float x;
float y;
public Cell(int row, int column) {
this.row = row;
this.column = column;
}
public int getIndex() {
return row * 3 + column;
}
@Override
public String toString() {
return String.valueOf(getIndex());
}
}
public LockPatternView(Context context) {
this(context, null);
}
public LockPatternView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaintNormal = new Paint();
mPaintNormal.setAntiAlias(true);
mPaintNormal.setColor(Color.LTGRAY);
mPaintNormal.setStyle(Paint.Style.FILL);
mPaintSelected = new Paint();
mPaintSelected.setAntiAlias(true);
mPaintSelected.setColor(Color.BLUE); // Default selection color
mPaintSelected.setStyle(Paint.Style.FILL);
mPaintError = new Paint();
mPaintError.setAntiAlias(true);
mPaintError.setColor(Color.RED);
mPaintError.setStyle(Paint.Style.FILL);
mPaintPath = new Paint();
mPaintPath.setAntiAlias(true);
mPaintPath.setStrokeWidth(10f);
mPaintPath.setStyle(Paint.Style.STROKE);
mPaintPath.setStrokeCap(Paint.Cap.ROUND);
mPaintPath.setStrokeJoin(Paint.Join.ROUND);
mPaintPath.setColor(Color.BLUE); // Default path color
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int width = w - getPaddingLeft() - getPaddingRight();
int height = h - getPaddingTop() - getPaddingBottom();
float cellWidth = width / 3f;
float cellHeight = height / 3f;
mRadius = Math.min(cellWidth, cellHeight) * 0.15f; // Radius of the dots
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
mCells[i][j] = new Cell(i, j);
mCells[i][j].x = getPaddingLeft() + j * cellWidth + cellWidth / 2;
mCells[i][j].y = getPaddingTop() + i * cellHeight + cellHeight / 2;
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw path
if (!mSelectedCells.isEmpty()) {
Path path = new Path();
Cell first = mSelectedCells.get(0);
path.moveTo(first.x, first.y);
for (int i = 1; i < mSelectedCells.size(); i++) {
Cell cell = mSelectedCells.get(i);
path.lineTo(cell.x, cell.y);
}
if (mDisplayMode == DisplayMode.Wrong) {
mPaintPath.setColor(Color.RED);
} else {
mPaintPath.setColor(Color.BLUE); // Or Theme color
}
canvas.drawPath(path, mPaintPath);
}
// Draw cells
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
Cell cell = mCells[i][j];
drawCell(canvas, cell);
}
}
}
private void drawCell(Canvas canvas, Cell cell) {
boolean isSelected = mSelectedCells.contains(cell);
if (isSelected) {
if (mDisplayMode == DisplayMode.Wrong) {
canvas.drawCircle(cell.x, cell.y, mRadius, mPaintError);
} else {
canvas.drawCircle(cell.x, cell.y, mRadius, mPaintSelected);
}
} else {
canvas.drawCircle(cell.x, cell.y, mRadius, mPaintNormal);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mInputEnabled || !isEnabled()) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
clearPattern();
if (mOnPatternListener != null) {
mOnPatternListener.onPatternStart();
}
handleActionMove(event);
return true;
case MotionEvent.ACTION_MOVE:
handleActionMove(event);
return true;
case MotionEvent.ACTION_UP:
if (mOnPatternListener != null) {
mOnPatternListener.onPatternDetected(mSelectedCells);
}
return true;
}
return false;
}
private void handleActionMove(MotionEvent event) {
float x = event.getX();
float y = event.getY();
Cell cell = detectCell(x, y);
if (cell != null && !mSelectedCells.contains(cell)) {
mSelectedCells.add(cell);
if (mOnPatternListener != null) {
mOnPatternListener.onPatternCellAdded(mSelectedCells);
}
invalidate();
}
}
private Cell detectCell(float x, float y) {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
Cell cell = mCells[i][j];
double dist = Math.sqrt(Math.pow(x - cell.x, 2) + Math.pow(y - cell.y, 2));
// Use a larger detection radius than visual radius for better UX
if (dist < mRadius * 3) {
return cell;
}
}
}
return null;
}
public void setOnPatternListener(OnPatternListener listener) {
mOnPatternListener = listener;
}
public void setDisplayMode(DisplayMode mode) {
mDisplayMode = mode;
invalidate();
}
public void clearPattern() {
mSelectedCells.clear();
mDisplayMode = DisplayMode.Correct;
invalidate();
if (mOnPatternListener != null) {
mOnPatternListener.onPatternCleared();
}
}
public void setInputEnabled(boolean enabled) {
mInputEnabled = enabled;
}
/**
* (e.g., "012")
*/
public static String patternToString(List<Cell> pattern) {
if (pattern == null) return "";
StringBuilder sb = new StringBuilder();
for (Cell cell : pattern) {
sb.append(cell.getIndex());
}
return sb.toString();
}
}

@ -23,10 +23,10 @@ import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.ImageButton;
import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
@ -211,8 +211,10 @@ public class NoteInfoAdapter extends BaseAdapter {
holder = new ViewHolder();
holder.title = convertView.findViewById(R.id.tv_title);
holder.time = convertView.findViewById(R.id.tv_time);
holder.typeIcon = convertView.findViewById(R.id.iv_type_icon);
holder.checkBox = convertView.findViewById(android.R.id.checkbox);
holder.pinnedIcon = convertView.findViewById(R.id.iv_pinned_icon);
holder.lockIcon = convertView.findViewById(R.id.iv_lock_icon);
convertView.setTag(holder);
convertView.setOnClickListener(v -> {
@ -255,7 +257,19 @@ public class NoteInfoAdapter extends BaseAdapter {
}
holder.title.setText(title);
holder.time.setText(formatDate(note.modifiedDate));
// 设置类型图标和时间显示
if (note.type == Notes.TYPE_FOLDER) {
// 文件夹
holder.typeIcon.setVisibility(View.VISIBLE);
holder.typeIcon.setImageResource(R.drawable.ic_folder);
// 文件夹不显示时间
holder.time.setVisibility(View.GONE);
} else {
// 便签
holder.typeIcon.setVisibility(View.GONE);
holder.time.setVisibility(View.VISIBLE);
holder.time.setText(formatDate(note.modifiedDate));
}
int bgResId;
int totalCount = getCount();
@ -300,6 +314,12 @@ public class NoteInfoAdapter extends BaseAdapter {
} else {
holder.pinnedIcon.setVisibility(View.GONE);
}
if (note.isLocked) {
holder.lockIcon.setVisibility(View.VISIBLE);
} else {
holder.lockIcon.setVisibility(View.GONE);
}
}
return convertView;
@ -322,6 +342,8 @@ public class NoteInfoAdapter extends BaseAdapter {
private static class ViewHolder {
TextView title;
TextView time;
ImageView typeIcon;
ImageView lockIcon;
CheckBox checkBox;
ImageView pinnedIcon;
int position;

@ -54,6 +54,7 @@ import androidx.lifecycle.ViewModelProvider;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.tool.SecurityManager;
import net.micode.notes.ui.NoteInfoAdapter;
import net.micode.notes.viewmodel.NotesListViewModel;
@ -82,6 +83,7 @@ public class NotesListActivity extends AppCompatActivity
private static final String TAG = "NotesListActivity";
private static final int REQUEST_CODE_OPEN_NODE = 102;
private static final int REQUEST_CODE_NEW_NODE = 103;
private static final int REQUEST_CODE_CHECK_PASSWORD_FOR_OPEN = 104;
private NotesListViewModel viewModel;
private ListView notesListView;
@ -94,6 +96,9 @@ public class NotesListActivity extends AppCompatActivity
// 多选模式状态
private boolean isMultiSelectMode = false;
// 待打开的受保护笔记
private NotesRepository.NoteInfo mPendingNoteToOpen;
/**
*
@ -255,6 +260,7 @@ public class NotesListActivity extends AppCompatActivity
@Override
public void onChanged(List<NotesRepository.NoteInfo> notes) {
updateAdapter(notes);
updateToolbarForNormalMode();
}
});
@ -417,7 +423,10 @@ public class NotesListActivity extends AppCompatActivity
Log.d(TAG, "Normal mode, checking item type");
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) adapter.getItem(position);
if (note != null) {
if (note.type == Notes.TYPE_FOLDER) {
if (viewModel.isTrashMode()) {
// 回收站模式:弹出恢复/删除对话框
showTrashItemDialog(note);
} else if (note.type == Notes.TYPE_FOLDER) {
// 文件夹:进入该文件夹
Log.d(TAG, "Folder clicked, entering folder: " + note.getId());
viewModel.enterFolder(note.getId());
@ -453,6 +462,50 @@ public class NotesListActivity extends AppCompatActivity
}
}
/**
*
*/
private void showTrashItemDialog(NotesRepository.NoteInfo note) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("操作确认");
builder.setMessage("要恢复还是永久删除此笔记?");
builder.setPositiveButton("恢复", (dialog, which) -> {
// 临时将选中的ID设为当前ID以便复用ViewModel的restoreSelectedNotes
// 这里为了简单我们直接调用ViewModel的新方法restoreNote(note.getId())
// 但ViewModel还没这个方法所以我们先手动构造一个List
viewModel.clearSelection();
viewModel.toggleNoteSelection(note.getId(), true);
viewModel.restoreSelectedNotes();
});
builder.setNegativeButton("永久删除", (dialog, which) -> {
showDeleteForeverConfirmDialog(note);
});
builder.setNeutralButton("再想想", null);
builder.show();
}
/**
*
*/
private void showDeleteForeverConfirmDialog(NotesRepository.NoteInfo note) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("永久删除");
builder.setMessage("确定要永久删除此笔记吗?删除后无法恢复!");
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setPositiveButton("确定", (dialog, which) -> {
viewModel.clearSelection();
viewModel.toggleNoteSelection(note.getId(), true);
viewModel.deleteSelectedNotesForever();
});
// 设置“确定”按钮颜色为深色通常系统默认就是强调色如果需要特定颜色需自定义View或Theme
// 这里使用默认样式通常Positive是强调色
builder.setNegativeButton("取消", null);
builder.show();
}
/**
*
*/
@ -522,6 +575,12 @@ public class NotesListActivity extends AppCompatActivity
// 使用上传图标代替置顶图标,或者如果有合适的资源可以使用
pinItem.setIcon(android.R.drawable.ic_menu_upload);
pinItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
// 锁定按钮
boolean allLocked = viewModel.isAllSelectedLocked();
MenuItem lockItem = menu.add(Menu.NONE, R.id.multi_select_lock, 4, getString(R.string.menu_lock));
lockItem.setIcon(android.R.drawable.ic_lock_lock);
lockItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
/**
@ -530,8 +589,17 @@ public class NotesListActivity extends AppCompatActivity
private void updateToolbarForNormalMode() {
if (toolbar == null) return;
// 设置标题为应用名称
toolbar.setTitle(R.string.app_name);
// 清除多选模式菜单
toolbar.getMenu().clear();
// 设置标题
if (viewModel.isTrashMode()) {
toolbar.setTitle(R.string.menu_trash);
} else {
toolbar.setTitle(R.string.app_name);
// 添加普通模式菜单
toolbar.inflateMenu(R.menu.note_list);
}
// 设置导航图标为汉堡菜单
toolbar.setNavigationIcon(android.R.drawable.ic_menu_sort_by_size);
@ -541,11 +609,16 @@ public class NotesListActivity extends AppCompatActivity
}
});
// 清除多选模式菜单
toolbar.getMenu().clear();
// 添加普通模式菜单(如果需要)
// getMenuInflater().inflate(R.menu.note_list_options, menu);
// 如果是回收站模式,不显示新建按钮
if (viewModel.isTrashMode()) {
if (fabNewNote != null) {
fabNewNote.setVisibility(View.GONE);
}
} else {
if (fabNewNote != null) {
fabNewNote.setVisibility(View.VISIBLE);
}
}
}
@ -587,6 +660,15 @@ public class NotesListActivity extends AppCompatActivity
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE) {
viewModel.refreshNotes();
} else if (requestCode == REQUEST_CODE_CHECK_PASSWORD_FOR_OPEN) {
if (mPendingNoteToOpen != null) {
if (mPendingNoteToOpen.type == Notes.TYPE_FOLDER) {
viewModel.enterFolder(mPendingNoteToOpen.getId());
} else {
openNoteEditor(mPendingNoteToOpen);
}
mPendingNoteToOpen = null;
}
}
}
}
@ -641,6 +723,20 @@ public class NotesListActivity extends AppCompatActivity
String toastMsg = wasPinned ? getString(R.string.menu_unpin) + "成功" : getString(R.string.menu_pin) + "成功";
Toast.makeText(this, toastMsg, Toast.LENGTH_SHORT).show();
return true;
case R.id.multi_select_lock:
// 检查是否设置了隐私密码
if (SecurityManager.getInstance(this).isPasswordSet()) {
boolean wasLocked = viewModel.isAllSelectedLocked();
viewModel.toggleSelectedNotesLock();
String lockMsg = wasLocked ? getString(R.string.menu_unlock) + "成功" : getString(R.string.menu_lock) + "成功";
Toast.makeText(this, lockMsg, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "请先在设置中设置隐私密码", Toast.LENGTH_SHORT).show();
// 跳转到设置密码界面
Intent intent = new Intent(this, NotesPreferenceActivity.class);
startActivity(intent);
}
return true;
default:
return super.onOptionsItemSelected(item);
}
@ -714,8 +810,8 @@ public class NotesListActivity extends AppCompatActivity
@Override
public void onTrashSelected() {
// TODO: 实现跳转到回收站
Log.d(TAG, "Trash selected");
// 跳转到回收站
viewModel.enterFolder(Notes.ID_TRASH_FOLER);
// 关闭侧栏
if (drawerLayout != null) {
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
@ -745,9 +841,13 @@ public class NotesListActivity extends AppCompatActivity
@Override
public void onSettingsSelected() {
// TODO: 实现设置功能
Log.d(TAG, "Settings selected");
Toast.makeText(this, "设置功能待实现", Toast.LENGTH_SHORT).show();
// 打开设置页面
Intent intent = new Intent(this, NotesPreferenceActivity.class);
startActivity(intent);
// 关闭侧栏
if (drawerLayout != null) {
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
}
}
@Override

@ -46,9 +46,8 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.remote.GTaskSyncService;
import android.os.Build; // 用于版本检查
import android.content.Context; // 用于 RECEIVER_NOT_EXPORTED 常量
import net.micode.notes.tool.SecurityManager;
import net.micode.notes.ui.PasswordActivity;
/**
@ -87,6 +86,9 @@ public class NotesPreferenceActivity extends PreferenceActivity {
*/
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
public static final String PREFERENCE_SECURITY_KEY = "pref_key_security";
public static final int REQUEST_CODE_CHECK_PASSWORD = 104;
/**
* Preference
*/
@ -153,6 +155,8 @@ public class NotesPreferenceActivity extends PreferenceActivity {
mOriAccounts = null;
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
getListView().addHeaderView(header, null, true);
loadSecurityPreference();
}
/**
@ -204,6 +208,60 @@ public class NotesPreferenceActivity extends PreferenceActivity {
super.onDestroy();
}
private void loadSecurityPreference() {
Preference securityPref = findPreference(PREFERENCE_SECURITY_KEY);
if (securityPref != null) {
securityPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
if (!SecurityManager.getInstance(NotesPreferenceActivity.this).isPasswordSet()) {
showSetPasswordDialog();
} else {
Intent intent = new Intent(NotesPreferenceActivity.this, PasswordActivity.class);
intent.setAction(PasswordActivity.ACTION_CHECK_PASSWORD);
startActivityForResult(intent, REQUEST_CODE_CHECK_PASSWORD);
}
return true;
}
});
}
}
private void showSetPasswordDialog() {
new AlertDialog.Builder(this)
.setTitle("设置密码")
.setItems(new String[]{"数字锁", "手势锁"}, (dialog, which) -> {
int type = (which == 0) ? SecurityManager.TYPE_PIN : SecurityManager.TYPE_PATTERN;
Intent intent = new Intent(this, PasswordActivity.class);
intent.setAction(PasswordActivity.ACTION_SETUP_PASSWORD);
intent.putExtra(PasswordActivity.EXTRA_PASSWORD_TYPE, type);
startActivity(intent);
})
.show();
}
private void showManagePasswordDialog() {
new AlertDialog.Builder(this)
.setTitle("管理密码")
.setItems(new String[]{"更改密码", "取消密码"}, (dialog, which) -> {
if (which == 0) { // Change
showSetPasswordDialog();
} else { // Remove
SecurityManager.getInstance(this).removePassword();
Toast.makeText(this, "密码已取消", Toast.LENGTH_SHORT).show();
}
})
.show();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CHECK_PASSWORD && resultCode == RESULT_OK) {
showManagePasswordDialog();
}
}
/**
*
* <p>

@ -0,0 +1,180 @@
package net.micode.notes.ui;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.tool.SecurityManager;
import java.util.List;
public class PasswordActivity extends Activity {
public static final String ACTION_SETUP_PASSWORD = "net.micode.notes.action.SETUP_PASSWORD";
public static final String ACTION_CHECK_PASSWORD = "net.micode.notes.action.CHECK_PASSWORD";
public static final String EXTRA_PASSWORD_TYPE = "extra_password_type";
private int mMode; // 0: Check, 1: Setup
private int mPasswordType;
private TextView mTvPrompt;
private EditText mEtPin;
private LockPatternView mLockPatternView;
private TextView mTvError;
private Button mBtnCancel;
private String mFirstInput = null; // For setup confirmation
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_password);
String action = getIntent().getAction();
if (ACTION_SETUP_PASSWORD.equals(action)) {
mMode = 1;
mPasswordType = getIntent().getIntExtra(EXTRA_PASSWORD_TYPE, SecurityManager.TYPE_PIN);
} else {
mMode = 0;
// Check mode: get type from SecurityManager
mPasswordType = SecurityManager.getInstance(this).getPasswordType();
}
initViews();
setupViews();
}
private void initViews() {
mTvPrompt = findViewById(R.id.tv_prompt);
mEtPin = findViewById(R.id.et_pin);
mLockPatternView = findViewById(R.id.lock_pattern_view);
mTvError = findViewById(R.id.tv_error);
mBtnCancel = findViewById(R.id.btn_cancel);
mBtnCancel.setOnClickListener(v -> {
setResult(RESULT_CANCELED);
finish();
});
}
private void setupViews() {
if (mMode == 1) { // Setup
mTvPrompt.setText("请设置密码");
} else { // Check
mTvPrompt.setText("请输入密码");
}
if (mPasswordType == SecurityManager.TYPE_PIN) {
mEtPin.setVisibility(View.VISIBLE);
mLockPatternView.setVisibility(View.GONE);
mEtPin.requestFocus(); // Auto focus
setupPinLogic();
} else if (mPasswordType == SecurityManager.TYPE_PATTERN) {
mEtPin.setVisibility(View.GONE);
mLockPatternView.setVisibility(View.VISIBLE);
setupPatternLogic();
} else {
// Should not happen
finish();
}
}
private void setupPinLogic() {
mEtPin.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE ||
(event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN)) {
handleInput(mEtPin.getText().toString());
return true;
}
return false;
});
}
private void setupPatternLogic() {
mLockPatternView.setOnPatternListener(new LockPatternView.OnPatternListener() {
@Override
public void onPatternStart() {
mTvError.setVisibility(View.INVISIBLE);
}
@Override
public void onPatternCleared() {}
@Override
public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {}
@Override
public void onPatternDetected(List<LockPatternView.Cell> pattern) {
if (pattern.size() < 3) {
mTvError.setText("连接至少3个点");
mTvError.setVisibility(View.VISIBLE);
mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
return;
}
handleInput(LockPatternView.patternToString(pattern));
}
});
}
private void handleInput(String input) {
if (TextUtils.isEmpty(input)) return;
mTvError.setVisibility(View.INVISIBLE);
if (mMode == 0) { // Check
if (SecurityManager.getInstance(this).checkPassword(input)) {
setResult(RESULT_OK);
finish();
} else {
mTvError.setText("密码错误");
mTvError.setVisibility(View.VISIBLE);
if (mPasswordType == SecurityManager.TYPE_PATTERN) {
mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
} else {
mEtPin.setText("");
}
}
} else { // Setup
if (mFirstInput == null) {
// First entry
mFirstInput = input;
mTvPrompt.setText("请再次输入以确认");
if (mPasswordType == SecurityManager.TYPE_PATTERN) {
mLockPatternView.clearPattern();
} else {
mEtPin.setText("");
}
} else {
// Second entry
if (mFirstInput.equals(input)) {
SecurityManager.getInstance(this).setPassword(input, mPasswordType);
Toast.makeText(this, "密码设置成功", Toast.LENGTH_SHORT).show();
setResult(RESULT_OK);
finish();
} else {
mTvError.setText("两次输入不一致,请重试");
mTvError.setVisibility(View.VISIBLE);
// Reset to start
mFirstInput = null;
mTvPrompt.setText("请设置密码");
if (mPasswordType == SecurityManager.TYPE_PATTERN) {
mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
mLockPatternView.postDelayed(() -> mLockPatternView.clearPattern(), 1000);
} else {
mEtPin.setText("");
}
}
}
}
}
}

@ -569,6 +569,140 @@ public class NotesListViewModel extends ViewModel {
return true;
}
/**
*
*/
public void toggleSelectedNotesLock() {
if (selectedNoteIds.isEmpty()) {
errorMessage.postValue("请先选择要操作的笔记");
return;
}
isLoading.postValue(true);
errorMessage.postValue(null);
// 如果有未锁定的,则全部锁定;否则全部解锁
final boolean newLockState = !isAllSelectedLocked();
List<Long> noteIds = new ArrayList<>(selectedNoteIds);
repository.batchLock(noteIds, newLockState, new NotesRepository.Callback<Integer>() {
@Override
public void onSuccess(Integer rowsAffected) {
isLoading.postValue(false);
refreshNotes();
Log.d(TAG, "Successfully toggled lock state to " + newLockState);
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "锁定操作失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
*
*
* @return true
*/
public boolean isAllSelectedLocked() {
if (selectedNoteIds.isEmpty()) return false;
List<NotesRepository.NoteInfo> allNotes = notesLiveData.getValue();
if (allNotes == null) return false;
for (NotesRepository.NoteInfo note : allNotes) {
if (selectedNoteIds.contains(note.getId())) {
if (!note.isLocked) {
return false;
}
}
}
return true;
}
/**
*
* <p>
*
* </p>
*/
public void restoreSelectedNotes() {
if (selectedNoteIds.isEmpty()) {
errorMessage.postValue("请先选择要恢复的笔记");
return;
}
isLoading.postValue(true);
errorMessage.postValue(null);
List<Long> noteIds = new ArrayList<>(selectedNoteIds);
repository.restoreNotes(noteIds, new NotesRepository.Callback<Integer>() {
@Override
public void onSuccess(Integer rowsAffected) {
isLoading.postValue(false);
selectedNoteIds.clear();
refreshNotes();
Log.d(TAG, "Successfully restored " + rowsAffected + " notes");
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "恢复失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
*
* <p>
*
* </p>
*/
public void deleteSelectedNotesForever() {
if (selectedNoteIds.isEmpty()) {
errorMessage.postValue("请先选择要删除的笔记");
return;
}
isLoading.postValue(true);
errorMessage.postValue(null);
List<Long> noteIds = new ArrayList<>(selectedNoteIds);
repository.deleteNotesForever(noteIds, new NotesRepository.Callback<Integer>() {
@Override
public void onSuccess(Integer rowsAffected) {
isLoading.postValue(false);
selectedNoteIds.clear();
refreshNotes();
Log.d(TAG, "Successfully permanently deleted " + rowsAffected + " notes");
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "永久删除失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
*
*
* @return true
*/
public boolean isTrashMode() {
return currentFolderId == Notes.ID_TRASH_FOLER;
}
/**
* ViewModel
* <p>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFC107"
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/>
</vector>

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="20dp"
android:background="@android:color/white">
<TextView
android:id="@+id/tv_prompt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="请输入密码"
android:textSize="20sp"
android:textColor="@android:color/black"
android:layout_marginTop="40dp"
android:layout_marginBottom="40dp"/>
<EditText
android:id="@+id/et_pin"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:inputType="numberPassword"
android:maxLength="6"
android:gravity="center"
android:textSize="24sp"
android:visibility="gone"/>
<net.micode.notes.ui.LockPatternView
android:id="@+id/lock_pattern_view"
android:layout_width="300dp"
android:layout_height="300dp"
android:visibility="gone"/>
<TextView
android:id="@+id/tv_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/holo_red_dark"
android:textSize="14sp"
android:visibility="invisible"
android:layout_marginTop="20dp"
android:text="密码错误"/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
<Button
android:id="@+id/btn_cancel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@android:string/cancel"/>
</LinearLayout>

@ -31,6 +31,14 @@
android:orientation="horizontal"
android:gravity="center_vertical">
<!-- 类型图标(文件夹/便签) -->
<ImageView
android:id="@+id/iv_type_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="8dp"
android:visibility="gone" />
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
@ -43,6 +51,15 @@
android:ellipsize="end"
android:singleLine="true" />
<ImageView
android:id="@+id/iv_lock_icon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="4dp"
android:src="@android:drawable/ic_lock_lock"
android:tint="@android:color/darker_gray"
android:visibility="gone" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"

@ -23,4 +23,10 @@
android:title="@string/menu_pin"
android:showAsAction="always" />
<item
android:id="@+id/multi_select_lock"
android:icon="@android:drawable/ic_lock_lock"
android:title="@string/menu_lock"
android:showAsAction="ifRoom" />
</menu>

@ -153,5 +153,12 @@
<string name="error_folder_name_exists">Folder already exists</string>
<string name="create_folder_success">Folder created successfully</string>
<string name="menu_pin">Pin</string>
<string name="menu_lock">Lock</string>
<string name="lock_confirmation">确定需要为其上锁?</string>
<string name="lock_confirm_button">确定</string>
<string name="lock_cancel_button">再想想</string>
<string name="delete_confirmation">Are you sure you want to delete selected notes?</string>
<string name="menu_unpin">Unpin</string>
<string name="menu_unlock">Unlock</string>
</resources>

@ -27,4 +27,12 @@
android:title="@string/preferences_bg_random_appear_title"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="Safe">
<Preference
android:key="pref_key_security"
android:title="Security Settings"
android:summary="Manage password lock"
android:icon="@android:drawable/ic_lock_lock" />
</PreferenceCategory>
</PreferenceScreen>

@ -2,30 +2,21 @@
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects.
# For more details, visit
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonFinalResIds=false
# 测试配置
android.testOptions.unitTests.isReturnDefaultValues=true

@ -1,5 +1,5 @@
[versions]
agp = "8.12.0"
agp = "8.13.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
@ -7,7 +7,7 @@ appcompat = "1.6.1"
material = "1.10.0"
activity = "1.8.0"
constraintlayout = "2.1.4"
mockito = "4.11.0"
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
@ -17,9 +17,7 @@ appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "a
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" }
mockito-junit = { group = "org.mockito", name = "mockito-junit-jupiter", version.ref = "mockito" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
Loading…
Cancel
Save