();
@@ -712,6 +918,12 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mWorkingNote.markDeleted(true);
}
+ /**
+ * 判断当前是否处于同步模式(已配置 Google 账户)。
+ * 同步模式下删除会移动至回收站,非同步模式下直接物理删除。
+ *
+ * @return true 表示已配置同步账户
+ */
private boolean isSyncMode() {
return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0;
}
@@ -721,15 +933,31 @@ public class NoteEditActivity extends Activity implements OnClickListener,
saveNote();
}
if (mWorkingNote.getNoteId() > 0) {
- Intent intent = new Intent(this, AlarmReceiver.class);
+ if (set && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
+ != android.content.pm.PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.POST_NOTIFICATIONS}, 0);
+ }
+ }
+ Intent intent = new Intent(this, NoteReminderReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId()));
- PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
+ int flags = PendingIntent.FLAG_UPDATE_CURRENT;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ flags |= PendingIntent.FLAG_IMMUTABLE;
+ }
+ int requestCode = (int) (mWorkingNote.getNoteId() % Integer.MAX_VALUE);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(this, requestCode, intent, flags);
AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE));
showAlertHeader();
if(!set) {
alarmManager.cancel(pendingIntent);
} else {
- alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, date, pendingIntent);
+ } else {
+ alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent);
+ }
}
} else {
Log.e(TAG, "Clock alert setting error");
@@ -766,6 +994,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
edit.append(text);
edit.requestFocus();
edit.setSelection(length);
+ updateCharCount();
}
// 处理清单项回车(新增一行)
@@ -783,6 +1012,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text))
.setIndex(i);
}
+ updateCharCount();
}
// 切换到清单模式:将文本拆分为列表项
@@ -801,21 +1031,26 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mNoteEditor.setVisibility(View.GONE);
mEditTextList.setVisibility(View.VISIBLE);
+ updateCharCount();
}
// 获取带有高亮的 Spannable 文本 (用于搜索结果高亮)
private Spannable getHighlightQueryResult(String fullText, String userQuery) {
- SpannableString spannable = new SpannableString(fullText == null ? "" : fullText);
+ String safeText = fullText == null ? "" : fullText;
+ SpannableString spannable = new SpannableString(safeText);
if (!TextUtils.isEmpty(userQuery)) {
- mPattern = Pattern.compile(userQuery);
- Matcher m = mPattern.matcher(fullText);
- int start = 0;
- while (m.find(start)) {
- spannable.setSpan(
- new BackgroundColorSpan(this.getResources().getColor(
- R.color.user_query_highlight)), m.start(), m.end(),
- Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
- start = m.end();
+ try {
+ mPattern = Pattern.compile(userQuery);
+ Matcher m = mPattern.matcher(safeText);
+ int start = 0;
+ while (m.find(start)) {
+ spannable.setSpan(
+ new BackgroundColorSpan(ContextCompat.getColor(this, R.color.user_query_highlight)),
+ m.start(), m.end(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ start = m.end();
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "getHighlightQueryResult: " + e.getMessage());
}
}
return spannable;
@@ -850,6 +1085,17 @@ public class NoteEditActivity extends Activity implements OnClickListener,
edit.setOnTextViewChangeListener(this);
edit.setIndex(index);
edit.setText(getHighlightQueryResult(item, mUserQuery));
+ // 清单模式:每项内容变化时实时更新字数
+ edit.addTextChangedListener(new 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) {}
+ @Override
+ public void afterTextChanged(Editable s) {
+ updateCharCount();
+ }
+ });
return view;
}
@@ -863,6 +1109,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
} else {
mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE);
}
+ updateCharCount();
}
public void onCheckListModeChanged(int oldMode, int newMode) {
@@ -877,8 +1124,15 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mEditTextList.setVisibility(View.GONE);
mNoteEditor.setVisibility(View.VISIBLE);
}
+ updateCharCount();
}
+ /**
+ * 从 UI 获取当前编辑内容并同步到 WorkingNote。
+ * 清单模式会按行拼接复选框符号(√/□)和文本。
+ *
+ * @return 清单模式下是否至少有一项被勾选
+ */
private boolean getWorkingText() {
boolean hasChecked = false;
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
@@ -912,6 +1166,10 @@ public class NoteEditActivity extends Activity implements OnClickListener,
return saved;
}
+ /**
+ * 将当前便签以快捷方式形式发送到桌面。
+ * 需便签已保存(有 noteId),否则提示错误。
+ */
private void sendToDesktop() {
if (!mWorkingNote.existInDatabase()) {
saveNote();
@@ -937,6 +1195,12 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
}
+ /**
+ * 创建桌面快捷方式时使用的标题:去除复选框符号,最多取前 10 个字符。
+ *
+ * @param content 便签正文
+ * @return 截断后的标题
+ */
private String makeShortcutIconTitle(String content) {
content = content.replace(TAG_CHECKED, "");
content = content.replace(TAG_UNCHECKED, "");
@@ -944,10 +1208,19 @@ public class NoteEditActivity extends Activity implements OnClickListener,
SHORTCUT_ICON_TITLE_MAX_LEN) : content;
}
+ /**
+ * 显示短时 Toast 提示。
+ */
private void showToast(int resId) {
showToast(resId, Toast.LENGTH_SHORT);
}
+ /**
+ * 显示指定时长的 Toast 提示。
+ *
+ * @param resId 字符串资源 ID
+ * @param duration Toast.LENGTH_SHORT 或 Toast.LENGTH_LONG
+ */
private void showToast(int resId, int duration) {
Toast.makeText(this, resId, duration).show();
}
@@ -1087,31 +1360,51 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// 2. 启动相机
private void takePhoto() {
Intent takePictureIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
- // 确保有相机应用能处理这个 Intent
- if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
- try {
- // 使用 MediaUtils 创建临时文件 (高内聚:Activity 不关心文件怎么创建的)
- java.io.File photoFile = net.micode.notes.tool.MediaUtils.createImageFile(this);
- mCurrentPhotoPath = photoFile.getAbsolutePath();
-
- // 获取安全的 Content Uri (使用我们刚配置好的 FileProvider)
- android.net.Uri photoURI = androidx.core.content.FileProvider.getUriForFile(this,
- "net.micode.notes.fileprovider",
- photoFile);
-
- takePictureIntent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, photoURI);
- startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
- } catch (java.io.IOException ex) {
- Toast.makeText(this, "创建图片文件失败", Toast.LENGTH_SHORT).show();
- }
- } else {
+ android.content.pm.ResolveInfo resolveInfo = getPackageManager().resolveActivity(
+ takePictureIntent, android.content.pm.PackageManager.MATCH_DEFAULT_ONLY);
+ if (resolveInfo == null || resolveInfo.activityInfo == null) {
Toast.makeText(this, "未找到相机应用", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ try {
+ java.io.File photoFile = net.micode.notes.tool.MediaUtils.createImageFile(this);
+ mCurrentPhotoPath = photoFile.getCanonicalPath();
+
+ android.net.Uri photoURI = androidx.core.content.FileProvider.getUriForFile(this,
+ "net.micode.notes.fileprovider",
+ photoFile);
+
+ takePictureIntent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, photoURI);
+ takePictureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ takePictureIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ // Android 7.1+ 部分机型依赖 ClipData 传递 URI 权限
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ takePictureIntent.setClipData(android.content.ClipData.newUri(getContentResolver(), "", photoURI));
+ }
+ // 显式授予已解析的相机应用对该 URI 的写权限,提高兼容性
+ grantUriPermission(resolveInfo.activityInfo.packageName, photoURI,
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
+ } catch (java.io.IOException ex) {
+ Log.e(TAG, "takePhoto createImageFile", ex);
+ Toast.makeText(this, "创建图片文件失败", Toast.LENGTH_SHORT).show();
+ } catch (IllegalArgumentException ex) {
+ Log.e(TAG, "takePhoto FileProvider URI", ex);
+ Toast.makeText(this, "无法创建拍照存储路径", Toast.LENGTH_SHORT).show();
+ } catch (Exception ex) {
+ Log.e(TAG, "takePhoto", ex);
+ Toast.makeText(this, "无法启动相机: " + ex.getMessage(), Toast.LENGTH_SHORT).show();
}
}
- // 3. 打开相册
+ // 3. 打开相册(使用 GET_CONTENT 以更好兼容截图、微信等来源的图片)
private void pickFromGallery() {
- Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("image/*");
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ // 确保选图器返回的 Uri 可被本应用读取(部分相册/文件管理器依赖此标志)
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(intent, REQUEST_CODE_OPEN_ALBUM);
}
@Override
@@ -1119,26 +1412,33 @@ public class NoteEditActivity extends Activity implements OnClickListener,
if (resultCode == RESULT_OK) {
switch (requestCode) {
case REQUEST_CODE_TAKE_PHOTO:
- if (mCurrentPhotoPath != null) {
- // === [修改] 调用 NoteEditText 的接口插入图片 ===
+ if (mCurrentPhotoPath != null && mNoteEditor != null) {
mNoteEditor.insertImage(mCurrentPhotoPath);
- // 【新增】立刻把含标签的新文本保存到 Model,防止 onResume 覆盖
- mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
+ mNoteEditor.setTextWithImages(mNoteEditor.getText().toString());
+ if (mWorkingNote != null) {
+ mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
+ }
}
+ mCurrentPhotoPath = null;
break;
case REQUEST_CODE_OPEN_ALBUM:
if (data != null && data.getData() != null) {
android.net.Uri selectedImage = data.getData();
+ try {
+ final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ if (takeFlags != 0) {
+ getContentResolver().takePersistableUriPermission(selectedImage, takeFlags);
+ }
+ } catch (SecurityException ignored) { }
String localPath = net.micode.notes.tool.MediaUtils.copyUriToInternalStorage(this, selectedImage);
if (localPath != null) {
mNoteEditor.insertImage(localPath);
- // 【新增】立刻保存到 Model
+ mNoteEditor.setTextWithImages(mNoteEditor.getText().toString());
mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
+ } else {
+ Toast.makeText(this, R.string.error_image_copy_failed, Toast.LENGTH_SHORT).show();
}
-
-
- // ============================================
}
break;
diff --git a/src/src/net/micode/notes/ui/NoteEditText.java b/src/app/src/main/java/net/micode/notes/ui/NoteEditText.java
similarity index 58%
rename from src/src/net/micode/notes/ui/NoteEditText.java
rename to src/app/src/main/java/net/micode/notes/ui/NoteEditText.java
index 217f887..9af7e4b 100644
--- a/src/src/net/micode/notes/ui/NoteEditText.java
+++ b/src/app/src/main/java/net/micode/notes/ui/NoteEditText.java
@@ -7,10 +7,15 @@ package net.micode.notes.ui;
import android.content.Context;
import android.graphics.Rect;
+import android.graphics.Typeface;
import android.text.Layout;
import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.util.Log;
@@ -19,6 +24,7 @@ import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
+import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
@@ -85,6 +91,13 @@ public class NoteEditText extends EditText {
private OnTextViewChangeListener mOnTextViewChangeListener;
+ /** 伪 Placeholder:当光标定位到该行且内容完全匹配时自动清空该行 */
+ private String mPlaceholderLine1;
+ private String mPlaceholderLine2;
+
+ /**
+ * 代码创建的构造器,默认索引为 0。
+ */
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
@@ -111,6 +124,13 @@ public class NoteEditText extends EditText {
e.printStackTrace();
}
}
+
+ /**
+ * 设置富文本内容:解析文本中的 <img src="路径"/> 标签,将图片渲染为 CenterImageSpan。
+ * 用于加载便签时还原内嵌图片,以及插入图片后刷新显示。
+ *
+ * @param text 包含 img 标签的原始文本
+ */
public void setTextWithImages(String text) {
// 1. 先设置纯文本
setText(text);
@@ -154,23 +174,78 @@ public class NoteEditText extends EditText {
e.printStackTrace();
}
}
+ // 强制重新测量与绘制,避免长文本/多图时高度未更新导致内容不可见
+ requestLayout();
+ post(new Runnable() {
+ @Override
+ public void run() {
+ invalidate();
+ }
+ });
}
+
+ /**
+ * 设置当前编辑框在清单列表中的索引,用于 Enter/Delete 回调时标识行号。
+ *
+ * @param index 从 0 开始的索引
+ */
public void setIndex(int index) {
mIndex = index;
}
+ /**
+ * 设置文本变更监听器,用于在行首删除、回车换行、焦点变化时通知父容器。
+ *
+ * @param listener 监听器实例,可为 null 表示不接收回调
+ */
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
+ /**
+ * 设置“占位行”文案:当用户光标定位到该行且内容完全等于 line1 或 line2 时,自动清空该行便于输入。
+ */
+ public void setPlaceholderLinesForClear(String line1, String line2) {
+ mPlaceholderLine1 = line1;
+ mPlaceholderLine2 = line2;
+ }
+
+ /**
+ * XML 布局 inflate 时使用的双参数构造器。
+ */
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
+ /**
+ * 带 defStyle 的完整构造器,用于主题或样式继承场景。
+ */
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
+ /**
+ * 确保 EditText 按全文排版并报告完整内容高度,避免“第 36 行起全部消失”。
+ * 原因:父布局传入 AT_MOST(一屏高) 时,DynamicLayout 只会排到该高度内的行数(约 35 行),
+ * 超出部分未进入 Layout,绘制时表现为整页消失。故在 AT_MOST 时先用 UNSPECIFIED 测一次,
+ * 让内部按全文排版,再以实际内容高度作为 measuredHeight。
+ */
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ int heightMode = View.MeasureSpec.getMode(heightSpec);
+ if (heightMode == View.MeasureSpec.AT_MOST) {
+ int unboundedHeight = View.MeasureSpec.makeMeasureSpec(0x3FFFFFFF, View.MeasureSpec.UNSPECIFIED);
+ super.onMeasure(widthSpec, unboundedHeight);
+ } else {
+ super.onMeasure(widthSpec, heightSpec);
+ }
+ Layout layout = getLayout();
+ if (layout != null) {
+ int contentHeight = layout.getHeight() + getCompoundPaddingTop() + getCompoundPaddingBottom();
+ setMeasuredDimension(getMeasuredWidth(), contentHeight);
+ }
+ }
+
/**
* 处理触摸事件
*
@@ -190,15 +265,50 @@ public class NoteEditText extends EditText {
y += getScrollY();
Layout layout = getLayout();
- int line = layout.getLineForVertical(y);
- int off = layout.getOffsetForHorizontal(line, x);
- Selection.setSelection(getText(), off);
+ if (layout != null) {
+ int line = layout.getLineForVertical(y);
+ int off = layout.getOffsetForHorizontal(line, x);
+ Selection.setSelection(getText(), off);
+ // 伪 Placeholder:若当前行内容为占位文案则清空该行
+ post(new Runnable() {
+ @Override
+ public void run() {
+ checkAndClearPlaceholderLine();
+ }
+ });
+ }
break;
}
return super.onTouchEvent(event);
}
+ /**
+ * 若光标所在行内容完全等于 mPlaceholderLine1 或 mPlaceholderLine2,则清空该行。
+ * 只删除占位文案,保留行尾换行符,避免下方时间行顶上来导致无法书写标题。
+ */
+ private void checkAndClearPlaceholderLine() {
+ if (TextUtils.isEmpty(mPlaceholderLine1) && TextUtils.isEmpty(mPlaceholderLine2)) return;
+ android.text.Editable text = getText();
+ if (text == null) return;
+ Layout layout = getLayout();
+ if (layout == null) return;
+ int selStart = getSelectionStart();
+ if (selStart < 0) return;
+ int line = layout.getLineForOffset(selStart);
+ int lineStart = layout.getLineStart(line);
+ int lineEnd = layout.getLineEnd(line);
+ String lineContent = text.subSequence(lineStart, lineEnd).toString().trim();
+ if (lineContent.equals(mPlaceholderLine1) || lineContent.equals(mPlaceholderLine2)) {
+ // 只替换占位文案,保留行尾换行符,避免下一行(如时间)顶上来
+ int replaceEnd = lineEnd;
+ if (lineEnd > lineStart && lineEnd <= text.length() && text.charAt(lineEnd - 1) == '\n') {
+ replaceEnd = lineEnd - 1;
+ }
+ text.replace(lineStart, replaceEnd, "");
+ }
+ }
+
/**
* 键盘按下事件拦截
* 记录关键状态,实际逻辑在 onKeyUp 中执行。
@@ -314,4 +424,88 @@ public class NoteEditText extends EditText {
}
super.onCreateContextMenu(menu);
}
+
+ // ================= 富文本工具栏:加粗、字号变大、字号变小(仅运行时效果,不持久化) =================
+
+ /**
+ * 对选中区域应用或取消加粗 (StyleSpan BOLD)。
+ * 若选区已全部加粗则取消加粗,否则应用加粗。
+ */
+ public void applyBoldToSelection() {
+ applySpanToSelection(new StyleSpan(Typeface.BOLD), StyleSpan.class, true);
+ }
+
+ /**
+ * 对选中区域应用相对字号 1.2 倍 (RelativeSizeSpan)。
+ */
+ public void applyFontSizeUpToSelection() {
+ applyRelativeSizeToSelection(1.2f);
+ }
+
+ /**
+ * 对选中区域应用相对字号 0.8 倍 (RelativeSizeSpan)。
+ */
+ public void applyFontSizeDownToSelection() {
+ applyRelativeSizeToSelection(0.8f);
+ }
+
+ /**
+ * 对选中区域应用或移除指定倍率的 RelativeSizeSpan。
+ * 若选区已有相同比例的 span 则移除(切换效果),否则添加。
+ *
+ * @param proportion 相对字号倍率,如 1.2f 表示 1.2 倍
+ */
+ private void applyRelativeSizeToSelection(float proportion) {
+ android.text.Editable text = getText();
+ if (text == null || !(text instanceof Spannable)) return;
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ if (start < 0 || end < 0 || start == end) return;
+ if (start > end) { int t = start; start = end; end = t; }
+ Spannable sp = (Spannable) text;
+ RelativeSizeSpan[] existing = sp.getSpans(start, end, RelativeSizeSpan.class);
+ // 若选区已有相同比例的 RelativeSizeSpan 则移除,否则添加
+ boolean hasSame = false;
+ for (RelativeSizeSpan s : existing) {
+ if (Math.abs(s.getSizeChange() - proportion) < 0.01f) {
+ sp.removeSpan(s);
+ hasSame = true;
+ }
+ }
+ if (!hasSame) {
+ sp.setSpan(new RelativeSizeSpan(proportion), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ /**
+ * 对选中区域应用或移除指定类型的 Span。
+ * @param addSpan 要添加的 span 实例(仅用于“添加”分支)
+ * @param spanClass 要检测/移除的 span 类型
+ * @param toggle 若为 true:若选区已全部为该 span 则移除,否则添加
+ */
+ private void applySpanToSelection(Object addSpan, Class> spanClass, boolean toggle) {
+ android.text.Editable text = getText();
+ if (text == null || !(text instanceof Spannable)) return;
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ if (start < 0 || end < 0 || start == end) return;
+ if (start > end) { int t = start; start = end; end = t; }
+ Spannable sp = (Spannable) text;
+ Object[] existing = sp.getSpans(start, end, spanClass);
+ boolean allCovered = true;
+ for (Object s : existing) {
+ int sStart = sp.getSpanStart(s);
+ int sEnd = sp.getSpanEnd(s);
+ if (sStart > start || sEnd < end) {
+ allCovered = false;
+ break;
+ }
+ }
+ if (existing.length == 0) allCovered = false;
+ if (toggle && allCovered && existing.length > 0) {
+ for (Object s : existing) sp.removeSpan(s);
+ } else {
+ sp.setSpan(addSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/ui/NoteItemData.java b/src/app/src/main/java/net/micode/notes/ui/NoteItemData.java
similarity index 96%
rename from src/src/net/micode/notes/ui/NoteItemData.java
rename to src/app/src/main/java/net/micode/notes/ui/NoteItemData.java
index 68087a8..662a55d 100644
--- a/src/src/net/micode/notes/ui/NoteItemData.java
+++ b/src/app/src/main/java/net/micode/notes/ui/NoteItemData.java
@@ -39,6 +39,7 @@ public class NoteItemData {
NoteColumns.TYPE,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
+ NoteColumns.PINNED,
};
// 列索引常量,对应 PROJECTION 数组
@@ -54,6 +55,7 @@ public class NoteItemData {
private static final int TYPE_COLUMN = 9;
private static final int WIDGET_ID_COLUMN = 10;
private static final int WIDGET_TYPE_COLUMN = 11;
+ private static final int PINNED_COLUMN = 12;
// 数据字段
private long mId;
@@ -68,6 +70,7 @@ public class NoteItemData {
private int mType; // 类型 (便签/文件夹)
private int mWidgetId;
private int mWidgetType;
+ private boolean mPinned; // 是否置顶
private String mName; // 联系人名字 (仅通话记录便签有效)
private String mPhoneNumber; // 电话号码 (仅通话记录便签有效)
@@ -99,6 +102,7 @@ public class NoteItemData {
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
+ mPinned = (cursor.getInt(PINNED_COLUMN) > 0);
mPhoneNumber = "";
// 特殊逻辑:如果是通话记录文件夹下的便签,需要查找联系人名称
@@ -240,6 +244,10 @@ public class NoteItemData {
return (mAlertDate > 0);
}
+ public boolean isPinned() {
+ return mPinned;
+ }
+
public boolean isCallRecord() {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
diff --git a/src/app/src/main/java/net/micode/notes/ui/NoteReminderNotifier.java b/src/app/src/main/java/net/micode/notes/ui/NoteReminderNotifier.java
new file mode 100644
index 0000000..0ff82b5
--- /dev/null
+++ b/src/app/src/main/java/net/micode/notes/ui/NoteReminderNotifier.java
@@ -0,0 +1,80 @@
+package net.micode.notes.ui;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.core.content.ContextCompat;
+
+import net.micode.notes.R;
+import net.micode.notes.tool.DataUtils;
+
+/**
+ * 便签时间提醒通知:到点后发出一条通知,点击可打开闹钟弹窗。
+ * 需在应用信息中开启「通知」权限(Android 13+)后才会显示。
+ */
+public final class NoteReminderNotifier {
+ private static final String TAG = "NoteReminderNotifier";
+ public static final String CHANNEL_ID = "note_reminder_channel";
+ private static final int SNIPPET_MAX_LEN = 40;
+
+ public static void show(Context context, long noteId, Uri noteUri) {
+ if (context == null || noteUri == null) return;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS)
+ != android.content.pm.PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "POST_NOTIFICATIONS not granted");
+ return;
+ }
+ }
+ String snippet;
+ try {
+ snippet = DataUtils.getSnippetById(context.getContentResolver(), noteId);
+ if (snippet == null) snippet = "";
+ snippet = snippet.trim();
+ if (snippet.length() > SNIPPET_MAX_LEN) {
+ snippet = snippet.substring(0, SNIPPET_MAX_LEN) + "…";
+ }
+ } catch (Exception e) {
+ snippet = context.getString(R.string.note_reminder_default_text);
+ }
+ NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ if (nm == null) return;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID,
+ context.getString(R.string.note_reminder_channel_name),
+ NotificationManager.IMPORTANCE_HIGH);
+ channel.setDescription(context.getString(R.string.note_reminder_channel_desc));
+ nm.createNotificationChannel(channel);
+ }
+ Intent openIntent = new Intent(context, AlarmAlertActivity.class);
+ openIntent.setData(noteUri);
+ openIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ PendingIntent pending = PendingIntent.getActivity(
+ context,
+ (int) (noteId % Integer.MAX_VALUE),
+ openIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.drawable.icon_app)
+ .setContentTitle(context.getString(R.string.note_reminder_title))
+ .setContentText(snippet.isEmpty() ? context.getString(R.string.note_reminder_default_text) : snippet)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_REMINDER)
+ .setAutoCancel(true)
+ .setContentIntent(pending);
+ try {
+ NotificationManagerCompat.from(context).notify((int) (noteId % Integer.MAX_VALUE), builder.build());
+ } catch (SecurityException e) {
+ Log.e(TAG, "Cannot show notification: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/app/src/main/java/net/micode/notes/ui/NoteReminderReceiver.java b/src/app/src/main/java/net/micode/notes/ui/NoteReminderReceiver.java
new file mode 100644
index 0000000..5705e20
--- /dev/null
+++ b/src/app/src/main/java/net/micode/notes/ui/NoteReminderReceiver.java
@@ -0,0 +1,40 @@
+package net.micode.notes.ui;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+
+/**
+ * 便签时间提醒广播:到点后先发通知,再尝试打开闹钟弹窗。
+ * 设置提醒时使用本 Receiver,确保到点有通知(需开启通知权限)。
+ */
+public class NoteReminderReceiver extends BroadcastReceiver {
+ private static final String TAG = "NoteReminderReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Uri data = intent.getData();
+ if (data == null || data.getPathSegments() == null || data.getPathSegments().size() < 2) {
+ Log.e(TAG, "Alarm intent has no note data");
+ return;
+ }
+ long noteId;
+ try {
+ noteId = Long.parseLong(data.getPathSegments().get(1));
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Invalid note id in alarm intent", e);
+ return;
+ }
+ NoteReminderNotifier.show(context, noteId, data);
+ Intent alertIntent = new Intent(context, AlarmAlertActivity.class);
+ alertIntent.setData(data);
+ alertIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ try {
+ context.startActivity(alertIntent);
+ } catch (Exception e) {
+ Log.d(TAG, "Could not start AlarmAlertActivity: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/src/net/micode/notes/ui/NotesListActivity.java b/src/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
similarity index 90%
rename from src/src/net/micode/notes/ui/NotesListActivity.java
rename to src/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
index ffcdc0d..48456c7 100644
--- a/src/src/net/micode/notes/ui/NotesListActivity.java
+++ b/src/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
@@ -27,22 +27,21 @@ import android.util.Log;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
-import android.view.Display;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
-import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
-import android.view.View.OnTouchListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.Button;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.PopupMenu;
@@ -87,6 +86,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private static final int MENU_FOLDER_DELETE = 0;
private static final int MENU_FOLDER_VIEW = 1;
private static final int MENU_FOLDER_CHANGE_NAME = 2;
+ private static final int MENU_FOLDER_PIN = 3;
+ private static final int MENU_FOLDER_UNPIN = 4;
private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction";
@@ -104,11 +105,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private NotesListAdapter mNotesListAdapter;
private ListView mNotesListView;
- private Button mAddNewNote;
-
- private boolean mDispatch;
- private int mOriginY;
- private int mDispatchY;
+ private FloatingActionButton mFab;
private TextView mTitleBar;
private long mCurrentFolderId; // 当前所在的文件夹 ID
@@ -162,11 +159,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) {
StringBuilder sb = new StringBuilder();
InputStream in = null;
+ InputStreamReader isr = null;
+ BufferedReader br = null;
try {
in = getResources().openRawResource(R.raw.introduction);
if (in != null) {
- InputStreamReader isr = new InputStreamReader(in);
- BufferedReader br = new BufferedReader(isr);
+ isr = new InputStreamReader(in);
+ br = new BufferedReader(isr);
char [] buf = new char[1024];
int len = 0;
while ((len = br.read(buf)) > 0) {
@@ -180,12 +179,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
e.printStackTrace();
return;
} finally {
- if(in != null) {
- try {
- in.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
+ if (br != null) {
+ try { br.close(); } catch (IOException e) { e.printStackTrace(); }
+ }
+ if (isr != null) {
+ try { isr.close(); } catch (IOException e) { e.printStackTrace(); }
+ }
+ if (in != null) {
+ try { in.close(); } catch (IOException e) { e.printStackTrace(); }
}
}
@@ -220,12 +221,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mNotesListView.setOnItemLongClickListener(this);
mNotesListAdapter = new NotesListAdapter(this);
mNotesListView.setAdapter(mNotesListAdapter);
- mAddNewNote = (Button) findViewById(R.id.btn_new_note);
- mAddNewNote.setOnClickListener(this);
- mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener());
- mDispatch = false;
- mDispatchY = 0;
- mOriginY = 0;
+ mFab = (FloatingActionButton) findViewById(R.id.btn_fab_new_note);
+ if (mFab != null) {
+ mFab.setOnClickListener(this);
+ }
mTitleBar = (TextView) findViewById(R.id.tv_title_bar);
mState = ListEditState.NOTE_LIST;
mModeCallBack = new ModeCallback();
@@ -243,6 +242,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
getMenuInflater().inflate(R.menu.note_list_options, menu);
menu.findItem(R.id.delete).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.pin_to_top).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.unpin).setOnMenuItemClickListener(this);
mMoveMenu = menu.findItem(R.id.move);
if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER
|| DataUtils.getUserFolderCount(mContentResolver) == 0) {
@@ -254,7 +255,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mActionMode = mode;
mNotesListAdapter.setChoiceMode(true);
mNotesListView.setLongClickable(false);
- mAddNewNote.setVisibility(View.GONE);
+ if (mFab != null) mFab.setVisibility(View.GONE);
// 自定义 ActionMode 的标题栏,植入全选下拉菜单
View customView = LayoutInflater.from(NotesListActivity.this).inflate(
@@ -301,7 +302,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
public void onDestroyActionMode(ActionMode mode) {
mNotesListAdapter.setChoiceMode(false);
mNotesListView.setLongClickable(true);
- mAddNewNote.setVisibility(View.VISIBLE);
+ if (mFab != null) mFab.setVisibility(View.VISIBLE);
}
public void finishActionMode() {
@@ -341,6 +342,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
case R.id.move:
startQueryDestinationFolders();
break;
+ case R.id.pin_to_top:
+ batchSetPinned(true);
+ break;
+ case R.id.unpin:
+ batchSetPinned(false);
+ break;
default:
return false;
}
@@ -348,63 +355,16 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
}
- /**
- * 新建便签按钮的触摸监听器
- *
- * 逻辑说明 (HACKME):
- * "New Note" 按钮有一部分背景是透明的。为了让用户点到透明区域时能穿透点击到下方的列表项,
- * 这里通过计算坐标,手动将事件 Dispatch 给 ListView。
- * 公式 y = -0.12x + 94 是根据 UI 设计图的形状拟合出来的。
- */
- private class NewNoteOnTouchListener implements OnTouchListener {
-
- public boolean onTouch(View v, MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN: {
- Display display = getWindowManager().getDefaultDisplay();
- int screenHeight = display.getHeight();
- int newNoteViewHeight = mAddNewNote.getHeight();
- int start = screenHeight - newNoteViewHeight;
- int eventY = start + (int) event.getY();
- if (mState == ListEditState.SUB_FOLDER) {
- eventY -= mTitleBar.getHeight();
- start -= mTitleBar.getHeight();
- }
- if (event.getY() < (event.getX() * (-0.12) + 94)) {
- View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1
- - mNotesListView.getFooterViewsCount());
- if (view != null && view.getBottom() > start
- && (view.getTop() < (start + 94))) {
- mOriginY = (int) event.getY();
- mDispatchY = eventY;
- event.setLocation(event.getX(), mDispatchY);
- mDispatch = true;
- return mNotesListView.dispatchTouchEvent(event);
- }
- }
- break;
- }
- case MotionEvent.ACTION_MOVE: {
- if (mDispatch) {
- mDispatchY += (int) event.getY() - mOriginY;
- event.setLocation(event.getX(), mDispatchY);
- return mNotesListView.dispatchTouchEvent(event);
- }
- break;
- }
- default: {
- if (mDispatch) {
- event.setLocation(event.getX(), mDispatchY);
- mDispatch = false;
- return mNotesListView.dispatchTouchEvent(event);
- }
- break;
- }
- }
- return false;
+ private void batchSetPinned(boolean pinned) {
+ HashSet ids = mNotesListAdapter.getSelectedItemIds();
+ if (ids == null || ids.isEmpty()) return;
+ if (DataUtils.batchSetPinned(mContentResolver, ids, pinned)) {
+ Toast.makeText(this, pinned ? getString(R.string.toast_pinned) : getString(R.string.toast_unpinned),
+ Toast.LENGTH_SHORT).show();
+ startAsyncNotesListQuery();
+ mModeCallBack.finishActionMode();
}
-
- };
+ }
/**
* 启动异步便签列表查询 (核心逻辑)
@@ -422,7 +382,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null,
Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] {
String.valueOf(mCurrentFolderId)
- }, NoteColumns.TYPE + " DESC," + NoteColumns.BG_COLOR_ID + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
+ }, NoteColumns.PINNED + " DESC," + NoteColumns.TYPE + " DESC," + NoteColumns.BG_COLOR_ID + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
}
private final class BackgroundQueryHandler extends AsyncQueryHandler {
@@ -552,9 +512,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
startAsyncNotesListQuery(); // 刷新列表,查询子文件夹内容
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mState = ListEditState.CALL_RECORD_FOLDER;
- mAddNewNote.setVisibility(View.GONE);
+ if (mFab != null) mFab.setVisibility(View.GONE);
} else {
mState = ListEditState.SUB_FOLDER;
+ if (mFab != null) mFab.setVisibility(View.VISIBLE);
}
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mTitleBar.setText(R.string.call_record_folder_name);
@@ -565,12 +526,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
public void onClick(View v) {
- switch (v.getId()) {
- case R.id.btn_new_note:
- createNewNote();
- break;
- default:
- break;
+ if (v.getId() == R.id.btn_fab_new_note) {
+ createNewNote();
}
}
@@ -677,7 +634,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
case CALL_RECORD_FOLDER:
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
- mAddNewNote.setVisibility(View.VISIBLE);
+ if (mFab != null) mFab.setVisibility(View.VISIBLE);
mTitleBar.setVisibility(View.GONE);
startAsyncNotesListQuery();
break;
@@ -713,6 +670,11 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
if (mFocusNoteDataItem != null) {
menu.setHeaderTitle(mFocusNoteDataItem.getSnippet());
menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view);
+ if (mFocusNoteDataItem.isPinned()) {
+ menu.add(0, MENU_FOLDER_UNPIN, 0, R.string.menu_unpin);
+ } else {
+ menu.add(0, MENU_FOLDER_PIN, 0, R.string.menu_pin_to_top);
+ }
menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete);
menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name);
}
@@ -754,6 +716,26 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
case MENU_FOLDER_CHANGE_NAME:
showCreateOrModifyFolderDialog(false);
break;
+ case MENU_FOLDER_PIN:
+ if (mFocusNoteDataItem != null && mFocusNoteDataItem.getId() > 0) {
+ HashSet ids = new HashSet();
+ ids.add(mFocusNoteDataItem.getId());
+ if (DataUtils.batchSetPinned(mContentResolver, ids, true)) {
+ Toast.makeText(this, getString(R.string.toast_pinned), Toast.LENGTH_SHORT).show();
+ startAsyncNotesListQuery();
+ }
+ }
+ break;
+ case MENU_FOLDER_UNPIN:
+ if (mFocusNoteDataItem != null && mFocusNoteDataItem.getId() > 0) {
+ HashSet ids = new HashSet();
+ ids.add(mFocusNoteDataItem.getId());
+ if (DataUtils.batchSetPinned(mContentResolver, ids, false)) {
+ Toast.makeText(this, getString(R.string.toast_unpinned), Toast.LENGTH_SHORT).show();
+ startAsyncNotesListQuery();
+ }
+ }
+ break;
default:
break;
}
diff --git a/src/src/net/micode/notes/ui/NotesListAdapter.java b/src/app/src/main/java/net/micode/notes/ui/NotesListAdapter.java
similarity index 98%
rename from src/src/net/micode/notes/ui/NotesListAdapter.java
rename to src/app/src/main/java/net/micode/notes/ui/NotesListAdapter.java
index 77c854c..ba34e75 100644
--- a/src/src/net/micode/notes/ui/NotesListAdapter.java
+++ b/src/app/src/main/java/net/micode/notes/ui/NotesListAdapter.java
@@ -98,6 +98,11 @@ public class NotesListAdapter extends CursorAdapter {
notifyDataSetChanged(); // 刷新 UI 以显示勾选框变化
}
+ /**
+ * 判断当前是否处于批量多选模式。
+ *
+ * @return true 表示多选模式,列表项显示复选框
+ */
public boolean isInChoiceMode() {
return mChoiceMode;
}
diff --git a/src/src/net/micode/notes/ui/NotesListFragment.java b/src/app/src/main/java/net/micode/notes/ui/NotesListFragment.java
similarity index 73%
rename from src/src/net/micode/notes/ui/NotesListFragment.java
rename to src/app/src/main/java/net/micode/notes/ui/NotesListFragment.java
index bbcb49c..503853c 100644
--- a/src/src/net/micode/notes/ui/NotesListFragment.java
+++ b/src/app/src/main/java/net/micode/notes/ui/NotesListFragment.java
@@ -11,9 +11,12 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
+import android.media.projection.MediaProjectionManager;
+import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
+import android.provider.Settings;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
@@ -21,17 +24,14 @@ import android.util.Log;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
-import android.view.Display;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
-import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
-import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
@@ -46,8 +46,12 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
+import com.google.android.material.appbar.MaterialToolbar;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
@@ -55,6 +59,8 @@ import net.micode.notes.gtask.remote.GTaskSyncService;
import net.micode.notes.model.WorkingNote;
import net.micode.notes.tool.BackupUtils;
import net.micode.notes.tool.DataUtils;
+import net.micode.notes.tool.FloatingService;
+import net.micode.notes.tool.PrivacySpaceManager;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import net.micode.notes.widget.NoteWidgetProvider_2x;
@@ -72,20 +78,26 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
private static final int MENU_FOLDER_DELETE = 0;
private static final int MENU_FOLDER_VIEW = 1;
private static final int MENU_FOLDER_CHANGE_NAME = 2;
+ private static final int MENU_FOLDER_PIN = 3;
+ private static final int MENU_FOLDER_UNPIN = 4;
private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction";
+ // === [新增] 灵感球相关常量 ===
+
+ // ==========================
+
private enum ListEditState {
- NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER
- };
+ NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER, PRIVACY_SPACE
+ }
private ListEditState mState;
private BackgroundQueryHandler mBackgroundQueryHandler;
private NotesListAdapter mNotesListAdapter;
private ListView mNotesListView;
- private Button mAddNewNote;
- private boolean mDispatch;
- private int mOriginY;
- private int mDispatchY;
+ private MaterialToolbar mToolbar;
+ private FloatingActionButton mFab;
+ private FloatingActionButton mFabBack;
+ private FloatingActionButton mFabExitPrivacy;
private TextView mTitleBar;
private long mCurrentFolderId;
private ContentResolver mContentResolver;
@@ -106,8 +118,7 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.note_list, container, false);
- // 重要:告诉系统这个 Fragment 有自己的菜单项(Sync, Setting等)
- setHasOptionsMenu(true);
+ setHasOptionsMenu(true); // 允许 Fragment 处理菜单
return view;
}
@@ -118,8 +129,16 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
setAppInfoFromRawRes();
}
+ @Override
+ public void onStart() {
+ super.onStart();
+ startAsyncNotesListQuery();
+ }
+
+ // === [修改] onActivityResult 处理所有请求 ===
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // 只保留原本便签编辑返回刷新的逻辑
if (resultCode == Activity.RESULT_OK
&& (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) {
mNotesListAdapter.changeCursor(null);
@@ -128,16 +147,149 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
}
}
+ private void initResources(View view) {
+ mContentResolver = getActivity().getContentResolver();
+ mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver);
+ // 若进程存活且处于隐私空间(如配置变更后重建),恢复状态
+ if (PrivacySpaceManager.isInPrivacySpace()) {
+ mCurrentFolderId = Notes.ID_PRIVACY_FOLDER;
+ mState = ListEditState.PRIVACY_SPACE;
+ applyPrivacySpaceTheme(view);
+ } else {
+ mCurrentFolderId = Notes.ID_ROOT_FOLDER;
+ mState = ListEditState.NOTE_LIST;
+ }
+ mNotesListView = (ListView) view.findViewById(R.id.notes_list);
+ mNotesListView.addFooterView(LayoutInflater.from(getActivity()).inflate(R.layout.note_list_footer, null),
+ null, false);
+ mNotesListView.setOnItemClickListener(new OnListItemClickListener());
+ mNotesListView.setOnItemLongClickListener(this);
+ mNotesListAdapter = new NotesListAdapter(getActivity());
+ mNotesListView.setAdapter(mNotesListAdapter);
+
+ mToolbar = (MaterialToolbar) view.findViewById(R.id.toolbar_notes);
+ mFab = (FloatingActionButton) view.findViewById(R.id.btn_fab_new_note);
+ mFabBack = (FloatingActionButton) view.findViewById(R.id.btn_fab_back);
+ mFabExitPrivacy = (FloatingActionButton) view.findViewById(R.id.btn_fab_exit_privacy);
+ mTitleBar = (TextView) view.findViewById(R.id.tv_title_bar);
+
+ if (mFabBack != null) {
+ mFabBack.setOnClickListener(v -> performBackToRoot());
+ mFabBack.setVisibility(View.GONE);
+ }
+ if (mFabExitPrivacy != null) {
+ mFabExitPrivacy.setOnClickListener(v -> performExitPrivacySpace());
+ mFabExitPrivacy.setVisibility(PrivacySpaceManager.isInPrivacySpace() ? View.VISIBLE : View.GONE);
+ }
+ if (mToolbar != null) {
+ mToolbar.setNavigationOnClickListener(v -> {
+ if (getActivity() instanceof MainActivity) {
+ ((MainActivity) getActivity()).openDrawer();
+ }
+ });
+ }
+ if (mFab != null) {
+ mFab.setOnClickListener(v -> createNewNote());
+ }
+
+ if (getActivity() instanceof MainActivity) {
+ ((MainActivity) getActivity()).setNotesListFragment(this);
+ }
+ if (!PrivacySpaceManager.isInPrivacySpace()) {
+ mState = ListEditState.NOTE_LIST;
+ }
+ mModeCallBack = new ModeCallback();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if (getActivity() instanceof MainActivity) {
+ ((MainActivity) getActivity()).setNotesListFragment(null);
+ }
+ }
+
+ // === [新增] 检查并启动灵感球服务 ===
+
+
+ // 处理菜单创建
+ @Override
+ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+ menu.clear();
+ if (mState == ListEditState.PRIVACY_SPACE) {
+ inflater.inflate(R.menu.note_list_privacy, menu);
+ } else if (mState == ListEditState.NOTE_LIST) {
+ inflater.inflate(R.menu.note_list, menu);
+ menu.findItem(R.id.menu_sync).setTitle(
+ GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync);
+ } else if (mState == ListEditState.SUB_FOLDER) {
+ inflater.inflate(R.menu.sub_folder, menu);
+ } else if (mState == ListEditState.CALL_RECORD_FOLDER) {
+ inflater.inflate(R.menu.call_record_folder, menu);
+ }
+ }
+
+ // 处理菜单点击
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ switch (item.getItemId()) {
+
+ case R.id.menu_new_folder: {
+ showCreateOrModifyFolderDialog(true);
+ break;
+ }
+ case R.id.menu_export_text: {
+ exportNoteToText();
+ break;
+ }
+ case R.id.menu_sync: {
+ if (isSyncMode()) {
+ if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) {
+ GTaskSyncService.startSync(getActivity());
+ } else {
+ GTaskSyncService.cancelSync(getActivity());
+ }
+ } else {
+ startPreferenceActivity();
+ }
+ break;
+ }
+ case R.id.menu_setting: {
+ startPreferenceActivity();
+ break;
+ }
+ case R.id.menu_new_note: {
+ createNewNote();
+ break;
+ }
+ case R.id.menu_search:
+ onSearchRequested();
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+ // 搜索功能适配
+ public boolean onSearchRequested() {
+ startActivity(new Intent(getActivity(), NotesListActivity.class).setAction(Intent.ACTION_SEARCH));
+ return true;
+ }
+
+ // 首次使用的介绍便签
private void setAppInfoFromRawRes() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) {
StringBuilder sb = new StringBuilder();
InputStream in = null;
+ InputStreamReader isr = null;
+ BufferedReader br = null;
try {
in = getResources().openRawResource(R.raw.introduction);
if (in != null) {
- InputStreamReader isr = new InputStreamReader(in);
- BufferedReader br = new BufferedReader(isr);
+ isr = new InputStreamReader(in);
+ br = new BufferedReader(isr);
char [] buf = new char[1024];
int len = 0;
while ((len = br.read(buf)) > 0) {
@@ -151,12 +303,14 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
e.printStackTrace();
return;
} finally {
- if(in != null) {
- try {
- in.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
+ if (br != null) {
+ try { br.close(); } catch (IOException e) { e.printStackTrace(); }
+ }
+ if (isr != null) {
+ try { isr.close(); } catch (IOException e) { e.printStackTrace(); }
+ }
+ if (in != null) {
+ try { in.close(); } catch (IOException e) { e.printStackTrace(); }
}
}
@@ -173,33 +327,9 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
}
}
- @Override
- public void onStart() {
- super.onStart();
- startAsyncNotesListQuery();
- }
+ // --- 以下为原有的辅助类和方法,适配了 Context ---
+
- private void initResources(View view) {
- mContentResolver = getActivity().getContentResolver();
- mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver);
- mCurrentFolderId = Notes.ID_ROOT_FOLDER;
- mNotesListView = (ListView) view.findViewById(R.id.notes_list);
- mNotesListView.addFooterView(LayoutInflater.from(getActivity()).inflate(R.layout.note_list_footer, null),
- null, false);
- mNotesListView.setOnItemClickListener(new OnListItemClickListener());
- mNotesListView.setOnItemLongClickListener(this);
- mNotesListAdapter = new NotesListAdapter(getActivity());
- mNotesListView.setAdapter(mNotesListAdapter);
- mAddNewNote = (Button) view.findViewById(R.id.btn_new_note);
- mAddNewNote.setOnClickListener(this);
- mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener());
- mDispatch = false;
- mDispatchY = 0;
- mOriginY = 0;
- mTitleBar = (TextView) view.findViewById(R.id.tv_title_bar);
- mState = ListEditState.NOTE_LIST;
- mModeCallBack = new ModeCallback();
- }
private class ModeCallback implements ListView.MultiChoiceModeListener, MenuItem.OnMenuItemClickListener {
private DropdownMenu mDropDownMenu;
@@ -209,6 +339,8 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
getActivity().getMenuInflater().inflate(R.menu.note_list_options, menu);
menu.findItem(R.id.delete).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.pin_to_top).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.unpin).setOnMenuItemClickListener(this);
mMoveMenu = menu.findItem(R.id.move);
if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER
|| DataUtils.getUserFolderCount(mContentResolver) == 0) {
@@ -220,7 +352,7 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
mActionMode = mode;
mNotesListAdapter.setChoiceMode(true);
mNotesListView.setLongClickable(false);
- mAddNewNote.setVisibility(View.GONE);
+ if (mFab != null) mFab.setVisibility(View.GONE);
View customView = LayoutInflater.from(getActivity()).inflate(
R.layout.note_list_dropdown_menu, null);
@@ -266,7 +398,7 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
public void onDestroyActionMode(ActionMode mode) {
mNotesListAdapter.setChoiceMode(false);
mNotesListView.setLongClickable(true);
- mAddNewNote.setVisibility(View.VISIBLE);
+ if (mFab != null) mFab.setVisibility(View.VISIBLE);
}
public void finishActionMode() {
@@ -306,6 +438,12 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
case R.id.move:
startQueryDestinationFolders();
break;
+ case R.id.pin_to_top:
+ batchSetPinned(true);
+ break;
+ case R.id.unpin:
+ batchSetPinned(false);
+ break;
default:
return false;
}
@@ -313,63 +451,33 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
}
}
- private class NewNoteOnTouchListener implements OnTouchListener {
-
- public boolean onTouch(View v, MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN: {
- Display display = getActivity().getWindowManager().getDefaultDisplay();
- int screenHeight = display.getHeight();
- int newNoteViewHeight = mAddNewNote.getHeight();
- int start = screenHeight - newNoteViewHeight;
- int eventY = start + (int) event.getY();
- if (mState == ListEditState.SUB_FOLDER) {
- eventY -= mTitleBar.getHeight();
- start -= mTitleBar.getHeight();
- }
- if (event.getY() < (event.getX() * (-0.12) + 94)) {
- View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1
- - mNotesListView.getFooterViewsCount());
- if (view != null && view.getBottom() > start
- && (view.getTop() < (start + 94))) {
- mOriginY = (int) event.getY();
- mDispatchY = eventY;
- event.setLocation(event.getX(), mDispatchY);
- mDispatch = true;
- return mNotesListView.dispatchTouchEvent(event);
- }
- }
- break;
- }
- case MotionEvent.ACTION_MOVE: {
- if (mDispatch) {
- mDispatchY += (int) event.getY() - mOriginY;
- event.setLocation(event.getX(), mDispatchY);
- return mNotesListView.dispatchTouchEvent(event);
- }
- break;
- }
- default: {
- if (mDispatch) {
- event.setLocation(event.getX(), mDispatchY);
- mDispatch = false;
- return mNotesListView.dispatchTouchEvent(event);
- }
- break;
- }
- }
- return false;
+ private void batchSetPinned(boolean pinned) {
+ HashSet ids = mNotesListAdapter.getSelectedItemIds();
+ if (ids == null || ids.isEmpty()) return;
+ if (DataUtils.batchSetPinned(mContentResolver, ids, pinned)) {
+ Toast.makeText(getActivity(), pinned ? getString(R.string.toast_pinned) : getString(R.string.toast_unpinned),
+ Toast.LENGTH_SHORT).show();
+ startAsyncNotesListQuery();
+ mModeCallBack.finishActionMode();
}
-
- };
+ }
private void startAsyncNotesListQuery() {
- String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION
- : NORMAL_SELECTION;
+ String selection;
+ String[] selectionArgs;
+ if (mState == ListEditState.PRIVACY_SPACE) {
+ selection = NORMAL_SELECTION;
+ selectionArgs = new String[] { String.valueOf(Notes.ID_PRIVACY_FOLDER) };
+ } else if (mCurrentFolderId == Notes.ID_ROOT_FOLDER) {
+ selection = ROOT_FOLDER_SELECTION;
+ selectionArgs = new String[] { String.valueOf(mCurrentFolderId) };
+ } else {
+ selection = NORMAL_SELECTION;
+ selectionArgs = new String[] { String.valueOf(mCurrentFolderId) };
+ }
mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null,
- Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] {
- String.valueOf(mCurrentFolderId)
- }, NoteColumns.TYPE + " DESC," + NoteColumns.BG_COLOR_ID + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
+ Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, selectionArgs,
+ NoteColumns.PINNED + " DESC," + NoteColumns.TYPE + " DESC," + NoteColumns.BG_COLOR_ID + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
}
private final class BackgroundQueryHandler extends AsyncQueryHandler {
@@ -495,25 +603,26 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
startAsyncNotesListQuery();
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mState = ListEditState.CALL_RECORD_FOLDER;
- mAddNewNote.setVisibility(View.GONE);
+ if (mFab != null) mFab.setVisibility(View.GONE);
} else {
mState = ListEditState.SUB_FOLDER;
+ if (mFab != null) mFab.setVisibility(View.VISIBLE);
}
- if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
- mTitleBar.setText(R.string.call_record_folder_name);
- } else {
- mTitleBar.setText(data.getSnippet());
+ if (mFabBack != null) mFabBack.setVisibility(View.VISIBLE);
+ if (mToolbar != null) {
+ mToolbar.setTitle(data.getId() == Notes.ID_CALL_RECORD_FOLDER
+ ? getString(R.string.call_record_folder_name) : data.getSnippet());
+ }
+ if (mTitleBar != null) {
+ mTitleBar.setText(data.getId() == Notes.ID_CALL_RECORD_FOLDER
+ ? getString(R.string.call_record_folder_name) : data.getSnippet());
+ mTitleBar.setVisibility(View.VISIBLE);
}
- mTitleBar.setVisibility(View.VISIBLE);
}
public void onClick(View v) {
- switch (v.getId()) {
- case R.id.btn_new_note:
- createNewNote();
- break;
- default:
- break;
+ if (v.getId() == R.id.btn_fab_new_note) {
+ createNewNote();
}
}
@@ -554,7 +663,6 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
}
});
- // 把 Dialog 改成 AlertDialog
final AlertDialog dialog = builder.setView(view).show();
final Button positive = (Button)dialog.findViewById(android.R.id.button1);
positive.setOnClickListener(new OnClickListener() {
@@ -604,25 +712,136 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
});
}
- // 重点:Fragment 没有 onBackPressed,这个方法供 MainActivity 调用
- public boolean onBackPressed() {
+ /** 由 MainActivity 在进入隐私空间时调用 */
+ public void onEnterPrivacySpace() {
+ mCurrentFolderId = Notes.ID_PRIVACY_FOLDER;
+ mState = ListEditState.PRIVACY_SPACE;
+ if (getView() != null) {
+ applyPrivacySpaceTheme(getView());
+ }
+ if (mToolbar != null) {
+ mToolbar.setTitle(R.string.privacy_space_title);
+ }
+ if (mTitleBar != null) {
+ mTitleBar.setVisibility(View.GONE);
+ }
+ if (mFabExitPrivacy != null) {
+ mFabExitPrivacy.setVisibility(View.VISIBLE);
+ }
+ startAsyncNotesListQuery();
+ }
+
+ /** 主动退出隐私空间 */
+ private void performExitPrivacySpace() {
+ PrivacySpaceManager.exitPrivacySpace();
+ mCurrentFolderId = Notes.ID_ROOT_FOLDER;
+ mState = ListEditState.NOTE_LIST;
+ if (getView() != null) {
+ restoreNormalTheme(getView());
+ }
+ if (mToolbar != null) {
+ mToolbar.setTitle(R.string.notes_tab_title);
+ }
+ if (mFabExitPrivacy != null) {
+ mFabExitPrivacy.setVisibility(View.GONE);
+ }
+ startAsyncNotesListQuery();
+ if (getActivity() instanceof MainActivity) {
+ ((MainActivity) getActivity()).exitPrivacySpace();
+ }
+ Toast.makeText(getActivity(), R.string.privacy_space_exit, Toast.LENGTH_SHORT).show();
+ }
+
+ private void restoreNormalTheme(View rootView) {
+ if (rootView == null) return;
+ int bgColor = ContextCompat.getColor(requireContext(), R.color.background_light);
+ rootView.setBackgroundColor(bgColor);
+ View listView = rootView.findViewById(R.id.notes_list);
+ if (listView != null) {
+ listView.setBackgroundColor(bgColor);
+ }
+ }
+
+ private void applyPrivacySpaceTheme(View rootView) {
+ if (rootView == null) return;
+ int darkColor = ContextCompat.getColor(requireContext(), R.color.privacy_space_background);
+ rootView.setBackgroundColor(darkColor);
+ View listView = rootView.findViewById(R.id.notes_list);
+ if (listView != null) {
+ listView.setBackgroundColor(darkColor);
+ }
+ }
+
+ /**
+ * 返回上一级便签列表(与 onBackPressed 中子文件夹/通话记录逻辑一致)。
+ * 仅在子文件夹或通话记录文件夹内显示返回按钮时调用。
+ */
+ private void performBackToRoot() {
switch (mState) {
case SUB_FOLDER:
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
+ if (mFabBack != null) mFabBack.setVisibility(View.GONE);
startAsyncNotesListQuery();
- mTitleBar.setVisibility(View.GONE);
- return true; // 消费了返回事件
+ if (mToolbar != null) mToolbar.setTitle(R.string.notes_tab_title);
+ if (mTitleBar != null) mTitleBar.setVisibility(View.GONE);
+ break;
case CALL_RECORD_FOLDER:
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
- mAddNewNote.setVisibility(View.VISIBLE);
- mTitleBar.setVisibility(View.GONE);
+ if (mFabBack != null) mFabBack.setVisibility(View.GONE);
+ if (mFab != null) mFab.setVisibility(View.VISIBLE);
startAsyncNotesListQuery();
- return true; // 消费了返回事件
+ if (mToolbar != null) mToolbar.setTitle(R.string.notes_tab_title);
+ if (mTitleBar != null) mTitleBar.setVisibility(View.GONE);
+ break;
+ default:
+ break;
+ }
+ }
+
+ public boolean onBackPressed() {
+ switch (mState) {
+ case PRIVACY_SPACE:
+ return false;
+ case SUB_FOLDER:
+ performBackToRoot();
+ return true;
+ case CALL_RECORD_FOLDER:
+ performBackToRoot();
+ return true;
case NOTE_LIST:
default:
- return false; // 不消费,让 MainActivity 处理(退出应用)
+ return false;
+ }
+ }
+
+ /** 侧边栏「新建便签」入口 */
+ public void createNewNoteFromDrawer() {
+ createNewNote();
+ }
+
+ /** 侧边栏「新建文件夹」入口 */
+ public void showCreateOrModifyFolderDialogFromDrawer(boolean create) {
+ mFocusNoteDataItem = null;
+ showCreateOrModifyFolderDialog(create);
+ }
+
+ /** 侧边栏「导出」入口 */
+ public void exportNoteToTextFromDrawer() {
+ exportNoteToText();
+ }
+
+ /** 侧边栏「同步」入口 */
+ public void handleSyncFromDrawer() {
+ if (isSyncMode()) {
+ if (GTaskSyncService.isSyncing()) {
+ GTaskSyncService.cancelSync(getActivity());
+ } else {
+ GTaskSyncService.startSync(getActivity());
+ }
+ } else {
+ startPreferenceActivity();
}
}
@@ -650,6 +869,11 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
if (mFocusNoteDataItem != null) {
menu.setHeaderTitle(mFocusNoteDataItem.getSnippet());
menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view);
+ if (mFocusNoteDataItem.isPinned()) {
+ menu.add(0, MENU_FOLDER_UNPIN, 0, R.string.menu_unpin);
+ } else {
+ menu.add(0, MENU_FOLDER_PIN, 0, R.string.menu_pin_to_top);
+ }
menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete);
menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name);
}
@@ -683,73 +907,30 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
case MENU_FOLDER_CHANGE_NAME:
showCreateOrModifyFolderDialog(false);
break;
- default:
- break;
- }
-
- return true;
- }
-
- @Override
- public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
- menu.clear();
- if (mState == ListEditState.NOTE_LIST) {
- inflater.inflate(R.menu.note_list, menu);
- menu.findItem(R.id.menu_sync).setTitle(
- GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync);
- } else if (mState == ListEditState.SUB_FOLDER) {
- inflater.inflate(R.menu.sub_folder, menu);
- } else if (mState == ListEditState.CALL_RECORD_FOLDER) {
- inflater.inflate(R.menu.call_record_folder, menu);
- } else {
- Log.e(TAG, "Wrong state:" + mState);
- }
- }
-
- @Override
- public boolean onOptionsItemSelected(@NonNull MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_new_folder: {
- showCreateOrModifyFolderDialog(true);
- break;
- }
- case R.id.menu_export_text: {
- exportNoteToText();
- break;
- }
- case R.id.menu_sync: {
- if (isSyncMode()) {
- if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) {
- GTaskSyncService.startSync(getActivity());
- } else {
- GTaskSyncService.cancelSync(getActivity());
+ case MENU_FOLDER_PIN:
+ if (mFocusNoteDataItem != null && mFocusNoteDataItem.getId() > 0) {
+ HashSet ids = new HashSet();
+ ids.add(mFocusNoteDataItem.getId());
+ if (DataUtils.batchSetPinned(mContentResolver, ids, true)) {
+ Toast.makeText(getActivity(), getString(R.string.toast_pinned), Toast.LENGTH_SHORT).show();
+ startAsyncNotesListQuery();
}
- } else {
- startPreferenceActivity();
}
break;
- }
- case R.id.menu_setting: {
- startPreferenceActivity();
- break;
- }
- case R.id.menu_new_note: {
- createNewNote();
- break;
- }
- case R.id.menu_search:
- onSearchRequested();
+ case MENU_FOLDER_UNPIN:
+ if (mFocusNoteDataItem != null && mFocusNoteDataItem.getId() > 0) {
+ HashSet ids = new HashSet();
+ ids.add(mFocusNoteDataItem.getId());
+ if (DataUtils.batchSetPinned(mContentResolver, ids, false)) {
+ Toast.makeText(getActivity(), getString(R.string.toast_unpinned), Toast.LENGTH_SHORT).show();
+ startAsyncNotesListQuery();
+ }
+ }
break;
default:
break;
}
- return true;
- }
- public boolean onSearchRequested() {
- // Fragment 没有 startSearch,需要调用 Activity 的
- startActivity(new Intent(getActivity(), NotesListActivity.class).setAction(Intent.ACTION_SEARCH));
- // 实际上 Fragment 很难直接调用系统的 SearchDialog 并回调,这里简单处理
return true;
}
@@ -837,6 +1018,11 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
Log.e(TAG, "Wrong note type in SUB_FOLDER");
}
break;
+ case PRIVACY_SPACE:
+ if (item.getType() == Notes.TYPE_NOTE) {
+ openNode(item);
+ }
+ break;
default:
break;
}
@@ -845,9 +1031,25 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
}
+ public boolean onItemLongClick(AdapterView> parent, View view, int position, long id) {
+ if (view instanceof NotesListItem) {
+ mFocusNoteDataItem = ((NotesListItem) view).getItemData();
+ if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) {
+ if (mNotesListView.startActionMode(mModeCallBack) != null) {
+ mModeCallBack.onItemCheckedStateChanged(null, position, id, true);
+ mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ } else {
+ Log.e(TAG, "startActionMode fails");
+ }
+ } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) {
+ mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener);
+ }
+ }
+ return false;
+ }
private void startQueryDestinationFolders() {
String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?";
- selection = (mState == ListEditState.NOTE_LIST) ? selection:
+ selection = (mState == ListEditState.NOTE_LIST) ? selection :
"(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")";
mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN,
@@ -863,20 +1065,6 @@ public class NotesListFragment extends Fragment implements OnClickListener, OnIt
NoteColumns.MODIFIED_DATE + " DESC");
}
- public boolean onItemLongClick(AdapterView> parent, View view, int position, long id) {
- if (view instanceof NotesListItem) {
- mFocusNoteDataItem = ((NotesListItem) view).getItemData();
- if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) {
- if (mNotesListView.startActionMode(mModeCallBack) != null) {
- mModeCallBack.onItemCheckedStateChanged(null, position, id, true);
- mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
- } else {
- Log.e(TAG, "startActionMode fails");
- }
- } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) {
- mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener);
- }
- }
- return false;
- }
-}
+
+
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/ui/NotesListItem.java b/src/app/src/main/java/net/micode/notes/ui/NotesListItem.java
similarity index 88%
rename from src/src/net/micode/notes/ui/NotesListItem.java
rename to src/app/src/main/java/net/micode/notes/ui/NotesListItem.java
index 84b1571..8cc504b 100644
--- a/src/src/net/micode/notes/ui/NotesListItem.java
+++ b/src/app/src/main/java/net/micode/notes/ui/NotesListItem.java
@@ -8,6 +8,8 @@ package net.micode.notes.ui;
import android.content.Context;
import android.text.format.DateUtils;
import android.view.View;
+
+import androidx.core.content.ContextCompat;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
@@ -17,6 +19,7 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
+import net.micode.notes.tool.ResourceParser;
/**
* 便签列表项视图 (UI Component)
@@ -34,6 +37,7 @@ public class NotesListItem extends LinearLayout {
private TextView mCallName; // 联系人名称 (仅通话记录便签显示)
private NoteItemData mItemData;
private CheckBox mCheckBox; // 批量选择模式下的复选框
+ private View mPinnedBar; // 置顶时左侧加深色条
public NotesListItem(Context context) {
super(context);
@@ -44,6 +48,7 @@ public class NotesListItem extends LinearLayout {
mTime = (TextView) findViewById(R.id.tv_time);
mCallName = (TextView) findViewById(R.id.tv_name);
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
+ mPinnedBar = findViewById(R.id.note_pinned_bar);
}
/**
@@ -116,6 +121,17 @@ public class NotesListItem extends LinearLayout {
// 设置相对时间显示 (例如 "刚刚", "昨天")
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
+ // 置顶时显示左侧加深色条
+ if (data.isPinned() && mPinnedBar != null) {
+ mPinnedBar.setVisibility(View.VISIBLE);
+ int colorResId = (data.getType() == Notes.TYPE_FOLDER || data.getType() == Notes.TYPE_SYSTEM)
+ ? ResourceParser.NoteItemBgResources.getPinnedBarColorRes(ResourceParser.YELLOW)
+ : ResourceParser.NoteItemBgResources.getPinnedBarColorRes(data.getBgColorId());
+ mPinnedBar.setBackgroundColor(ContextCompat.getColor(context, colorResId));
+ } else if (mPinnedBar != null) {
+ mPinnedBar.setVisibility(View.GONE);
+ }
+
// 设置动态背景
setBackground(data);
}
diff --git a/src/src/net/micode/notes/ui/NotesPreferenceActivity.java b/src/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java
similarity index 100%
rename from src/src/net/micode/notes/ui/NotesPreferenceActivity.java
rename to src/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java
diff --git a/src/app/src/main/java/net/micode/notes/ui/SplashActivity.java b/src/app/src/main/java/net/micode/notes/ui/SplashActivity.java
new file mode 100644
index 0000000..8c35f3a
--- /dev/null
+++ b/src/app/src/main/java/net/micode/notes/ui/SplashActivity.java
@@ -0,0 +1,168 @@
+package net.micode.notes.ui;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import net.micode.notes.R;
+
+/**
+ * 开场动画页
+ * 简约高级感:深色背景 + Logo 缩放淡入 + 标题淡入 + 细线点缀,短暂停留后淡出并进入主界面。
+ * 已做防闪退与 ANR:空指针检查、防重复跳转、安全超时、使用 post 替代 GlobalLayoutListener。
+ */
+public class SplashActivity extends AppCompatActivity {
+
+ private static final int PHASE_ENTER_MS = 900;
+ private static final int HOLD_MS = 1100;
+ private static final int PHASE_EXIT_MS = 450;
+ /** 最大等待时间,超时则直接进入主界面,防止卡死 */
+ private static final int SAFETY_TIMEOUT_MS = 4000;
+
+ private View mContentRoot;
+ private ImageView mLogo;
+ private TextView mTitle;
+ private View mAccent;
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private boolean mEnterAnimationStarted;
+ private boolean mGoMainCalled;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_splash);
+
+ mContentRoot = findViewById(R.id.splash_content);
+ mLogo = findViewById(R.id.splash_logo);
+ mTitle = findViewById(R.id.splash_title);
+ mAccent = findViewById(R.id.splash_accent);
+
+ if (mContentRoot == null || mLogo == null || mTitle == null || mAccent == null) {
+ goToMain();
+ return;
+ }
+
+ if (mLogo != null) {
+ mLogo.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN);
+ }
+
+ // 使用 post 在下一帧启动动画,避免与 GlobalLayoutListener 的 API 差异导致不触发
+ mContentRoot.post(this::startEnterAnimation);
+
+ // 安全超时:若动画或跳转异常,最多等待 SAFETY_TIMEOUT_MS 后强制进入主界面
+ mHandler.postDelayed(this::goToMain, SAFETY_TIMEOUT_MS);
+ }
+
+ private void startEnterAnimation() {
+ if (mEnterAnimationStarted || isFinishing()) return;
+ if (mLogo == null || mTitle == null || mAccent == null) {
+ goToMain();
+ return;
+ }
+ mEnterAnimationStarted = true;
+
+ // 1. Logo:缩放 + 淡入
+ mLogo.setScaleX(0.82f);
+ mLogo.setScaleY(0.82f);
+ mLogo.setAlpha(0f);
+
+ ObjectAnimator scaleX = ObjectAnimator.ofFloat(mLogo, View.SCALE_X, 0.82f, 1f);
+ ObjectAnimator scaleY = ObjectAnimator.ofFloat(mLogo, View.SCALE_Y, 0.82f, 1f);
+ ObjectAnimator alphaLogo = ObjectAnimator.ofFloat(mLogo, View.ALPHA, 0f, 1f);
+ scaleX.setDuration(520);
+ scaleY.setDuration(520);
+ alphaLogo.setDuration(420);
+ AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
+ scaleX.setInterpolator(interpolator);
+ scaleY.setInterpolator(interpolator);
+ alphaLogo.setInterpolator(interpolator);
+ scaleX.start();
+ scaleY.start();
+ alphaLogo.start();
+
+ // 2. 标题:延迟淡入 + 上移
+ mTitle.setAlpha(0f);
+ mTitle.setTranslationY(12f);
+ mTitle.animate()
+ .alpha(1f)
+ .translationY(0f)
+ .setStartDelay(280)
+ .setDuration(400)
+ .setInterpolator(interpolator)
+ .start();
+
+ // 3. 底部细线:淡入
+ mAccent.setAlpha(0f);
+ mAccent.animate()
+ .alpha(1f)
+ .setStartDelay(460)
+ .setDuration(320)
+ .setInterpolator(new LinearInterpolator())
+ .start();
+
+ // 4. 停留后退场并跳转
+ mHandler.postDelayed(this::startExitAnimation, PHASE_ENTER_MS + HOLD_MS);
+ }
+
+ private void startExitAnimation() {
+ if (isFinishing() || mGoMainCalled) return;
+ if (mContentRoot == null) {
+ goToMain();
+ return;
+ }
+ mContentRoot.animate()
+ .alpha(0f)
+ .setDuration(PHASE_EXIT_MS)
+ .setInterpolator(new AccelerateDecelerateInterpolator())
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mContentRoot != null) {
+ mContentRoot.animate().setListener(null);
+ }
+ goToMain();
+ }
+ })
+ .start();
+ }
+
+ private void goToMain() {
+ if (mGoMainCalled) return;
+ mGoMainCalled = true;
+ mHandler.removeCallbacksAndMessages(null);
+
+ if (isFinishing()) return;
+
+ try {
+ startActivity(new Intent(this, MainActivity.class));
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+ } catch (Exception e) {
+ overridePendingTransition(0, 0);
+ }
+ finish();
+ }
+
+ @Override
+ protected void onDestroy() {
+ mHandler.removeCallbacksAndMessages(null);
+ mContentRoot = null;
+ mLogo = null;
+ mTitle = null;
+ mAccent = null;
+ super.onDestroy();
+ }
+}
diff --git a/src/app/src/main/java/net/micode/notes/ui/TodoEditActivity.java b/src/app/src/main/java/net/micode/notes/ui/TodoEditActivity.java
new file mode 100644
index 0000000..4757b93
--- /dev/null
+++ b/src/app/src/main/java/net/micode/notes/ui/TodoEditActivity.java
@@ -0,0 +1,215 @@
+package net.micode.notes.ui;
+
+/**
+ * 待办编辑/新建页面
+ *
+ * 支持新增或编辑待办事项,可设置时间提醒(DatePicker + TimePicker)或地点提醒(Geofence)。
+ * 保存时调用 TodoReminderManager 注册 AlarmManager/Geofencing 提醒。
+ *
+ *
+ * @see TodoItem
+ * @see TodoReminderManager
+ * @see TodoDao
+ */
+import android.Manifest;
+import android.app.DatePickerDialog;
+import android.app.TimePickerDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+
+import android.widget.EditText;
+
+import net.micode.notes.R;
+import net.micode.notes.data.TodoDao;
+import net.micode.notes.todo.TodoItem;
+import net.micode.notes.todo.TodoReminderManager;
+
+import java.util.Calendar;
+
+public class TodoEditActivity extends AppCompatActivity {
+ private static final String EXTRA_TODO_ID = "todo_id";
+
+ private EditText mEtContent;
+ private Button mBtnAddReminder;
+ private TextView mTvReminderInfo;
+ private Button mBtnSave;
+
+ private TodoDao mTodoDao;
+ private TodoItem mItem;
+ private boolean mIsEdit;
+
+ private final ActivityResultLauncher mLocationPermissionLauncher =
+ registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
+ if (Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))) {
+ applyMockLocation();
+ } else {
+ Toast.makeText(this, "需要位置权限才能设置地点提醒", Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ public static Intent newIntent(Context context) {
+ return new Intent(context, TodoEditActivity.class);
+ }
+
+ public static Intent newIntent(Context context, long todoId) {
+ Intent i = new Intent(context, TodoEditActivity.class);
+ i.putExtra(EXTRA_TODO_ID, todoId);
+ return i;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_todo_edit);
+ if (getSupportActionBar() != null) getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ mTodoDao = new TodoDao(this);
+ Intent intent = getIntent();
+ long todoId = (intent != null) ? intent.getLongExtra(EXTRA_TODO_ID, 0) : 0;
+ mIsEdit = todoId > 0;
+ mItem = mIsEdit ? mTodoDao.getById(todoId) : new TodoItem();
+ if (mItem == null) mItem = new TodoItem();
+
+ mEtContent = findViewById(R.id.et_todo_content);
+ mBtnAddReminder = findViewById(R.id.btn_add_reminder);
+ mTvReminderInfo = findViewById(R.id.tv_reminder_info);
+ mBtnSave = findViewById(R.id.btn_save);
+
+ if (mIsEdit) mEtContent.setText(mItem.getContent());
+ updateReminderInfo();
+
+ mBtnAddReminder.setOnClickListener(v -> showReminderOptions());
+ mBtnSave.setOnClickListener(v -> save());
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ finish();
+ return true;
+ }
+
+ private void showReminderOptions() {
+ String[] options = new String[]{
+ getString(R.string.todo_reminder_time),
+ getString(R.string.todo_reminder_location),
+ getString(R.string.todo_reminder_none)
+ };
+ new android.app.AlertDialog.Builder(this)
+ .setTitle(R.string.todo_add_reminder)
+ .setItems(options, (dialog, which) -> {
+ if (which == 0) pickTime();
+ else if (which == 1) pickLocation();
+ else clearReminder();
+ })
+ .show();
+ }
+
+ private void clearReminder() {
+ mItem.setReminderType(TodoItem.REMINDER_NONE);
+ mItem.setReminderTimestamp(0);
+ mItem.setLatitude(0);
+ mItem.setLongitude(0);
+ mItem.setLocationName("");
+ updateReminderInfo();
+ }
+
+ private void pickTime() {
+ Calendar cal = Calendar.getInstance();
+ if (mItem.getReminderTimestamp() > 0) cal.setTimeInMillis(mItem.getReminderTimestamp());
+
+ DatePickerDialog dateDialog = new DatePickerDialog(this,
+ (v, year, month, dayOfMonth) -> {
+ cal.set(year, month, dayOfMonth);
+ TimePickerDialog timeDialog = new TimePickerDialog(this,
+ (tv, hour, minute) -> {
+ cal.set(Calendar.HOUR_OF_DAY, hour);
+ cal.set(Calendar.MINUTE, minute);
+ cal.set(Calendar.SECOND, 0);
+ mItem.setReminderType(TodoItem.REMINDER_TIME);
+ mItem.setReminderTimestamp(cal.getTimeInMillis());
+ mItem.setLatitude(0);
+ mItem.setLongitude(0);
+ mItem.setLocationName("");
+ updateReminderInfo();
+ },
+ cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE),
+ DateFormat.is24HourFormat(this));
+ timeDialog.show();
+ },
+ cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH));
+ dateDialog.show();
+ }
+
+ private void pickLocation() {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ mLocationPermissionLauncher.launch(new String[]{
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION
+ });
+ } else {
+ mLocationPermissionLauncher.launch(new String[]{Manifest.permission.ACCESS_FINE_LOCATION});
+ }
+ } else {
+ applyMockLocation();
+ }
+ }
+
+ private void applyMockLocation() {
+ // 模拟选点:返回虚拟坐标和地点名称(无真实地图 SDK)
+ mItem.setReminderType(TodoItem.REMINDER_LOCATION);
+ mItem.setReminderTimestamp(0);
+ mItem.setLatitude(39.9042);
+ mItem.setLongitude(116.4074);
+ mItem.setLocationName("附近超市");
+ updateReminderInfo();
+ Toast.makeText(this, "已设置地点提醒:附近超市", Toast.LENGTH_SHORT).show();
+ }
+
+ private void updateReminderInfo() {
+ if (mItem.getReminderType() == TodoItem.REMINDER_TIME && mItem.getReminderTimestamp() > 0) {
+ mTvReminderInfo.setVisibility(android.view.View.VISIBLE);
+ mTvReminderInfo.setText("时间提醒: " + DateFormat.format("yyyy-MM-dd HH:mm", mItem.getReminderTimestamp()));
+ } else if (mItem.getReminderType() == TodoItem.REMINDER_LOCATION) {
+ mTvReminderInfo.setVisibility(android.view.View.VISIBLE);
+ mTvReminderInfo.setText("地点提醒: " + mItem.getLocationName());
+ } else {
+ mTvReminderInfo.setVisibility(android.view.View.GONE);
+ }
+ }
+
+ private void save() {
+ String content = mEtContent.getText() != null ? mEtContent.getText().toString().trim() : "";
+ if (TextUtils.isEmpty(content)) {
+ Toast.makeText(this, "请输入待办内容", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ mItem.setContent(content);
+ if (mItem.getCreatedTime() == 0) mItem.setCreatedTime(System.currentTimeMillis());
+
+ if (mIsEdit && mItem.getId() > 0) {
+ mTodoDao.update(mItem);
+ } else {
+ long id = mTodoDao.insert(mItem);
+ mItem.setId(id);
+ }
+
+ TodoReminderManager.scheduleReminders(this, mItem);
+ setResult(RESULT_OK);
+ finish();
+ }
+}
diff --git a/src/app/src/main/java/net/micode/notes/ui/TodoFragment.java b/src/app/src/main/java/net/micode/notes/ui/TodoFragment.java
new file mode 100644
index 0000000..7e2dc0f
--- /dev/null
+++ b/src/app/src/main/java/net/micode/notes/ui/TodoFragment.java
@@ -0,0 +1,238 @@
+package net.micode.notes.ui;
+
+/**
+ * 待办事项列表 Fragment
+ *
+ * 主界面底部导航「待办」页,展示未完成与已完成待办,支持增删改查、时间/地点提醒。
+ * 提供游戏化反馈:全部完成后触发震动、提示音、撒花动画。
+ * 长按进入多选模式,可批量删除。
+ *
+ *
+ * @see TodoAdapter
+ * @see TodoEditActivity
+ * @see TodoReminderManager
+ */
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+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.appbar.MaterialToolbar;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import net.micode.notes.R;
+import net.micode.notes.data.TodoDao;
+import net.micode.notes.todo.TodoReminderManager;
+import net.micode.notes.todo.ConfettiView;
+import net.micode.notes.todo.TodoAdapter;
+import net.micode.notes.todo.TodoItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TodoFragment extends Fragment {
+ private static final int REQUEST_EDIT = 2001;
+
+ private RecyclerView mRecyclerView;
+ private TodoAdapter mAdapter;
+ private FloatingActionButton mFab;
+ private MaterialToolbar mToolbar;
+ private ConfettiView mConfettiView;
+
+ private TodoDao mTodoDao;
+ private List mUndoneList = new ArrayList<>();
+ private List mDoneList = new ArrayList<>();
+
+ private ActionModeCallback mActionModeCallback;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_todo, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mTodoDao = new TodoDao(requireContext());
+ mRecyclerView = view.findViewById(R.id.recycler_todo);
+ mFab = view.findViewById(R.id.fab_add_todo);
+ mToolbar = view.findViewById(R.id.toolbar_todo);
+ mConfettiView = view.findViewById(R.id.confetti_view);
+
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
+ mAdapter = new TodoAdapter();
+ mRecyclerView.setAdapter(mAdapter);
+
+ mAdapter.setOnItemClickListener(new TodoAdapter.OnItemClickListener() {
+ @Override
+ public void onItemClick(TodoItem item, int position) {
+ if (!mAdapter.isChoiceMode()) {
+ Intent intent = TodoEditActivity.newIntent(requireContext(), item.getId());
+ startActivityForResult(intent, REQUEST_EDIT);
+ }
+ }
+
+ @Override
+ public void onCheckChanged(TodoItem item, boolean checked) {
+ if (mAdapter.isChoiceMode()) return;
+ if (item == null || item.getId() <= 0) return;
+ item.setDone(checked);
+ mTodoDao.update(item);
+ refreshData();
+ if (checked) {
+ checkAndTriggerGamification();
+ }
+ }
+ });
+
+ mAdapter.setOnItemLongClickListener((item, position) -> {
+ if (!mAdapter.isChoiceMode() && mActionModeCallback == null) {
+ mActionModeCallback = new ActionModeCallback();
+ requireActivity().startActionMode(mActionModeCallback);
+ mAdapter.setChoiceMode(true);
+ mAdapter.setChecked(position, true);
+ mFab.setVisibility(View.GONE);
+ }
+ });
+
+ mFab.setOnClickListener(v -> {
+ if (getActivity() == null || !isAdded()) return;
+ Intent intent = new Intent(getActivity(), TodoEditActivity.class);
+ startActivityForResult(intent, REQUEST_EDIT);
+ });
+
+ if (mToolbar != null && getActivity() instanceof MainActivity) {
+ mToolbar.setNavigationOnClickListener(v -> ((MainActivity) getActivity()).openDrawer());
+ }
+
+ mConfettiView.setOnClickListener(v -> mConfettiView.setVisibility(View.GONE));
+
+ refreshData();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_EDIT && resultCode == Activity.RESULT_OK) {
+ refreshData();
+ }
+ }
+
+ private void refreshData() {
+ mUndoneList = mTodoDao.getUndone();
+ mDoneList = mTodoDao.getDone();
+ mAdapter.setData(mUndoneList, mDoneList);
+ }
+
+ private void checkAndTriggerGamification() {
+ int undoneCount = mTodoDao.getUndoneCount();
+ if (undoneCount == 0) {
+ triggerGamification();
+ }
+ }
+
+ private void triggerGamification() {
+ vibrate();
+ playSuccessSound();
+ showConfetti();
+ }
+
+ private void vibrate() {
+ try {
+ if (getContext() == null) return;
+ Vibrator v = (Vibrator) getContext().getSystemService(android.content.Context.VIBRATOR_SERVICE);
+ if (v != null && v.hasVibrator()) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ v.vibrate(VibrationEffect.createOneShot(150, VibrationEffect.DEFAULT_AMPLITUDE));
+ } else {
+ v.vibrate(150);
+ }
+ }
+ } catch (Exception e) { /* ignore */ }
+ }
+
+ /** 撒花时的短促有力提示音(系统 ToneGenerator,无需 raw 资源) */
+ private static final int SUCCESS_TONE_DURATION_MS = 120;
+
+ private void playSuccessSound() {
+ try {
+ Context ctx = getContext();
+ if (ctx == null) return;
+ ToneGenerator tone = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 85);
+ tone.startTone(ToneGenerator.TONE_PROP_ACK, SUCCESS_TONE_DURATION_MS);
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ try {
+ tone.release();
+ } catch (Exception ignored) { }
+ }, SUCCESS_TONE_DURATION_MS + 80);
+ } catch (Exception e) { /* 无扬声器或权限时静默 */ }
+ }
+
+ private void showConfetti() {
+ if (mConfettiView != null) {
+ mConfettiView.post(() -> {
+ mConfettiView.start();
+ });
+ }
+ }
+
+ private class ActionModeCallback implements android.view.ActionMode.Callback {
+ @Override
+ public boolean onCreateActionMode(android.view.ActionMode mode, android.view.Menu menu) {
+ requireActivity().getMenuInflater().inflate(R.menu.todo_list_options, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(android.view.ActionMode mode, android.view.Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(android.view.ActionMode mode, android.view.MenuItem item) {
+ if (item.getItemId() == R.id.delete) {
+ List ids = mAdapter.getSelectedIds();
+ if (ids.isEmpty()) {
+ Toast.makeText(requireContext(), R.string.menu_select_none, Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ new android.app.AlertDialog.Builder(requireContext())
+ .setTitle(R.string.alert_title_delete)
+ .setMessage(getString(R.string.todo_delete_confirm, ids.size()))
+ .setPositiveButton(android.R.string.ok, (d, w) -> {
+ for (long id : ids) TodoReminderManager.cancelAllForTodo(requireContext(), id);
+ mTodoDao.deleteByIds(ids);
+ refreshData();
+ mode.finish();
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ }
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(android.view.ActionMode mode) {
+ mAdapter.setChoiceMode(false);
+ mFab.setVisibility(View.VISIBLE);
+ mActionModeCallback = null;
+ }
+ }
+}
diff --git a/src/src/net/micode/notes/widget/NoteWidgetProvider.java b/src/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider.java
similarity index 100%
rename from src/src/net/micode/notes/widget/NoteWidgetProvider.java
rename to src/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider.java
diff --git a/src/src/net/micode/notes/widget/NoteWidgetProvider_2x.java b/src/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider_2x.java
similarity index 100%
rename from src/src/net/micode/notes/widget/NoteWidgetProvider_2x.java
rename to src/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider_2x.java
diff --git a/src/src/net/micode/notes/widget/NoteWidgetProvider_4x.java b/src/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider_4x.java
similarity index 100%
rename from src/src/net/micode/notes/widget/NoteWidgetProvider_4x.java
rename to src/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider_4x.java
diff --git a/src/app/src/main/res/color/bottom_nav_color.xml b/src/app/src/main/res/color/bottom_nav_color.xml
new file mode 100644
index 0000000..aeb73c4
--- /dev/null
+++ b/src/app/src/main/res/color/bottom_nav_color.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/res/color/primary_text_dark.xml b/src/app/src/main/res/color/primary_text_dark.xml
similarity index 100%
rename from src/res/color/primary_text_dark.xml
rename to src/app/src/main/res/color/primary_text_dark.xml
diff --git a/src/res/color/secondary_text_dark.xml b/src/app/src/main/res/color/secondary_text_dark.xml
similarity index 100%
rename from src/res/color/secondary_text_dark.xml
rename to src/app/src/main/res/color/secondary_text_dark.xml
diff --git a/src/res/drawable-hdpi/bg_btn_set_color.png b/src/app/src/main/res/drawable-hdpi/bg_btn_set_color.png
similarity index 100%
rename from src/res/drawable-hdpi/bg_btn_set_color.png
rename to src/app/src/main/res/drawable-hdpi/bg_btn_set_color.png
diff --git a/src/res/drawable-hdpi/bg_color_btn_mask.png b/src/app/src/main/res/drawable-hdpi/bg_color_btn_mask.png
similarity index 100%
rename from src/res/drawable-hdpi/bg_color_btn_mask.png
rename to src/app/src/main/res/drawable-hdpi/bg_color_btn_mask.png
diff --git a/src/res/drawable-hdpi/call_record.png b/src/app/src/main/res/drawable-hdpi/call_record.png
similarity index 100%
rename from src/res/drawable-hdpi/call_record.png
rename to src/app/src/main/res/drawable-hdpi/call_record.png
diff --git a/src/res/drawable-hdpi/clock.png b/src/app/src/main/res/drawable-hdpi/clock.png
similarity index 100%
rename from src/res/drawable-hdpi/clock.png
rename to src/app/src/main/res/drawable-hdpi/clock.png
diff --git a/src/res/drawable-hdpi/delete.png b/src/app/src/main/res/drawable-hdpi/delete.png
similarity index 100%
rename from src/res/drawable-hdpi/delete.png
rename to src/app/src/main/res/drawable-hdpi/delete.png
diff --git a/src/res/drawable-hdpi/dropdown_icon.9.png b/src/app/src/main/res/drawable-hdpi/dropdown_icon.9.png
similarity index 100%
rename from src/res/drawable-hdpi/dropdown_icon.9.png
rename to src/app/src/main/res/drawable-hdpi/dropdown_icon.9.png
diff --git a/src/res/drawable-hdpi/edit_blue.9.png b/src/app/src/main/res/drawable-hdpi/edit_blue.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_blue.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_blue.9.png
diff --git a/src/res/drawable-hdpi/edit_green.9.png b/src/app/src/main/res/drawable-hdpi/edit_green.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_green.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_green.9.png
diff --git a/src/res/drawable-hdpi/edit_red.9.png b/src/app/src/main/res/drawable-hdpi/edit_red.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_red.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_red.9.png
diff --git a/src/res/drawable-hdpi/edit_title_blue.9.png b/src/app/src/main/res/drawable-hdpi/edit_title_blue.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_title_blue.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_title_blue.9.png
diff --git a/src/res/drawable-hdpi/edit_title_green.9.png b/src/app/src/main/res/drawable-hdpi/edit_title_green.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_title_green.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_title_green.9.png
diff --git a/src/res/drawable-hdpi/edit_title_red.9.png b/src/app/src/main/res/drawable-hdpi/edit_title_red.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_title_red.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_title_red.9.png
diff --git a/src/res/drawable-hdpi/edit_title_white.9.png b/src/app/src/main/res/drawable-hdpi/edit_title_white.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_title_white.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_title_white.9.png
diff --git a/src/res/drawable-hdpi/edit_title_yellow.9.png b/src/app/src/main/res/drawable-hdpi/edit_title_yellow.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_title_yellow.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_title_yellow.9.png
diff --git a/src/res/drawable-hdpi/edit_white.9.png b/src/app/src/main/res/drawable-hdpi/edit_white.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_white.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_white.9.png
diff --git a/src/res/drawable-hdpi/edit_yellow.9.png b/src/app/src/main/res/drawable-hdpi/edit_yellow.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_yellow.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_yellow.9.png
diff --git a/src/res/drawable-hdpi/font_large.png b/src/app/src/main/res/drawable-hdpi/font_large.png
similarity index 100%
rename from src/res/drawable-hdpi/font_large.png
rename to src/app/src/main/res/drawable-hdpi/font_large.png
diff --git a/src/res/drawable-hdpi/font_normal.png b/src/app/src/main/res/drawable-hdpi/font_normal.png
similarity index 100%
rename from src/res/drawable-hdpi/font_normal.png
rename to src/app/src/main/res/drawable-hdpi/font_normal.png
diff --git a/src/res/drawable-hdpi/font_size_selector_bg.9.png b/src/app/src/main/res/drawable-hdpi/font_size_selector_bg.9.png
similarity index 100%
rename from src/res/drawable-hdpi/font_size_selector_bg.9.png
rename to src/app/src/main/res/drawable-hdpi/font_size_selector_bg.9.png
diff --git a/src/res/drawable-hdpi/font_small.png b/src/app/src/main/res/drawable-hdpi/font_small.png
similarity index 100%
rename from src/res/drawable-hdpi/font_small.png
rename to src/app/src/main/res/drawable-hdpi/font_small.png
diff --git a/src/res/drawable-hdpi/font_super.png b/src/app/src/main/res/drawable-hdpi/font_super.png
similarity index 100%
rename from src/res/drawable-hdpi/font_super.png
rename to src/app/src/main/res/drawable-hdpi/font_super.png
diff --git a/src/res/drawable-hdpi/icon_app.png b/src/app/src/main/res/drawable-hdpi/icon_app.png
similarity index 100%
rename from src/res/drawable-hdpi/icon_app.png
rename to src/app/src/main/res/drawable-hdpi/icon_app.png
diff --git a/src/res/drawable-hdpi/list_background.png b/src/app/src/main/res/drawable-hdpi/list_background.png
similarity index 100%
rename from src/res/drawable-hdpi/list_background.png
rename to src/app/src/main/res/drawable-hdpi/list_background.png
diff --git a/src/res/drawable-hdpi/list_blue_down.9.png b/src/app/src/main/res/drawable-hdpi/list_blue_down.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_blue_down.9.png
rename to src/app/src/main/res/drawable-hdpi/list_blue_down.9.png
diff --git a/src/res/drawable-hdpi/list_blue_middle.9.png b/src/app/src/main/res/drawable-hdpi/list_blue_middle.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_blue_middle.9.png
rename to src/app/src/main/res/drawable-hdpi/list_blue_middle.9.png
diff --git a/src/res/drawable-hdpi/list_blue_single.9.png b/src/app/src/main/res/drawable-hdpi/list_blue_single.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_blue_single.9.png
rename to src/app/src/main/res/drawable-hdpi/list_blue_single.9.png
diff --git a/src/res/drawable-hdpi/list_blue_up.9.png b/src/app/src/main/res/drawable-hdpi/list_blue_up.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_blue_up.9.png
rename to src/app/src/main/res/drawable-hdpi/list_blue_up.9.png
diff --git a/src/res/drawable-hdpi/list_folder.9.png b/src/app/src/main/res/drawable-hdpi/list_folder.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_folder.9.png
rename to src/app/src/main/res/drawable-hdpi/list_folder.9.png
diff --git a/src/res/drawable-hdpi/list_footer_bg.9.png b/src/app/src/main/res/drawable-hdpi/list_footer_bg.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_footer_bg.9.png
rename to src/app/src/main/res/drawable-hdpi/list_footer_bg.9.png
diff --git a/src/res/drawable-hdpi/list_green_down.9.png b/src/app/src/main/res/drawable-hdpi/list_green_down.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_green_down.9.png
rename to src/app/src/main/res/drawable-hdpi/list_green_down.9.png
diff --git a/src/res/drawable-hdpi/list_green_middle.9.png b/src/app/src/main/res/drawable-hdpi/list_green_middle.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_green_middle.9.png
rename to src/app/src/main/res/drawable-hdpi/list_green_middle.9.png
diff --git a/src/res/drawable-hdpi/list_green_single.9.png b/src/app/src/main/res/drawable-hdpi/list_green_single.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_green_single.9.png
rename to src/app/src/main/res/drawable-hdpi/list_green_single.9.png
diff --git a/src/res/drawable-hdpi/list_green_up.9.png b/src/app/src/main/res/drawable-hdpi/list_green_up.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_green_up.9.png
rename to src/app/src/main/res/drawable-hdpi/list_green_up.9.png
diff --git a/src/res/drawable-hdpi/list_red_down.9.png b/src/app/src/main/res/drawable-hdpi/list_red_down.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_red_down.9.png
rename to src/app/src/main/res/drawable-hdpi/list_red_down.9.png
diff --git a/src/res/drawable-hdpi/list_red_middle.9.png b/src/app/src/main/res/drawable-hdpi/list_red_middle.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_red_middle.9.png
rename to src/app/src/main/res/drawable-hdpi/list_red_middle.9.png
diff --git a/src/res/drawable-hdpi/list_red_single.9.png b/src/app/src/main/res/drawable-hdpi/list_red_single.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_red_single.9.png
rename to src/app/src/main/res/drawable-hdpi/list_red_single.9.png
diff --git a/src/res/drawable-hdpi/list_red_up.9.png b/src/app/src/main/res/drawable-hdpi/list_red_up.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_red_up.9.png
rename to src/app/src/main/res/drawable-hdpi/list_red_up.9.png
diff --git a/src/res/drawable-hdpi/list_white_down.9.png b/src/app/src/main/res/drawable-hdpi/list_white_down.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_white_down.9.png
rename to src/app/src/main/res/drawable-hdpi/list_white_down.9.png
diff --git a/src/res/drawable-hdpi/list_white_middle.9.png b/src/app/src/main/res/drawable-hdpi/list_white_middle.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_white_middle.9.png
rename to src/app/src/main/res/drawable-hdpi/list_white_middle.9.png
diff --git a/src/res/drawable-hdpi/list_white_single.9.png b/src/app/src/main/res/drawable-hdpi/list_white_single.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_white_single.9.png
rename to src/app/src/main/res/drawable-hdpi/list_white_single.9.png
diff --git a/src/res/drawable-hdpi/list_white_up.9.png b/src/app/src/main/res/drawable-hdpi/list_white_up.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_white_up.9.png
rename to src/app/src/main/res/drawable-hdpi/list_white_up.9.png
diff --git a/src/res/drawable-hdpi/list_yellow_down.9.png b/src/app/src/main/res/drawable-hdpi/list_yellow_down.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_yellow_down.9.png
rename to src/app/src/main/res/drawable-hdpi/list_yellow_down.9.png
diff --git a/src/res/drawable-hdpi/list_yellow_middle.9.png b/src/app/src/main/res/drawable-hdpi/list_yellow_middle.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_yellow_middle.9.png
rename to src/app/src/main/res/drawable-hdpi/list_yellow_middle.9.png
diff --git a/src/res/drawable-hdpi/list_yellow_single.9.png b/src/app/src/main/res/drawable-hdpi/list_yellow_single.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_yellow_single.9.png
rename to src/app/src/main/res/drawable-hdpi/list_yellow_single.9.png
diff --git a/src/res/drawable-hdpi/list_yellow_up.9.png b/src/app/src/main/res/drawable-hdpi/list_yellow_up.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_yellow_up.9.png
rename to src/app/src/main/res/drawable-hdpi/list_yellow_up.9.png
diff --git a/src/res/drawable-hdpi/menu_delete.png b/src/app/src/main/res/drawable-hdpi/menu_delete.png
similarity index 100%
rename from src/res/drawable-hdpi/menu_delete.png
rename to src/app/src/main/res/drawable-hdpi/menu_delete.png
diff --git a/src/res/drawable-hdpi/menu_move.png b/src/app/src/main/res/drawable-hdpi/menu_move.png
similarity index 100%
rename from src/res/drawable-hdpi/menu_move.png
rename to src/app/src/main/res/drawable-hdpi/menu_move.png
diff --git a/src/res/drawable-hdpi/new_note_normal.png b/src/app/src/main/res/drawable-hdpi/new_note_normal.png
similarity index 100%
rename from src/res/drawable-hdpi/new_note_normal.png
rename to src/app/src/main/res/drawable-hdpi/new_note_normal.png
diff --git a/src/res/drawable-hdpi/new_note_pressed.png b/src/app/src/main/res/drawable-hdpi/new_note_pressed.png
similarity index 100%
rename from src/res/drawable-hdpi/new_note_pressed.png
rename to src/app/src/main/res/drawable-hdpi/new_note_pressed.png
diff --git a/src/res/drawable-hdpi/note_edit_color_selector_panel.png b/src/app/src/main/res/drawable-hdpi/note_edit_color_selector_panel.png
similarity index 100%
rename from src/res/drawable-hdpi/note_edit_color_selector_panel.png
rename to src/app/src/main/res/drawable-hdpi/note_edit_color_selector_panel.png
diff --git a/src/res/drawable-hdpi/notification.png b/src/app/src/main/res/drawable-hdpi/notification.png
similarity index 100%
rename from src/res/drawable-hdpi/notification.png
rename to src/app/src/main/res/drawable-hdpi/notification.png
diff --git a/src/res/drawable-hdpi/search_result.png b/src/app/src/main/res/drawable-hdpi/search_result.png
similarity index 100%
rename from src/res/drawable-hdpi/search_result.png
rename to src/app/src/main/res/drawable-hdpi/search_result.png
diff --git a/src/res/drawable-hdpi/selected.png b/src/app/src/main/res/drawable-hdpi/selected.png
similarity index 100%
rename from src/res/drawable-hdpi/selected.png
rename to src/app/src/main/res/drawable-hdpi/selected.png
diff --git a/src/res/drawable-hdpi/title_alert.png b/src/app/src/main/res/drawable-hdpi/title_alert.png
similarity index 100%
rename from src/res/drawable-hdpi/title_alert.png
rename to src/app/src/main/res/drawable-hdpi/title_alert.png
diff --git a/src/res/drawable-hdpi/title_bar_bg.9.png b/src/app/src/main/res/drawable-hdpi/title_bar_bg.9.png
similarity index 100%
rename from src/res/drawable-hdpi/title_bar_bg.9.png
rename to src/app/src/main/res/drawable-hdpi/title_bar_bg.9.png
diff --git a/src/res/drawable-hdpi/widget_2x_blue.png b/src/app/src/main/res/drawable-hdpi/widget_2x_blue.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_2x_blue.png
rename to src/app/src/main/res/drawable-hdpi/widget_2x_blue.png
diff --git a/src/res/drawable-hdpi/widget_2x_green.png b/src/app/src/main/res/drawable-hdpi/widget_2x_green.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_2x_green.png
rename to src/app/src/main/res/drawable-hdpi/widget_2x_green.png
diff --git a/src/res/drawable-hdpi/widget_2x_red.png b/src/app/src/main/res/drawable-hdpi/widget_2x_red.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_2x_red.png
rename to src/app/src/main/res/drawable-hdpi/widget_2x_red.png
diff --git a/src/res/drawable-hdpi/widget_2x_white.png b/src/app/src/main/res/drawable-hdpi/widget_2x_white.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_2x_white.png
rename to src/app/src/main/res/drawable-hdpi/widget_2x_white.png
diff --git a/src/res/drawable-hdpi/widget_2x_yellow.png b/src/app/src/main/res/drawable-hdpi/widget_2x_yellow.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_2x_yellow.png
rename to src/app/src/main/res/drawable-hdpi/widget_2x_yellow.png
diff --git a/src/res/drawable-hdpi/widget_4x_blue.png b/src/app/src/main/res/drawable-hdpi/widget_4x_blue.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_4x_blue.png
rename to src/app/src/main/res/drawable-hdpi/widget_4x_blue.png
diff --git a/src/res/drawable-hdpi/widget_4x_green.png b/src/app/src/main/res/drawable-hdpi/widget_4x_green.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_4x_green.png
rename to src/app/src/main/res/drawable-hdpi/widget_4x_green.png
diff --git a/src/res/drawable-hdpi/widget_4x_red.png b/src/app/src/main/res/drawable-hdpi/widget_4x_red.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_4x_red.png
rename to src/app/src/main/res/drawable-hdpi/widget_4x_red.png
diff --git a/src/res/drawable-hdpi/widget_4x_white.png b/src/app/src/main/res/drawable-hdpi/widget_4x_white.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_4x_white.png
rename to src/app/src/main/res/drawable-hdpi/widget_4x_white.png
diff --git a/src/res/drawable-hdpi/widget_4x_yellow.png b/src/app/src/main/res/drawable-hdpi/widget_4x_yellow.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_4x_yellow.png
rename to src/app/src/main/res/drawable-hdpi/widget_4x_yellow.png
diff --git a/src/app/src/main/res/drawable/bg_floating_ball.xml b/src/app/src/main/res/drawable/bg_floating_ball.xml
new file mode 100644
index 0000000..181dcad
--- /dev/null
+++ b/src/app/src/main/res/drawable/bg_floating_ball.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/src/main/res/drawable/ic_add_24dp.xml b/src/app/src/main/res/drawable/ic_add_24dp.xml
new file mode 100644
index 0000000..ea55c4e
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_add_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_arrow_back_24dp.xml b/src/app/src/main/res/drawable/ic_arrow_back_24dp.xml
new file mode 100644
index 0000000..4e13e4a
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_arrow_back_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_edit_note_24dp.xml b/src/app/src/main/res/drawable/ic_edit_note_24dp.xml
new file mode 100644
index 0000000..7e51a3c
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_edit_note_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_exit_privacy_24dp.xml b/src/app/src/main/res/drawable/ic_exit_privacy_24dp.xml
new file mode 100644
index 0000000..99b02d7
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_exit_privacy_24dp.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_export_24dp.xml b/src/app/src/main/res/drawable/ic_export_24dp.xml
new file mode 100644
index 0000000..5c3bfe4
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_export_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_folder_24dp.xml b/src/app/src/main/res/drawable/ic_folder_24dp.xml
new file mode 100644
index 0000000..591115a
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_folder_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_menu_24dp.xml b/src/app/src/main/res/drawable/ic_menu_24dp.xml
new file mode 100644
index 0000000..ecb13cc
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_menu_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_search_24dp.xml b/src/app/src/main/res/drawable/ic_search_24dp.xml
new file mode 100644
index 0000000..0a6ea2c
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_search_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_settings_24dp.xml b/src/app/src/main/res/drawable/ic_settings_24dp.xml
new file mode 100644
index 0000000..5aa8318
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_settings_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_sync_24dp.xml b/src/app/src/main/res/drawable/ic_sync_24dp.xml
new file mode 100644
index 0000000..3633fd0
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_sync_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/res/drawable/new_note.xml b/src/app/src/main/res/drawable/new_note.xml
similarity index 100%
rename from src/res/drawable/new_note.xml
rename to src/app/src/main/res/drawable/new_note.xml
diff --git a/src/app/src/main/res/drawable/splash_accent_line.xml b/src/app/src/main/res/drawable/splash_accent_line.xml
new file mode 100644
index 0000000..3c5bf62
--- /dev/null
+++ b/src/app/src/main/res/drawable/splash_accent_line.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/src/app/src/main/res/drawable/splash_background.xml b/src/app/src/main/res/drawable/splash_background.xml
new file mode 100644
index 0000000..90642cb
--- /dev/null
+++ b/src/app/src/main/res/drawable/splash_background.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/res/layout/account_dialog_title.xml b/src/app/src/main/res/layout/account_dialog_title.xml
similarity index 100%
rename from src/res/layout/account_dialog_title.xml
rename to src/app/src/main/res/layout/account_dialog_title.xml
diff --git a/src/app/src/main/res/layout/activity_main.xml b/src/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..5a6601a
--- /dev/null
+++ b/src/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/src/main/res/layout/activity_splash.xml b/src/app/src/main/res/layout/activity_splash.xml
new file mode 100644
index 0000000..6be03d3
--- /dev/null
+++ b/src/app/src/main/res/layout/activity_splash.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/src/main/res/layout/activity_todo_edit.xml b/src/app/src/main/res/layout/activity_todo_edit.xml
new file mode 100644
index 0000000..9185c33
--- /dev/null
+++ b/src/app/src/main/res/layout/activity_todo_edit.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/res/layout/add_account_text.xml b/src/app/src/main/res/layout/add_account_text.xml
similarity index 100%
rename from src/res/layout/add_account_text.xml
rename to src/app/src/main/res/layout/add_account_text.xml
diff --git a/src/res/layout/datetime_picker.xml b/src/app/src/main/res/layout/datetime_picker.xml
similarity index 100%
rename from src/res/layout/datetime_picker.xml
rename to src/app/src/main/res/layout/datetime_picker.xml
diff --git a/src/res/layout/dialog_edit_text.xml b/src/app/src/main/res/layout/dialog_edit_text.xml
similarity index 100%
rename from src/res/layout/dialog_edit_text.xml
rename to src/app/src/main/res/layout/dialog_edit_text.xml
diff --git a/src/app/src/main/res/layout/drawer_header.xml b/src/app/src/main/res/layout/drawer_header.xml
new file mode 100644
index 0000000..250e079
--- /dev/null
+++ b/src/app/src/main/res/layout/drawer_header.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/src/res/layout/folder_list_item.xml b/src/app/src/main/res/layout/folder_list_item.xml
similarity index 100%
rename from src/res/layout/folder_list_item.xml
rename to src/app/src/main/res/layout/folder_list_item.xml
diff --git a/src/app/src/main/res/layout/fragment_inspiration.xml b/src/app/src/main/res/layout/fragment_inspiration.xml
new file mode 100644
index 0000000..f608b43
--- /dev/null
+++ b/src/app/src/main/res/layout/fragment_inspiration.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/res/layout/fragment_mi_steward.xml b/src/app/src/main/res/layout/fragment_mi_steward.xml
similarity index 81%
rename from src/res/layout/fragment_mi_steward.xml
rename to src/app/src/main/res/layout/fragment_mi_steward.xml
index bbcbcd3..540e8dc 100644
--- a/src/res/layout/fragment_mi_steward.xml
+++ b/src/app/src/main/res/layout/fragment_mi_steward.xml
@@ -2,16 +2,27 @@
+ android:background="@color/background_light">
+
+
+
-
+
+ android:layout_marginRight="10dp"
+ android:text="灵感球"
+ android:textSize="12sp" />
@@ -51,7 +62,7 @@
android:text="你的私人管家"
android:textSize="20sp"
android:textStyle="bold"
- android:textColor="#333333"/>
+ android:textColor="@color/text_primary"/>
@@ -72,7 +83,7 @@
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:padding="12dp"
- android:background="#FFFFFF"
+ android:background="@color/surface_white"
android:orientation="vertical">
+ android:textColor="@color/text_secondary"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/res/layout/item_chat_ai.xml b/src/app/src/main/res/layout/item_chat_ai.xml
similarity index 100%
rename from src/res/layout/item_chat_ai.xml
rename to src/app/src/main/res/layout/item_chat_ai.xml
diff --git a/src/res/layout/item_chat_user.xml b/src/app/src/main/res/layout/item_chat_user.xml
similarity index 100%
rename from src/res/layout/item_chat_user.xml
rename to src/app/src/main/res/layout/item_chat_user.xml
diff --git a/src/app/src/main/res/layout/item_todo.xml b/src/app/src/main/res/layout/item_todo.xml
new file mode 100644
index 0000000..03c615f
--- /dev/null
+++ b/src/app/src/main/res/layout/item_todo.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
diff --git a/src/app/src/main/res/layout/item_todo_section.xml b/src/app/src/main/res/layout/item_todo_section.xml
new file mode 100644
index 0000000..df262f5
--- /dev/null
+++ b/src/app/src/main/res/layout/item_todo_section.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
diff --git a/src/app/src/main/res/layout/layout_floating_ball.xml b/src/app/src/main/res/layout/layout_floating_ball.xml
new file mode 100644
index 0000000..0f11a0c
--- /dev/null
+++ b/src/app/src/main/res/layout/layout_floating_ball.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/src/main/res/layout/layout_floating_input.xml b/src/app/src/main/res/layout/layout_floating_input.xml
new file mode 100644
index 0000000..a4ccd83
--- /dev/null
+++ b/src/app/src/main/res/layout/layout_floating_input.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/res/layout/note_edit.xml b/src/app/src/main/res/layout/note_edit.xml
similarity index 88%
rename from src/res/layout/note_edit.xml
rename to src/app/src/main/res/layout/note_edit.xml
index 0b5bcdc..fe7a1a6 100644
--- a/src/res/layout/note_edit.xml
+++ b/src/app/src/main/res/layout/note_edit.xml
@@ -31,12 +31,42 @@
android:layout_width="fill_parent"
android:layout_height="wrap_content">
+
+
+
+
+
@@ -81,11 +111,12 @@
android:scrollbars="none"
android:overScrollMode="never"
android:layout_gravity="left|top"
- android:fadingEdgeLength="0dip">
+ android:fadingEdgeLength="0dip"
+ android:fillViewport="true">
+ android:layout_height="wrap_content">
+
+
+
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/res/layout/note_list_dropdown_menu.xml b/src/app/src/main/res/layout/note_list_dropdown_menu.xml
similarity index 100%
rename from src/res/layout/note_list_dropdown_menu.xml
rename to src/app/src/main/res/layout/note_list_dropdown_menu.xml
diff --git a/src/res/layout/note_list_footer.xml b/src/app/src/main/res/layout/note_list_footer.xml
similarity index 83%
rename from src/res/layout/note_list_footer.xml
rename to src/app/src/main/res/layout/note_list_footer.xml
index 5ca7b22..29dead6 100644
--- a/src/res/layout/note_list_footer.xml
+++ b/src/app/src/main/res/layout/note_list_footer.xml
@@ -17,8 +17,7 @@
\ No newline at end of file
+ android:background="@color/background_light" />
\ No newline at end of file
diff --git a/src/res/layout/settings_header.xml b/src/app/src/main/res/layout/settings_header.xml
similarity index 100%
rename from src/res/layout/settings_header.xml
rename to src/app/src/main/res/layout/settings_header.xml
diff --git a/src/res/layout/widget_2x.xml b/src/app/src/main/res/layout/widget_2x.xml
similarity index 100%
rename from src/res/layout/widget_2x.xml
rename to src/app/src/main/res/layout/widget_2x.xml
diff --git a/src/res/layout/widget_4x.xml b/src/app/src/main/res/layout/widget_4x.xml
similarity index 100%
rename from src/res/layout/widget_4x.xml
rename to src/app/src/main/res/layout/widget_4x.xml
diff --git a/src/res/menu/bottom_nav_menu.xml b/src/app/src/main/res/menu/bottom_nav_menu.xml
similarity index 55%
rename from src/res/menu/bottom_nav_menu.xml
rename to src/app/src/main/res/menu/bottom_nav_menu.xml
index df01352..d98017d 100644
--- a/src/res/menu/bottom_nav_menu.xml
+++ b/src/app/src/main/res/menu/bottom_nav_menu.xml
@@ -3,9 +3,13 @@
+ android:title="@string/nav_mi_steward" />
+ android:title="@string/nav_notes" />
+
\ No newline at end of file
diff --git a/src/res/menu/call_note_edit.xml b/src/app/src/main/res/menu/call_note_edit.xml
similarity index 100%
rename from src/res/menu/call_note_edit.xml
rename to src/app/src/main/res/menu/call_note_edit.xml
diff --git a/src/res/menu/call_record_folder.xml b/src/app/src/main/res/menu/call_record_folder.xml
similarity index 100%
rename from src/res/menu/call_record_folder.xml
rename to src/app/src/main/res/menu/call_record_folder.xml
diff --git a/src/app/src/main/res/menu/drawer_menu.xml b/src/app/src/main/res/menu/drawer_menu.xml
new file mode 100644
index 0000000..d099ba7
--- /dev/null
+++ b/src/app/src/main/res/menu/drawer_menu.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/src/res/menu/note_edit.xml b/src/app/src/main/res/menu/note_edit.xml
similarity index 100%
rename from src/res/menu/note_edit.xml
rename to src/app/src/main/res/menu/note_edit.xml
diff --git a/src/res/menu/note_list.xml b/src/app/src/main/res/menu/note_list.xml
similarity index 100%
rename from src/res/menu/note_list.xml
rename to src/app/src/main/res/menu/note_list.xml
diff --git a/src/res/menu/note_list_dropdown.xml b/src/app/src/main/res/menu/note_list_dropdown.xml
similarity index 100%
rename from src/res/menu/note_list_dropdown.xml
rename to src/app/src/main/res/menu/note_list_dropdown.xml
diff --git a/src/res/menu/note_list_options.xml b/src/app/src/main/res/menu/note_list_options.xml
similarity index 79%
rename from src/res/menu/note_list_options.xml
rename to src/app/src/main/res/menu/note_list_options.xml
index daac008..95e89b6 100644
--- a/src/res/menu/note_list_options.xml
+++ b/src/app/src/main/res/menu/note_list_options.xml
@@ -17,6 +17,14 @@
+
+
-
+
+
+
+
diff --git a/src/res/menu/sub_folder.xml b/src/app/src/main/res/menu/sub_folder.xml
similarity index 100%
rename from src/res/menu/sub_folder.xml
rename to src/app/src/main/res/menu/sub_folder.xml
diff --git a/src/app/src/main/res/menu/todo_list_options.xml b/src/app/src/main/res/menu/todo_list_options.xml
new file mode 100644
index 0000000..9ec9c59
--- /dev/null
+++ b/src/app/src/main/res/menu/todo_list_options.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/src/res/raw-zh-rCN/introduction b/src/app/src/main/res/raw-zh-rCN/introduction
similarity index 100%
rename from src/res/raw-zh-rCN/introduction
rename to src/app/src/main/res/raw-zh-rCN/introduction
diff --git a/src/res/raw/introduction b/src/app/src/main/res/raw/introduction
similarity index 100%
rename from src/res/raw/introduction
rename to src/app/src/main/res/raw/introduction
diff --git a/src/res/values-zh-rCN/arrays.xml b/src/app/src/main/res/values-zh-rCN/arrays.xml
similarity index 100%
rename from src/res/values-zh-rCN/arrays.xml
rename to src/app/src/main/res/values-zh-rCN/arrays.xml
diff --git a/src/res/values-zh-rCN/strings.xml b/src/app/src/main/res/values-zh-rCN/strings.xml
similarity index 81%
rename from src/res/values-zh-rCN/strings.xml
rename to src/app/src/main/res/values-zh-rCN/strings.xml
index 09f75ed..a78effd 100644
--- a/src/res/values-zh-rCN/strings.xml
+++ b/src/app/src/main/res/values-zh-rCN/strings.xml
@@ -24,9 +24,29 @@
访客模式下,便签内容不可见
...
新建便签
+ 便签
+ 返回上一级
+ Mi 管家
+ 便签
+ 待办
+ 待办事项
+ 未完成
+ 已完成
+ 输入待办内容...
+ 添加提醒
+ 时间提醒
+ 地点提醒
+ 保存
+ 删除
+ 确定删除选中的 %d 项?
+ 无提醒
成功删除提醒
创建提醒
已过期
+ 便签提醒
+ 您有一条便签提醒
+ 便签提醒
+ 时间提醒到点后在此渠道显示通知
yyyyMMdd
MM月dd日 kk:mm
知道了
@@ -43,6 +63,10 @@
设置
搜索
删除
+ 置顶
+ 取消置顶
+ 已置顶
+ 已取消置顶
移动到文件夹
选中了 %d 项
没有选中项,操作无效
@@ -53,6 +77,11 @@
正常
大
超大
+ 加粗
+ 字号变大
+ 字号变小
+ 无标题
+ 图片便签
进入清单模式
退出清单模式
查看文件夹
@@ -77,6 +106,7 @@
要查看的便签不存在
不能为空便签设置闹钟提醒
不能将空便签发送到桌面
+ 无法读取该图片,请换一张试试
导出成功
导出失败
已将文本文件(%1$s)输出至SD卡(%2$s)目录
diff --git a/src/res/values-zh-rTW/arrays.xml b/src/app/src/main/res/values-zh-rTW/arrays.xml
similarity index 100%
rename from src/res/values-zh-rTW/arrays.xml
rename to src/app/src/main/res/values-zh-rTW/arrays.xml
diff --git a/src/res/values-zh-rTW/strings.xml b/src/app/src/main/res/values-zh-rTW/strings.xml
similarity index 95%
rename from src/res/values-zh-rTW/strings.xml
rename to src/app/src/main/res/values-zh-rTW/strings.xml
index 3c41894..2e7bd9e 100644
--- a/src/res/values-zh-rTW/strings.xml
+++ b/src/app/src/main/res/values-zh-rTW/strings.xml
@@ -24,6 +24,7 @@
訪客模式下,便籤內容不可見
...
新建便簽
+ 返回上一級
成功刪除提醒
創建提醒
已過期
@@ -44,6 +45,10 @@
設置
搜尋
刪除
+ 置頂
+ 取消置頂
+ 已置頂
+ 已取消置頂
移動到文件夾
選中了 %d 項
沒有選中項,操作無效
@@ -76,6 +81,7 @@
要查看的便籤不存在
不能爲空便籤設置鬧鐘提醒
不能將空便籤發送到桌面
+ 無法讀取該圖片,請換一張試試
導出成功
導出失敗
已將文本文件(%1$s)導出至SD(%2$s)目錄
diff --git a/src/res/values/arrays.xml b/src/app/src/main/res/values/arrays.xml
similarity index 100%
rename from src/res/values/arrays.xml
rename to src/app/src/main/res/values/arrays.xml
diff --git a/src/app/src/main/res/values/colors.xml b/src/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..d26450b
--- /dev/null
+++ b/src/app/src/main/res/values/colors.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+ #335b5b5b
+
+ #CCB8860B
+ #CC1565C0
+ #CC616161
+ #CC2E7D32
+ #CCC62828
+
+
+ #FFFFFF
+ #F7F8FA
+ #EFF1F3
+ #1D1D1D
+ #657786
+ #1DA1F2
+ #1A91DA
+
+ #657786
+ #1DA1F2
+ #657786
+ #1D1D1D
+
+
+ #FF1C1C1E
+
diff --git a/src/res/values/dimens.xml b/src/app/src/main/res/values/dimens.xml
similarity index 100%
rename from src/res/values/dimens.xml
rename to src/app/src/main/res/values/dimens.xml
diff --git a/src/res/values/strings.xml b/src/app/src/main/res/values/strings.xml
similarity index 79%
rename from src/res/values/strings.xml
rename to src/app/src/main/res/values/strings.xml
index 55df868..158acfc 100644
--- a/src/res/values/strings.xml
+++ b/src/app/src/main/res/values/strings.xml
@@ -24,9 +24,33 @@
Privacy mode,can not see note content
...
Add note
+ Notes
+ Back to note list
+ 隐私空间
+ 退出隐私空间
+ Mi 管家
+ 便签
+ 待办
+ 待办事项
+ 未完成
+ 已完成
+ 输入待办内容...
+ 添加提醒
+ 时间提醒
+ 地点提醒
+ 保存
+ 删除
+ 确定删除选中的 %d 项?
+ 无提醒
+ 全部完成!
+ 太棒了,所有待办都已完成!
Delete reminder successfully
Set reminder
Expired
+ 便签提醒
+ 您有一条便签提醒
+ 便签提醒
+ 时间提醒到点后在此渠道显示通知
yyyyMMdd
MMMd kk:mm
Got it
@@ -47,6 +71,10 @@
Settings
Search
Delete
+ Pin to top
+ Unpin
+ Pinned to top
+ Unpinned
Move to folder
%d selected
Nothing selected, the operation is invalid
@@ -57,6 +85,11 @@
Medium
Large
Super
+ Bold
+ Larger text
+ Smaller text
+ Untitled
+ Image note
Enter check list
Leave check list
View folder
@@ -81,6 +114,7 @@
The note is not exist
Sorry, can not set clock on empty note
Sorry, can not send and empty note to home
+ Failed to load image, please try another one
Export successful
Export fail
Export text file (%1$s) to SD (%2$s) directory
@@ -119,6 +153,7 @@
Delete
Call notes
Input name
+ 共 %d 字
Searching Notes
Search notes
diff --git a/src/res/values/styles.xml b/src/app/src/main/res/values/styles.xml
similarity index 73%
rename from src/res/values/styles.xml
rename to src/app/src/main/res/values/styles.xml
index d750e65..fe9df45 100644
--- a/src/res/values/styles.xml
+++ b/src/app/src/main/res/values/styles.xml
@@ -66,4 +66,23 @@
- gone
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/src/main/res/xml/file_paths.xml b/src/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..fb653e4
--- /dev/null
+++ b/src/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/res/xml/preferences.xml b/src/app/src/main/res/xml/preferences.xml
similarity index 100%
rename from src/res/xml/preferences.xml
rename to src/app/src/main/res/xml/preferences.xml
diff --git a/src/res/xml/searchable.xml b/src/app/src/main/res/xml/searchable.xml
similarity index 100%
rename from src/res/xml/searchable.xml
rename to src/app/src/main/res/xml/searchable.xml
diff --git a/src/res/xml/widget_2x_info.xml b/src/app/src/main/res/xml/widget_2x_info.xml
similarity index 100%
rename from src/res/xml/widget_2x_info.xml
rename to src/app/src/main/res/xml/widget_2x_info.xml
diff --git a/src/res/xml/widget_4x_info.xml b/src/app/src/main/res/xml/widget_4x_info.xml
similarity index 100%
rename from src/res/xml/widget_4x_info.xml
rename to src/app/src/main/res/xml/widget_4x_info.xml
diff --git a/src/app/src/test/java/net/micode/notes/ExampleUnitTest.java b/src/app/src/test/java/net/micode/notes/ExampleUnitTest.java
new file mode 100644
index 0000000..296adc2
--- /dev/null
+++ b/src/app/src/test/java/net/micode/notes/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package net.micode.notes;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/src/docs/置顶功能类图.md b/src/docs/置顶功能类图.md
new file mode 100644
index 0000000..e69de29
diff --git a/src/docs/置顶功能类图.puml b/src/docs/置顶功能类图.puml
new file mode 100644
index 0000000..e69de29
diff --git a/src/gradle/libs.versions.toml b/src/gradle/libs.versions.toml
new file mode 100644
index 0000000..1bcc061
--- /dev/null
+++ b/src/gradle/libs.versions.toml
@@ -0,0 +1,22 @@
+[versions]
+agp = "8.13.1"
+junit = "4.13.2"
+junitVersion = "1.3.0"
+espressoCore = "3.7.0"
+appcompat = "1.7.1"
+material = "1.13.0"
+activity = "1.11.0"
+constraintlayout = "2.2.1"
+
+[libraries]
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+
diff --git a/src/gradle/wrapper/gradle-wrapper.jar b/src/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8bdaf60
Binary files /dev/null and b/src/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/src/gradle/wrapper/gradle-wrapper.properties b/src/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..884f9ef
--- /dev/null
+++ b/src/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,8 @@
+#Wed Nov 19 20:15:31 CST 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/src/res/layout/activity_main.xml b/src/res/layout/activity_main.xml
deleted file mode 100644
index 1a66676..0000000
--- a/src/res/layout/activity_main.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/res/layout/note_list.xml b/src/res/layout/note_list.xml
deleted file mode 100644
index 6b25d38..0000000
--- a/src/res/layout/note_list.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/res/values/colors.xml b/src/res/values/colors.xml
deleted file mode 100644
index 123ffbf..0000000
--- a/src/res/values/colors.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
- #335b5b5b
-
diff --git a/src/res/xml/file_paths.xml b/src/res/xml/file_paths.xml
deleted file mode 100644
index f6a96e3..0000000
--- a/src/res/xml/file_paths.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/src/src/net/micode/notes/tool/MediaUtils.java b/src/src/net/micode/notes/tool/MediaUtils.java
deleted file mode 100644
index 4148eb4..0000000
--- a/src/src/net/micode/notes/tool/MediaUtils.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package net.micode.notes.tool;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.net.Uri;
-import android.os.Environment;
-import android.util.Log;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-
-/**
- * 媒体处理工具类
- * 职责:封装所有与图片文件IO、Uri解析、Bitmap压缩相关的底层逻辑。
- * 符合“高内聚、信息隐藏”原则。
- */
-public class MediaUtils {
- private static final String TAG = "MediaUtils";
- private static final String IMAGE_DIR_NAME = "images";
-
- /**
- * 创建一个用于存放相机拍摄照片的空文件
- * 位置:/Android/data/包名/files/Pictures/images/
- */
- public static File createImageFile(Context context) throws IOException {
- String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
- String imageFileName = "NOTE_" + timeStamp + "_";
-
- // 使用应用私有目录,不需要运行时存储权限即可写入
- File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
- if (storageDir != null && !storageDir.exists()) {
- storageDir.mkdirs();
- }
-
- // 创建临时文件
- return File.createTempFile(
- imageFileName, /* prefix */
- ".jpg", /* suffix */
- storageDir /* directory */
- );
- }
-
- /**
- * 将 Uri 指向的图片(可能是相册里的 ContentProvider Uri)复制到应用沙盒
- * 目的:防止用户在相册删除原图后,便签内图片丢失。
- * @return 复制后的本地文件路径
- */
- public static String copyUriToInternalStorage(Context context, Uri sourceUri) {
- InputStream inputStream = null;
- FileOutputStream outputStream = null;
- try {
- ContentResolver contentResolver = context.getContentResolver();
- // 1. 获取输入流
- inputStream = contentResolver.openInputStream(sourceUri);
- if (inputStream == null) return null;
-
- // 2. 创建目标文件
- File targetFile = createImageFile(context);
-
- // 3. 写入文件
- outputStream = new FileOutputStream(targetFile);
- byte[] buffer = new byte[1024];
- int length;
- while ((length = inputStream.read(buffer)) > 0) {
- outputStream.write(buffer, 0, length);
- }
-
- return targetFile.getAbsolutePath();
- } catch (IOException e) {
- Log.e(TAG, "Failed to copy image", e);
- return null;
- } finally {
- // 安全关闭流
- try {
- if (inputStream != null) inputStream.close();
- if (outputStream != null) outputStream.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
-
- /**
- * 获取压缩后的 Bitmap,防止 OOM (Out Of Memory)
- * @param path 图片路径
- * @param reqWidth 期望的宽度(通常是屏幕宽度)
- * @param reqHeight 期望的高度
- */
- public static Bitmap getCompressedBitmap(String path, int reqWidth, int reqHeight) {
- // 1. 只读取图片的尺寸信息,不加载像素到内存
- final BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(path, options);
-
- // 2. 计算压缩比例
- options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
-
- // 3. 真正加载图片
- options.inJustDecodeBounds = false;
- return BitmapFactory.decodeFile(path, options);
- }
-
- // 计算采样率的辅助算法
- private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
- final int height = options.outHeight;
- final int width = options.outWidth;
- int inSampleSize = 1;
-
- if (height > reqHeight || width > reqWidth) {
- final int halfHeight = height / 2;
- final int halfWidth = width / 2;
- // 计算最大的 2 的幂次,保证宽高仍大于期望宽高
- while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
- inSampleSize *= 2;
- }
- }
- return inSampleSize;
- }
-}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/ui/MainActivity.java b/src/src/net/micode/notes/ui/MainActivity.java
deleted file mode 100644
index bfc2977..0000000
--- a/src/src/net/micode/notes/ui/MainActivity.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package net.micode.notes.ui;
-
-import android.os.Bundle;
-import android.view.MenuItem;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.fragment.app.Fragment;
-import androidx.viewpager2.adapter.FragmentStateAdapter;
-import androidx.viewpager2.widget.ViewPager2;
-import com.google.android.material.bottomnavigation.BottomNavigationView;
-import net.micode.notes.R;
-
-public class MainActivity extends AppCompatActivity {
-
- private ViewPager2 mViewPager;
- private BottomNavigationView mBottomNav;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
-
- mViewPager = findViewById(R.id.view_pager);
- mBottomNav = findViewById(R.id.bottom_navigation);
-
- // 设置 Adapter
- mViewPager.setAdapter(new FragmentStateAdapter(this) {
- @NonNull
- @Override
- public Fragment createFragment(int position) {
- if (position == 0) {
- return new MiStewardFragment(); // 默认第一页是管家
- } else {
- return new NotesListFragment(); // 第二页是便签列表
- }
- }
-
- @Override
- public int getItemCount() {
- return 2;
- }
- });
-
- // 联动逻辑:点击底部 -> 切换 ViewPager
- mBottomNav.setOnItemSelectedListener(new BottomNavigationView.OnItemSelectedListener() {
- @Override
- public boolean onNavigationItemSelected(@NonNull MenuItem item) {
- if (item.getItemId() == R.id.nav_mi_steward) {
- mViewPager.setCurrentItem(0);
- return true;
- } else if (item.getItemId() == R.id.nav_notes) {
- mViewPager.setCurrentItem(1);
- return true;
- }
- return false;
- }
- });
-
- // 联动逻辑:滑动 ViewPager -> 切换底部按钮
- mViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
- @Override
- public void onPageSelected(int position) {
- super.onPageSelected(position);
- if (position == 0) {
- mBottomNav.setSelectedItemId(R.id.nav_mi_steward);
- } else {
- mBottomNav.setSelectedItemId(R.id.nav_notes);
- }
- }
- });
- }
-}
diff --git a/src/src/net/micode/notes/ui/MiStewardFragment.java b/src/src/net/micode/notes/ui/MiStewardFragment.java
deleted file mode 100644
index 78cfc0b..0000000
--- a/src/src/net/micode/notes/ui/MiStewardFragment.java
+++ /dev/null
@@ -1,159 +0,0 @@
-package net.micode.notes.ui;
-
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.Toast;
-import net.micode.notes.ai.NoteRetriever;
-import net.micode.notes.ai.PromptBuilder;
-import net.micode.notes.ai.AIService;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import android.widget.CheckBox;
-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.model.ChatMessage;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class MiStewardFragment extends Fragment {
-
- private RecyclerView mRecyclerView;
- private ChatAdapter mAdapter;
- private EditText mInputBox;
- private View mLogoArea;
- private View mTopBar;
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- View view = inflater.inflate(R.layout.fragment_mi_steward, container, false);
- initViews(view);
- return view;
- }
-
- private void initViews(View view) {
- mRecyclerView = view.findViewById(R.id.recycler_view);
- mInputBox = view.findViewById(R.id.et_input);
- mLogoArea = view.findViewById(R.id.center_logo_area);
- mTopBar = view.findViewById(R.id.top_bar);
- Button btnSend = view.findViewById(R.id.btn_send);
- ImageView btnNewChat = view.findViewById(R.id.btn_new_chat);
-
- mAdapter = new ChatAdapter();
- mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
- mRecyclerView.setAdapter(mAdapter);
-
- // 发送按钮点击
- btnSend.setOnClickListener(v -> {
- String text = mInputBox.getText().toString().trim();
- if (TextUtils.isEmpty(text)) return;
-
- // 1. 切换 UI 状态
- mLogoArea.setVisibility(View.GONE);
- mRecyclerView.setVisibility(View.VISIBLE);
-
- // 2. 添加用户消息
- mAdapter.addMessage(new ChatMessage(text));
- mInputBox.setText("");
- scrollToBottom();
-
- // 3. 模拟 AI 思考 (这是个占位逻辑,下一阶段会接真实的 DeepSeek)
- performAIQuery(text);
- });
-
- // 新对话按钮点击
- btnNewChat.setOnClickListener(v -> {
- mAdapter.clear();
- mLogoArea.setVisibility(View.VISIBLE);
- mRecyclerView.setVisibility(View.GONE);
- Toast.makeText(getContext(), "已开启新对话", Toast.LENGTH_SHORT).show();
- });
- }
-
- private void scrollToBottom() {
- mRecyclerView.scrollToPosition(mAdapter.getItemCount() - 1);
- }
-
- // 模拟 AI 回复 (测试 UI 用)
- // 真实的 AI 响应处理
- private void performAIQuery(String userQuery) {
- // 先在 UI 上显示“思考中”
- ChatMessage aiMsg = new ChatMessage();
- mAdapter.addMessage(aiMsg);
- scrollToBottom();
-
- // 1. 判断是否勾选“使用知识库”
- CheckBox cbUseKnowledge = getView().findViewById(R.id.cb_use_knowledge);
- boolean useRAG = cbUseKnowledge.isChecked();
-
- // 2. 异步检索知识库 (检索需要在子线程,虽然 SQLite 很快,但为了严谨)
- new Thread(() -> {
- List relatedNotes = null;
- if (useRAG) {
- // 去数据库检索
- relatedNotes = NoteRetriever.searchRelevantNotes(getContext(), userQuery);
- }
-
- // 3. 构建 Prompt
- String systemPrompt = PromptBuilder.buildSystemPrompt(relatedNotes,useRAG);
-
- // 4. 调用 API
- AIService.chatWithKnowledge(systemPrompt, userQuery, new AIService.AIResultCallback() {
- @Override
- public void onSuccess(String result) {
- // 解析结果中的引用 [Ref:xxx]
- List refs = new ArrayList<>();
- // 使用正则提取引用并从文本中移除(或者保留,看你喜好,这里我们提取出来单独展示)
- String cleanContent = result;
-
- // 正则匹配 [Ref:...]
- Pattern pattern = Pattern.compile("\\[Ref:(.*?)\\]");
- Matcher matcher = pattern.matcher(result);
- while (matcher.find()) {
- String refTitle = matcher.group(1);
- if (!refs.contains(refTitle)) {
- refs.add("📌 " + refTitle);
- }
- }
- // 如果你想把正文里的 [Ref:xxx] 去掉,可以取消下面这行的注释
- cleanContent = matcher.replaceAll("").trim();
-
- // 更新 UI
- ChatMessage lastMsg = mAdapter.getLastMessage();
- if (lastMsg != null && lastMsg.getType() == ChatMessage.TYPE_AI) {
- lastMsg.setThinking(false);
- lastMsg.setContent(cleanContent);
- lastMsg.setReferences(refs);
-
- mAdapter.updateLastMessage();
- scrollToBottom();
- }
- }
-
- @Override
- public void onError(String error) {
- ChatMessage lastMsg = mAdapter.getLastMessage();
- if (lastMsg != null) {
- lastMsg.setThinking(false);
- lastMsg.setContent("大脑短路了... 错误原因:" + error);
- mAdapter.updateLastMessage();
- }
- }
- });
- }).start();
- }
-}
\ No newline at end of file