From 7b21ad6271b76a080a86bd1a15f6a4ae8c922d71 Mon Sep 17 00:00:00 2001
From: gy <2293314358@qq.com>
Date: Fri, 30 Jan 2026 20:49:08 +0800
Subject: [PATCH 1/2] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=B8=83=E5=B1=80?=
=?UTF-8?q?=E7=BB=93=E6=9E=84=20=EF=BC=9A=E9=80=82=E5=BA=94=E5=BA=95?=
=?UTF-8?q?=E9=83=A8=E5=AF=BC=E8=88=AA=E7=9A=84=E9=AB=98=E5=BA=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/Notes-master/res/layout/note_list.xml | 38 ++++++++++++++++++-----
src/Notes-master/res/values/colors.xml | 4 +++
2 files changed, 35 insertions(+), 7 deletions(-)
diff --git a/src/Notes-master/res/layout/note_list.xml b/src/Notes-master/res/layout/note_list.xml
index edbdeaa..28c7284 100644
--- a/src/Notes-master/res/layout/note_list.xml
+++ b/src/Notes-master/res/layout/note_list.xml
@@ -8,7 +8,6 @@
android:id="@+id/note_list_root"
android:background="@drawable/list_background">
-
@@ -57,37 +56,62 @@
android:gravity="center_vertical" />
-
-
+
+
+
+
+
+
+ android:layout_above="@id/bottom_navigation">
+ android:paddingBottom="16dp" />
+
+
+
-
+
+
+
\ No newline at end of file
diff --git a/src/Notes-master/res/values/colors.xml b/src/Notes-master/res/values/colors.xml
index 53eff09..0dca8e6 100644
--- a/src/Notes-master/res/values/colors.xml
+++ b/src/Notes-master/res/values/colors.xml
@@ -24,4 +24,8 @@
#FFD700
#FFFFFF
#333333
+
+ #F5F5F5
+ #000000
+ #757575
--
2.34.1
From 518dbb098d30d1e5f562eb9a7eac692bdf69bbc7 Mon Sep 17 00:00:00 2001
From: gy <2293314358@qq.com>
Date: Fri, 30 Jan 2026 20:57:00 +0800
Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/net/micode/notes/model/CloudNote.java | 33 ++
.../src/net/micode/notes/sync/SyncWorker.java | 206 +++++++++++++
.../src/net/micode/notes/tool/SyncMapper.java | 61 ++++
.../net/micode/notes/ui/AgendaFragment.java | 127 ++++++++
.../net/micode/notes/ui/LoginActivity.java | 121 ++++++++
.../micode/notes/ui/NotesListActivity.java | 51 ++++
.../micode/notes/ui/NotesListItemAdapter.java | 281 ++++++++++++++++++
7 files changed, 880 insertions(+)
create mode 100644 src/Notes-master/src/net/micode/notes/model/CloudNote.java
create mode 100644 src/Notes-master/src/net/micode/notes/sync/SyncWorker.java
create mode 100644 src/Notes-master/src/net/micode/notes/tool/SyncMapper.java
create mode 100644 src/Notes-master/src/net/micode/notes/ui/AgendaFragment.java
create mode 100644 src/Notes-master/src/net/micode/notes/ui/LoginActivity.java
create mode 100644 src/Notes-master/src/net/micode/notes/ui/NotesListItemAdapter.java
diff --git a/src/Notes-master/src/net/micode/notes/model/CloudNote.java b/src/Notes-master/src/net/micode/notes/model/CloudNote.java
new file mode 100644
index 0000000..9a4356e
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/model/CloudNote.java
@@ -0,0 +1,33 @@
+package net.micode.notes.model;
+
+import com.google.firebase.firestore.IgnoreExtraProperties;
+
+/**
+ * 云端便签实体类 - 用于 Firebase Firestore 同步
+ */
+@IgnoreExtraProperties
+public class CloudNote {
+ public String serverId; // 云端 ID
+ public String title; // 标题
+ public String content; // 完整正文内容
+ public long modifiedDate; // 修改时间
+ public int bgColorId; // 背景颜色 ID
+ public int isAgenda; // 是否为日程 (0/1)
+ public long agendaDate; // 日程时间戳
+ public int isCompleted; // 是否完成 (0/1)
+
+ // Firebase 必须要求的空构造函数
+ public CloudNote() {}
+
+ public CloudNote(String serverId, String title, String content, long modifiedDate,
+ int bgColorId, int isAgenda, long agendaDate, int isCompleted) {
+ this.serverId = serverId;
+ this.title = title;
+ this.content = content;
+ this.modifiedDate = modifiedDate;
+ this.bgColorId = bgColorId;
+ this.isAgenda = isAgenda;
+ this.agendaDate = agendaDate;
+ this.isCompleted = isCompleted;
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/sync/SyncWorker.java b/src/Notes-master/src/net/micode/notes/sync/SyncWorker.java
new file mode 100644
index 0000000..30bfdc8
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/sync/SyncWorker.java
@@ -0,0 +1,206 @@
+package net.micode.notes.sync;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+import com.google.firebase.auth.FirebaseAuth;
+import com.google.firebase.firestore.FirebaseFirestore;
+import com.google.firebase.firestore.QueryDocumentSnapshot;
+
+import net.micode.notes.data.Notes;
+import net.micode.notes.data.Notes.NoteColumns;
+import net.micode.notes.model.CloudNote;
+import net.micode.notes.tool.SyncMapper;
+
+import java.util.UUID;
+
+public class SyncWorker extends Worker {
+ private static final String TAG = "SyncWorker";
+
+ public SyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+ super(context, workerParams);
+ }
+
+ @NonNull
+ @Override
+ public Result doWork() {
+ // [确切位置]:在方法入口处立即进行安全隔离
+ try {
+ String uid = FirebaseAuth.getInstance().getUid();
+ if (uid == null) {
+ Log.w(TAG, "Sync skipped: No user logged in.");
+ return Result.success();
+ }
+
+ FirebaseFirestore db = FirebaseFirestore.getInstance();
+
+ // 1. 先执行上行同步
+ performPush(db, uid);
+
+ // 2. 后执行下行同步
+ performPull(db, uid);
+
+ return Result.success();
+
+ } catch (SecurityException e) {
+ // [核心修复]:捕获 "Unknown calling package name 'com.google.android.gms'"
+ Log.e(TAG, "GMS Security Exception caught, retrying later: " + e.getMessage());
+ return Result.retry(); // 告诉系统稍后重试,不要直接闪退进程
+ } catch (Exception e) {
+ Log.e(TAG, "General sync error: " + e.getMessage());
+ return Result.failure();
+ }
+ }
+
+ /**
+ * [上行逻辑]:查询本地所有 sync_state = 1 的记录并上传
+ */
+ private void performPush(FirebaseFirestore db, String uid) {
+ Cursor cursor = getApplicationContext().getContentResolver().query(
+ Notes.CONTENT_NOTE_URI,
+ null,
+ NoteColumns.SYNC_STATE + "=? AND " + NoteColumns.TYPE + "=? AND " + NoteColumns.ID + ">0",
+ new String[]{"1", String.valueOf(Notes.TYPE_NOTE)},
+ null
+ );
+
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ long localId = cursor.getLong(cursor.getColumnIndex(NoteColumns.ID));
+ String serverId = cursor.getString(cursor.getColumnIndex(NoteColumns.SERVER_ID));
+
+ // 如果没有 serverId,说明是本地新建的,分配一个 UUID
+ if (TextUtils.isEmpty(serverId)) {
+ serverId = UUID.randomUUID().toString();
+ }
+
+ // 获取完整正文 (跨表查询 data 表)
+ String fullContent = getNoteFullContent(localId);
+
+ // 翻译为云端对象
+ CloudNote cloudNote = SyncMapper.fromCursor(cursor, fullContent);
+ cloudNote.serverId = serverId;
+
+ // 上传 Firestore
+ final String finalServerId = serverId;
+ db.collection("users").document(uid)
+ .collection("notes").document(finalServerId)
+ .set(cloudNote)
+ .addOnSuccessListener(aVoid -> {
+ // 同步成功,回写本地状态为 0 (已同步)
+ ContentValues values = new ContentValues();
+ values.put(NoteColumns.SYNC_STATE, 0);
+ values.put(NoteColumns.SERVER_ID, finalServerId);
+
+ getApplicationContext().getContentResolver().update(
+ Uri.withAppendedPath(Notes.CONTENT_NOTE_URI, String.valueOf(localId)),
+ values, null, null);
+ Log.d(TAG, "Push Success: " + finalServerId);
+ });
+ }
+ cursor.close();
+ }
+ }
+
+ /**
+ * [下行逻辑]:拉取云端所有数据并合并
+ */
+ private void performPull(FirebaseFirestore db, String uid) {
+ db.collection("users").document(uid).collection("notes")
+ .get()
+ .addOnSuccessListener(queryDocumentSnapshots -> {
+ for (QueryDocumentSnapshot doc : queryDocumentSnapshots) {
+ CloudNote cloudNote = doc.toObject(CloudNote.class);
+ if (cloudNote != null) {
+ mergeCloudNoteToLocal(cloudNote);
+ }
+ }
+ Log.d(TAG, "Pull complete. Cloud docs processed.");
+ })
+ .addOnFailureListener(e -> Log.e(TAG, "Pull failed", e));
+ }
+
+ /**
+ * [合并逻辑]:决定是插入还是更新本地数据
+ */
+ private void mergeCloudNoteToLocal(CloudNote cloudNote) {
+ Cursor c = getApplicationContext().getContentResolver().query(
+ Notes.CONTENT_NOTE_URI,
+ new String[]{NoteColumns.ID, NoteColumns.MODIFIED_DATE, NoteColumns.SYNC_STATE},
+ NoteColumns.SERVER_ID + "=?",
+ new String[]{cloudNote.serverId},
+ null
+ );
+
+ if (c != null && c.moveToFirst()) {
+ // 本地已存在该便签
+ long localId = c.getLong(0);
+ long localModified = c.getLong(1);
+ int syncState = c.getInt(2);
+
+ // 只有当云端修改时间更新,且本地不是“待上传”状态时,才覆盖本地
+ if (cloudNote.modifiedDate > localModified && syncState == 0) {
+ updateLocalNote(localId, cloudNote);
+ }
+ c.close();
+ } else {
+ // 本地没有,直接新建
+ insertCloudNoteToLocal(cloudNote);
+ }
+ }
+
+ private void insertCloudNoteToLocal(CloudNote cloudNote) {
+ ContentValues values = SyncMapper.toNoteValues(cloudNote);
+ Uri noteUri = getApplicationContext().getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
+
+ if (noteUri != null) {
+ long newId = ContentUris.parseId(noteUri);
+ // 写入正文到 data 表
+ ContentValues dataValues = new ContentValues();
+ dataValues.put(Notes.DataColumns.NOTE_ID, newId);
+ dataValues.put(Notes.DataColumns.MIME_TYPE, Notes.TextNote.CONTENT_ITEM_TYPE);
+ dataValues.put(Notes.DataColumns.CONTENT, cloudNote.content);
+ getApplicationContext().getContentResolver().insert(Notes.CONTENT_DATA_URI, dataValues);
+ }
+ }
+
+ private void updateLocalNote(long localId, CloudNote cloudNote) {
+ ContentValues values = SyncMapper.toNoteValues(cloudNote);
+ getApplicationContext().getContentResolver().update(
+ ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, localId),
+ values, null, null);
+
+ ContentValues dataValues = new ContentValues();
+ dataValues.put(Notes.DataColumns.CONTENT, cloudNote.content);
+ getApplicationContext().getContentResolver().update(
+ Notes.CONTENT_DATA_URI,
+ dataValues,
+ Notes.DataColumns.NOTE_ID + "=?",
+ new String[]{String.valueOf(localId)});
+ }
+
+ private String getNoteFullContent(long noteId) {
+ Cursor c = getApplicationContext().getContentResolver().query(
+ Notes.CONTENT_DATA_URI,
+ new String[]{Notes.DataColumns.CONTENT},
+ Notes.DataColumns.NOTE_ID + "=? AND " + Notes.DataColumns.MIME_TYPE + "=?",
+ new String[]{String.valueOf(noteId), Notes.TextNote.CONTENT_ITEM_TYPE},
+ null
+ );
+ String content = "";
+ if (c != null) {
+ if (c.moveToFirst()) content = c.getString(0);
+ c.close();
+ }
+ return content;
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/tool/SyncMapper.java b/src/Notes-master/src/net/micode/notes/tool/SyncMapper.java
new file mode 100644
index 0000000..a410788
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/tool/SyncMapper.java
@@ -0,0 +1,61 @@
+package net.micode.notes.tool;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import net.micode.notes.data.Notes.NoteColumns;
+import net.micode.notes.model.CloudNote;
+
+/**
+ * 数据映射器:负责本地数据库游标与云端对象之间的转换
+ */
+public class SyncMapper {
+
+ /**
+ * 1. [本地 -> 云端]
+ * 将数据库查询到的 Cursor 转换为 CloudNote 对象
+ * 注意:Cursor 必须包含同步所需的所有列
+ */
+ public static CloudNote fromCursor(Cursor cursor, String fullContent) {
+ CloudNote cloudNote = new CloudNote();
+
+ // 映射字段
+ cloudNote.serverId = cursor.getString(cursor.getColumnIndex(NoteColumns.SERVER_ID));
+ cloudNote.title = cursor.getString(cursor.getColumnIndex(NoteColumns.TITLE));
+ cloudNote.modifiedDate = cursor.getLong(cursor.getColumnIndex(NoteColumns.MODIFIED_DATE));
+ cloudNote.bgColorId = cursor.getInt(cursor.getColumnIndex(NoteColumns.BG_COLOR_ID));
+ cloudNote.isAgenda = cursor.getInt(cursor.getColumnIndex(NoteColumns.IS_AGENDA));
+ cloudNote.agendaDate = cursor.getLong(cursor.getColumnIndex(NoteColumns.AGENDA_DATE));
+ cloudNote.isCompleted = cursor.getInt(cursor.getColumnIndex(NoteColumns.IS_COMPLETED));
+
+ // 重要:content 是从 data 表中查询出的完整字符串
+ cloudNote.content = fullContent;
+
+ return cloudNote;
+ }
+
+ /**
+ * 2. [云端 -> 本地]
+ * 将从 Firebase 下载的 CloudNote 转换为可以写入数据库的 ContentValues
+ */
+ public static ContentValues toNoteValues(CloudNote cloudNote) {
+ ContentValues values = new ContentValues();
+ values.put(NoteColumns.SERVER_ID, cloudNote.serverId);
+ values.put(NoteColumns.TITLE, cloudNote.title);
+ // 云端同步下来的数据,状态设为“已同步 (0)”
+ values.put(NoteColumns.SYNC_STATE, 0);
+ values.put(NoteColumns.MODIFIED_DATE, cloudNote.modifiedDate);
+ values.put(NoteColumns.BG_COLOR_ID, cloudNote.bgColorId);
+ values.put(NoteColumns.IS_AGENDA, cloudNote.isAgenda);
+ values.put(NoteColumns.AGENDA_DATE, cloudNote.agendaDate);
+ values.put(NoteColumns.IS_COMPLETED, cloudNote.isCompleted);
+
+ // 摘要处理:取正文前 60 个字符存入 snippet 字段用于列表展示
+ String snippet = cloudNote.content;
+ if (snippet != null && snippet.length() > 60) {
+ snippet = snippet.substring(0, 60);
+ }
+ values.put(NoteColumns.SNIPPET, snippet);
+
+ return values;
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/ui/AgendaFragment.java b/src/Notes-master/src/net/micode/notes/ui/AgendaFragment.java
new file mode 100644
index 0000000..bc3de59
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/AgendaFragment.java
@@ -0,0 +1,127 @@
+package net.micode.notes.ui;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CalendarView;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import net.micode.notes.R;
+import net.micode.notes.data.Notes;
+import net.micode.notes.data.Notes.NoteColumns;
+import java.util.Calendar;
+
+public class AgendaFragment extends Fragment {
+ private RecyclerView mRecyclerView;
+ private NotesListItemAdapter mAdapter;
+ private CalendarView mCalendarView;
+ private TextView mTvEmpty;
+ private long mSelectedDayStart;
+ private long mSelectedDayEnd;
+
+ private TextView tvDateHeader;
+ private View emptyState;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_agenda, container, false);
+ initUI(view);
+ return view;
+ }
+
+ private void initUI(View view) {
+ mCalendarView = view.findViewById(R.id.calendar_view);
+ mRecyclerView = view.findViewById(R.id.agenda_list);
+ tvDateHeader = view.findViewById(R.id.tv_agenda_date_header);
+ emptyState = view.findViewById(R.id.ll_empty_state);
+
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ mAdapter = new NotesListItemAdapter(getContext());
+ mRecyclerView.setAdapter(mAdapter);
+
+ // 默认显示今天
+ updateDateDisplay(Calendar.getInstance().get(Calendar.YEAR),
+ Calendar.getInstance().get(Calendar.MONTH),
+ Calendar.getInstance().get(Calendar.DAY_OF_MONTH));
+
+ mCalendarView.setOnDateChangeListener((view1, year, month, dayOfMonth) -> {
+ updateDateDisplay(year, month, dayOfMonth);
+ loadAgendaData();
+ });
+
+ loadAgendaData();
+ }
+
+ private void updateDateDisplay(int year, int month, int day) {
+ tvDateHeader.setText(String.format("%d年%d月%d日 的日程", year, month + 1, day));
+ Calendar cal = Calendar.getInstance();
+ cal.set(year, month, day);
+ calculateDayRange(cal.getTimeInMillis());
+ }
+
+ // 计算选中日期的 [00:00:00, 23:59:59] 时间戳
+ private void calculateDayRange(long timeInMillis) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTimeInMillis(timeInMillis);
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ mSelectedDayStart = cal.getTimeInMillis();
+
+ cal.set(Calendar.HOUR_OF_DAY, 23);
+ cal.set(Calendar.MINUTE, 59);
+ cal.set(Calendar.SECOND, 59);
+ mSelectedDayEnd = cal.getTimeInMillis();
+ }
+
+ private void loadAgendaData() {
+ // 增加空检查,防止 fragment 已经 detach 时还执行异步回调
+ if (!isAdded() || getContext() == null) return;
+
+ ContentResolver resolver = getContext().getContentResolver();
+
+ // 使用异步查询(推荐)或者简单的后台线程
+ new Thread(() -> {
+ String selection = NoteColumns.IS_AGENDA + "=1 AND "
+ + NoteColumns.AGENDA_DATE + " >= ? AND "
+ + NoteColumns.AGENDA_DATE + " <= ?";
+
+ String[] selectionArgs = new String[] {
+ String.valueOf(mSelectedDayStart),
+ String.valueOf(mSelectedDayEnd)
+ };
+
+ // 执行查询
+ final Cursor cursor = resolver.query(
+ Notes.CONTENT_NOTE_URI,
+ NoteItemData.PROJECTION,
+ selection,
+ selectionArgs,
+ NoteColumns.AGENDA_DATE + " ASC"
+ );
+
+ // 切回主线程更新 UI
+ if (getActivity() != null) {
+ getActivity().runOnUiThread(() -> {
+ mAdapter.changeCursor(cursor);
+ if (cursor == null || cursor.getCount() == 0) {
+ emptyState.setVisibility(View.VISIBLE);
+ mRecyclerView.setVisibility(View.GONE);
+ } else {
+ emptyState.setVisibility(View.GONE);
+ mRecyclerView.setVisibility(View.VISIBLE);
+ }
+ });
+ }
+ }).start();
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/ui/LoginActivity.java b/src/Notes-master/src/net/micode/notes/ui/LoginActivity.java
new file mode 100644
index 0000000..b0665ef
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/LoginActivity.java
@@ -0,0 +1,121 @@
+package net.micode.notes.ui;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+import com.google.firebase.auth.FirebaseUser;
+import android.content.ContentValues;
+import android.net.Uri;
+import net.micode.notes.data.Notes;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.firebase.auth.FirebaseAuth;
+
+import net.micode.notes.R;
+
+public class LoginActivity extends AppCompatActivity {
+ private EditText mEmailField;
+ private EditText mPasswordField;
+ private Button mLoginBtn;
+ private ProgressBar mLoadingBar;
+ private FirebaseAuth mAuth;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_login);
+
+ // 1. 初始化 Firebase Auth
+ mAuth = FirebaseAuth.getInstance();
+
+ // 2. 绑定 UI 控件
+ mEmailField = findViewById(R.id.et_email);
+ mPasswordField = findViewById(R.id.et_password);
+ mLoginBtn = findViewById(R.id.btn_login);
+ mLoadingBar = findViewById(R.id.loading_bar);
+
+ // 3. 登录逻辑
+ mLoginBtn.setOnClickListener(v -> handleAuth());
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ // [新增] 自动登录检查:如果 Firebase 已经记录了登录状态,直接跳过登录页
+ FirebaseUser currentUser = mAuth.getCurrentUser();
+ if (currentUser != null) {
+ updateLocalAccount(currentUser); // 确保本地存储了最新 UID
+ startActivity(new Intent(this, NotesListActivity.class));
+ finish();
+ }
+ }
+
+ // [新增] 辅助方法:将 Firebase 用户信息写入步骤 1.2 创建的数据库表中
+ private void updateLocalAccount(FirebaseUser user) {
+ android.content.ContentValues values = new android.content.ContentValues();
+ values.put(net.micode.notes.data.Notes.AccountColumns.UID, user.getUid());
+ values.put(net.micode.notes.data.Notes.AccountColumns.EMAIL, user.getEmail());
+
+ // 使用本地 ContentResolver 写入 user_account 表
+ getContentResolver().insert(
+ android.net.Uri.parse("content://" + net.micode.notes.data.Notes.AUTHORITY + "/user_account"),
+ values
+ );
+ }
+
+ private void handleAuth() {
+ String email = mEmailField.getText().toString().trim();
+ String password = mPasswordField.getText().toString().trim();
+
+ if (TextUtils.isEmpty(email) || TextUtils.isEmpty(password)) {
+ Toast.makeText(this, "请填写邮箱和密码", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (password.length() < 6) {
+ Toast.makeText(this, "密码至少需要6位", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ showLoading(true);
+
+ // 先尝试登录
+ mAuth.signInWithEmailAndPassword(email, password)
+ .addOnCompleteListener(this, task -> {
+ if (task.isSuccessful()) {
+ onAuthSuccess();
+ } else {
+ // 登录失败,尝试注册(方便演示)
+ mAuth.createUserWithEmailAndPassword(email, password)
+ .addOnCompleteListener(this, regTask -> {
+ showLoading(false);
+ if (regTask.isSuccessful()) {
+ onAuthSuccess();
+ } else {
+ Toast.makeText(this, "认证失败: " + regTask.getException().getMessage(), Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+ });
+ }
+
+ private void onAuthSuccess() {
+ // [新增] 成功认证后,立即同步身份到本地
+ FirebaseUser user = mAuth.getCurrentUser();
+ updateLocalAccount(user);
+
+ Toast.makeText(this, "登录成功!", Toast.LENGTH_SHORT).show();
+ startActivity(new Intent(this, NotesListActivity.class));
+ finish();
+ }
+
+ private void showLoading(boolean loading) {
+ mLoadingBar.setVisibility(loading ? View.VISIBLE : View.GONE);
+ mLoginBtn.setEnabled(!loading);
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java b/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java
index 80d00ee..289d154 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java
@@ -156,6 +156,8 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe
setContentView(R.layout.note_list);
initResources();
+ setupBottomNavigation();
+
/**
* Insert an introduction when user firstly use this application
*/
@@ -470,6 +472,55 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe
});
}
+ private void setupBottomNavigation() {
+ com.google.android.material.bottomnavigation.BottomNavigationView navView = findViewById(R.id.bottom_navigation);
+
+ navView.setOnItemSelectedListener(item -> {
+ int itemId = item.getItemId();
+
+ // 1. 获取 Fragment 管理器
+ androidx.fragment.app.FragmentManager fm = getSupportFragmentManager();
+ androidx.fragment.app.FragmentTransaction ft = fm.beginTransaction();
+
+ // 2. 隐藏所有的交互组件(默认隐藏)
+ findViewById(R.id.swipe_refresh_layout).setVisibility(View.GONE);
+ findViewById(R.id.fragment_container).setVisibility(View.GONE);
+ mAddNewNote.hide();
+
+ if (itemId == R.id.nav_notes) {
+ // --- 显示便签列表模式 ---
+ findViewById(R.id.swipe_refresh_layout).setVisibility(View.VISIBLE);
+ mAddNewNote.show();
+ if (getSupportActionBar() != null) getSupportActionBar().setTitle(R.string.app_name);
+
+ // 移除可能存在的日程 Fragment,释放资源
+ androidx.fragment.app.Fragment agenda = fm.findFragmentByTag("AGENDA");
+ if (agenda != null) ft.remove(agenda);
+
+ } else if (itemId == R.id.nav_agenda) {
+ // --- 切换到日程视图模式 ---
+ findViewById(R.id.fragment_container).setVisibility(View.VISIBLE);
+ if (getSupportActionBar() != null) getSupportActionBar().setTitle("智能日程");
+
+ // 使用 TAG 查找,避免重复创建引发的 GMS 校验冲突
+ androidx.fragment.app.Fragment agenda = fm.findFragmentByTag("AGENDA");
+ if (agenda == null) {
+ // 确切位置:在此注入我们之前设计的 AgendaFragment
+ ft.replace(R.id.fragment_container, new AgendaFragment(), "AGENDA");
+ }
+
+ } else if (itemId == R.id.nav_ai) {
+ // --- AI 助手(预留) ---
+ findViewById(R.id.fragment_container).setVisibility(View.VISIBLE);
+ if (getSupportActionBar() != null) getSupportActionBar().setTitle("AI 助理");
+ }
+
+ // 使用 commitAllowingStateLoss 彻底杜绝由于异步同步导致的 Activity 状态丢失闪退
+ ft.commitAllowingStateLoss();
+ return true;
+ });
+ }
+
private void showPasswordDialog() {
final EditText et = new EditText(this);
et.setInputType(android.text.InputType.TYPE_CLASS_NUMBER | android.text.InputType.TYPE_NUMBER_VARIATION_PASSWORD);
diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListItemAdapter.java b/src/Notes-master/src/net/micode/notes/ui/NotesListItemAdapter.java
new file mode 100644
index 0000000..1fdaaea
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/NotesListItemAdapter.java
@@ -0,0 +1,281 @@
+package net.micode.notes.ui;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.micode.notes.data.Notes;
+import net.micode.notes.tool.DataUtils;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+
+public class NotesListItemAdapter extends RecyclerView.Adapter {
+ private static final String TAG = "NotesListItemAdapter";
+ private Context mContext;
+ private Cursor mCursor;
+ private HashMap mSelectedIndex;
+ private int mNotesCount;
+ private boolean mChoiceMode;
+
+ private OnItemClickListener mOnItemClickListener;
+ private OnItemLongClickListener mOnItemLongClickListener;
+
+ public interface OnItemClickListener {
+ void onItemClick(View view, int position);
+ }
+
+ public interface OnItemLongClickListener {
+ boolean onItemLongClick(View view, int position);
+ }
+
+ public void setOnItemClickListener(OnItemClickListener listener) {
+ mOnItemClickListener = listener;
+ }
+
+ public void setOnItemLongClickListener(OnItemLongClickListener listener) {
+ mOnItemLongClickListener = listener;
+ }
+
+ /*
+ public static class AppWidgetAttribute {
+ public int widgetId;
+ public int widgetType;
+ }
+ */
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ public ViewHolder(View itemView) {
+ super(itemView);
+ }
+ }
+
+ public NotesListItemAdapter(Context context) {
+ mContext = context;
+ mSelectedIndex = new HashMap();
+ mNotesCount = 0;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ NotesListItem view = new NotesListItem(mContext);
+ view.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ final ViewHolder holder = new ViewHolder(view);
+
+ view.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mOnItemClickListener != null) {
+ mOnItemClickListener.onItemClick(v, holder.getAdapterPosition());
+ }
+ }
+ });
+
+ view.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ if (mOnItemLongClickListener != null) {
+ return mOnItemLongClickListener.onItemLongClick(v, holder.getAdapterPosition());
+ }
+ return false;
+ }
+ });
+
+ return holder;
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ if (!mCursor.moveToPosition(position)) {
+ Log.e(TAG, "couldn't move cursor to position " + position);
+ return;
+ }
+
+ View view = holder.itemView;
+ if (view instanceof NotesListItem) {
+ NoteItemData itemData = new NoteItemData(mContext, mCursor);
+ ((NotesListItem) view).bind(mContext, itemData, mChoiceMode,
+ isSelectedItem(position));
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ if (mCursor != null) {
+ return mCursor.getCount();
+ }
+ return 0;
+ }
+
+ public void setCheckedItem(final int position, final boolean checked) {
+ mSelectedIndex.put(position, checked);
+ notifyItemChanged(position);
+ }
+
+ public boolean isInChoiceMode() {
+ return mChoiceMode;
+ }
+
+ public void setChoiceMode(boolean mode) {
+ mSelectedIndex.clear();
+ mChoiceMode = mode;
+ notifyDataSetChanged();
+ }
+
+ public void selectAll(boolean checked) {
+ Cursor cursor = getCursor();
+ for (int i = 0; i < getItemCount(); i++) {
+ if (cursor.moveToPosition(i)) {
+ if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) {
+ setCheckedItem(i, checked);
+ }
+ }
+ }
+ notifyDataSetChanged();
+ }
+
+ public HashSet getSelectedItemIds() {
+ HashSet itemSet = new HashSet();
+ for (Integer position : mSelectedIndex.keySet()) {
+ if (mSelectedIndex.get(position) == true) {
+ Long id = getItemId(position);
+ if (id == Notes.ID_ROOT_FOLDER) {
+ Log.d(TAG, "Wrong item id, should not happen");
+ } else {
+ itemSet.add(id);
+ }
+ }
+ }
+ return itemSet;
+ }
+
+ public HashSet getSelectedWidget() {
+ HashSet itemSet = new HashSet();
+ for (Integer position : mSelectedIndex.keySet()) {
+ if (mSelectedIndex.get(position) == true) {
+ Cursor c = (Cursor) getItem(position);
+ if (c != null) {
+ DataUtils.AppWidgetAttribute widget = new DataUtils.AppWidgetAttribute();
+ NoteItemData item = new NoteItemData(mContext, c);
+ widget.widgetId = item.getWidgetId();
+ widget.widgetType = item.getWidgetType();
+ itemSet.add(widget);
+ } else {
+ Log.e(TAG, "Invalid cursor");
+ return null;
+ }
+ }
+ }
+ return itemSet;
+ }
+
+ public int getSelectedCount() {
+ Collection values = mSelectedIndex.values();
+ if (null == values) {
+ return 0;
+ }
+ Iterator iter = values.iterator();
+ int count = 0;
+ while (iter.hasNext()) {
+ if (true == iter.next()) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public boolean isAllSelected() {
+ int checkedCount = getSelectedCount();
+ return (checkedCount != 0 && checkedCount == mNotesCount);
+ }
+
+ public boolean isSelectedItem(final int position) {
+ if (null == mSelectedIndex.get(position)) {
+ return false;
+ }
+ return mSelectedIndex.get(position);
+ }
+
+ public void changeCursor(Cursor cursor) {
+ if (mCursor == cursor) {
+ return;
+ }
+ if (mCursor != null) {
+ mCursor.close();
+ }
+ mCursor = cursor;
+ if (mCursor != null) {
+ calcNotesCount();
+ }
+ notifyDataSetChanged();
+ }
+
+ public Cursor getCursor() {
+ return mCursor;
+ }
+
+ public Object getItem(int position) {
+ if (mCursor != null && mCursor.moveToPosition(position)) {
+ return mCursor;
+ }
+ return null;
+ }
+
+ public long getItemId(int position) {
+ if (mCursor != null && mCursor.moveToPosition(position)) {
+ // 使用标准的 NoteColumns.ID 常量,确保安全
+ int idColumnIndex = mCursor.getColumnIndex(net.micode.notes.data.Notes.NoteColumns.ID);
+ return mCursor.getLong(idColumnIndex);
+ }
+ return 0;
+ }
+
+ private void calcNotesCount() {
+ mNotesCount = 0;
+ for (int i = 0; i < getItemCount(); i++) {
+ Cursor c = (Cursor) getItem(i);
+ if (c != null) {
+ if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) {
+ mNotesCount++;
+ }
+ } else {
+ Log.e(TAG, "Invalid cursor");
+ return;
+ }
+ }
+ }
+
+ // [新增] 判断当前选中的所有项是否都已经置顶
+ public boolean isAllSelectedItemsPinned() {
+ if (mSelectedIndex == null || mSelectedIndex.size() == 0) {
+ return false;
+ }
+
+ // [Fix] 动态获取索引,比写死 13 更安全
+ int isPinnedColumnIndex = mCursor.getColumnIndex(Notes.NoteColumns.IS_PINNED);
+ if (isPinnedColumnIndex == -1) {
+ return false; // 如果查不到该列,默认视为未置顶
+ }
+
+ for (Integer position : mSelectedIndex.keySet()) {
+ if (mSelectedIndex.get(position)) {
+ if (mCursor != null && mCursor.moveToPosition(position)) {
+ // 使用动态索引读取
+ int isPinned = mCursor.getInt(isPinnedColumnIndex);
+ if (isPinned <= 0) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+}
--
2.34.1