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);
+ }
+ });
+ }
+
/**
* 搜索笔记
*
@@ -859,6 +939,45 @@ public class NotesRepository {
});
}
+ /**
+ * 批量更新笔记锁定状态
+ *
+ * @param noteIds 笔记ID列表
+ * @param isLocked 是否锁定
+ * @param callback 回调接口
+ */
+ public void batchLock(List noteIds, boolean isLocked, Callback 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);
+ }
+ });
+ }
+
/**
* 从内容中提取摘要
*
diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/SecurityManager.java b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/SecurityManager.java
new file mode 100644
index 0000000..4ae87c2
--- /dev/null
+++ b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/SecurityManager.java
@@ -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;
+
+/**
+ * 安全管理器
+ *
+ * 负责管理应用的隐私锁功能,包括密码的设置、验证、清除以及密码类型的管理。
+ * 使用SHA-256对密码进行哈希存储,保障安全性。
+ *
+ */
+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; // 理论上不会发生
+ }
+ }
+}
diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LockPatternView.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LockPatternView.java
new file mode 100644
index 0000000..c068516
--- /dev/null
+++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LockPatternView.java
@@ -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;
+
+/**
+ * 自定义手势密码控件
+ *
+ * 实现一个 3x3 的九宫格手势解锁视图。
+ * 支持触摸绘制路径,并提供回调接口。
+ *
+ */
+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 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 pattern);
+ void onPatternDetected(List 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 pattern) {
+ if (pattern == null) return "";
+ StringBuilder sb = new StringBuilder();
+ for (Cell cell : pattern) {
+ sb.append(cell.getIndex());
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteInfoAdapter.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteInfoAdapter.java
index 79200de..3d56955 100644
--- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteInfoAdapter.java
+++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteInfoAdapter.java
@@ -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;
diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
index 067fc53..14a7c24 100644
--- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
+++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
@@ -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 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
diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java
index fe02819..2de6ec3 100644
--- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java
+++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java
@@ -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();
+ }
+ }
+
/**
* 加载账户设置选项
*
diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/PasswordActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/PasswordActivity.java
new file mode 100644
index 0000000..d52117a
--- /dev/null
+++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/PasswordActivity.java
@@ -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 pattern) {}
+
+ @Override
+ public void onPatternDetected(List 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("");
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java b/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java
index 38daea0..d6f854b 100644
--- a/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java
+++ b/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java
@@ -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 noteIds = new ArrayList<>(selectedNoteIds);
+
+ repository.batchLock(noteIds, newLockState, new NotesRepository.Callback() {
+ @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 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;
+ }
+
+ /**
+ * 恢复选中的笔记
+ *
+ * 将选中的回收站笔记移回根目录
+ *
+ */
+ public void restoreSelectedNotes() {
+ if (selectedNoteIds.isEmpty()) {
+ errorMessage.postValue("请先选择要恢复的笔记");
+ return;
+ }
+
+ isLoading.postValue(true);
+ errorMessage.postValue(null);
+
+ List noteIds = new ArrayList<>(selectedNoteIds);
+ repository.restoreNotes(noteIds, new NotesRepository.Callback() {
+ @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);
+ }
+ });
+ }
+
+ /**
+ * 永久删除选中的笔记
+ *
+ * 物理删除选中的笔记
+ *
+ */
+ public void deleteSelectedNotesForever() {
+ if (selectedNoteIds.isEmpty()) {
+ errorMessage.postValue("请先选择要删除的笔记");
+ return;
+ }
+
+ isLoading.postValue(true);
+ errorMessage.postValue(null);
+
+ List noteIds = new ArrayList<>(selectedNoteIds);
+ repository.deleteNotesForever(noteIds, new NotesRepository.Callback() {
+ @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销毁时的清理
*
diff --git a/src/Notesmaster/app/src/main/res/drawable/ic_folder.xml b/src/Notesmaster/app/src/main/res/drawable/ic_folder.xml
new file mode 100644
index 0000000..1e53dbe
--- /dev/null
+++ b/src/Notesmaster/app/src/main/res/drawable/ic_folder.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/src/Notesmaster/app/src/main/res/layout/activity_password.xml b/src/Notesmaster/app/src/main/res/layout/activity_password.xml
new file mode 100644
index 0000000..d9cc0c7
--- /dev/null
+++ b/src/Notesmaster/app/src/main/res/layout/activity_password.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Notesmaster/app/src/main/res/layout/note_item.xml b/src/Notesmaster/app/src/main/res/layout/note_item.xml
index b23af8f..a479806 100644
--- a/src/Notesmaster/app/src/main/res/layout/note_item.xml
+++ b/src/Notesmaster/app/src/main/res/layout/note_item.xml
@@ -31,6 +31,14 @@
android:orientation="horizontal"
android:gravity="center_vertical">
+
+
+
+
+
+
+
diff --git a/src/Notesmaster/app/src/main/res/values/strings.xml b/src/Notesmaster/app/src/main/res/values/strings.xml
index a4b4991..c6f38f4 100644
--- a/src/Notesmaster/app/src/main/res/values/strings.xml
+++ b/src/Notesmaster/app/src/main/res/values/strings.xml
@@ -153,5 +153,12 @@
Folder already exists
Folder created successfully
Pin
+ Lock
+ 确定需要为其上锁?
+ 确定
+ 再想想
+
+ Are you sure you want to delete selected notes?
Unpin
+ Unlock
diff --git a/src/Notesmaster/app/src/main/res/xml/preferences.xml b/src/Notesmaster/app/src/main/res/xml/preferences.xml
index fe58f8f..1ae2a83 100644
--- a/src/Notesmaster/app/src/main/res/xml/preferences.xml
+++ b/src/Notesmaster/app/src/main/res/xml/preferences.xml
@@ -27,4 +27,12 @@
android:title="@string/preferences_bg_random_appear_title"
android:defaultValue="false" />
+
+
+
+
diff --git a/src/Notesmaster/gradle.properties b/src/Notesmaster/gradle.properties
index 547122f..fd5ffa1 100644
--- a/src/Notesmaster/gradle.properties
+++ b/src/Notesmaster/gradle.properties
@@ -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
diff --git a/src/Notesmaster/gradle/libs.versions.toml b/src/Notesmaster/gradle/libs.versions.toml
index 468f3be..d912124 100644
--- a/src/Notesmaster/gradle/libs.versions.toml
+++ b/src/Notesmaster/gradle/libs.versions.toml
@@ -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" }
\ No newline at end of file
| | | |