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 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; + } +}