allNotes = dataManager.getNotes();
+ boolean exists = false;
+ for (Note n : allNotes) {
+ if (n.getId().equals(editingNote.getId())) {
+ exists = true;
+ break;
+ }
+ }
+
+ if (exists) {
+ dataManager.updateNote(editingNote);
+ } else {
+ dataManager.addNote(editingNote);
+ }
+
+ runOnUiThread(() -> {
+ Toast.makeText(NoteEditorActivity.this, "笔记已保存", Toast.LENGTH_SHORT).show();
+ finish();
+ });
+ }
+ });
+ }
+
+ public class WebAppInterface {
+ @JavascriptInterface
+ public void onContentChanged(String content) {
+ // 内容变化时的回调
+ }
+ }
+}
diff --git a/src/app/src/main/java/com/example/myapplication/NoteReaderActivity.java b/src/app/src/main/java/com/example/myapplication/NoteReaderActivity.java
new file mode 100644
index 0000000..6394a84
--- /dev/null
+++ b/src/app/src/main/java/com/example/myapplication/NoteReaderActivity.java
@@ -0,0 +1,177 @@
+package com.example.myapplication;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+
+public class NoteReaderActivity extends AppCompatActivity {
+
+ public static final String EXTRA_NOTE_ID = "noteId";
+
+ private WebView webView;
+ private Note note;
+ private DataManager dataManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_note_reader);
+
+ dataManager = new DataManager(this);
+ loadNote();
+ initViews();
+ displayNote();
+ }
+
+ private void loadNote() {
+ String noteId = getIntent().getStringExtra(EXTRA_NOTE_ID);
+ if (noteId != null && !noteId.isEmpty()) {
+ for (Note n : dataManager.getNotes()) {
+ if (noteId.equals(n.getId())) {
+ note = n;
+ break;
+ }
+ }
+ }
+ }
+
+ private void initViews() {
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ if (getSupportActionBar() != null) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setTitle(note != null ? note.getTitle() : "笔记");
+ }
+
+ webView = findViewById(R.id.webview);
+ android.webkit.WebSettings settings = webView.getSettings();
+ settings.setJavaScriptEnabled(true);
+ settings.setDomStorageEnabled(true);
+ settings.setAllowFileAccess(true);
+ settings.setAllowContentAccess(true);
+ settings.setMixedContentMode(android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
+ webView.setWebViewClient(new WebViewClient());
+ }
+
+ private void displayNote() {
+ if (note == null) return;
+
+ StringBuilder html = new StringBuilder();
+ html.append("");
+ html.append("");
+ html.append("");
+
+ if (note.getTitle() != null && !note.getTitle().isEmpty()) {
+ html.append("").append(escapeHtml(note.getTitle())).append("
");
+ }
+
+ if (note.getCourseName() != null && !note.getCourseName().isEmpty()) {
+ html.append("课程: ").append(escapeHtml(note.getCourseName())).append("
");
+ }
+
+ // 显示内容(HTML格式)
+ String content = note.getContent() != null ? note.getContent() : "";
+ // 兼容历史数据:还原 Unicode 转义(例如 \\u003C -> <)
+ content = content
+ .replace("\\u003C", "<").replace("\\u003c", "<")
+ .replace("\\u003E", ">").replace("\\u003e", ">")
+ .replace("\\u0026", "&");
+ if (!content.isEmpty()) {
+ // 如果内容包含HTML,直接使用;否则转换为HTML
+ if (!content.contains("<")) {
+ content = content.replace("\n", "
");
+ }
+ html.append("").append(content).append("
");
+ }
+
+ // 显示图片(如果内容中没有图片,则单独显示)
+ if (note.getImagePaths() != null && !note.getImagePaths().isEmpty()) {
+ boolean hasImagesInContent = content != null && content.contains("
");
+ }
+ }
+ }
+ }
+
+ html.append("");
+
+ String baseUrl = "file://" + getFilesDir().getAbsolutePath() + "/";
+ webView.loadDataWithBaseURL(baseUrl, html.toString(), "text/html", "UTF-8", null);
+ }
+
+ private String escapeHtml(String text) {
+ if (text == null) return "";
+ return text.replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace("\"", """)
+ .replace("'", "'");
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_note_reader, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void editNote() {
+ if (note == null) return;
+ Intent intent = new Intent(this, NoteEditorActivity.class);
+ intent.putExtra(NoteEditorActivity.EXTRA_NOTE_ID, note.getId());
+ startActivity(intent);
+ finish();
+ }
+
+ private void deleteNote() {
+ if (note == null) return;
+ new android.app.AlertDialog.Builder(this)
+ .setTitle("删除笔记")
+ .setMessage("确定要删除笔记《" + note.getTitle() + "》吗?")
+ .setPositiveButton("删除", (dialog, which) -> {
+ dataManager.deleteNote(note.getId());
+ // 删除图片文件
+ if (note.getImagePaths() != null) {
+ for (String imagePath : note.getImagePaths()) {
+ try {
+ java.io.File file = new java.io.File(imagePath);
+ if (file.exists()) {
+ file.delete();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ finish();
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ }
+}
+
diff --git a/src/app/src/main/java/com/example/myapplication/NotesAdapter.java b/src/app/src/main/java/com/example/myapplication/NotesAdapter.java
new file mode 100644
index 0000000..3146fbc
--- /dev/null
+++ b/src/app/src/main/java/com/example/myapplication/NotesAdapter.java
@@ -0,0 +1,290 @@
+package com.example.myapplication;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.cardview.widget.CardView;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public class NotesAdapter extends RecyclerView.Adapter {
+
+ private List notes;
+ private Context context;
+ private OnNoteClickListener listener;
+ private NoteViewHolder currentSwipedHolder; // 当前滑动的ViewHolder
+
+ public interface OnNoteClickListener {
+ void onNoteClick(Note note);
+ void onNoteLongClick(Note note);
+ void onEditClick(Note note);
+ void onDeleteClick(Note note);
+ void onResetOtherItems(); // 重置其他项的滑动状态
+ }
+
+ public NotesAdapter(Context context, List notes) {
+ this.context = context;
+ this.notes = notes;
+ }
+
+ public void setOnNoteClickListener(OnNoteClickListener listener) {
+ this.listener = listener;
+ }
+
+ @NonNull
+ @Override
+ public NoteViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(context).inflate(R.layout.item_note, parent, false);
+ return new NoteViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull NoteViewHolder holder, int position) {
+ Note note = notes.get(position);
+ holder.bind(note);
+ // 重置滑动状态
+ holder.resetSwipeState();
+ }
+
+ @Override
+ public int getItemCount() {
+ return notes.size();
+ }
+
+ public void updateNotes(List newNotes) {
+ this.notes = newNotes;
+ notifyDataSetChanged();
+ }
+
+ class NoteViewHolder extends RecyclerView.ViewHolder {
+ TextView tvNoteTitle, tvNoteContent, tvCourseName, tvModifyTime;
+ CardView cardContent;
+ LinearLayout layoutSwipeActions;
+ ImageButton btnEdit, btnDelete;
+ private float startX = 0;
+ private boolean isSwiped = false;
+ private boolean isDragging = false; // 标记是否正在拖动
+ private final float revealPx; // 需要左滑露出的宽度(px)
+
+ public NoteViewHolder(@NonNull View itemView) {
+ super(itemView);
+ tvNoteTitle = itemView.findViewById(R.id.tv_note_title);
+ tvNoteContent = itemView.findViewById(R.id.tv_note_content);
+ tvCourseName = itemView.findViewById(R.id.tv_course_name);
+ tvModifyTime = itemView.findViewById(R.id.tv_modify_time);
+ cardContent = itemView.findViewById(R.id.card_content);
+ layoutSwipeActions = itemView.findViewById(R.id.layout_swipe_actions);
+ btnEdit = itemView.findViewById(R.id.btn_edit);
+ btnDelete = itemView.findViewById(R.id.btn_delete);
+
+ // 以 96dp 作为露出宽度,转换为 px
+ float density = itemView.getResources().getDisplayMetrics().density;
+ revealPx = 96f * density;
+
+ // 编辑按钮
+ btnEdit.setOnClickListener(v -> {
+ if (listener != null) {
+ int position = getAdapterPosition();
+ if (position != RecyclerView.NO_POSITION) {
+ listener.onEditClick(notes.get(position));
+ resetSwipeState();
+ }
+ }
+ });
+
+ // 删除按钮
+ btnDelete.setOnClickListener(v -> {
+ if (listener != null) {
+ int position = getAdapterPosition();
+ if (position != RecyclerView.NO_POSITION) {
+ listener.onDeleteClick(notes.get(position));
+ resetSwipeState();
+ }
+ }
+ });
+
+ // 使用 GestureDetector 处理点击和长按
+ final GestureDetector gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ // 单击事件
+ android.util.Log.d("NotesAdapter", "Card tapped");
+ if (listener != null && !isSwiped) {
+ int position = getAdapterPosition();
+ if (position != RecyclerView.NO_POSITION && position < notes.size()) {
+ listener.onNoteClick(notes.get(position));
+ }
+ } else if (isSwiped) {
+ // 如果已滑动,点击恢复
+ resetSwipeState();
+ }
+ return true;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ // 长按不做任何操作(禁用长按菜单)
+ }
+ });
+
+ // 触摸事件处理滑动
+ cardContent.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ // 先让 GestureDetector 处理
+ gestureDetector.onTouchEvent(event);
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ startX = event.getRawX();
+ isDragging = false;
+ return true; // 消费事件以接收后续事件
+
+ case MotionEvent.ACTION_MOVE:
+ float deltaX = event.getRawX() - startX;
+ // 如果横向移动超过阈值,认为是滑动
+ if (Math.abs(deltaX) > 20) {
+ if (!isDragging) {
+ isDragging = true;
+ // 请求父视图不拦截触摸事件
+ ViewParent parent = v.getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+
+ // 实时跟随手指移动
+ if (deltaX < 0 && !isSwiped) { // 向左滑动
+ float translationX = Math.max(deltaX, -revealPx);
+ cardContent.setTranslationX(translationX);
+ if (translationX < -revealPx / 2f) {
+ layoutSwipeActions.setVisibility(View.VISIBLE);
+ }
+ } else if (deltaX > 0 && isSwiped) { // 向右滑动恢复
+ float translationX = Math.min(deltaX, 0f);
+ cardContent.setTranslationX(translationX - revealPx);
+ if (translationX > -revealPx / 2f) {
+ layoutSwipeActions.setVisibility(View.GONE);
+ }
+ }
+ }
+ return true;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ ViewParent parent = v.getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(false);
+ }
+
+ if (isDragging) {
+ float finalDeltaX = event.getRawX() - startX;
+ if (finalDeltaX < -revealPx * 0.5f && !isSwiped) {
+ // 滑动足够距离,显示按钮
+ showSwipeActions();
+ } else if (!isSwiped) {
+ // 滑动距离不够,恢复
+ resetSwipeState();
+ } else if (finalDeltaX > revealPx * 0.3f && isSwiped) {
+ // 向右滑动恢复
+ resetSwipeState();
+ } else if (isSwiped) {
+ // 保持滑动状态
+ showSwipeActions();
+ }
+ isDragging = false;
+ }
+ return true;
+ }
+ return true;
+ }
+ });
+
+ // 点击空白区域恢复滑动状态
+ itemView.setOnClickListener(v -> {
+ if (isSwiped) {
+ resetSwipeState();
+ }
+ });
+ }
+
+ private void resetOtherItems() {
+ // 重置其他ViewHolder的滑动状态
+ if (listener != null) {
+ listener.onResetOtherItems();
+ }
+ }
+
+ private void showSwipeActions() {
+ // 重置其他项的滑动状态
+ if (currentSwipedHolder != null && currentSwipedHolder != NoteViewHolder.this) {
+ currentSwipedHolder.resetSwipeState();
+ }
+ currentSwipedHolder = NoteViewHolder.this;
+
+ isSwiped = true;
+ layoutSwipeActions.setVisibility(View.VISIBLE);
+ // 动画滑动到最终位置
+ cardContent.animate()
+ .translationX(-revealPx)
+ .setDuration(200)
+ .start();
+ }
+
+ public void resetSwipeState() {
+ isSwiped = false;
+ layoutSwipeActions.setVisibility(View.GONE);
+ cardContent.animate()
+ .translationX(0f)
+ .setDuration(200)
+ .start();
+ if (currentSwipedHolder == NoteViewHolder.this) {
+ currentSwipedHolder = null;
+ }
+ }
+
+ public void bind(Note note) {
+ tvNoteTitle.setText(note.getTitle() != null ? note.getTitle() : "");
+
+ // 显示内容预览(去除HTML标签)
+ String content = note.getContent() != null ? note.getContent() : "";
+ // 兼容历史数据:还原 Unicode 转义再做预览(例如 \\u003C -> <)
+ content = content
+ .replace("\\u003C", "<").replace("\\u003c", "<")
+ .replace("\\u003E", ">").replace("\\u003e", ">")
+ .replace("\\u0026", "&");
+ // 简单去除HTML标签
+ content = content.replaceAll("<[^>]+>", "").trim();
+ if (content.isEmpty() && note.getImagePaths() != null && !note.getImagePaths().isEmpty()) {
+ content = "[包含 " + note.getImagePaths().size() + " 张图片]";
+ }
+ tvNoteContent.setText(content);
+
+ // 显示关联课程
+ if (note.getCourseName() != null && !note.getCourseName().isEmpty()) {
+ tvCourseName.setText(note.getCourseName());
+ tvCourseName.setVisibility(View.VISIBLE);
+ } else {
+ tvCourseName.setVisibility(View.GONE);
+ }
+
+ // 格式化修改时间
+ SimpleDateFormat sdf = new SimpleDateFormat("MM-dd HH:mm", Locale.getDefault());
+ tvModifyTime.setText(sdf.format(new Date(note.getModifyTime())));
+ }
+ }
+}
diff --git a/src/app/src/main/java/com/example/myapplication/NotesFragment.java b/src/app/src/main/java/com/example/myapplication/NotesFragment.java
new file mode 100644
index 0000000..d176e97
--- /dev/null
+++ b/src/app/src/main/java/com/example/myapplication/NotesFragment.java
@@ -0,0 +1,368 @@
+package com.example.myapplication;
+
+import android.app.AlertDialog;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.*;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NotesFragment extends Fragment {
+
+ private RecyclerView rvNotes;
+ private FloatingActionButton fabAddNote;
+ private Spinner spinnerCourseFilter;
+ private LinearLayout layoutEmpty;
+ private com.google.android.material.textfield.TextInputLayout tilNoteSearch;
+ private android.widget.EditText etNoteSearch;
+
+ private DataManager dataManager;
+ private List allNotes;
+ private List filteredNotes;
+ private List courses;
+ private NotesAdapter notesAdapter;
+
+ private String selectedCourseId = "";
+ private String searchQuery = "";
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_notes, container, false);
+
+ initViews(view);
+ setupData();
+ setupListeners();
+ setupRecyclerView();
+ setupCourseFilter();
+ loadNotes();
+
+ return view;
+ }
+
+ private void initViews(View view) {
+ rvNotes = view.findViewById(R.id.rv_notes);
+ fabAddNote = view.findViewById(R.id.fab_add_note);
+ spinnerCourseFilter = view.findViewById(R.id.spinner_course_filter);
+ layoutEmpty = view.findViewById(R.id.layout_empty);
+ tilNoteSearch = view.findViewById(R.id.til_note_search);
+ etNoteSearch = view.findViewById(R.id.et_note_search);
+ }
+
+ private void setupData() {
+ dataManager = new DataManager(getContext());
+ allNotes = new ArrayList<>();
+ filteredNotes = new ArrayList<>();
+ courses = dataManager.getCourses();
+ }
+
+ private void setupListeners() {
+ fabAddNote.setOnClickListener(v -> {
+ android.content.Intent intent = new android.content.Intent(getContext(), NoteEditorActivity.class);
+ startActivity(intent);
+ });
+
+ if (tilNoteSearch != null) {
+ tilNoteSearch.setEndIconOnClickListener(v -> filterNotes());
+ }
+ if (etNoteSearch != null) {
+ etNoteSearch.addTextChangedListener(new android.text.TextWatcher() {
+ @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+ @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
+ searchQuery = s.toString().trim();
+ filterNotes();
+ }
+ @Override public void afterTextChanged(android.text.Editable s) {}
+ });
+ }
+ }
+
+ private void setupRecyclerView() {
+ notesAdapter = new NotesAdapter(getContext(), filteredNotes);
+ notesAdapter.setOnNoteClickListener(new NotesAdapter.OnNoteClickListener() {
+ @Override
+ public void onNoteClick(Note note) {
+ // 打开阅读模式
+ android.content.Intent intent = new android.content.Intent(getContext(), NoteReaderActivity.class);
+ intent.putExtra(NoteReaderActivity.EXTRA_NOTE_ID, note.getId());
+ startActivity(intent);
+ }
+
+ @Override
+ public void onNoteLongClick(Note note) {
+ showNoteOptions(note);
+ }
+
+ @Override
+ public void onEditClick(Note note) {
+ // 打开编辑模式
+ android.content.Intent intent = new android.content.Intent(getContext(), NoteEditorActivity.class);
+ intent.putExtra(NoteEditorActivity.EXTRA_NOTE_ID, note.getId());
+ startActivity(intent);
+ }
+
+ @Override
+ public void onDeleteClick(Note note) {
+ deleteNote(note);
+ }
+
+ @Override
+ public void onResetOtherItems() {
+ // 重置所有滑动状态
+ notesAdapter.notifyDataSetChanged();
+ }
+ });
+ rvNotes.setLayoutManager(new LinearLayoutManager(getContext()));
+ rvNotes.setAdapter(notesAdapter);
+ }
+
+ private void setupCourseFilter() {
+ List courseNames = new ArrayList<>();
+ courseNames.add("全部课程");
+
+ List courseIds = new ArrayList<>();
+ courseIds.add("");
+
+ for (Course course : courses) {
+ courseNames.add(course.getName());
+ courseIds.add(course.getId());
+ }
+
+ ArrayAdapter adapter = new ArrayAdapter<>(getContext(),
+ android.R.layout.simple_spinner_item, courseNames);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinnerCourseFilter.setAdapter(adapter);
+
+ spinnerCourseFilter.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ selectedCourseId = courseIds.get(position);
+ filterNotes();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {}
+ });
+ }
+
+ private void loadNotes() {
+ allNotes = dataManager.getNotes();
+ filterNotes();
+ }
+
+ private void filterNotes() {
+ filteredNotes.clear();
+ String q = searchQuery == null ? "" : searchQuery.toLowerCase();
+ for (Note note : allNotes) {
+ boolean courseOk = selectedCourseId.isEmpty() || (note.getCourseId() != null && note.getCourseId().equals(selectedCourseId));
+ if (!courseOk) continue;
+ if (q.isEmpty()) {
+ filteredNotes.add(note);
+ } else {
+ String title = note.getTitle() == null ? "" : note.getTitle().toLowerCase();
+ String content = note.getContent() == null ? "" : note.getContent().toLowerCase();
+ String course = note.getCourseName() == null ? "" : note.getCourseName().toLowerCase();
+ if (title.contains(q) || content.contains(q) || course.contains(q)) {
+ filteredNotes.add(note);
+ }
+ }
+ }
+ // 按修改时间倒序
+ java.util.Collections.sort(filteredNotes, (a, b) -> Long.compare(b.getModifyTime(), a.getModifyTime()));
+ notesAdapter.updateNotes(filteredNotes);
+ updateEmptyView();
+ }
+
+ private void updateEmptyView() {
+ if (filteredNotes.isEmpty()) {
+ rvNotes.setVisibility(View.GONE);
+ layoutEmpty.setVisibility(View.VISIBLE);
+ } else {
+ rvNotes.setVisibility(View.VISIBLE);
+ layoutEmpty.setVisibility(View.GONE);
+ }
+ }
+
+ private void showAddNoteDialog(Note editNote) {
+ View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.dialog_add_note, null);
+
+ TextView tvDialogTitle = dialogView.findViewById(R.id.tv_dialog_title);
+ EditText etNoteTitle = dialogView.findViewById(R.id.et_note_title);
+ EditText etNoteContent = dialogView.findViewById(R.id.et_note_content);
+ Spinner spinnerCourse = dialogView.findViewById(R.id.spinner_course);
+
+ // 设置标题
+ tvDialogTitle.setText(editNote == null ? "添加笔记" : "编辑笔记");
+
+ // 设置课程选择器
+ List courseOptions = new ArrayList<>();
+ courseOptions.add("无关联课程");
+
+ List courseIdOptions = new ArrayList<>();
+ courseIdOptions.add("");
+
+ for (Course course : courses) {
+ courseOptions.add(course.getName());
+ courseIdOptions.add(course.getId());
+ }
+
+ ArrayAdapter courseAdapter = new ArrayAdapter<>(getContext(),
+ android.R.layout.simple_spinner_item, courseOptions);
+ courseAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinnerCourse.setAdapter(courseAdapter);
+
+ // 如果是编辑模式,填充现有数据
+ if (editNote != null) {
+ etNoteTitle.setText(editNote.getTitle());
+ etNoteContent.setText(editNote.getContent());
+
+ // 设置课程选择
+ if (editNote.getCourseId() != null) {
+ for (int i = 0; i < courseIdOptions.size(); i++) {
+ if (courseIdOptions.get(i).equals(editNote.getCourseId())) {
+ spinnerCourse.setSelection(i);
+ break;
+ }
+ }
+ }
+ }
+
+ AlertDialog dialog = new AlertDialog.Builder(getContext())
+ .setView(dialogView)
+ .create();
+
+ dialogView.findViewById(R.id.btn_cancel).setOnClickListener(v -> dialog.dismiss());
+
+ dialogView.findViewById(R.id.btn_save).setOnClickListener(v -> {
+ String title = etNoteTitle.getText().toString().trim();
+ String content = etNoteContent.getText().toString().trim();
+
+ if (title.isEmpty()) {
+ Toast.makeText(getContext(), "请输入笔记标题", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (content.isEmpty()) {
+ Toast.makeText(getContext(), "请输入笔记内容", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ String selectedCourseId = courseIdOptions.get(spinnerCourse.getSelectedItemPosition());
+ String selectedCourseName = "";
+ if (!selectedCourseId.isEmpty()) {
+ selectedCourseName = courseOptions.get(spinnerCourse.getSelectedItemPosition());
+ }
+
+ if (editNote == null) {
+ // 添加新笔记
+ Note newNote = new Note(title, content, selectedCourseId, selectedCourseName);
+ dataManager.addNote(newNote);
+ allNotes.add(newNote);
+ Toast.makeText(getContext(), "笔记添加成功", Toast.LENGTH_SHORT).show();
+ } else {
+ // 编辑现有笔记
+ editNote.setTitle(title);
+ editNote.setContent(content);
+ editNote.setCourseId(selectedCourseId);
+ editNote.setCourseName(selectedCourseName);
+ dataManager.saveNotes(allNotes);
+ Toast.makeText(getContext(), "笔记修改成功", Toast.LENGTH_SHORT).show();
+ }
+
+ filterNotes();
+ dialog.dismiss();
+ });
+
+ dialog.show();
+ }
+
+ private void deleteNote(Note note) {
+ new AlertDialog.Builder(getContext())
+ .setTitle("删除笔记")
+ .setMessage("确定要删除笔记《" + note.getTitle() + "》吗?")
+ .setPositiveButton("删除", (dialog, which) -> {
+ dataManager.deleteNote(note.getId());
+ // 删除图片文件
+ if (note.getImagePaths() != null) {
+ for (String imagePath : note.getImagePaths()) {
+ try {
+ java.io.File file = new java.io.File(imagePath);
+ if (file.exists()) {
+ file.delete();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ allNotes.remove(note);
+ filterNotes();
+ Toast.makeText(getContext(), "笔记已删除", Toast.LENGTH_SHORT).show();
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ }
+
+ private void showNoteDetails(Note note) {
+ String details = note.getContent();
+ String title = note.getTitle();
+ if (note.getCourseName() != null && !note.getCourseName().isEmpty()) {
+ title += " (" + note.getCourseName() + ")";
+ }
+
+ new AlertDialog.Builder(getContext())
+ .setTitle(title)
+ .setMessage(details)
+ .setPositiveButton("确定", null)
+ .setNeutralButton("编辑", (dialog, which) -> showAddNoteDialog(note))
+ .show();
+ }
+
+ private void showNoteOptions(Note note) {
+ String[] options = {"查看详情", "编辑笔记", "删除笔记"};
+
+ new AlertDialog.Builder(getContext())
+ .setTitle(note.getTitle())
+ .setItems(options, (dialog, which) -> {
+ switch (which) {
+ case 0:
+ showNoteDetails(note);
+ break;
+ case 1:
+ showAddNoteDialog(note);
+ break;
+ case 2:
+ deleteNote(note);
+ break;
+ }
+ })
+ .show();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ // 重新加载课程数据(可能有新增的课程)
+ courses = dataManager.getCourses();
+ setupCourseFilter();
+ // 返回时刷新笔记列表
+ loadNotes();
+ // 重置所有滑动状态
+ if (notesAdapter != null) {
+ notesAdapter.notifyDataSetChanged();
+ }
+ }
+}
+
+
diff --git a/src/app/src/main/java/com/example/myapplication/ReminderAlertActivity.java b/src/app/src/main/java/com/example/myapplication/ReminderAlertActivity.java
new file mode 100644
index 0000000..e29a8af
--- /dev/null
+++ b/src/app/src/main/java/com/example/myapplication/ReminderAlertActivity.java
@@ -0,0 +1,80 @@
+package com.example.myapplication;
+
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+
+public class ReminderAlertActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ setShowWhenLocked(true);
+ setTurnScreenOn(true);
+ }
+
+ getWindow().addFlags(
+ android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ | android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ | android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ | android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+ );
+
+ setFinishOnTouchOutside(false);
+
+ String courseName = getIntent().getStringExtra(ReminderReceiver.EXTRA_COURSE_NAME);
+ String location = getIntent().getStringExtra(ReminderReceiver.EXTRA_COURSE_LOCATION);
+ String teacher = getIntent().getStringExtra(ReminderReceiver.EXTRA_COURSE_TEACHER);
+
+ StringBuilder message = new StringBuilder();
+ if (location != null && !location.isEmpty()) {
+ message.append("上课地点:").append(location);
+ }
+ if (teacher != null && !teacher.isEmpty()) {
+ if (message.length() > 0) {
+ message.append("\n");
+ }
+ message.append("任课教师:").append(teacher);
+ }
+ if (message.length() > 0) {
+ message.append("\n");
+ }
+ message.append("请准时到达教室,祝您上课顺利!");
+
+ new AlertDialog.Builder(this)
+ .setTitle(courseName == null || courseName.isEmpty() ? "上课提醒" : "上课提醒:" + courseName)
+ .setMessage(message.toString())
+ .setCancelable(false)
+ .setPositiveButton("知道了", (dialog, which) -> finish())
+ .setNegativeButton("稍后提醒", (dialog, which) -> {
+ // 五分钟后再次提醒
+ Course course = findCourseById(getIntent().getStringExtra(ReminderReceiver.EXTRA_COURSE_ID));
+ if (course != null) {
+ ReminderScheduler.scheduleReminderInMinutes(this, course, 5);
+ }
+ finish();
+ })
+ .setOnCancelListener(dialog -> finish())
+ .show();
+ }
+
+ private Course findCourseById(String courseId) {
+ if (courseId == null || courseId.isEmpty()) {
+ return null;
+ }
+ DataManager dataManager = new DataManager(this);
+ for (Course course : dataManager.getCourses()) {
+ if (course != null && courseId.equals(course.getId())) {
+ return course;
+ }
+ }
+ return null;
+ }
+}
+
+
diff --git a/src/app/src/main/java/com/example/myapplication/ReminderReceiver.java b/src/app/src/main/java/com/example/myapplication/ReminderReceiver.java
new file mode 100644
index 0000000..e27dd56
--- /dev/null
+++ b/src/app/src/main/java/com/example/myapplication/ReminderReceiver.java
@@ -0,0 +1,254 @@
+package com.example.myapplication;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.Build;
+import android.text.TextUtils;
+
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import java.util.List;
+
+public class ReminderReceiver extends BroadcastReceiver {
+
+ public static final String ACTION_SHOW_REMINDER = "com.example.myapplication.action.SHOW_REMINDER";
+ public static final String EXTRA_COURSE_ID = "extra_course_id";
+ public static final String EXTRA_COURSE_NAME = "extra_course_name";
+ public static final String EXTRA_COURSE_LOCATION = "extra_course_location";
+ public static final String EXTRA_COURSE_TEACHER = "extra_course_teacher";
+ public static final String EXTRA_REMINDER_MINUTES = "extra_reminder_minutes";
+ public static final String EXTRA_REMINDER_SECONDS = "extra_reminder_seconds";
+ public static final String EXTRA_TIME_SLOT = "extra_time_slot";
+ public static final String EXTRA_DAY_OF_WEEK = "extra_day_of_week";
+ public static final String EXTRA_DISPLAY_TIME = "extra_display_time";
+ public static final String ACTION_INAPP_REMINDER = "com.example.myapplication.action.INAPP_REMINDER";
+
+ private static final String CHANNEL_ID = "course_reminder_channel";
+ private static final String CHANNEL_NAME = "课程提醒";
+ private static final int NOTIFICATION_ID_BASE = 10000;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ android.util.Log.d("ReminderReceiver", "onReceive 被调用");
+ if (context == null || intent == null) {
+ android.util.Log.w("ReminderReceiver", "context 或 intent 为空");
+ return;
+ }
+
+ String courseId = intent.getStringExtra(EXTRA_COURSE_ID);
+ if (TextUtils.isEmpty(courseId)) {
+ android.util.Log.w("ReminderReceiver", "courseId 为空");
+ return;
+ }
+ android.util.Log.d("ReminderReceiver", "收到提醒广播,课程ID: " + courseId);
+
+ DataManager dataManager = new DataManager(context);
+ List courses = dataManager.getCourses();
+ Course course = null;
+ for (Course c : courses) {
+ if (c != null && courseId.equals(c.getId())) {
+ course = c;
+ break;
+ }
+ }
+
+ if (course == null) {
+ return;
+ }
+
+ // 移除防重复逻辑,允许用户设置多个提醒时间
+ // 例如:提前10分钟一次,提前5分钟一次
+ long now = System.currentTimeMillis();
+ SharedPreferences oncePrefs = context.getSharedPreferences("reminder_once", Context.MODE_PRIVATE);
+ String onceKey = "last_" + courseId;
+ long lastTime = oncePrefs.getLong(onceKey, 0L);
+
+ // 只在10秒内防止重复(避免系统bug导致的重复触发)
+ if (now - lastTime < 10000L) {
+ android.util.Log.d("ReminderReceiver", "忽略10秒内重复提醒,课程ID: " + courseId);
+ return;
+ }
+ oncePrefs.edit().putLong(onceKey, now).apply();
+
+ // 只显示通知栏提醒,不打断用户当前操作
+ showNotification(context, course);
+ // 移除全屏弹窗,避免打断用户使用app
+ // showAlertPopup(context, course);
+ showAlertPopup(context, course);
+
+ // 重新调度下一次提醒
+ // 注意:对于5秒测试提醒,如果已经触发过了,应该调度到下一周的课程时间
+ // ReminderScheduler.scheduleReminder 内部会处理,确保不会立即触发
+ if (course.getReminderAdvanceSeconds() == 5) {
+ // 5秒测试提醒:触发后不再重新调度,避免循环
+ // 如果需要继续提醒,应该在课程正常时间重新调度
+ // 这里暂时不重新调度,避免立即触发
+ } else {
+ // 正常提醒:重新调度到下一周的课程时间
+ ReminderScheduler.scheduleReminder(context, course);
+ }
+ }
+
+ private void showNotification(Context context, Course course) {
+ createChannelIfNeeded(context);
+
+ // 移除点击通知打开Activity的行为,避免打断用户
+ int requestCode = course.getId() != null ? course.getId().hashCode() : course.hashCode();
+ int notificationId = NOTIFICATION_ID_BASE + Math.abs(requestCode);
+
+ int delaySeconds = course.getReminderAdvanceSeconds();
+ String title;
+ String message;
+ if (delaySeconds == 5) {
+ title = "课程提醒(测试)";
+ message = "《" + safeText(course.getName()) + "》提醒测试";
+ } else if (delaySeconds < 60) {
+ title = "课程提醒";
+ message = "《" + safeText(course.getName()) + "》还有" + delaySeconds + "秒开始";
+ } else if (delaySeconds % 3600 == 0) {
+ title = "课程提醒";
+ message = "《" + safeText(course.getName()) + "》还有" + (delaySeconds / 3600) + "小时开始";
+ } else {
+ title = "课程提醒";
+ message = "《" + safeText(course.getName()) + "》还有" + (delaySeconds / 60) + "分钟开始";
+ }
+ String detail = buildContentText(course);
+ String fullContent = message + "\n" + detail;
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setContentTitle(title)
+ .setContentText(message)
+ .setStyle(new NotificationCompat.BigTextStyle().bigText(fullContent))
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_REMINDER)
+ .setAutoCancel(true)
+ .setColor(context.getResources().getColor(R.color.primary, null))
+ // 不需要震动/声音,仅以消息提示
+ .setVibrate(new long[]{0L})
+ .setSound(null)
+ .setOnlyAlertOnce(true)
+ .setOngoing(false);
+
+ NotificationManagerCompat.from(context).notify(notificationId, builder.build());
+
+ // 同时发一条本地广播,让前台界面显示顶部提示
+ notifyInAppBanner(context, course);
+ }
+
+ private void showAlertPopup(Context context, Course course) {
+ Intent intent = new Intent(context, ReminderAlertActivity.class);
+ intent.putExtra(EXTRA_COURSE_ID, course.getId());
+ intent.putExtra(EXTRA_COURSE_NAME, course.getName());
+ intent.putExtra(EXTRA_COURSE_LOCATION, course.getLocation());
+ intent.putExtra(EXTRA_COURSE_TEACHER, course.getTeacher());
+ intent.putExtra(EXTRA_TIME_SLOT, course.getTimeSlot());
+ intent.putExtra(EXTRA_DAY_OF_WEEK, course.getDayOfWeek());
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ context.startActivity(intent);
+ }
+
+ private void createChannelIfNeeded(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationManager manager = context.getSystemService(NotificationManager.class);
+ if (manager == null) {
+ return;
+ }
+ NotificationChannel channel = manager.getNotificationChannel(CHANNEL_ID);
+ if (channel == null) {
+ channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
+ channel.enableLights(false);
+ channel.enableVibration(false);
+ channel.setSound(null, null);
+ channel.setDescription("课程开始前的消息提醒");
+ manager.createNotificationChannel(channel);
+ }
+ }
+ }
+
+ private String buildContentText(Course course) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("地点:").append(safeText(course.getLocation()));
+ String teacher = safeText(course.getTeacher());
+ if (!TextUtils.isEmpty(teacher)) {
+ builder.append(" | 教师:").append(teacher);
+ }
+ builder.append(" | 时间:").append(ReminderScheduler.formatDisplayTime(course));
+ return builder.toString();
+ }
+
+ private String safeText(String text) {
+ return text == null ? "" : text;
+ }
+
+ private void notifyInAppBanner(Context context, Course course) {
+ try {
+ // 检查应用是否在前台运行
+ boolean isAppInForeground = isAppInForeground(context);
+ android.util.Log.d("ReminderReceiver", "应用是否在前台: " + isAppInForeground);
+
+ Intent intent = new Intent(ACTION_INAPP_REMINDER);
+ intent.putExtra(EXTRA_COURSE_NAME, safeText(course.getName()));
+ intent.putExtra(EXTRA_COURSE_LOCATION, safeText(course.getLocation()));
+ intent.putExtra(EXTRA_COURSE_TEACHER, safeText(course.getTeacher()));
+ intent.putExtra(EXTRA_DAY_OF_WEEK, course.getDayOfWeek());
+ intent.putExtra(EXTRA_TIME_SLOT, course.getTimeSlot());
+ intent.putExtra(EXTRA_DISPLAY_TIME, ReminderScheduler.formatDisplayTime(course));
+
+ // 发送本地广播
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+ android.util.Log.d("ReminderReceiver", "已发送应用内横幅广播: " + course.getName());
+
+ // 如果应用不在前台,尝试启动 MainActivity 来显示横幅
+ if (!isAppInForeground) {
+ try {
+ Intent mainIntent = new Intent(context, MainActivity.class);
+ mainIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ mainIntent.putExtra(ACTION_INAPP_REMINDER, true);
+ mainIntent.putExtra(EXTRA_COURSE_NAME, safeText(course.getName()));
+ mainIntent.putExtra(EXTRA_COURSE_LOCATION, safeText(course.getLocation()));
+ mainIntent.putExtra(EXTRA_DISPLAY_TIME, ReminderScheduler.formatDisplayTime(course));
+ context.startActivity(mainIntent);
+ android.util.Log.d("ReminderReceiver", "应用在后台,尝试启动 MainActivity");
+ } catch (Exception e) {
+ android.util.Log.e("ReminderReceiver", "启动 MainActivity 失败", e);
+ }
+ }
+ } catch (Exception e) {
+ android.util.Log.e("ReminderReceiver", "发送应用内横幅广播失败", e);
+ }
+ }
+
+ private boolean isAppInForeground(Context context) {
+ try {
+ android.app.ActivityManager activityManager = (android.app.ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ if (activityManager == null) {
+ return false;
+ }
+ java.util.List appProcesses = activityManager.getRunningAppProcesses();
+ if (appProcesses == null) {
+ return false;
+ }
+ String packageName = context.getPackageName();
+ for (android.app.ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
+ if (appProcess.importance == android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
+ && appProcess.processName.equals(packageName)) {
+ return true;
+ }
+ }
+ } catch (Exception e) {
+ android.util.Log.e("ReminderReceiver", "检查应用前台状态失败", e);
+ }
+ return false;
+ }
+}
+
+
diff --git a/src/app/src/main/java/com/example/myapplication/ReminderScheduler.java b/src/app/src/main/java/com/example/myapplication/ReminderScheduler.java
new file mode 100644
index 0000000..b86cb87
--- /dev/null
+++ b/src/app/src/main/java/com/example/myapplication/ReminderScheduler.java
@@ -0,0 +1,253 @@
+package com.example.myapplication;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.text.TextUtils;
+
+import java.util.Calendar;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * 统一管理课程提醒的调度与取消。
+ */
+public class ReminderScheduler {
+
+ private static final int[][] TIME_SLOT_START_TIMES = new int[][]{
+ {8, 0}, // 第1-2节 08:00
+ {10, 5}, // 第3-4节 10:05
+ {13, 30}, // 第5-6节 13:30
+ {15, 15}, // 第7-8节 15:15
+ {18, 30}, // 第9-10节 18:30
+ {20, 35} // 第11-12节 20:35
+ };
+
+ private ReminderScheduler() {
+ // no instance
+ }
+
+ public static void setupAllReminders(Context context, List courses) {
+ if (context == null || courses == null) {
+ return;
+ }
+ for (Course course : courses) {
+ updateReminder(context, course);
+ }
+ }
+
+ public static void updateReminder(Context context, Course course) {
+ if (context == null || course == null || TextUtils.isEmpty(course.getId())) {
+ return;
+ }
+ cancelReminder(context, course);
+ if (course.isReminderEnabled()) {
+ scheduleReminder(context, course);
+ }
+ }
+
+ public static void scheduleReminder(Context context, Course course) {
+ if (context == null || course == null || !course.isReminderEnabled()) {
+ android.util.Log.w("ReminderScheduler", "scheduleReminder: 参数无效或提醒未启用");
+ return;
+ }
+ if (course.getReminderAdvanceSeconds() <= 0) {
+ android.util.Log.w("ReminderScheduler", "scheduleReminder: 提醒秒数为0");
+ return;
+ }
+
+ long triggerAtMillis;
+ long now = System.currentTimeMillis();
+
+ if (course.getReminderAdvanceSeconds() == 5) {
+ // 测试提醒:保存后5秒触发(仅用于测试)
+ // 对于5秒测试提醒,直接使用当前时间+5秒,不受最小延迟限制
+ triggerAtMillis = now + 5000L;
+ android.util.Log.d("ReminderScheduler", "5秒测试提醒: 当前时间=" + now +
+ ", 测试触发时间=" + triggerAtMillis +
+ ", 延迟约=" + ((triggerAtMillis - now) / 1000) + "秒");
+ } else {
+ triggerAtMillis = calculateNextTriggerTime(course);
+ android.util.Log.d("ReminderScheduler", "正常提醒: 触发时间=" + triggerAtMillis);
+ }
+ if (triggerAtMillis <= 0) {
+ android.util.Log.e("ReminderScheduler", "计算出的触发时间无效: " + triggerAtMillis);
+ return;
+ }
+
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ if (alarmManager == null) {
+ android.util.Log.e("ReminderScheduler", "无法获取 AlarmManager");
+ return;
+ }
+
+ PendingIntent pendingIntent = buildPendingIntent(context, course);
+ if (pendingIntent == null) {
+ android.util.Log.e("ReminderScheduler", "无法创建 PendingIntent");
+ return;
+ }
+
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
+ } else {
+ alarmManager.set(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
+ }
+ long delaySeconds = (triggerAtMillis - now) / 1000;
+ android.util.Log.d("ReminderScheduler", "提醒已调度: 课程=" + course.getName() +
+ ", 触发时间=" + triggerAtMillis +
+ ", 延迟=" + delaySeconds + "秒");
+ } catch (SecurityException e) {
+ android.util.Log.e("ReminderScheduler", "调度提醒失败: 权限不足", e);
+ } catch (Exception e) {
+ android.util.Log.e("ReminderScheduler", "调度提醒失败", e);
+ }
+ }
+
+ public static void cancelReminder(Context context, Course course) {
+ if (context == null || course == null || TextUtils.isEmpty(course.getId())) {
+ return;
+ }
+
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ if (alarmManager == null) {
+ return;
+ }
+
+ PendingIntent pendingIntent = buildPendingIntent(context, course, PendingIntent.FLAG_NO_CREATE);
+ if (pendingIntent != null) {
+ alarmManager.cancel(pendingIntent);
+ pendingIntent.cancel();
+ }
+ }
+
+ private static PendingIntent buildPendingIntent(Context context, Course course) {
+ return buildPendingIntent(context, course, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ private static PendingIntent buildPendingIntent(Context context, Course course, int flag) {
+ Intent intent = new Intent(context, ReminderReceiver.class);
+ intent.setAction(ReminderReceiver.ACTION_SHOW_REMINDER);
+ intent.putExtra(ReminderReceiver.EXTRA_COURSE_ID, course.getId());
+ intent.putExtra(ReminderReceiver.EXTRA_COURSE_NAME, course.getName());
+ intent.putExtra(ReminderReceiver.EXTRA_COURSE_LOCATION, course.getLocation());
+ intent.putExtra(ReminderReceiver.EXTRA_COURSE_TEACHER, course.getTeacher());
+ intent.putExtra(ReminderReceiver.EXTRA_REMINDER_SECONDS, course.getReminderAdvanceSeconds());
+ intent.putExtra(ReminderReceiver.EXTRA_REMINDER_MINUTES, Math.max(0, course.getReminderAdvanceSeconds() / 60));
+ intent.putExtra(ReminderReceiver.EXTRA_TIME_SLOT, course.getTimeSlot());
+ intent.putExtra(ReminderReceiver.EXTRA_DAY_OF_WEEK, course.getDayOfWeek());
+
+ int requestCode = Objects.hash(course.getId());
+ int flags = flag;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ flags |= PendingIntent.FLAG_IMMUTABLE;
+ }
+ return PendingIntent.getBroadcast(context, requestCode, intent, flags);
+ }
+
+ private static long calculateNextTriggerTime(Course course) {
+ if (course.getDayOfWeek() < 1 || course.getDayOfWeek() > 7) {
+ return -1;
+ }
+ int delaySeconds = course.getReminderAdvanceSeconds();
+ if (delaySeconds <= 0) {
+ return -1;
+ }
+
+ Calendar now = Calendar.getInstance();
+ Calendar courseStartTime = Calendar.getInstance();
+
+ int currentDow = convertToCourseDow(now.get(Calendar.DAY_OF_WEEK));
+ int targetDow = course.getDayOfWeek();
+
+ // 计算到目标星期几的天数差
+ int daysDiff = targetDow - currentDow;
+ if (daysDiff < 0) {
+ daysDiff += 7;
+ }
+
+ // 设置课程开始时间(本周的课程时间)
+ courseStartTime.add(Calendar.DAY_OF_YEAR, daysDiff);
+ int[] time = extractStartTime(course.getTimeSlot());
+ courseStartTime.set(Calendar.HOUR_OF_DAY, time[0]);
+ courseStartTime.set(Calendar.MINUTE, time[1]);
+ courseStartTime.set(Calendar.SECOND, 0);
+ courseStartTime.set(Calendar.MILLISECOND, 0);
+
+ // 如果本周的课程开始时间已过,推迟到下一周
+ if (courseStartTime.getTimeInMillis() <= now.getTimeInMillis()) {
+ courseStartTime.add(Calendar.DAY_OF_YEAR, 7);
+ }
+
+ // 计算提醒时间:课程开始时间 + 延迟时间
+ Calendar reminderTime = (Calendar) courseStartTime.clone();
+ // 提前提醒:在课程开始前 delaySeconds 触发
+ if (delaySeconds != 0) {
+ reminderTime.add(Calendar.SECOND, -delaySeconds);
+ }
+
+ // 如果提醒时间已过或距离当前时间太近,推迟到下一周
+ long nowMillis = now.getTimeInMillis();
+ long minDelay = 60000; // 最小延迟60秒(1分钟)
+ while (reminderTime.getTimeInMillis() - nowMillis < minDelay) {
+ reminderTime.add(Calendar.DAY_OF_YEAR, 7);
+ }
+
+ return reminderTime.getTimeInMillis();
+ }
+
+ public static void scheduleReminderInMinutes(Context context, Course course, int minutes) {
+ if (context == null || course == null || minutes <= 0 || !course.isReminderEnabled()) {
+ return;
+ }
+
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ if (alarmManager == null) {
+ return;
+ }
+
+ PendingIntent pendingIntent = buildPendingIntent(context, course);
+ if (pendingIntent == null) {
+ return;
+ }
+
+ long triggerAt = System.currentTimeMillis() + minutes * 60L * 1000L;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent);
+ } else {
+ alarmManager.set(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent);
+ }
+ }
+
+ private static int[] extractStartTime(int timeSlot) {
+ if (timeSlot >= 0 && timeSlot < TIME_SLOT_START_TIMES.length) {
+ return TIME_SLOT_START_TIMES[timeSlot];
+ }
+ // 默认08:00
+ return new int[]{8, 0};
+ }
+
+ private static int convertToCourseDow(int calendarDow) {
+ if (calendarDow == Calendar.SUNDAY) {
+ return 7;
+ }
+ return calendarDow - 1;
+ }
+
+ public static String formatDisplayTime(Course course) {
+ int[] start = extractStartTime(course.getTimeSlot());
+ Calendar calendar = Calendar.getInstance();
+ calendar.set(Calendar.HOUR_OF_DAY, start[0]);
+ calendar.set(Calendar.MINUTE, start[1]);
+ return String.format(Locale.getDefault(), "%02d:%02d", start[0], start[1]);
+ }
+}
+
+
diff --git a/src/app/src/main/java/com/example/myapplication/SimpleMapView.java b/src/app/src/main/java/com/example/myapplication/SimpleMapView.java
new file mode 100644
index 0000000..707771b
--- /dev/null
+++ b/src/app/src/main/java/com/example/myapplication/SimpleMapView.java
@@ -0,0 +1,265 @@
+package com.example.myapplication;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 简单地图视图组件
+ * 在高德地图完全集成前提供基础的地图可视化功能
+ */
+public class SimpleMapView extends View {
+
+ private Paint paintBackground;
+ private Paint paintGrid;
+ private Paint paintCurrentLocation;
+ private Paint paintDestination;
+ private Paint paintPath;
+ private Paint paintText;
+
+ private List locations;
+ private LocationPoint currentLocation;
+ private LocationPoint destination;
+
+ private float centerX = 400f;
+ private float centerY = 400f;
+ private float scale = 100000f; // 地图缩放比例
+
+ public interface OnLocationClickListener {
+ void onLocationClicked(LocationPoint point);
+ }
+
+ private OnLocationClickListener onLocationClickListener;
+
+ public SimpleMapView(Context context) {
+ super(context);
+ init();
+ }
+
+ public SimpleMapView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ private void init() {
+ initPaints();
+ initData();
+ }
+
+ private void initPaints() {
+ paintBackground = new Paint();
+ paintBackground.setColor(getResources().getColor(R.color.accent_green_bg, null));
+ paintBackground.setStyle(Paint.Style.FILL);
+
+ paintGrid = new Paint();
+ paintGrid.setColor(getResources().getColor(R.color.divider, null));
+ paintGrid.setStrokeWidth(1);
+ paintGrid.setAlpha(100);
+
+ paintCurrentLocation = new Paint();
+ paintCurrentLocation.setColor(getResources().getColor(R.color.primary, null));
+ paintCurrentLocation.setStyle(Paint.Style.FILL);
+
+ paintDestination = new Paint();
+ paintDestination.setColor(getResources().getColor(R.color.error, null));
+ paintDestination.setStyle(Paint.Style.FILL);
+
+ paintPath = new Paint();
+ paintPath.setColor(getResources().getColor(R.color.accent_orange_dark, null));
+ paintPath.setStrokeWidth(6);
+ paintPath.setStyle(Paint.Style.STROKE);
+
+ paintText = new Paint();
+ paintText.setColor(Color.BLACK);
+ paintText.setTextSize(24);
+ paintText.setAntiAlias(true);
+ }
+
+ private void initData() {
+ locations = new ArrayList<>();
+
+ // 添加一些示例位置点(使用相对坐标)
+ locations.add(new LocationPoint("图书馆", 0, 100, "📚"));
+ locations.add(new LocationPoint("第一食堂", 150, 50, "🍽️"));
+ locations.add(new LocationPoint("第二食堂", -100, 80, "🍽️"));
+ locations.add(new LocationPoint("教学楼A", 80, -50, "🏫"));
+ locations.add(new LocationPoint("教学楼B", 120, -80, "🏫"));
+ locations.add(new LocationPoint("实验楼", -80, -100, "🔬"));
+ locations.add(new LocationPoint("体育馆", -150, 20, "🏃"));
+ locations.add(new LocationPoint("宿舍区", 50, 150, "🏠"));
+
+ // 设置默认当前位置
+ currentLocation = new LocationPoint("校门口", 0, 0, "📍");
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ int width = getWidth();
+ int height = getHeight();
+
+ // 绘制背景
+ canvas.drawRect(0, 0, width, height, paintBackground);
+
+ // 绘制网格
+ drawGrid(canvas, width, height);
+
+ // 绘制路径
+ if (currentLocation != null && destination != null) {
+ drawPath(canvas);
+ }
+
+ // 绘制位置点
+ for (LocationPoint point : locations) {
+ drawLocationPoint(canvas, point, paintCurrentLocation);
+ }
+
+ // 绘制当前位置
+ if (currentLocation != null) {
+ drawLocationPoint(canvas, currentLocation, paintCurrentLocation);
+ }
+
+ // 绘制目标位置
+ if (destination != null) {
+ drawLocationPoint(canvas, destination, paintDestination);
+ }
+
+ // 绘制图例
+ drawLegend(canvas, width, height);
+ }
+
+ private void drawGrid(Canvas canvas, int width, int height) {
+ int gridSize = 50;
+
+ // 垂直线
+ for (int x = 0; x < width; x += gridSize) {
+ canvas.drawLine(x, 0, x, height, paintGrid);
+ }
+
+ // 水平线
+ for (int y = 0; y < height; y += gridSize) {
+ canvas.drawLine(0, y, width, y, paintGrid);
+ }
+ }
+
+ private void drawLocationPoint(Canvas canvas, LocationPoint point, Paint paint) {
+ float screenX = centerX + point.x;
+ float screenY = centerY - point.y; // Y轴翻转
+
+ // 绘制圆点
+ canvas.drawCircle(screenX, screenY, 15, paint);
+
+ // 绘制标签
+ String label = point.icon + " " + point.name;
+ canvas.drawText(label, screenX - 50, screenY - 25, paintText);
+ }
+
+ private void drawPath(Canvas canvas) {
+ if (currentLocation == null || destination == null) return;
+
+ float startX = centerX + currentLocation.x;
+ float startY = centerY - currentLocation.y;
+ float endX = centerX + destination.x;
+ float endY = centerY - destination.y;
+
+ canvas.drawLine(startX, startY, endX, endY, paintPath);
+ }
+
+ private void drawLegend(Canvas canvas, int width, int height) {
+ int legendX = 20;
+ int legendY = height - 120;
+
+ // 绘制图例背景
+ RectF legendRect = new RectF(legendX - 10, legendY - 10, legendX + 200, legendY + 80);
+ Paint legendBg = new Paint();
+ legendBg.setColor(Color.WHITE);
+ legendBg.setAlpha(200);
+ canvas.drawRect(legendRect, legendBg);
+
+ // 绘制图例内容
+ canvas.drawText("📍 当前位置", legendX, legendY, paintText);
+ canvas.drawText("🎯 目标位置", legendX, legendY + 25, paintText);
+ canvas.drawText("📚🍽️🏫 校园位置", legendX, legendY + 50, paintText);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ float touchX = event.getX();
+ float touchY = event.getY();
+
+ // 检查是否点击了某个位置点
+ LocationPoint clickedPoint = findClickedLocation(touchX, touchY);
+ if (clickedPoint != null && onLocationClickListener != null) {
+ onLocationClickListener.onLocationClicked(clickedPoint);
+ return true;
+ }
+ }
+ return super.onTouchEvent(event);
+ }
+
+ private LocationPoint findClickedLocation(float touchX, float touchY) {
+ for (LocationPoint point : locations) {
+ float screenX = centerX + point.x;
+ float screenY = centerY - point.y;
+
+ float distance = (float) Math.sqrt(
+ Math.pow(touchX - screenX, 2) + Math.pow(touchY - screenY, 2)
+ );
+
+ if (distance <= 30) { // 点击范围
+ return point;
+ }
+ }
+ return null;
+ }
+
+ public void setCurrentLocation(String name, float x, float y) {
+ currentLocation = new LocationPoint(name, x, y, "📍");
+ invalidate();
+ }
+
+ public void setDestination(String name, float x, float y) {
+ destination = new LocationPoint(name, x, y, "🎯");
+ invalidate();
+ }
+
+ public void setOnLocationClickListener(OnLocationClickListener listener) {
+ this.onLocationClickListener = listener;
+ }
+
+ public void addLocation(String name, float x, float y, String icon) {
+ locations.add(new LocationPoint(name, x, y, icon));
+ invalidate();
+ }
+
+ public void clearDestination() {
+ destination = null;
+ invalidate();
+ }
+
+ /**
+ * 位置点数据类
+ */
+ public static class LocationPoint {
+ public String name;
+ public float x, y;
+ public String icon;
+
+ public LocationPoint(String name, float x, float y, String icon) {
+ this.name = name;
+ this.x = x;
+ this.y = y;
+ this.icon = icon;
+ }
+ }
+}
diff --git a/src/app/src/main/java/com/example/myapplication/TimetableFragment.java b/src/app/src/main/java/com/example/myapplication/TimetableFragment.java
new file mode 100644
index 0000000..5fafbde
--- /dev/null
+++ b/src/app/src/main/java/com/example/myapplication/TimetableFragment.java
@@ -0,0 +1,2545 @@
+package com.example.myapplication;
+
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.app.AlarmManager;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.Settings;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.*;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class TimetableFragment extends Fragment {
+
+ private static final String[] DAYS = {"", "周一", "周二", "周三", "周四", "周五", "周六", "周日"};
+
+ private RelativeLayout timetableContainer;
+ private LinearLayout weekDaysContainer;
+ private TextView tvTimeHeader;
+ private FloatingActionButton fabAddCourse;
+ private Button btnPrevWeek, btnNextWeek;
+ private Button btnImportCourses;
+ private Button btnRefresh;
+ private Button btnClearAll;
+ private TextView tvCurrentWeek;
+
+ private DataManager dataManager;
+ private List courses;
+ private int currentWeek = 1;
+ private BroadcastReceiver refreshReceiver;
+
+ // 课程时间表 [节次][星期]
+ private static final int MAX_PERIODS = 12;
+ private static final int MAX_DAYS = 7; // 显示周一到周日,支持所有7天的课程显示
+ private Course[][] timetableData = new Course[MAX_PERIODS][MAX_DAYS];
+
+ // 时间段定义(根据中国民航大学实际时间安排)
+ private static final String[] TIME_SLOTS = {
+ "第1-2节 08:00-09:35", // 上午1-2节
+ "第3-4节 10:05-11:40", // 上午3-4节
+ "第5-6节 13:30-15:05", // 下午5-6节
+ "第7-8节 15:15-17:10", // 下午7-8节
+ "第9-10节 18:30-20:05", // 晚上9-10节
+ "第11-12节 20:35-22:05" // 晚上11-12节
+ };
+ private static final int TIME_SLOTS_COUNT = TIME_SLOTS.length;
+
+ // 尺寸(dp)
+ private static final int TIME_COLUMN_WIDTH_DP = 56;
+ private static final int DAY_COLUMN_WIDTH_DP = 56;
+ private static final int ROW_HEIGHT_DP = 64;
+ private static final int GRID_LINE_DP = 1;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_timetable, container, false);
+
+ initViews(view);
+ setupData();
+ setupListeners();
+ buildTimetable();
+ setupBroadcastReceiver();
+
+ return view;
+ }
+
+ private void initViews(View view) {
+ timetableContainer = view.findViewById(R.id.timetable_container);
+ weekDaysContainer = view.findViewById(R.id.week_days_container);
+ tvTimeHeader = view.findViewById(R.id.tv_time_header);
+ fabAddCourse = view.findViewById(R.id.fab_add_course);
+ btnPrevWeek = view.findViewById(R.id.btn_prev_week);
+ btnNextWeek = view.findViewById(R.id.btn_next_week);
+ btnImportCourses = view.findViewById(R.id.btn_import_courses);
+ btnRefresh = view.findViewById(R.id.btn_refresh);
+ btnClearAll = view.findViewById(R.id.btn_clear_all);
+ tvCurrentWeek = view.findViewById(R.id.tv_current_week);
+ }
+
+ private void setupData() {
+ // 重新创建DataManager,确保使用当前用户的数据
+ dataManager = new DataManager(getContext());
+
+ // 获取当前用户信息
+ UserManager userManager = UserManager.getInstance(getContext());
+ String currentUserId = userManager.getCurrentUserId();
+ System.out.println("TimetableFragment.setupData: 当前用户ID: " + currentUserId);
+
+ // 加载当前用户的课程(不再自动清空,保留已导入的课程)
+ courses = dataManager.getCourses();
+
+ // 清理所有不是从教务系统导入的课程(手动添加的课程保留,但检查是否有硬编码的测试数据)
+ cleanupNonImportedCourses();
+
+ System.out.println("==========================================");
+ System.out.println("TimetableFragment.setupData: 当前用户课程总数: " + courses.size());
+ System.out.println("==========================================");
+
+ // 按星期分组显示课程
+ printCoursesByDay();
+
+ // 详细输出所有课程信息
+ if (courses.isEmpty()) {
+ System.out.println("当前没有课程数据");
+ } else {
+ for (int i = 0; i < courses.size(); i++) {
+ Course c = courses.get(i);
+ System.out.println("课程 [" + (i + 1) + "]:");
+ System.out.println(" 名称: " + c.getName());
+ System.out.println(" 教师: " + c.getTeacher());
+ System.out.println(" 地点: " + c.getLocation());
+ System.out.println(" 星期: " + c.getDayOfWeek() + " (1=周一, 2=周二, 3=周三, 4=周四, 5=周五, 6=周六, 7=周日)");
+ System.out.println(" 时间段: " + c.getTimeSlot() + " (0=1-2节, 1=3-4节, 2=5-6节, 3=7-8节, 4=9-10节, 5=11-12节)");
+ System.out.println(" 节次: " + c.getStartPeriod() + "-" + c.getEndPeriod() + "节");
+ System.out.println(" 周次: " + c.getStartWeek() + "-" + c.getEndWeek() + "周");
+ System.out.println(" 是否导入: " + c.isImported());
+ System.out.println(" ---");
+ }
+ }
+ System.out.println("==========================================");
+
+ updateCurrentWeekByTermStart();
+ updateWeekDisplay();
+ loadCoursesToTimetable();
+ }
+
+ /**
+ * 根据用户提供的课表结构导入所有课程
+ * 【已禁用】此方法包含硬编码测试数据,不应使用
+ */
+ @Deprecated
+ private void importAllCoursesFromStructure() {
+ // 已禁用:此方法包含硬编码测试数据,不应使用
+ return;
+ /*
+ System.out.println("开始根据课表结构导入所有课程...");
+
+ // ========== 上午 1-2节(时间段0)==========
+ createAndAddCourse("编译原理(Ⅰ)", "张志远", "东丽校区(北) 北教25-110", 1, 0, 1, 8, 1, 2);
+ createAndAddCourse("接口技术及应用", "谈娴茹", "东丽校区(北) 北教25-109", 2, 0, 1, 8, 1, 2);
+ createAndAddCourse("云计算导论", "鲁亮", "东丽校区(北) 北教25-210", 3, 0, 1, 16, 1, 2);
+ createAndAddCourse("操作系统", "杨志娴", "东丽校区(北) 北教25-201", 4, 0, 1, 12, 1, 2);
+ createAndAddCourse("形势与政策(5)", "孙树贵", "东丽校区(北) 北教4-405", 5, 0, 9, 10, 1, 2);
+ createAndAddCourse("【调】互联网应用服务开发与安全", "刘亮", "东丽校区(北) 北教23-303", 6, 0, 8, 8, 1, 4); // 周六,1-4节
+
+ // ========== 上午 3-4节(时间段1)==========
+ createAndAddCourse("毛泽东思想和中国特色社会主义理论体系概论(B)", "阎维洁", "东丽校区(北) 北教25-111", 1, 1, 1, 16, 3, 4);
+ createAndAddCourse("习近平新时代中国特色社会主义思想概论(B)", "田蕊", "东丽校区(北) 北教25-110", 2, 1, 1, 15, 3, 4);
+ createAndAddCourse("编译原理课程设计", "张志远", "东丽校区(北) 北教25-518", 3, 1, 9, 12, 3, 4); // 周四3-4节
+ createAndAddCourse("大数据采集与预处理", "高思华", "东丽校区(北) 北教25-210", 5, 1, 1, 16, 3, 4); // 周五3-4节(只在周五,不在周四)
+
+ // ========== 下午 5-6节(时间段2)==========
+ createAndAddCourse("操作系统", "杨志娴", "东丽校区(北) 北教25-201", 1, 2, 1, 12, 5, 6);
+ createAndAddCourse("编译原理(Ⅰ)", "张志远", "东丽校区(北) 北教25-109", 2, 2, 1, 8, 5, 6);
+ createAndAddCourse("毛泽东思想和中国特色社会主义理论体系概论(B)", "阎维洁", "东丽校区(北) 北教4-101", 3, 2, 12, 14, 5, 6);
+ createAndAddCourse("接口技术及应用", "谈娴茹", "东丽校区(北) 北教25-109", 4, 2, 1, 8, 5, 6);
+
+ // ========== 下午 7-8节(时间段3)==========
+ createAndAddCourse("编译原理课程设计", "张志远", "东丽校区(北) 北教25-518", 1, 3, 9, 12, 7, 8); // 周一7-8节
+ createAndAddCourse("党史", "王广峰", "东丽校区(南) 南教1-102", 2, 3, 6, 9, 7, 8); // 周二7-8节
+ createAndAddCourse("软件工程课程设计", "衡红军", "东丽校区(北) 北教25-412", 3, 3, 1, 10, 7, 8); // 周三7-8节(修正:应该是周三,不是周四)
+ createAndAddCourse("接口技术及应用实验", "谈娴茹", "东丽校区(北) 北教25-333", 4, 3, 6, 13, 7, 8); // 周四7-8节
+
+ // ========== 晚上 9-12节(时间段4)==========
+ createAndAddCourse("计算机网络课程设计(Ⅰ)", "刘春波", "东丽校区(北) 北教25-518", 1, 4, 1, 4, 9, 12);
+ createAndAddCourse("操作系统课程设计(Ⅰ)", "杨志娴", "东丽校区(北) 北教25-414", 2, 4, 9, 12, 9, 12);
+ createAndAddCourse("网络安全技术(Ⅰ)", "张礼哲", "东丽校区(北) 北教4-101", 3, 4, 1, 10, 9, 10);
+ createAndAddCourse("【调】互联网应用服务开发与安全", "刘亮", "东丽校区(北) 北教23-303", 3, 4, 1, 8, 9, 12); // 周三,和网络安全技术同一时间段
+ createAndAddCourse("新中国史", "白月薇", "东丽校区(南) 南教1-103", 4, 4, 6, 9, 9, 10);
+
+ System.out.println("完成导入所有课程");
+ */
+ }
+
+ /**
+ * 创建并添加课程
+ */
+ private void createAndAddCourse(String name, String teacher, String location,
+ int dayOfWeek, int timeSlot,
+ int startWeek, int endWeek,
+ int startPeriod, int endPeriod) {
+ Course course = new Course(name, teacher, location, dayOfWeek, timeSlot);
+ course.setStartWeek(startWeek);
+ course.setEndWeek(endWeek);
+ course.setStartPeriod(startPeriod);
+ course.setEndPeriod(endPeriod);
+ course.setSemester("2025-2026-1");
+ course.setImported(true);
+ dataManager.addCourse(course);
+ System.out.println("导入: " + name + " -> 星期" + dayOfWeek + ", 时间段" + timeSlot +
+ "(" + startPeriod + "-" + endPeriod + "节), " + startWeek + "-" + endWeek + "周");
+ }
+
+ /**
+ * 清除所有课程数据
+ */
+ private void clearAllCourses() {
+ System.out.println("========================================");
+ System.out.println("TimetableFragment.clearAllCourses: 开始清空所有课程数据");
+ System.out.println("========================================");
+
+ dataManager.clearAllCourses();
+ // 清空课程列表
+ courses.clear();
+ // 清空时间表显示(将二维数组所有元素设为null)
+ for (int i = 0; i < MAX_PERIODS; i++) {
+ for (int j = 0; j < MAX_DAYS; j++) {
+ timetableData[i][j] = null;
+ }
+ }
+ // 重新加载课程到时间表(会更新界面显示)
+ loadCoursesToTimetable();
+ // 重建课表界面
+ buildTimetable();
+
+ System.out.println("========================================");
+ System.out.println("TimetableFragment.clearAllCourses: 所有课程数据已清除,界面已更新");
+ System.out.println("========================================");
+ }
+
+ /**
+ * 清理所有错误位置的课程
+ */
+ private void cleanupIncorrectCourses() {
+ List allCourses = dataManager.getCourses();
+ List toRemove = new ArrayList<>();
+
+ for (Course course : allCourses) {
+ String courseName = course.getName();
+ int dayOfWeek = course.getDayOfWeek();
+ int timeSlot = course.getTimeSlot();
+
+ // 1. 删除周四3-4节的大数据课程(应该在周五)
+ if (dayOfWeek == 4 &&
+ timeSlot == 1 &&
+ (courseName.contains("大数据") || courseName.contains("采集"))) {
+ toRemove.add(course);
+ System.out.println("删除:周四3-4节的大数据课程(应该在周五)");
+ }
+
+ // 2. 删除周一7-8节的党史课程(应该在周二)
+ if (dayOfWeek == 1 &&
+ timeSlot == 3 &&
+ courseName.contains("党史")) {
+ toRemove.add(course);
+ System.out.println("删除:周一7-8节的党史课程(应该在周二)");
+ }
+
+ // 3. 删除周三7-8节的接口技术实验课程(应该只在周四)
+ if (dayOfWeek == 3 &&
+ timeSlot == 3 &&
+ courseName.contains("接口技术") && courseName.contains("实验")) {
+ toRemove.add(course);
+ System.out.println("删除:周三7-8节的接口技术实验课程(应该只在周四)");
+ }
+
+ // 4. 毛泽东思想和中国特色社会主义理论体系概论(B) - 应该在周一3-4节(时间段1)
+ // 删除任何不在周一或不在时间段1的该课程
+ if (courseName.contains("毛泽东思想和中国特色社会主义理论体系概论") &&
+ !courseName.contains("新时代中国特色社会主义")) {
+ if (dayOfWeek != 1 || timeSlot != 1) {
+ // 例外:周三下午5-6节(时间段2)的该课程是正确的(12-14周)
+ if (!(dayOfWeek == 3 && timeSlot == 2)) {
+ toRemove.add(course);
+ System.out.println("删除:错误位置的毛泽东思想和中国特色社会主义理论体系概论(B)课程(应该在周一3-4节或周三5-6节)");
+ }
+ }
+ }
+
+ // 5. 习近平新时代中国特色社会主义思想概论(B) - 应该在周二3-4节(时间段1)
+ // 删除任何不在周二或不在时间段1的该课程
+ if (courseName.contains("习近平新时代中国特色社会主义思想") ||
+ courseName.contains("新时代中国特色社会主义")) {
+ if (dayOfWeek != 2 || timeSlot != 1) {
+ toRemove.add(course);
+ System.out.println("删除:错误位置的习近平新时代中国特色社会主义思想概论(B)课程(应该在周二3-4节)");
+ }
+ }
+ }
+
+ // 删除错误位置的课程
+ Context context = getContext();
+ for (Course course : toRemove) {
+ if (context != null) {
+ ReminderScheduler.cancelReminder(context, course);
+ }
+ dataManager.deleteCourse(course.getId());
+ }
+
+ if (!toRemove.isEmpty()) {
+ System.out.println("清理了 " + toRemove.size() + " 门错误位置的课程");
+ }
+ }
+
+ /**
+ * 清理所有不是从教务系统导入的硬编码测试课程
+ * 删除所有写死的测试数据,只保留从教务系统导入的课程
+ */
+ private void cleanupNonImportedCourses() {
+ List allCourses = dataManager.getCourses();
+ List toRemove = new ArrayList<>();
+
+ System.out.println("========================================");
+ System.out.println("开始清理硬编码的测试课程");
+ System.out.println("========================================");
+
+ // 检查每个课程,如果是已知的硬编码测试课程,删除它
+ // 特别处理:毛泽东思想课程应该只有两个实例:周一3-4节(1-16周)和周三5-6节(12-14周)
+ Map maoZeDongCourseCount = new HashMap<>();
+ maoZeDongCourseCount.put("周一3-4节-1-16周", 0);
+ maoZeDongCourseCount.put("周三5-6节-12-14周", 0);
+
+ for (Course course : allCourses) {
+ String courseName = course.getName();
+ int dayOfWeek = course.getDayOfWeek();
+ int timeSlot = course.getTimeSlot();
+ int startWeek = course.getStartWeek();
+ int endWeek = course.getEndWeek();
+
+ // 特别处理毛泽东思想课程:检查是否有重复或错误的位置
+ if (courseName != null && courseName.contains("毛泽东") && !courseName.contains("新时代")) {
+ String key = "星期" + dayOfWeek + "-时间段" + timeSlot + "-" + startWeek + "-" + endWeek + "周";
+ System.out.println("检查毛泽东思想课程: " + key);
+
+ // 正确的课程应该是:周一3-4节(1-16周)或周三5-6节(12-14周)
+ boolean isCorrect = false;
+ if (dayOfWeek == 1 && timeSlot == 1 && startWeek == 1 && endWeek == 16) {
+ isCorrect = true;
+ maoZeDongCourseCount.put("周一3-4节-1-16周", maoZeDongCourseCount.get("周一3-4节-1-16周") + 1);
+ } else if (dayOfWeek == 3 && timeSlot == 2 && startWeek == 12 && endWeek == 14) {
+ isCorrect = true;
+ maoZeDongCourseCount.put("周三5-6节-12-14周", maoZeDongCourseCount.get("周三5-6节-12-14周") + 1);
+ }
+
+ if (!isCorrect) {
+ toRemove.add(course);
+ System.out.println("删除硬编码:毛泽东思想课程位置或周次错误 - " +
+ "星期" + dayOfWeek + ", 时间段" + timeSlot + ", " +
+ startWeek + "-" + endWeek + "周 (应该是: 周一3-4节1-16周或周三5-6节12-14周)");
+ }
+ }
+
+ // 检查是否是接口技术及应用实验,如果位置不对则删除
+ // 接口技术及应用实验应该只在周四7-8节,6-13周
+ // 特别注意:如果发现在周三7-8节,必须删除(这是写死的错误数据)
+ if (courseName.contains("接口技术") && courseName.contains("实验")) {
+ // 如果是在周三7-8节,直接删除(这是写死的错误数据)
+ if (dayOfWeek == 3 && timeSlot == 3) {
+ toRemove.add(course);
+ System.out.println("删除硬编码:接口技术及应用实验错误地位于周三7-8节(应该是周四7-8节)");
+ }
+ // 如果位置或周次不对,也删除
+ else if (dayOfWeek != 4 || timeSlot != 3 || startWeek != 6 || endWeek != 13) {
+ toRemove.add(course);
+ System.out.println("删除硬编码:接口技术及应用实验位置错误或周次错误 - " +
+ "星期" + dayOfWeek + ", 时间段" + timeSlot + ", " +
+ startWeek + "-" + endWeek + "周 (应该是: 周四7-8节, 6-13周)");
+ }
+ }
+
+ // 检查软件工程课程设计,如果位置不对则删除
+ // 软件工程课程设计应该只在周三7-8节,1-10周
+ if (courseName.contains("软件工程") && courseName.contains("课程设计")) {
+ if (dayOfWeek != 3 || timeSlot != 3 || startWeek != 1 || endWeek != 10) {
+ toRemove.add(course);
+ System.out.println("删除硬编码:软件工程课程设计位置错误或周次错误 - " +
+ "星期" + dayOfWeek + ", 时间段" + timeSlot + ", " +
+ startWeek + "-" + endWeek + "周 (应该是: 周三7-8节, 1-10周)");
+ }
+ }
+
+ // 检查编译原理课程设计,如果位置不对则删除
+ // 编译原理课程设计应该在:周四3-4节(9-12周) 或 周一7-8节(9-12周)
+ if (courseName.contains("编译原理") && courseName.contains("课程设计")) {
+ boolean isCorrect = false;
+ // 周四3-4节,9-12周(优先)
+ if (dayOfWeek == 4 && timeSlot == 1 && startWeek == 9 && endWeek == 12) {
+ isCorrect = true;
+ }
+ // 周一7-8节,9-12周(备选)
+ else if (dayOfWeek == 1 && timeSlot == 3 && startWeek == 9 && endWeek == 12) {
+ isCorrect = true;
+ }
+ if (!isCorrect) {
+ toRemove.add(course);
+ System.out.println("删除硬编码:编译原理课程设计位置错误或周次错误 - " +
+ "星期" + dayOfWeek + ", 时间段" + timeSlot + ", " +
+ startWeek + "-" + endWeek + "周 (应该是: 周四3-4节或周一7-8节, 9-12周)");
+ }
+ }
+
+ // 检查计算机网络课程设计,如果周次不对则删除
+ // 计算机网络课程设计应该只在周一晚上9-12节,1-4周
+ if (courseName.contains("计算机网络") && courseName.contains("课程设计")) {
+ if (dayOfWeek != 1 || timeSlot != 4 || startWeek != 1 || endWeek != 4) {
+ toRemove.add(course);
+ System.out.println("删除硬编码:计算机网络课程设计位置错误或周次错误 - " +
+ "星期" + dayOfWeek + ", 时间段" + timeSlot + ", " +
+ startWeek + "-" + endWeek + "周 (应该是: 周一晚上9-12节, 1-4周)");
+ }
+ }
+ }
+
+ // 处理重复的毛泽东思想课程:如果有多个相同的课程,只保留一个
+ System.out.println("\n检查毛泽东思想课程重复情况:");
+ System.out.println(" 周一3-4节(1-16周): " + maoZeDongCourseCount.get("周一3-4节-1-16周") + " 个");
+ System.out.println(" 周三5-6节(12-14周): " + maoZeDongCourseCount.get("周三5-6节-12-14周") + " 个");
+
+ // 如果有多于1个的相同课程,删除多余的
+ List maoZeDongCourses = new ArrayList<>();
+ for (Course course : allCourses) {
+ if (course != null && course.getName() != null &&
+ course.getName().contains("毛泽东") && !course.getName().contains("新时代")) {
+ maoZeDongCourses.add(course);
+ }
+ }
+
+ // 检查周一3-4节(1-16周)的重复
+ List mondayCourses = new ArrayList<>();
+ for (Course course : maoZeDongCourses) {
+ if (course.getDayOfWeek() == 1 && course.getTimeSlot() == 1 &&
+ course.getStartWeek() == 1 && course.getEndWeek() == 16) {
+ mondayCourses.add(course);
+ }
+ }
+ // 如果有多于1个,删除多余的(保留第一个)
+ if (mondayCourses.size() > 1) {
+ System.out.println(" 发现 " + mondayCourses.size() + " 个重复的周一3-4节(1-16周)毛泽东思想课程,删除多余的");
+ for (int i = 1; i < mondayCourses.size(); i++) {
+ toRemove.add(mondayCourses.get(i));
+ System.out.println(" 删除重复课程: " + mondayCourses.get(i).getName() + " (ID: " + mondayCourses.get(i).getId() + ")");
+ }
+ }
+
+ // 检查周三5-6节(12-14周)的重复
+ List wednesdayCourses = new ArrayList<>();
+ for (Course course : maoZeDongCourses) {
+ if (course.getDayOfWeek() == 3 && course.getTimeSlot() == 2 &&
+ course.getStartWeek() == 12 && course.getEndWeek() == 14) {
+ wednesdayCourses.add(course);
+ }
+ }
+ // 如果有多于1个,删除多余的(保留第一个)
+ if (wednesdayCourses.size() > 1) {
+ System.out.println(" 发现 " + wednesdayCourses.size() + " 个重复的周三5-6节(12-14周)毛泽东思想课程,删除多余的");
+ for (int i = 1; i < wednesdayCourses.size(); i++) {
+ toRemove.add(wednesdayCourses.get(i));
+ System.out.println(" 删除重复课程: " + wednesdayCourses.get(i).getName() + " (ID: " + wednesdayCourses.get(i).getId() + ")");
+ }
+ }
+
+ // 删除所有标记为删除的课程
+ Context context = getContext();
+ for (Course course : toRemove) {
+ if (context != null) {
+ ReminderScheduler.cancelReminder(context, course);
+ }
+ dataManager.deleteCourse(course.getId());
+ System.out.println("已删除硬编码课程: " + course.getName() +
+ " (星期" + course.getDayOfWeek() + ", 时间段" + course.getTimeSlot() +
+ ", " + course.getStartWeek() + "-" + course.getEndWeek() + "周)");
+ }
+
+ if (!toRemove.isEmpty()) {
+ System.out.println("========================================");
+ System.out.println("清理完成:删除了 " + toRemove.size() + " 门硬编码的测试课程");
+ System.out.println("========================================");
+ // 重新加载课程列表
+ courses = dataManager.getCourses();
+ } else {
+ System.out.println("========================================");
+ System.out.println("未发现硬编码的测试课程");
+ System.out.println("========================================");
+ }
+ }
+
+ /**
+ * 导入周一的课程(根据图片中的课表数据)
+ * 【已禁用】此方法包含硬编码测试数据,不应使用
+ */
+ @Deprecated
+ private void importMondayCourses() {
+ // 已禁用:此方法包含硬编码测试数据,不应使用
+ return;
+ /*
+ // 检查是否已经导入过这些课程(避免重复导入)
+ List existingCourses = dataManager.getCourses();
+ boolean hasMondayCourses = false;
+ for (Course c : existingCourses) {
+ if (c.getDayOfWeek() == 1 && c.getName().contains("编译原理")) {
+ hasMondayCourses = true;
+ break;
+ }
+ }
+
+ if (hasMondayCourses) {
+ System.out.println("周一课程已存在,跳过导入");
+ return;
+ }
+
+ System.out.println("开始导入周一的课程...");
+
+ // 1. 编译原理(Ⅰ)★ - 上午1-2节,1-8周
+ Course course1 = new Course("编译原理(Ⅰ)", "张志远", "东丽校区(北) 北教25-110", 1, 0);
+ course1.setStartWeek(1);
+ course1.setEndWeek(8);
+ course1.setStartPeriod(1);
+ course1.setEndPeriod(2);
+ course1.setSemester("2025-2026-1");
+ course1.setImported(true);
+ dataManager.addCourse(course1);
+ System.out.println("导入: " + course1.getName() + " (星期1, 时间段0, 1-8周)");
+
+ // 2. 毛泽东思想和中国特色社会主义理论体系概论(B)★ - 上午3-4节,1-16周
+ Course course2 = new Course("毛泽东思想和中国特色社会主义理论体系概论(B)", "阎维洁", "东丽校区(北) 北教25-111", 1, 1);
+ course2.setStartWeek(1);
+ course2.setEndWeek(16);
+ course2.setStartPeriod(3);
+ course2.setEndPeriod(4);
+ course2.setSemester("2025-2026-1");
+ course2.setImported(true);
+ dataManager.addCourse(course2);
+ System.out.println("导入: " + course2.getName() + " (星期1, 时间段1, 1-16周)");
+
+ // 3. 操作系统★ - 下午5-6节,1-12周
+ Course course3 = new Course("操作系统", "杨志娴", "东丽校区(北) 北教25-201", 1, 2);
+ course3.setStartWeek(1);
+ course3.setEndWeek(12);
+ course3.setStartPeriod(5);
+ course3.setEndPeriod(6);
+ course3.setSemester("2025-2026-1");
+ course3.setImported(true);
+ dataManager.addCourse(course3);
+ System.out.println("导入: " + course3.getName() + " (星期1, 时间段2, 1-12周)");
+
+ // 4. 编译原理课程设计■ - 下午7-8节,9-12周
+ Course course4 = new Course("编译原理课程设计", "张志远", "东丽校区(北) 北教25-518", 1, 3);
+ course4.setStartWeek(9);
+ course4.setEndWeek(12);
+ course4.setStartPeriod(7);
+ course4.setEndPeriod(8);
+ course4.setSemester("2025-2026-1");
+ course4.setImported(true);
+ dataManager.addCourse(course4);
+ System.out.println("导入: " + course4.getName() + " (星期1, 时间段3, 9-12周)");
+
+ // 5. 计算机网络课程设计(Ⅰ)■ - 晚上9-12节,1-4周
+ Course course5 = new Course("计算机网络课程设计(Ⅰ)", "刘春波", "东丽校区(北) 北教25-518", 1, 4);
+ course5.setStartWeek(1);
+ course5.setEndWeek(4);
+ course5.setStartPeriod(9);
+ course5.setEndPeriod(12);
+ course5.setSemester("2025-2026-1");
+ course5.setImported(true);
+ dataManager.addCourse(course5);
+ System.out.println("导入: " + course5.getName() + " (星期1, 时间段4, 1-4周)");
+
+ System.out.println("完成导入周一课程,共5门");
+ */
+ }
+
+ /**
+ * 导入周二的课程(根据图片中的课表数据)
+ * 【已禁用】此方法包含硬编码测试数据,不应使用
+ */
+ @Deprecated
+ private void importTuesdayCourses() {
+ // 已禁用:此方法包含硬编码测试数据,不应使用
+ return;
+ /*
+ // 检查是否已经导入过这些课程(避免重复导入)
+ List