Merge pull request '将之前没有提交的代码文件提交' (#30) from jiangtianxiang_branch into master

pull/32/head
p7tupf26b 4 weeks ago
commit 795bee106a

@ -0,0 +1,137 @@
package net.micode.notes.model;
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.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
public class Task {
private static final String TAG = "Task";
public long id;
public String snippet; // Content
public long createdDate;
public long modifiedDate;
public int priority; // 0=Low, 1=Mid, 2=High
public long dueDate;
public int status; // 0=Active, 1=Completed
public long finishedTime;
public long alertDate;
public static final int PRIORITY_LOW = 0;
public static final int PRIORITY_NORMAL = 1;
public static final int PRIORITY_HIGH = 2;
public static final int STATUS_ACTIVE = 0;
public static final int STATUS_COMPLETED = 1;
public Task() {
id = 0;
snippet = "";
createdDate = System.currentTimeMillis();
modifiedDate = System.currentTimeMillis();
priority = PRIORITY_LOW;
dueDate = 0;
status = STATUS_ACTIVE;
finishedTime = 0;
alertDate = 0;
}
public static Task fromCursor(Cursor cursor) {
Task task = new Task();
// Use getColumnIndex instead of getColumnIndexOrThrow for safety
int idxId = cursor.getColumnIndex(NoteColumns.ID);
if (idxId != -1) task.id = cursor.getLong(idxId);
int idxSnippet = cursor.getColumnIndex(NoteColumns.SNIPPET);
if (idxSnippet != -1) task.snippet = cursor.getString(idxSnippet);
int idxCreated = cursor.getColumnIndex(NoteColumns.CREATED_DATE);
if (idxCreated != -1) task.createdDate = cursor.getLong(idxCreated);
int idxModified = cursor.getColumnIndex(NoteColumns.MODIFIED_DATE);
if (idxModified != -1) task.modifiedDate = cursor.getLong(idxModified);
int idxAlert = cursor.getColumnIndex(NoteColumns.ALERTED_DATE);
if (idxAlert != -1) task.alertDate = cursor.getLong(idxAlert);
int idxPriority = cursor.getColumnIndex(NoteColumns.GTASK_PRIORITY);
if (idxPriority != -1) task.priority = cursor.getInt(idxPriority);
int idxDueDate = cursor.getColumnIndex(NoteColumns.GTASK_DUE_DATE);
if (idxDueDate != -1) task.dueDate = cursor.getLong(idxDueDate);
int idxStatus = cursor.getColumnIndex(NoteColumns.GTASK_STATUS);
if (idxStatus != -1) task.status = cursor.getInt(idxStatus);
int idxFinished = cursor.getColumnIndex(NoteColumns.GTASK_FINISHED_TIME);
if (idxFinished != -1) task.finishedTime = cursor.getLong(idxFinished);
return task;
}
public Uri save(Context context) {
ContentValues values = new ContentValues();
values.put(NoteColumns.TYPE, Notes.TYPE_TASK);
values.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
values.put(NoteColumns.ALERTED_DATE, alertDate);
values.put(NoteColumns.GTASK_PRIORITY, priority);
values.put(NoteColumns.GTASK_DUE_DATE, dueDate);
values.put(NoteColumns.GTASK_STATUS, status);
values.put(NoteColumns.GTASK_FINISHED_TIME, finishedTime);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
// Ensure snippet is updated in note table too, though trigger might handle it,
// explicit update is safer if trigger fails or data table logic changes.
values.put(NoteColumns.SNIPPET, snippet);
if (id == 0) {
values.put(NoteColumns.CREATED_DATE, System.currentTimeMillis());
values.put(NoteColumns.PARENT_ID, Notes.ID_ROOT_FOLDER);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
if (uri != null) {
id = ContentUris.parseId(uri);
updateData(context);
}
return uri;
} else {
context.getContentResolver().update(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id),
values, null, null
);
updateData(context);
return ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id);
}
}
private void updateData(Context context) {
ContentValues values = new ContentValues();
values.put(DataColumns.NOTE_ID, id);
values.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
values.put(DataColumns.CONTENT, snippet);
values.put(DataColumns.MODIFIED_DATE, System.currentTimeMillis());
Cursor c = context.getContentResolver().query(Notes.CONTENT_DATA_URI, new String[]{DataColumns.ID},
DataColumns.NOTE_ID + "=?", new String[]{String.valueOf(id)}, null);
if (c != null) {
if (c.moveToFirst()) {
long dataId = c.getLong(0);
context.getContentResolver().update(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), values, null, null);
} else {
context.getContentResolver().insert(Notes.CONTENT_DATA_URI, values);
}
c.close();
} else {
context.getContentResolver().insert(Notes.CONTENT_DATA_URI, values);
}
}
}

@ -0,0 +1,205 @@
package net.micode.notes.ui;
import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.model.Task;
import java.util.Calendar;
public class TaskEditActivity extends AppCompatActivity {
private EditText contentEdit;
private ImageView alarmBtn;
private ImageView tagBtn;
private Button doneBtn;
private Task task;
private long taskId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_task_edit);
contentEdit = findViewById(R.id.task_edit_content);
alarmBtn = findViewById(R.id.btn_alarm);
tagBtn = findViewById(R.id.btn_tag);
doneBtn = findViewById(R.id.btn_done);
Intent intent = getIntent();
taskId = intent.getLongExtra(Intent.EXTRA_UID, 0);
if (taskId > 0) {
loadTask();
} else {
task = new Task();
}
setupListeners();
}
private void loadTask() {
new Thread(() -> {
Cursor cursor = getContentResolver().query(
Notes.CONTENT_NOTE_URI,
null,
NoteColumns.ID + "=?",
new String[]{String.valueOf(taskId)},
null
);
if (cursor != null) {
if (cursor.moveToFirst()) {
task = Task.fromCursor(cursor);
runOnUiThread(() -> {
contentEdit.setText(task.snippet);
contentEdit.setSelection(task.snippet.length());
});
} else {
task = new Task();
}
cursor.close();
} else {
task = new Task();
}
}).start();
}
private void setupListeners() {
doneBtn.setOnClickListener(v -> {
saveTask();
setResult(RESULT_OK);
finish();
});
alarmBtn.setOnClickListener(v -> {
showAlarmDialog();
});
tagBtn.setOnClickListener(v -> {
showTagDialog();
});
}
@Override
public void onBackPressed() {
if (saveTask()) {
setResult(RESULT_OK);
}
super.onBackPressed();
}
private boolean saveTask() {
String content = contentEdit.getText().toString();
if (content.trim().length() == 0) {
if (task.id == 0) {
return false;
}
}
task.snippet = content;
task.save(this);
// Register Alarm if needed.
if (task.alertDate > 0 && task.alertDate > System.currentTimeMillis()) {
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(android.content.ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, task.id));
android.app.PendingIntent pendingIntent = android.app.PendingIntent.getBroadcast(this, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT | android.app.PendingIntent.FLAG_IMMUTABLE);
android.app.AlarmManager alarmManager = (android.app.AlarmManager) getSystemService(ALARM_SERVICE);
alarmManager.set(android.app.AlarmManager.RTC_WAKEUP, task.alertDate, pendingIntent);
}
return true;
}
private void showAlarmDialog() {
final Calendar c = Calendar.getInstance();
if (task.alertDate > 0) {
c.setTimeInMillis(task.alertDate);
}
new DatePickerDialog(this, (view, year, month, dayOfMonth) -> {
c.set(Calendar.YEAR, year);
c.set(Calendar.MONTH, month);
c.set(Calendar.DAY_OF_MONTH, dayOfMonth);
new TimePickerDialog(this, (view1, hourOfDay, minute) -> {
c.set(Calendar.HOUR_OF_DAY, hourOfDay);
c.set(Calendar.MINUTE, minute);
c.set(Calendar.SECOND, 0);
task.alertDate = c.getTimeInMillis();
Toast.makeText(this, "Alarm set", Toast.LENGTH_SHORT).show();
}, c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), true).show();
}, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)).show();
}
private void showTagDialog() {
View view = LayoutInflater.from(this).inflate(R.layout.dialog_task_tag, null);
RadioGroup priorityGroup = view.findViewById(R.id.priority_group);
TextView dateText = view.findViewById(R.id.date_text);
Button dateBtn = view.findViewById(R.id.btn_set_date);
if (task.priority == Task.PRIORITY_HIGH) priorityGroup.check(R.id.priority_high);
else if (task.priority == Task.PRIORITY_NORMAL) priorityGroup.check(R.id.priority_mid);
else priorityGroup.check(R.id.priority_low);
final Calendar c = Calendar.getInstance();
if (task.dueDate > 0) {
c.setTimeInMillis(task.dueDate);
dateText.setText(android.text.format.DateFormat.format("yyyy-MM-dd HH:mm", c));
} else {
dateText.setText("No Due Date");
}
dateBtn.setOnClickListener(v -> {
new DatePickerDialog(this, (dView, year, month, dayOfMonth) -> {
c.set(Calendar.YEAR, year);
c.set(Calendar.MONTH, month);
c.set(Calendar.DAY_OF_MONTH, dayOfMonth);
new TimePickerDialog(this, (tView, hourOfDay, minute) -> {
c.set(Calendar.HOUR_OF_DAY, hourOfDay);
c.set(Calendar.MINUTE, minute);
task.dueDate = c.getTimeInMillis();
dateText.setText(android.text.format.DateFormat.format("yyyy-MM-dd HH:mm", c));
}, c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), true).show();
}, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)).show();
});
new AlertDialog.Builder(this)
.setTitle("Set Tag")
.setView(view)
.setPositiveButton("OK", (dialog, which) -> {
int id = priorityGroup.getCheckedRadioButtonId();
if (id == R.id.priority_high) task.priority = Task.PRIORITY_HIGH;
else if (id == R.id.priority_mid) task.priority = Task.PRIORITY_NORMAL;
else task.priority = Task.PRIORITY_LOW;
})
.setNegativeButton("Cancel", null)
.show();
}
}

@ -0,0 +1,156 @@
package net.micode.notes.ui;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.model.Task;
import java.util.ArrayList;
import java.util.List;
public class TaskListActivity extends AppCompatActivity implements TaskListAdapter.OnTaskItemClickListener {
private RecyclerView recyclerView;
private TaskListAdapter adapter;
private FloatingActionButton fab;
private static final int REQUEST_EDIT_TASK = 1001;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_task_list);
// Initialize Toolbar
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// Remove default title
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayShowTitleEnabled(false);
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
// Setup custom "Notes" navigation
View notesTitle = findViewById(R.id.tv_toolbar_title_notes);
if (notesTitle != null) {
notesTitle.setOnClickListener(v -> {
finish();
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);
});
}
// Setup Navigation Icon (Back Button) - REMOVED as per new requirement
// toolbar.setNavigationOnClickListener(v -> {
// finish();
// overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);
// });
recyclerView = findViewById(R.id.task_list_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new TaskListAdapter(this, this);
recyclerView.setAdapter(adapter);
fab = findViewById(R.id.btn_new_task);
fab.setOnClickListener(v -> {
Intent intent = new Intent(TaskListActivity.this, TaskEditActivity.class);
startActivityForResult(intent, REQUEST_EDIT_TASK);
});
}
@Override
protected void onResume() {
super.onResume();
loadTasks();
}
private void loadTasks() {
new Thread(() -> {
Cursor cursor = getContentResolver().query(
Notes.CONTENT_NOTE_URI,
null,
NoteColumns.TYPE + "=?",
new String[]{String.valueOf(Notes.TYPE_TASK)},
null
);
List<Task> tasks = new ArrayList<>();
if (cursor != null) {
while (cursor.moveToNext()) {
tasks.add(Task.fromCursor(cursor));
}
cursor.close();
}
android.util.Log.d("TaskListActivity", "Loaded tasks count: " + tasks.size());
runOnUiThread(() -> adapter.setTasks(tasks));
}).start();
}
@Override
public void onItemClick(Task task) {
Intent intent = new Intent(this, TaskEditActivity.class);
intent.putExtra(Intent.EXTRA_UID, task.id);
startActivityForResult(intent, REQUEST_EDIT_TASK);
}
@Override
public void onCheckBoxClick(Task task) {
task.status = (task.status == Task.STATUS_ACTIVE) ? Task.STATUS_COMPLETED : Task.STATUS_ACTIVE;
if (task.status == Task.STATUS_COMPLETED) {
task.finishedTime = System.currentTimeMillis();
} else {
task.finishedTime = 0;
}
new Thread(() -> {
task.save(this);
runOnUiThread(() -> loadTasks()); // Reload to sort
}).start();
}
@Override
public boolean onCreateOptionsMenu(android.view.Menu menu) {
// Removed menu_notes as per requirement, keeping empty or future menus
// getMenuInflater().inflate(R.menu.task_list, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
super.onBackPressed();
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_EDIT_TASK && resultCode == RESULT_OK) {
loadTasks();
}
}
}

@ -0,0 +1,168 @@
package net.micode.notes.ui;
import android.content.Context;
import android.graphics.Paint;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.model.Task;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class TaskListAdapter extends RecyclerView.Adapter<TaskListAdapter.TaskViewHolder> {
private List<Task> tasks = new ArrayList<>();
private Context context;
private OnTaskItemClickListener listener;
public interface OnTaskItemClickListener {
void onItemClick(Task task);
void onCheckBoxClick(Task task);
}
public TaskListAdapter(Context context, OnTaskItemClickListener listener) {
this.context = context;
this.listener = listener;
}
public void setTasks(List<Task> newTasks) {
this.tasks = new ArrayList<>(newTasks);
sortTasks();
notifyDataSetChanged();
}
private void sortTasks() {
Collections.sort(tasks, new Comparator<Task>() {
@Override
public int compare(Task t1, Task t2) {
// 1. Status: Active (0) < Completed (1)
if (t1.status != t2.status) {
return Integer.compare(t1.status, t2.status);
}
// 2. If both Active
if (t1.status == Task.STATUS_ACTIVE) {
// Priority: High (2) > Mid (1) > Low (0) -> DESC
if (t1.priority != t2.priority) {
return Integer.compare(t2.priority, t1.priority);
}
if (t1.dueDate != t2.dueDate) {
if (t1.dueDate == 0) return 1; // t1 no date -> bottom
if (t2.dueDate == 0) return -1; // t2 no date -> bottom
return Long.compare(t1.dueDate, t2.dueDate); // Early date first
}
// Creation Date (Fallback)
return Long.compare(t2.createdDate, t1.createdDate);
}
// 3. If both Completed
// DESC sort by finishedTime.
return Long.compare(t2.finishedTime, t1.finishedTime);
}
});
}
@NonNull
@Override
public TaskViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.task_list_item, parent, false);
return new TaskViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull TaskViewHolder holder, int position) {
Task task = tasks.get(position);
holder.bind(task);
}
@Override
public int getItemCount() {
return tasks.size();
}
class TaskViewHolder extends RecyclerView.ViewHolder {
CheckBox checkBox;
TextView content;
TextView priority;
TextView date;
ImageView alarm;
public TaskViewHolder(@NonNull View itemView) {
super(itemView);
checkBox = itemView.findViewById(R.id.task_checkbox);
content = itemView.findViewById(R.id.task_content);
priority = itemView.findViewById(R.id.task_priority);
date = itemView.findViewById(R.id.task_date);
alarm = itemView.findViewById(R.id.task_alarm_icon);
itemView.setOnClickListener(v -> {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onItemClick(tasks.get(getAdapterPosition()));
}
});
checkBox.setOnClickListener(v -> {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onCheckBoxClick(tasks.get(getAdapterPosition()));
}
});
}
public void bind(Task task) {
content.setText(task.snippet);
checkBox.setChecked(task.status == Task.STATUS_COMPLETED);
if (task.status == Task.STATUS_COMPLETED) {
content.setPaintFlags(content.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
content.setAlpha(0.5f);
} else {
content.setPaintFlags(content.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
content.setAlpha(1.0f);
}
// Priority
if (task.priority == Task.PRIORITY_HIGH) {
priority.setVisibility(View.VISIBLE);
priority.setText("HIGH");
priority.setBackgroundColor(0xFFFFCDD2); // Light Red
priority.setTextColor(0xFFB71C1C); // Dark Red
} else if (task.priority == Task.PRIORITY_NORMAL) {
priority.setVisibility(View.VISIBLE);
priority.setText("MED");
priority.setBackgroundColor(0xFFFFF9C4); // Light Yellow
priority.setTextColor(0xFFF57F17); // Dark Yellow
} else {
priority.setVisibility(View.GONE);
}
// Due Date
if (task.dueDate > 0) {
date.setVisibility(View.VISIBLE);
date.setText(DateFormat.format("MM/dd HH:mm", task.dueDate));
} else {
date.setVisibility(View.GONE);
}
// Alarm
if (task.alertDate > 0) {
alarm.setVisibility(View.VISIBLE);
} else {
alarm.setVisibility(View.GONE);
}
}
}
}

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="-100%"
android:toXDelta="0%"
android:interpolator="@android:anim/decelerate_interpolator"/>
</set>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="100%"
android:toXDelta="0%"
android:interpolator="@android:anim/decelerate_interpolator"/>
</set>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="0%"
android:toXDelta="-100%"
android:interpolator="@android:anim/accelerate_interpolator"/>
</set>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="0%"
android:toXDelta="100%"
android:interpolator="@android:anim/accelerate_interpolator"/>
</set>

@ -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="#FF000000"
android:pathData="M14,2H6c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2V8l-6,-6zM16,18H8v-2h8v2zM16,14H8v-2h8v2zM13,9V3.5L18.5,9H13z"/>
</vector>

@ -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="#FF000000"
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,17H7v-2h7v2zM17,13H7v-2h10v2zM17,9H7V7h10v2z"/>
</vector>

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
<LinearLayout
android:id="@+id/bottom_bar"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_alignParentBottom="true"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="#F0F0F0"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/btn_alarm"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_lock_idle_alarm"
android:layout_marginEnd="24dp"
android:clickable="true"
android:background="?android:attr/selectableItemBackgroundBorderless"/>
<ImageView
android:id="@+id/btn_tag"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/ic_menu_rich_text"
android:layout_marginEnd="24dp"
android:clickable="true"
android:background="?android:attr/selectableItemBackgroundBorderless"/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/btn_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Done" />
</LinearLayout>
<EditText
android:id="@+id/task_edit_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/bottom_bar"
android:gravity="top|start"
android:padding="16dp"
android:background="@null"
android:hint="Enter task..."
android:textSize="18sp" />
</RelativeLayout>

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_color">
<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"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:contentInsetStart="0dp"
app:contentInsetStartWithNavigation="0dp">
<TextView
android:id="@+id/tv_toolbar_title_notes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Notes"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_gravity="start|center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/task_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="80dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_new_task"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@drawable/ic_add"
app:backgroundTint="@color/fab_color"
app:tint="@color/text_color_primary"
android:contentDescription="New Task" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,71 @@
<?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="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Priority"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<RadioGroup
android:id="@+id/priority_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/priority_low"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Low"
android:layout_weight="1"/>
<RadioButton
android:id="@+id/priority_mid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Mid"
android:layout_weight="1"/>
<RadioButton
android:id="@+id/priority_high"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="High"
android:layout_weight="1"/>
</RadioGroup>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Due Date"
android:textStyle="bold"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/date_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="No Due Date" />
<Button
android:id="@+id/btn_set_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Set" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,70 @@
<?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="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical"
android:background="@color/bg_white"
android:layout_marginBottom="1dp">
<CheckBox
android:id="@+id/task_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<TextView
android:id="@+id/task_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/text_color_primary"
android:ellipsize="end"
android:maxLines="2" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp"
android:gravity="center_vertical">
<TextView
android:id="@+id/task_priority"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:background="#FFCDD2"
android:textColor="#B71C1C"
android:text="HIGH"
android:visibility="gone"/>
<TextView
android:id="@+id/task_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@color/text_color_secondary" />
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/task_alarm_icon"
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@android:drawable/ic_lock_idle_alarm"
android:tint="@color/text_color_secondary"
android:visibility="gone" />
</LinearLayout>

@ -0,0 +1,757 @@
# Xiaomi Notes 功能扩展规划(精简版)
## 概述
本文档规划了小米笔记应用的潜在功能扩展,按优先级和时间线组织。核心功能优先,高级功能作为后续迭代。
## 项目当前状态2026-01-21
### 已实现的核心功能 ✅
**基础功能**
- ✅ 笔记创建和编辑
- ✅ 笔记列表显示
- ✅ 文件夹管理(树形结构、展开收起、面包屑导航)
- ✅ 笔记提醒(闹钟功能)
- ✅ 笔记背景颜色5种颜色
- ✅ 笔记字体样式4种大小
- ✅ 本地数据存储SQLite + ContentProvider
**高级功能**
- ✅ MVVM架构重构ViewModel + Repository Pattern
- ✅ Google Tasks云同步
- ✅ 密码保护(图案锁 + 密码验证)
- ✅ 笔记锁定功能
- ✅ 笔记置顶功能
- ✅ 数据备份和恢复
- ✅ 桌面小部件2x2, 4x4
- ✅ 搜索功能ContentProvider支持
- ✅ 回收站功能
- ✅ 多语言支持(简体中文、繁体中文、英文)
- ✅ 材料设计UIMaterial Design
**技术架构**
- ✅ MVVM架构模式
- ✅ Repository数据访问层
- ✅ LiveData响应式数据更新
- ✅ ContentProvider标准API
- ✅ SQLiteOpenHelper数据库管理
- ✅ ExecutorService异步操作
- ✅ 48个Java源文件
- ✅ 135个资源文件
- ✅ 数据库版本5含10个触发器
### 项目统计
| 类别 | 数量 | 说明 |
|------|-------|------|
| Java源文件 | 48个 | 包括data、ui、viewmodel、model、tool、widget、gtask |
| 资源文件 | 135个 | layout、values、drawable、menu、xml、raw |
| Android组件 | 14个 | 8个Activity、3个Receiver、1个Service、2个Widget |
| 测试文件 | 4个 | 1个单元测试、2个数据层测试、1个集成测试 |
| 数据库表 | 2个 | note表(21字段)、data表(11字段) |
| 系统文件夹 | 4个 | 根(0)、临时(-1)、通话记录(-2)、回收站(-3) |
## 功能分类
### 核心功能 (Phase 1 - 已完成)
- ✅ 笔记创建和编辑
- ✅ 笔记列表显示
- ✅ 文件夹管理(树形结构、面包屑导航)
- ✅ 笔记提醒(闹钟)
- ✅ 笔记背景颜色(黄/红/蓝/绿/白)
- ✅ 笔记字体样式(小/中/大/超大)
- ✅ 本地数据存储SQLite + ContentProvider
- ✅ 笔记锁定功能
- ✅ 笔记置顶功能
- ✅ 回收站功能
- ✅ Google Tasks同步
## 短期扩展 (Phase 2 - 1-2个月)
### P0 - 必须实现
#### 2.1 搜索功能增强 ⚠️ 部分实现
**描述**: 提供强大的搜索功能,支持全文搜索、筛选和排序
**当前状态**:
- ✅ 基础搜索功能ContentProvider支持search URI
- ✅ 搜索建议功能
- ✅ 搜索历史记录
- ✅ 高级筛选选项
- ✅ 搜索结果高亮
**待实现功能点**:
- ✅ 搜索历史记录(本地存储常用搜索词)
- ✅ 搜索结果高亮显示
- ✅ 搜索频率排序
**技术方案**:
- 使用 SharedPreferences 存储搜索历史
- 扩展 NotesRepository 搜索逻辑
- 实现搜索 UI 筛选面板
**优先级**: 高
**工作量**: 2-3天
#### 2.2 导入导出功能增强 ✅ 已实现基础版本
**描述**: 支持笔记的导入导出,便于数据迁移和分享
**用户需求**: 希望可以实现与没有应用的人分享也能有应用类似的便签显示效果
**当前状态**:
- ✅ 数据备份功能BackupUtils.java
- ✅ 数据恢复功能
- ❌ 导出为便签图片格式(便于分享给非用户)
- ❌ 导出为 Markdown/TXT/JSON
**待实现功能点**:
- [ ] 导出为便签图片格式(类似应用内笔记卡片样式)
- [ ] 支持自定义背景颜色
- [ ] 支持字体大小选择
- [ ] 支持水印(可选)
- [ ] 高分辨率导出(分享清晰)
- [ ] 导出为 Markdown 格式
- [ ] 导出为 TXT 格式
- [ ] 导出为 JSON 格式
- [ ] 批量导出选择界面
- [ ] 导入 JSON 格式
- [ ] 导入验证和冲突处理
**技术方案**:
- 使用现有的 BackupUtils 基础
- 使用 Canvas 绘制便签卡片并保存为图片
- 集成 Markdown 处理库
- 实现文件选择器Storage Access Framework
- 添加分享功能Intent.ACTION_SEND
**优先级**: 高
**工作量**: 3-4天
#### 2.3 撤回功能
**描述**: 支持简单的笔记撤回操作
**功能点**:
- ✅ 撤回上一次编辑
- ✅ 撤回历史栈可连续撤回10-20次
- ✅ 重做功能
- ✅ 撤回/重做状态提示
- ✅ 清空撤回历史
**技术方案**:
- 实现 UndoStack 数据结构
- 在 NoteEditText 中记录编辑历史
- 使用 Command Pattern 实现撤回逻辑
- 添加撤回/重做 UI 按钮
**优先级**: 中
**工作量**: 2-3天
### P1 - 应该实现
#### 2.4 标签系统
**描述**: 为笔记添加标签,便于分类和筛选
**用户疑问**: 是否与文件夹功能重叠?
**功能定位**:
- **文件夹**: 层级分类(父文件夹 → 子文件夹),一个笔记只能属于一个文件夹
- **标签**: 扁平分类,一个笔记可以有多个标签,支持交叉分类
**场景对比**:
| 功能 | 文件夹 | 标签 |
|------|-------|------|
| 结构 | 树形层级 | 扁平列表 |
| 归属关系 | 一个笔记 → 一个文件夹 | 一个笔记 ↔ 多个标签 |
| 查询 | 按文件夹筛选 | 按标签筛选、多标签组合 |
| 适用场景 | 主要分类、项目分组 | 辅助分类、主题标记、状态标记 |
| 示例 | "工作/项目A/会议记录" | "重要"、"待办"、"学习"、"会议" |
**功能点**:
- [ ] 创建标签
- [ ] 编辑标签(名称、颜色)
- [ ] 删除标签
- [ ] 为笔记添加/移除标签(多选支持)
- [ ] 按标签筛选笔记
- [ ] 按多标签组合筛选AND/OR 逻辑)
- [ ] 标签颜色自定义
- [ ] 标签统计(每个标签的笔记数)
- [ ] 标签云视图
**技术方案**:
- 设计 Tag 数据表id, name, color, created_date
- 设计 NoteTag 关联表note_id, tag_id
- 实现标签选择器 UI类似文件夹树
- 扩展 NotesRepository 标签查询逻辑
- 添加标签管理 ViewModel
**优先级**: 高
**工作量**: 3-4天
#### 2.5 笔记锁定 ✅ 已实现
**描述**: 为敏感笔记添加密码保护
**用户需求**: 只需要密码保护就行了
**当前状态**:
- ✅ 图案锁保护LockPatternView.java
- ✅ 密码保护PasswordActivity.java
- ✅ SecurityManager.java 加密工具
**优先级**: 已完成
**工作量**: 0天
#### 2.6 笔记模板
**描述**: 提供常用笔记模板,快速创建标准化笔记
**功能点**:
- [ ] 预置模板(会议记录、日记、待办事项、读书笔记等)
- [ ] 创建自定义模板
- [ ] 编辑模板
- [ ] 删除模板
- [ ] 应用模板到新笔记
- [ ] 模板分类(工作、生活、学习)
- [ ] 模板预览
**技术方案**:
- 设计 Template 数据表id, name, content, category, created_date
- 实现模板管理 UI
- 创建模板应用逻辑(复制模板内容到新笔记)
- 添加模板管理 Activity/Fragment
**优先级**: 中
**工作量**: 2-3天
### P2 - 可以实现
#### 2.7 笔记统计和分析
**描述**: 提供笔记使用情况的统计信息
**功能点**:
- [ ] 笔记数量统计(总数、本周新增、本月新增)
- [ ] 笔记创建频率图表
- [ ] 常用词分析
- [ ] 文件夹分布(饼图)
- [ ] 标签使用统计
- [ ] 笔记长度统计
- [ ] 活跃时间段分析
- [ ] 周/月/年报告
**技术方案**:
- 扩展 NotesRepository 统计查询
- 集成图表库MPAndroidChart 或 AnyChart
- 实现统计数据计算(基于现有表)
- 创建统计图表 UI Activity
**优先级**: 低
**工作量**: 3-4天
#### 2.8 快捷操作优化 ⚠️ 部分实现
**描述**: 添加快捷操作,提高效率
**当前状态**:
- ✅ 长按笔记快捷菜单(复制、分享、删除、移动、设置提醒)
- ✅ 桌面小组件2x2, 4x4
- ❌ 通知栏快捷操作
- ❌ 文本格式快捷工具栏
**待实现功能点**:
- [ ] 通知栏快捷操作(创建笔记、语音输入)
- [ ] 快捷方式Launcher Shortcuts API
- [ ] 快捷手势(左滑删除、右滑置顶)
**优先级**: 低
**工作量**: 2天
## 中期扩展 (Phase 3 - 3-4个月)
### P1 - 应该实现
#### 3.1 富文本编辑
**描述**: 增强文本编辑功能,支持多种格式
**功能点**:
- ✅ 粗体、斜体、下划线
- ✅ 删除线
- ✅ 标题层级 (H1-H6)
- ✅ 列表(无序、有序、检查列表)
- ✅ 引用块
- ✅ 代码块
- ✅ 链接
- ✅ 分割线
- ✅ 文本颜色
- ✅ 文本背景色
**技术方案**:
- 集成富文本编辑库(如 RichEditor、SpannableStringBuilder
- 或使用 Markdown 渲染器(如 Markwon
- 实现格式工具栏
- 扩展 NoteEditText.java 支持富文本
**优先级**: 高
**工作量**: 4-5天
#### 3.2 图片附件
**描述**: 支持在笔记中插入图片
**功能点**:
- [ ] 从相册选择图片
- [ ] 拍照插入
- [ ] 图片裁剪
- [ ] 图片压缩
- [ ] 图片预览(全屏查看)
- [ ] 图片删除
- [ ] 图片大小调整
- [ ] 图片旋转
- [ ] 图片备注
**技术方案**:
- 使用 ContentResolver 访问图片
- 集成图片裁剪库UCrop 或 Android Image Cropper
- 集成图片加载库Glide 或 Coil
- 扩展 data 表支持图片存储mime_type = "image/*"
- 创建图片查看器 Activity
**优先级**: 高
**工作量**: 4-5天
### P2 - 可以实现
#### 3.3 智能识别功能
**描述**: 智能识别笔记中的地址和时间,支持快速跳转
**用户需求**: 希望可以添加智能识别地址、时间功能,跳转导航和闹钟设置
**功能点**:
- [ ] 智能识别地址信息
- [ ] 自动检测地址模式(省市区、街道地址)
- [ ] 高亮显示识别的地址
- [ ] 点击地址跳转地图应用高德地图、百度地图、Google Maps
- [ ] 支持地址标注(在地址旁显示位置图标)
- [ ] 智能识别时间信息
- [ ] 自动检测日期时间模式今天14:30、明天上午9点、2025年1月21日
- [ ] 高亮显示识别的时间
- [ ] 点击时间快速设置闹钟/提醒
- [ ] 支持时间相对表达(下周三、后天)
- [ ] 智能识别电话号码
- [ ] 自动检测电话号码
- [ ] 点击电话号码直接拨号
- [ ] 智能识别URL链接
- [ ] 自动检测URL并使其可点击
- [ ] 长按URL显示预览或复制选项
**技术方案**:
- 使用正则表达式识别地址、时间、电话、URL模式
- 实现智能解析器SmartParser.java
- 扩展 NoteEditText 支持高亮显示
- 集成 Intent 跳转地图、电话、闹钟
- 使用 SpannableString 设置可点击区域
**优先级**: 中
**工作量**: 5-6天
#### 3.4 链接笔记
**描述**: 支持笔记之间的链接引用
**功能点**:
- [ ] 创建笔记链接([[笔记标题]]语法)
- [ ] 自动检测笔记标题链接
- [ ] 反向链接查看(哪些笔记引用了当前笔记)
- [ ] 链接预览(悬浮显示笔记摘要)
- [ ] 链接计数
- [ ] 图谱视图(可视化笔记关系)
**技术方案**:
- 设计 NoteLink 数据表id, source_note_id, target_note_id, position
- 实现链接语法解析(正则表达式)
- 创建链接管理 UI
- 集成图表库绘制笔记关系图谱
**优先级**: 中
**工作量**: 4-5天
#### 3.5 任务清单
**描述**: 在笔记中创建待办事项清单
**功能点**:
- ✅ 添加任务项(- [ ] 语法)
- ✅ 标记完成/未完成
- ✅ 任务优先级(高/中/低)
- ✅ 任务截止日期
- ✅ 任务提醒
- ❌ 任务统计(完成率)
- ✅ 过滤已完成任务
- ❌ 任务拖拽排序
**技术方案**:
- 扩展 data 表支持任务类型mime_type = "text/x-todo"
- 实现任务 UI 组件TodoItemView
- 扩展 NoteEditText 解析任务语法
- 集成现有提醒功能
**优先级**: 中
**工作量**: 3-4天
## 长期扩展 (Phase 4 - 6个月+)
### P2 - 可以实现
#### 4.1 主题和自定义 ⚠️ 部分实现
**描述**: 提供更多主题和界面自定义选项
**用户需求**: 自定义壁纸
**当前状态**:
- ✅ 笔记背景颜色5种
- ✅ 字体大小4种
- ✅ 夜间主题values-night
- ❌ 完整主题系统
- ❌ 自定义主题颜色
- ❌ 自定义壁纸
**待实现功能点**:
- [ ] 多种预设主题
- [ ] 简约白
- [ ] 深夜黑
- [ ] 护眼绿
- [ ] 暖色系
- [ ] 冷色系
- [ ] 自定义主题颜色
- [ ] 自定义应用壁纸
- [ ] 从相册选择壁纸
- [ ] 预设壁纸库
- [ ] 瓷片平铺/居中/拉伸
- [ ] 磨砂玻璃效果(可选)
- [ ] 自定义字体家族(使用 Google Fonts
- [ ] 自定义卡片样式
- [ ] 自定义图标包
- [ ] 动态取色(基于壁纸)
**技术方案**:
- 使用 Material You (Material 3) 动态颜色
- 实现主题管理 ViewModel
- 创建主题编辑器 Activity
- 集成 Google Fonts API
- 实现壁纸设置和管理
**优先级**: 低
**工作量**: 5-6天
#### 4.2 智能功能
**描述**: 利用 AI 提供智能辅助功能
**功能点**:
- [ ] 自动摘要AI生成笔记摘要
- [ ] 智能分类建议(基于内容推荐文件夹/标签)
- [ ] 关键词提取
- [ ] 相关笔记推荐(基于内容相似度)
- [ ] 语法检查
- [ ] 翻译功能
- [ ] 智能纠错
**技术方案**:
- 集成本地 NLP 模型TensorFlow Lite
- 或使用第三方 AI APIOpenAI、百度文心等
- 实现智能分析逻辑
- 创建智能功能 UI 组件
**优先级**: 低
**工作量**: 7-10天
#### 4.3 云同步和账号系统 ✅ 已实现基础版本
**描述**: 完善的数据备份和云同步机制
**用户需求**: 实现注册、登录和云同步
**当前状态**:
- ✅ Google Tasks 同步(已有账号系统)
- ✅ 手动备份BackupUtils.java
- ✅ 手动恢复
- ❌ 自定义账号系统(注册/登录)
- ❌ 云备份到自建服务器
- ❌ 多设备同步
**待实现功能点**:
- [ ] 自定义账号系统
- [ ] 用户注册(手机号/邮箱)
- [ ] 用户登录(密码 + 验证码)
- [ ] 密码找回
- [ ] 账号注销
- [ ] 账号安全设置(修改密码、绑定手机号)
- [ ] 云同步到自建服务器
- [ ] 自动同步(实时或定时)
- [ ] 手动同步
- [ ] 同步冲突解决
- [ ] 同步状态显示
- [ ] 多设备管理
- [ ] 查看已登录设备
- [ ] 远程登出设备
- [ ] 设备命名
- [ ] 备份文件加密
- [ ] 端到端加密
- [ ] 数据传输加密HTTPS + 证书固定)
**技术方案**:
- 设计服务端Node.js + MongoDB/PostgreSQL
- 实现 REST API用户认证、数据同步
- 客户端集成 Retrofit + OkHttp
- 使用 JWT 或 OAuth 进行认证
- 实现增量同步算法
- 扩展现有 BackupUtils.java
**优先级**: 中
**工作量**: 10-15天含服务端开发
## 功能实现时间线(精简版)
### Month 1-2: 核心功能增强
- ✅ Week 1: 搜索功能增强 (2.1) - 搜索历史、高级筛选
- [ ] Week 2: 导入导出功能增强 (2.2) - 便签图片导出、Markdown/TXT
- ✅ Week 3: 撤回功能 (2.3) - 撤回/重做
- [ ] Week 4: 标签系统 (2.4) - 标签分类和筛选
### Month 3-4: 用户体验提升
- [ ] Week 5: 笔记模板 (2.6) - 模板管理
- ✅ Week 6-8: 富文本编辑 (3.1) - 完整格式支持
### Month 5-6: 功能扩展
- [ ] Week 9-10: 图片附件 (3.2) - 图片管理和预览
- [ ] Week 11-12: 智能识别功能 (3.3) - 地址、时间识别
### Month 7-8: 高级功能
- [ ] Week 13-14: 链接笔记 (3.4) - 笔图谱
- ✅ Week 15-16: 任务清单 (3.5) - 任务管理
### Month 9+: 智能化和生态
- [ ] Week 17-20: 云同步和账号系统 (4.3) - 注册登录、云同步
- [ ] Week 21-22: 主题和自定义 (4.1) - Material 3 动态主题、自定义壁纸
- [ ] Week 23+: 智能功能 (4.2)、跨平台同步 (4.4) 等
## 技术栈规划(基于当前架构)
### 当前技术栈
- **UI 框架**: Material Design Components
- **架构**: MVVM + Repository Pattern
- **数据库**: SQLite + SQLiteOpenHelper
- **异步**: ExecutorService
- **视图绑定**: findViewById传统方式
- **依赖管理**: Gradle Version Catalog (libs.versions.toml)
- **网络**: Apache HttpClient 4.5.14
### 建议技术演进
#### 短期演进保持Java
- **UI 框架**: 升级到 Material Design 3
- **架构**: 保持 MVVM + Repository Pattern
- **数据库**: 保持 SQLiteOpenHelper考虑迁移到 Room
- **异步**: 保持 ExecutorService考虑迁移到 Kotlin Coroutines
- **依赖注入**: 考虑引入 Hilt可选
#### 中期演进可选Kotlin迁移
- **语言**: 逐步迁移到 Kotlin100% 互操作)
- **UI**: 迁移到 Jetpack Compose可选
- **异步**: 迁移到 Kotlin Coroutines + Flow
- **依赖注入**: Hilt
- **数据库**: Room + Coroutines
#### 可能引入的技术栈
- **富文本编辑**:
- RichEditor (Java)
- Markwon (Markdown渲染Kotlin优先)
- **图片处理**:
- Glide / Coil (图片加载)
- UCrop / Android Image Cropper (图片裁剪)
- **图表**: MPAndroidChart / AnyChart / ECharts
- **文件压缩**: Zip4j
- **加密**: Android Keystore + AES (已有 SecurityManager)
- **智能识别**:
- 正则表达式(地址、时间、电话识别)
- 第三方 API地址解析、时间解析
- **服务端开发**:
- Node.js + Express/Koa
- MongoDB / PostgreSQL
- Redis缓存
- Nginx反向代理
### 技术债务清理
#### 高优先级
- [ ] 迁移从 findViewById 到 ViewBinding / Jetpack Compose
- [ ] 迁移从 ExecutorService 到 Kotlin Coroutines / Flow
- [ ] 迁移从 SQLiteOpenHelper 到 Room
- [ ] 重构 Apache HttpClient 依赖(使用 Retrofit + OkHttp
#### 中优先级
- [ ] 引入依赖注入框架Hilt
- [ ] 升级到 Material Design 3
- [ ] 迁移到 Kotlin逐步
- [ ] 统一异常处理机制
#### 低优先级
- [ ] Jetpack Compose 迁移
- [ ] 单向数据流UDF架构优化
- [ ] 模块化架构Dynamic Feature Modules
## 性能优化
### 短期优化
- [ ] RecyclerView 性能优化DiffUtil、预加载
- [ ] 图片加载和缓存优化(使用 Glide/Coil
- [ ] 数据库查询优化(添加索引、避免 N+1 查询)
- [ ] 内存泄漏检测和修复LeakCanary
- [ ] 启动速度优化Application 初始化优化)
### 长期优化
- [ ] 启用 ProGuard/R8 混淆和优化
- [ ] APK 体积优化(资源压缩、动态下发)
- [ ] 启动速度优化(延迟加载、线程优化)
- [ ] 电池使用优化WorkManager 替代 AlarmManager
- [ ] 网络请求优化(缓存策略、请求合并)
## 安全性考虑
### 数据安全
- [x] 数据加密存储(已有 SecurityManager
- [ ] 敏感信息保护(日志脱敏)
- [x] 权限最小化原则(运行时权限请求)
- [ ] 安全的文件存储Scoped Storage
- [ ] 密码保护(已有)
### 隐私保护
- [x] 本地数据处理优先
- [ ] 明确的隐私政策
- [ ] 用户数据控制权(数据导出、删除)
- [ ] 安全的数据传输HTTPS + 证书固定)
### 代码安全
- [ ] 代码混淆ProGuard/R8
- [ ] 防止重打包(签名校验)
- [ ] 日志脱敏(不记录敏感信息)
- [ ] SQL 注入防护(使用参数化查询,已实现)
## 测试策略
### 单元测试
- [x] 数据库操作测试(已有 FolderDatabaseTest
- [x] 仓库层测试(已有 NotesRepositoryTest
- [ ] 业务逻辑测试ViewModel 单元测试)
- [ ] 工具类测试Utils 单元测试)
### 集成测试
- [ ] 主要功能流程测试(笔记创建、编辑、删除)
- [ ] 数据迁移测试(数据库版本升级)
- [ ] ContentProvider 接口测试
- [ ] 同步功能测试
### UI 测试
- [ ] Espresso UI 测试(关键流程)
- [ ] 用户交互测试(手势、快捷操作)
- [ ] 兼容性测试(不同 Android 版本、屏幕尺寸)
### 性能测试
- [ ] 大数据量测试1000+ 笔记)
- [ ] 内存使用测试(内存泄漏、峰值)
- [ ] 启动时间测试(冷启动、热启动)
- [ ] 电池使用测试(长时间运行)
## 用户反馈和迭代
### 收集反馈
- [ ] 应用内反馈功能(反馈表单)
- [ ] 应用商店评论监控
- [ ] 用户调研(问卷调查)
- [ ] 数据分析(崩溃率、使用频率、功能使用率)
- [ ] Beta 测试计划Play Console
### 迭代流程
1. 收集用户反馈(多渠道)
2. 分析需求优先级(影响力 vs 成本)
3. 制定开发计划Sprint Planning
4. 开发和测试Code Review + CI/CD
5. 发布新版本(灰度发布)
6. 收集新反馈(持续监控)
### 数据驱动决策
- [ ] 功能使用率统计
- [ ] 用户留存率分析
- [ ] 崩溃率监控
- [ ] 性能指标追踪
- [ ] A/B 测试(新功能)
## 风险评估
### 技术风险
- [ ] 第三方库兼容性(版本冲突、废弃)
- [ ] Android 版本兼容性(碎片化)
- [ ] 性能瓶颈(大数据量、复杂查询)
- [ ] 数据迁移风险(版本升级失败)
- [ ] Kotlin 迁移风险(互操作问题)
- [ ] 服务端开发风险(并发、安全、扩展性)
### 产品风险
- [ ] 功能过度复杂(用户体验下降)
- [ ] 用户学习成本高(功能过多)
- [ ] 功能使用率低(开发浪费)
- [ ] 维护成本高(技术债务积累)
- [ ] 竞品功能差距(市场竞争力)
### 缓解措施
- [ ] 渐进式功能发布Feature Flags
- [ ] 充分的测试覆盖(单元测试、集成测试)
- [ ] 用户教育文档(帮助中心、教程)
- [ ] 灵活的架构设计(解耦、可扩展)
- [ ] 持续重构(技术债管理)
- [ ] 可配置功能(用户可选)
## 成功指标
### 技术指标
- [ ] 崩溃率 < 0.5%
- [ ] ANR 率 < 0.1%
- [ ] 启动时间 < 2
- [ ] 代码覆盖率 > 60%
- [ ] 技术债务指数降低
### 产品指标
- [ ] 日活跃用户DAU增长
- [ ] 用户留存率7日、30日
- [ ] 功能使用率(每个功能的用户比例)
- [ ] 应用商店评分 > 4.5
- [ ] 用户反馈响应时间 < 24h
## 总结
本规划基于当前项目实际状态MVVM架构、Repository Pattern、48个Java文件、135个资源文件并根据用户反馈精简了功能列表提供了从核心功能增强到高级功能扩展的完整路线图。
**项目优势**
1. ✅ 完整的 MVVM 架构ViewModel + Repository
2. ✅ 响应式数据更新LiveData
3. ✅ 标准化数据访问ContentProvider
4. ✅ 稳定的数据库设计SQLite + 10个触发器
5. ✅ 多语言支持(中英文)
6. ✅ Google Tasks 云同步
7. ✅ 密码保护和安全功能
8. ✅ 备份和恢复功能
**关键成功因素**
1. 优先实现用户最需要的功能(搜索、便签导出、智能识别)
2. 保持代码质量和可维护性(持续重构)
3. 持续收集用户反馈并迭代(数据驱动)
4. 平衡功能丰富度和简洁性(用户体验)
5. 注重性能和用户体验(响应速度、流畅度)
6. 稳健的架构设计(可扩展、可测试)
**下一步行动**
1. 实施 Phase 2 P0 功能(搜索增强、便签导出、撤回功能)
2. 技术债务清理ViewBinding、Kotlin 迁移)
3. 建立自动化测试体系(单元测试、集成测试)
4. 设置监控和分析(崩溃率、使用率)
5. 规划服务端开发(云同步、账号系统)
---
**文档版本**: v3.0(精简版)
**更新日期**: 2026-01-21
**维护者**: Sisyphus AI Agent
**更新说明**: 根据用户反馈精简功能列表,移除不必要的生物识别和复杂版本历史,强调便签图片导出、智能识别和云同步功能
Loading…
Cancel
Save