实现MVVM结构

pull/12/head
包尔俊 3 months ago
parent 4cc62124bd
commit 11217cb843

5
.gitignore vendored

@ -13,3 +13,8 @@
.externalNativeBuild
.cxx
local.properties
# OpenCode
.opencode/
opencode.json
.opencode.backup

@ -7,6 +7,9 @@ android {
compileSdk {
version = release(36)
}
buildFeatures {
viewBinding = true
}
defaultConfig {
applicationId = "net.micode.notes"

@ -42,7 +42,7 @@
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Holo.Light"
android:theme="@style/Theme.Notesmaster"
android:uiOptions="splitActionBarWhenNarrow"
android:windowSoftInputMode="adjustPan"
android:exported="true">

@ -0,0 +1,650 @@
/*
* Copyright (c) 2025, Modern Notes Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.model.Note;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
/**
*
* <p>
* 访Content Provider
*
* </p>
* <p>
* 使Executor线访UI线
* </p>
*
* @see Note
* @see Notes
*/
public class NotesRepository {
/**
*
* <p>
*
* </p>
*/
public static class NoteInfo {
public long id;
public String title;
public String snippet;
public long parentId;
public long createdDate;
public long modifiedDate;
public int type;
public int localModified;
public int bgColorId;
public NoteInfo() {}
public long getId() {
return id;
}
public long getParentId() {
return parentId;
}
public String getNoteDataValue() {
return snippet;
}
}
private static final String TAG = "NotesRepository";
private final ContentResolver contentResolver;
private final ExecutorService executor;
// 选择条件常量
private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + " = ?";
private static final String ROOT_FOLDER_SELECTION = "(" +
NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " +
NoteColumns.PARENT_ID + "=?) OR (" +
NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " +
NoteColumns.NOTES_COUNT + ">0)";
/**
* 访
* <p>
* 访
* </p>
*
* @param <T>
*/
public interface Callback<T> {
/**
*
*
* @param result
*/
void onSuccess(T result);
/**
*
*
* @param error
*/
void onError(Exception error);
}
/**
* Cursor NoteInfo
*
* @param cursor
* @return NoteInfo
*/
private NoteInfo noteFromCursor(Cursor cursor) {
NoteInfo noteInfo = new NoteInfo();
noteInfo.id = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.ID));
noteInfo.title = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.SNIPPET));
noteInfo.snippet = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.SNIPPET));
noteInfo.parentId = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.PARENT_ID));
noteInfo.createdDate = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.CREATED_DATE));
noteInfo.modifiedDate = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.MODIFIED_DATE));
noteInfo.type = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.TYPE));
noteInfo.localModified = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.LOCAL_MODIFIED));
int bgColorIdIndex = cursor.getColumnIndex(NoteColumns.BG_COLOR_ID);
if (bgColorIdIndex != -1 && !cursor.isNull(bgColorIdIndex)) {
noteInfo.bgColorId = cursor.getInt(bgColorIdIndex);
} else {
noteInfo.bgColorId = 0;
}
return noteInfo;
}
/**
*
* <p>
* ContentResolver线
* </p>
*
* @param contentResolver Content
*/
public NotesRepository(ContentResolver contentResolver) {
this.contentResolver = contentResolver;
// 使用单线程Executor确保数据访问的顺序性
this.executor = java.util.concurrent.Executors.newSingleThreadExecutor();
Log.d(TAG, "NotesRepository initialized");
}
/**
*
* <p>
*
* </p>
*
* @param folderId ID{@link Notes#ID_ROOT_FOLDER}
* @param callback
*/
public void getNotes(long folderId, Callback<List<NoteInfo>> callback) {
executor.execute(() -> {
try {
List<NoteInfo> notes = queryNotes(folderId);
callback.onSuccess(notes);
Log.d(TAG, "Successfully loaded notes for folder: " + folderId);
} catch (Exception e) {
Log.e(TAG, "Failed to load notes for folder: " + folderId, e);
callback.onError(e);
}
});
}
/**
*
*
* @param folderId ID
* @return
*/
private List<NoteInfo> queryNotes(long folderId) {
List<NoteInfo> notes = new ArrayList<>();
String selection;
String[] selectionArgs;
if (folderId == Notes.ID_ROOT_FOLDER) {
// 根文件夹:显示所有非系统笔记和有内容的通话记录文件夹
selection = ROOT_FOLDER_SELECTION;
selectionArgs = new String[]{String.valueOf(Notes.ID_ROOT_FOLDER)};
} else {
// 子文件夹:只显示该文件夹下的笔记
selection = NORMAL_SELECTION;
selectionArgs = new String[]{String.valueOf(folderId)};
}
Cursor cursor = contentResolver.query(
Notes.CONTENT_NOTE_URI,
null,
selection,
selectionArgs,
NoteColumns.MODIFIED_DATE + " DESC"
);
if (cursor != null) {
try {
while (cursor.moveToNext()) {
notes.add(noteFromCursor(cursor));
}
Log.d(TAG, "Query returned " + cursor.getCount() + " notes");
} finally {
cursor.close();
}
}
return notes;
}
/**
*
* <p>
*
* </p>
*
* @param folderId ID
* @param callback ID
*/
public void createNote(long folderId, Callback<Long> callback) {
executor.execute(() -> {
try {
ContentValues values = new ContentValues();
long currentTime = System.currentTimeMillis();
values.put(NoteColumns.PARENT_ID, folderId);
values.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
values.put(NoteColumns.CREATED_DATE, currentTime);
values.put(NoteColumns.MODIFIED_DATE, currentTime);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.SNIPPET, "");
Uri uri = contentResolver.insert(Notes.CONTENT_NOTE_URI, values);
Long noteId = 0L;
if (uri != null) {
try {
noteId = ContentUris.parseId(uri);
} catch (Exception e) {
Log.e(TAG, "Failed to parse note ID from URI", e);
}
}
if (noteId > 0) {
callback.onSuccess(noteId);
Log.d(TAG, "Successfully created note with ID: " + noteId);
} else {
callback.onError(new IllegalStateException("Failed to create note, invalid ID returned"));
}
} catch (Exception e) {
Log.e(TAG, "Failed to create note", e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param noteId ID
* @param content
* @param callback
*/
public void updateNote(long noteId, String content, Callback<Integer> callback) {
executor.execute(() -> {
try {
ContentValues values = new ContentValues();
long currentTime = System.currentTimeMillis();
values.put(NoteColumns.SNIPPET, extractSnippet(content));
values.put(NoteColumns.MODIFIED_DATE, currentTime);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
int rows = contentResolver.update(uri, values, null, null);
if (rows > 0) {
// 查询现有的文本数据记录
Cursor cursor = contentResolver.query(
Notes.CONTENT_DATA_URI,
new String[]{DataColumns.ID},
DataColumns.NOTE_ID + " = ? AND " + DataColumns.MIME_TYPE + " = ?",
new String[]{String.valueOf(noteId), TextNote.CONTENT_ITEM_TYPE},
null
);
long dataId = 0;
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
dataId = cursor.getLong(cursor.getColumnIndexOrThrow(DataColumns.ID));
}
} finally {
cursor.close();
}
}
// 更新或插入文本数据
ContentValues dataValues = new ContentValues();
dataValues.put(DataColumns.CONTENT, content);
if (dataId > 0) {
// 更新现有记录
Uri dataUri = ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId);
int dataRows = contentResolver.update(dataUri, dataValues, null, null);
if (dataRows > 0) {
callback.onSuccess(rows);
Log.d(TAG, "Successfully updated note: " + noteId);
} else {
callback.onError(new RuntimeException("Failed to update note data"));
}
} else {
// 插入新记录
dataValues.put(DataColumns.NOTE_ID, noteId);
dataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
Uri dataUri = contentResolver.insert(Notes.CONTENT_DATA_URI, dataValues);
if (dataUri != null) {
callback.onSuccess(rows);
Log.d(TAG, "Successfully updated note: " + noteId);
} else {
callback.onError(new RuntimeException("Failed to insert note data"));
}
}
} else {
callback.onError(new RuntimeException("No note found with ID: " + noteId));
}
} catch (Exception e) {
Log.e(TAG, "Failed to update note: " + noteId, e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param noteId ID
* @param callback
*/
public void deleteNote(long noteId, Callback<Integer> callback) {
executor.execute(() -> {
try {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
int rows = contentResolver.update(uri, values, null, null);
if (rows > 0) {
callback.onSuccess(rows);
Log.d(TAG, "Successfully moved note to trash: " + noteId);
} else {
callback.onError(new RuntimeException("No note found with ID: " + noteId));
}
} catch (Exception e) {
Log.e(TAG, "Failed to delete note: " + noteId, e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param noteIds ID
* @param callback
*/
public void deleteNotes(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) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, Notes.ID_TRASH_FOLER);
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 moved " + totalRows + " notes to trash");
} else {
callback.onError(new RuntimeException("No notes were deleted"));
}
} catch (Exception e) {
Log.e(TAG, "Failed to batch delete notes", e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param keyword
* @param callback
*/
public void searchNotes(String keyword, Callback<List<NoteInfo>> callback) {
executor.execute(() -> {
try {
if (keyword == null || keyword.trim().isEmpty()) {
callback.onSuccess(new ArrayList<>());
return;
}
String selection = "(" + NoteColumns.TYPE + " = ?) AND (" +
NoteColumns.SNIPPET + " LIKE ? OR " +
NoteColumns.ID + " IN (SELECT " + DataColumns.NOTE_ID +
" FROM data WHERE " + DataColumns.CONTENT + " LIKE ?))";
String[] selectionArgs = new String[]{
String.valueOf(Notes.TYPE_NOTE),
"%" + keyword + "%",
"%" + keyword + "%"
};
Cursor cursor = contentResolver.query(
Notes.CONTENT_NOTE_URI,
null,
selection,
selectionArgs,
NoteColumns.MODIFIED_DATE + " DESC"
);
List<NoteInfo> notes = new ArrayList<>();
if (cursor != null) {
try {
while (cursor.moveToNext()) {
notes.add(noteFromCursor(cursor));
}
Log.d(TAG, "Search returned " + cursor.getCount() + " results for: " + keyword);
} finally {
cursor.close();
}
}
callback.onSuccess(notes);
} catch (Exception e) {
Log.e(TAG, "Failed to search notes: " + keyword, e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param folderId ID
* @param callback
*/
public void countNotes(long folderId, Callback<Integer> callback) {
executor.execute(() -> {
try {
String selection;
String[] selectionArgs;
if (folderId == Notes.ID_ROOT_FOLDER) {
selection = NoteColumns.TYPE + " != ?";
selectionArgs = new String[]{String.valueOf(Notes.TYPE_FOLDER)};
} else {
selection = NoteColumns.PARENT_ID + " = ?";
selectionArgs = new String[]{String.valueOf(folderId)};
}
Cursor cursor = contentResolver.query(
Notes.CONTENT_NOTE_URI,
new String[]{"COUNT(*) AS count"},
selection,
selectionArgs,
null
);
int count = 0;
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
count = cursor.getInt(0);
}
} finally {
cursor.close();
}
}
callback.onSuccess(count);
Log.d(TAG, "Counted " + count + " notes in folder: " + folderId);
} catch (Exception e) {
Log.e(TAG, "Failed to count notes in folder: " + folderId, e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param callback
*/
public void getFolders(Callback<List<NoteInfo>> callback) {
executor.execute(() -> {
try {
String selection = NoteColumns.TYPE + " = ?";
String[] selectionArgs = new String[]{
String.valueOf(Notes.TYPE_FOLDER)
};
Cursor cursor = contentResolver.query(
Notes.CONTENT_NOTE_URI,
null,
selection,
selectionArgs,
NoteColumns.MODIFIED_DATE + " DESC"
);
List<NoteInfo> folders = new ArrayList<>();
if (cursor != null) {
try {
while (cursor.moveToNext()) {
folders.add(noteFromCursor(cursor));
}
Log.d(TAG, "Found " + cursor.getCount() + " folders");
} finally {
cursor.close();
}
}
callback.onSuccess(folders);
} catch (Exception e) {
Log.e(TAG, "Failed to load folders", e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param noteIds ID
* @param targetFolderId ID
* @param callback
*/
public void moveNotes(List<Long> noteIds, long targetFolderId, 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) {
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 moved " + totalRows + " notes to folder: " + targetFolderId);
} else {
callback.onError(new RuntimeException("No notes were moved"));
}
} catch (Exception e) {
Log.e(TAG, "Failed to move notes", e);
callback.onError(e);
}
});
}
/**
*
*
* @param content
* @return 100
*/
private String extractSnippet(String content) {
if (content == null || content.isEmpty()) {
return "";
}
int maxLength = 100;
return content.length() > maxLength
? content.substring(0, maxLength)
: content;
}
/**
* Executor
* <p>
* 访线
* </p>
*/
public void shutdown() {
if (executor != null && !executor.isShutdown()) {
executor.shutdown();
Log.d(TAG, "Executor shutdown");
}
}
}

@ -0,0 +1,320 @@
/*
* Copyright (c) 2025, Modern Notes Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.ImageButton;
import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
import android.util.Log;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
/**
* 便
* <p>
* List<NoteInfo> ListView
* 便
* </p>
* <p>
* 使 ViewHolder
* </p>
*/
public class NoteInfoAdapter extends BaseAdapter {
private LayoutInflater inflater;
private List<NotesRepository.NoteInfo> notes;
private HashSet<Long> selectedIds;
private OnNoteButtonClickListener buttonClickListener;
private OnNoteItemClickListener itemClickListener;
private OnNoteItemLongClickListener itemLongClickListener;
/**
* 便
*/
public interface OnNoteButtonClickListener {
/**
*
*
* @param position
* @param noteId 便 ID
*/
void onEditButtonClick(int position, long noteId);
}
/**
* 便
*/
public interface OnNoteItemClickListener {
void onNoteItemClick(int position, long noteId);
}
/**
* 便
*/
public interface OnNoteItemLongClickListener {
void onNoteItemLongClick(int position, long noteId);
}
/**
*
*
* @param context
*/
public NoteInfoAdapter(Context context) {
this.inflater = LayoutInflater.from(context);
this.notes = new ArrayList<>();
this.selectedIds = new HashSet<>();
}
/**
* 便
*
* @param notes 便
*/
public void setNotes(List<NotesRepository.NoteInfo> notes) {
this.notes = notes != null ? notes : new ArrayList<>();
notifyDataSetChanged();
}
/**
* 便 ID
* <p>
* ViewModel selectedNoteIds
* Adapter selectedIds
* </p>
*
* @param selectedIds 便 ID
*/
public void setSelectedIds(HashSet<Long> selectedIds) {
if (selectedIds != null && selectedIds != this.selectedIds) {
this.selectedIds.clear();
this.selectedIds.addAll(selectedIds);
notifyDataSetChanged();
} else if (selectedIds == null) {
this.selectedIds.clear();
notifyDataSetChanged();
}
}
/**
* 便 ID
* <p>
* List<Long> HashSet
* </p>
*
* @param selectedIds 便 ID
*/
public void setSelectedIds(List<Long> selectedIds) {
if (selectedIds != null && !selectedIds.isEmpty()) {
this.selectedIds.clear();
this.selectedIds.addAll(selectedIds);
notifyDataSetChanged();
} else {
this.selectedIds.clear();
notifyDataSetChanged();
}
}
/**
* 便 ID
*
* @return 便 ID
*/
public HashSet<Long> getSelectedIds() {
return selectedIds;
}
/**
*
*
* @param noteId 便 ID
*/
public void toggleSelection(long noteId) {
if (selectedIds.contains(noteId)) {
selectedIds.remove(noteId);
} else {
selectedIds.add(noteId);
}
notifyDataSetChanged();
}
/**
*
*
* @param listener
*/
public void setOnNoteButtonClickListener(OnNoteButtonClickListener listener) {
this.buttonClickListener = listener;
}
public void setOnNoteItemClickListener(OnNoteItemClickListener listener) {
this.itemClickListener = listener;
}
public void setOnNoteItemLongClickListener(OnNoteItemLongClickListener listener) {
this.itemLongClickListener = listener;
}
@Override
public int getCount() {
return notes.size();
}
@Override
public Object getItem(int position) {
return position >= 0 && position < notes.size() ? notes.get(position) : null;
}
@Override
public long getItemId(int position) {
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) getItem(position);
return note != null ? note.getId() : -1;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Log.d("NoteInfoAdapter", "getView called, position: " + position + ", convertView: " + (convertView != null ? "REUSED" : "NEW"));
ViewHolder holder;
if (convertView == null) {
convertView = inflater.inflate(R.layout.note_item, parent, false);
holder = new ViewHolder();
holder.title = convertView.findViewById(R.id.tv_title);
holder.time = convertView.findViewById(R.id.tv_time);
holder.checkBox = convertView.findViewById(android.R.id.checkbox);
convertView.setTag(holder);
convertView.setOnClickListener(v -> {
Log.d("NoteInfoAdapter", "===== onClick TRIGGERED =====");
ViewHolder currentHolder = (ViewHolder) v.getTag();
if (currentHolder != null && itemClickListener != null) {
Log.d("NoteInfoAdapter", "Calling itemClickListener");
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) getItem(currentHolder.position);
if (note != null) {
itemClickListener.onNoteItemClick(currentHolder.position, note.getId());
}
}
Log.d("NoteInfoAdapter", "===== onClick END =====");
});
convertView.setOnLongClickListener(v -> {
Log.d("NoteInfoAdapter", "===== setOnLongClickListener TRIGGERED =====");
Log.d("NoteInfoAdapter", "Event triggered on view: " + v.getClass().getSimpleName());
ViewHolder currentHolder = (ViewHolder) v.getTag();
if (currentHolder != null && itemLongClickListener != null) {
Log.d("NoteInfoAdapter", "Calling itemLongClickListener");
itemLongClickListener.onNoteItemLongClick(currentHolder.position, currentHolder.position < notes.size() ? notes.get(currentHolder.position).getId() : -1);
} else {
Log.e("NoteInfoAdapter", "itemLongClickListener is NULL!");
}
Log.d("NoteInfoAdapter", "===== setOnLongClickListener END =====");
return true;
});
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.position = position;
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) getItem(position);
if (note != null) {
String title = note.snippet;
if (title == null || title.trim().isEmpty()) {
title = "无标题";
}
holder.title.setText(title);
holder.time.setText(formatDate(note.modifiedDate));
int bgResId;
int totalCount = getCount();
int bgColorId = note.bgColorId;
if (totalCount == 1) {
bgResId = NoteItemBgResources.getNoteBgSingleRes(bgColorId);
} else if (position == 0) {
bgResId = NoteItemBgResources.getNoteBgFirstRes(bgColorId);
} else if (position == totalCount - 1) {
bgResId = NoteItemBgResources.getNoteBgLastRes(bgColorId);
} else {
bgResId = NoteItemBgResources.getNoteBgNormalRes(bgColorId);
}
convertView.setBackgroundResource(bgResId);
if (selectedIds.contains(note.getId())) {
convertView.setActivated(true);
} else {
convertView.setActivated(false);
}
Log.d("NoteInfoAdapter", "===== Setting checkbox visibility =====");
Log.d("NoteInfoAdapter", "selectedIds.isEmpty(): " + selectedIds.isEmpty());
Log.d("NoteInfoAdapter", "selectedIds.size(): " + selectedIds.size());
Log.d("NoteInfoAdapter", "selectedIds contains note " + note.getId() + ": " + selectedIds.contains(note.getId()));
if (!selectedIds.isEmpty()) {
Log.d("NoteInfoAdapter", "Setting checkbox VISIBLE");
holder.checkBox.setVisibility(View.VISIBLE);
holder.checkBox.setChecked(selectedIds.contains(note.getId()));
holder.checkBox.setClickable(false);
} else {
Log.d("NoteInfoAdapter", "Setting checkbox GONE");
holder.checkBox.setVisibility(View.GONE);
}
Log.d("NoteInfoAdapter", "===== Checkbox visibility set =====");
}
return convertView;
}
/**
*
*
* @param timestamp
* @return
*/
private String formatDate(long timestamp) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
return sdf.format(new Date(timestamp));
}
/**
* ViewHolder ListView
*/
private static class ViewHolder {
TextView title;
TextView time;
CheckBox checkBox;
int position;
}
}

@ -0,0 +1,439 @@
/*
* Copyright (c) 2025, Modern Notes Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.viewmodel;
import android.util.Log;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesRepository;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* ViewModel
* <p>
* UIActivity
*
* </p>
*
* @see NotesRepository
* @see Note
*/
public class NotesListViewModel extends ViewModel {
private static final String TAG = "NotesListViewModel";
private final NotesRepository repository;
// 笔记列表LiveData
private final MutableLiveData<List<NotesRepository.NoteInfo>> notesLiveData = new MutableLiveData<>();
// 加载状态LiveData
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
// 错误消息LiveData
private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
// 选中的笔记ID集合
private final HashSet<Long> selectedNoteIds = new HashSet<>();
// 当前文件夹ID
private long currentFolderId = Notes.ID_ROOT_FOLDER;
/**
*
*
* @param repository
*/
public NotesListViewModel(NotesRepository repository) {
this.repository = repository;
Log.d(TAG, "ViewModel created");
}
/**
* LiveData
*
* @return LiveData
*/
public MutableLiveData<List<NotesRepository.NoteInfo>> getNotesLiveData() {
return notesLiveData;
}
/**
* LiveData
*
* @return LiveData
*/
public MutableLiveData<Boolean> getIsLoading() {
return isLoading;
}
/**
* LiveData
*
* @return LiveData
*/
public MutableLiveData<String> getErrorMessage() {
return errorMessage;
}
/**
*
* <p>
*
* </p>
*
* @param folderId ID{@link Notes#ID_ROOT_FOLDER}
*/
public void loadNotes(long folderId) {
this.currentFolderId = folderId;
isLoading.postValue(true);
errorMessage.postValue(null);
repository.getNotes(folderId, new NotesRepository.Callback<List<NotesRepository.NoteInfo>>() {
@Override
public void onSuccess(List<NotesRepository.NoteInfo> notes) {
isLoading.postValue(false);
notesLiveData.postValue(notes);
Log.d(TAG, "Successfully loaded " + notes.size() + " 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 refreshNotes() {
loadNotes(currentFolderId);
}
/**
*
* <p>
*
* </p>
*/
public void createNote() {
isLoading.postValue(true);
errorMessage.postValue(null);
repository.createNote(currentFolderId, new NotesRepository.Callback<Long>() {
@Override
public void onSuccess(Long noteId) {
isLoading.postValue(false);
Log.d(TAG, "Successfully created note with ID: " + noteId);
refreshNotes();
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "创建笔记失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param noteId ID
*/
public void deleteNote(long noteId) {
isLoading.postValue(true);
errorMessage.postValue(null);
repository.deleteNote(noteId, new NotesRepository.Callback<Integer>() {
@Override
public void onSuccess(Integer rowsAffected) {
isLoading.postValue(false);
selectedNoteIds.remove(noteId);
refreshNotes();
Log.d(TAG, "Successfully deleted note: " + noteId);
}
@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 deleteSelectedNotes() {
if (selectedNoteIds.isEmpty()) {
errorMessage.postValue("请先选择要删除的笔记");
return;
}
isLoading.postValue(true);
errorMessage.postValue(null);
List<Long> noteIds = new ArrayList<>(selectedNoteIds);
repository.deleteNotes(noteIds, new NotesRepository.Callback<Integer>() {
@Override
public void onSuccess(Integer rowsAffected) {
isLoading.postValue(false);
selectedNoteIds.clear();
refreshNotes();
Log.d(TAG, "Successfully deleted " + 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>
*
* @param keyword
*/
public void searchNotes(String keyword) {
isLoading.postValue(true);
errorMessage.postValue(null);
repository.searchNotes(keyword, new NotesRepository.Callback<List<NotesRepository.NoteInfo>>() {
@Override
public void onSuccess(List<NotesRepository.NoteInfo> notes) {
isLoading.postValue(false);
notesLiveData.postValue(notes);
Log.d(TAG, "Search returned " + notes.size() + " results");
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "搜索失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
*
*
* @param noteId ID
* @param selected
*/
public void toggleNoteSelection(long noteId, boolean selected) {
if (selected) {
selectedNoteIds.add(noteId);
} else {
selectedNoteIds.remove(noteId);
}
}
/**
*
* <p>
*
* </p>
*/
public void selectAllNotes() {
List<NotesRepository.NoteInfo> notes = notesLiveData.getValue();
if (notes != null) {
for (NotesRepository.NoteInfo note : notes) {
selectedNoteIds.add(note.getId());
}
}
}
/**
*
* <p>
*
* </p>
*/
public void deselectAllNotes() {
selectedNoteIds.clear();
}
/**
*
*
* @return true
*/
public boolean isAllSelected() {
List<NotesRepository.NoteInfo> notes = notesLiveData.getValue();
if (notes == null || notes.isEmpty()) {
return false;
}
return notes.size() == selectedNoteIds.size();
}
/**
*
*
* @return
*/
public int getSelectedCount() {
return selectedNoteIds.size();
}
/**
* ID
*
* @return ID
*/
public List<Long> getSelectedNoteIds() {
return new ArrayList<>(selectedNoteIds);
}
/**
* ID
*
* @return ID
*/
public long getCurrentFolderId() {
return currentFolderId;
}
/**
*
*
* @param folderId ID
*/
public void setCurrentFolderId(long folderId) {
this.currentFolderId = folderId;
}
/**
*
* <p>
* 退
* </p>
*/
public void clearSelection() {
selectedNoteIds.clear();
}
/**
*
* <p>
*
* </p>
*/
public void loadFolders() {
isLoading.postValue(true);
errorMessage.postValue(null);
repository.getFolders(new NotesRepository.Callback<List<NotesRepository.NoteInfo>>() {
@Override
public void onSuccess(List<NotesRepository.NoteInfo> folders) {
isLoading.postValue(false);
notesLiveData.postValue(folders);
Log.d(TAG, "Successfully loaded " + folders.size() + " folders");
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "加载文件夹失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param targetFolderId ID
*/
public void moveSelectedNotesToFolder(long targetFolderId) {
if (selectedNoteIds.isEmpty()) {
errorMessage.postValue("请先选择要移动的笔记");
return;
}
isLoading.postValue(true);
errorMessage.postValue(null);
List<Long> noteIds = new ArrayList<>(selectedNoteIds);
repository.moveNotes(noteIds, targetFolderId, new NotesRepository.Callback<Integer>() {
@Override
public void onSuccess(Integer rowsAffected) {
isLoading.postValue(false);
selectedNoteIds.clear();
refreshNotes();
Log.d(TAG, "Successfully moved " + rowsAffected + " notes");
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "移动笔记失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
* ViewModel
* <p>
*
* </p>
*/
@Override
protected void onCleared() {
super.onCleared();
selectedNoteIds.clear();
Log.d(TAG, "ViewModel cleared");
}
}

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@android:color/white">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@android:color/darker_gray">
<path
android:fillColor="#666666"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@android:color/darker_gray">
<path
android:fillColor="#666666"
android:pathData="M3,18h12v-2H3v2zM3,6v2h18V6H3zM3,13h18v-2H3v2z"/>
</vector>

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- 便签项背景:可点击效果 -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="true">
<shape android:shape="rectangle">
<solid android:color="#BBDEFB"/>
</shape>
</item>
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#E0E0E0"/>
</shape>
</item>
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="#BBDEFB"/>
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#FFFFFF"/>
</shape>
</item>
</selector>

@ -15,51 +15,62 @@
limitations under the License.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/note_item"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
android:background="@null">
<!-- 标题行 -->
<LinearLayout
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="horizontal"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dip"
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
android:textSize="16sp"
android:textColor="@android:color/black"
android:textStyle="bold"
android:maxLines="1"
android:ellipsize="end"
android:singleLine="true" />
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="0dip"
android:layout_weight="1"
android:textAppearance="@style/TextAppearancePrimaryItem"
android:visibility="gone" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical">
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textSize="12sp"
android:textColor="@android:color/darker_gray" />
</LinearLayout>
<TextView
android:id="@+id/tv_title"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true" />
<!-- 通话名称行(用于通话记录) -->
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textSize="14sp"
android:textColor="@android:color/darker_gray"
android:maxLines="1"
android:ellipsize="end"
android:visibility="gone" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearanceSecondaryItem" />
</LinearLayout>
</LinearLayout>
<!-- 底部控制行:复选框和提醒图标 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<CheckBox
android:id="@android:id/checkbox"
@ -68,11 +79,13 @@
android:focusable="false"
android:clickable="false"
android:visibility="gone" />
<ImageView
android:id="@+id/iv_alert_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:visibility="gone" />
</LinearLayout>
<ImageView
android:id="@+id/iv_alert_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|right"/>
</FrameLayout>
</LinearLayout>

@ -15,44 +15,66 @@
limitations under the License.
-->
<FrameLayout
<!-- 现代化布局:使用 CoordinatorLayout + AppBarLayout + Toolbar -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/list_background">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<!-- AppBarLayout替代传统 ActionBar -->
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.Material3.ActionBar">
<!-- Toolbar现代替代 ActionBar 的标准组件 -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/app_name"
app:layout_scrollFlags="scroll|enterAlways|snap" />
</com.google.android.material.appbar.AppBarLayout>
<!-- 便签列表:使用 NestedScrollView 包裹 ListView 以支持滚动 -->
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<TextView
android:id="@+id/tv_title_bar"
android:layout_width="fill_parent"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/title_bar_bg"
android:visibility="gone"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="#FFEAD1AE"
android:textSize="@dimen/text_font_size_medium" />
<ListView
android:id="@+id/notes_list"
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:cacheColorHint="@null"
android:listSelector="@android:color/transparent"
android:divider="@null"
android:fadingEdge="@null" />
</LinearLayout>
<Button
android:orientation="vertical">
<!-- 便签列表 -->
<ListView
android:id="@+id/notes_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="200dp"
android:cacheColorHint="@null"
android:listSelector="@android:color/transparent"
android:divider="@null"
android:fadingEdge="@null" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<!-- 悬浮按钮:替代原来的底部按钮 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_new_note"
android:background="@drawable/new_note"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:layout_gravity="bottom" />
</FrameLayout>
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/notelist_menu_new"
app:srcCompat="@android:drawable/ic_input_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -17,4 +17,7 @@
<resources>
<color name="user_query_highlight">#335b5b5b</color>
<color name="primary_color">#1976D2</color>
<color name="on_primary_color">#FFFFFF</color>
<color name="background_color">#FAFAFA</color>
</resources>

@ -81,6 +81,8 @@
<string name="error_note_not_exist">The note is not exist</string>
<string name="error_note_empty_for_clock">Sorry, can not set clock on empty note</string>
<string name="error_note_empty_for_send_to_desktop">Sorry, can not send and empty note to home</string>
<string name="error_intent_invalid">Invalid intent</string>
<string name="error_intent_unsupported">Unsupported intent action</string>
<string name="success_sdcard_export">Export successful</string>
<string name="failed_sdcard_export">Export fail</string>
<string name="format_exported_file_location">Export text file (%1$s) to SD (%2$s) directory</string>
@ -132,4 +134,7 @@
<item quantity="other"><xliff:g id="number" example="15">%1$s</xliff:g> results for \"<xliff:g id="search" example="???">%2$s</xliff:g>\"</item>
</plurals>
<string name="empty_notes_hint">暂无便签,点击右下角按钮创建</string>
<string name="empty_notes_icon">空便签图标</string>
<string name="menu_edit_note">Edit note</string>
</resources>

@ -1,8 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Notesmaster" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
<item name="colorPrimary">@color/primary_color</item>
<item name="colorOnPrimary">@color/on_primary_color</item>
<item name="android:statusBarColor">@color/primary_color</item>
</style>
<style name="Theme.Notesmaster" parent="Base.Theme.Notesmaster" />

@ -0,0 +1,254 @@
/*
* Copyright (c) 2025, Modern Notes Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.content.ContentResolver;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import net.micode.notes.model.Note;
import java.util.List;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* NotesRepository
* <p>
*
* </p>
*
* @see NotesRepository
*/
@RunWith(MockitoJUnitRunner.class)
public class NotesRepositoryTest {
private static final String TAG = "NotesRepositoryTest";
@Mock
private ContentResolver mockContentResolver;
private NotesRepository repository;
@Before
public void setUp() {
repository = new NotesRepository(mockContentResolver);
}
@After
public void tearDown() {
repository.shutdown();
}
/**
* Repository
*/
@Test
public void testRepositoryCreation() {
assertNotNull("Repository should not be null", repository);
}
/**
*
*/
@Test
public void testGetNotes() {
// Arrange
long folderId = Notes.ID_ROOT_FOLDER;
// Act
repository.getNotes(folderId, new NotesRepository.Callback<List<Note>>() {
@Override
public void onSuccess(List<Note> result) {
assertNotNull("Notes list should not be null", result);
// Mock返回空列表实际应用中会有数据
assertTrue("Notes count should be >= 0", result.size() >= 0);
}
@Override
public void onError(Exception error) {
fail("Should not throw error: " + error.getMessage());
}
});
}
/**
*
*/
@Test
public void testCreateNote() {
// Arrange
long folderId = Notes.ID_ROOT_FOLDER;
// Act
repository.createNote(folderId, new NotesRepository.Callback<Long>() {
@Override
public void onSuccess(Long noteId) {
assertNotNull("Note ID should not be null", noteId);
assertTrue("Note ID should be > 0", noteId > 0);
}
@Override
public void onError(Exception error) {
fail("Should not throw error: " + error.getMessage());
}
});
}
/**
*
*/
@Test
public void testUpdateNote() {
// Arrange
long noteId = 1L;
String content = "测试笔记内容";
// Act
repository.updateNote(noteId, content, new NotesRepository.Callback<Integer>() {
@Override
public void onSuccess(Integer rowsAffected) {
assertNotNull("Rows affected should not be null", rowsAffected);
assertTrue("Rows affected should be >= 0", rowsAffected >= 0);
}
@Override
public void onError(Exception error) {
fail("Should not throw error: " + error.getMessage());
}
});
}
/**
*
*/
@Test
public void testDeleteNote() {
// Arrange
long noteId = 1L;
// Act
repository.deleteNote(noteId, new NotesRepository.Callback<Integer>() {
@Override
public void onSuccess(Integer rowsAffected) {
assertNotNull("Rows affected should not be null", rowsAffected);
assertTrue("Rows affected should be >= 0", rowsAffected >= 0);
}
@Override
public void onError(Exception error) {
fail("Should not throw error: " + error.getMessage());
}
});
}
/**
*
*/
@Test
public void testSearchNotes() {
// Arrange
String keyword = "测试";
// Act
repository.searchNotes(keyword, new NotesRepository.Callback<List<Note>>() {
@Override
public void onSuccess(List<Note> result) {
assertNotNull("Search results should not be null", result);
assertTrue("Search results count should be >= 0", result.size() >= 0);
}
@Override
public void onError(Exception error) {
fail("Should not throw error: " + error.getMessage());
}
});
}
/**
*
*/
@Test
public void testSearchNotesWithEmptyKeyword() {
// Arrange
String keyword = "";
// Act
repository.searchNotes(keyword, new NotesRepository.Callback<List<Note>>() {
@Override
public void onSuccess(List<Note> result) {
assertNotNull("Search results should not be null", result);
// 空关键字应返回空列表
assertEquals("Search results should be empty", 0, result.size());
}
@Override
public void onError(Exception error) {
fail("Should not throw error: " + error.getMessage());
}
});
}
/**
*
*/
@Test
public void testCountNotes() {
// Arrange
long folderId = Notes.ID_ROOT_FOLDER;
// Act
repository.countNotes(folderId, new NotesRepository.Callback<Integer>() {
@Override
public void onSuccess(Integer count) {
assertNotNull("Count should not be null", count);
assertTrue("Count should be >= 0", count >= 0);
}
@Override
public void onError(Exception error) {
fail("Should not throw error: " + error.getMessage());
}
});
}
/**
*
*/
@Test
public void testGetFolders() {
// Act
repository.getFolders(new NotesRepository.Callback<List<Note>>() {
@Override
public void onSuccess(List<Note> result) {
assertNotNull("Folders should not be null", result);
assertTrue("Folders count should be >= 0", result.size() >= 0);
}
@Override
public void onError(Exception error) {
fail("Should not throw error: " + error.getMessage());
}
});
}
}
Loading…
Cancel
Save