{
});
}
- private void showNotification(int tickerId, String content) {
- Notification notification = new Notification(R.drawable.notification, mContext
- .getString(tickerId), System.currentTimeMillis());
- notification.defaults = Notification.DEFAULT_LIGHTS;
- notification.flags = Notification.FLAG_AUTO_CANCEL;
- PendingIntent pendingIntent;
- if (tickerId != R.string.ticker_success) {
- pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
- NotesPreferenceActivity.class), 0);
-
- } else {
- pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
- NotesListActivity.class), 0);
- }
- notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content,
- pendingIntent);
- mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
+// private void showNotification(int tickerId, String content) {
+// Notification notification = new Notification(R.drawable.notification, mContext
+// .getString(tickerId), System.currentTimeMillis());
+// notification.defaults = Notification.DEFAULT_LIGHTS;
+// notification.flags = Notification.FLAG_AUTO_CANCEL;
+// PendingIntent pendingIntent;
+// if (tickerId != R.string.ticker_success) {
+// pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
+// NotesPreferenceActivity.class), 0);
+//
+// } else {
+// pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
+// NotesListActivity.class), 0);
+// }
+// notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content,
+// pendingIntent);
+// mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
+// }
+private void showNotification(int tickerId, String content) {
+ PendingIntent pendingIntent;
+ if (tickerId != R.string.ticker_success) {
+ pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
+ NotesPreferenceActivity.class), PendingIntent.FLAG_IMMUTABLE);
+ } else {
+ pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
+ NotesListActivity.class), PendingIntent.FLAG_IMMUTABLE);
}
-
+ Notification.Builder builder = new Notification.Builder(mContext)
+ .setAutoCancel(true)
+ .setContentTitle(mContext.getString(R.string.app_name))
+ .setContentText(content)
+ .setContentIntent(pendingIntent)
+ .setWhen(System.currentTimeMillis())
+ .setOngoing(true);
+ Notification notification=builder.getNotification();
+ mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
+}
@Override
protected Integer doInBackground(Void... unused) {
publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity
diff --git a/src/Notes-master/src/net/micode/notes/model/Note.java b/src/Notes-master/src/net/micode/notes/model/Note.java
index 6706cf6..5596030 100644
--- a/src/Notes-master/src/net/micode/notes/model/Note.java
+++ b/src/Notes-master/src/net/micode/notes/model/Note.java
@@ -30,6 +30,7 @@ import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
+import net.micode.notes.tool.UserManager;
import java.util.ArrayList;
@@ -50,14 +51,22 @@ public class Note {
values.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.PARENT_ID, folderId);
+ // 设置当前用户ID
+ long currentUserId = UserManager.getInstance(context).getCurrentUserId();
+ values.put(NoteColumns.USER_ID, currentUserId);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
long noteId = 0;
- try {
- noteId = Long.valueOf(uri.getPathSegments().get(1));
- } catch (NumberFormatException e) {
- Log.e(TAG, "Get note id error :" + e.toString());
- noteId = 0;
+ if (uri != null) {
+ try {
+ noteId = Long.valueOf(uri.getPathSegments().get(1));
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Get note id error :" + e.toString());
+ noteId = 0;
+ } catch (IndexOutOfBoundsException e) {
+ Log.e(TAG, "Get note id error :" + e.toString());
+ noteId = 0;
+ }
}
if (noteId == -1) {
throw new IllegalStateException("Wrong note id:" + noteId);
diff --git a/src/Notes-master/src/net/micode/notes/model/WorkingNote.java b/src/Notes-master/src/net/micode/notes/model/WorkingNote.java
index be081e4..3762f45 100644
--- a/src/Notes-master/src/net/micode/notes/model/WorkingNote.java
+++ b/src/Notes-master/src/net/micode/notes/model/WorkingNote.java
@@ -39,6 +39,8 @@ public class WorkingNote {
private long mNoteId;
// Note content
private String mContent;
+ // Note title
+ private String mTitle;
// Note mode
private int mMode;
@@ -78,7 +80,8 @@ public class WorkingNote {
NoteColumns.BG_COLOR_ID,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
- NoteColumns.MODIFIED_DATE
+ NoteColumns.MODIFIED_DATE,
+ NoteColumns.TITLE
};
private static final int DATA_ID_COLUMN = 0;
@@ -101,6 +104,8 @@ public class WorkingNote {
private static final int NOTE_MODIFIED_DATE_COLUMN = 5;
+ private static final int NOTE_TITLE_COLUMN = 6;
+
// New note construct
private WorkingNote(Context context, long folderId) {
mContext = context;
@@ -125,6 +130,7 @@ public class WorkingNote {
}
private void loadNote() {
+ // 执行查询,不添加用户过滤条件,因为NotesProvider已经处理了公开便签的访问权限
Cursor cursor = mContext.getContentResolver().query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null,
null, null);
@@ -137,6 +143,7 @@ public class WorkingNote {
mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN);
mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);
mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);
+ mTitle = cursor.getString(NOTE_TITLE_COLUMN);
}
cursor.close();
} else {
@@ -149,9 +156,13 @@ public class WorkingNote {
private void loadNoteData() {
Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION,
DataColumns.NOTE_ID + "=?", new String[] {
- String.valueOf(mNoteId)
+ String.valueOf(mNoteId)
}, null);
+ // 初始化默认值
+ mContent = "";
+ mMode = 0;
+
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
@@ -170,12 +181,11 @@ public class WorkingNote {
cursor.close();
} else {
Log.e(TAG, "No data with id:" + mNoteId);
- throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId);
}
}
public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId,
- int widgetType, int defaultBgColorId) {
+ int widgetType, int defaultBgColorId) {
WorkingNote note = new WorkingNote(context, folderId);
note.setBgColorId(defaultBgColorId);
note.setWidgetId(widgetId);
@@ -188,26 +198,54 @@ public class WorkingNote {
}
public synchronized boolean saveNote() {
- if (isWorthSaving()) {
- if (!existInDatabase()) {
- if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) {
- Log.e(TAG, "Create new note fail with id:" + mNoteId);
- return false;
+ try {
+ if (isWorthSaving()) {
+ if (!existInDatabase()) {
+ if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) {
+ Log.e(TAG, "Create new note fail with id:" + mNoteId);
+ return false;
+ }
}
- }
- mNote.syncNote(mContext, mNoteId);
+ mNote.syncNote(mContext, mNoteId);
+
+ // 自动分类逻辑
+ try {
+ // 优先使用标题分类,如果标题为空则使用内容分类
+ String contentForCategory = mTitle;
+ if (contentForCategory == null || contentForCategory.isEmpty()) {
+ contentForCategory = mContent;
+ }
+ // 根据标题或内容自动分类
+ String category = net.micode.notes.tool.CategoryUtil.autoCategorize(contentForCategory);
+
+ // 创建或获取对应的文件夹
+ long categoryFolderId = net.micode.notes.tool.DataUtils.createFolder(mContext.getContentResolver(), category);
+ if (categoryFolderId > 0 && mFolderId != categoryFolderId) {
+ // 将便签移动到分类文件夹
+ net.micode.notes.tool.DataUtils.moveNoteToFoler(mContext.getContentResolver(), mNoteId, mFolderId, categoryFolderId);
+ mFolderId = categoryFolderId;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Auto categorize fail: " + e.getMessage());
+ // 自动分类失败不影响便签保存
+ }
- /**
- * Update widget content if there exist any widget of this note
- */
- if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
- && mWidgetType != Notes.TYPE_WIDGET_INVALIDE
- && mNoteSettingStatusListener != null) {
- mNoteSettingStatusListener.onWidgetChanged();
+ /**
+ * Update widget content if there exist any widget of this note
+ */
+ if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
+ && mWidgetType != Notes.TYPE_WIDGET_INVALIDE
+ && mNoteSettingStatusListener != null) {
+ mNoteSettingStatusListener.onWidgetChanged();
+ }
+ return true;
+ } else {
+ return false;
}
- return true;
- } else {
+ } catch (Exception e) {
+ Log.e(TAG, "Save note fail: " + e.getMessage());
+ e.printStackTrace();
return false;
}
}
@@ -217,10 +255,10 @@ public class WorkingNote {
}
private boolean isWorthSaving() {
- if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent))
- || (existInDatabase() && !mNote.isLocalModified())) {
+ if (mIsDeleted) {
return false;
} else {
+ // 允许保存空便签
return true;
}
}
@@ -243,7 +281,7 @@ public class WorkingNote {
mIsDeleted = mark;
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) {
- mNoteSettingStatusListener.onWidgetChanged();
+ mNoteSettingStatusListener.onWidgetChanged();
}
}
@@ -287,6 +325,17 @@ public class WorkingNote {
mNote.setTextData(DataColumns.CONTENT, mContent);
}
}
+
+ public void setWorkingTitle(String title) {
+ if (!TextUtils.equals(mTitle, title)) {
+ mTitle = title;
+ mNote.setNoteValue(NoteColumns.TITLE, title);
+ }
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
public void convertToCallNote(String phoneNumber, long callDate) {
mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate));
diff --git a/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java b/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java
index 4734dec..befac70 100644
--- a/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java
+++ b/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,61 +14,46 @@
* limitations under the License.
*/
-// 包声明:归属小米便签的工具模块,提供笔记备份(导出为文本文件)核心功能
package net.micode.notes.tool;
-// 导入安卓上下文类:访问资源、ContentResolver
import android.content.Context;
-// 导入安卓数据库游标类:查询便签/便签数据的核心载体
import android.database.Cursor;
-// 导入安卓外部存储类:判断SD卡挂载状态、获取SD卡根目录
import android.os.Environment;
-// 导入安卓文本工具类:判空、字符串处理
import android.text.TextUtils;
-// 导入安卓日期格式化类:格式化便签修改时间、导出文件名
import android.text.format.DateFormat;
-// 导入安卓日志类:输出备份过程中的日志(调试/错误)
import android.util.Log;
-// 导入小米便签资源类:引用字符串(文件路径、格式模板、文件夹名称)
import net.micode.notes.R;
-// 导入便签数据常量类:定义ContentURI、字段、便签类型、特殊文件夹ID等
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
-// 导入文件操作相关类:创建文件/目录、文件输出流、打印流
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
+
/**
- * 便签备份工具类
- * 核心特性:
- * 1. 单例模式:全局唯一实例,避免重复初始化资源;
- * 2. 核心功能:将便签数据库中的所有有效便签(排除回收站)导出为SD卡上的文本文件;
- * 3. 导出范围:
- * - 所有文件夹(排除回收站)及文件夹下的便签;
- * - 通话记录文件夹及其中的通话记录便签;
- * - 根目录下的普通便签;
- * 4. 导出格式:按文件夹分组,包含便签修改时间、内容(普通文本/通话记录详情);
- * 5. 状态码机制:返回备份操作的结果(SD卡未挂载、文件创建失败、成功等)。
+ * 便签备份工具类,用于将便签数据导出为文本文件
+ *
+ * 该类采用单例模式,提供便签数据的文本导出功能,支持将便签内容(包括文件夹结构)导出到SD卡上的文本文件中,
+ * 方便用户备份和查看便签内容。
*/
public class BackupUtils {
- // 日志标签:用于备份过程的日志输出
private static final String TAG = "BackupUtils";
-
- // 单例实例:全局唯一的BackupUtils对象
+ // Singleton stuff
private static BackupUtils sInstance;
/**
- * 获取单例实例(线程安全)
- * 采用同步方法确保多线程环境下实例唯一,避免重复创建
- * @param context 应用上下文:用于初始化内部导出器、访问资源
- * @return BackupUtils全局唯一实例
+ * 获取BackupUtils实例
+ *
+ * 采用单例模式,确保整个应用中只有一个BackupUtils实例
+ *
+ * @param context 上下文对象
+ * @return BackupUtils实例
*/
public static synchronized BackupUtils getInstance(Context context) {
if (sInstance == null) {
@@ -78,76 +63,73 @@ public class BackupUtils {
}
/**
- * 备份操作状态码:标识导出过程的结果,供外部判断操作是否成功
+ * 备份或恢复状态的常量定义
*/
- // SD卡未挂载(无法创建/写入导出文件)
+ // 当前SD卡未挂载
public static final int STATE_SD_CARD_UNMOUONTED = 0;
- // 备份文件不存在(仅恢复操作使用,当前导出逻辑未用到)
+ // 备份文件不存在
public static final int STATE_BACKUP_FILE_NOT_EXIST = 1;
- // 数据格式错误(导出时未用到,预留恢复操作的状态码)
+ // 数据格式不正确,可能被其他程序修改
public static final int STATE_DATA_DESTROIED = 2;
- // 系统错误(如文件创建失败、IO异常等运行时错误)
+ // 运行时异常导致备份或恢复失败
public static final int STATE_SYSTEM_ERROR = 3;
- // 导出操作成功完成
+ // 备份或恢复成功
public static final int STATE_SUCCESS = 4;
- // 文本导出器实例:封装实际的文本导出逻辑,与工具类解耦
private TextExport mTextExport;
/**
- * 私有构造方法:单例模式的核心,禁止外部直接实例化
- * @param context 应用上下文:传递给TextExport初始化资源
+ * 构造方法
+ *
+ * @param context 上下文对象
*/
private BackupUtils(Context context) {
mTextExport = new TextExport(context);
}
/**
- * 检查外部存储(SD卡)是否可用
- * 仅当SD卡处于“已挂载”状态时,才允许执行导出操作
- * @return true=SD卡已挂载且可读写,false=不可用
+ * 检查外部存储是否可用
+ *
+ * @return 外部存储是否可用
*/
private static boolean externalStorageAvailable() {
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
/**
- * 对外暴露的导出方法:触发便签导出为文本文件的核心逻辑
- * @return 操作状态码:参考BackupUtils的STATE_XXX常量
+ * 将便签导出为文本文件
+ *
+ * 调用内部TextExport类的exportToText方法执行导出操作
+ *
+ * @return 导出状态,参考STATE_*常量
*/
public int exportToText() {
return mTextExport.exportToText();
}
/**
- * 获取本次导出的文件名(导出成功后有效)
- * @return 导出文件的名称(如notes_20251223.txt)
+ * 获取导出的文本文件名
+ *
+ * @return 导出的文本文件名
*/
public String getExportedTextFileName() {
return mTextExport.mFileName;
}
/**
- * 获取本次导出的文件目录(导出成功后有效)
- * @return 导出文件所在的目录路径(如/sdcard/MiNotes/)
+ * 获取导出的文本文件目录
+ *
+ * @return 导出的文本文件目录
*/
public String getExportedTextFileDir() {
return mTextExport.mFileDirectory;
}
/**
- * 内部文本导出器类:封装所有导出相关的核心逻辑
- * 与外部BackupUtils解耦,专注处理“查询便签数据→格式化→写入文本文件”的完整流程
+ * 文本导出内部类,负责具体的导出逻辑
*/
private static class TextExport {
- /**
- * 便签查询投影数组:仅查询导出所需的核心字段,减少IO开销
- * 字段说明:
- * - NoteColumns.ID:便签/文件夹唯一ID;
- * - NoteColumns.MODIFIED_DATE:便签最后修改时间(用于导出展示);
- * - NoteColumns.SNIPPET:便签摘要/文件夹名称;
- * - NoteColumns.TYPE:便签类型(文件夹/普通便签/系统项)。
- */
+ // 便签查询的投影列
private static final String[] NOTE_PROJECTION = {
NoteColumns.ID,
NoteColumns.MODIFIED_DATE,
@@ -155,20 +137,11 @@ public class BackupUtils {
NoteColumns.TYPE
};
- // 便签投影数组列索引常量:简化Cursor取值,避免硬编码
private static final int NOTE_COLUMN_ID = 0;
private static final int NOTE_COLUMN_MODIFIED_DATE = 1;
private static final int NOTE_COLUMN_SNIPPET = 2;
- /**
- * 便签数据查询投影数组:查询便签的具体内容(普通文本/通话记录)
- * 字段说明:
- * - DataColumns.CONTENT:普通便签内容/通话记录附件位置;
- * - DataColumns.MIME_TYPE:数据类型(普通便签/通话记录);
- * - DataColumns.DATA1:通话记录时间;
- * - DataColumns.DATA4:通话记录手机号;
- * 其他DATA字段为预留,暂未使用。
- */
+ // 便签数据查询的投影列
private static final String[] DATA_PROJECTION = {
DataColumns.CONTENT,
DataColumns.MIME_TYPE,
@@ -178,198 +151,174 @@ public class BackupUtils {
DataColumns.DATA4,
};
- // 便签数据投影数组列索引常量
private static final int DATA_COLUMN_CONTENT = 0;
private static final int DATA_COLUMN_MIME_TYPE = 1;
private static final int DATA_COLUMN_CALL_DATE = 2;
private static final int DATA_COLUMN_PHONE_NUMBER = 4;
- /**
- * 导出文本格式模板数组:从资源文件读取,适配多语言,避免硬编码文本格式
- * 数组索引对应:
- * - FORMAT_FOLDER_NAME:文件夹名称展示格式(如“【文件夹:XXX】”);
- * - FORMAT_NOTE_DATE:便签修改时间展示格式(如“修改时间:2025-12-23 15:30”);
- * - FORMAT_NOTE_CONTENT:便签内容展示格式(如“内容:XXX”)。
- */
+ // 文本格式数组
private final String [] TEXT_FORMAT;
- // 格式模板索引常量
private static final int FORMAT_FOLDER_NAME = 0;
private static final int FORMAT_NOTE_DATE = 1;
private static final int FORMAT_NOTE_CONTENT = 2;
- // 上下文:用于访问ContentResolver、资源文件
private Context mContext;
- private String mFileName; // 导出文件的名称(如notes_20251223.txt)
- private String mFileDirectory; // 导出文件的目录路径(如/sdcard/MiNotes/)
+ private String mFileName;
+ private String mFileDirectory;
/**
- * 文本导出器构造方法
- * @param context 应用上下文:读取格式模板、资源字符串
+ * 构造方法
+ *
+ * @param context 上下文对象
*/
public TextExport(Context context) {
- // 从资源文件加载导出文本的格式模板
TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note);
mContext = context;
- // 初始化文件名/目录为空,导出成功后赋值
mFileName = "";
mFileDirectory = "";
}
/**
- * 获取指定索引的文本格式模板
- * @param id 格式模板索引(FORMAT_FOLDER_NAME/FORMAT_NOTE_DATE/FORMAT_NOTE_CONTENT)
- * @return 格式模板字符串
+ * 获取指定ID的格式字符串
+ *
+ * @param id 格式ID
+ * @return 格式字符串
*/
private String getFormat(int id) {
return TEXT_FORMAT[id];
}
/**
- * 导出指定文件夹下的所有便签到打印流
- * @param folderId 文件夹ID:要导出的文件夹唯一标识
- * @param ps 打印流:指向SD卡的导出文件,用于写入文本内容
+ * 将指定文件夹及其包含的便签导出为文本
+ *
+ * @param folderId 文件夹ID
+ * @param ps 打印流
*/
private void exportFolderToText(String folderId, PrintStream ps) {
- // 查询该文件夹下的所有普通便签
+ // 查询该文件夹下的所有便签
Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI,
- NOTE_PROJECTION,
- NoteColumns.PARENT_ID + "=?", // 查询条件:父文件夹ID匹配
- new String[] { folderId },
- null);
+ NOTE_PROJECTION, NoteColumns.PARENT_ID + "=?", new String[] {
+ folderId
+ }, null);
if (notesCursor != null) {
- // 遍历文件夹下的所有便签
if (notesCursor.moveToFirst()) {
do {
- // 1. 打印便签最后修改时间(按格式模板)
+ // 打印便签的最后修改时间
ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format(
- mContext.getString(R.string.format_datetime_mdhm), // 时间格式:月日时分
+ mContext.getString(R.string.format_datetime_mdhm),
notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
- // 2. 导出单条便签的详细内容
+ // 查询属于该便签的数据
String noteId = notesCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (notesCursor.moveToNext());
}
- // 关闭游标,释放数据库资源
notesCursor.close();
}
}
/**
- * 导出单条便签的详细内容到打印流
- * 区分处理普通文本便签和通话记录便签,按不同格式写入内容
- * @param noteId 便签ID:要导出的便签唯一标识
- * @param ps 打印流:指向SD卡的导出文件
+ * 将指定便签导出为文本
+ *
+ * @param noteId 便签ID
+ * @param ps 打印流
*/
private void exportNoteToText(String noteId, PrintStream ps) {
- // 查询该便签的具体数据(内容/通话记录详情)
+ // 查询该便签的所有数据
Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI,
- DATA_PROJECTION,
- DataColumns.NOTE_ID + "=?", // 查询条件:便签ID匹配
- new String[] { noteId },
- null);
+ DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] {
+ noteId
+ }, null);
if (dataCursor != null) {
- // 遍历便签的所有数据项(单条便签可能包含多个数据项,如通话记录+附件)
if (dataCursor.moveToFirst()) {
do {
String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE);
- // 1. 处理通话记录类型便签
if (DataConstants.CALL_NOTE.equals(mimeType)) {
- // 获取通话记录核心信息
+ // 打印电话号码
String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER);
long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE);
String location = dataCursor.getString(DATA_COLUMN_CONTENT);
- // 打印手机号(非空时)
if (!TextUtils.isEmpty(phoneNumber)) {
- ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), phoneNumber));
+ ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
+ phoneNumber));
}
- // 打印通话时间
+ // 打印通话日期
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), DateFormat
- .format(mContext.getString(R.string.format_datetime_mdhm), callDate)));
- // 打印通话记录附件位置(非空时)
+ .format(mContext.getString(R.string.format_datetime_mdhm),
+ callDate)));
+ // 打印通话附件位置
if (!TextUtils.isEmpty(location)) {
- ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), location));
+ ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
+ location));
}
- }
- // 2. 处理普通文本便签
- else if (DataConstants.NOTE.equals(mimeType)) {
+ } else if (DataConstants.NOTE.equals(mimeType)) {
+ // 打印便签内容
String content = dataCursor.getString(DATA_COLUMN_CONTENT);
- // 打印便签内容(非空时)
if (!TextUtils.isEmpty(content)) {
- ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), content));
+ ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
+ content));
}
}
} while (dataCursor.moveToNext());
}
- // 关闭游标,释放资源
dataCursor.close();
}
-
- // 便签内容结束后,添加分隔符(换行+分隔符),区分不同便签
+ // 打印便签之间的分隔线
try {
ps.write(new byte[] {
- Character.LINE_SEPARATOR, Character.LETTER_NUMBER
+ Character.LINE_SEPARATOR, Character.LINE_SEPARATOR
});
} catch (IOException e) {
- // 写入分隔符失败时输出错误日志,不中断整体导出流程
Log.e(TAG, e.toString());
}
}
/**
- * 执行核心导出逻辑:将所有有效便签导出为SD卡上的文本文件
- * 导出流程:
- * 1. 检查SD卡是否可用;
- * 2. 创建导出文件的打印流;
- * 3. 导出所有有效文件夹(排除回收站)+ 通话记录文件夹;
- * 4. 导出根目录下的普通便签;
- * 5. 关闭打印流,返回操作状态码。
- * @return 操作状态码:参考BackupUtils的STATE_XXX常量
+ * 将便签导出为用户可读的文本文件
+ *
+ * 导出过程包括:
+ * 1. 检查SD卡是否可用
+ * 2. 创建导出文件
+ * 3. 导出文件夹及其便签
+ * 4. 导出根目录下的便签
+ *
+ * @return 导出状态,参考STATE_*常量
*/
public int exportToText() {
- // 步骤1:检查SD卡是否挂载,未挂载直接返回对应状态码
if (!externalStorageAvailable()) {
Log.d(TAG, "Media was not mounted");
return STATE_SD_CARD_UNMOUONTED;
}
- // 步骤2:创建导出文件的打印流,失败则返回系统错误
PrintStream ps = getExportToTextPrintStream();
if (ps == null) {
Log.e(TAG, "get print stream error");
return STATE_SYSTEM_ERROR;
}
-
- // 步骤3:导出所有有效文件夹(排除回收站)+ 通话记录文件夹
+
+ // 导出文件夹及其便签
Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
- // 查询条件:
- // - 类型为文件夹 + 父文件夹不是回收站;
- // - 或ID为通话记录文件夹(特殊系统文件夹)
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR "
- + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER,
- null,
- null);
+ + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null);
if (folderCursor != null) {
if (folderCursor.moveToFirst()) {
do {
- // 处理文件夹名称:通话记录文件夹使用固定名称,其他文件夹用摘要
+ // 打印文件夹名称
String folderName = "";
if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) {
folderName = mContext.getString(R.string.call_record_folder_name);
} else {
folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET);
}
- // 打印文件夹名称(非空时)
if (!TextUtils.isEmpty(folderName)) {
ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName));
}
- // 导出该文件夹下的所有便签
String folderId = folderCursor.getString(NOTE_COLUMN_ID);
exportFolderToText(folderId, ps);
} while (folderCursor.moveToNext());
@@ -377,66 +326,53 @@ public class BackupUtils {
folderCursor.close();
}
- // 步骤4:导出根目录下的普通便签(父文件夹ID为0)
+ // 导出根目录下的便签
Cursor noteCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
- // 查询条件:类型为普通便签 + 父文件夹为根目录
NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID
- + "=0",
- null,
- null);
+ + "=0", null, null);
if (noteCursor != null) {
if (noteCursor.moveToFirst()) {
do {
- // 打印便签修改时间
ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
- // 导出单条便签内容
+ // 查询属于该便签的数据
String noteId = noteCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (noteCursor.moveToNext());
}
noteCursor.close();
}
-
- // 步骤5:关闭打印流,释放文件资源
ps.close();
- // 导出成功,返回成功状态码
return STATE_SUCCESS;
}
/**
- * 创建指向SD卡导出文件的PrintStream
- * 核心逻辑:调用generateFileMountedOnSDcard创建文件,再包装为PrintStream
- * @return PrintStream对象(成功)/null(失败)
+ * 获取导出文本的打印流
+ *
+ * @return 打印流
*/
private PrintStream getExportToTextPrintStream() {
- // 生成SD卡上的导出文件(带日期的文本文件)
File file = generateFileMountedOnSDcard(mContext, R.string.file_path,
R.string.file_name_txt_format);
if (file == null) {
Log.e(TAG, "create file to exported failed");
return null;
}
- // 记录导出文件的名称和目录(供外部获取)
mFileName = file.getName();
mFileDirectory = mContext.getString(R.string.file_path);
-
PrintStream ps = null;
try {
- // 创建文件输出流,包装为打印流(方便文本写入)
FileOutputStream fos = new FileOutputStream(file);
ps = new PrintStream(fos);
} catch (FileNotFoundException e) {
- // 文件未找到异常:打印堆栈,返回null
e.printStackTrace();
return null;
} catch (NullPointerException e) {
- // 空指针异常:打印堆栈,返回null
e.printStackTrace();
return null;
}
@@ -445,48 +381,40 @@ public class BackupUtils {
}
/**
- * 在SD卡上生成导出用的文本文件
- * 文件名规则:带当前日期(如notes_20251223.txt),目录为预设的便签备份目录
- * @param context 应用上下文:读取目录路径、文件名格式资源
- * @param filePathResId 目录路径资源ID:如R.string.file_path(/MiNotes/)
- * @param fileNameFormatResId 文件名格式资源ID:如R.string.file_name_txt_format(notes_%s.txt)
- * @return 生成的File对象(成功)/null(失败)
+ * 生成存储导入数据的文本文件
+ *
+ * @param context 上下文对象
+ * @param filePathResId 文件路径资源ID
+ * @param fileNameFormatResId 文件名格式资源ID
+ * @return 生成的文件
*/
private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) {
StringBuilder sb = new StringBuilder();
- // 1. 拼接SD卡根目录 + 预设目录路径(如/sdcard/MiNotes/)
sb.append(Environment.getExternalStorageDirectory());
sb.append(context.getString(filePathResId));
File filedir = new File(sb.toString());
-
- // 2. 拼接完整文件路径:目录 + 带日期的文件名(如/sdcard/MiNotes/notes_20251223.txt)
sb.append(context.getString(
fileNameFormatResId,
- // 格式化文件名中的日期部分(年-月-日)
DateFormat.format(context.getString(R.string.format_date_ymd),
System.currentTimeMillis())));
File file = new File(sb.toString());
try {
- // 3. 创建目录(不存在时)
if (!filedir.exists()) {
filedir.mkdir();
}
- // 4. 创建文件(不存在时)
if (!file.exists()) {
file.createNewFile();
}
- // 5. 返回创建成功的文件对象
return file;
} catch (SecurityException e) {
- // 权限异常(如无SD卡写入权限):打印堆栈,返回null
e.printStackTrace();
} catch (IOException e) {
- // IO异常(如创建文件失败):打印堆栈,返回null
e.printStackTrace();
}
- // 创建失败,返回null
return null;
}
-}
\ No newline at end of file
+}
+
+
diff --git a/src/Notes-master/src/net/micode/notes/tool/CategoryUtil.java b/src/Notes-master/src/net/micode/notes/tool/CategoryUtil.java
new file mode 100644
index 0000000..5a88f56
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/tool/CategoryUtil.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.micode.notes.tool;
+
+import android.text.TextUtils;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * 便签自动分类工具类
+ *
+ * 该类用于根据便签内容自动为便签分配分类,基于关键词匹配的方式实现。
+ * 支持多种分类类型,包括工作、学习、生活、想法、待办等,方便用户对便签进行管理和查找。
+ *
+ * [2025 智能分类特性]: 该类是AI智能分类功能的基础,通过关键词匹配为便签提供初步分类,
+ * 后续可结合AI模型进行更精准的分类。
+ */
+public class CategoryUtil {
+ // 分类关键词映射表,存储关键词与对应分类的映射关系
+ private static final Map CATEGORY_KEYWORDS = new HashMap<>();
+
+ // 静态初始化块,初始化分类关键词
+ static {
+ // 工作相关关键词
+ addKeywords("工作", "工作", "任务", "项目", "会议", "报告", "加班", "上班", "下班", "同事", "领导", "客户", "公司");
+
+ // 学习相关关键词
+ addKeywords("学习", "学习", "考试", "作业", "复习", "预习", "课程", "老师", "学生", "学校", "教材", "笔记", "知识点");
+
+ // 生活相关关键词
+ addKeywords("生活", "生活", "日常", "购物", "吃饭", "旅游", "电影", "音乐", "健身", "运动", "休息", "睡觉", "起床");
+
+ // 想法相关关键词
+ addKeywords("想法", "想法", "创意", "灵感", "思考", "感悟", "心得", "体会", "观点", "意见", "建议");
+
+ // 待办相关关键词
+ addKeywords("待办", "待办", "todo", "需要", "必须", "应该", "计划", "安排", "准备");
+
+ // 其他默认分类
+ addKeywords("其他", "");
+ }
+
+ /**
+ * 批量添加关键词到分类映射表
+ *
+ * @param category 分类名称
+ * @param keywords 关键词数组
+ */
+ private static void addKeywords(String category, String... keywords) {
+ for (String keyword : keywords) {
+ CATEGORY_KEYWORDS.put(keyword, category);
+ }
+ }
+
+ /**
+ * 根据便签内容自动分类
+ *
+ * 通过匹配便签内容中的关键词,为便签分配对应的分类
+ *
+ * @param content 便签内容
+ * @return 分类结果,不超过3个字符
+ */
+ public static String autoCategorize(String content) {
+ if (TextUtils.isEmpty(content)) {
+ return "其他";
+ }
+
+ // 转为小写进行匹配,提高匹配成功率
+ String lowerContent = content.toLowerCase();
+
+ // 遍历关键词,匹配分类
+ for (Map.Entry entry : CATEGORY_KEYWORDS.entrySet()) {
+ String keyword = entry.getKey();
+ String category = entry.getValue();
+
+ // 跳过空关键词(默认分类)
+ if (TextUtils.isEmpty(keyword)) {
+ continue;
+ }
+
+ // 关键词匹配
+ if (lowerContent.contains(keyword.toLowerCase())) {
+ return category;
+ }
+ }
+
+ // 默认分类
+ return "其他";
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/tool/DataUtils.java b/src/Notes-master/src/net/micode/notes/tool/DataUtils.java
index cb09442..874d578 100644
--- a/src/Notes-master/src/net/micode/notes/tool/DataUtils.java
+++ b/src/Notes-master/src/net/micode/notes/tool/DataUtils.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -34,18 +34,25 @@ import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import java.util.ArrayList;
import java.util.HashSet;
+
/**
- * 数据库操作工具类
- * 提供笔记的增删改查、批量操作、数据校验等功能
+ * 数据操作工具类
+ *
+ * 该类提供了一系列静态方法,用于执行便签数据的批量操作,包括批量删除、批量移动、文件夹管理、
+ * 数据查询等功能。它是连接UI层与数据层的桥梁,通过ContentResolver操作ContentProvider,
+ * 实现对便签数据的增删改查。
*/
public class DataUtils {
public static final String TAG = "DataUtils";
-
+
/**
- * 批量删除笔记
- * @param resolver ContentResolver用于数据库操作
- * @param ids 待删除的笔记ID集合
- * @return true删除成功,false删除失败
+ * 批量删除便签
+ *
+ * 通过ContentProvider的批量操作接口,一次性删除多个便签
+ *
+ * @param resolver ContentResolver对象,用于操作ContentProvider
+ * @param ids 需要删除的便签ID集合
+ * @return 是否删除成功
*/
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet ids) {
if (ids == null) {
@@ -57,23 +64,19 @@ public class DataUtils {
return true;
}
- // 批量操作列表,用于执行事务性删除
ArrayList operationList = new ArrayList();
for (long id : ids) {
- // 保护系统根文件夹不被删除
if(id == Notes.ID_ROOT_FOLDER) {
Log.e(TAG, "Don't delete system folder root");
continue;
}
- // 构建删除操作,根据URI删除指定ID的笔记
ContentProviderOperation.Builder builder = ContentProviderOperation
.newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
operationList.add(builder.build());
}
try {
- // 批量执行删除操作
+ // DB操作:批量删除便签
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
- // 检查结果是否有效
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
return false;
@@ -88,47 +91,65 @@ public class DataUtils {
}
/**
- * 移动单条笔记到目标文件夹
- * @param resolver ContentResolver用于数据库操作
- * @param id 笔记ID
- * @param srcFolderId 源文件夹ID(记录原始位置)
+ * 将单个便签移动到指定文件夹
+ *
+ * @param resolver ContentResolver对象
+ * @param id 便签ID
+ * @param srcFolderId 源文件夹ID
* @param desFolderId 目标文件夹ID
*/
public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) {
ContentValues values = new ContentValues();
- values.put(NoteColumns.PARENT_ID, desFolderId); // 设置新的父文件夹
- values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); // 记录原始父文件夹
- values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改
+ values.put(NoteColumns.PARENT_ID, desFolderId);
+ values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId);
+ values.put(NoteColumns.LOCAL_MODIFIED, 1);
+ // DB操作:更新便签的文件夹信息
resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null);
}
/**
- * 批量移动笔记到指定文件夹
- * @param resolver ContentResolver用于数据库操作
- * @param ids 待移动的笔记ID集合
+ * 批量移动便签到指定文件夹
+ *
+ * 支持将多个便签一次性移动到指定文件夹,包括回收站
+ *
+ * @param resolver ContentResolver对象
+ * @param ids 需要移动的便签ID集合
* @param folderId 目标文件夹ID
- * @return true成功,false失败
+ * @return 是否移动成功
*/
public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids,
- long folderId) {
+ long folderId) {
if (ids == null) {
Log.d(TAG, "the ids is null");
return true;
}
- // 批量操作列表,用于执行事务性更新
ArrayList operationList = new ArrayList();
for (long id : ids) {
- // 构建更新操作,修改笔记的父文件夹
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
- builder.withValue(NoteColumns.PARENT_ID, folderId); // 设置新的父文件夹
- builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改
+
+ // 如果是移动到回收站,保存原始父文件夹ID
+ if (folderId == Notes.ID_TRASH_FOLER) {
+ // 查询当前父文件夹ID
+ Cursor cursor = resolver.query(
+ ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id),
+ new String[]{NoteColumns.PARENT_ID},
+ null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ long originParentId = cursor.getLong(0);
+ builder.withValue(NoteColumns.ORIGIN_PARENT_ID, originParentId);
+ cursor.close();
+ }
+ }
+
+ builder.withValue(NoteColumns.PARENT_ID, folderId);
+ builder.withValue(NoteColumns.LOCAL_MODIFIED, 1);
operationList.add(builder.build());
}
try {
- // 批量执行移动操作
+ // DB操作:批量更新便签的文件夹信息
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
@@ -144,12 +165,13 @@ public class DataUtils {
}
/**
- * 获取用户创建的文件夹数量(排除系统文件夹和回收站)
- * @param resolver ContentResolver用于数据库操作
- * @return 文件夹数量
+ * 获取用户创建的文件夹数量(排除系统文件夹)
+ *
+ * @param resolver ContentResolver对象
+ * @return 用户文件夹数量
*/
public static int getUserFolderCount(ContentResolver resolver) {
- // 查询类型为文件夹且父ID不是回收站的记录数
+ // DB操作:查询用户文件夹数量
Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { "COUNT(*)" },
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?",
@@ -160,11 +182,11 @@ public class DataUtils {
if(cursor != null) {
if(cursor.moveToFirst()) {
try {
- count = cursor.getInt(0); // 获取第一列的计数结果
+ count = cursor.getInt(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "get folder count failed:" + e.toString());
} finally {
- cursor.close(); // 确保游标被关闭
+ cursor.close();
}
}
}
@@ -172,14 +194,15 @@ public class DataUtils {
}
/**
- * 检查笔记是否在可见数据库中(不在回收站)
- * @param resolver ContentResolver用于数据库操作
- * @param noteId 笔记ID
- * @param type 笔记类型
- * @return true存在且可见,false不存在或已被删除到回收站
+ * 检查便签是否在数据库中可见(未被删除到回收站)
+ *
+ * @param resolver ContentResolver对象
+ * @param noteId 便签ID
+ * @param type 便签类型
+ * @return 是否可见
*/
public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) {
- // 查询指定ID和类型的笔记,且父ID不是回收站
+ // DB操作:查询便签是否存在且未被删除到回收站
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null,
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER,
@@ -188,7 +211,7 @@ public class DataUtils {
boolean exist = false;
if (cursor != null) {
- if (cursor.getCount() > 0) { // 检查是否有匹配的记录
+ if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
@@ -197,19 +220,20 @@ public class DataUtils {
}
/**
- * 检查笔记是否存在(包括回收站中的)
- * @param resolver ContentResolver用于数据库操作
- * @param noteId 笔记ID
- * @return true存在,false不存在
+ * 检查便签是否存在于数据库中
+ *
+ * @param resolver ContentResolver对象
+ * @param noteId 便签ID
+ * @return 是否存在
*/
public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) {
- // 查询指定ID的笔记,不做其他条件限制
+ // DB操作:查询便签是否存在
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null, null, null, null);
boolean exist = false;
if (cursor != null) {
- if (cursor.getCount() > 0) { // 检查是否有匹配的记录
+ if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
@@ -218,19 +242,20 @@ public class DataUtils {
}
/**
- * 检查数据记录是否存在
- * @param resolver ContentResolver用于数据库操作
+ * 检查数据是否存在于数据库中
+ *
+ * @param resolver ContentResolver对象
* @param dataId 数据ID
- * @return true存在,false不存在
+ * @return 是否存在
*/
public static boolean existInDataDatabase(ContentResolver resolver, long dataId) {
- // 查询指定ID的数据记录
+ // DB操作:查询数据是否存在
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId),
null, null, null, null);
boolean exist = false;
if (cursor != null) {
- if (cursor.getCount() > 0) { // 检查是否有匹配的记录
+ if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
@@ -239,21 +264,22 @@ public class DataUtils {
}
/**
- * 检查可见文件夹名称是否已存在(用于重命名或新建时的冲突检测)
- * @param resolver ContentResolver用于数据库操作
+ * 检查可见文件夹名称是否已存在
+ *
+ * @param resolver ContentResolver对象
* @param name 文件夹名称
- * @return true已存在,false不存在
+ * @return 是否存在
*/
public static boolean checkVisibleFolderName(ContentResolver resolver, String name) {
- // 查询类型为文件夹、不在回收站中且名称匹配的记录
+ // DB操作:查询文件夹名称是否已存在
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null,
NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER +
- " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER +
- " AND " + NoteColumns.SNIPPET + "=?",
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER +
+ " AND " + NoteColumns.SNIPPET + "=?",
new String[] { name }, null);
boolean exist = false;
if(cursor != null) {
- if(cursor.getCount() > 0) { // 检查是否有同名文件夹
+ if(cursor.getCount() > 0) {
exist = true;
}
cursor.close();
@@ -262,13 +288,14 @@ public class DataUtils {
}
/**
- * 获取文件夹中所有笔记的桌面小部件信息
- * @param resolver ContentResolver用于数据库操作
+ * 获取文件夹中包含的小部件属性
+ *
+ * @param resolver ContentResolver对象
* @param folderId 文件夹ID
* @return 小部件属性集合
*/
public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) {
- // 查询文件夹下所有笔记的小部件ID和类型
+ // DB操作:查询文件夹中的小部件信息
Cursor c = resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE },
NoteColumns.PARENT_ID + "=?",
@@ -281,10 +308,9 @@ public class DataUtils {
set = new HashSet();
do {
try {
- // 封装小部件属性对象
AppWidgetAttribute widget = new AppWidgetAttribute();
- widget.widgetId = c.getInt(0); // 小部件ID
- widget.widgetType = c.getInt(1); // 小部件类型
+ widget.widgetId = c.getInt(0);
+ widget.widgetType = c.getInt(1);
set.add(widget);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, e.toString());
@@ -297,13 +323,14 @@ public class DataUtils {
}
/**
- * 通过笔记ID获取通话号码(针对通话记录类型笔记)
- * @param resolver ContentResolver用于数据库操作
- * @param noteId 笔记ID
- * @return 通话号码,失败返回空字符串
+ * 根据便签ID获取通话记录的电话号码
+ *
+ * @param resolver ContentResolver对象
+ * @param noteId 便签ID
+ * @return 电话号码
*/
public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) {
- // 查询指定笔记的通话号码,限定MIME类型为通话记录
+ // DB操作:查询通话记录的电话号码
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.PHONE_NUMBER },
CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?",
@@ -312,36 +339,37 @@ public class DataUtils {
if (cursor != null && cursor.moveToFirst()) {
try {
- return cursor.getString(0); // 获取通话号码
+ return cursor.getString(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call number fails " + e.toString());
} finally {
- cursor.close(); // 确保游标被关闭
+ cursor.close();
}
}
return "";
}
/**
- * 通过通话号码和日期查找对应的笔记ID
- * @param resolver ContentResolver用于数据库操作
- * @param phoneNumber 通话号码
- * @param callDate 通话日期时间戳
- * @return 笔记ID,未找到返回0
+ * 根据电话号码和通话日期获取通话记录的便签ID
+ *
+ * @param resolver ContentResolver对象
+ * @param phoneNumber 电话号码
+ * @param callDate 通话日期
+ * @return 便签ID
*/
public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) {
- // 使用自定义的PHONE_NUMBERS_EQUAL函数比较号码,确保同一联系人的不同格式号码能匹配
+ // DB操作:查询通话记录的便签ID
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.NOTE_ID },
CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL("
- + CallNote.PHONE_NUMBER + ",?)",
+ + CallNote.PHONE_NUMBER + ",?)",
new String [] { String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber },
null);
if (cursor != null) {
if (cursor.moveToFirst()) {
try {
- return cursor.getLong(0); // 获取笔记ID
+ return cursor.getLong(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call note id fails " + e.toString());
}
@@ -352,14 +380,14 @@ public class DataUtils {
}
/**
- * 通过笔记ID获取摘要内容
- * @param resolver ContentResolver用于数据库操作
- * @param noteId 笔记ID
- * @return 摘要内容
- * @throws IllegalArgumentException 笔记不存在时抛出异常
+ * 根据便签ID获取便签摘要
+ *
+ * @param resolver ContentResolver对象
+ * @param noteId 便签ID
+ * @return 便签摘要
*/
public static String getSnippetById(ContentResolver resolver, long noteId) {
- // 查询指定ID笔记的摘要字段
+ // DB操作:查询便签摘要
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String [] { NoteColumns.SNIPPET },
NoteColumns.ID + "=?",
@@ -369,27 +397,94 @@ public class DataUtils {
if (cursor != null) {
String snippet = "";
if (cursor.moveToFirst()) {
- snippet = cursor.getString(0); // 获取摘要内容
+ snippet = cursor.getString(0);
}
cursor.close();
return snippet;
}
- throw new IllegalArgumentException("Note is not found with id: " + noteId);
+ // 如果找不到noteId,返回空字符串,而不是抛出异常
+ return "";
}
/**
- * 格式化笔记摘要(取第一行非空内容并去除首尾空格)
+ * 格式化便签摘要
+ *
+ * 去除首尾空格,并截取第一行作为摘要
+ *
* @param snippet 原始摘要
* @return 格式化后的摘要
*/
public static String getFormattedSnippet(String snippet) {
if (snippet != null) {
- snippet = snippet.trim(); // 去除首尾空格
+ snippet = snippet.trim();
int index = snippet.indexOf('\n');
if (index != -1) {
- snippet = snippet.substring(0, index); // 只保留第一行
+ snippet = snippet.substring(0, index);
}
}
return snippet;
}
-}
\ No newline at end of file
+
+ /**
+ * 根据文件夹名称获取文件夹ID
+ *
+ * @param resolver ContentResolver
+ * @param folderName 文件夹名称
+ * @return 文件夹ID,若不存在则返回0
+ */
+ public static long getFolderIdByName(ContentResolver resolver, String folderName) {
+ // DB操作:根据文件夹名称查询文件夹ID
+ Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
+ new String[] { NoteColumns.ID },
+ NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "=? AND " + NoteColumns.SNIPPET + "=?",
+ new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_ROOT_FOLDER), folderName },
+ null);
+
+ long folderId = 0;
+ if (cursor != null) {
+ if (cursor.moveToFirst()) {
+ try {
+ folderId = cursor.getLong(0);
+ } catch (IndexOutOfBoundsException e) {
+ Log.e(TAG, "Get folder id failed: " + e.toString());
+ }
+ }
+ cursor.close();
+ }
+
+ return folderId;
+ }
+
+ /**
+ * 创建新文件夹
+ *
+ * 如果文件夹已存在,则返回已存在的文件夹ID
+ *
+ * @param resolver ContentResolver
+ * @param folderName 文件夹名称
+ * @return 新创建的文件夹ID,若创建失败则返回0
+ */
+ public static long createFolder(ContentResolver resolver, String folderName) {
+ // 检查文件夹是否已存在
+ long existingFolderId = getFolderIdByName(resolver, folderName);
+ if (existingFolderId > 0) {
+ return existingFolderId;
+ }
+
+ // 创建新文件夹
+ ContentValues values = new ContentValues();
+ values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
+ values.put(NoteColumns.PARENT_ID, Notes.ID_ROOT_FOLDER);
+ values.put(NoteColumns.SNIPPET, folderName);
+ values.put(NoteColumns.NOTES_COUNT, 0);
+
+ // DB操作:插入新文件夹
+ android.net.Uri uri = resolver.insert(Notes.CONTENT_NOTE_URI, values);
+ if (uri != null) {
+ return ContentUris.parseId(uri);
+ }
+
+ Log.e(TAG, "Create folder failed: " + folderName);
+ return 0;
+ }
+}
diff --git a/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java b/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java
index 0af3f5e..b409c0a 100644
--- a/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java
+++ b/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java
@@ -14,159 +14,123 @@
* limitations under the License.
*/
-// 包声明:归属小米便签的工具模块,定义Google Tasks(GTask)同步相关的核心字符串常量
package net.micode.notes.tool;
/**
- * Google Tasks(GTask)同步字符串常量类
- * 核心职责:
- * 1. 统一定义便签与GTask同步过程中JSON交互的所有字段名,避免硬编码;
- * 2. 定义GTask侧的文件夹命名规则(区分MIUI便签专属文件夹、系统默认文件夹);
- * 3. 定义同步元数据的标识字段(用于存储GTask与便签的映射关系);
- * 设计目的:提升代码可维护性,便于统一修改GTask同步的字段/命名规则。
+ * GTask 字符串工具类
+ *
+ * 该类定义了与 GTask 相关的 JSON 字段常量,用于 GTask 功能的实现。
+ *
*/
public class GTaskStringUtils {
- // ======================== GTask同步JSON交互 - 动作相关字段 ========================
- /** JSON字段:动作ID(标识单次同步操作的唯一ID) */
+ /**
+ * GTask JSON 字段常量定义
+ */
+ // 动作相关字段
public final static String GTASK_JSON_ACTION_ID = "action_id";
-
- /** JSON字段:动作列表(批量同步时存储多个动作的数组) */
public final static String GTASK_JSON_ACTION_LIST = "action_list";
-
- /** JSON字段:动作类型(标识当前同步动作的类型,如创建/查询/移动/更新) */
public final static String GTASK_JSON_ACTION_TYPE = "action_type";
-
- /** JSON字段:动作类型-创建(同步时向GTask创建新任务/文件夹) */
public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create";
-
- /** JSON字段:动作类型-全量查询(从GTask拉取所有任务/文件夹数据) */
public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all";
-
- /** JSON字段:动作类型-移动(将GTask任务/文件夹移动到指定目录) */
public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move";
-
- /** JSON字段:动作类型-更新(更新GTask中已存在的任务/文件夹信息) */
public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update";
-
- // ======================== GTask同步JSON交互 - 实体/用户相关字段 ========================
- /** JSON字段:创建者ID(标识GTask实体的创建者账号ID) */
+
+ // 创建者相关字段
public final static String GTASK_JSON_CREATOR_ID = "creator_id";
-
- /** JSON字段:子实体(存储GTask文件夹下的子任务/子文件夹) */
+
+ // 子实体相关字段
public final static String GTASK_JSON_CHILD_ENTITY = "child_entity";
-
- /** JSON字段:客户端版本(标识便签客户端的版本号,用于GTask服务端兼容) */
+
+ // 客户端版本字段
public final static String GTASK_JSON_CLIENT_VERSION = "client_version";
-
- /** JSON字段:完成状态(标识GTask任务是否已完成,布尔值) */
+
+ // 完成状态字段
public final static String GTASK_JSON_COMPLETED = "completed";
-
- /** JSON字段:当前列表ID(标识任务/文件夹所属的GTask列表ID) */
+
+ // 列表相关字段
public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id";
-
- /** JSON字段:默认列表ID(GTask默认任务列表的ID) */
public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id";
-
- /** JSON字段:删除标记(标识GTask实体是否已被删除,布尔值) */
+
+ // 删除相关字段
public final static String GTASK_JSON_DELETED = "deleted";
-
- /** JSON字段:目标列表(移动操作时的目标列表ID) */
+
+ // 移动相关字段
public final static String GTASK_JSON_DEST_LIST = "dest_list";
-
- /** JSON字段:目标父节点(移动操作时的目标父实体ID) */
public final static String GTASK_JSON_DEST_PARENT = "dest_parent";
-
- /** JSON字段:目标父节点类型(移动操作时目标父实体的类型:GROUP/TASK) */
public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type";
-
- /** JSON字段:实体增量(同步时仅传输实体的变更部分,减少数据传输) */
+
+ // 实体相关字段
public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta";
-
- /** JSON字段:实体类型(标识GTask实体的类型:任务/文件夹) */
public final static String GTASK_JSON_ENTITY_TYPE = "entity_type";
-
- /** JSON字段:获取已删除项(同步时是否拉取GTask中已删除的实体) */
+
+ // 获取删除相关字段
public final static String GTASK_JSON_GET_DELETED = "get_deleted";
-
- /** JSON字段:实体ID(GTask任务/文件夹的唯一标识ID) */
+
+ // ID 相关字段
public final static String GTASK_JSON_ID = "id";
-
- /** JSON字段:索引(GTask实体在父节点中的排序索引) */
public final static String GTASK_JSON_INDEX = "index";
-
- /** JSON字段:最后修改时间(GTask实体的最后修改时间戳) */
+
+ // 修改时间相关字段
public final static String GTASK_JSON_LAST_MODIFIED = "last_modified";
-
- /** JSON字段:最新同步点(标识上次同步的位置,用于增量同步) */
+
+ // 同步相关字段
public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point";
-
- /** JSON字段:列表ID(GTask列表的唯一标识ID) */
+
+ // 列表相关字段
public final static String GTASK_JSON_LIST_ID = "list_id";
-
- /** JSON字段:列表数组(存储GTask所有列表的数组) */
public final static String GTASK_JSON_LISTS = "lists";
-
- /** JSON字段:名称(GTask任务/文件夹的名称) */
+
+ // 名称相关字段
public final static String GTASK_JSON_NAME = "name";
-
- /** JSON字段:新ID(创建/移动操作后生成的新实体ID) */
+
+ // 新 ID 字段
public final static String GTASK_JSON_NEW_ID = "new_id";
-
- /** JSON字段:备注(GTask任务的备注内容,对应便签的正文) */
+
+ // 便签相关字段
public final static String GTASK_JSON_NOTES = "notes";
-
- /** JSON字段:父节点ID(GTask实体的父文件夹/父任务ID) */
+
+ // 父级相关字段
public final static String GTASK_JSON_PARENT_ID = "parent_id";
-
- /** JSON字段:前序兄弟ID(标识实体在父节点中的前一个兄弟实体ID,用于排序) */
+
+ // 排序相关字段
public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id";
-
- /** JSON字段:同步结果(GTask服务端返回的同步操作结果) */
+
+ // 结果相关字段
public final static String GTASK_JSON_RESULTS = "results";
-
- /** JSON字段:源列表(移动操作时的源列表ID) */
+
+ // 源列表字段
public final static String GTASK_JSON_SOURCE_LIST = "source_list";
-
- /** JSON字段:任务数组(存储GTask所有任务的数组) */
+
+ // 任务相关字段
public final static String GTASK_JSON_TASKS = "tasks";
-
- /** JSON字段:类型(兼容字段,同entity_type) */
+
+ // 类型相关字段
public final static String GTASK_JSON_TYPE = "type";
-
- /** JSON字段:类型-分组(GTask文件夹的类型标识) */
public final static String GTASK_JSON_TYPE_GROUP = "GROUP";
-
- /** JSON字段:类型-任务(GTask普通任务的类型标识) */
public final static String GTASK_JSON_TYPE_TASK = "TASK";
-
- /** JSON字段:用户(标识GTask所属的用户账号信息) */
+
+ // 用户相关字段
public final static String GTASK_JSON_USER = "user";
-
- // ======================== GTask侧文件夹命名规则 ========================
- /** MIUI便签专属文件夹前缀(区分GTask中其他文件夹,避免命名冲突) */
+
+ // MIUI 文件夹前缀
public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]";
-
- /** GTask侧默认文件夹名称(对应便签的根目录) */
+
+ // 默认文件夹
public final static String FOLDER_DEFAULT = "Default";
-
- /** GTask侧通话记录文件夹名称(对应便签的通话记录文件夹) */
+
+ // 通话便签文件夹
public final static String FOLDER_CALL_NOTE = "Call_Note";
-
- /** GTask侧元数据文件夹名称(存储便签与GTask的同步映射元数据) */
+
+ // 元数据文件夹
public final static String FOLDER_META = "METADATA";
-
- // ======================== 同步元数据标识字段 ========================
- /** 元数据头-GTask ID(存储便签对应的GTask实体ID) */
+
+ // 元数据头信息
public final static String META_HEAD_GTASK_ID = "meta_gid";
-
- /** 元数据头-便签(存储便签核心信息的元数据标识) */
public final static String META_HEAD_NOTE = "meta_note";
-
- /** 元数据头-数据(存储便签扩展数据的元数据标识) */
public final static String META_HEAD_DATA = "meta_data";
-
- /** 元数据便签名称(GTask中元数据便签的固定名称,禁止修改/删除) */
+
+ // 元数据便签名称
public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE";
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java b/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java
index beca99b..4554684 100644
--- a/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java
+++ b/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -24,36 +24,44 @@ import net.micode.notes.ui.NotesPreferenceActivity;
/**
* 资源解析工具类
- * 负责管理笔记背景、字体大小、桌面小部件等资源的ID映射
- * 提供常量定义和静态方法获取各类资源
+ *
+ * 该类定义了便签的背景颜色、字体大小等常量,并提供了获取各种资源的方法。
+ * 它包含多个内部类,分别管理便签编辑界面、列表界面、小部件和文本外观的资源。
*/
public class ResourceParser {
- // 背景颜色常量定义
+ /**
+ * 背景颜色常量定义
+ */
public static final int YELLOW = 0;
public static final int BLUE = 1;
public static final int WHITE = 2;
public static final int GREEN = 3;
public static final int RED = 4;
- // 默认背景颜色
+ /**
+ * 默认背景颜色
+ */
public static final int BG_DEFAULT_COLOR = YELLOW;
- // 字体大小常量定义
+ /**
+ * 字体大小常量定义
+ */
public static final int TEXT_SMALL = 0;
public static final int TEXT_MEDIUM = 1;
public static final int TEXT_LARGE = 2;
public static final int TEXT_SUPER = 3;
- // 默认字体大小
+ /**
+ * 默认字体大小
+ */
public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM;
/**
- * 笔记编辑界面背景资源类
- * 管理编辑页面不同颜色主题的背景图片
+ * 便签编辑界面背景资源管理类
*/
public static class NoteBgResources {
- // 编辑界面背景资源数组,索引对应颜色常量
+ // 编辑界面背景资源数组
private final static int [] BG_EDIT_RESOURCES = new int [] {
R.drawable.edit_yellow,
R.drawable.edit_blue,
@@ -62,7 +70,7 @@ public class ResourceParser {
R.drawable.edit_red
};
- // 编辑界面标题栏背景资源数组
+ // 编辑界面标题背景资源数组
private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] {
R.drawable.edit_title_yellow,
R.drawable.edit_title_blue,
@@ -72,8 +80,9 @@ public class ResourceParser {
};
/**
- * 获取笔记编辑界面背景资源ID
- * @param id 颜色常量索引
+ * 获取便签编辑界面背景资源
+ *
+ * @param id 背景颜色ID
* @return 背景资源ID
*/
public static int getNoteBgResource(int id) {
@@ -81,9 +90,10 @@ public class ResourceParser {
}
/**
- * 获取笔记标题栏背景资源ID
- * @param id 颜色常量索引
- * @return 标题栏背景资源ID
+ * 获取便签编辑界面标题背景资源
+ *
+ * @param id 背景颜色ID
+ * @return 标题背景资源ID
*/
public static int getNoteTitleBgResource(int id) {
return BG_EDIT_TITLE_RESOURCES[id];
@@ -92,28 +102,26 @@ public class ResourceParser {
/**
* 获取默认背景颜色ID
- * 根据用户设置决定是随机颜色还是固定黄色
- * @param context 应用上下文
- * @return 背景颜色常量索引
+ *
+ * 根据用户偏好设置,返回默认的背景颜色ID
+ *
+ * @param context 上下文对象
+ * @return 背景颜色ID
*/
public static int getDefaultBgId(Context context) {
- // 检查用户是否启用了随机背景色设置
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) {
- // 生成随机颜色索引
return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length);
} else {
- // 返回默认黄色
return BG_DEFAULT_COLOR;
}
}
/**
- * 笔记列表项背景资源类
- * 管理列表中不同位置(首项、中间项、末项、单项)的背景图片
+ * 便签列表项背景资源管理类
*/
public static class NoteItemBgResources {
- // 列表首项背景资源数组
+ // 列表项第一项背景资源数组
private final static int [] BG_FIRST_RESOURCES = new int [] {
R.drawable.list_yellow_up,
R.drawable.list_blue_up,
@@ -122,7 +130,7 @@ public class ResourceParser {
R.drawable.list_red_up
};
- // 列表中间项背景资源数组
+ // 列表项中间项背景资源数组
private final static int [] BG_NORMAL_RESOURCES = new int [] {
R.drawable.list_yellow_middle,
R.drawable.list_blue_middle,
@@ -131,7 +139,7 @@ public class ResourceParser {
R.drawable.list_red_middle
};
- // 列表末项背景资源数组
+ // 列表项最后一项背景资源数组
private final static int [] BG_LAST_RESOURCES = new int [] {
R.drawable.list_yellow_down,
R.drawable.list_blue_down,
@@ -140,7 +148,7 @@ public class ResourceParser {
R.drawable.list_red_down,
};
- // 列表单一项背景资源数组(只有一项时使用)
+ // 列表项单独一项背景资源数组
private final static int [] BG_SINGLE_RESOURCES = new int [] {
R.drawable.list_yellow_single,
R.drawable.list_blue_single,
@@ -150,8 +158,9 @@ public class ResourceParser {
};
/**
- * 获取列表首项背景资源ID
- * @param id 颜色常量索引
+ * 获取列表项第一项背景资源
+ *
+ * @param id 背景颜色ID
* @return 背景资源ID
*/
public static int getNoteBgFirstRes(int id) {
@@ -159,8 +168,9 @@ public class ResourceParser {
}
/**
- * 获取列表末项背景资源ID
- * @param id 颜色常量索引
+ * 获取列表项最后一项背景资源
+ *
+ * @param id 背景颜色ID
* @return 背景资源ID
*/
public static int getNoteBgLastRes(int id) {
@@ -168,8 +178,9 @@ public class ResourceParser {
}
/**
- * 获取列表单一项背景资源ID
- * @param id 颜色常量索引
+ * 获取列表项单独一项背景资源
+ *
+ * @param id 背景颜色ID
* @return 背景资源ID
*/
public static int getNoteBgSingleRes(int id) {
@@ -177,8 +188,9 @@ public class ResourceParser {
}
/**
- * 获取列表中间项背景资源ID
- * @param id 颜色常量索引
+ * 获取列表项中间项背景资源
+ *
+ * @param id 背景颜色ID
* @return 背景资源ID
*/
public static int getNoteBgNormalRes(int id) {
@@ -186,7 +198,8 @@ public class ResourceParser {
}
/**
- * 获取文件夹背景资源ID(固定资源)
+ * 获取文件夹背景资源
+ *
* @return 文件夹背景资源ID
*/
public static int getFolderBgRes() {
@@ -195,8 +208,7 @@ public class ResourceParser {
}
/**
- * 桌面小部件背景资源类
- * 管理2x2和4x4尺寸小部件的背景图片
+ * 小部件背景资源管理类
*/
public static class WidgetBgResources {
// 2x2小部件背景资源数组
@@ -209,8 +221,9 @@ public class ResourceParser {
};
/**
- * 获取2x2小部件背景资源ID
- * @param id 颜色常量索引
+ * 获取2x2小部件背景资源
+ *
+ * @param id 背景颜色ID
* @return 背景资源ID
*/
public static int getWidget2xBgResource(int id) {
@@ -227,8 +240,9 @@ public class ResourceParser {
};
/**
- * 获取4x4小部件背景资源ID
- * @param id 颜色常量索引
+ * 获取4x4小部件背景资源
+ *
+ * @param id 背景颜色ID
* @return 背景资源ID
*/
public static int getWidget4xBgResource(int id) {
@@ -237,11 +251,10 @@ public class ResourceParser {
}
/**
- * 文本外观资源类
- * 管理不同字体大小的样式资源
+ * 文本外观资源管理类
*/
public static class TextAppearanceResources {
- // 字体大小样式资源数组,索引对应字体大小常量
+ // 文本外观资源数组
private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] {
R.style.TextAppearanceNormal,
R.style.TextAppearanceMedium,
@@ -250,27 +263,30 @@ public class ResourceParser {
};
/**
- * 获取文本外观资源ID
- * @param id 字体大小常量索引
- * @return 样式资源ID
+ * 获取文本外观资源
+ *
+ * @param id 字体大小ID
+ * @return 文本外观资源ID
*/
public static int getTexAppearanceResource(int id) {
/**
- * HACKME: 修复将资源ID存储在SharedPreference中的bug
- * 存储的ID可能大于资源数组长度,此时返回默认字体大小
+ * HACKME: Fix bug of store the resource id in shared preference.
+ * The id may larger than the length of resources, in this case,
+ * return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE}
*/
if (id >= TEXTAPPEARANCE_RESOURCES.length) {
- return BG_DEFAULT_FONT_SIZE; // 越界时返回默认值
+ return BG_DEFAULT_FONT_SIZE;
}
return TEXTAPPEARANCE_RESOURCES[id];
}
/**
- * 获取可用资源数量
- * @return 字体大小选项总数
+ * 获取文本外观资源数量
+ *
+ * @return 资源数量
*/
public static int getResourcesSize() {
return TEXTAPPEARANCE_RESOURCES.length;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/tool/UserManager.java b/src/Notes-master/src/net/micode/notes/tool/UserManager.java
new file mode 100644
index 0000000..fa0436b
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/tool/UserManager.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.micode.notes.tool;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+import net.micode.notes.data.NotesDatabaseHelper;
+import net.micode.notes.data.Users;
+
+/**
+ * 用户管理类
+ *
+ * 该类用于管理当前登录用户的信息,包括保存、获取和清除用户信息。
+ * 采用单例模式,通过SharedPreferences持久化存储用户信息,
+ * 同时提供用户密码验证功能。
+ */
+public class UserManager {
+ private static final String TAG = "UserManager";
+ private static final String PREF_NAME = "user_preferences";
+ private static final String KEY_CURRENT_USER_ID = "current_user_id";
+ private static final String KEY_CURRENT_USERNAME = "current_username";
+
+ private static UserManager sInstance;
+ private SharedPreferences mPrefs;
+ private Context mContext;
+
+ /**
+ * 构造方法
+ *
+ * @param context 上下文对象
+ */
+ private UserManager(Context context) {
+ mContext = context.getApplicationContext();
+ mPrefs = mContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+ }
+
+ /**
+ * 获取UserManager实例
+ *
+ * 采用单例模式,确保整个应用中只有一个UserManager实例
+ *
+ * @param context 上下文对象
+ * @return UserManager实例
+ */
+ public static synchronized UserManager getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new UserManager(context);
+ }
+ return sInstance;
+ }
+
+ /**
+ * 保存当前登录用户的信息
+ *
+ * 将用户ID和用户名保存到SharedPreferences中,持久化存储
+ *
+ * @param userId 用户ID
+ * @param username 用户名
+ */
+ public void saveCurrentUser(long userId, String username) {
+ SharedPreferences.Editor editor = mPrefs.edit();
+ editor.putLong(KEY_CURRENT_USER_ID, userId);
+ editor.putString(KEY_CURRENT_USERNAME, username);
+ editor.apply();
+ }
+
+ /**
+ * 获取当前登录用户的ID
+ *
+ * @return 当前用户ID,未登录返回-1
+ */
+ public long getCurrentUserId() {
+ return mPrefs.getLong(KEY_CURRENT_USER_ID, -1);
+ }
+
+ /**
+ * 获取当前登录用户的用户名
+ *
+ * @return 当前用户名,未登录返回null
+ */
+ public String getCurrentUsername() {
+ return mPrefs.getString(KEY_CURRENT_USERNAME, null);
+ }
+
+ /**
+ * 清除当前用户信息,用于退出登录
+ */
+ public void clearCurrentUser() {
+ SharedPreferences.Editor editor = mPrefs.edit();
+ editor.remove(KEY_CURRENT_USER_ID);
+ editor.remove(KEY_CURRENT_USERNAME);
+ editor.apply();
+ }
+
+ /**
+ * 检查是否已登录
+ *
+ * @return 是否已登录
+ */
+ public boolean isLoggedIn() {
+ return getCurrentUserId() != -1;
+ }
+
+ /**
+ * 验证用户密码
+ *
+ * 通过直接访问数据库,验证用户ID和密码是否匹配
+ *
+ * @param userId 用户ID
+ * @param password 输入的密码
+ * @return 密码是否正确
+ */
+ public boolean validatePassword(long userId, String password) {
+ try {
+ // DB操作:验证用户密码
+ NotesDatabaseHelper helper = NotesDatabaseHelper.getInstance(mContext);
+ SQLiteDatabase db = helper.getReadableDatabase();
+
+ Cursor cursor = null;
+ try {
+ String selection = Users.UserColumns.ID + " = ? AND " + Users.UserColumns.PASSWORD + " = ?";
+ String[] selectionArgs = {String.valueOf(userId), password};
+
+ cursor = db.query(
+ NotesDatabaseHelper.TABLE.USER,
+ new String[]{Users.UserColumns.ID},
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ );
+
+ return cursor != null && cursor.moveToFirst();
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ /**
+ * 设置当前用户
+ *
+ * 根据用户ID查询用户名,然后保存当前用户信息
+ *
+ * @param userId 用户ID
+ */
+ public void setCurrentUser(long userId) {
+ try {
+ // DB操作:查询用户的用户名
+ NotesDatabaseHelper helper = NotesDatabaseHelper.getInstance(mContext);
+ SQLiteDatabase db = helper.getReadableDatabase();
+
+ Cursor cursor = null;
+ String username = "未知用户";
+ try {
+ String selection = Users.UserColumns.ID + " = ?";
+ String[] selectionArgs = {String.valueOf(userId)};
+
+ cursor = db.query(
+ NotesDatabaseHelper.TABLE.USER,
+ new String[]{Users.UserColumns.USERNAME},
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ );
+
+ if (cursor != null && cursor.moveToFirst()) {
+ username = cursor.getString(0);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ // 保存当前用户信息
+ SharedPreferences.Editor editor = mPrefs.edit();
+ editor.putLong(KEY_CURRENT_USER_ID, userId);
+ editor.putString(KEY_CURRENT_USERNAME, username);
+ editor.apply();
+ } catch (Exception e) {
+ Log.e(TAG, "Error in setCurrentUser: " + e.getMessage());
+ e.printStackTrace();
+ // 即使发生异常,也确保保存用户ID,避免状态不一致
+ try {
+ SharedPreferences.Editor editor = mPrefs.edit();
+ editor.putLong(KEY_CURRENT_USER_ID, userId);
+ editor.putString(KEY_CURRENT_USERNAME, "未知用户");
+ editor.apply();
+ } catch (Exception innerE) {
+ Log.e(TAG, "Error in emergency save: " + innerE.getMessage());
+ innerE.printStackTrace();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java b/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java
index be52332..2be6551 100644
--- a/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java
+++ b/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java
@@ -14,260 +14,223 @@
* limitations under the License.
*/
-// 包声明:归属小米便签UI模块,便签提醒功能的最终展示页,是提醒触发时用户感知的核心页面
package net.micode.notes.ui;
-// 安卓页面核心基类,所有页面的父类,提供页面生命周期、窗口管理、组件交互等基础能力
import android.app.Activity;
-// 安卓系统对话框核心类,用于展示标准化的弹窗,承载提醒内容与交互按钮,是本页面的核心展示载体
import android.app.AlertDialog;
-// 安卓上下文核心类,提供系统服务获取、资源访问、组件通信等基础能力
import android.content.Context;
-// 安卓对话框事件回调相关类,处理按钮点击、对话框关闭等交互事件,实现页面的核心交互逻辑
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
-// 安卓意图核心类,用于页面跳转、数据传递,此处用于跳转便签编辑页并携带便签ID
import android.content.Intent;
-// 安卓音频管理核心类,用于指定音频流类型,适配系统音量与静音规则,控制提醒铃声的播放策略
import android.media.AudioManager;
-// 安卓媒体播放核心类,用于加载、播放、停止系统铃声资源,实现提醒音效的播放功能
import android.media.MediaPlayer;
-// 安卓铃声管理核心类,用于获取系统默认的闹钟铃声Uri,统一访问系统铃声资源
import android.media.RingtoneManager;
-// 安卓统一资源标识类,用于标记铃声资源地址、便签数据的唯一标识
import android.net.Uri;
-// 安卓页面状态保存类,用于横竖屏切换等场景的页面数据恢复,本页面未使用
import android.os.Bundle;
-// 安卓电源管理核心类,用于获取设备屏幕的亮灭状态,判断当前是亮屏/熄屏/锁屏状态
import android.os.PowerManager;
-// 安卓系统设置核心类,用于读取系统的铃声静音模式配置,适配不同的系统音频策略
import android.provider.Settings;
-// 安卓窗口管理相关类,用于配置页面窗口的显示属性,实现锁屏显示、屏幕唤醒、常亮等核心功能
+import android.util.Log;
import android.view.Window;
import android.view.WindowManager;
-// 小米便签资源常量类,引用字符串、布局、颜色等本地资源,统一管理资源ID
import net.micode.notes.R;
-// 便签应用核心数据常量类,定义便签类型、ContentURI等全局常量,用于数据合法性校验
import net.micode.notes.data.Notes;
-// 便签数据工具类,封装数据库查询相关的通用方法,提供便签摘要查询、便签存在性校验等能力
import net.micode.notes.tool.DataUtils;
-// Java IO异常类,捕获媒体播放器加载铃声资源时的IO错误,保证程序健壮性
import java.io.IOException;
+
/**
- * 便签提醒功能核心弹窗页面【提醒展示最终页、用户感知唯一页】
- * 继承:Activity 安卓页面基类,具备页面生命周期与窗口管理能力
- * 实现:OnClickListener + OnDismissListener 对话框交互监听器,处理按钮点击与弹窗关闭事件
- * 核心定位:小米便签「提醒功能」的终点站,是整个提醒流程的最终展示载体,也是用户能直接感知到的核心页面,所有提醒逻辑最终都汇聚于此,是便签提醒功能的核心价值体现
- * 核心设计意义:作为提醒触发后的唯一展示页面,承担「强提醒+友好交互+精准跳转」的核心职责,既要保证用户不会错过提醒,又要提供便捷的后续操作,兼顾提醒的强制性与使用的人性化
- * 核心职责(四大核心模块,逻辑闭环完整):
- * 一、屏幕状态智能管控【优先级最高,保障提醒可见性】
- * 1. 精准判断设备当前屏幕状态:亮屏/熄屏/锁屏;
- * 2. 熄屏/锁屏场景:自动唤醒屏幕、保持屏幕常亮、允许锁屏时显示窗口、适配系统装饰布局,确保弹窗能穿透锁屏界面直接展示,用户无需解锁即可看到提醒,核心保障「提醒必见」;
- * 3. 亮屏场景:仅展示弹窗,不修改任何屏幕状态,避免干扰用户当前操作,兼顾使用体验;
- * 4. 统一隐藏原生页面标题栏,仅展示核心的提醒弹窗,页面极简无冗余。
- * 二、提醒数据解析与合法性校验【数据安全,避免无效展示】
- * 1. 从跳转Intent中解析出绑定的便签ID:该ID由AlarmReceiver透传而来,是当前提醒的核心标识;
- * 2. 根据便签ID查询数据库,获取便签的文本摘要内容,用于弹窗展示;
- * 3. 摘要内容规范化处理:超过60字符自动裁剪并添加省略号,保证弹窗展示美观、无内容溢出;
- * 4. 关键合法性校验:通过工具类判断该便签是否真实存在于数据库中(用户可能已删除该便签),仅当便签有效时才展示弹窗和播放铃声,无效则直接关闭页面,无任何无效操作。
- * 三、系统铃声智能播放【听觉提醒,保障提醒感知】
- * 1. 自动获取系统默认的闹钟铃声Uri,统一使用系统级铃声资源,适配用户的个性化铃声设置;
- * 2. 适配系统静音模式:读取系统铃声流配置,自动选择合适的音频流类型,遵循系统的音量与静音规则,不强制发声干扰用户;
- * 3. 铃声播放策略:循环播放提醒铃声,直到用户点击按钮关闭弹窗,确保用户能听到提醒;
- * 4. 异常兜底处理:捕获播放器初始化、资源加载的各类异常,打印日志不崩溃,保证页面稳定性;
- * 5. 资源安全释放:弹窗关闭时立即停止播放并释放媒体播放器资源,杜绝内存泄漏与音频残留。
- * 四、人性化交互设计【便捷操作,闭环提醒流程】
- * 1. 弹窗内容极简清晰:标题为应用名称,内容为便签摘要,核心信息一目了然;
- * 2. 按钮动态适配:亮屏时展示「确认」+「进入便签」双按钮,熄屏/锁屏时仅展示「确认」单按钮,贴合不同场景的用户操作习惯;
- * 3. 精准交互逻辑:「确认」按钮仅关闭弹窗,「进入便签」按钮跳转至该便签的编辑页面,直接定位到对应内容,无需用户手动查找;
- * 4. 弹窗关闭统一处理:无论点击按钮还是手动关闭弹窗,均触发统一的收尾逻辑,停止铃声并关闭页面,逻辑闭环无遗漏。
- * 核心特性&关键技术要点:
- * 1. 优先级保障:窗口标记配置为最高优先级,能穿透锁屏、熄屏界面展示,是安卓系统中「强提醒」的标准实现方式;
- * 2. 资源安全:所有系统服务、媒体播放器资源均做了精准的创建与释放,无内存泄漏、无资源残留,符合安卓开发最佳实践;
- * 3. 异常健壮:对数据解析、数据库查询、媒体播放等所有可能出现异常的环节均做了捕获处理,程序容错性极强,不会因单一异常导致崩溃;
- * 4. 体验友好:所有逻辑均围绕「用户感知」设计,既保证提醒的强制性,又兼顾使用的人性化,无过度打扰、无无效操作;
- * 5. 生命周期极简:页面的生命周期与弹窗强绑定,弹窗展示则页面存活,弹窗关闭则页面立即销毁,无后台残留,内存占用极低。
- * 完整业务流程闭环(提醒功能最终链路):
- * AlarmManager触发广播 → AlarmReceiver接收广播 → 启动本页面 → 屏幕唤醒+解析便签数据 → 校验便签有效 → 播放铃声+展示弹窗 → 用户点击按钮 → 停止铃声+关闭弹窗 → 页面销毁 / 跳转便签编辑页。
- * 核心业务约束:
- * - 仅处理有效便签的提醒:便签已删除/类型非法时,直接关闭页面,不展示任何内容;
- * - 严格遵循系统规则:铃声播放、屏幕唤醒均遵循安卓系统的安全与权限规则,无越权操作;
- * - 页面无残留:弹窗关闭即页面销毁,无任何后台进程或服务残留,性能友好。
+ * 便签提醒活动类
+ * 当便签的提醒时间到达时显示提醒对话框并播放闹钟声音
+ * 提供用户交互界面,允许用户查看或编辑提醒的便签
+ *
+ * 架构设计:
+ * - 继承自Activity,实现OnClickListener和OnDismissListener接口
+ * - 在提醒触发时创建全屏对话框
+ * - 播放系统闹钟铃声
+ * - 提供查看和编辑便签的选项
+ * - 适配不同Android版本的屏幕唤醒和锁定屏幕显示机制
+ *
+ * 核心功能:
+ * - 显示提醒对话框,包含便签内容摘要
+ * - 播放闹钟声音
+ * - 允许用户点击查看便签详情
+ * - 适配锁定屏幕和黑屏状态
+ * - 安全处理异常情况,避免崩溃
*/
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener {
- // ======================== 成员变量区 ========================
- /** 当前提醒绑定的便签唯一ID,核心标识,所有数据操作均基于此ID */
+ /**
+ * 提醒的便签ID
+ */
private long mNoteId;
- /** 便签的文本摘要内容,用于弹窗展示,做了长度限制处理 */
+ /**
+ * 便签内容摘要
+ */
private String mSnippet;
- /** 便签摘要预览的最大字符长度,超过该长度自动裁剪并添加省略号,保证展示美观 */
+ /**
+ * 摘要预览最大长度
+ */
private static final int SNIPPET_PREW_MAX_LEN = 60;
- /** 媒体播放器实例,用于加载和播放系统闹钟铃声,实现听觉提醒 */
- MediaPlayer mPlayer;
-
/**
- * 页面创建核心回调方法,页面生命周期的入口,承载所有初始化逻辑
- * 本页面的所有核心功能均在该方法中完成初始化,逻辑清晰、步骤明确、无冗余处理
- * @param savedInstanceState 页面状态保存对象,本页面未使用该参数
+ * 媒体播放器,用于播放闹钟声音
*/
+ MediaPlayer mPlayer;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- // 核心配置:隐藏原生的页面标题栏,本页面仅展示对话框,无需标题栏,极简展示
requestWindowFeature(Window.FEATURE_NO_TITLE);
- // ========== 第一步:窗口属性配置,实现锁屏显示、屏幕唤醒等核心能力 ==========
final Window win = getWindow();
- // 必加窗口标记:允许窗口在设备锁屏状态下依然显示,是锁屏提醒的基础配置
- win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ // 添加适当的标志,确保在各种情况下都能显示提醒窗口
+ win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
+
+ // 在Android 10+中,需要确保Intent有正确的标志
+ // 注意:FLAG_ACTIVITY_NEW_TASK和FLAG_ACTIVITY_CLEAR_TASK是Intent的常量,不是WindowManager.LayoutParams的常量
+ // 这些标志已经在AlarmReceiver中通过intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)设置
- // 智能判断屏幕状态:仅在熄屏时添加额外的唤醒与常亮标记,亮屏时不做修改
- if (!isScreenOn()) {
- win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); // 保持屏幕常亮,直到弹窗关闭
- win.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); // 强制唤醒熄屏的屏幕,核心保障用户能看到提醒
- win.addFlags(WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON);// 允许屏幕在亮屏状态下依然保持锁定,兼顾安全性
- win.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); // 适配系统的装饰布局,避免弹窗内容被遮挡
- }
- // ========== 第二步:解析Intent数据,获取便签ID并查询摘要内容 ==========
Intent intent = getIntent();
+
try {
- // 核心解析逻辑:从Intent的Data字段中解析出便签ID,Data字段的格式为 content://notes/note/[id]
- mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
- // 根据便签ID查询数据库,获取便签的文本摘要
- mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
- // 摘要内容规范化处理:超过60字符则截取前60位,并添加省略号,保证弹窗展示效果
- mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0,
- SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
- : mSnippet;
- } catch (IllegalArgumentException e) {
- // 异常处理:ID解析失败(如格式错误、数据异常),打印日志并直接返回,不展示任何内容
- e.printStackTrace();
+ // 检查Intent和数据是否存在
+ if (intent != null && intent.getData() != null) {
+ mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
+ mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
+ mSnippet = mSnippet != null && mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0,
+ SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
+ : mSnippet;
+ } else {
+ Log.e("AlarmAlertActivity", "Intent or data is null");
+ finish();
+ return;
+ }
+ } catch (Exception e) {
+ Log.e("AlarmAlertActivity", "Error processing intent: " + e.getMessage(), e);
+ finish();
return;
}
- // ========== 第三步:初始化媒体播放器,校验便签有效性并执行核心逻辑 ==========
mPlayer = new MediaPlayer();
- // 关键校验:仅当该便签真实存在于数据库且为普通便签类型时,才展示弹窗和播放铃声
- if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
- showActionDialog(); // 展示提醒弹窗,核心交互载体
- playAlarmSound(); // 播放系统闹钟铃声,核心听觉提醒
+ if (mNoteId > 0 && DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
+ showActionDialog();
+ playAlarmSound();
} else {
- // 便签已被删除或类型非法:直接关闭页面,无任何展示与播放,避免无效操作
finish();
}
}
- /**
- * 私有工具方法:判断当前设备的屏幕是否处于亮屏状态
- * @return boolean true=屏幕亮屏(解锁/未解锁但亮屏) false=屏幕熄屏/锁屏
- */
private boolean isScreenOn() {
- // 获取系统电源管理服务实例
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
- // 返回当前屏幕的亮灭状态
- return pm.isScreenOn();
+ // 在Android 10+中,isScreenOn()方法已被弃用,应使用isInteractive()
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT_WATCH) {
+ return pm.isInteractive();
+ } else {
+ return pm.isScreenOn();
+ }
}
- /**
- * 核心功能方法:初始化并播放系统默认的闹钟铃声,实现听觉提醒
- * 完整逻辑:获取系统铃声Uri → 适配系统静音模式 → 初始化播放器 → 循环播放 → 异常捕获,全链路健壮处理
- */
private void playAlarmSound() {
- // 第一步:获取系统默认的闹钟铃声Uri,使用系统级铃声资源,适配用户个性化设置
- Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
-
- // 第二步:读取系统铃声静音模式配置,判断闹钟音频流是否受静音规则影响,适配系统策略
- int silentModeStreams = Settings.System.getInt(getContentResolver(),
- Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0);
-
- // 第三步:设置音频流类型,核心适配逻辑,保证铃声播放遵循系统音量规则
- if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) {
- mPlayer.setAudioStreamType(silentModeStreams);
- } else {
- // 默认使用闹钟专属音频流,优先级高于普通媒体流,不会被媒体音量影响
- mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
+ // 添加空检查,避免崩溃
+ if (mPlayer == null) {
+ return;
}
-
- // 第四步:初始化媒体播放器并启动循环播放,捕获所有可能的异常,保证程序稳定
+
try {
- mPlayer.setDataSource(this, url); // 设置铃声资源的数据源
- mPlayer.prepare(); // 同步准备播放器,加载铃声资源
- mPlayer.setLooping(true); // 核心配置:循环播放铃声,直到用户手动关闭
- mPlayer.start(); // 启动铃声播放,触发听觉提醒
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- } catch (SecurityException e) {
- e.printStackTrace();
- } catch (IllegalStateException e) {
- e.printStackTrace();
- } catch (IOException e) {
+ Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
+ if (url == null) {
+ // 如果没有默认的闹钟铃声,使用系统默认铃声
+ url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_RINGTONE);
+ }
+ if (url == null) {
+ // 如果没有系统默认铃声,使用通知铃声
+ url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_NOTIFICATION);
+ }
+ if (url == null) {
+ // 如果都没有,直接返回
+ return;
+ }
+
+ int silentModeStreams = Settings.System.getInt(getContentResolver(),
+ Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0);
+
+ if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) {
+ mPlayer.setAudioStreamType(silentModeStreams);
+ } else {
+ mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
+ }
+
+ mPlayer.setDataSource(this, url);
+ mPlayer.prepare();
+ mPlayer.setLooping(true);
+ mPlayer.start();
+ } catch (Exception e) {
+ // 捕获所有异常,避免崩溃
e.printStackTrace();
+ // 发生异常时,释放播放器资源
+ if (mPlayer != null) {
+ try {
+ mPlayer.release();
+ mPlayer = null;
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
}
}
- /**
- * 核心功能方法:创建并展示便签提醒的核心弹窗,是页面的核心展示载体
- * 弹窗设计:标题为应用名,内容为便签摘要,按钮动态适配屏幕状态,绑定交互监听器,实现核心交互逻辑
- */
private void showActionDialog() {
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
- dialog.setTitle(R.string.app_name); // 弹窗标题:固定为应用名称,简洁明了
- dialog.setMessage(mSnippet); // 弹窗核心内容:便签的文本摘要,展示提醒的核心信息
- dialog.setPositiveButton(R.string.notealert_ok, this); // 确认按钮,绑定点击事件监听器
-
- // 智能按钮适配:仅在屏幕亮屏时展示「进入便签」按钮,熄屏/锁屏时不展示,贴合操作场景
+ dialog.setTitle(R.string.app_name);
+ // 如果mSnippet为空,显示默认提示信息
+ if (mSnippet == null || mSnippet.isEmpty()) {
+ dialog.setMessage(getString(R.string.set_remind_time_message));
+ } else {
+ dialog.setMessage(mSnippet);
+ }
+ dialog.setPositiveButton(R.string.notealert_ok, this);
if (isScreenOn()) {
dialog.setNegativeButton(R.string.notealert_enter, this);
}
-
- // 展示弹窗并绑定关闭监听器,无论何种方式关闭弹窗,均触发统一的收尾逻辑
dialog.show().setOnDismissListener(this);
}
- /**
- * 对话框按钮点击事件回调方法,实现弹窗的核心交互逻辑
- * @param dialog 触发点击事件的对话框实例
- * @param which 点击的按钮类型,区分「确认」和「进入便签」按钮
- */
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_NEGATIVE:
- // 点击「进入便签」按钮:跳转至该便签的编辑页面,精准定位到对应内容
Intent intent = new Intent(this, NoteEditActivity.class);
- intent.setAction(Intent.ACTION_VIEW); // 设置跳转动作:查看/编辑便签
- intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递便签ID,目标页面根据ID查询并展示内容
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.putExtra(Intent.EXTRA_UID, mNoteId);
startActivity(intent);
break;
default:
- // 点击「确认」按钮:无额外业务逻辑,仅关闭弹窗,收尾逻辑由OnDismissListener统一处理
break;
}
}
- /**
- * 对话框关闭事件回调方法,页面的统一收尾逻辑入口
- * 无论通过何种方式关闭弹窗(点击按钮、手动关闭),均执行该方法,保证逻辑闭环无遗漏
- * @param dialog 被关闭的对话框实例
- */
public void onDismiss(DialogInterface dialog) {
- stopAlarmSound(); // 核心收尾:停止铃声播放并释放媒体资源,杜绝内存泄漏与音频残留
- finish(); // 关闭当前页面,释放所有资源,页面生命周期结束
+ stopAlarmSound();
+ finish();
}
- /**
- * 私有工具方法:停止铃声播放并安全释放媒体播放器资源
- * 核心作用:资源清理,是安卓媒体播放的强制规范,避免内存泄漏和音频通道占用
- */
private void stopAlarmSound() {
if (mPlayer != null) {
- mPlayer.stop(); // 立即停止铃声播放
- mPlayer.release(); // 释放播放器的所有资源,包括音频通道、内存等
- mPlayer = null; // 置空引用,便于GC回收,彻底杜绝内存泄漏
+ try {
+ mPlayer.stop();
+ mPlayer.release();
+ mPlayer = null;
+ } catch (Exception e) {
+ // 捕获所有异常,避免崩溃
+ e.printStackTrace();
+ mPlayer = null;
+ }
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java b/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java
index 9cb875f..b9166eb 100644
--- a/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java
+++ b/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java
@@ -14,129 +14,86 @@
* limitations under the License.
*/
-// 包声明:归属小米便签UI模块,便签提醒功能的初始化核心广播接收器,负责重启恢复所有未过期提醒
package net.micode.notes.ui;
-// 安卓系统闹钟服务核心类,用于注册、设置、取消定时闹钟,是便签提醒的核心调度组件
import android.app.AlarmManager;
-// 安卓延迟意图核心类,封装广播/页面跳转意图,交由AlarmManager在指定时间触发,核心桥梁类
import android.app.PendingIntent;
-// 安卓广播核心基类,继承此类实现广播监听与处理能力,为本类的核心父类
import android.content.BroadcastReceiver;
-// 安卓ContentURI拼接工具类,用于将Uri和数据ID拼接,生成唯一标识Uri,便于数据精准匹配
import android.content.ContentUris;
-// 安卓上下文核心类,提供系统服务获取、内容解析器访问、组件通信等基础能力
import android.content.Context;
-// 安卓意图核心类,组件间通信的核心载体,用于封装广播意图并携带数据
import android.content.Intent;
-// 安卓数据库游标核心类,用于遍历查询数据库返回的结果集,读取便签提醒数据
import android.database.Cursor;
-// 便签应用核心数据常量类,定义数据库Uri、便签类型、字段名等全局常量,统一管理
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
+
/**
- * 便签提醒初始化专属广播接收器【提醒恢复核心类】
- * 继承:BroadcastReceiver 安卓系统广播接收器基类,具备监听并处理广播事件的核心能力
- * 核心定位:小米便签「提醒功能」的保障类,解决「设备重启/应用进程被杀」后提醒丢失的核心问题,是便签提醒可靠性的关键支撑
- * 核心设计意义:安卓系统的AlarmManager注册的闹钟,在设备重启后会全部失效,应用进程被系统回收后也可能丢失未触发的闹钟,本类就是为了解决该问题,实现提醒的持久化保障
- * 核心职责:
- * 1. 监听系统/应用的初始化类广播,包含「设备开机完成广播、应用启动广播、系统刷新广播」等核心触发时机;
- * 2. 接收到广播后,通过ContentResolver查询便签数据库中所有符合条件的有效提醒数据;
- * 3. 精准筛选:仅查询「提醒时间戳大于当前时间(未过期)」+「便签类型为普通便签TYPE_NOTE」的记录,排除文件夹/系统项/已过期提醒,无无效处理;
- * 4. 遍历有效提醒数据,为每一条记录重新创建广播意图与延迟意图,通过AlarmManager完成闹钟的重新注册;
- * 5. 保证所有未过期的便签提醒,在设备重启/应用重启后都能恢复如初,按时触发,无任何遗漏。
- * 核心特性&关键技术要点:
- * 1. 精准数据筛选:查询条件双重过滤,既保证时效性(未过期),又保证数据合法性(普通便签),避免无效数据处理,降低性能损耗;
- * 2. 轻量查询设计:自定义投影数组仅查询「便签ID、提醒时间戳」两个核心字段,不查询冗余内容,减少数据库IO开销与内存占用;
- * 3. 唯一标识绑定:通过ContentUris拼接便签ID到Intent的Data字段,为每个提醒生成唯一的意图标识,精准绑定提醒与对应便签,无串号风险;
- * 4. 强力唤醒保障:使用AlarmManager.RTC_WAKEUP闹钟类型,基于系统绝对时间,触发时会强制唤醒休眠的设备(亮屏/唤醒CPU),确保提醒必达,不会因设备休眠漏提醒;
- * 5. 资源安全释放:查询数据库后及时关闭Cursor游标,释放数据库连接与内存资源,杜绝内存泄漏风险,符合安卓开发最佳实践;
- * 6. 无状态极简设计:本类无成员变量、无复杂逻辑、无内存占用,仅在广播触发时执行一次性初始化逻辑,执行完成后立即释放资源,性能友好。
- * 核心业务约束:
- * - 不处理已过期的提醒:提醒时间小于当前时间的记录直接过滤,无需注册,避免无效操作;
- * - 不处理非普通便签:仅对TYPE_NOTE类型生效,文件夹(TYPE_FOLDER)等无提醒功能的类型直接排除;
- * - 不处理无提醒的便签:仅查询ALERTED_DATE字段大于0的记录,无提醒的便签不会进入查询结果。
- * 完整业务闭环:
- * 便签设置提醒 → AlarmManager注册闹钟 → 设备重启/应用重启 → 本接收器接收初始化广播 → 查询数据库有效提醒 → 重新注册所有闹钟 → 提醒时间到达 → AlarmReceiver触发 → 展示提醒弹窗。
+ * 闹钟初始化广播接收器
+ * 在系统启动时自动初始化所有待处理的便签提醒闹钟
+ * 确保设备重启后,所有设置了提醒的便签仍然能够按时触发
+ *
+ * 架构设计:
+ * - 继承自BroadcastReceiver,监听系统启动完成广播
+ * - 通过ContentResolver查询所有设置了未来提醒时间的便签
+ * - 为每个符合条件的便签重新设置AlarmManager闹钟
+ * - 适配不同Android版本的AlarmManager API变化
+ *
+ * 核心功能:
+ * - 系统启动后自动初始化闹钟
+ * - 查询所有未触发的提醒便签
+ * - 为每个便签设置准确的闹钟
+ * - 适配Android 6.0+的setExact方法
+ * - 适配Android 12+的PendingIntent flag要求
*/
public class AlarmInitReceiver extends BroadcastReceiver {
/**
- * 数据库查询投影数组【按需查询,性能优化】
- * 设计目的:指定本次数据库查询仅需要返回的字段,不查询冗余字段,减少数据库IO传输数据量,降低内存消耗,提升查询效率
- * 字段说明:仅包含两个核心必要字段,满足业务需求的最小化查询
+ * 查询投影列,包含便签ID和提醒日期
*/
private static final String [] PROJECTION = new String [] {
- NoteColumns.ID, // 数组索引0:便签的唯一主键ID,用于绑定提醒与便签、生成唯一Intent标识
- NoteColumns.ALERTED_DATE // 数组索引1:便签设置的提醒时间戳,毫秒级,用于设置闹钟的触发时间
+ NoteColumns.ID,
+ NoteColumns.ALERTED_DATE
};
- // 投影数组列索引常量【硬编码优化】
- // 设计目的:将数组索引封装为常量,替代代码中的硬编码数字,提升代码可读性、可维护性,避免索引写错导致的程序异常
- private static final int COLUMN_ID = 0; // 对应PROJECTION[0],便签ID列索引
- private static final int COLUMN_ALERTED_DATE = 1; // 对应PROJECTION[1],提醒时间戳列索引
-
/**
- * 广播接收核心回调方法,广播触发时由安卓系统自动调用
- * 本类的唯一核心方法,承载了「查询有效提醒+重新注册闹钟」的全部核心逻辑,逻辑清晰、步骤明确、无冗余处理
- * @param context 广播接收器的运行上下文对象,不可为空,核心作用:获取ContentResolver查询数据库、获取AlarmManager系统服务、创建Intent意图
- * @param intent 触发本次广播的意图对象,携带广播的触发类型信息,本类无需解析该意图内容,仅做触发执行
+ * 便签ID列索引
+ */
+ private static final int COLUMN_ID = 0;
+ /**
+ * 提醒日期列索引
*/
+ private static final int COLUMN_ALERTED_DATE = 1;
+
@Override
public void onReceive(Context context, Intent intent) {
- // 第一步:获取当前系统时间戳,作为筛选「未过期提醒」的核心阈值,只处理提醒时间在当前时间之后的记录
long currentDate = System.currentTimeMillis();
-
- // 第二步:通过ContentResolver查询便签数据库,获取所有符合条件的有效提醒数据
- // 核心查询参数说明:
- // 1. uri:查询地址 → Notes.CONTENT_NOTE_URI 普通便签的专属Uri,精准定位查询表
- // 2. projection:查询字段 → 自定义的PROJECTION数组,仅查ID和提醒时间戳
- // 3. selection:查询条件 → 双重过滤:提醒时间>当前时间 且 便签类型为普通便签,精准筛选有效数据
- // 4. selectionArgs:条件参数 → 传入当前时间戳的字符串形式,防止SQL注入,符合安卓安全规范
- // 5. sortOrder:排序规则 → null,使用数据库默认排序,无需额外排序,提升查询效率
Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION,
NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE,
new String[] { String.valueOf(currentDate) },
null);
- // 第三步:遍历查询结果,为每个有效提醒重新注册闹钟
- if (c != null) { // 游标非空校验:避免查询结果为空时的空指针异常,代码健壮性保障
- // 游标移动到第一条数据,存在有效提醒时进入循环遍历逻辑
+ if (c != null) {
if (c.moveToFirst()) {
do {
- // 3.1 从游标中读取当前行的核心数据:便签ID、提醒触发时间戳
- long alertDate = c.getLong(COLUMN_ALERTED_DATE); // 该便签的提醒触发时间,毫秒级
- long noteId = c.getLong(COLUMN_ID); // 该便签的唯一主键ID
-
- // 3.2 创建广播意图:指定意图的目标为AlarmReceiver,即提醒时间到达时,需要触发的广播接收器
+ long alertDate = c.getLong(COLUMN_ALERTED_DATE);
Intent sender = new Intent(context, AlarmReceiver.class);
- // 核心关键:将便签ID拼接至Intent的Data字段,生成唯一的Uri标识,精准绑定该提醒与对应便签
- // 作用:AlarmReceiver接收到广播时,可通过该Uri解析出便签ID,进而查询便签详情展示提醒内容,无串号风险
- sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId));
-
- // 3.3 创建PendingIntent延迟意图:封装上述广播意图,交由AlarmManager管理,在指定时间触发广播
- // PendingIntent.getBroadcast参数说明:上下文、请求码(此处传0即可)、待封装的广播意图、flags标记(默认0)
- // 核心作用:PendingIntent是一种授权意图,允许系统在未来的指定时间,以当前应用的身份发送该广播,是闹钟触发的核心载体
- PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0);
-
- // 3.4 获取系统AlarmManager服务实例,系统级的闹钟调度核心服务
+ sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)));
+ // 使用适当的PendingIntent flag,确保在Android 12+中正常工作
+ int flags = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S ? PendingIntent.FLAG_IMMUTABLE : 0;
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, flags);
AlarmManager alermManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
-
- // 3.5 核心操作:重新注册闹钟,完成提醒恢复
- // alermManager.set参数说明:
- // 1. type → AlarmManager.RTC_WAKEUP 最核心的闹钟类型,基于系统绝对时间,触发时唤醒设备CPU/屏幕,保证提醒必达
- // 2. triggerAtTime → 闹钟触发的具体时间,即从数据库读取的提醒时间戳alertDate
- // 3. operation → 待触发的PendingIntent延迟意图,触发时发送广播至AlarmReceiver
- alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
-
- } while (c.moveToNext()); // 循环遍历游标,处理所有有效提醒数据
+ // 在Android 6.0+中,使用setExact方法以确保准确的提醒时间
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+ alermManager.setExact(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
+ } else {
+ alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
+ }
+ } while (c.moveToNext());
}
- // 第四步:必须关闭游标,释放数据库资源与内存,杜绝内存泄漏,安卓数据库操作的强制规范
c.close();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java b/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java
index 0f59670..4a33790 100644
--- a/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java
+++ b/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java
@@ -14,50 +14,43 @@
* limitations under the License.
*/
-// 包声明:归属小米便签UI模块,便签提醒功能的广播接收核心类,承接闹钟触发逻辑
package net.micode.notes.ui;
-// 安卓广播核心基类,继承此类实现广播接收能力,监听系统/应用发送的广播事件
import android.content.BroadcastReceiver;
-// 安卓上下文核心类,提供应用运行环境、组件启动、资源访问等基础能力
import android.content.Context;
-// 安卓意图核心类,用于组件间通信、页面跳转、数据传递的核心载体
import android.content.Intent;
/**
- * 便签提醒功能专属广播接收器【轻量级广播处理类】
- * 继承:BroadcastReceiver 安卓系统广播接收器基类,具备监听并处理广播的核心能力
- * 核心定位:小米便签「提醒功能」的核心中转类,是闹钟触发与提醒弹窗展示的唯一桥梁,无任何业务逻辑,只做事件转发
- * 核心职责:
- * 1. 监听由系统AlarmManager定时发送的闹钟广播,该广播在用户设置的便签提醒时间到达时触发;
- * 2. 接收到广播后,无缝承接Intent中携带的便签提醒数据(便签ID、标题、内容等);
- * 3. 对Intent做标准化配置,指定跳转目标页面并补充必要的启动标记;
- * 4. 启动便签提醒弹窗页面,完成提醒事件的最终展示,触发用户感知。
- * 核心特性&关键注意点:
- * 1. 无状态设计:该类无任何成员变量、无初始化逻辑、无复杂处理,是纯功能性的极简类,内存占用极低;
- * 2. 必加启动标记:BroadcastReceiver 属于四大组件之一,运行时无独立的Activity任务栈上下文,启动Activity时必须添加 FLAG_ACTIVITY_NEW_TASK 标记,否则会抛出运行时异常,这是安卓的硬性规范;
- * 3. 数据透传:广播携带的所有Intent数据会完整透传给目标页面,无任何数据丢失或修改,保证提醒内容的正确性;
- * 4. 生命周期极简:广播接收器的onReceive方法执行时间被系统严格限制,本类逻辑极致轻量化,可在瞬时完成执行,无超时风险;
- * 5. 唯一触发源:该接收器只响应便签应用自身注册的闹钟广播,不接收其他任何广播事件,功能单一无干扰。
- * 典型业务流程闭环:
- * 便签编辑页设置提醒时间 → 后台通过AlarmManager注册定时闹钟 → 提醒时间到达,系统发送广播 → 本接收器接收广播 → 启动AlarmAlertActivity → 展示便签提醒弹窗,响铃/震动提醒用户。
- * 典型使用场景:唯一用途就是接收便签的提醒广播,无其他任何业务场景。
+ * 闹钟广播接收器
+ * 接收AlarmManager触发的闹钟广播,启动AlarmAlertActivity显示便签提醒
+ * 作为闹钟触发和提醒显示之间的桥梁
+ *
+ * 架构设计:
+ * - 继承自BroadcastReceiver,监听闹钟触发广播
+ * - 接收到广播后,创建指向AlarmAlertActivity的Intent
+ * - 添加FLAG_ACTIVITY_NEW_TASK标志,确保在非活动上下文下也能启动活动
+ * - 启动AlarmAlertActivity显示提醒界面
+ * - 包含异常处理,确保广播接收过程不会崩溃
+ *
+ * 核心功能:
+ * - 接收闹钟触发广播
+ * - 启动提醒显示活动
+ * - 确保启动过程的安全性和稳定性
*/
public class AlarmReceiver extends BroadcastReceiver {
-
- /**
- * 广播接收核心回调方法,广播触发时系统自动调用该方法
- * 该方法是本类的唯一核心方法,承载了所有的广播处理逻辑,极简且高效
- * @param context 广播接收器的运行上下文对象,不可为空,用于启动目标Activity组件
- * @param intent 触发本次广播的意图对象,内部携带了完整的便签提醒数据,也是页面跳转的核心数据载体
- */
@Override
public void onReceive(Context context, Intent intent) {
- // 第一步:为当前Intent指定跳转的目标页面,将广播意图转为页面跳转意图
- intent.setClass(context, AlarmAlertActivity.class);
- // 第二步:添加必选的新任务启动标记,解决广播无任务栈启动Activity的上下文问题,规避运行时异常
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- // 第三步:启动提醒弹窗页面,完成广播事件的最终转发,展示便签提醒内容
- context.startActivity(intent);
+ // 添加空检查,避免崩溃
+ if (context == null || intent == null) {
+ return;
+ }
+ try {
+ intent.setClass(context, AlarmAlertActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ } catch (Exception e) {
+ // 捕获所有异常,避免崩溃
+ e.printStackTrace();
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/ui/ChatActivity.java b/src/Notes-master/src/net/micode/notes/ui/ChatActivity.java
new file mode 100644
index 0000000..ab87c88
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/ChatActivity.java
@@ -0,0 +1,438 @@
+package net.micode.notes.ui;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.text.TextUtils;
+import android.graphics.Color;
+
+import net.micode.notes.R;
+import net.micode.notes.data.Messages;
+import net.micode.notes.data.NotesDatabaseHelper;
+import net.micode.notes.tool.UserManager;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 聊天活动
+ *
+ * 该类负责实现用户与好友之间的聊天功能,包括消息的发送、接收和显示。
+ * 它通过NotesDatabaseHelper操作消息数据表,实现聊天记录的存储和加载。
+ */
+public class ChatActivity extends Activity {
+ private static final String TAG = "ChatActivity";
+
+ private ListView mChatListView;
+ private EditText mMessageEditText;
+ private Button mSendButton;
+ private ChatAdapter mChatAdapter;
+ private List mMessageList;
+
+ private NotesDatabaseHelper mDbHelper;
+ private SQLiteDatabase mDb;
+ private UserManager mUserManager;
+
+ private long mCurrentUserId;
+ private long mFriendId;
+ private String mFriendUsername;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ try {
+ setContentView(R.layout.chat_activity);
+
+ // 设置ActionBar
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ // 获取Intent参数
+ mFriendId = getIntent().getLongExtra("friend_id", -1);
+ mFriendUsername = getIntent().getStringExtra("friend_username");
+
+ if (mFriendId == -1 || mFriendUsername == null) {
+ Toast.makeText(this, "无效的好友信息", Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+
+ // 设置ActionBar标题为好友用户名
+ if (actionBar != null) {
+ actionBar.setTitle(mFriendUsername);
+ }
+
+ // 初始化数据库
+ mDbHelper = NotesDatabaseHelper.getInstance(this);
+ if (mDbHelper != null) {
+ mDb = mDbHelper.getWritableDatabase();
+ }
+
+ // 初始化UserManager
+ mUserManager = UserManager.getInstance(this);
+ if (mUserManager != null) {
+ mCurrentUserId = mUserManager.getCurrentUserId();
+ }
+
+ // 初始化界面控件
+ mChatListView = findViewById(R.id.chat_list);
+ mMessageEditText = findViewById(R.id.message_edit_text);
+ mSendButton = findViewById(R.id.send_button);
+
+ // 初始化消息列表
+ mMessageList = new ArrayList<>();
+ mChatAdapter = new ChatAdapter(this, mMessageList, mCurrentUserId);
+ mChatListView.setAdapter(mChatAdapter);
+
+ // 设置发送按钮点击事件
+ mSendButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ sendMessage();
+ }
+ });
+
+ // 加载聊天记录
+ loadChatHistory();
+ } catch (Exception e) {
+ Log.e(TAG, "Error in onCreate: " + e.getMessage(), e);
+ Toast.makeText(this, "聊天界面初始化失败", Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ try {
+ // 重新获取当前用户ID,确保在账号切换后能使用正确的用户ID
+ if (mUserManager != null) {
+ mCurrentUserId = mUserManager.getCurrentUserId();
+ Log.d(TAG, "Updated current user ID to: " + mCurrentUserId);
+ }
+
+ // 更新适配器的当前用户ID
+ if (mChatAdapter != null) {
+ mChatAdapter.mCurrentUserId = mCurrentUserId;
+ }
+
+ // 重新加载聊天记录
+ loadChatHistory();
+ } catch (Exception e) {
+ Log.e(TAG, "Error in onResume: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 关闭数据库连接
+ if (mDb != null && mDb.isOpen()) {
+ mDb.close();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(android.view.MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // 返回上一级活动
+ finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /**
+ * 加载聊天记录
+ */
+ private void loadChatHistory() {
+ mMessageList.clear();
+
+ // 查询聊天记录
+ String sql = "SELECT * FROM " + NotesDatabaseHelper.TABLE.MESSAGE + " WHERE " +
+ "(" + Messages.MessageColumns.SENDER_ID + " = ? AND " + Messages.MessageColumns.RECEIVER_ID + " = ?) OR " +
+ "(" + Messages.MessageColumns.SENDER_ID + " = ? AND " + Messages.MessageColumns.RECEIVER_ID + " = ?) " +
+ "ORDER BY " + Messages.MessageColumns.CREATED_DATE + " ASC";
+
+ Cursor cursor = mDb.rawQuery(sql, new String[]{
+ String.valueOf(mCurrentUserId), String.valueOf(mFriendId),
+ String.valueOf(mFriendId), String.valueOf(mCurrentUserId)
+ });
+
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(Messages.MessageColumns.ID));
+ long senderId = cursor.getLong(cursor.getColumnIndexOrThrow(Messages.MessageColumns.SENDER_ID));
+ long receiverId = cursor.getLong(cursor.getColumnIndexOrThrow(Messages.MessageColumns.RECEIVER_ID));
+ String content = cursor.getString(cursor.getColumnIndexOrThrow(Messages.MessageColumns.CONTENT));
+ int messageType = cursor.getInt(cursor.getColumnIndexOrThrow(Messages.MessageColumns.MESSAGE_TYPE));
+ long createdDate = cursor.getLong(cursor.getColumnIndexOrThrow(Messages.MessageColumns.CREATED_DATE));
+ int isRead = cursor.getInt(cursor.getColumnIndexOrThrow(Messages.MessageColumns.IS_READ));
+
+ mMessageList.add(new ChatMessage(id, senderId, receiverId, content, messageType, createdDate, isRead));
+ }
+ cursor.close();
+ }
+
+ // 通知适配器数据变化
+ mChatAdapter.notifyDataSetChanged();
+
+ // 滚动到底部
+ if (!mMessageList.isEmpty()) {
+ mChatListView.setSelection(mMessageList.size() - 1);
+ }
+
+ // 将接收到的消息标记为已读
+ markMessagesAsRead();
+ }
+
+ /**
+ * 发送消息
+ */
+ private void sendMessage() {
+ String content = mMessageEditText.getText().toString().trim();
+ if (content.isEmpty()) {
+ Toast.makeText(this, "消息内容不能为空", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 创建消息对象
+ ContentValues values = new ContentValues();
+ values.put(Messages.MessageColumns.SENDER_ID, mCurrentUserId);
+ values.put(Messages.MessageColumns.RECEIVER_ID, mFriendId);
+ values.put(Messages.MessageColumns.CONTENT, content);
+ values.put(Messages.MessageColumns.MESSAGE_TYPE, Messages.MessageType.TEXT);
+ values.put(Messages.MessageColumns.CREATED_DATE, System.currentTimeMillis());
+ values.put(Messages.MessageColumns.IS_READ, 0);
+
+ // 插入消息到数据库
+ long messageId = mDb.insert(NotesDatabaseHelper.TABLE.MESSAGE, null, values);
+ if (messageId != -1) {
+ // 清空输入框
+ mMessageEditText.setText("");
+
+ // 重新加载聊天记录
+ loadChatHistory();
+ } else {
+ Toast.makeText(this, "发送失败", Toast.LENGTH_SHORT).show();
+ Log.e(TAG, "Failed to send message");
+ }
+ }
+
+ /**
+ * 将接收到的消息标记为已读
+ */
+ private void markMessagesAsRead() {
+ ContentValues values = new ContentValues();
+ values.put(Messages.MessageColumns.IS_READ, 1);
+
+ int updatedRows = mDb.update(NotesDatabaseHelper.TABLE.MESSAGE, values,
+ Messages.MessageColumns.SENDER_ID + " = ? AND " + Messages.MessageColumns.RECEIVER_ID + " = ? AND " + Messages.MessageColumns.IS_READ + " = 0",
+ new String[]{String.valueOf(mFriendId), String.valueOf(mCurrentUserId)});
+
+ Log.d(TAG, "Marked " + updatedRows + " messages as read");
+ }
+
+ /**
+ * 显示便签详情
+ */
+ private void showNoteDetail(String noteData) {
+ try {
+ // 解析便签数据
+ String[] noteParts = noteData.split("\\|");
+ if (noteParts.length < 2) {
+ Toast.makeText(this, "无效的便签数据", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ String noteTitle = noteParts[0];
+ String noteContent = noteParts[1];
+
+ if (TextUtils.isEmpty(noteTitle)) {
+ noteTitle = "无标题便签";
+ }
+
+ // 创建并显示便签详情对话框
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(noteTitle);
+ builder.setMessage(noteContent);
+ builder.setPositiveButton("确定", null);
+ builder.show();
+ } catch (Exception e) {
+ Log.e(TAG, "Error showing note detail: " + e.getMessage(), e);
+ Toast.makeText(this, "显示便签详情失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 聊天消息实体类
+ */
+ private static class ChatMessage {
+ long id;
+ long senderId;
+ long receiverId;
+ String content;
+ int messageType;
+ long createdDate;
+ int isRead;
+
+ ChatMessage(long id, long senderId, long receiverId, String content, int messageType, long createdDate, int isRead) {
+ this.id = id;
+ this.senderId = senderId;
+ this.receiverId = receiverId;
+ this.content = content;
+ this.messageType = messageType;
+ this.createdDate = createdDate;
+ this.isRead = isRead;
+ }
+ }
+
+ /**
+ * 聊天消息适配器
+ */
+ private static class ChatAdapter extends BaseAdapter {
+ private static final int VIEW_TYPE_SENT_TEXT = 0;
+ private static final int VIEW_TYPE_RECEIVED_TEXT = 1;
+ private static final int VIEW_TYPE_SENT_NOTE = 2;
+ private static final int VIEW_TYPE_RECEIVED_NOTE = 3;
+
+ private Context mContext;
+ private List mMessageList;
+ public long mCurrentUserId;
+ private LayoutInflater mInflater;
+ private SimpleDateFormat mDateFormat;
+
+ ChatAdapter(Context context, List messageList, long currentUserId) {
+ mContext = context;
+ mMessageList = messageList;
+ mCurrentUserId = currentUserId;
+ mInflater = LayoutInflater.from(context);
+ mDateFormat = new SimpleDateFormat("HH:mm");
+ }
+
+ @Override
+ public int getCount() {
+ return mMessageList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mMessageList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 4; // 四种视图类型:发送文本、接收文本、发送便签、接收便签
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ ChatMessage message = mMessageList.get(position);
+ boolean isSentByMe = message.senderId == mCurrentUserId;
+ if (message.messageType == Messages.MessageType.NOTE) {
+ return isSentByMe ? VIEW_TYPE_SENT_NOTE : VIEW_TYPE_RECEIVED_NOTE;
+ } else {
+ return isSentByMe ? VIEW_TYPE_SENT_TEXT : VIEW_TYPE_RECEIVED_TEXT;
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ChatMessage message = mMessageList.get(position);
+ boolean isSentByMe = message.senderId == mCurrentUserId;
+
+ ViewHolder holder;
+ if (convertView == null) {
+ // 根据视图类型选择不同的布局
+ int viewType = getItemViewType(position);
+ switch (viewType) {
+ case VIEW_TYPE_SENT_NOTE:
+ convertView = mInflater.inflate(R.layout.chat_message_sent_note_item, parent, false);
+ break;
+ case VIEW_TYPE_RECEIVED_NOTE:
+ convertView = mInflater.inflate(R.layout.chat_message_received_note_item, parent, false);
+ break;
+ case VIEW_TYPE_SENT_TEXT:
+ default:
+ convertView = mInflater.inflate(R.layout.chat_message_sent_item, parent, false);
+ break;
+ case VIEW_TYPE_RECEIVED_TEXT:
+ convertView = mInflater.inflate(R.layout.chat_message_received_item, parent, false);
+ break;
+ }
+
+ holder = new ViewHolder();
+ holder.contentTextView = convertView.findViewById(R.id.message_content);
+ holder.timeTextView = convertView.findViewById(R.id.message_time);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ // 设置消息内容
+ if (message.messageType == Messages.MessageType.NOTE) {
+ // 便签类型消息,只显示便签标题
+ String[] noteData = message.content.split("\\|");
+ if (noteData.length >= 2) {
+ String noteTitle = noteData[0];
+ if (TextUtils.isEmpty(noteTitle)) {
+ noteTitle = "无标题便签";
+ }
+ holder.contentTextView.setText(noteTitle);
+ // 设置不同的颜色,区分于普通文本消息
+ holder.contentTextView.setTextColor(Color.BLUE); // 使用蓝色区分便签消息
+ holder.contentTextView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // 点击便签,查看详情
+ ((ChatActivity) mContext).showNoteDetail(message.content);
+ }
+ });
+ }
+ } else {
+ // 普通文本消息
+ holder.contentTextView.setText(message.content);
+ holder.contentTextView.setOnClickListener(null);
+ }
+
+ // 设置消息时间
+ Date date = new Date(message.createdDate);
+ holder.timeTextView.setText(mDateFormat.format(date));
+
+ return convertView;
+ }
+
+ private static class ViewHolder {
+ TextView contentTextView;
+ TextView timeTextView;
+ }
+ }
+}
diff --git a/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java b/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java
index d829df7..b9ec91b 100644
--- a/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java
+++ b/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java
@@ -14,190 +14,197 @@
* limitations under the License.
*/
-// 包声明:归属小米便签UI模块,日期时间选择的核心自定义复合控件,供弹窗调用
package net.micode.notes.ui;
-// Java文本格式化工具类,获取系统的上午/下午文本标识,适配多语言环境
import java.text.DateFormatSymbols;
-// Java核心日历工具类,全局唯一的时间数据载体,处理所有年月日时分的计算、联动、转换逻辑
import java.util.Calendar;
-// 小米便签资源文件引用,获取布局、字符串等资源ID常量
import net.micode.notes.R;
-// 安卓系统核心类 - 上下文,提供控件创建、资源加载、布局填充的运行环境
+
import android.content.Context;
-// 安卓系统日期格式化工具类,提供系统24小时制判断、日期文本格式化能力
import android.text.format.DateFormat;
-// 安卓视图体系核心类,视图基础属性配置、可见性控制、布局容器核心父类
import android.view.View;
import android.widget.FrameLayout;
-// 安卓数字滚轮选择器,本控件的核心子组件,实现年月日时分的滚轮滑动选择UI
import android.widget.NumberPicker;
/**
- * 自定义日期时间复合选择控件【核心基础UI控件】
- * 继承:FrameLayout 帧布局,作为复合控件的根布局容器,承载多个子选择器
- * 核心定位:小米便签「提醒时间设置」功能的底层核心控件,被DateTimePickerDialog弹窗集成使用
- * 核心设计理念:将「日期选择+小时选择+分钟选择+上下午选择」整合为统一控件,封装所有时间联动逻辑、格式适配逻辑、数据处理逻辑,对外提供极简的调用与通信接口
- * 核心特性与职责:
- * 1. 布局整合:内置日期、小时、分钟、上下午共4个NumberPicker滚轮选择器,形成一体化的时间选择UI;
- * 2. 数据闭环:基于Calendar类做全局唯一的时间数据管理,所有选择操作最终同步至该对象,保证数据一致性;
- * 3. 智能联动:完美处理所有时间边界联动场景,如分钟59→0时小时+1、小时23→0时日期+1、12小时制跨上下午切换等,无数据断层;
- * 4. 系统适配:自动识别并适配系统的24小时制/12小时制设置,支持手动强制切换,两种制式无缝兼容,交互无感知;
- * 5. 日期展示:固定展示近7天的日期列表,格式为「月.日 星期」,满足便签短周期提醒的业务需求;
- * 6. 状态统一:支持控件整体启用/禁用,一键同步所有子选择器的交互状态,无需单独配置;
- * 7. 标准化通信:提供时间变化的回调接口,选择操作实时触发回调,向外传递标准化的年月日时分数据;
- * 8. 细节优化:分钟选择器支持长按快速滚动、上下午文本适配系统多语言、时间数值边界校验等细节体验优化;
- * 9. 防抖动处理:初始化阶段屏蔽回调触发,避免初始化时的无效数据通知,提升性能与稳定性。
- * 核心优势:控件内聚性极强,所有时间相关的逻辑全部封装内部,外部调用方只需关心「设置初始时间」「获取选中时间」「监听时间变化」三个核心操作,完全无需处理内部复杂逻辑。
- * 典型业务场景:唯一使用场景为DateTimePickerDialog弹窗的核心内容视图,支撑便签的提醒时间选择功能。
+ * 日期时间选择器控件
+ * 提供用户界面来选择日期和时间,支持年、月、日、时、分的精确选择
+ * 继承自FrameLayout,内部使用多个NumberPicker组件实现
+ *
+ * 架构设计:
+ * - 继承自FrameLayout,作为容器包含多个NumberPicker
+ * - 内部使用Calendar实例管理日期时间数据
+ * - 提供年、月、日、时、分的选择器
+ * - 支持24小时制和12小时制切换
+ * - 实现各种值变化监听器,处理选择逻辑
+ * - 提供回调接口通知外部日期时间变化
+ *
+ * 核心功能:
+ * - 日期选择:支持选择一周内的日期
+ * - 时间选择:支持小时和分钟的选择
+ * - 24/12小时制切换:根据系统设置或手动切换
+ * - AM/PM切换:在12小时制下显示
+ * - 边界处理:自动处理日期时间的边界情况(如跨天、跨小时)
+ * - 实时更新:选择变化时实时更新内部日期时间
+ * - 回调通知:通过接口通知外部日期时间变化
+ * - 状态保存:保存和恢复当前选择的日期时间
*/
public class DateTimePicker extends FrameLayout {
- // ======================== 基础常量区 - 控件默认配置与固定数值 ========================
- /** 控件默认启用状态:初始化时默认所有选择器均可交互 */
+ /**
+ * 默认启用状态
+ */
private static final boolean DEFAULT_ENABLE_STATE = true;
- // ======================== 常量区 - 时间单位核心数值 ========================
- /** 半天小时数:12小时制的核心分界值,上午/下午的小时数上限 */
+ /**
+ * 半天的小时数
+ */
private static final int HOURS_IN_HALF_DAY = 12;
- /** 全天小时数:24小时制的核心数值,一天的总小时数 */
+ /**
+ * 一天的小时数
+ */
private static final int HOURS_IN_ALL_DAY = 24;
- /** 一周天数:日期选择器固定展示的天数,不可修改,适配短周期提醒业务 */
+ /**
+ * 一周的天数
+ */
private static final int DAYS_IN_ALL_WEEK = 7;
-
- // ======================== 常量区 - NumberPicker滚轮选择器 取值范围约束 ========================
- /** 日期选择器-最小值:固定为0,对应近7天列表的第一条数据 */
+ /**
+ * 日期选择器最小值
+ */
private static final int DATE_SPINNER_MIN_VAL = 0;
- /** 日期选择器-最大值:固定为6,对应近7天列表的最后一条数据,数值等于天数减1 */
+ /**
+ * 日期选择器最大值
+ */
private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1;
-
- /** 24小时制-小时选择器-最小值:凌晨0点,时间起点 */
+ /**
+ * 24小时制下小时选择器最小值
+ */
private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0;
- /** 24小时制-小时选择器-最大值:深夜23点,时间终点 */
+ /**
+ * 24小时制下小时选择器最大值
+ */
private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23;
-
- /** 12小时制-小时选择器-最小值:上午/下午的1点,无0点概念 */
+ /**
+ * 12小时制下小时选择器最小值
+ */
private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1;
- /** 12小时制-小时选择器-最大值:上午/下午的12点,封顶数值 */
+ /**
+ * 12小时制下小时选择器最大值
+ */
private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12;
-
- /** 分钟选择器-最小值:整点0分,分钟起点 */
+ /**
+ * 分钟选择器最小值
+ */
private static final int MINUT_SPINNER_MIN_VAL = 0;
- /** 分钟选择器-最大值:整点前1分钟,59分,分钟终点 */
+ /**
+ * 分钟选择器最大值
+ */
private static final int MINUT_SPINNER_MAX_VAL = 59;
-
- /** 上下午选择器-最小值:0,对应上午/AM */
+ /**
+ * AM/PM选择器最小值
+ */
private static final int AMPM_SPINNER_MIN_VAL = 0;
- /** 上下午选择器-最大值:1,对应下午/PM */
+ /**
+ * AM/PM选择器最大值
+ */
private static final int AMPM_SPINNER_MAX_VAL = 1;
- // ======================== 成员变量区 - UI核心组件【所有子选择器,全局持用避免重复查找】 ========================
- /** 日期滚轮选择器:核心展示近7天的格式化日期文本,如「01.15 周四」,支持滑动切换日期 */
+ /**
+ * 日期选择器
+ */
private final NumberPicker mDateSpinner;
- /** 小时滚轮选择器:根据24/12小时制展示对应范围的小时数,核心时间选择组件 */
+ /**
+ * 小时选择器
+ */
private final NumberPicker mHourSpinner;
- /** 分钟滚轮选择器:固定0~59的分钟数选择,支持长按快速滚动,核心时间选择组件 */
+ /**
+ * 分钟选择器
+ */
private final NumberPicker mMinuteSpinner;
- /** 上下午滚轮选择器:仅12小时制显示,0=上午/AM,1=下午/PM,适配12小时制的时间展示 */
+ /**
+ * AM/PM选择器
+ */
private final NumberPicker mAmPmSpinner;
-
- // ======================== 成员变量区 - 核心状态与数据载体【全局核心数据,控件的大脑】 ========================
- /** 核心日历对象:全局唯一的时间数据载体,存储当前选中的完整年月日时分信息,所有选择操作最终同步至此,所有外部获取操作均来源于此,保证数据唯一可信 */
+ /**
+ * 日期时间日历实例
+ */
private Calendar mDate;
- /** 日期展示文本数组:缓存近7天的格式化日期文本,供日期选择器展示使用,避免重复计算 */
+
+ /**
+ * 日期显示值数组
+ */
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
- /** 上下午状态标记:true=上午/AM,false=下午/PM,仅在12小时制下生效,控制时间计算与展示 */
+
+ /**
+ * 是否为上午
+ */
private boolean mIsAm;
- /** 24小时制状态标记:true=启用24小时制,隐藏上下午选择器;false=启用12小时制,显示上下午选择器 */
+
+ /**
+ * 是否使用24小时制
+ */
private boolean mIs24HourView;
- /** 控件整体启用状态标记:true=所有选择器可交互,false=所有选择器禁用,统一管控交互权限 */
+
+ /**
+ * 是否启用
+ */
private boolean mIsEnabled = DEFAULT_ENABLE_STATE;
- /** 初始化状态标记:true=控件正在初始化,屏蔽所有回调触发;false=初始化完成,正常响应所有操作与回调,防止初始化阶段的无效数据通知 */
- private boolean mInitialising;
- // ======================== 成员变量区 - 回调通信接口 ========================
- /** 日期时间变化的回调监听器:外部实现该接口,接收控件的时间变化通知,是控件与外部通信的唯一桥梁 */
- private OnDateTimeChangedListener mOnDateTimeChangedListener;
+ /**
+ * 是否正在初始化
+ */
+ private boolean mInitialising;
- // ======================== 成员变量区 - 滚轮选择器 值变化监听器【所有选择器的核心交互逻辑,内部闭环】 ========================
/**
- * 日期选择器 值变化监听器:处理日期滑动切换的核心逻辑
- * 核心能力:监听日期选择器的数值变化,计算日期偏移量,同步更新核心日历对象的日期,刷新日期展示文本,最终触发时间变化回调
- * 无边界特殊处理:日期选择器固定展示近7天,滑动仅在7天内切换,无需处理跨月跨年的复杂逻辑
+ * 日期时间变化监听器
*/
+ private OnDateTimeChangedListener mOnDateTimeChangedListener;
+
private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
- // 计算日期偏移量:新值-旧值 即为需要增减的天数,直接同步至核心日历对象
mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal);
- // 刷新日期选择器的展示文本,保证文本与选中日期一致
updateDateControl();
- // 触发全局时间变化回调,向外通知日期已更新
onDateTimeChanged();
}
};
- /**
- * 小时选择器 值变化监听器:处理小时滑动切换的核心逻辑【最复杂的联动逻辑】
- * 核心能力:兼容24/12小时制的小时选择,处理所有小时边界的联动场景,包含「小时→日期」「小时→上下午」的双层联动,同步更新核心日历对象
- * 核心处理场景:
- * 1. 12小时制:下午11点→12点 → 日期+1;上午12点→11点 → 日期-1;小时11↔12时自动切换上下午状态;
- * 2. 24小时制:23点→0点 → 日期+1;0点→23点 → 日期-1;无上下切换逻辑;
- * 3. 所有场景下,最终将选中的小时数适配转换为24小时制,同步至核心日历对象,保证数据统一。
- */
private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
- boolean isDateChanged = false; // 日期是否发生变化的标记位,默认无变化
- Calendar cal = Calendar.getInstance(); // 临时日历对象,用于处理日期偏移
-
- // ========== 12小时制 小时边界特殊处理 ==========
+ boolean isDateChanged = false;
+ Calendar cal = Calendar.getInstance();
if (!mIs24HourView) {
- // 场景1:下午状态下,小时从11→12,触发日期+1(跨天)
if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
- }
- // 场景2:上午状态下,小时从12→11,触发日期-1(跨天)
- else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
+ } else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
-
- // 场景3:小时在11和12之间切换时,自动翻转上下午状态(核心联动逻辑)
if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY ||
oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
mIsAm = !mIsAm;
- updateAmPmControl(); // 同步刷新上下午选择器的选中状态
+ updateAmPmControl();
}
- }
- // ========== 24小时制 小时边界特殊处理 ==========
- else {
- // 场景1:小时从23→0,触发日期+1(跨天,一天的结束)
+ } else {
if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
- }
- // 场景2:小时从0→23,触发日期-1(跨天,一天的开始)
- else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
+ } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
}
-
- // ========== 统一处理:将选中的小时数转换为24小时制,同步至核心日历对象 ==========
int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY);
mDate.set(Calendar.HOUR_OF_DAY, newHour);
- // 触发全局时间变化回调,向外通知小时已更新
onDateTimeChanged();
-
- // ========== 日期发生变化时,同步更新核心日历对象的年月日 ==========
if (isDateChanged) {
setCurrentYear(cal.get(Calendar.YEAR));
setCurrentMonth(cal.get(Calendar.MONTH));
@@ -206,146 +213,82 @@ public class DateTimePicker extends FrameLayout {
}
};
- /**
- * 分钟选择器 值变化监听器:处理分钟滑动切换的核心逻辑【分钟→小时→日期 三层联动】
- * 核心能力:监听分钟选择器的数值变化,处理分钟的边界联动场景,是最基础也是最核心的时间联动逻辑
- * 核心处理场景:分钟从59→0 触发小时+1;分钟从0→59 触发小时-1;小时变化后可能触发日期变化,自动联动处理
- * 附加能力:小时变化后,自动同步更新上下午状态与选择器,保证12小时制的展示一致性
- */
private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
int minValue = mMinuteSpinner.getMinValue();
int maxValue = mMinuteSpinner.getMaxValue();
- int offset = 0; // 小时偏移量,默认无偏移
-
- // 场景1:分钟从59→0,触发小时+1(分钟到顶,进位到小时)
+ int offset = 0;
if (oldVal == maxValue && newVal == minValue) {
offset += 1;
- }
- // 场景2:分钟从0→59,触发小时-1(分钟到底,退位到小时)
- else if (oldVal == minValue && newVal == maxValue) {
+ } else if (oldVal == minValue && newVal == maxValue) {
offset -= 1;
}
-
- // ========== 小时需要偏移时,执行联动逻辑 ==========
if (offset != 0) {
- mDate.add(Calendar.HOUR_OF_DAY, offset); // 同步更新核心日历对象的小时数
- mHourSpinner.setValue(getCurrentHour()); // 刷新小时选择器的选中值
- updateDateControl(); // 刷新日期选择器,小时偏移可能导致日期变化
-
- // 根据新的小时数,更新上下午状态标记
+ mDate.add(Calendar.HOUR_OF_DAY, offset);
+ mHourSpinner.setValue(getCurrentHour());
+ updateDateControl();
int newHour = getCurrentHourOfDay();
- mIsAm = newHour < HOURS_IN_HALF_DAY;
- updateAmPmControl(); // 同步刷新上下午选择器的选中状态
+ if (newHour >= HOURS_IN_HALF_DAY) {
+ mIsAm = false;
+ updateAmPmControl();
+ } else {
+ mIsAm = true;
+ updateAmPmControl();
+ }
}
-
- // ========== 统一处理:将选中的分钟数同步至核心日历对象 ==========
mDate.set(Calendar.MINUTE, newVal);
- // 触发全局时间变化回调,向外通知分钟已更新
onDateTimeChanged();
}
};
- /**
- * 上下午选择器 值变化监听器:处理上下午切换的核心逻辑【仅12小时制生效】
- * 核心能力:监听上下午选择器的切换操作,翻转上下午状态标记,同步调整核心日历对象的小时数(±12小时)
- * 核心逻辑:上午→下午,小时+12;下午→上午,小时-12,保证时间数值的正确性,无数据误差
- */
private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
- // 翻转上下午状态标记
mIsAm = !mIsAm;
- // 根据新状态,调整核心日历对象的小时数,保证时间正确
if (mIsAm) {
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY);
} else {
mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY);
}
- // 刷新上下午选择器的展示状态
updateAmPmControl();
- // 触发全局时间变化回调,向外通知上下午已切换
onDateTimeChanged();
}
};
- // ======================== 内部回调接口 - 日期时间变化通知【标准化通信协议】 ========================
- /**
- * 日期时间变化回调接口:控件对外提供的唯一通信接口,所有时间选择操作的最终数据出口
- * 设计原则:解耦控件内部逻辑与外部业务逻辑,控件只负责提供时间选择能力,不处理任何业务逻辑
- * 调用时机:日期、小时、分钟、上下午任一选择器发生变化时,均会触发该接口的回调方法,实时传递最新的时间数据
- */
public interface OnDateTimeChangedListener {
- /**
- * 日期时间变化的回调方法,传递标准化的时间数据
- * @param view 当前的DateTimePicker控件实例,外部可通过该实例获取更多信息或执行操作
- * @param year 选中的年份,如 2026
- * @param month 选中的月份,遵循Calendar规范,0代表1月,11代表12月
- * @param dayOfMonth 选中的日期,当月的第几天,如 15
- * @param hourOfDay 选中的小时,固定为24小时制数值,0~23,外部无需做格式转换,直接使用
- * @param minute 选中的分钟,0~59,标准化数值
- */
void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute);
}
- // ======================== 构造方法区 - 三级重载构造,满足不同初始化需求【核心初始化入口】 ========================
- /**
- * 构造方法1:最简初始化,无参重载
- * 核心能力:使用系统当前时间作为初始值,自动适配系统的24小时制设置,一键创建控件
- * @param context 应用上下文对象,不可为空
- */
public DateTimePicker(Context context) {
this(context, System.currentTimeMillis());
}
- /**
- * 构造方法2:指定初始时间初始化
- * 核心能力:传入指定的毫秒级时间戳作为初始值,自动适配系统的24小时制设置,灵活适配业务需求
- * @param context 应用上下文对象,不可为空
- * @param date 初始选中的时间戳,单位:毫秒,支持任意合法的时间戳
- */
public DateTimePicker(Context context, long date) {
this(context, date, DateFormat.is24HourFormat(context));
}
- /**
- * 构造方法3:全参数核心初始化,控件的最终初始化入口,所有构造方法最终均调用此方法
- * 核心能力:一站式完成「上下文初始化+核心数据初始化+布局填充+子选择器初始化+监听器绑定+状态配置+初始时间设置」,无需外部执行任何额外配置,开箱即用
- * @param context 应用上下文对象,用于加载布局、创建控件、获取系统配置
- * @param date 初始选中的时间戳,单位:毫秒,控件打开时默认展示的时间
- * @param is24HourView 是否启用24小时制,true=启用,false=启用12小时制
- */
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
- mDate = Calendar.getInstance(); // 初始化核心日历对象,默认加载系统当前时间
- mInitialising = true; // 标记进入初始化阶段,屏蔽所有回调触发,防止无效数据通知
- // 初始化上下午状态标记:根据当前小时数判断,≥12为下午,<12为上午
+ mDate = Calendar.getInstance();
+ mInitialising = true;
mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY;
-
- // 第一步:填充控件的核心布局,将xml布局文件加载至当前FrameLayout根容器
inflate(context, R.layout.datetime_picker, this);
- // 第二步:初始化所有子滚轮选择器,绑定控件ID,配置基础属性与监听器
- // 初始化日期选择器:配置取值范围+绑定值变化监听器
mDateSpinner = (NumberPicker) findViewById(R.id.date);
mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL);
mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL);
mDateSpinner.setOnValueChangedListener(mOnDateChangedListener);
- // 初始化小时选择器:仅绑定值变化监听器,取值范围在24/12小时制设置时动态配置
mHourSpinner = (NumberPicker) findViewById(R.id.hour);
mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
-
- // 初始化分钟选择器:配置取值范围+设置长按快速滚动间隔+绑定值变化监听器,细节体验优化
mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
- mMinuteSpinner.setLongPressUpdateInterval(100); // 长按滚动间隔100ms,滚动更快更流畅
+ mMinuteSpinner.setOnLongPressUpdateInterval(100);
mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);
- // 初始化上下午选择器:配置取值范围+加载系统原生的AM/PM文本(适配多语言)+绑定值变化监听器
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings();
mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm);
mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
@@ -353,85 +296,69 @@ public class DateTimePicker extends FrameLayout {
mAmPmSpinner.setDisplayedValues(stringsForAmPm);
mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);
- // 第三步:刷新所有子选择器的初始展示状态,保证UI与初始数据一致
+ // update controls to initial state
updateDateControl();
updateHourControl();
updateAmPmControl();
- // 第四步:配置24/12小时制状态,完成制式适配
set24HourView(is24HourView);
- // 第五步:设置控件的初始选中时间,同步至所有选择器与核心日历对象
+ // set to current time
setCurrentDate(date);
- // 第六步:配置控件的整体启用状态,默认启用
setEnabled(isEnabled());
- // 第七步:初始化完成,解除初始化标记,控件进入正常工作状态,可响应所有操作与回调
+ // set the content descriptions
mInitialising = false;
}
- // ======================== 重写父类方法 - 控件整体启用/禁用【统一状态管控】 ========================
- /**
- * 重写FrameLayout的setEnabled方法,实现控件整体的启用/禁用控制
- * 核心能力:一键同步所有子滚轮选择器的交互状态,无需单独配置每个选择器,保证状态一致性
- * 优化点:状态未变化时直接返回,避免重复执行无效操作,提升性能
- * @param enabled true=启用,所有选择器可滑动选择;false=禁用,所有选择器不可交互,灰显
- */
@Override
public void setEnabled(boolean enabled) {
if (mIsEnabled == enabled) {
return;
}
super.setEnabled(enabled);
- // 同步所有子选择器的启用状态
mDateSpinner.setEnabled(enabled);
mMinuteSpinner.setEnabled(enabled);
mHourSpinner.setEnabled(enabled);
mAmPmSpinner.setEnabled(enabled);
- // 更新全局启用状态标记
mIsEnabled = enabled;
}
- /**
- * 重写FrameLayout的isEnabled方法,获取控件的整体启用状态
- * @return true=控件已启用,false=控件已禁用
- */
@Override
public boolean isEnabled() {
return mIsEnabled;
}
- // ======================== 公共方法区 - 日期时间 取值/赋值 标准化API【外部核心调用接口,最全】 ========================
/**
- * 获取当前选中的完整时间戳,外部最常用的取值方法
- * @return 选中时间的毫秒级时间戳,可直接用于存储、传输、转换,标准化输出
+ * Get the current date in millis
+ *
+ * @return the current date in millis
*/
public long getCurrentDateInTimeMillis() {
return mDate.getTimeInMillis();
}
/**
- * 设置当前选中的时间,通过毫秒级时间戳赋值,外部最常用的赋值方法
- * 核心能力:自动解析时间戳为年月日时分,同步至核心日历对象与所有子选择器,无需手动拆分
- * @param date 要设置的时间戳,单位:毫秒,支持任意合法的时间戳
+ * Set the current date
+ *
+ * @param date The current date in millis
*/
public void setCurrentDate(long date) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(date);
- // 解析时间戳为标准化的年月日时分,调用重载方法完成赋值
setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH),
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
}
/**
- * 设置当前选中的时间,通过标准化的年月日时分赋值,最底层的赋值方法
- * 核心能力:精细化控制每个时间维度的数值,同步至核心日历对象与所有子选择器,数据完全可控
- * @param year 年份,如 2026
- * @param month 月份,Calendar规范,0=1月
- * @param dayOfMonth 日期,当月的第几天,1~31
- * @param hourOfDay 小时,24小时制,0~23
- * @param minute 分钟,0~59
+ * Set the current date
+ *
+ * @param year The current year
+ * @param month The current month
+ * @param dayOfMonth The current dayOfMonth
+ * @param hourOfDay The current hourOfDay
+ * @param minute The current minute
*/
public void setCurrentDate(int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
@@ -443,17 +370,18 @@ public class DateTimePicker extends FrameLayout {
}
/**
- * 获取当前选中的年份
- * @return 年份数值,如 2026
+ * Get current year
+ *
+ * @return The current year
*/
public int getCurrentYear() {
return mDate.get(Calendar.YEAR);
}
/**
- * 设置当前选中的年份
- * 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发
- * @param year 要设置的年份,如 2026
+ * Set current year
+ *
+ * @param year The current year
*/
public void setCurrentYear(int year) {
if (!mInitialising && year == getCurrentYear()) {
@@ -465,17 +393,18 @@ public class DateTimePicker extends FrameLayout {
}
/**
- * 获取当前选中的月份
- * @return 月份数值,遵循Calendar规范,0代表1月,11代表12月
+ * Get current month in the year
+ *
+ * @return The current month in the year
*/
public int getCurrentMonth() {
return mDate.get(Calendar.MONTH);
}
/**
- * 设置当前选中的月份
- * 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发
- * @param month 要设置的月份,Calendar规范,0=1月
+ * Set current month in the year
+ *
+ * @param month The month in the year
*/
public void setCurrentMonth(int month) {
if (!mInitialising && month == getCurrentMonth()) {
@@ -487,17 +416,18 @@ public class DateTimePicker extends FrameLayout {
}
/**
- * 获取当前选中的日期
- * @return 日期数值,当月的第几天,1~31
+ * Get current day of the month
+ *
+ * @return The day of the month
*/
public int getCurrentDay() {
return mDate.get(Calendar.DAY_OF_MONTH);
}
/**
- * 设置当前选中的日期
- * 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发
- * @param dayOfMonth 要设置的日期,1~31
+ * Set current day of the month
+ *
+ * @param dayOfMonth The day of the month
*/
public void setCurrentDay(int dayOfMonth) {
if (!mInitialising && dayOfMonth == getCurrentDay()) {
@@ -509,68 +439,65 @@ public class DateTimePicker extends FrameLayout {
}
/**
- * 获取当前选中的小时数,固定返回24小时制数值,标准化输出,外部无需转换
- * @return 小时数值,0~23
+ * Get current hour in 24 hour mode, in the range (0~23)
+ * @return The current hour in 24 hour mode
*/
public int getCurrentHourOfDay() {
return mDate.get(Calendar.HOUR_OF_DAY);
}
- /**
- * 内部私有方法:获取适配当前制式的小时数
- * 核心能力:根据24/12小时制,返回对应范围的小时数,供内部选择器赋值使用,不对外暴露
- * @return 24小时制返回0~23,12小时制返回1~12,适配选择器的取值范围
- */
private int getCurrentHour() {
if (mIs24HourView){
return getCurrentHourOfDay();
} else {
int hour = getCurrentHourOfDay();
- // 12小时制特殊转换:0点→12点,13点→1点,保证选择器展示正确
- return hour > HOURS_IN_HALF_DAY ? hour - HOURS_IN_HALF_DAY : (hour == 0 ? HOURS_IN_HALF_DAY : hour);
+ if (hour > HOURS_IN_HALF_DAY) {
+ return hour - HOURS_IN_HALF_DAY;
+ } else {
+ return hour == 0 ? HOURS_IN_HALF_DAY : hour;
+ }
}
}
/**
- * 设置当前选中的小时数,接收24小时制数值,标准化输入,内部自动适配转换
- * 核心能力:自动处理24/12小时制的转换逻辑,同步更新上下午状态与选择器,数据无误差
- * 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发
- * @param hourOfDay 要设置的小时数,24小时制,0~23
+ * Set current hour in 24 hour mode, in the range (0~23)
+ *
+ * @param hourOfDay
*/
public void setCurrentHour(int hourOfDay) {
if (!mInitialising && hourOfDay == getCurrentHourOfDay()) {
return;
}
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
-
- // 12小时制下的特殊处理:转换小时数+更新上下午状态
if (!mIs24HourView) {
if (hourOfDay >= HOURS_IN_HALF_DAY) {
mIsAm = false;
- hourOfDay = hourOfDay > HOURS_IN_HALF_DAY ? hourOfDay - HOURS_IN_HALF_DAY : hourOfDay;
+ if (hourOfDay > HOURS_IN_HALF_DAY) {
+ hourOfDay -= HOURS_IN_HALF_DAY;
+ }
} else {
mIsAm = true;
- hourOfDay = hourOfDay == 0 ? HOURS_IN_HALF_DAY : hourOfDay;
+ if (hourOfDay == 0) {
+ hourOfDay = HOURS_IN_HALF_DAY;
+ }
}
updateAmPmControl();
}
-
mHourSpinner.setValue(hourOfDay);
onDateTimeChanged();
}
/**
- * 获取当前选中的分钟数
- * @return 分钟数值,0~59
+ * Get currentMinute
+ *
+ * @return The Current Minute
*/
public int getCurrentMinute() {
return mDate.get(Calendar.MINUTE);
}
/**
- * 设置当前选中的分钟数
- * 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发
- * @param minute 要设置的分钟数,0~59
+ * Set current minute
*/
public void setCurrentMinute(int minute) {
if (!mInitialising && minute == getCurrentMinute()) {
@@ -581,110 +508,76 @@ public class DateTimePicker extends FrameLayout {
onDateTimeChanged();
}
- // ======================== 公共方法区 - 24小时制适配【系统级适配能力】 ========================
/**
- * 判断当前控件是否启用24小时制
- * @return true=24小时制,false=12小时制
+ * @return true if this is in 24 hour view else false.
*/
- public boolean is24HourView () {
- return mIs24HourView;
- }
+ public boolean is24HourView () {
+ return mIs24HourView;
+ }
/**
- * 设置控件的时间展示制式,手动强制切换24/12小时制,优先级高于系统配置
- * 核心能力:切换制式时,自动刷新小时选择器的取值范围、上下午选择器的可见性、当前小时数的展示值,无缝切换无感知
- * 优化点:状态未变化时直接返回,避免无效操作
- * @param is24HourView true=启用24小时制,隐藏上下午选择器;false=启用12小时制,显示上下午选择器
+ * Set whether in 24 hour or AM/PM mode.
+ *
+ * @param is24HourView True for 24 hour mode. False for AM/PM mode.
*/
- public void set24HourView(boolean is24HourView) {
- if (mIs24HourView == is24HourView) {
- return;
- }
- mIs24HourView = is24HourView;
- // 控制上下午选择器的可见性
+ public void set24HourView(boolean is24HourView) {
+ if (mIs24HourView == is24HourView) {
+ return;
+ }
+ mIs24HourView = is24HourView;
mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE);
int hour = getCurrentHourOfDay();
- // 刷新小时选择器的取值范围
updateHourControl();
- // 重新适配并设置小时数,保证展示正确
setCurrentHour(hour);
- // 刷新上下午选择器的状态
updateAmPmControl();
- }
+ }
- // ======================== 内部私有方法区 - 控件UI刷新【核心UI更新逻辑,内部闭环】 ========================
- /**
- * 刷新日期选择器的展示文本与选中状态
- * 核心能力:重新计算并生成近7天的格式化日期文本,更新至日期选择器,保证展示的日期与核心日历对象一致
- * 展示格式:固定为「MM.dd EEEE」,即「月.日 星期」,如「01.15 星期四」
- */
private void updateDateControl() {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(mDate.getTimeInMillis());
- // 计算近7天的起始日期:当前日期向前推4天,保证选中日期在列表中间位置,交互更友好
cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1);
-
- mDateSpinner.setDisplayedValues(null); // 清空原有文本,避免残留
- // 循环生成7天的格式化日期文本
+ mDateSpinner.setDisplayedValues(null);
for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) {
cal.add(Calendar.DAY_OF_YEAR, 1);
mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal);
}
- // 将生成的文本数组设置到日期选择器
mDateSpinner.setDisplayedValues(mDateDisplayValues);
- // 默认选中列表中间项,即当前日期
mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2);
- mDateSpinner.invalidate(); // 强制刷新UI,保证展示生效
+ mDateSpinner.invalidate();
}
- /**
- * 刷新上下午选择器的选中状态与可见性
- * 核心能力:根据当前的24小时制状态与上下午标记,同步更新上下午选择器的展示,保证UI与数据一致
- */
private void updateAmPmControl() {
if (mIs24HourView) {
mAmPmSpinner.setVisibility(View.GONE);
} else {
- // 设置选中项:0=上午/AM,1=下午/PM
int index = mIsAm ? Calendar.AM : Calendar.PM;
mAmPmSpinner.setValue(index);
mAmPmSpinner.setVisibility(View.VISIBLE);
}
}
- /**
- * 刷新小时选择器的取值范围
- * 核心能力:根据当前的24小时制状态,动态配置小时选择器的最小值与最大值,适配不同制式的展示需求
- */
- private void updateHourControl() {
- if (mIs24HourView) {
- mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
- mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW);
- } else {
- mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
- mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
- }
- }
+ private void updateHourControl() {
+ if (mIs24HourView) {
+ mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
+ mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW);
+ } else {
+ mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
+ mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
+ }
+ }
- // ======================== 公共方法区 - 回调监听器绑定【通信入口】 ========================
/**
- * 设置日期时间变化的回调监听器,外部通过此方法绑定业务逻辑
- * @param callback 外部实现的OnDateTimeChangedListener接口实例,传null则取消监听
+ * Set the callback that indicates the 'Set' button has been pressed.
+ * @param callback the callback, if null will do nothing
*/
public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) {
mOnDateTimeChangedListener = callback;
}
- // ======================== 内部私有方法区 - 回调触发【核心通信逻辑,内部闭环】 ========================
- /**
- * 触发日期时间变化的回调方法,所有选择操作的最终数据出口
- * 核心能力:从核心日历对象中读取标准化的年月日时分数据,调用外部绑定的监听器,完成数据传递
- * 优化点:监听器为null时直接返回,避免空指针异常
- */
private void onDateTimeChanged() {
if (mOnDateTimeChangedListener != null) {
mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(),
getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute());
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java b/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java
index 019694a..a97590a 100644
--- a/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java
+++ b/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java
@@ -14,160 +14,115 @@
* limitations under the License.
*/
-// 包声明:归属小米便签UI模块,日期时间选择弹窗的核心封装类,集成自定义时间选择控件
package net.micode.notes.ui;
-// Java日历工具类,核心时间处理载体,存储用户选中的年月日时分秒,提供时间的赋值与转换能力
import java.util.Calendar;
-// 小米便签资源文件引用,获取字符串、布局等资源ID常量
import net.micode.notes.R;
-// 自定义日期时间选择核心控件,本弹窗的核心内容视图,提供年月日时分滚轮选择UI
import net.micode.notes.ui.DateTimePicker;
import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener;
-// 安卓系统弹窗核心类,本类的父类,提供弹窗的基础展示与按钮配置能力
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
-// 安卓系统日期格式化工具类,提供24小时制判断、时间戳转友好文本的格式化能力
import android.text.format.DateFormat;
import android.text.format.DateUtils;
/**
- * 日期时间选择弹窗封装类【功能型弹窗核心类】
- * 继承:AlertDialog 安卓原生弹窗基类,具备系统弹窗的所有基础特性
- * 核心定位:小米便签「设置便签提醒时间」功能的专属弹窗,整合自定义DateTimePicker控件实现完整的时间选择能力
- * 核心设计思想:将「时间选择控件+弹窗载体+时间数据处理+回调通信」全部封装,对外提供极简调用API
- * 核心职责:
- * 1. 集成自定义DateTimePicker滚轮选择控件,作为弹窗的核心内容视图,提供年月日时分的可视化选择;
- * 2. 统一管理选中的时间数据,基于Calendar类存储完整时间信息,自动处理时间联动更新逻辑;
- * 3. 适配系统全局的24小时制/12小时制显示规则,自动切换时间展示格式,保证与系统行为一致;
- * 4. 实时更新弹窗标题为当前选中的格式化时间文本,给用户直观的选择反馈;
- * 5. 封装标准化的回调接口,选择完成后向外传递最终的毫秒级时间戳,解耦业务逻辑;
- * 6. 固化弹窗的按钮交互逻辑:确定按钮触发回调、取消按钮关闭弹窗无操作,交互统一;
- * 7. 时间精度控制:仅保留到分钟级,自动置空秒数,符合便签提醒的业务使用场景。
- * 典型业务场景:便签编辑页的「添加提醒」「修改提醒时间」功能弹窗,是便签日程提醒的核心交互载体。
+ * 日期时间选择对话框
+ * 提供用户界面来选择日期和时间,用于设置便签的提醒时间
+ * 继承自AlertDialog,集成了DateTimePicker组件
+ *
+ * 架构设计:
+ * - 继承自AlertDialog,实现OnClickListener接口
+ * - 内部使用DateTimePicker作为日期时间选择控件
+ * - 提供OnDateTimeSetListener接口回调选择结果
+ * - 支持24小时制和12小时制显示
+ * - 自动更新对话框标题以反映当前选择的时间
+ *
+ * 核心功能:
+ * - 显示日期时间选择界面
+ * - 支持年、月、日、时、分的选择
+ * - 实时更新选择的日期时间
+ * - 提供确定和取消按钮
+ * - 回调选择的日期时间结果
+ * - 适配系统的时间格式设置
*/
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
- // 核心时间数据载体:存储用户当前选中的完整日期时间,初始化时获取系统当前时间,秒数固定置0
+ /**
+ * 日期时间日历实例
+ */
private Calendar mDate = Calendar.getInstance();
- // 24小时制标记位:适配系统设置,决定时间的展示格式(24小时/12小时带上午下午)
+ /**
+ * 是否使用24小时制
+ */
private boolean mIs24HourView;
- // 时间选择完成的回调监听器:外部实现该接口接收最终选中的时间戳,核心通信桥梁
+ /**
+ * 日期时间设置监听器
+ */
private OnDateTimeSetListener mOnDateTimeSetListener;
- // 自定义日期时间选择控件:本弹窗的核心内容视图,提供滚轮式年月日时分选择UI,业务核心控件
+ /**
+ * 日期时间选择器
+ */
private DateTimePicker mDateTimePicker;
/**
- * 日期时间选择完成的回调接口【标准化通信接口】
- * 设计原则:解耦弹窗内部逻辑与外部业务逻辑,弹窗只负责提供选择能力,不处理业务逻辑
- * 调用时机:用户点击弹窗的「确定」按钮后,触发该接口的回调方法,传递选中的时间数据
+ * 日期时间设置监听器接口
*/
public interface OnDateTimeSetListener {
/**
- * 时间选择完成的回调方法
- * @param dialog 当前的日期时间选择弹窗实例,外部可通过该实例做弹窗关闭等操作
- * @param date 用户最终选中的时间戳,单位:毫秒,秒数已固定置为0,精度到分钟级
+ * 当用户设置日期时间时调用
+ * @param dialog 对话框实例
+ * @param date 选择的日期时间(毫秒)
*/
void OnDateTimeSet(AlertDialog dialog, long date);
}
- /**
- * 构造方法:初始化日期时间选择弹窗的全部核心配置【一站式初始化】
- * 核心能力:传入上下文和初始时间戳后,自动完成「控件创建+视图绑定+数据初始化+事件绑定+按钮配置+标题更新」
- * 无需外部执行任何额外配置,开箱即用,调用简洁
- * @param context 应用上下文对象,用于创建弹窗、加载资源、适配系统设置,不可为空
- * @param date 弹窗初始化时的默认选中时间戳,单位:毫秒,支持传入指定时间做默认值展示
- */
public DateTimePickerDialog(Context context, long date) {
super(context);
- // 第一步:创建自定义的日期时间选择控件,作为弹窗的核心内容视图,替换原生弹窗的默认布局
mDateTimePicker = new DateTimePicker(context);
setView(mDateTimePicker);
-
- // 第二步:为时间选择控件绑定实时变化监听器,核心联动逻辑
- // 监听用户在滚轮上的每一次选择操作,实时同步选中的时间数据到Calendar对象中
mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
public void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
- // 实时更新核心时间载体:将滚轮选中的年月日时分赋值到Calendar对象
mDate.set(Calendar.YEAR, year);
mDate.set(Calendar.MONTH, month);
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
mDate.set(Calendar.MINUTE, minute);
- // 实时更新弹窗标题:将最新选中的时间格式化后展示在弹窗顶部,给用户即时反馈
updateTitle(mDate.getTimeInMillis());
}
});
-
- // 第三步:初始化选中的时间数据,设置默认选中的时间戳,统一时间精度
mDate.setTimeInMillis(date);
- mDate.set(Calendar.SECOND, 0); // 强制置空秒数,仅保留到分钟级,符合业务使用场景
- // 将初始化的时间数据同步到时间选择控件,保证弹窗打开时展示正确的默认时间
+ mDate.set(Calendar.SECOND, 0);
mDateTimePicker.setCurrentDate(mDate.getTimeInMillis());
-
- // 第四步:配置弹窗的底部按钮,固化交互逻辑,符合用户操作习惯
- setButton(context.getString(R.string.datetime_dialog_ok), this); // 确定按钮,绑定当前类的点击事件
- setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); // 取消按钮,无业务逻辑,点击仅关闭弹窗
-
- // 第五步:适配系统全局的时间显示规则,自动获取系统是否开启24小时制,保证体验一致性
+ setButton(context.getString(R.string.datetime_dialog_ok), this);
+ setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null);
set24HourView(DateFormat.is24HourFormat(this.getContext()));
-
- // 第六步:初始化弹窗标题,将默认时间格式化后展示,完成弹窗的最终初始化
updateTitle(mDate.getTimeInMillis());
}
- /**
- * 公有配置方法:手动设置弹窗的时间显示格式
- * 补充能力:支持外部手动覆盖系统的24小时制设置,按需指定显示规则,适配特殊业务场景
- * @param is24HourView true=强制使用24小时制展示时间,false=强制使用12小时制展示时间
- */
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
- /**
- * 公有绑定方法:设置时间选择完成的回调监听器
- * 核心作用:为弹窗绑定外部的业务逻辑处理类,是弹窗向外传递数据的唯一入口
- * @param callBack 外部实现的OnDateTimeSetListener接口实例,接收最终选中的时间戳
- */
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
- /**
- * 私有工具方法:更新弹窗标题文本【核心格式化方法】
- * 核心职责:将毫秒级的时间戳,按照指定的格式规则,转换为用户友好的日期时间文本
- * 设计亮点:通过格式化标记位组合,灵活配置展示内容,无需手写格式化字符串,适配性更强
- * @param date 需要格式化展示的时间戳,单位:毫秒
- */
private void updateTitle(long date) {
- // 定义时间格式化的标记位组合,指定需要展示的时间维度:年 + 月日 + 时分
int flag =
- DateUtils.FORMAT_SHOW_YEAR | // 强制展示年份,如「2026年」
- DateUtils.FORMAT_SHOW_DATE | // 强制展示日期,如「1月15日」
- DateUtils.FORMAT_SHOW_TIME; // 强制展示时间,如「15:30」或「下午3:30」
- // 根据24小时制标记位,追加对应的格式化规则,自动切换显示格式
+ DateUtils.FORMAT_SHOW_YEAR |
+ DateUtils.FORMAT_SHOW_DATE |
+ DateUtils.FORMAT_SHOW_TIME;
flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR;
- // 将时间戳格式化后,设置为弹窗的标题文本,完成UI更新
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
}
- /**
- * 弹窗确定按钮的点击事件处理【核心交互方法】
- * 实现OnClickListener接口的核心方法,仅响应确定按钮的点击行为
- * 核心逻辑:判断是否绑定了回调监听器,若绑定则触发回调,传递选中的时间戳,完成数据通信
- * @param arg0 触发点击事件的弹窗对话框实例
- * @param arg1 被点击按钮的索引标识,对应确定/取消等按钮类型
- */
public void onClick(DialogInterface arg0, int arg1) {
- // 空值安全校验:避免未绑定回调监听器时触发空指针异常
if (mOnDateTimeSetListener != null) {
- // 触发外部回调,传递弹窗实例和最终选中的时间戳,完成时间选择的业务闭环
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
}
diff --git a/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java b/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java
index 849b09f..2360cf0 100644
--- a/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java
+++ b/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java
@@ -14,109 +14,96 @@
* limitations under the License.
*/
-// 包声明:归属小米便签UI模块,下拉菜单功能的统一封装工具类,全局复用
package net.micode.notes.ui;
-// 安卓系统核心类 - 上下文,提供资源加载、控件创建的运行环境,必须依赖项
import android.content.Context;
-// 安卓菜单体系核心类,Menu管理菜单整体结构,MenuItem表示单个菜单项
import android.view.Menu;
import android.view.MenuItem;
-// 安卓视图体系核心类,处理视图点击事件、视图基础属性配置
import android.view.View;
import android.view.View.OnClickListener;
-// 安卓按钮控件,作为下拉菜单的触发载体,本类核心绑定控件
import android.widget.Button;
-// 安卓原生下拉菜单核心控件,实现弹窗式菜单展示,本类封装的核心原生控件
import android.widget.PopupMenu;
-// 安卓下拉菜单的菜单项点击事件监听器,监听菜单选项的点击行为
import android.widget.PopupMenu.OnMenuItemClickListener;
-// 小米便签资源文件引用,获取图标、布局等资源ID常量
import net.micode.notes.R;
/**
- * 下拉菜单功能封装工具类【全局复用型UI工具类】
- * 核心设计模式:封装模式,对Android原生PopupMenu+触发Button进行一站式封装
- * 核心定位:小米便签内所有下拉菜单场景的统一实现方案,抽离通用逻辑,避免重复开发
- * 核心价值:
- * 1. 屏蔽原生PopupMenu的复杂创建流程,对外提供极简的调用API,一行代码即可创建下拉菜单;
- * 2. 统一应用内所有下拉菜单的视觉样式(触发按钮背景图标、菜单展示位置),保证UI一致性;
- * 3. 封装菜单资源加载、点击事件绑定、菜单项查找、按钮文本更新等全套核心逻辑;
- * 4. 解耦下拉菜单的「创建-展示-事件处理」逻辑,外部无需关心内部实现细节;
- * 5. 轻量化封装,无冗余逻辑,仅做功能整合,不侵入业务代码,适配所有下拉菜单使用场景。
- * 典型业务场景:便签列表页的排序方式选择、文件夹操作菜单、更多功能选项、筛选条件下拉框等。
+ * 下拉菜单工具类
+ * 封装了PopupMenu的创建和管理,提供简洁的接口来创建和使用下拉菜单
+ * 简化了下拉菜单的初始化和事件处理流程
+ *
+ * 架构设计:
+ * - 包装Button和PopupMenu,形成完整的下拉菜单组件
+ * - 提供简洁的构造函数,一次性完成菜单初始化
+ * - 支持菜单项点击事件监听
+ * - 提供菜单标题设置和菜单项查找功能
+ *
+ * 核心功能:
+ * - 创建基于Button的下拉菜单
+ * - 自动设置下拉箭头图标
+ * - 从菜单资源文件加载菜单项
+ * - 处理按钮点击显示菜单的逻辑
+ * - 支持设置菜单项点击监听器
+ * - 提供菜单标题设置功能
+ * - 支持通过ID查找菜单项
*/
public class DropdownMenu {
- // 下拉菜单的触发按钮,全局持用引用:点击该按钮即可弹出下拉菜单,统一配置背景样式
+ /**
+ * 下拉菜单按钮
+ */
private Button mButton;
- // Android原生下拉菜单核心控件,本类封装的核心对象:负责菜单的弹出、收起、承载菜单项
+ /**
+ * 弹出菜单实例
+ */
private PopupMenu mPopupMenu;
- // PopupMenu对应的菜单容器对象:用于动态查找、修改、管理菜单项的状态(显示/隐藏/禁用等)
+ /**
+ * 菜单实例
+ */
private Menu mMenu;
/**
- * 构造方法:初始化下拉菜单的全部核心配置【一站式初始化】
- * 核心能力:传入必要参数后,自动完成「按钮样式配置+下拉菜单创建+菜单资源加载+点击触发绑定」
- * 无需外部执行任何额外初始化操作,极简调用,开箱即用
- * @param context 上下文对象,用于创建PopupMenu、加载菜单资源、获取应用运行环境,不可为空
- * @param button 触发下拉菜单的按钮控件,菜单会锚定该按钮在下方弹出,与菜单强绑定
- * @param menuId 菜单布局的资源ID(如R.menu.menu_sort),定义下拉菜单的所有选项列表
+ * 构造函数
+ * @param context 上下文
+ * @param button 下拉菜单按钮
+ * @param menuId 菜单资源ID
*/
public DropdownMenu(Context context, Button button, int menuId) {
- // 保存触发按钮的全局引用,供后续设置文本、复用控件使用
mButton = button;
- // 统一设置触发按钮的背景样式:加载内置的下拉箭头图标,保证所有下拉按钮视觉统一
mButton.setBackgroundResource(R.drawable.dropdown_icon);
- // 创建原生PopupMenu对象,绑定上下文和触发按钮,指定菜单弹出的锚点位置
mPopupMenu = new PopupMenu(context, mButton);
- // 获取PopupMenu内部的Menu容器对象,缓存引用,供后续菜单项查找使用,避免重复获取
mMenu = mPopupMenu.getMenu();
- // 通过菜单解析器,将指定的菜单布局资源加载到Menu容器中,完成菜单项的初始化展示
mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
- // 为触发按钮绑定点击事件监听器:点击按钮时,自动弹出下拉菜单,核心触发逻辑
mButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
- // 弹出下拉菜单,菜单默认展示在触发按钮的下方,对齐方式由系统原生适配
mPopupMenu.show();
}
});
}
/**
- * 对外提供的事件绑定方法:设置下拉菜单项的点击事件监听器
- * 核心设计:事件逻辑完全交由外部实现,本类只做事件转发,无任何业务逻辑侵入,高度解耦
- * 外部可根据菜单项的ID,分别处理不同选项的点击业务(如排序、筛选、删除、移动等)
- * @param listener 菜单项点击事件监听器,外部实现该接口的onMenuItemClick方法处理具体逻辑
+ * 设置菜单项点击监听器
+ * @param listener 菜单项点击监听器
*/
public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) {
- // 空值安全校验:避免PopupMenu未初始化完成时绑定监听器导致空指针异常
if (mPopupMenu != null) {
- // 将外部传入的监听器绑定到PopupMenu,所有菜单项的点击事件都会回调该监听器
mPopupMenu.setOnMenuItemClickListener(listener);
}
}
/**
- * 工具方法:根据菜单项的资源ID查找对应的MenuItem对象
- * 核心用途:支持外部动态修改菜单项的状态,如「隐藏/显示菜单项」「禁用/启用菜单项」「修改菜单项文本/图标」等
- * 是实现菜单动态化配置的核心入口,满足复杂业务场景的菜单定制需求
- * @param id 目标菜单项的资源ID(如R.id.menu_sort_by_time、R.id.menu_move_folder)
- * @return MenuItem 找到的菜单项对象,未找到时返回null
+ * 根据ID查找菜单项
+ * @param id 菜单项ID
+ * @return 菜单项实例
*/
public MenuItem findItem(int id) {
- // 直接通过缓存的Menu对象查找菜单项,高效无冗余
return mMenu.findItem(id);
}
/**
- * 工具方法:动态设置下拉菜单触发按钮的显示文本
- * 核心业务价值:支持按钮文本的动态更新,适配「当前选中状态展示」场景
- * 比如:排序菜单按钮显示「按时间排序」、文件夹菜单按钮显示「当前文件夹:工作」等
- * @param title 按钮需要展示的文本内容,支持字符串常量、动态拼接字符串
+ * 设置下拉菜单按钮标题
+ * @param title 标题文本
*/
public void setTitle(CharSequence title) {
- // 直接为触发按钮设置文本内容,更新UI展示
mButton.setText(title);
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java b/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java
index 340650f..736096b 100644
--- a/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java
+++ b/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java
@@ -14,166 +14,97 @@
* limitations under the License.
*/
-// 包声明:归属小米便签UI模块,是文件夹列表展示的核心数据适配器
package net.micode.notes.ui;
-// 安卓系统核心类 - 上下文,提供资源加载、视图创建的运行环境
import android.content.Context;
-// 安卓数据库游标类,承载文件夹的数据库查询结果集,适配器的核心数据源
import android.database.Cursor;
-// 安卓视图体系核心类,用于创建和装载列表子项视图
import android.view.View;
import android.view.ViewGroup;
-// 安卓游标适配器基类,适配Cursor数据源的列表专用适配器,本类核心父类
import android.widget.CursorAdapter;
-// 安卓线性布局,作为自定义列表项的根布局容器
import android.widget.LinearLayout;
-// 安卓文本控件,用于展示文件夹名称文本内容
import android.widget.TextView;
-// 小米便签资源文件引用,获取布局、字符串等资源id
import net.micode.notes.R;
-// 小米便签核心数据常量,定义文件夹id、数据库字段名等全局常量
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
+
/**
- * 文件夹列表专用数据适配器【核心功能适配器】
- * 继承:CursorAdapter 安卓原生游标适配器,完美适配数据库查询的Cursor结果集
- * 核心定位:小米便签中「移动便签到文件夹」弹窗的核心适配器,负责文件夹列表的数据绑定与视图渲染
- * 核心职责:
- * 1. 定义文件夹查询的精简投影字段,只查必要数据,减少数据库IO开销,提升列表加载效率;
- * 2. 实现Cursor数据与列表子项视图的绑定逻辑,规范数据渲染流程;
- * 3. 对「根文件夹」做特殊文本适配,将系统根文件夹名称替换为业务友好的文本提示;
- * 4. 封装文件夹名称获取工具方法,对外提供统一的名称读取接口,解耦外部调用逻辑;
- * 5. 内置自定义列表项布局,封装视图创建与控件绑定,避免外部多次 findViewById 性能损耗。
- * 核心特点:轻量高效、职责单一,仅处理文件夹列表的「数据-视图」映射关系,无业务逻辑侵入。
+ * 文件夹列表适配器
+ * 用于显示便签文件夹列表,支持文件夹选择功能
+ * 作为便签分类管理的重要组件,提供文件夹的可视化展示
+ *
+ * 架构设计:
+ * - 继承自CursorAdapter,使用游标数据绑定
+ * - 自定义FolderListItem作为列表项视图
+ * - 支持根文件夹的特殊显示
+ * - 提供获取文件夹名称的方法
+ *
+ * 核心功能:
+ * - 显示文件夹列表
+ * - 根文件夹特殊显示为"父文件夹"
+ * - 绑定文件夹数据到列表项
+ * - 提供通过位置获取文件夹名称的方法
*/
public class FoldersListAdapter extends CursorAdapter {
/**
- * 文件夹数据库查询投影数组【核心常量】
- * 设计原则:按需查询,只获取列表展示所需的核心字段,不查冗余字段,降低内存占用和查询耗时
- * 字段组成:仅包含文件夹的唯一标识和展示名称,满足列表所有业务需求
+ * 查询投影列,包含文件夹ID和名称
*/
public static final String [] PROJECTION = {
- NoteColumns.ID, // 数组索引0 - 文件夹的唯一ID,数据库主键
- NoteColumns.SNIPPET // 数组索引1 - 文件夹的名称,用于列表展示
+ NoteColumns.ID,
+ NoteColumns.SNIPPET
};
/**
- * 投影字段对应的列索引常量
- * 设计目的:封装索引数值,避免代码中出现硬编码数字,提升代码可读性和可维护性
- * 作用:通过常量直接取值,Cursor.getInt(ID_COLUMN) 比 Cursor.getInt(0) 语义更清晰
+ * ID列索引
+ */
+ public static final int ID_COLUMN = 0;
+ /**
+ * 名称列索引
*/
- public static final int ID_COLUMN = 0; // 文件夹ID对应的游标列索引
- public static final int NAME_COLUMN = 1; // 文件夹名称对应的游标列索引
+ public static final int NAME_COLUMN = 1;
/**
- * 构造方法:初始化文件夹列表适配器
- * 父类传参:将上下文和游标数据源传递给CursorAdapter基类,完成适配器初始化
- * @param context 上下文对象,用于加载资源、创建视图,不可为空
- * @param c 数据库查询返回的游标,封装了所有文件夹的ID和名称数据,游标已完成查询定位
+ * 构造函数
+ * @param context 上下文
+ * @param c 包含文件夹数据的游标
*/
public FoldersListAdapter(Context context, Cursor c) {
super(context, c);
}
- /**
- * 重写父类方法:创建列表子项的空白视图
- * 生命周期:列表滚动时,为屏幕外新进入的位置创建全新的视图对象,复用性低
- * 核心逻辑:仅创建视图容器,不做任何数据绑定,数据绑定由bindView方法完成
- * @param context 上下文对象,用于创建视图
- * @param cursor 当前位置对应的游标数据,本方法中暂未使用,仅遵循父类接口规范
- * @param parent 列表子项的父容器,即承载所有文件夹项的ListView
- * @return View 新建的、未绑定数据的文件夹列表子项视图
- */
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
- // 创建自定义的文件夹列表项视图,内部已完成布局加载和控件绑定
return new FolderListItem(context);
}
- /**
- * 重写父类核心方法:为列表子项视图绑定对应的数据【核心业务方法】
- * 生命周期:列表首次加载/滚动复用视图时调用,所有数据渲染逻辑均在此实现
- * 核心业务规则:
- * 1. 根文件夹(ID=Notes.ID_ROOT_FOLDER):不展示原始名称,替换为业务文本「移动到上级文件夹」;
- * 2. 普通文件夹:直接展示数据库中存储的文件夹名称;
- * 3. 类型安全校验:只处理自定义的FolderListItem视图,防止视图类型异常导致崩溃。
- * @param view 待绑定数据的列表子项视图,可为newView创建的新视图,也可为复用的旧视图
- * @param context 上下文对象,用于加载字符串资源
- * @param cursor 当前列表位置对应的游标,已定位到对应行,可直接读取字段值
- */
@Override
public void bindView(View view, Context context, Cursor cursor) {
- // 视图类型校验,保证类型安全,避免视图强转异常
if (view instanceof FolderListItem) {
- // 读取当前游标中的文件夹ID,判断是否为根文件夹
- long folderId = cursor.getLong(ID_COLUMN);
- String folderName;
-
- // 根文件夹特殊处理:替换展示文本;普通文件夹使用原始名称
- if (folderId == Notes.ID_ROOT_FOLDER) {
- folderName = context.getString(R.string.menu_move_parent_folder);
- } else {
- folderName = cursor.getString(NAME_COLUMN);
- }
-
- // 调用自定义视图的绑定方法,将处理后的文件夹名称设置到文本控件
+ String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context
+ .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
((FolderListItem) view).bind(folderName);
}
}
- /**
- * 对外提供的工具方法:根据列表位置获取对应的文件夹名称
- * 封装价值:外部调用方无需操作游标,直接传入位置即可获取名称,隐藏游标操作细节,解耦调用逻辑
- * 业务规则:与bindView保持完全一致,根文件夹返回「移动到上级文件夹」,保证数据一致性
- * @param context 上下文对象,用于加载根文件夹的业务文本
- * @param position 列表中的目标位置索引,从0开始
- * @return String 处理后的文件夹展示名称
- */
public String getFolderName(Context context, int position) {
- // 根据位置获取对应的游标对象,游标已自动定位到对应行
Cursor cursor = (Cursor) getItem(position);
- long folderId = cursor.getLong(ID_COLUMN);
-
- // 根文件夹判断与名称适配,逻辑与bindView完全一致
- return folderId == Notes.ID_ROOT_FOLDER ? context.getString(R.string.menu_move_parent_folder)
- : cursor.getString(NAME_COLUMN);
+ return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context
+ .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
}
- /**
- * 私有内部类:文件夹列表的自定义子项视图【视图封装核心类】
- * 继承:LinearLayout 线性布局,作为子项的根布局
- * 设计思想:视图控件封装化,将列表项的布局加载、控件绑定、数据绑定全部封装在内部
- * 核心优势:外部无需关心视图内部结构,只需调用bind方法即可完成数据渲染,符合封装原则
- * 访问权限:private 私有,仅当前适配器可创建使用,不对外暴露,保证视图安全性
- */
private class FolderListItem extends LinearLayout {
- // 列表项核心控件:展示文件夹名称的文本控件,全局缓存避免重复查找
private TextView mName;
- /**
- * 构造方法:初始化自定义列表项视图
- * 核心逻辑:加载布局文件 + 绑定内部控件,只执行一次,视图创建时完成初始化
- * @param context 上下文对象,用于加载布局资源和查找控件
- */
public FolderListItem(Context context) {
super(context);
- // 将文件夹列表项的布局文件,填充到当前的LinearLayout根布局中
inflate(context, R.layout.folder_list_item, this);
- // 查找并缓存文件夹名称文本控件,后续直接复用,提升性能
mName = (TextView) findViewById(R.id.tv_folder_name);
}
- /**
- * 视图数据绑定方法:为文本控件设置文件夹名称
- * 核心作用:作为适配器与自定义视图的通信桥梁,只做单一的文本赋值操作
- * @param name 处理后的文件夹展示名称(根文件夹/普通文件夹)
- */
public void bind(String name) {
mName.setText(name);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/ui/FriendManagementActivity.java b/src/Notes-master/src/net/micode/notes/ui/FriendManagementActivity.java
new file mode 100644
index 0000000..f299a36
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/FriendManagementActivity.java
@@ -0,0 +1,212 @@
+package net.micode.notes.ui;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import net.micode.notes.R;
+import net.micode.notes.data.NotesDatabaseHelper;
+import net.micode.notes.tool.UserManager;
+import net.micode.notes.data.Users;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 好友管理活动
+ *
+ * 该类负责实现好友列表的显示和管理功能,用户可以通过该界面查看好友列表并启动与好友的聊天。
+ * 它通过NotesDatabaseHelper操作用户数据表,获取除当前用户以外的所有用户作为好友列表。
+ */
+public class FriendManagementActivity extends Activity {
+ private ListView mFriendListView;
+ private FriendAdapter mFriendAdapter;
+ private List mFriendList;
+ private NotesDatabaseHelper mDbHelper;
+ private SQLiteDatabase mDb;
+ private UserManager mUserManager;
+ private long mCurrentUserId;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_friend_management);
+
+ // 设置ActionBar
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setTitle("好友管理");
+ }
+
+ // 初始化数据库
+ mDbHelper = NotesDatabaseHelper.getInstance(this);
+ mDb = mDbHelper.getWritableDatabase();
+
+ // 初始化UserManager
+ mUserManager = UserManager.getInstance(this);
+ mCurrentUserId = mUserManager.getCurrentUserId();
+
+ // 初始化ListView
+ mFriendListView = findViewById(R.id.friend_list);
+ mFriendList = new ArrayList<>();
+ mFriendAdapter = new FriendAdapter(this, mFriendList);
+ mFriendListView.setAdapter(mFriendAdapter);
+
+ // 设置ListView点击事件
+ mFriendListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ Friend friend = mFriendList.get(position);
+ // 启动与好友聊天的活动
+ Intent intent = new Intent(FriendManagementActivity.this, ChatActivity.class);
+ intent.putExtra("friend_id", friend.id);
+ intent.putExtra("friend_username", friend.username);
+ startActivity(intent);
+ }
+ });
+
+ // 加载好友列表
+ loadFriendList();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // 重新加载好友列表
+ loadFriendList();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 关闭数据库连接
+ if (mDb != null && mDb.isOpen()) {
+ mDb.close();
+ }
+ }
+
+ /**
+ * 加载好友列表,即除当前用户以外的所有用户
+ */
+ private void loadFriendList() {
+ mFriendList.clear();
+
+ // 查询除当前用户以外的所有用户
+ Cursor cursor = mDb.query(
+ NotesDatabaseHelper.TABLE.USER,
+ new String[]{Users.UserColumns.ID, Users.UserColumns.USERNAME},
+ Users.UserColumns.ID + " != ?",
+ new String[]{String.valueOf(mCurrentUserId)},
+ null,
+ null,
+ null
+ );
+
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(Users.UserColumns.ID));
+ String username = cursor.getString(cursor.getColumnIndexOrThrow(Users.UserColumns.USERNAME));
+ mFriendList.add(new Friend(id, username));
+ }
+ cursor.close();
+ }
+
+ // 通知适配器数据变化
+ mFriendAdapter.notifyDataSetChanged();
+
+ // 如果没有好友,显示提示
+ if (mFriendList.isEmpty()) {
+ Toast.makeText(this, "暂无其他用户", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 好友实体类
+ */
+ private static class Friend {
+ long id;
+ String username;
+
+ Friend(long id, String username) {
+ this.id = id;
+ this.username = username;
+ }
+ }
+
+ /**
+ * 好友列表适配器
+ */
+ private static class FriendAdapter extends BaseAdapter {
+ private Context mContext;
+ private List mFriendList;
+ private LayoutInflater mInflater;
+
+ FriendAdapter(Context context, List friendList) {
+ mContext = context;
+ mFriendList = friendList;
+ mInflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public int getCount() {
+ return mFriendList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mFriendList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.item_friend, parent, false);
+ holder = new ViewHolder();
+ holder.usernameTextView = convertView.findViewById(R.id.friend_username);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ Friend friend = mFriendList.get(position);
+ holder.usernameTextView.setText(friend.username);
+
+ return convertView;
+ }
+
+ private static class ViewHolder {
+ TextView usernameTextView;
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(android.view.MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // 返回上一级活动
+ finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/ui/FriendNoteEditActivity.java b/src/Notes-master/src/net/micode/notes/ui/FriendNoteEditActivity.java
new file mode 100644
index 0000000..48916ad
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/FriendNoteEditActivity.java
@@ -0,0 +1,264 @@
+package net.micode.notes.ui;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import net.micode.notes.R;
+import net.micode.notes.data.Notes;
+
+
+/**
+ * 好友便签查看活动类
+ * 用于查看好友的公开便签详情,以只读模式显示便签内容
+ * 作为FriendNoteListActivity的配套组件,提供便签内容的详细展示
+ *
+ * 架构设计:
+ * - 继承自Activity,使用简单的TextView布局
+ * - 以只读模式显示便签标题和内容
+ * - 显示便签的修改时间
+ * - 支持从Intent中获取便签ID和好友ID
+ * - 包含状态保存和恢复机制
+ *
+ * 核心功能:
+ * - 查看好友公开便签的详细内容
+ * - 显示便签标题和修改时间
+ * - 支持返回上一级活动
+ * - 处理便签数据加载失败的情况
+ * - 适配不同的便签状态(有标题/无标题,有内容/无内容)
+ */
+public class FriendNoteEditActivity extends Activity {
+ /**
+ * 便签内容编辑器
+ */
+ private TextView mNoteEditor;
+ /**
+ * 便签标题视图
+ */
+ private TextView mNoteTitleView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_friend_note_edit);
+
+ // 设置ActionBar
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setTitle("查看便签");
+ }
+
+ // 初始化控件
+ mNoteEditor = findViewById(R.id.note_edit_view);
+ mNoteTitleView = findViewById(R.id.note_title_view);
+
+ // 加载便签数据
+ if (savedInstanceState == null && !initActivityState(getIntent())) {
+ finish();
+ return;
+ }
+ initNoteScreen();
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID));
+ intent.putExtra("friend_id", savedInstanceState.getLong("friend_id"));
+ if (!initActivityState(intent)) {
+ finish();
+ return;
+ }
+ // 恢复状态后重新初始化界面
+ initNoteScreen();
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (getIntent() != null) {
+ outState.putLong(Intent.EXTRA_UID, getIntent().getLongExtra(Intent.EXTRA_UID, 0));
+ outState.putLong("friend_id", getIntent().getLongExtra("friend_id", -1));
+ }
+ }
+
+ private String mNoteContent;
+ private String mNoteTitle;
+ private long mModifiedDate;
+
+ private boolean initActivityState(Intent intent) {
+ /**
+ * 只支持查看模式
+ */
+ long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0);
+ long friendId = intent.getLongExtra("friend_id", -1);
+
+ if (noteId <= 0 || friendId <= 0) {
+ Toast.makeText(this, "便签信息错误", Toast.LENGTH_SHORT).show();
+ finish();
+ return false;
+ }
+
+ try {
+ // 查询便签基本信息,包括标题
+ String[] noteProjection = {
+ net.micode.notes.data.Notes.NoteColumns.MODIFIED_DATE,
+ net.micode.notes.data.Notes.NoteColumns.TITLE
+ };
+
+ android.database.Cursor noteCursor = null;
+ try {
+ // 使用ContentResolver查询便签基本信息
+ noteCursor = getContentResolver().query(
+ android.content.ContentUris.withAppendedId(net.micode.notes.data.Notes.CONTENT_NOTE_URI, noteId),
+ noteProjection,
+ null,
+ null,
+ null
+ );
+
+ if (noteCursor != null) {
+ if (noteCursor.moveToFirst()) {
+ mModifiedDate = noteCursor.getLong(0);
+ mNoteTitle = noteCursor.getString(1);
+ }
+ noteCursor.close();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "查询便签信息失败", Toast.LENGTH_SHORT).show();
+ }
+
+ // 查询便签具体内容,这是必须的,因为完整内容存储在Data表中
+ String[] dataProjection = {net.micode.notes.data.Notes.DataColumns.CONTENT};
+ String dataSelection = net.micode.notes.data.Notes.DataColumns.NOTE_ID + " = ? AND " +
+ net.micode.notes.data.Notes.DataColumns.MIME_TYPE + " = ?";
+ String[] dataSelectionArgs = {String.valueOf(noteId), net.micode.notes.data.Notes.DataConstants.NOTE};
+
+ android.database.Cursor dataCursor = null;
+ try {
+ // 使用ContentResolver查询数据
+ dataCursor = getContentResolver().query(
+ net.micode.notes.data.Notes.CONTENT_DATA_URI,
+ dataProjection,
+ dataSelection,
+ dataSelectionArgs,
+ null
+ );
+
+ if (dataCursor != null) {
+ if (dataCursor.moveToFirst()) {
+ mNoteContent = dataCursor.getString(0);
+ }
+ dataCursor.close();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "查询便签内容失败", Toast.LENGTH_SHORT).show();
+ } finally {
+ if (dataCursor != null) {
+ dataCursor.close();
+ }
+ }
+
+ // 如果查询失败,尝试使用数据库直接查询
+ if (mNoteContent == null || mNoteContent.isEmpty()) {
+ try {
+ // 直接使用数据库查询,绕过ContentProvider的用户过滤
+ net.micode.notes.data.NotesDatabaseHelper helper = net.micode.notes.data.NotesDatabaseHelper.getInstance(FriendNoteEditActivity.this);
+ if (helper != null) {
+ dataCursor = helper.getReadableDatabase().query(
+ net.micode.notes.data.NotesDatabaseHelper.TABLE.DATA,
+ dataProjection,
+ dataSelection,
+ dataSelectionArgs,
+ null,
+ null,
+ null
+ );
+
+ if (dataCursor != null) {
+ if (dataCursor.moveToFirst()) {
+ mNoteContent = dataCursor.getString(0);
+ }
+ dataCursor.close();
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "直接查询便签内容失败", Toast.LENGTH_SHORT).show();
+ } finally {
+ if (dataCursor != null) {
+ dataCursor.close();
+ }
+ }
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "初始化便签失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
+ finish();
+ return false;
+ }
+ return true;
+ }
+
+ private void initNoteScreen() {
+ // 显示修改日期
+ TextView modifiedDateTextView = findViewById(R.id.tv_modified_date);
+ modifiedDateTextView.setText(android.text.format.DateUtils.formatDateTime(
+ this, mModifiedDate,
+ android.text.format.DateUtils.FORMAT_SHOW_DATE |
+ android.text.format.DateUtils.FORMAT_NUMERIC_DATE |
+ android.text.format.DateUtils.FORMAT_SHOW_TIME));
+
+ // 显示便签标题
+ if (mNoteTitle != null && !mNoteTitle.isEmpty()) {
+ mNoteTitleView.setVisibility(View.VISIBLE);
+ mNoteTitleView.setText(mNoteTitle);
+ } else {
+ // 如果没有标题,隐藏标题文本视图
+ mNoteTitleView.setVisibility(View.GONE);
+ }
+
+ // 显示便签内容,添加调试日志
+ if (mNoteContent != null && !mNoteContent.isEmpty()) {
+ mNoteEditor.setText(mNoteContent);
+ // 设置文本颜色为深色,确保可见
+ mNoteEditor.setTextColor(getResources().getColor(android.R.color.primary_text_dark));
+ // 设置背景色为白色,确保可见
+ mNoteEditor.setBackgroundColor(getResources().getColor(android.R.color.white));
+ } else {
+ // 如果内容为空,显示提示文本
+ mNoteEditor.setText("(空便签)");
+ mNoteEditor.setTextColor(getResources().getColor(android.R.color.secondary_text_dark));
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // 不显示菜单,因为是只读模式
+ return false;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(android.view.MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // 返回上一级活动
+ finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/ui/FriendNoteListActivity.java b/src/Notes-master/src/net/micode/notes/ui/FriendNoteListActivity.java
new file mode 100644
index 0000000..623eda6
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/FriendNoteListActivity.java
@@ -0,0 +1,305 @@
+package net.micode.notes.ui;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import net.micode.notes.R;
+import net.micode.notes.data.Notes;
+import net.micode.notes.data.Notes.NoteColumns;
+import net.micode.notes.model.WorkingNote;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 好友便签列表活动类
+ * 显示指定好友的所有公开便签,允许用户查看好友的便签内容
+ * 作为好友功能的重要组成部分,实现了便签的社交分享功能
+ *
+ * 架构设计:
+ * - 继承自Activity,使用ListView显示便签列表
+ * - 自定义NoteAdapter适配器处理数据绑定
+ * - 通过ContentResolver查询好友的公开便签
+ * - 支持点击查看便签详情
+ * - 包含错误处理和空状态提示
+ *
+ * 核心功能:
+ * - 显示好友的公开便签列表
+ * - 按置顶状态和修改时间排序
+ * - 点击查看便签详情
+ * - 支持返回上一级活动
+ * - 显示便签的置顶、锁定和公开状态
+ */
+public class FriendNoteListActivity extends Activity {
+ /**
+ * 便签列表视图
+ */
+ private ListView mNoteListView;
+ /**
+ * 便签适配器
+ */
+ private NoteAdapter mNoteAdapter;
+ /**
+ * 便签数据列表
+ */
+ private List mNoteList;
+ /**
+ * 好友ID
+ */
+ private long mFriendId;
+ /**
+ * 好友用户名
+ */
+ private String mFriendUsername;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_friend_note_list);
+
+ // 获取好友信息
+ mFriendId = getIntent().getLongExtra("friend_id", -1);
+ mFriendUsername = getIntent().getStringExtra("friend_username");
+
+ if (mFriendId == -1 || mFriendUsername == null) {
+ Toast.makeText(this, "好友信息错误", Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+
+ // 设置ActionBar
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setTitle(mFriendUsername + "的公开便签");
+ }
+
+ // 初始化ListView
+ mNoteListView = findViewById(R.id.friend_note_list);
+ mNoteList = new ArrayList<>();
+ mNoteAdapter = new NoteAdapter(this, mNoteList);
+ mNoteListView.setAdapter(mNoteAdapter);
+
+ // 设置ListView点击事件
+ mNoteListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ Note note = mNoteList.get(position);
+ // 启动查看便签详情的活动
+ Intent intent = new Intent(FriendNoteListActivity.this, FriendNoteEditActivity.class);
+ intent.putExtra(Intent.EXTRA_UID, note.id);
+ intent.putExtra("friend_id", mFriendId);
+ startActivity(intent);
+ }
+ });
+
+ // 加载好友的公开便签
+ loadFriendNotes();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // 重新加载好友便签
+ loadFriendNotes();
+ }
+
+ /**
+ * 加载好友的公开便签
+ */
+ private void loadFriendNotes() {
+ try {
+ mNoteList.clear();
+
+ // 查询好友的所有公开便签
+ String[] projection = {
+ NoteColumns.ID,
+ NoteColumns.TITLE,
+ NoteColumns.SNIPPET,
+ NoteColumns.MODIFIED_DATE,
+ NoteColumns.TYPE,
+ NoteColumns.PINNED,
+ NoteColumns.LOCKED,
+ NoteColumns.PUBLIC
+ };
+
+ String selection = NoteColumns.USER_ID + " = ? AND " + NoteColumns.PUBLIC + " = 1 AND " + NoteColumns.PARENT_ID + " <> " + Notes.ID_TRASH_FOLER;
+ String[] selectionArgs = {String.valueOf(mFriendId)};
+
+ Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(
+ Notes.CONTENT_NOTE_URI,
+ projection,
+ selection,
+ selectionArgs,
+ NoteColumns.PINNED + " DESC, " + NoteColumns.MODIFIED_DATE + " DESC"
+ );
+
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.ID));
+ String title = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.TITLE));
+ String content = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.SNIPPET));
+ long modifiedDate = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.MODIFIED_DATE));
+ int type = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.TYPE));
+ int pinned = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.PINNED));
+ int locked = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.LOCKED));
+ int isPublic = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.PUBLIC));
+
+ mNoteList.add(new Note(id, title, content, modifiedDate, type, pinned, locked, isPublic));
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "查询好友便签失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ // 通知适配器数据变化
+ mNoteAdapter.notifyDataSetChanged();
+
+ // 如果没有公开便签,显示提示
+ if (mNoteList.isEmpty()) {
+ Toast.makeText(this, mFriendUsername + "没有公开的便签", Toast.LENGTH_SHORT).show();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "加载好友便签失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 便签实体类
+ */
+ private static class Note {
+ long id;
+ String title;
+ String content;
+ long modifiedDate;
+ int type;
+ int pinned;
+ int locked;
+ int isPublic;
+
+ Note(long id, String title, String content, long modifiedDate, int type, int pinned, int locked, int isPublic) {
+ this.id = id;
+ this.title = title;
+ this.content = content;
+ this.modifiedDate = modifiedDate;
+ this.type = type;
+ this.pinned = pinned;
+ this.locked = locked;
+ this.isPublic = isPublic;
+ }
+ }
+
+ /**
+ * 便签列表适配器
+ */
+ private static class NoteAdapter extends BaseAdapter {
+ private Activity mActivity;
+ private List mNoteList;
+ private LayoutInflater mInflater;
+
+ NoteAdapter(Activity activity, List noteList) {
+ mActivity = activity;
+ mNoteList = noteList;
+ mInflater = LayoutInflater.from(activity);
+ }
+
+ @Override
+ public int getCount() {
+ return mNoteList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mNoteList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.note_item, parent, false);
+ holder = new ViewHolder();
+ holder.titleTextView = convertView.findViewById(R.id.tv_title);
+ holder.modifiedDateTextView = convertView.findViewById(R.id.tv_time);
+ holder.pinnedImageView = convertView.findViewById(R.id.iv_alert_icon);
+ holder.lockedImageView = convertView.findViewById(R.id.iv_lock_icon);
+ holder.publicImageView = convertView.findViewById(R.id.iv_public_icon);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ Note note = mNoteList.get(position);
+ // 显示标题,如果标题为空则显示内容摘要
+ String displayText;
+ if (note.title != null && !note.title.isEmpty()) {
+ displayText = note.title;
+ } else {
+ displayText = note.content;
+ }
+ holder.titleTextView.setText(displayText);
+ holder.modifiedDateTextView.setText(android.text.format.DateUtils.formatDateTime(
+ mActivity, note.modifiedDate,
+ android.text.format.DateUtils.FORMAT_SHOW_DATE |
+ android.text.format.DateUtils.FORMAT_NUMERIC_DATE |
+ android.text.format.DateUtils.FORMAT_SHOW_TIME));
+
+ // 设置置顶图标
+ holder.pinnedImageView.setVisibility(note.pinned == 1 ? View.VISIBLE : View.GONE);
+
+ // 设置锁定图标
+ holder.lockedImageView.setVisibility(note.locked == 1 ? View.VISIBLE : View.GONE);
+
+ // 设置公开图标
+ holder.publicImageView.setVisibility(note.isPublic == 1 ? View.VISIBLE : View.GONE);
+
+ return convertView;
+ }
+
+ private static class ViewHolder {
+ TextView titleTextView;
+ TextView modifiedDateTextView;
+ ImageView pinnedImageView;
+ ImageView lockedImageView;
+ ImageView publicImageView;
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(android.view.MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // 返回上一级活动
+ finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/ui/LoginActivity.java b/src/Notes-master/src/net/micode/notes/ui/LoginActivity.java
new file mode 100644
index 0000000..dae80f6
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/LoginActivity.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.micode.notes.ui;
+
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+
+import net.micode.notes.R;
+import net.micode.notes.data.NotesDatabaseHelper;
+import net.micode.notes.data.Users;
+import net.micode.notes.tool.UserManager;
+
+/**
+ * 登录活动
+ *
+ * 该类负责用户登录、注册和密码修改功能,是用户认证的入口界面。
+ * 它通过NotesDatabaseHelper操作用户数据表,实现用户的身份验证和管理。
+ */
+public class LoginActivity extends AppCompatActivity {
+
+ private EditText etUsername;
+ private EditText etPassword;
+
+ private NotesDatabaseHelper mDbHelper;
+ private SQLiteDatabase mDb;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_login);
+
+ // 初始化数据库
+ mDbHelper = NotesDatabaseHelper.getInstance(this);
+ mDb = mDbHelper.getWritableDatabase();
+
+ // 初始化控件
+ etUsername = findViewById(R.id.et_username);
+ etPassword = findViewById(R.id.et_password);
+ }
+
+ /**
+ * 登录按钮点击事件
+ */
+ public void onLoginClick(View view) {
+ String username = etUsername.getText().toString().trim();
+ String password = etPassword.getText().toString().trim();
+
+ // 验证输入
+ if (username.isEmpty() || password.isEmpty()) {
+ Toast.makeText(this, "用户名和密码不能为空", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 验证用户名和密码
+ long userId = validateUser(username, password);
+ if (userId != -1) {
+ // 登录成功,保存用户信息
+ UserManager userManager = UserManager.getInstance(this);
+ userManager.saveCurrentUser(userId, username);
+
+ // 跳转到便签列表
+ Intent intent = new Intent(this, NotesListActivity.class);
+ startActivity(intent);
+ finish();
+ } else {
+ // 登录失败,提示错误
+ Toast.makeText(this, "用户名或密码错误", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 注册按钮点击事件
+ */
+ public void onRegisterClick(View view) {
+ showRegisterDialog();
+ }
+
+ /**
+ * 修改密码按钮点击事件
+ */
+ public void onChangePasswordClick(View view) {
+ showChangePasswordDialog();
+ }
+
+ /**
+ * 显示注册对话框
+ */
+ private void showRegisterDialog() {
+ View dialogView = getLayoutInflater().inflate(R.layout.dialog_register, null);
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setView(dialogView);
+
+ final AlertDialog dialog = builder.create();
+
+ // 初始化注册对话框控件
+ final EditText etRegisterUsername = dialogView.findViewById(R.id.et_register_username);
+ final EditText etRegisterPassword = dialogView.findViewById(R.id.et_register_password);
+ final EditText etRegisterConfirmPassword = dialogView.findViewById(R.id.et_register_confirm_password);
+ Button btnRegisterCancel = dialogView.findViewById(R.id.btn_register_cancel);
+ Button btnRegisterConfirm = dialogView.findViewById(R.id.btn_register_confirm);
+
+ // 取消按钮点击事件
+ btnRegisterCancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dialog.dismiss();
+ }
+ });
+
+ // 注册按钮点击事件
+ btnRegisterConfirm.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String username = etRegisterUsername.getText().toString().trim();
+ String password = etRegisterPassword.getText().toString().trim();
+ String confirmPassword = etRegisterConfirmPassword.getText().toString().trim();
+
+ // 验证输入
+ if (username.isEmpty() || password.isEmpty() || confirmPassword.isEmpty()) {
+ Toast.makeText(LoginActivity.this, "请填写完整信息", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (!password.equals(confirmPassword)) {
+ Toast.makeText(LoginActivity.this, "两次输入的密码不一致", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 检查用户名是否已存在
+ if (isUsernameExists(username)) {
+ Toast.makeText(LoginActivity.this, "用户名已存在", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 注册用户
+ if (registerUser(username, password)) {
+ Toast.makeText(LoginActivity.this, "注册成功", Toast.LENGTH_SHORT).show();
+ dialog.dismiss();
+ } else {
+ Toast.makeText(LoginActivity.this, "注册失败,请重试", Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ dialog.show();
+ }
+
+ /**
+ * 显示修改密码对话框
+ */
+ private void showChangePasswordDialog() {
+ View dialogView = getLayoutInflater().inflate(R.layout.dialog_change_password, null);
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setView(dialogView);
+
+ final AlertDialog dialog = builder.create();
+
+ // 初始化修改密码对话框控件
+ final EditText etChangeUsername = dialogView.findViewById(R.id.et_change_username);
+ final EditText etCurrentPassword = dialogView.findViewById(R.id.et_current_password);
+ final EditText etNewPassword = dialogView.findViewById(R.id.et_new_password);
+ final EditText etConfirmNewPassword = dialogView.findViewById(R.id.et_confirm_new_password);
+ Button btnChangeCancel = dialogView.findViewById(R.id.btn_change_cancel);
+ Button btnChangeConfirm = dialogView.findViewById(R.id.btn_change_confirm);
+
+ // 取消按钮点击事件
+ btnChangeCancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dialog.dismiss();
+ }
+ });
+
+ // 确认修改按钮点击事件
+ btnChangeConfirm.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String username = etChangeUsername.getText().toString().trim();
+ String currentPassword = etCurrentPassword.getText().toString().trim();
+ String newPassword = etNewPassword.getText().toString().trim();
+ String confirmNewPassword = etConfirmNewPassword.getText().toString().trim();
+
+ // 验证输入
+ if (username.isEmpty() || currentPassword.isEmpty() || newPassword.isEmpty() || confirmNewPassword.isEmpty()) {
+ Toast.makeText(LoginActivity.this, "请填写完整信息", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (!newPassword.equals(confirmNewPassword)) {
+ Toast.makeText(LoginActivity.this, "两次输入的新密码不一致", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 验证当前密码
+ if (validateUser(username, currentPassword) == -1) {
+ Toast.makeText(LoginActivity.this, "用户名或当前密码错误", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 修改密码
+ if (changePassword(username, newPassword)) {
+ Toast.makeText(LoginActivity.this, "密码修改成功", Toast.LENGTH_SHORT).show();
+ dialog.dismiss();
+ } else {
+ Toast.makeText(LoginActivity.this, "密码修改失败,请重试", Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ dialog.show();
+ }
+
+ /**
+ * 验证用户名和密码,并返回用户ID
+ */
+ private long validateUser(String username, String password) {
+ String[] projection = {Users.UserColumns.ID};
+ String selection = Users.UserColumns.USERNAME + " = ? AND " + Users.UserColumns.PASSWORD + " = ?";
+ String[] selectionArgs = {username, password};
+
+ Cursor cursor = mDb.query(
+ NotesDatabaseHelper.TABLE.USER,
+ projection,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ );
+
+ long userId = -1;
+ if (cursor.moveToFirst()) {
+ userId = cursor.getLong(cursor.getColumnIndexOrThrow(Users.UserColumns.ID));
+ }
+ cursor.close();
+ return userId;
+ }
+
+ /**
+ * 检查用户名是否已存在
+ */
+ private boolean isUsernameExists(String username) {
+ String[] projection = {Users.UserColumns.ID};
+ String selection = Users.UserColumns.USERNAME + " = ?";
+ String[] selectionArgs = {username};
+
+ Cursor cursor = mDb.query(
+ NotesDatabaseHelper.TABLE.USER,
+ projection,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ );
+
+ boolean exists = cursor.getCount() > 0;
+ cursor.close();
+ return exists;
+ }
+
+ /**
+ * 注册用户
+ */
+ private boolean registerUser(String username, String password) {
+ ContentValues values = new ContentValues();
+ values.put(Users.UserColumns.USERNAME, username);
+ values.put(Users.UserColumns.PASSWORD, password);
+
+ long result = mDb.insert(NotesDatabaseHelper.TABLE.USER, null, values);
+ return result != -1;
+ }
+
+ /**
+ * 修改密码
+ */
+ private boolean changePassword(String username, String newPassword) {
+ ContentValues values = new ContentValues();
+ values.put(Users.UserColumns.PASSWORD, newPassword);
+ values.put(Users.UserColumns.MODIFIED_DATE, System.currentTimeMillis());
+
+ String selection = Users.UserColumns.USERNAME + " = ?";
+ String[] selectionArgs = {username};
+
+ int result = mDb.update(
+ NotesDatabaseHelper.TABLE.USER,
+ values,
+ selection,
+ selectionArgs
+ );
+
+ return result > 0;
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 关闭数据库连接
+ if (mDb != null && mDb.isOpen()) {
+ mDb.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java b/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java
index 355f794..5fbda6e 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java
@@ -14,148 +14,90 @@
* limitations under the License.
*/
-// 包声明:归属小米便签UI模块,核心编辑页面,承载所有便签的新建/编辑/查看核心逻辑
package net.micode.notes.ui;
-// Android系统服务-闹钟:实现便签提醒功能的核心系统服务
import android.app.Activity;
import android.app.AlarmManager;
-// Android弹窗:实现删除确认、时间选择等核心弹窗交互
import android.app.AlertDialog;
-// Android延迟意图:绑定闹钟事件,触发提醒广播
import android.app.PendingIntent;
-// Android搜索服务:接收搜索跳转参数,实现搜索关键词高亮
import android.app.SearchManager;
-// Android小组件管理:实现便签与桌面小组件的绑定和更新
import android.appwidget.AppWidgetManager;
-// Android内容URI工具:拼接便签ID生成唯一URI,用于闹钟广播标识
import android.content.ContentUris;
-// Android上下文:页面运行环境核心类
import android.content.Context;
-// Android对话框监听:处理弹窗的确认/取消点击事件
import android.content.DialogInterface;
import android.content.Intent;
-// Android轻量级存储:持久化保存字体大小等用户偏好设置
import android.content.SharedPreferences;
-// Android图形画笔:实现清单模式勾选后的删除线文本样式
import android.graphics.Paint;
-// Android页面状态存储:屏幕旋转/内存不足重建时保存便签ID
import android.os.Bundle;
-// Android偏好设置工具:获取全局的SharedPreferences实例
import android.preference.PreferenceManager;
-// Android富文本核心类:实现搜索关键词的背景高亮效果
+import android.graphics.Bitmap;
+import android.net.Uri;
import android.text.Spannable;
import android.text.SpannableString;
-// Android文本工具类:判空、文本截取等安全高效操作
+import android.text.SpannableStringBuilder;
import android.text.TextUtils;
-// Android时间格式化工具:格式化便签修改时间、提醒相对时间
import android.text.format.DateUtils;
-// Android富文本样式:为搜索关键词添加背景色高亮
import android.text.style.BackgroundColorSpan;
-// Android日志工具:输出调试日志,便于问题排查
+import android.text.style.ImageSpan;
import android.util.Log;
-// Android布局加载:加载菜单布局、清单模式子项布局
import android.view.LayoutInflater;
-// Android菜单核心类:创建页面右上角的功能菜单
import android.view.Menu;
import android.view.MenuItem;
-// Android触摸事件:处理点击外部关闭弹窗的核心事件分发
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
-// Android窗口管理:控制软键盘的显示/隐藏模式
import android.view.WindowManager;
-// Android复选框:清单模式的核心选择控件
import android.widget.CheckBox;
+import android.text.style.URLSpan;
+import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
-// Android编辑框:普通文本模式的核心输入控件
import android.widget.EditText;
-// Android图片控件:背景色选择、选中态展示、功能图标
import android.widget.ImageView;
-// Android线性布局:清单模式的列表容器,承载多个编辑项
import android.widget.LinearLayout;
-// Android文本控件:展示修改时间、提醒信息等静态文本
import android.widget.TextView;
-// Android轻提示:操作成功/失败的吐司提示
import android.widget.Toast;
-// 小米便签资源类:引用字符串、颜色、布局、图片等项目资源
import net.micode.notes.R;
-// 小米便签数据常量:定义便签类型、文件夹ID、Intent参数等核心常量
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.TextNote;
-// 小米便签核心数据模型:封装便签的所有数据、状态和业务操作,解耦UI与数据层
import net.micode.notes.model.WorkingNote;
import net.micode.notes.model.WorkingNote.NoteSettingChangedListener;
-// 小米便签数据工具类:数据库操作、数据校验、通话记录匹配等通用工具
import net.micode.notes.tool.DataUtils;
-// 小米便签样式解析工具:解析背景色、字体大小等样式资源,统一管理样式规范
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.tool.ResourceParser.TextAppearanceResources;
-// 小米便签自定义时间选择器:年月日时分一体化选择弹窗
import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener;
-// 小米便签自定义编辑框回调:处理清单模式的增删、回车事件
import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener;
import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;
-
-// Java集合框架:HashMap实现键值对映射,解耦控件ID与业务标识;HashSet存储批量操作ID
+import net.micode.notes.ui.FoldersListAdapter;
+import android.view.ViewGroup;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
-// Java正则表达式:实现搜索关键词的文本匹配,支撑高亮逻辑
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-// Android异步查询:实现文件夹列表的异步加载
-import android.content.AsyncQueryHandler;
-// Android数据库游标:存储文件夹列表查询结果
-import android.database.Cursor;
-// Android列表适配器:展示文件夹列表
-import android.widget.ListAdapter;
/**
- * 小米便签 核心编辑页面【整个应用的核心页面】
- * 页面定位:Activity基类,承载便签的「新建、编辑、查看、删除」全生命周期操作
- * 核心业务能力(十大核心职责):
- * 1. 多场景页面初始化:区分「查看已有便签、新建普通便签、新建通话记录便签、搜索跳转」4种启动场景;
- * 2. 双编辑模式无缝切换:支持「普通文本模式」与「清单勾选模式」互相切换,数据无损转换;
- * 3. 样式个性化定制:支持5种背景色切换、4种字体大小选择,样式偏好持久化存储;
- * 4. 提醒功能完整实现:设置/取消便签提醒、注册系统闹钟、过期提醒展示、相对时间显示;
- * 5. 丰富的功能菜单:新建、删除、分享、创建桌面快捷方式、字体设置、模式切换、提醒管理;
- * 6. 搜索关键词高亮:从搜索列表跳转时,自动高亮匹配的搜索关键词,提升浏览体验;
- * 7. 通话记录深度适配:自动匹配通话记录生成便签,避免重复创建,一键记录通话信息;
- * 8. 桌面小组件联动:编辑便签后自动同步更新绑定的桌面小组件内容;
- * 9. 防数据丢失机制:页面暂停/销毁/返回时自动保存便签,屏幕旋转时恢复编辑状态;
- * 10. 完善的异常处理:所有关键操作均做数据校验,异常场景友好提示,杜绝崩溃闪退。
- * 核心设计模式:
- * - 数据与UI解耦:通过WorkingNote数据模型封装所有数据操作,页面仅做UI展示和事件分发;
- * - 回调解耦业务逻辑:通过多个回调接口处理子控件事件、数据状态变化,符合单一职责原则;
- * - 常量映射解耦:通过HashMap映射控件ID与业务标识,避免硬编码,提升代码可维护性。
+ * 便签编辑活动
+ *
+ * 该类是便签编辑的核心界面,负责处理便签的创建、编辑、保存等操作。
+ * 它实现了多个接口,用于处理点击事件、便签设置变更和文本视图变更。
*/
public class NoteEditActivity extends Activity implements OnClickListener,
NoteSettingChangedListener, OnTextViewChangeListener {
- /**
- * 标题栏控件持有者 内部类【控件缓存设计】
- * 设计目的:将标题栏的所有核心控件统一缓存到该类,避免多次调用findViewById造成性能损耗
- * 核心价值:控件只查找一次,复用全局,提升页面初始化和刷新的效率
- */
private class HeadViewHolder {
- public TextView tvModified; // 展示便签的「最后修改时间」文本控件
- public ImageView ivAlertIcon; // 提醒功能的标识图标,有提醒时显示,无则隐藏
- public TextView tvAlertDate; // 展示提醒「剩余时间/已过期」文本,联动提醒图标
- public ImageView ibSetBgColor; // 背景色设置的功能按钮,点击弹出背景色选择面板
- }
+ public TextView tvModified;
- // ===================== 静态常量映射表区 - 核心解耦设计【重中之重】=====================
- // 映射原则:所有映射表均为静态不可变,静态代码块初始化,全局唯一,避免内存浪费
- // 核心价值:彻底解耦「控件ID」与「业务常量标识」,修改控件ID或业务标识时互不影响,提升可维护性
+ public ImageView ivAlertIcon;
+
+ public TextView tvAlertDate;
+
+ public ImageView ibSetBgColor;
+ }
- /**
- * 背景色选择按钮ID → ResourceParser背景色常量 映射表
- * 作用:点击不同背景色按钮时,快速匹配对应的背景色业务标识,无需多个if判断
- */
private static final Map sBgSelectorBtnsMap = new HashMap();
static {
sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW);
@@ -165,10 +107,6 @@ public class NoteEditActivity extends Activity implements OnClickListener,
sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE);
}
- /**
- * ResourceParser背景色常量 → 背景色选中态控件ID 映射表
- * 作用:选中某背景色后,快速显示对应的选中标识,未选中则隐藏,控制选择器的视觉反馈
- */
private static final Map sBgSelectorSelectionMap = new HashMap();
static {
sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select);
@@ -178,10 +116,6 @@ public class NoteEditActivity extends Activity implements OnClickListener,
sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select);
}
- /**
- * 字体大小选择按钮ID → ResourceParser字体常量 映射表
- * 作用:点击不同字体按钮时,快速匹配对应的字体大小业务标识,统一管理字体样式
- */
private static final Map sFontSizeBtnsMap = new HashMap();
static {
sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE);
@@ -190,10 +124,6 @@ public class NoteEditActivity extends Activity implements OnClickListener,
sFontSizeBtnsMap.put(R.id.ll_font_super, ResourceParser.TEXT_SUPER);
}
- /**
- * ResourceParser字体常量 → 字体选中态控件ID 映射表
- * 作用:选中某字体后,快速显示对应的选中标识,控制字体选择器的视觉反馈
- */
private static final Map sFontSelectorSelectionMap = new HashMap();
static {
sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select);
@@ -202,57 +132,69 @@ public class NoteEditActivity extends Activity implements OnClickListener,
sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select);
}
- // ===================== 全局常量区 - 日志/存储/业务规则 =====================
- private static final String TAG = "NoteEditActivity"; // 日志过滤TAG,固定值
- private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; // 字体大小偏好存储Key,持久化保存
- private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; // 桌面快捷方式标题最大长度,避免标题过长
- public static final String TAG_CHECKED = String.valueOf('\u221A'); // 清单模式-已勾选标记符 ✔
- public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); // 清单模式-未勾选标记符 □
-
- // ===================== 核心成员变量区 - 页面所有核心数据/控件/状态 =====================
- private HeadViewHolder mNoteHeaderHolder; // 标题栏控件持有者实例,缓存标题栏所有控件
- private View mHeadViewPanel; // 标题栏根布局,用于整体设置背景色
- private View mNoteBgColorSelector; // 背景色选择器面板,弹窗式展示,点击外部关闭
- private View mFontSizeSelector; // 字体大小选择器面板,弹窗式展示,点击外部关闭
- private EditText mNoteEditor; // 普通文本模式的核心编辑框,单行/多行输入均可
- private View mNoteEditorPanel; // 编辑区域根布局,承载普通编辑框/清单列表,用于设置整体背景色
- private WorkingNote mWorkingNote; // 核心数据模型【页面的核心】,所有便签数据/操作均通过该类完成
- private SharedPreferences mSharedPrefs; // 应用全局偏好设置,持久化存储用户的字体大小选择
- private int mFontSizeId; // 当前选中的字体大小业务标识,关联ResourceParser常量
- private LinearLayout mEditTextList; // 清单模式的核心列表容器,承载所有带复选框的编辑项
- private String mUserQuery; // 搜索关键词,从搜索列表跳转时传入,用于文本高亮
- private Pattern mPattern; // 搜索关键词的正则匹配器,编译一次复用多次,提升匹配效率
- private BackgroundQueryHandler mBackgroundQueryHandler; // 异步查询处理器,用于加载文件夹列表
- private static final int FOLDER_LIST_QUERY_TOKEN = 1; // 文件夹列表查询的token标识
-
- // ===================== Activity 生命周期核心方法 =====================
- /**
- * 页面创建入口:初始化布局、读取启动参数、初始化页面核心状态
- * 生命周期:页面第一次创建时调用,只执行一次
- * @param savedInstanceState 页面重建时的状态缓存,屏幕旋转/内存不足时保存数据
- */
+ private static final String TAG = "NoteEditActivity";
+
+ private HeadViewHolder mNoteHeaderHolder;
+
+ private View mHeadViewPanel;
+
+ private View mNoteBgColorSelector;
+
+ private View mFontSizeSelector;
+
+ private NoteEditText mNoteEditor;
+ private NoteEditText mNoteTitleEditor; // 标题编辑器(支持富文本)
+
+ private View mNoteEditorPanel;
+ private View mCharacterCountLayout; // 字数统计布局
+
+ private WorkingNote mWorkingNote;
+
+ private SharedPreferences mSharedPrefs;
+ private int mFontSizeId;
+
+ private TextView mCharacterCountView; // 字数统计显示
+
+ private static final String PREFERENCE_FONT_SIZE = "pref_font_size";
+
+ private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10;
+
+ public static final String TAG_CHECKED = String.valueOf('\u221A');
+ public static final String TAG_UNCHECKED = String.valueOf('\u25A1');
+
+ private LinearLayout mEditTextList;
+
+ private String mUserQuery;
+ private Pattern mPattern;
+
+ // 撤回功能相关
+ private java.util.Stack mUndoStack; // 用于保存撤回历史记录(支持富文本)
+ private boolean mIsUndoing; // 标记是否正在执行撤回操作,避免递归调用
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- setContentView(R.layout.note_edit); // 加载核心编辑页面布局
+ this.setContentView(R.layout.note_edit);
- // 核心初始化逻辑:无重建状态时,通过启动Intent初始化页面;初始化失败则直接关闭页面
if (savedInstanceState == null && !initActivityState(getIntent())) {
finish();
return;
}
- initResources(); // 初始化控件、监听器、偏好设置等所有页面资源
+ initResources();
+
+ // Check if we need to show change background dialog directly
+ if (getIntent().getBooleanExtra("CHANGE_BACKGROUND", false)) {
+ changeBackground();
+ }
}
/**
- * 页面状态恢复:Activity因内存不足被销毁后重建时,恢复之前的编辑状态
- * 生命周期:页面重建时调用,仅在savedInstanceState不为空时执行
- * @param savedInstanceState 保存的页面状态,核心存储便签ID
+ * Current activity may be killed when the memory is low. Once it is killed, for another time
+ * user load this activity, we should restore the former state
*/
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
- // 从缓存中读取便签ID,重新构建Intent并初始化页面状态
if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID));
@@ -264,26 +206,24 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
}
- /**
- * 核心初始化方法:根据启动Intent的Action,区分不同的业务场景,初始化页面核心状态
- * 四大核心场景全覆盖,是页面的入口逻辑,所有数据初始化均从此处开始
- * @param intent 启动当前页面的Intent对象,携带所有启动参数
- * @return true-初始化成功,false-初始化失败(如便签不存在、参数异常)
- */
private boolean initActivityState(Intent intent) {
- mWorkingNote = null; // 初始化数据模型为空,避免脏数据
- // 场景一:ACTION_VIEW → 查看/编辑【已有便签】(从便签列表点击进入)
+ /**
+ * If the user specified the {@link Intent#ACTION_VIEW} but not provided with id,
+ * then jump to the NotesListActivity
+ */
+ mWorkingNote = null;
if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) {
- long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); // 获取要查看的便签ID
- mUserQuery = ""; // 默认无搜索关键词
+ long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0);
+ mUserQuery = "";
- // 子场景:从【搜索结果列表】跳转 → 携带搜索关键词,需要文本高亮
+ /**
+ * Starting from the searched result
+ */
if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) {
noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY));
mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY);
}
- // 数据校验:校验便签是否存在于数据库且为有效便签类型,不存在则跳转列表并提示
if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) {
Intent jump = new Intent(this, NotesListActivity.class);
startActivity(jump);
@@ -291,28 +231,27 @@ public class NoteEditActivity extends Activity implements OnClickListener,
finish();
return false;
} else {
- // 加载便签数据:通过WorkingNote从数据库加载便签的所有信息
mWorkingNote = WorkingNote.load(this, noteId);
- if (mWorkingNote == null) { // 加载失败则关闭页面,避免空指针
+ if (mWorkingNote == null) {
Log.e(TAG, "load note failed with note id" + noteId);
finish();
return false;
}
}
- // 查看模式软键盘策略:隐藏软键盘,布局自适应,提升浏览体验
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
| WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
- }
- // 场景二:ACTION_INSERT_OR_EDIT → 【新建便签】(包含普通新建/通话记录新建)
- else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) {
- // 解析新建便签的核心参数:文件夹ID、小组件ID、小组件类型、默认背景色
+ } else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) {
+ // New note
long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0);
- int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
- int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, Notes.TYPE_WIDGET_INVALIDE);
- int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, ResourceParser.getDefaultBgId(this));
-
- // 子场景一:从【通话记录】跳转 → 新建/编辑通话记录便签,避免重复创建
+ int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID);
+ int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE,
+ Notes.TYPE_WIDGET_INVALIDE);
+ int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID,
+ ResourceParser.getDefaultBgId(this));
+
+ // Parse call-record note
String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0);
if (callDate != 0 && phoneNumber != null) {
@@ -320,8 +259,8 @@ public class NoteEditActivity extends Activity implements OnClickListener,
Log.w(TAG, "The call record number is null");
}
long noteId = 0;
- // 先查询是否已有该通话记录的便签,有则加载,无则新建
- if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(), phoneNumber, callDate)) > 0) {
+ if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(),
+ phoneNumber, callDate)) > 0) {
mWorkingNote = WorkingNote.load(this, noteId);
if (mWorkingNote == null) {
Log.e(TAG, "load call note failed with note id" + noteId);
@@ -329,239 +268,292 @@ public class NoteEditActivity extends Activity implements OnClickListener,
return false;
}
} else {
- // 创建空便签并转换为通话记录专属便签,自动填充手机号和通话时间
- mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, bgResId);
+ mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId,
+ widgetType, bgResId);
mWorkingNote.convertToCallNote(phoneNumber, callDate);
}
} else {
- // 子场景二:普通新建便签 → 创建空白便签,使用默认参数
- mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, bgResId);
+ mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType,
+ bgResId);
}
- // 新建模式软键盘策略:自动弹出软键盘,布局自适应,提升输入体验
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
} else {
- // 异常场景:Intent无合法Action,直接关闭页面,避免未知错误
Log.e(TAG, "Intent not specified action, should not support");
finish();
return false;
}
- // 注册数据模型监听器:监听便签的背景色、提醒、模式、小组件等状态变化,实时更新UI
mWorkingNote.setOnSettingStatusChangedListener(this);
return true;
}
- /**
- * 页面恢复可见:页面从后台切回前台、锁屏解锁后调用
- * 生命周期:每次页面恢复可见时调用,可执行多次
- */
@Override
protected void onResume() {
super.onResume();
- initNoteScreen(); // 恢复/初始化便签的展示样式、模式、高亮等UI状态
+ initNoteScreen();
}
- /**
- * 初始化便签展示界面:页面核心UI渲染方法,适配所有样式和模式
- * 核心逻辑:根据WorkingNote的状态,应用字体样式、切换编辑模式、设置背景色、展示时间和提醒信息
- */
private void initNoteScreen() {
- // 第一步:为普通编辑框应用当前选中的字体大小样式
- mNoteEditor.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
+ mNoteEditor.setTextAppearance(this, TextAppearanceResources
+ .getTexAppearanceResource(mFontSizeId));
+ mNoteTitleEditor.setTextAppearance(this, TextAppearanceResources
+ .getTexAppearanceResource(mFontSizeId));
+ // 设置 MovementMethod,确保 ImageSpan 能正确显示
+ mNoteEditor.setMovementMethod(android.text.method.LinkMovementMethod.getInstance());
+
+ // 设置标题
+ mNoteTitleEditor.setText(mWorkingNote.getTitle());
- // 第二步:根据便签模式,切换对应的编辑布局
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
- switchToListMode(mWorkingNote.getContent()); // 清单模式:加载清单列表
+ switchToListMode(mWorkingNote.getContent());
} else {
- // 普通模式:设置文本并高亮搜索关键词,光标定位到文本末尾,提升编辑体验
mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
mNoteEditor.setSelection(mNoteEditor.getText().length());
}
-
- // 第三步:初始化背景色选择器的选中态,默认全部隐藏,只显示当前选中的背景色标识
+
for (Integer id : sBgSelectorSelectionMap.keySet()) {
findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE);
}
-
- // 第四步:为标题栏和编辑区域应用当前选中的背景色样式
+
+ // 初始化字数统计显示
+ updateCharacterCount();
+
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
- mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
+
+ // 检查是否有保存的背景图片
+ SharedPreferences sharedPreferences = getSharedPreferences("NoteSettings", Context.MODE_PRIVATE);
+ String backgroundImagePath = sharedPreferences.getString("background_image", null);
+ if (backgroundImagePath != null) {
+ // 加载保存的背景图片
+ try {
+ android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeFile(backgroundImagePath);
+ if (bitmap != null) {
+ android.graphics.drawable.BitmapDrawable drawable = new android.graphics.drawable.BitmapDrawable(getResources(), bitmap);
+ mNoteEditorPanel.setBackground(drawable);
+ } else {
+ // 如果图片加载失败,使用默认背景
+ mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error loading saved background image: " + e.getMessage());
+ mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
+ }
+ } else {
+ // 使用默认背景
+ mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
+ }
+
+ // 设置字数统计布局的背景颜色与便签一致
+ mCharacterCountLayout.setBackgroundResource(mWorkingNote.getBgColorResId());
- // 第五步:格式化展示便签的最后修改时间,包含年月日时分,样式统一
mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this,
mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME
| DateUtils.FORMAT_SHOW_YEAR));
- // 第六步:展示提醒信息,区分过期/未过期状态,更新对应的文本和图标
+ /**
+ * TODO: Add the menu for setting alert. Currently disable it because the DateTimePicker
+ * is not ready
+ */
showAlertHeader();
}
- /**
- * 更新提醒信息展示:根据便签的提醒状态,动态显示/隐藏提醒相关控件
- * 核心逻辑:有提醒则显示图标+时间/过期提示,无提醒则隐藏所有相关控件,界面整洁
- */
private void showAlertHeader() {
if (mWorkingNote.hasClockAlert()) {
long time = System.currentTimeMillis();
- // 提醒已过期:显示「已过期」文本,提示用户及时处理
if (time > mWorkingNote.getAlertDate()) {
mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired);
} else {
- // 提醒未过期:显示相对时间(如「10分钟后」「1小时后」),更符合用户阅读习惯
mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString(
mWorkingNote.getAlertDate(), time, DateUtils.MINUTE_IN_MILLIS));
}
mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE);
mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE);
} else {
- // 无提醒:隐藏所有提醒相关控件,节省界面空间
mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE);
mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE);
};
}
- /**
- * 处理新的启动意图:页面已存在时,接收到新的Intent调用该方法(如桌面快捷方式再次打开)
- * 核心逻辑:复用当前页面,重新初始化状态,避免创建多个页面实例
- * @param intent 新的启动意图
- */
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
- initActivityState(intent); // 重新初始化页面状态,适配新的Intent参数
+ initActivityState(intent);
}
- /**
- * 页面状态保存:页面即将被销毁时调用,保存核心状态数据,用于重建时恢复
- * 生命周期:屏幕旋转、内存不足、页面退到后台时调用
- * @param outState 状态缓存容器,存储需要恢复的数据
- */
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
- // 关键逻辑:未保存到数据库的新便签,先执行保存生成ID,避免数据丢失
+ /**
+ * For new note without note id, we should firstly save it to
+ * generate a id. If the editing note is not worth saving, there
+ * is no id which is equivalent to create new note
+ */
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
- // 将核心的便签ID保存到缓存,页面重建时通过该ID恢复数据
outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId());
Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState");
}
- /**
- * 触摸事件分发:页面的核心触摸事件处理入口,优先级高于所有子控件的触摸事件
- * 核心业务:实现「点击外部关闭弹窗」的交互逻辑,处理背景色/字体选择器的关闭
- * @param ev 触摸事件对象,包含触摸坐标、动作类型等信息
- * @return true-事件已消费,false-事件继续分发
- */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
- // 背景色选择器显示时,点击外部区域则关闭面板,消费事件避免穿透
if (mNoteBgColorSelector.getVisibility() == View.VISIBLE
&& !inRangeOfView(mNoteBgColorSelector, ev)) {
mNoteBgColorSelector.setVisibility(View.GONE);
return true;
}
- // 字体大小选择器显示时,点击外部区域则关闭面板,消费事件避免穿透
if (mFontSizeSelector.getVisibility() == View.VISIBLE
&& !inRangeOfView(mFontSizeSelector, ev)) {
mFontSizeSelector.setVisibility(View.GONE);
return true;
}
- return super.dispatchTouchEvent(ev); // 未消费则交给父类处理,不影响其他触摸逻辑
+ return super.dispatchTouchEvent(ev);
}
- /**
- * 坐标范围校验工具:判断触摸坐标是否在指定View的矩形范围内
- * 核心作用:支撑「点击外部关闭弹窗」的逻辑,精准判断触摸位置
- * @param view 目标校验的View
- * @param ev 触摸事件对象
- * @return true-触摸在View范围内,false-触摸在View范围外
- */
private boolean inRangeOfView(View view, MotionEvent ev) {
int []location = new int[2];
- view.getLocationOnScreen(location); // 获取View在屏幕上的绝对坐标
+ view.getLocationOnScreen(location);
int x = location[0];
int y = location[1];
- // 校验触摸坐标是否超出View的上下左右边界
- if (ev.getX() < x || ev.getX() > (x + view.getWidth()) || ev.getY() < y || ev.getY() > (y + view.getHeight())) {
+ if (ev.getX() < x
+ || ev.getX() > (x + view.getWidth())
+ || ev.getY() < y
+ || ev.getY() > (y + view.getHeight())) {
return false;
}
return true;
}
- /**
- * 页面资源初始化:绑定所有UI控件、设置点击监听器、读取用户偏好设置
- * 核心逻辑:所有控件只查找一次,监听器一次性绑定,偏好设置一次性读取,提升页面性能
- */
private void initResources() {
- // 第一步:绑定标题栏根布局和控件持有者,缓存标题栏所有控件
mHeadViewPanel = findViewById(R.id.note_title);
mNoteHeaderHolder = new HeadViewHolder();
mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date);
mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon);
mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date);
mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color);
- mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); // 绑定背景色按钮点击监听
-
- // 初始化异步查询处理器,用于加载文件夹列表
- mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver());
-
- // 第二步:绑定编辑区域核心控件
- mNoteEditor = (EditText) findViewById(R.id.note_edit_view);
+ mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this);
+ mNoteEditor = (NoteEditText) findViewById(R.id.note_edit_view);
+ mNoteTitleEditor = (NoteEditText) findViewById(R.id.note_title_edit); // 初始化标题编辑器(支持富文本)
+ // 显式设置输入法类型,确保支持中文输入
+ mNoteEditor.setInputType(android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE |
+ android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES |
+ android.text.InputType.TYPE_TEXT_FLAG_AUTO_CORRECT |
+ android.text.InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
+ // 为标题编辑器设置输入类型
+ mNoteTitleEditor.setInputType(android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES |
+ android.text.InputType.TYPE_TEXT_FLAG_AUTO_CORRECT |
+ android.text.InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
mNoteEditorPanel = findViewById(R.id.sv_note_edit);
mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector);
-
- // 第三步:为所有背景色选择按钮绑定点击监听器,统一处理背景色切换逻辑
for (int id : sBgSelectorBtnsMap.keySet()) {
ImageView iv = (ImageView) findViewById(id);
iv.setOnClickListener(this);
}
- // 第四步:绑定字体大小选择器,并为所有字体按钮绑定点击监听器
mFontSizeSelector = findViewById(R.id.font_size_selector);
for (int id : sFontSizeBtnsMap.keySet()) {
View view = findViewById(id);
view.setOnClickListener(this);
};
-
- // 第五步:读取用户的字体大小偏好设置,持久化存储,兼容异常值(重置为默认)
mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE);
+ /**
+ * HACKME: Fix bug of store the resource id in shared preference.
+ * The id may larger than the length of resources, in this case,
+ * return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE}
+ */
if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) {
mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE;
}
-
- // 第六步:绑定清单模式的核心列表容器
mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list);
+
+ // 初始化字数统计显示
+ mCharacterCountLayout = findViewById(R.id.ll_character_count);
+ mCharacterCountView = (TextView) findViewById(R.id.tv_character_count);
+
+ // 初始化撤回栈,保存完整的SpannableStringBuilder,包括富文本格式
+ mUndoStack = new java.util.Stack();
+ mIsUndoing = false;
+
+ // 创建一个通用的文本变化监听器,用于处理标题和内容编辑器
+ final android.text.TextWatcher textWatcher = new android.text.TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ if (!mIsUndoing) {
+ // 获取当前焦点的编辑器
+ View focusedView = getCurrentFocus();
+ if (focusedView == null) return;
+
+ // 保存完整的便签内容,包括标题和正文(支持富文本)
+ android.text.SpannableStringBuilder fullContent = new android.text.SpannableStringBuilder();
+
+ // 先保存标题
+ android.text.SpannableStringBuilder titleContent = new android.text.SpannableStringBuilder(mNoteTitleEditor.getText());
+ fullContent.append(titleContent);
+ // 添加分隔符,用于区分标题和正文
+ fullContent.append("\n---\n");
+ // 再保存正文
+ android.text.SpannableStringBuilder noteContent = new android.text.SpannableStringBuilder(mNoteEditor.getText());
+ fullContent.append(noteContent);
+
+ // 检查是否与栈顶内容相同,避免重复保存
+ if (!mUndoStack.isEmpty()) {
+ String topContent = mUndoStack.peek().toString();
+ if (topContent.equals(fullContent.toString())) {
+ return;
+ }
+ }
+
+ // 保存到撤回栈
+ mUndoStack.push(fullContent);
+
+ // 限制撤回栈的大小
+ if (mUndoStack.size() > 20) {
+ mUndoStack.remove(0);
+ }
+ }
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+ @Override
+ public void afterTextChanged(android.text.Editable s) {
+ // 更新字数统计
+ updateCharacterCount();
+
+ // 实时保存标题
+ mWorkingNote.setWorkingTitle(mNoteTitleEditor.getText().toString());
+ }
+ };
+
+ // 为内容编辑器添加监听器
+ mNoteEditor.addTextChangedListener(textWatcher);
+
+ // 为标题编辑器添加监听器
+ mNoteTitleEditor.addTextChangedListener(textWatcher);
+
+ // 为NoteEditText添加MovementMethod,确保链接可以被点击
+ mNoteEditor.setMovementMethod(android.text.method.LinkMovementMethod.getInstance());
+
+
}
- /**
- * 页面暂停:页面退到后台、锁屏、启动新页面时调用
- * 生命周期:每次页面失去焦点时调用,可执行多次
- * 核心逻辑:自动保存便签数据,避免数据丢失;清理弹窗状态,保证界面整洁
- */
@Override
protected void onPause() {
super.onPause();
- // 自动保存便签:页面暂停时必保存,所有编辑操作不会丢失,核心防丢机制
if(saveNote()) {
Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length());
}
- clearSettingState(); // 关闭所有弹窗面板,避免重建时残留显示
+ clearSettingState();
}
- /**
- * 更新桌面小组件:当便签关联了桌面小组件时,编辑后同步更新小组件内容
- * 核心逻辑:发送广播通知对应的小组件刷新,保证便签与小组件数据一致
- */
private void updateWidget() {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
- // 根据便签绑定的小组件类型,选择对应的小组件广播接收者
if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) {
intent.setClass(this, NoteWidgetProvider_2x.class);
} else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) {
@@ -571,68 +563,51 @@ public class NoteEditActivity extends Activity implements OnClickListener,
return;
}
- // 传入小组件ID,发送精准的更新广播
- intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { mWorkingNote.getWidgetId() });
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {
+ mWorkingNote.getWidgetId()
+ });
+
sendBroadcast(intent);
- setResult(RESULT_OK, intent); // 设置返回结果,告知调用方更新成功
+ setResult(RESULT_OK, intent);
}
- /**
- * 点击事件统一处理:所有控件的点击事件均在此处分发,统一管理,避免分散处理
- * 核心处理:背景色选择、字体大小选择、背景色面板打开,所有点击逻辑集中管理
- * @param v 被点击的View控件
- */
public void onClick(View v) {
int id = v.getId();
- // 点击背景色按钮:打开背景色选择面板,显示当前选中的背景色标识
if (id == R.id.btn_set_bg_color) {
mNoteBgColorSelector.setVisibility(View.VISIBLE);
- findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(View.VISIBLE);
- }
- // 点击背景色选择按钮:切换便签背景色,隐藏选择面板,触发背景色变化回调
- else if (sBgSelectorBtnsMap.containsKey(id)) {
- findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(View.GONE);
+ findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
+ - View.VISIBLE);
+ } else if (sBgSelectorBtnsMap.containsKey(id)) {
+ findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
+ View.GONE);
mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id));
mNoteBgColorSelector.setVisibility(View.GONE);
- }
- // 点击字体大小选择按钮:持久化保存字体选择、更新UI样式、隐藏选择面板
- else if (sFontSizeBtnsMap.containsKey(id)) {
+ } else if (sFontSizeBtnsMap.containsKey(id)) {
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE);
mFontSizeId = sFontSizeBtnsMap.get(id);
- // 持久化存储用户的字体选择,下次打开页面自动应用
mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit();
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE);
-
- // 根据当前编辑模式,分别更新字体样式,保证样式统一
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
getWorkingText();
switchToListMode(mWorkingNote.getContent());
} else {
- mNoteEditor.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
+ mNoteEditor.setTextAppearance(this,
+ TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
}
mFontSizeSelector.setVisibility(View.GONE);
}
}
- /**
- * 返回键事件处理:重写系统返回键,增加弹窗关闭逻辑,提升用户体验
- * 设计意图:用户点击返回键时,优先关闭打开的弹窗面板,而非直接退出页面,避免误操作
- */
@Override
public void onBackPressed() {
- // 若有弹窗面板打开,则先关闭面板,不执行返回逻辑
if(clearSettingState()) {
return;
}
- saveNote(); // 保存当前编辑内容,避免数据丢失
- super.onBackPressed(); // 执行系统默认的返回逻辑,退出当前页面
+ saveNote();
+ super.onBackPressed();
}
- /**
- * 清理弹窗面板状态:关闭背景色/字体大小选择器,统一的弹窗关闭方法
- * @return true-关闭了某一个面板,false-无面板需要关闭
- */
private boolean clearSettingState() {
if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) {
mNoteBgColorSelector.setVisibility(View.GONE);
@@ -644,112 +619,45 @@ public class NoteEditActivity extends Activity implements OnClickListener,
return false;
}
- // ===================== WorkingNote 状态变化回调接口实现 =====================
- // 该接口由WorkingNote定义,当便签的背景色、提醒、模式、小组件等状态变化时,自动回调以下方法
- // 核心价值:解耦数据模型与UI层,数据变化自动更新UI,无需手动调用,符合观察者模式
-
- /**
- * 背景色变化回调:便签背景色修改后,自动更新页面的背景色样式和选择器选中态
- */
public void onBackgroundColorChanged() {
- findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(View.VISIBLE);
+ findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
+ View.VISIBLE);
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
+ // 设置字数统计布局的背景颜色与便签一致
+ mCharacterCountLayout.setBackgroundResource(mWorkingNote.getBgColorResId());
}
- /**
- * 提醒时间变化回调:便签的提醒时间设置/取消后,注册/取消系统闹钟,更新提醒UI展示
- * @param date 新的提醒时间戳,0表示取消提醒
- * @param set true-设置提醒,false-取消提醒
- */
- public void onClockAlertChanged(long date, boolean set) {
- // 关键逻辑:未保存的新便签,设置提醒前必须先保存,否则无唯一ID绑定闹钟
- if (!mWorkingNote.existInDatabase()) {
- saveNote();
- }
-
- // 仅处理有效便签ID的提醒设置,避免无效操作
- if (mWorkingNote.getNoteId() > 0) {
- // 构建提醒广播Intent,通过URI绑定唯一的便签ID,确保闹钟触发时能找到对应的便签
- Intent intent = new Intent(this, AlarmReceiver.class);
- intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId()));
- PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
- AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE));
-
- showAlertHeader(); // 更新提醒信息的UI展示
-
- // 取消提醒:移除已注册的闹钟,清理提醒状态
- if(!set) {
- alarmManager.cancel(pendingIntent);
- } else {
- // 设置提醒:注册系统闹钟,RTC_WAKEUP模式保证即使设备休眠也能触发提醒
- alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent);
- }
- } else {
- // 异常提示:便签无内容未保存,无法设置提醒,引导用户输入内容
- Log.e(TAG, "Clock alert setting error");
- showToast(R.string.error_note_empty_for_clock);
- }
- }
-
- /**
- * 小组件关联变化回调:便签绑定的小组件信息变更后,自动更新桌面小组件内容
- */
- public void onWidgetChanged() {
- updateWidget();
- }
-
- /**
- * 编辑模式变化回调:便签在普通/清单模式间切换时,更新对应的编辑布局和数据
- * @param oldMode 切换前的模式,0=普通模式,MODE_CHECK_LIST=清单模式
- * @param newMode 切换后的模式
- */
- public void onCheckListModeChanged(int oldMode, int newMode) {
- if (newMode == TextNote.MODE_CHECK_LIST) {
- switchToListMode(mNoteEditor.getText().toString()); // 切换到清单模式
- } else {
- // 切换到普通模式:先获取清单模式的所有内容,合并为文本后展示
- if (!getWorkingText()) {
- mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", ""));
- }
- mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
- mEditTextList.setVisibility(View.GONE);
- mNoteEditor.setVisibility(View.VISIBLE);
- }
- }
-
- // ===================== 选项菜单核心逻辑 =====================
- /**
- * 准备选项菜单:页面右上角的功能菜单,动态加载菜单项,适配不同的便签状态
- * 核心逻辑:根据便签类型(普通/通话记录)、模式(清单/普通)、提醒状态,动态显示/隐藏菜单项
- * @param menu 菜单容器,用于加载和展示菜单项
- * @return true-菜单初始化成功,false-初始化失败
- */
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
- if (isFinishing()) { // 页面正在关闭时,无需初始化菜单
+ Log.d(TAG, "onPrepareOptionsMenu called");
+ if (isFinishing()) {
return true;
}
- clearSettingState(); // 关闭所有弹窗面板,避免菜单与面板叠加显示
- menu.clear(); // 清空原有菜单,避免重复加载
+ clearSettingState();
+ menu.clear();
- // 根据便签所属文件夹,加载对应的菜单布局:通话记录便签有专属菜单
if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) {
+ Log.d(TAG, "Inflating call_note_edit menu");
getMenuInflater().inflate(R.menu.call_note_edit, menu);
} else {
+ Log.d(TAG, "Inflating note_edit menu");
getMenuInflater().inflate(R.menu.note_edit, menu);
- // 动态添加"移动到文件夹"菜单项
- menu.add(Menu.NONE, R.id.menu_move_to_folder, Menu.CATEGORY_CONTAINER, R.string.menu_move);
}
- // 动态更新模式切换菜单的标题:清单模式显示「普通模式」,普通模式显示「清单模式」
+ // 检查menu_change_background菜单项是否存在
+ MenuItem changeBackgroundItem = menu.findItem(R.id.menu_change_background);
+ if (changeBackgroundItem != null) {
+ Log.d(TAG, "menu_change_background item found in menu");
+ } else {
+ Log.d(TAG, "menu_change_background item NOT found in menu");
+ }
+
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode);
} else {
menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode);
}
-
- // 动态控制提醒菜单的显隐:有提醒则隐藏「设置提醒」,无提醒则隐藏「取消提醒」
if (mWorkingNote.hasClockAlert()) {
menu.findItem(R.id.menu_alert).setVisible(false);
} else {
@@ -758,58 +666,186 @@ public class NoteEditActivity extends Activity implements OnClickListener,
return true;
}
- /**
- * 选项菜单点击事件处理:所有菜单项的核心业务逻辑入口,页面的核心功能集合
- * 包含:新建便签、删除便签、字体设置、模式切换、分享、桌面快捷方式、提醒设置/取消
- * @param item 被点击的菜单项
- * @return true-事件已处理,false-事件未处理
- */
@Override
public boolean onOptionsItemSelected(MenuItem item) {
+ Log.d(TAG, "onOptionsItemSelected called, item id: " + item.getItemId());
switch (item.getItemId()) {
case R.id.menu_new_note:
- createNewNote(); // 新建便签:保存当前便签后,启动新的编辑页面
+ createNewNote();
+ break;
+ case R.id.menu_undo:
+ // 实现撤回功能,支持标题和正文的富文本格式
+ if (!mUndoStack.isEmpty()) {
+ try {
+ mIsUndoing = true;
+ android.text.SpannableStringBuilder previousFullContent = mUndoStack.pop();
+
+ // 解析保存的完整内容,分离标题和正文
+ String fullContentStr = previousFullContent.toString();
+ String titleContentStr = fullContentStr;
+ String noteContentStr = "";
+
+ // 查找分隔符
+ int separatorIndex = fullContentStr.indexOf("\n---\n");
+ if (separatorIndex != -1) {
+ titleContentStr = fullContentStr.substring(0, separatorIndex);
+ noteContentStr = fullContentStr.substring(separatorIndex + 5); // 跳过分隔符
+ }
+
+ // 恢复标题(带富文本格式)
+ android.text.SpannableStringBuilder titleContent = new android.text.SpannableStringBuilder(previousFullContent);
+ titleContent = new android.text.SpannableStringBuilder(titleContent.subSequence(0, titleContentStr.length()));
+ mNoteTitleEditor.setText(titleContent, android.widget.TextView.BufferType.SPANNABLE);
+
+ // 恢复正文(带富文本格式)
+ android.text.SpannableStringBuilder noteContent = new android.text.SpannableStringBuilder(previousFullContent);
+ if (separatorIndex != -1) {
+ noteContent = new android.text.SpannableStringBuilder(noteContent.subSequence(separatorIndex + 5, previousFullContent.length()));
+ }
+ mNoteEditor.setText(noteContent, android.widget.TextView.BufferType.SPANNABLE);
+
+ // 更新工作便签和字数统计
+ mWorkingNote.setWorkingTitle(titleContentStr);
+ mWorkingNote.setWorkingText(noteContentStr);
+ updateCharacterCount();
+ } finally {
+ mIsUndoing = false;
+ }
+ }
break;
case R.id.menu_delete:
- // 删除便签:弹出确认弹窗,用户确认后执行删除逻辑,避免误删
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.alert_title_delete));
builder.setIcon(android.R.drawable.ic_dialog_alert);
- builder.setMessage(getString(R.string.alert_message_delete_note));
- builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- deleteCurrentNote();
- finish();
- }
- });
+ builder.setMessage("请选择删除方式");
+ builder.setPositiveButton("移动到回收站",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ deleteCurrentNote();
+ finish();
+ }
+ });
+ builder.setNeutralButton("直接删除",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ // 直接彻底删除
+ if (mWorkingNote.existInDatabase()) {
+ HashSet ids = new HashSet();
+ long id = mWorkingNote.getNoteId();
+ if (id != Notes.ID_ROOT_FOLDER) {
+ ids.add(id);
+ } else {
+ Log.d(TAG, "Wrong note id, should not happen");
+ }
+ DataUtils.batchDeleteNotes(getContentResolver(), ids);
+ }
+ mWorkingNote.markDeleted(true);
+ finish();
+ }
+ });
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
break;
case R.id.menu_font_size:
- // 打开字体大小选择面板,显示当前选中的字体标识
mFontSizeSelector.setVisibility(View.VISIBLE);
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE);
break;
case R.id.menu_list_mode:
- // 切换便签编辑模式:普通 ↔ 清单,数据自动转换,无损切换
- mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ? TextNote.MODE_CHECK_LIST : 0);
+ mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ?
+ TextNote.MODE_CHECK_LIST : 0);
break;
case R.id.menu_share:
- // 分享便签:获取最新编辑内容,调用系统分享接口,支持所有分享渠道
getWorkingText();
sendTo(this, mWorkingNote.getContent());
break;
case R.id.menu_send_to_desktop:
- sendToDesktop(); // 创建桌面快捷方式:一键生成便签的桌面图标,快速访问
+ sendToDesktop();
break;
case R.id.menu_alert:
- setReminder(); // 设置提醒:弹出时间选择弹窗,选择后绑定闹钟
+ setReminder();
break;
case R.id.menu_delete_remind:
- mWorkingNote.setAlertDate(0, false); // 取消提醒:清空便签的提醒时间
+ mWorkingNote.setAlertDate(0, false);
+ break;
+ case R.id.menu_add_picture:
+ addPicture();
+ break;
+ case R.id.menu_change_background:
+ changeBackground();
+ break;
+ case R.id.menu_format_bold:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置加粗
+ mNoteEditor.setBold();
+ break;
+ case R.id.menu_format_italic:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置斜体
+ mNoteEditor.setItalic();
+ break;
+ case R.id.menu_format_underline:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 切换下划线
+ mNoteEditor.toggleUnderline();
+ break;
+ case R.id.menu_format_strikethrough:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 切换删除线
+ mNoteEditor.toggleStrikethrough();
+ break;
+ case R.id.menu_format_text_color:
+ // 设置文字颜色(内部已保存状态)
+ showTextColorPicker();
+ break;
+ case R.id.menu_format_bg_color:
+ // 设置文字背景颜色(内部已保存状态)
+ showTextBackgroundColorPicker();
+ break;
+ case R.id.menu_format_align_left:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置左对齐
+ mNoteEditor.setAlignLeft();
+ break;
+ case R.id.menu_format_align_center:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置居中对齐
+ mNoteEditor.setAlignCenter();
+ break;
+ case R.id.menu_format_align_right:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置右对齐
+ mNoteEditor.setAlignRight();
+ break;
+ case R.id.menu_format_align_justify:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置两端对齐
+ mNoteEditor.setAlignJustify();
break;
- case R.id.menu_move_to_folder:
- startQueryDestinationFolders(); // 查询文件夹列表,让用户选择要移动到的文件夹
+ case R.id.menu_format_font_large:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置大字号
+ mNoteEditor.setTextSize(1.2f);
+ break;
+ case R.id.menu_format_font_normal:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置正常字号
+ mNoteEditor.setTextSize(1.0f);
+ break;
+ case R.id.menu_format_font_small:
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 设置小字号
+ mNoteEditor.setTextSize(0.8f);
break;
default:
break;
@@ -817,23 +853,106 @@ public class NoteEditActivity extends Activity implements OnClickListener,
return true;
}
- /**
- * 设置便签提醒:弹出自定义的时间选择弹窗,支持年月日时分的精准选择
- */
private void setReminder() {
DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis());
d.setOnDateTimeSetListener(new OnDateTimeSetListener() {
public void OnDateTimeSet(AlertDialog dialog, long date) {
- mWorkingNote.setAlertDate(date, true);
+ mWorkingNote.setAlertDate(date , true);
}
});
d.show();
}
+
+ /**
+ * 保存当前便签状态到撤回栈(支持富文本样式修改)
+ */
+ private void saveCurrentStateToUndoStack() {
+ if (mIsUndoing) return;
+
+ // 保存完整的便签内容,包括标题和正文(支持富文本)
+ android.text.SpannableStringBuilder fullContent = new android.text.SpannableStringBuilder();
+
+ // 先保存标题
+ android.text.SpannableStringBuilder titleContent = new android.text.SpannableStringBuilder(mNoteTitleEditor.getText());
+ fullContent.append(titleContent);
+ // 添加分隔符,用于区分标题和正文
+ fullContent.append("\n---\n");
+ // 再保存正文
+ android.text.SpannableStringBuilder noteContent = new android.text.SpannableStringBuilder(mNoteEditor.getText());
+ fullContent.append(noteContent);
+
+ // 检查是否与栈顶内容相同,避免重复保存
+ if (!mUndoStack.isEmpty()) {
+ String topContent = mUndoStack.peek().toString();
+ if (topContent.equals(fullContent.toString())) {
+ return;
+ }
+ }
+
+ // 保存到撤回栈
+ mUndoStack.push(fullContent);
+
+ // 限制撤回栈的大小
+ if (mUndoStack.size() > 20) {
+ mUndoStack.remove(0);
+ }
+ }
+
+ /**
+ * 显示文字颜色选择器
+ */
+ private void showTextColorPicker() {
+ // 创建颜色选择器对话框
+ final int[] colors = {android.graphics.Color.RED, android.graphics.Color.BLUE,
+ android.graphics.Color.GREEN, android.graphics.Color.BLACK,
+ android.graphics.Color.GRAY, android.graphics.Color.YELLOW,
+ android.graphics.Color.MAGENTA, android.graphics.Color.CYAN};
+ final String[] colorNames = {"红色", "蓝色", "绿色", "黑色",
+ "灰色", "黄色", "紫色", "青色"};
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("选择文字颜色");
+ builder.setItems(colorNames, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 应用颜色
+ mNoteEditor.setTextColor(colors[which]);
+ }
+ });
+ builder.show();
+ }
+
+ /**
+ * 显示文字背景颜色选择器
+ */
+ private void showTextBackgroundColorPicker() {
+ // 创建背景颜色选择器对话框
+ final int[] colors = {android.graphics.Color.RED, android.graphics.Color.BLUE,
+ android.graphics.Color.GREEN, android.graphics.Color.YELLOW,
+ android.graphics.Color.MAGENTA, android.graphics.Color.CYAN,
+ android.graphics.Color.WHITE, android.graphics.Color.GRAY};
+ final String[] colorNames = {"红色", "蓝色", "绿色", "黄色",
+ "紫色", "青色", "白色", "灰色"};
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("选择文字背景颜色");
+ builder.setItems(colorNames, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // 保存当前状态到撤回栈
+ saveCurrentStateToUndoStack();
+ // 应用背景颜色
+ mNoteEditor.setTextBackgroundColor(colors[which]);
+ }
+ });
+ builder.show();
+ }
/**
- * 系统分享功能:调用Android原生的分享接口,分享便签的纯文本内容
- * @param context 上下文对象
- * @param info 要分享的便签文本内容
+ * Share note to apps that support {@link Intent#ACTION_SEND} action
+ * and {@text/plain} type
*/
private void sendTo(Context context, String info) {
Intent intent = new Intent(Intent.ACTION_SEND);
@@ -842,239 +961,250 @@ public class NoteEditActivity extends Activity implements OnClickListener,
context.startActivity(intent);
}
- /**
- * 新建便签:保存当前便签后,启动新的编辑页面,继承当前便签的文件夹属性
- * 设计意图:用户新建便签时,保留当前的编辑上下文,提升操作效率
- */
private void createNewNote() {
- saveNote(); // 先保存当前编辑的便签,避免数据丢失
- finish(); // 关闭当前页面,避免页面栈过深
+ // Firstly, save current editing notes
+ saveNote();
+
+ // For safety, start a new NoteEditActivity
+ finish();
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId());
startActivity(intent);
}
- /**
- * 删除当前便签:核心删除逻辑,区分同步模式和非同步模式,执行不同的删除策略
- * 同步模式:有绑定同步账号,便签移至回收站,可恢复;非同步模式:直接从数据库删除,不可恢复
- */
- private void deleteCurrentNote() {
- if (mWorkingNote.existInDatabase()) { // 仅处理已保存到数据库的便签
+ private void deleteCurrentNote() {
+ if (mWorkingNote.existInDatabase()) {
HashSet ids = new HashSet();
long id = mWorkingNote.getNoteId();
- if (id != Notes.ID_ROOT_FOLDER) { // 校验便签ID有效性,避免误删根文件夹
+ if (id != Notes.ID_ROOT_FOLDER) {
ids.add(id);
} else {
Log.d(TAG, "Wrong note id, should not happen");
}
-
- // 非同步模式:直接批量删除便签
- if (!isSyncMode()) {
- if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) {
- Log.e(TAG, "Delete Note error");
- }
- } else {
- // 同步模式:将便签移至回收站文件夹,而非直接删除,支持恢复
- if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) {
- Log.e(TAG, "Move notes to trash folder error, should not happens");
- }
+ // 无论是否同步,都将便签移动到回收站
+ if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) {
+ Log.e(TAG, "Move notes to trash folder error, should not happens");
}
}
- mWorkingNote.markDeleted(true); // 标记便签为已删除,更新数据模型状态
- }
+ mWorkingNote.markDeleted(true);
+ }
- /**
- * 判断是否为同步模式:检测用户是否配置了同步账号,决定删除策略
- * @return true-已配置同步账号(同步模式),false-未配置(非同步模式)
- */
private boolean isSyncMode() {
return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0;
}
-
- /**
- * 异步查询处理器:用于加载文件夹列表
- */
- private final class BackgroundQueryHandler extends AsyncQueryHandler {
- public BackgroundQueryHandler(ContentResolver cr) {
- super(cr);
+
+ public void onClockAlertChanged(long date, boolean set) {
+ /**
+ * User could set clock to an unsaved note, so before setting the
+ * alert clock, we should save the note first
+ */
+ if (!mWorkingNote.existInDatabase()) {
+ saveNote();
}
-
- @Override
- protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
- if (token == FOLDER_LIST_QUERY_TOKEN) {
- showFolderListMenu(cursor);
+ if (mWorkingNote.getNoteId() > 0) {
+ Intent intent = new Intent(this, AlarmReceiver.class);
+ intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId()));
+ // 使用适当的PendingIntent flag,确保在Android 12+中正常工作
+ int flags = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S ? PendingIntent.FLAG_IMMUTABLE : 0;
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, flags);
+ AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE));
+ showAlertHeader();
+ if(!set) {
+ alarmManager.cancel(pendingIntent);
+ // 提示用户提醒已取消
+ showToast(R.string.note_alert_canceled);
+ } else {
+ // 在Android 6.0+中,使用setExact方法以确保准确的提醒时间
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+ alarmManager.setExact(AlarmManager.RTC_WAKEUP, date, pendingIntent);
+ } else {
+ alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent);
+ }
+ // 提示用户提醒设置成功
+ showToast(R.string.note_alert_set_success);
}
+ } else {
+ /**
+ * There is the condition that user has input nothing (the note is
+ * not worthy saving), we have no note id, remind the user that he
+ * should input something
+ */
+ Log.e(TAG, "Clock alert setting error");
+ showToast(R.string.error_note_empty_for_clock);
}
}
-
- /**
- * 查询文件夹列表:异步加载所有可用的文件夹
- */
- private void startQueryDestinationFolders() {
- mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, null,
- Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION,
- Notes.NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER, null,
- Notes.NoteColumns.SNIPPET + " COLLATE LOCALIZED ASC");
- }
-
- /**
- * 显示文件夹选择菜单:让用户选择要移动到的文件夹
- * @param cursor 文件夹列表的查询结果
- */
- private void showFolderListMenu(Cursor cursor) {
- AlertDialog.Builder builder = new AlertDialog.Builder(NoteEditActivity.this);
- builder.setTitle(R.string.menu_title_select_folder);
- final FoldersListAdapter adapter = new FoldersListAdapter(NoteEditActivity.this, cursor);
- builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- // 保存当前编辑的内容
- getWorkingText();
- // 更新便签的文件夹ID
- mWorkingNote.setFolderId(adapter.getItemId(which));
- // 保存便签
- saveNote();
- // 提示用户操作成功
- Toast.makeText(NoteEditActivity.this,
- getString(R.string.format_move_notes_to_folder, 1,
- adapter.getFolderName(NoteEditActivity.this, which)),
- Toast.LENGTH_SHORT).show();
- }
- });
- builder.show();
- }
- // ===================== NoteEditText 回调接口实现(清单模式核心) =====================
- // 该接口由自定义NoteEditText定义,处理清单模式下的「删除项、回车新增项」核心交互
- // 是清单模式能够正常增删、换行的核心支撑,所有清单的动态操作均通过该接口回调实现
+ public void onWidgetChanged() {
+ updateWidget();
+ }
- /**
- * 清单模式-删除项回调:删除指定索引的清单项,合并文本到前一项,调整后续项的索引
- * @param index 要删除的清单项索引
- * @param text 被删除项的文本内容,需要合并到前一项
- */
public void onEditTextDelete(int index, String text) {
int childCount = mEditTextList.getChildCount();
- if (childCount == 1) { // 仅保留最后一项时,禁止删除,避免清单为空
+ if (childCount == 1) {
return;
}
- // 调整删除项之后的所有项索引,保证索引连续
for (int i = index + 1; i < childCount; i++) {
- ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)).setIndex(i - 1);
+ ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text))
+ .setIndex(i - 1);
}
- mEditTextList.removeViewAt(index); // 移除指定索引的清单项
-
- // 获取合并目标项,将被删除的文本合并到目标项中
+ mEditTextList.removeViewAt(index);
NoteEditText edit = null;
if(index == 0) {
- edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById(R.id.et_edit_text);
+ edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById(
+ R.id.et_edit_text);
} else {
- edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById(R.id.et_edit_text);
+ edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById(
+ R.id.et_edit_text);
}
-
int length = edit.length();
edit.append(text);
edit.requestFocus();
- edit.setSelection(length); // 光标定位到合并后的文本末尾,提升编辑体验
+ edit.setSelection(length);
}
- /**
- * 清单模式-回车回调:在指定索引位置新增清单项,拆分文本,调整后续项的索引
- * @param index 触发回车的清单项索引
- * @param text 原项中回车后的文本内容,拆分到新项中
- */
public void onEditTextEnter(int index, String text) {
- if(index > mEditTextList.getChildCount()) { // 异常校验,避免索引越界
+ /**
+ * Should not happen, check for debug
+ */
+ if(index > mEditTextList.getChildCount()) {
Log.e(TAG, "Index out of mEditTextList boundrary, should not happen");
}
- View view = getListItem(text, index); // 创建新的清单项View
- mEditTextList.addView(view, index); // 插入到指定索引位置
-
- // 新项获取焦点,光标定位到开头,便于用户继续输入
+ View view = getListItem(text, index);
+ mEditTextList.addView(view, index);
NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
edit.requestFocus();
edit.setSelection(0);
-
- // 调整新项之后的所有项索引,保证索引连续
for (int i = index + 1; i < mEditTextList.getChildCount(); i++) {
- ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)).setIndex(i);
+ ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text))
+ .setIndex(i);
}
}
- /**
- * 切换到清单模式:将普通文本按换行拆分,生成对应的带复选框的清单项列表
- * @param text 普通模式的文本内容,按换行符拆分为多个清单项
- */
private void switchToListMode(String text) {
- mEditTextList.removeAllViews(); // 清空原有列表,避免重复加载
- String[] items = text.split("\n"); // 按换行拆分文本
+ mEditTextList.removeAllViews();
+ String[] items = text.split("\n");
int index = 0;
- // 遍历拆分后的文本,创建并添加清单项,仅处理非空文本
for (String item : items) {
if(!TextUtils.isEmpty(item)) {
mEditTextList.addView(getListItem(item, index));
index++;
}
}
- mEditTextList.addView(getListItem("", index)); // 添加空项,便于用户继续输入
- mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus(); // 空项获取焦点
+ mEditTextList.addView(getListItem("", index));
+ mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus();
- // 切换UI显示:隐藏普通编辑框,显示清单列表
mNoteEditor.setVisibility(View.GONE);
mEditTextList.setVisibility(View.VISIBLE);
}
- /**
- * 搜索关键词高亮:为匹配的搜索关键词添加背景色高亮效果,提升搜索体验
- * 核心实现:使用Spannable富文本,为匹配的文本段添加BackgroundColorSpan样式
- * @param fullText 便签的完整文本内容
- * @param userQuery 搜索关键词,为空则直接返回原文本
- * @return 带高亮效果的富文本,无匹配则返回原文本
- */
private Spannable getHighlightQueryResult(String fullText, String userQuery) {
- SpannableString spannable = new SpannableString(fullText == null ? "" : fullText);
+ if (TextUtils.isEmpty(fullText)) {
+ return new SpannableString("");
+ }
+
+ // 创建一个 SpannableStringBuilder 用于构建最终的 Spannable
+ SpannableStringBuilder builder = new SpannableStringBuilder(fullText);
+
+ // 正则表达式匹配 [IMAGE]imageUri[/IMAGE] 格式的图片标记
+ Pattern imagePattern = Pattern.compile("\\[IMAGE\\](.*?)\\[/IMAGE\\]");
+ Matcher imageMatcher = imagePattern.matcher(fullText);
+
+ // 倒序处理,避免替换后影响后续匹配位置
+ ArrayList imageInfos = new ArrayList<>();
+ while (imageMatcher.find()) {
+ String imageUriStr = imageMatcher.group(1);
+ imageInfos.add(new ImageInfo(imageMatcher.start(), imageMatcher.end(), imageUriStr));
+ }
+
+ // 倒序处理图片标记
+ for (int i = imageInfos.size() - 1; i >= 0; i--) {
+ ImageInfo info = imageInfos.get(i);
+ try {
+ // 获取图片URI
+ Uri imageUri = Uri.parse(info.uri);
+
+ // 获取压缩后的 bitmap
+ Bitmap bitmap = getCompressedBitmap(imageUri);
+ if (bitmap != null) {
+ // 计算合适的图片大小(相对于文本大小)
+ float scale = getResources().getDisplayMetrics().density;
+
+ // 使用屏幕宽度,更可靠
+ int screenWidth = getResources().getDisplayMetrics().widthPixels;
+ int maxImageWidth = (int) (screenWidth * 0.8); // 80% of screen width
+ int maxImageHeight = (int) (500 * scale); // 500dp max height
+
+ // 调整图片大小
+ Bitmap scaledBitmap = getScaledBitmap(bitmap, maxImageWidth, maxImageHeight);
+
+ // 创建 ImageSpan(使用兼容旧版本的构造方式)
+ ImageSpan imageSpan = new ImageSpan(this, scaledBitmap);
+
+ // 不替换图片标记,直接在图片标记上应用 ImageSpan
+ // 这样可以保持文本内容不变,避免 getWorkingText 方法中的问题
+ builder.setSpan(imageSpan, info.start, info.end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ // 回收临时 bitmap
+ if (scaledBitmap != bitmap) {
+ bitmap.recycle();
+ }
+ }
+ // 如果图片加载失败,保持原始标记不变
+ } catch (Exception e) {
+ // 发生异常时,保持原始标记不变
+ e.printStackTrace();
+ }
+ }
+
+ // 处理搜索高亮
if (!TextUtils.isEmpty(userQuery)) {
- mPattern = Pattern.compile(userQuery); // 编译正则表达式,提升匹配效率
- Matcher m = mPattern.matcher(fullText);
+ mPattern = Pattern.compile(userQuery);
+ Matcher m = mPattern.matcher(builder);
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(); // 更新起始位置,避免重复匹配同一文本
+ builder.setSpan(
+ new BackgroundColorSpan(this.getResources().getColor(
+ R.color.user_query_highlight)), m.start(), m.end(),
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ start = m.end();
}
}
- return spannable;
+
+ return builder;
+ }
+
+ // 用于存储图片信息的内部类
+ private static class ImageInfo {
+ int start;
+ int end;
+ String uri;
+
+ ImageInfo(int start, int end, String uri) {
+ this.start = start;
+ this.end = end;
+ this.uri = uri;
+ }
}
- /**
- * 创建清单模式的子项View:加载布局、绑定控件、设置样式、处理勾选状态和文本高亮
- * 清单模式的核心子项构建方法,每个清单项均通过该方法创建
- * @param item 清单项的文本内容
- * @param index 清单项的索引,用于绑定回调和定位
- * @return 组装完成的清单项View
- */
private View getListItem(String item, int index) {
- View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null); // 加载子项布局
+ View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null);
final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
- edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); // 应用字体样式
-
- // 绑定复选框,设置勾选状态变化监听,控制删除线样式
+ edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item));
cb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
- edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); // 勾选:添加删除线
+ edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
} else {
- edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); // 取消勾选:移除删除线
+ edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
}
}
});
- // 处理文本中的勾选标记符,初始化复选框状态和文本内容
if (item.startsWith(TAG_CHECKED)) {
cb.setChecked(true);
edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
@@ -1085,20 +1215,29 @@ public class NoteEditActivity extends Activity implements OnClickListener,
item = item.substring(TAG_UNCHECKED.length(), item.length()).trim();
}
- // 绑定回调和索引,设置文本并高亮搜索关键词
edit.setOnTextViewChangeListener(this);
edit.setIndex(index);
edit.setText(getHighlightQueryResult(item, mUserQuery));
+
+ // 为清单模式的编辑框添加文本变化监听器,用于更新字数统计
+ edit.addTextChangedListener(new android.text.TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+ @Override
+ public void afterTextChanged(android.text.Editable s) {
+ updateCharacterCount();
+ }
+ });
+
return view;
}
- /**
- * 清单项文本变化回调:根据文本是否为空,控制复选框的显隐,保证界面整洁
- * @param index 清单项的索引
- * @param hasText 文本是否非空,true-显示复选框,false-隐藏复选框
- */
public void onTextChange(int index, boolean hasText) {
- if (index >= mEditTextList.getChildCount()) { // 异常校验,避免索引越界
+ if (index >= mEditTextList.getChildCount()) {
Log.e(TAG, "Wrong index, should not happen");
return;
}
@@ -1109,16 +1248,26 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
}
- /**
- * 获取当前编辑的文本内容:根据当前的编辑模式,读取对应的文本并更新到WorkingNote
- * 核心数据同步方法,保存便签、切换模式、分享等操作前均需调用该方法
- * @return true-清单模式下存在已勾选的项,false-无勾选/普通模式
- */
+ public void onCheckListModeChanged(int oldMode, int newMode) {
+ if (newMode == TextNote.MODE_CHECK_LIST) {
+ switchToListMode(mNoteEditor.getText().toString());
+ } else {
+ if (!getWorkingText()) {
+ mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ",
+ ""));
+ }
+ mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
+ mEditTextList.setVisibility(View.GONE);
+ mNoteEditor.setVisibility(View.VISIBLE);
+ }
+ // 模式切换后更新字数统计
+ updateCharacterCount();
+ }
+
private boolean getWorkingText() {
boolean hasChecked = false;
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
StringBuilder sb = new StringBuilder();
- // 遍历所有清单项,拼接带标记符的文本,已勾选加✔,未勾选加□
for (int i = 0; i < mEditTextList.getChildCount(); i++) {
View view = mEditTextList.getChildAt(i);
NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
@@ -1131,87 +1280,477 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
}
}
- mWorkingNote.setWorkingText(sb.toString()); // 更新数据模型的内容
+ mWorkingNote.setWorkingText(sb.toString());
+ // 确保便签模式被正确设置
+ mWorkingNote.setCheckListMode(TextNote.MODE_CHECK_LIST);
} else {
- mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); // 普通模式直接读取编辑框文本
+ // 对于普通文本模式,获取编辑器中的文本内容
+ mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
+ // 确保便签模式被正确设置
+ mWorkingNote.setCheckListMode(0); // 0 表示普通文本模式
}
return hasChecked;
}
- /**
- * 保存便签核心方法:页面的核心数据持久化入口,所有保存操作均通过该方法完成
- * 核心逻辑:先同步编辑内容到WorkingNote,再调用WorkingNote的saveNote方法持久化到数据库
- * @return true-保存成功,false-保存失败
- */
private boolean saveNote() {
- getWorkingText(); // 同步当前编辑的内容到数据模型
- boolean saved = mWorkingNote.saveNote(); // 持久化到数据库
-
+ getWorkingText();
+ boolean saved = mWorkingNote.saveNote();
if (saved) {
- setResult(RESULT_OK); // 设置返回结果,告知调用方数据已更新,需要刷新列表
+ /**
+ * There are two modes from List view to edit view, open one note,
+ * create/edit a node. Opening node requires to the original
+ * position in the list when back from edit view, while creating a
+ * new node requires to the top of the list. This code
+ * {@link #RESULT_OK} is used to identify the create/edit state
+ */
+ setResult(RESULT_OK);
}
return saved;
}
-
+
/**
- * 创建桌面快捷方式:生成指向当前便签的桌面图标,点击可直接打开该便签
- * 核心逻辑:发送系统广播通知桌面启动器创建快捷方式,绑定便签的唯一ID
+ * 更新字数统计显示
*/
+ private void updateCharacterCount() {
+ int count = 0;
+ if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
+ // 清单模式:遍历所有编辑框计算总字数
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < mEditTextList.getChildCount(); i++) {
+ View view = mEditTextList.getChildAt(i);
+ NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
+ if (!TextUtils.isEmpty(edit.getText())) {
+ sb.append(edit.getText()).append("\n");
+ }
+ }
+ count = sb.length();
+ } else {
+ // 普通文本模式:直接获取编辑器内容长度
+ count = mNoteEditor.getText().length();
+ }
+
+ // 更新显示
+ mCharacterCountView.setText(getString(R.string.character_count, count));
+ }
+
private void sendToDesktop() {
- // 关键逻辑:未保存的新便签,创建快捷方式前必须先保存,否则无唯一ID
+ /**
+ * Before send message to home, we should make sure that current
+ * editing note is exists in databases. So, for new note, firstly
+ * save it
+ */
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
if (mWorkingNote.getNoteId() > 0) {
- // 构建快捷方式的Intent,指向当前便签的查看页面
Intent sender = new Intent();
Intent shortcutIntent = new Intent(this, NoteEditActivity.class);
shortcutIntent.setAction(Intent.ACTION_VIEW);
shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId());
-
- // 设置快捷方式的核心参数:意图、标题、图标、允许重复创建
sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
- sender.putExtra(Intent.EXTRA_SHORTCUT_NAME, makeShortcutIconTitle(mWorkingNote.getContent()));
- sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app));
+ sender.putExtra(Intent.EXTRA_SHORTCUT_NAME,
+ makeShortcutIconTitle(mWorkingNote.getContent()));
+ sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
+ Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app));
sender.putExtra("duplicate", true);
sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
-
- showToast(R.string.info_note_enter_desktop); // 提示创建成功
- sendBroadcast(sender); // 发送广播,通知桌面创建快捷方式
+ showToast(R.string.info_note_enter_desktop);
+ sendBroadcast(sender);
} else {
- // 异常提示:便签无内容未保存,无法创建快捷方式
+ /**
+ * There is the condition that user has input nothing (the note is
+ * not worthy saving), we have no note id, remind the user that he
+ * should input something
+ */
Log.e(TAG, "Send to desktop error");
showToast(R.string.error_note_empty_for_send_to_desktop);
}
}
- /**
- * 生成桌面快捷方式标题:清理清单标记符,截断超长文本,保证标题简洁美观
- * @param content 便签的原始内容
- * @return 处理后的快捷方式标题,最长10个字符
- */
private String makeShortcutIconTitle(String content) {
content = content.replace(TAG_CHECKED, "");
content = content.replace(TAG_UNCHECKED, "");
- return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0, SHORTCUT_ICON_TITLE_MAX_LEN) : content;
+ return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0,
+ SHORTCUT_ICON_TITLE_MAX_LEN) : content;
}
- // ===================== 工具方法封装 - 简化重复调用 =====================
- /**
- * 显示短时长Toast提示:封装系统Toast,简化调用,统一提示样式
- * @param resId 提示文本的资源ID
- */
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();
}
-}
\ No newline at end of file
+
+ private static final int REQUEST_CODE_PICK_IMAGE = 100;
+ private static final int REQUEST_CODE_CAMERA = 101;
+ private static final int REQUEST_CODE_PICK_BACKGROUND = 102;
+ private static final int REQUEST_CODE_TAKE_BACKGROUND = 103;
+ private static final int MAX_IMAGE_SIZE = 1024; // 限制图片大小为1024x1024
+
+ private void addPicture() {
+ // 显示选择对话框:从相册导入或相机拍摄
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.menu_add_picture);
+ String[] options = {"从相册导入", "相机拍摄"};
+ builder.setItems(options, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == 0) {
+ // 从相册导入
+ Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ intent.setType("image/*");
+ startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE);
+ } else {
+ // 相机拍摄
+ Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
+ // 使用简单的方式启动相机,让系统处理图片存储
+ startActivityForResult(intent, REQUEST_CODE_CAMERA);
+ }
+ }
+ });
+ builder.show();
+ }
+
+ /**
+ * 更换背景方法
+ */
+ private void changeBackground() {
+ Log.d(TAG, "changeBackground method called");
+
+ // 先显示一个Toast,确认方法被调用
+ Toast.makeText(this, "正在打开更换背景选项", Toast.LENGTH_SHORT).show();
+
+ // 直接使用runOnUiThread确保在主线程执行
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Log.d(TAG, "Creating AlertDialog.Builder");
+
+ // 使用默认主题,避免样式问题
+ AlertDialog.Builder builder = new AlertDialog.Builder(NoteEditActivity.this);
+ builder.setTitle("更换背景");
+ builder.setIcon(android.R.drawable.ic_dialog_info); // 添加图标,让对话框更明显
+ builder.setCancelable(true);
+
+ String[] options = {"从相册选择", "拍照"};
+ builder.setItems(options, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Log.d(TAG, "Dialog item clicked: " + which);
+ dialog.dismiss();
+
+ if (which == 0) {
+ // 从相册选择
+ Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ intent.setType("image/*");
+ startActivityForResult(intent, REQUEST_CODE_PICK_BACKGROUND);
+ } else {
+ // 拍照
+ Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
+ // 使用简单的方式启动相机,让系统处理图片存储
+ startActivityForResult(intent, REQUEST_CODE_TAKE_BACKGROUND);
+ }
+ }
+ });
+
+ // 添加取消按钮
+ builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+
+ Log.d(TAG, "Creating dialog");
+ AlertDialog dialog = builder.create();
+
+ // 设置对话框属性,确保显示在最上层
+ android.view.Window window = dialog.getWindow();
+ if (window != null) {
+ // 移除TYPE_APPLICATION_OVERLAY设置,因为在Android 8.0+中需要特殊权限
+ // 设置窗口属性,确保不会被遮挡
+ window.setFlags(android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
+ android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
+ window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ // 设置对话框背景为白色,确保可见
+ window.setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(android.graphics.Color.WHITE));
+ }
+
+ // 设置对话框可取消
+ dialog.setCanceledOnTouchOutside(true);
+ dialog.setCancelable(true);
+
+ Log.d(TAG, "Showing dialog");
+ // 确保对话框能够显示
+ dialog.show();
+ Log.d(TAG, "Dialog shown successfully");
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error in changeBackground: " + e.getMessage(), e);
+ // 显示错误信息
+ Toast.makeText(NoteEditActivity.this, "更换背景失败: " + e.getMessage(), Toast.LENGTH_LONG).show();
+ }
+ }
+ });
+ }
+
+
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK) {
+ android.net.Uri imageUri = null;
+
+ // 处理从相册导入图片
+ if (requestCode == REQUEST_CODE_PICK_IMAGE) {
+ if (data != null && data.getData() != null) {
+ imageUri = data.getData();
+ }
+ // 处理图片URI(添加图片到便签)
+ if (imageUri != null) {
+ handleAddPicture(imageUri);
+ }
+ }
+ // 处理相机拍摄图片
+ else if (requestCode == REQUEST_CODE_CAMERA) {
+ // 相机拍摄的图片URI处理
+ if (data != null && data.getData() != null) {
+ // 有些相机应用会直接返回URI
+ imageUri = data.getData();
+ } else {
+ // 从媒体库获取最新拍摄的图片
+ try {
+ android.database.Cursor cursor = getContentResolver().query(
+ android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ new String[]{android.provider.MediaStore.Images.Media._ID},
+ null, null, "DATE_ADDED DESC");
+ if (cursor != null && cursor.moveToFirst()) {
+ long id = cursor.getLong(cursor.getColumnIndex(android.provider.MediaStore.Images.Media._ID));
+ imageUri = android.content.ContentUris.withAppendedId(
+ android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
+ cursor.close();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error getting camera image: " + e.getMessage());
+ }
+ }
+ // 处理图片URI(添加图片到便签)
+ if (imageUri != null) {
+ handleAddPicture(imageUri);
+ }
+ }
+ // 处理从相册选择背景图片
+ else if (requestCode == REQUEST_CODE_PICK_BACKGROUND) {
+ if (data != null && data.getData() != null) {
+ imageUri = data.getData();
+ // 设置背景图片
+ setBackgroundImage(imageUri);
+ }
+ }
+ // 处理拍照更换背景
+ else if (requestCode == REQUEST_CODE_TAKE_BACKGROUND) {
+ // 相机拍摄的图片URI处理
+ if (data != null && data.getData() != null) {
+ // 有些相机应用会直接返回URI
+ imageUri = data.getData();
+ } else {
+ // 从媒体库获取最新拍摄的图片
+ try {
+ android.database.Cursor cursor = getContentResolver().query(
+ android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ new String[]{android.provider.MediaStore.Images.Media._ID},
+ null, null, "DATE_ADDED DESC");
+ if (cursor != null && cursor.moveToFirst()) {
+ long id = cursor.getLong(cursor.getColumnIndex(android.provider.MediaStore.Images.Media._ID));
+ imageUri = android.content.ContentUris.withAppendedId(
+ android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
+ cursor.close();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error getting camera image: " + e.getMessage());
+ }
+ }
+ // 设置背景图片
+ if (imageUri != null) {
+ setBackgroundImage(imageUri);
+ }
+ }
+ }
+ }
+
+ /**
+ * 处理添加图片到便签的逻辑
+ */
+ private void handleAddPicture(android.net.Uri imageUri) {
+ try {
+ // 构建图片标记
+ String imageTag = "\n[IMAGE]" + imageUri.toString() + "[/IMAGE]\n";
+
+ // 直接使用 mWorkingNote.getContent() 获取当前内容,它包含完整的图片标记
+ String currentContent = mWorkingNote.getContent();
+
+ // 构建新内容
+ String newContent;
+ if (currentContent.isEmpty()) {
+ // 空便签情况:直接添加图片标记,不需要前面的换行符
+ newContent = "[IMAGE]" + imageUri.toString() + "[/IMAGE]\n";
+ } else {
+ // 非空便签情况:在末尾添加图片标记
+ newContent = currentContent + imageTag;
+ }
+
+ // 更新便签内容
+ mWorkingNote.setWorkingText(newContent);
+
+ // 刷新编辑器显示
+ if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
+ switchToListMode(mWorkingNote.getContent());
+ } else {
+ mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
+ mNoteEditor.setSelection(mNoteEditor.getText().length());
+ }
+
+ // 保存到数据库
+ saveNote();
+
+ // 使用正确的Toast提示
+ Toast.makeText(NoteEditActivity.this, "图片已添加到便签", Toast.LENGTH_SHORT).show();
+ } catch (Exception e) {
+ Log.e(TAG, "Error handling image: " + e.getMessage());
+ showToast(R.string.error_sdcard_export, Toast.LENGTH_SHORT);
+ }
+ }
+
+ /**
+ * 设置背景图片
+ */
+ private void setBackgroundImage(android.net.Uri imageUri) {
+ try {
+ // 获取图片的真实路径
+ String imagePath = getRealPathFromUri(imageUri);
+ if (imagePath != null) {
+ // 将图片路径保存到SharedPreferences
+ SharedPreferences sharedPreferences = getSharedPreferences("NoteSettings", Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putString("background_image", imagePath);
+ editor.apply();
+
+ // 加载图片并设置背景
+ android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeFile(imagePath);
+ if (bitmap != null) {
+ // 创建BitmapDrawable对象
+ android.graphics.drawable.BitmapDrawable drawable = new android.graphics.drawable.BitmapDrawable(getResources(), bitmap);
+ // 设置背景图片
+ mNoteEditorPanel.setBackground(drawable);
+ // 提示用户背景已更换
+ Toast.makeText(NoteEditActivity.this, "背景已更换", Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(NoteEditActivity.this, "图片加载失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error setting background image: " + e.getMessage());
+ Toast.makeText(NoteEditActivity.this, "更换背景失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 从URI获取真实的文件路径
+ */
+ private String getRealPathFromUri(android.net.Uri uri) {
+ String path = null;
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
+ // Android 4.4及以上版本
+ if ("content".equalsIgnoreCase(uri.getScheme())) {
+ String[] projection = {android.provider.MediaStore.Images.Media.DATA};
+ android.database.Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(uri, projection, null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ int column_index = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA);
+ path = cursor.getString(column_index);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ } else if ("file".equalsIgnoreCase(uri.getScheme())) {
+ path = uri.getPath();
+ }
+ } else {
+ // Android 4.4以下版本
+ String[] projection = {android.provider.MediaStore.Images.Media.DATA};
+ android.database.Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(uri, projection, null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ int column_index = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA);
+ path = cursor.getString(column_index);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ return path;
+ }
+
+ private android.graphics.Bitmap getCompressedBitmap(android.net.Uri uri) throws Exception {
+ // 获取图片的宽高
+ android.content.ContentResolver resolver = getContentResolver();
+ android.graphics.BitmapFactory.Options options = new android.graphics.BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ android.graphics.BitmapFactory.decodeStream(resolver.openInputStream(uri), null, options);
+ int width = options.outWidth;
+ int height = options.outHeight;
+
+ // 计算缩放比例
+ int scale = 1;
+ while (width / scale > MAX_IMAGE_SIZE || height / scale > MAX_IMAGE_SIZE) {
+ scale *= 2;
+ }
+
+ // 加载压缩后的图片
+ options.inJustDecodeBounds = false;
+ options.inSampleSize = scale;
+ return android.graphics.BitmapFactory.decodeStream(resolver.openInputStream(uri), null, options);
+ }
+
+ /**
+ * 根据最大宽度和高度缩放图片,保持原始宽高比
+ */
+ private android.graphics.Bitmap getScaledBitmap(android.graphics.Bitmap bitmap, int maxWidth, int maxHeight) {
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+
+ // 计算缩放比例
+ float scale = 1.0f;
+ if (width > maxWidth || height > maxHeight) {
+ float widthScale = (float) maxWidth / width;
+ float heightScale = (float) maxHeight / height;
+ scale = Math.min(widthScale, heightScale);
+ }
+
+ // 如果不需要缩放,直接返回原始 bitmap
+ if (scale == 1.0f) {
+ return bitmap;
+ }
+
+ // 计算新的宽度和高度
+ int newWidth = (int) (width * scale);
+ int newHeight = (int) (height * scale);
+
+ // 创建缩放后的 bitmap
+ return android.graphics.Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
+ }
+}
diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java b/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java
index 65401cb..1487a34 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java
@@ -14,264 +14,177 @@
* limitations under the License.
*/
-// 包声明:归属小米便签的UI模块,该类是便签编辑页的核心自定义输入控件
package net.micode.notes.ui;
-// 安卓系统上下文:提供应用运行环境与系统服务访问能力
import android.content.Context;
-// 安卓图形矩形类:用于焦点切换时的焦点区域坐标计算与传递
+import android.graphics.Color;
+import android.graphics.Typeface;
import android.graphics.Rect;
-// 安卓文本布局核心类:管理文本的排版、行高、行列定位,核心支撑触摸光标精准定位
import android.text.Layout;
-// 安卓文本选择工具类:用于手动设置文本光标位置、选中文本区域
import android.text.Selection;
-// 安卓富文本标记接口:标识带格式的文本(如包含链接、颜色的文本),本类核心处理该类型文本
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
import android.text.Spanned;
-// 安卓文本工具类:提供字符串判空、文本处理等安全高效的工具方法
+import android.text.TextPaint;
import android.text.TextUtils;
-// 安卓文本链接样式类:封装文本中的超链接数据,包含链接地址与点击事件
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ClickableSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
-// 安卓属性集类:承载布局xml中配置的控件属性,自定义View必备构造参数
+import android.text.style.UnderlineSpan;
import android.util.AttributeSet;
-// 安卓日志工具类:输出编辑框相关的调试日志,便于问题排查
import android.util.Log;
-// 安卓上下文菜单类:构建长按文本弹出的菜单容器
import android.view.ContextMenu;
-// 安卓按键事件类:封装物理按键/软键盘按键的事件信息,用于监听回车、删除键
import android.view.KeyEvent;
-// 安卓菜单项类:上下文菜单中的单个选项对象
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
-// 安卓触摸事件类:封装屏幕触摸的坐标、动作类型,用于重写触摸定位光标逻辑
import android.view.MotionEvent;
-// 安卓原生编辑框:本自定义控件的父类,继承所有原生EditText基础能力
+import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
-// 小米便签资源类:引用项目中的字符串、图片等资源文件,此处核心用链接菜单的文本资源
import net.micode.notes.R;
-// Java集合类:HashMap存储链接协议与菜单文本的映射关系,实现协议与文案解耦
import java.util.HashMap;
import java.util.Map;
/**
- * 小米便签 编辑页核心自定义EditText控件
- * 核心定位:继承原生Android EditText,基于便签编辑业务做深度定制扩展,无侵入式修改原生逻辑
- * 核心设计原则:重写系统回调方法实现定制化需求,所有扩展逻辑均通过回调接口解耦给上层Activity处理
- * 核心扩展能力(六大核心职责):
- * 1. 定制化按键交互:重写回车/删除键逻辑,适配多编辑框联动的核心业务;
- * 2. 精准触摸光标定位:修复原生触摸光标偏移问题,优化富文本编辑的触摸体验;
- * 3. 富文本链接专属处理:长按识别文本中的链接,弹出自定义上下文菜单并支持一键跳转;
- * 4. 焦点状态联动回调:编辑框获焦/失焦时,根据文本是否为空回调上层控制功能按钮显隐;
- * 5. 多编辑框索引管理:维护自身在编辑框列表中的索引,支撑增删联动逻辑;
- * 6. 完整兼容原生能力:所有重写方法最终都会调用父类实现,保留原生EditText全部功能。
+ * 富文本编辑框
+ *
+ * 该类继承自EditText,实现了富文本编辑的功能,包括加粗、斜体、下划线、删除线、
+ * 文本颜色、背景颜色、字号大小等功能。它还实现了链接检测和处理功能。
*/
public class NoteEditText extends EditText {
- // 日志打印的TAG标识:固定值,便于过滤该控件的所有日志信息
private static final String TAG = "NoteEditText";
-
- // 核心成员变量:当前编辑框在【多编辑框列表】中的索引值
- // 作用:用于向Activity回调增删操作时,标识当前操作的编辑框位置,核心支撑多框联动
private int mIndex;
-
- // 核心成员变量:删除键按下瞬间的光标起始位置
- // 作用:在onKeyDown中记录、onKeyUp中校验,判断是否触发「删除当前编辑框」的业务逻辑
private int mSelectionStartBeforeDelete;
- // ========== 常量区:文本链接的协议头(Scheme)常量 ==========
- // 定义安卓原生支持的三大核心链接协议前缀,用于匹配文本中的URLSpan链接类型
- private static final String SCHEME_TEL = "tel:"; // 电话链接协议头 → 匹配电话号码链接
- private static final String SCHEME_HTTP = "http:"; // 网页链接协议头 → 匹配http/https网页链接
- private static final String SCHEME_EMAIL = "mailto:";// 邮箱链接协议头 → 匹配邮件发送链接
+ private static final String SCHEME_TEL = "tel:" ;
+ private static final String SCHEME_HTTP = "http:" ;
+ private static final String SCHEME_EMAIL = "mailto:" ;
- /**
- * 静态不可变映射表:链接协议头(Scheme) → 上下文菜单文本的资源ID
- * 设计模式:静态代码块初始化的常量映射,全局唯一,避免多次创建对象造成内存浪费
- * 核心作用:根据识别到的链接协议,自动匹配对应的菜单文本,实现协议与文案的解耦管理
- */
private static final Map sSchemaActionResMap = new HashMap();
static {
- // 初始化映射关系:协议头 → 对应的菜单文本资源ID
- sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); // 电话链接 → 展示「拨打电话」
- sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); // 网页链接 → 展示「打开网页」
- sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); // 邮箱链接 → 展示「发送邮件」
+ sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel);
+ sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web);
+ sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
}
/**
- * 编辑框状态变化 回调接口【核心解耦设计】
- * 设计思想:将所有业务逻辑(增删编辑框、控制显隐)完全抽离到接口中,由上层NoteEditActivity实现
- * 核心价值:该控件只负责「触发事件」,不负责「处理事件」,彻底解耦UI控件与业务逻辑,符合单一职责
+ * Call by the {@link NoteEditActivity} to delete or add edit text
*/
public interface OnTextViewChangeListener {
/**
- * 删除当前编辑框的回调方法
- * 触发时机:按下删除键 + 光标在文本起始位 + 当前编辑框不是第一个编辑框
- * @param index 当前触发删除的编辑框索引
- * @param text 当前编辑框中的文本内容,供上层做数据保存
+ * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens
+ * and the text is null
*/
void onEditTextDelete(int index, String text);
/**
- * 新增编辑框的回调方法
- * 触发时机:按下回车键时,自动分割文本并回调上层新增编辑框
- * @param index 新增编辑框应该插入的目标索引(当前索引+1)
- * @param text 回车光标后的分割文本,作为新增编辑框的初始化内容
+ * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER}
+ * happen
*/
void onEditTextEnter(int index, String text);
/**
- * 文本/焦点状态变化的回调方法
- * 触发时机:编辑框获焦/失焦、文本内容变化时
- * @param index 当前编辑框的索引
- * @param hasText 当前编辑框是否有文本内容 → true显示功能按钮,false隐藏功能按钮
+ * Hide or show item option when text change
*/
void onTextChange(int index, boolean hasText);
}
- // 成员变量:回调接口的实例对象,由上层Activity通过set方法注入
private OnTextViewChangeListener mOnTextViewChangeListener;
- /**
- * 构造方法一:代码中手动创建控件时调用(无布局属性)
- * @param context 应用上下文对象,必传参数
- */
public NoteEditText(Context context) {
super(context, null);
- mIndex = 0; // 默认索引为0,代表首个编辑框,后续可通过setIndex重新赋值
+ mIndex = 0;
}
- /**
- * 对外提供的设置方法:为当前编辑框绑定在列表中的索引值
- * @param index 多编辑框列表中的位置索引
- */
public void setIndex(int index) {
mIndex = index;
}
- /**
- * 对外提供的设置方法:注入状态变化的回调监听器
- * @param listener 实现了OnTextViewChangeListener接口的实例(上层Activity)
- */
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
- /**
- * 构造方法二:布局xml中引用该控件时自动调用(带布局属性)
- * 安卓自定义View的标准构造方法,适配布局文件中的属性配置
- * @param context 应用上下文
- * @param attrs 布局xml中配置的控件属性集
- */
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
- /**
- * 构造方法三:带默认样式的构造方法,适配主题样式定制场景
- * 安卓自定义View的完整构造方法,满足所有创建场景,无业务逻辑,仅做父类调用
- * @param context 应用上下文
- * @param attrs 布局属性集
- * @param defStyle 默认样式资源ID
- */
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+ // TODO Auto-generated constructor stub
}
- /**
- * 重写触摸事件处理方法:核心优化【触摸光标精准定位】
- * 原生EditText痛点:触摸文本时光标位置容易偏移,尤其富文本编辑时体验差
- * 本方法核心逻辑:将触摸的屏幕坐标,精准转换为文本的行列偏移量,手动设置光标位置
- * @param event 触摸事件对象,包含触摸坐标、动作类型等信息
- * @return boolean 事件消费标记:返回父类处理结果,不拦截原生触摸逻辑
- */
@Override
public boolean onTouchEvent(MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN: // 只处理「按下」动作,是触摸的起始事件
- // 步骤1:计算触摸点的【文本相对坐标】- 修正内边距与滚动偏移的影响
- int x = (int) event.getX();
- int y = (int) event.getY();
- x -= getTotalPaddingLeft(); // 减去左侧总内边距,得到文本区域内的X坐标
- y -= getTotalPaddingTop(); // 减去顶部总内边距,得到文本区域内的Y坐标
- x += getScrollX(); // 加上横向滚动偏移,适配文本横向滚动的场景
- y += getScrollY(); // 加上纵向滚动偏移,适配文本纵向滚动的场景
-
- // 步骤2:通过文本布局对象,将坐标转换为文本的行列信息
- Layout layout = getLayout(); // 获取当前编辑框的文本布局管理器
- int line = layout.getLineForVertical(y); // 根据Y坐标获取触摸到的文本行号
- int off = layout.getOffsetForHorizontal(line, x); // 根据行号+X坐标获取该行的字符偏移量
-
- // 步骤3:手动设置光标到精准的触摸位置,核心修复原生偏移问题
- Selection.setSelection(getText(), off);
- break;
+ // 确保获得焦点,无论是否有内容
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ if (!hasFocus()) {
+ requestFocus();
+ }
+ }
+
+ // 调用父类方法处理事件
+ boolean handled = super.onTouchEvent(event);
+
+ // 处理点击事件
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ // 无论是否有layout,都确保光标在正确位置
+ setSelection(getText().length());
+
+ // 显示软键盘
+ Context context = getContext();
+ if (context != null) {
+ InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT);
+ }
+ }
}
- // 必须调用父类方法:保留原生的滑动、长按、双击等所有触摸逻辑,只做增量优化
- return super.onTouchEvent(event);
+
+ return handled;
}
- /**
- * 重写按键按下事件:预处理核心按键(回车/删除),做事件标记,不做业务逻辑处理
- * 核心设计:按键按下时「只记录状态」,按键抬起时「执行业务逻辑」,符合安卓按键事件的处理规范
- * 原因:按下事件可能有长按重复触发,抬起事件只会触发一次,保证业务逻辑只执行一次
- * @param keyCode 按键码:标识按下的是哪个按键(回车/删除/字母等)
- * @param event 按键事件对象,包含按键动作、重复次数等信息
- * @return boolean 事件消费标记:false=不消费,交给onKeyUp处理;true=消费,拦截后续处理
- */
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
- // 回车键按下:有监听器则返回false,交给onKeyUp处理分割+新增逻辑,无则走原生逻辑
if (mOnTextViewChangeListener != null) {
return false;
}
break;
case KeyEvent.KEYCODE_DEL:
- // 删除键按下:记录此时的光标起始位置,供onKeyUp校验是否触发删除编辑框逻辑
mSelectionStartBeforeDelete = getSelectionStart();
break;
default:
break;
}
- // 调用父类方法:处理所有其他按键的原生逻辑,无侵入式修改
return super.onKeyDown(keyCode, event);
}
- /**
- * 重写按键抬起事件:核心业务逻辑处理入口【重中之重】
- * 该方法是本控件的核心,集中处理「回车新增编辑框」「删除当前编辑框」两大核心业务逻辑
- * 所有业务逻辑执行完毕后,均调用父类方法,保证原生按键功能不受影响
- * @param keyCode 按键码
- * @param event 按键事件对象
- * @return boolean 事件消费标记:true=已处理业务逻辑,拦截原生行为;false=未处理,走原生逻辑
- */
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
case KeyEvent.KEYCODE_DEL:
- // ========== 删除键抬起:处理「删除编辑框」核心逻辑 ==========
if (mOnTextViewChangeListener != null) {
- // 触发条件【三重校验,缺一不可】:
- // 1. 光标在文本的起始位置(0);2. 当前编辑框不是第一个编辑框(索引≠0);3. 有回调监听器
if (0 == mSelectionStartBeforeDelete && mIndex != 0) {
- // 回调上层Activity:执行删除当前编辑框的逻辑,并传递当前文本内容
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
- return true; // 消费事件,避免原生删除逻辑执行,防止文本内容错乱
+ return true;
}
} else {
- // 无监听器时打印日志,便于调试排查问题
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
case KeyEvent.KEYCODE_ENTER:
- // ========== 回车键抬起:处理「分割文本+新增编辑框」核心逻辑 ==========
if (mOnTextViewChangeListener != null) {
- int selectionStart = getSelectionStart(); // 获取当前光标位置
- // 步骤1:分割文本 → 光标后的内容作为新增编辑框的初始化文本
+ int selectionStart = getSelectionStart();
String text = getText().subSequence(selectionStart, length()).toString();
- // 步骤2:更新当前编辑框文本 → 只保留光标前的内容,完成文本分割
setText(getText().subSequence(0, selectionStart));
- // 步骤3:回调上层Activity → 在当前索引+1的位置新增编辑框,并传递分割后的文本
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text);
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
@@ -280,58 +193,33 @@ public class NoteEditText extends EditText {
default:
break;
}
- // 调用父类方法:处理其他按键的原生抬起逻辑,兼容所有原生功能
return super.onKeyUp(keyCode, event);
}
- /**
- * 重写焦点变化事件:处理「焦点联动功能按钮显隐」的业务逻辑
- * 触发时机:编辑框获取焦点/失去焦点时自动调用
- * 核心逻辑:失焦时如果文本为空,回调隐藏功能按钮;其他情况回调显示功能按钮
- * 设计亮点:只关心「失焦+空文本」的特殊场景,其他场景统一回调显示,逻辑简洁高效
- * @param focused 当前是否获取到焦点:true=获焦,false=失焦
- * @param direction 焦点变化的方向,系统传递的参数,无实际业务用途
- * @param previouslyFocusedRect 上一个焦点控件的矩形区域,系统传递的参数
- */
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (mOnTextViewChangeListener != null) {
- // 核心判断:失焦 并且 文本为空 → 回调隐藏按钮;否则回调显示按钮
if (!focused && TextUtils.isEmpty(getText())) {
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
mOnTextViewChangeListener.onTextChange(mIndex, true);
}
}
- // 调用父类方法:执行原生的焦点变化逻辑,如光标显示/隐藏、背景色变化等
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
- /**
- * 重写上下文菜单创建方法:核心实现「富文本链接的自定义菜单」功能
- * 触发时机:长按编辑框中的文本时,系统自动调用该方法创建上下文菜单
- * 核心业务逻辑:识别长按区域是否包含链接 → 是则添加自定义链接菜单 → 点击菜单触发链接跳转
- * 原生兼容:识别不到链接时,自动调用父类方法创建默认菜单(复制/粘贴/全选等)
- * @param menu 系统传入的上下文菜单容器,用于添加自定义菜单项
- */
@Override
protected void onCreateContextMenu(ContextMenu menu) {
- // 前置判断:只有文本是【富文本(Spanned)】类型时,才处理链接识别逻辑
if (getText() instanceof Spanned) {
- // 步骤1:获取当前光标选中的文本区域,兼容正选/反选两种情况
int selStart = getSelectionStart();
int selEnd = getSelectionEnd();
- int min = Math.min(selStart, selEnd); // 选中区域的起始位置
- int max = Math.max(selStart, selEnd); // 选中区域的结束位置
- // 步骤2:从选中区域中提取所有的URLSpan链接对象
+ int min = Math.min(selStart, selEnd);
+ int max = Math.max(selStart, selEnd);
+
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
-
- // 步骤3:仅处理「单个链接」的场景,避免多链接冲突,保证菜单的唯一性
if (urls.length == 1) {
- int defaultResId = 0; // 初始化菜单文本资源ID
-
- // 步骤4:遍历协议映射表,匹配当前链接的协议类型,获取对应的菜单文本
+ int defaultResId = 0;
for(String schema: sSchemaActionResMap.keySet()) {
if(urls[0].getURL().indexOf(schema) >= 0) {
defaultResId = sSchemaActionResMap.get(schema);
@@ -339,23 +227,217 @@ public class NoteEditText extends EditText {
}
}
- // 兜底处理:未匹配到已知协议时,显示「其他链接」的默认文本
if (defaultResId == 0) {
defaultResId = R.string.note_link_other;
}
- // 步骤5:向菜单中添加自定义选项,并绑定点击事件
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
- // 核心逻辑:触发URLSpan原生的点击事件 → 自动跳转对应链接(拨打电话/打开网页/发送邮件)
+ // goto a new intent
urls[0].onClick(NoteEditText.this);
- return true; // 消费菜单点击事件,避免后续处理
+ return true;
}
});
}
}
- // 必须调用父类方法:无链接时创建原生默认菜单(复制、粘贴、剪切、选择全部等),完整兼容原生功能
super.onCreateContextMenu(menu);
}
-}
\ No newline at end of file
+
+ /**
+ * 设置文字加粗
+ */
+ public void setBold() {
+ toggleStyle(Typeface.BOLD);
+ }
+
+ /**
+ * 设置文字斜体
+ */
+ public void setItalic() {
+ toggleStyle(Typeface.ITALIC);
+ }
+
+ /**
+ * 设置文字粗斜体
+ */
+ public void setBoldItalic() {
+ applyStyle(Typeface.BOLD_ITALIC);
+ }
+
+ /**
+ * 设置文字正常
+ */
+ public void setNormal() {
+ applyStyle(Typeface.NORMAL);
+ }
+
+ /**
+ * 切换下划线
+ */
+ public void toggleUnderline() {
+ toggleSpan(UnderlineSpan.class);
+ }
+
+ /**
+ * 切换删除线
+ */
+ public void toggleStrikethrough() {
+ toggleSpan(StrikethroughSpan.class);
+ }
+
+ /**
+ * 设置文字颜色
+ */
+ public void setTextColor(int color) {
+ applySpan(new ForegroundColorSpan(color));
+ }
+
+ /**
+ * 设置文字背景颜色
+ */
+ public void setTextBackgroundColor(int color) {
+ applySpan(new BackgroundColorSpan(color));
+ }
+
+ /**
+ * 设置字号大小
+ */
+ public void setTextSize(float size) {
+ // 将字号稍微放大一些
+ float adjustedSize = size * 1.1f;
+ applySpan(new RelativeSizeSpan(adjustedSize));
+ }
+
+ /**
+ * 设置文字左对齐
+ */
+ public void setAlignLeft() {
+ setGravity(android.view.Gravity.LEFT);
+ }
+
+ /**
+ * 设置文字居中对齐
+ */
+ public void setAlignCenter() {
+ setGravity(android.view.Gravity.CENTER);
+ }
+
+ /**
+ * 设置文字右对齐
+ */
+ public void setAlignRight() {
+ setGravity(android.view.Gravity.RIGHT);
+ }
+
+ /**
+ * 设置文字两端对齐
+ */
+ public void setAlignJustify() {
+ setGravity(android.view.Gravity.FILL_HORIZONTAL);
+ }
+
+ /**
+ * 应用样式
+ */
+ private void applyStyle(int style) {
+ Spannable spannable = getText();
+ if (spannable == null) return;
+
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ if (start == end) return;
+
+ StyleSpan[] spans = spannable.getSpans(start, end, StyleSpan.class);
+ for (StyleSpan span : spans) {
+ spannable.removeSpan(span);
+ }
+
+ spannable.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ setSelection(start, end);
+ }
+
+ /**
+ * 切换样式
+ */
+ private void toggleStyle(int style) {
+ Spannable spannable = getText();
+ if (spannable == null) return;
+
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ if (start == end) return;
+
+ // 检查当前样式
+ boolean hasStyle = false;
+ StyleSpan[] spans = spannable.getSpans(start, end, StyleSpan.class);
+ for (StyleSpan span : spans) {
+ if (span.getStyle() == style) {
+ hasStyle = true;
+ spannable.removeSpan(span);
+ }
+ }
+
+ if (!hasStyle) {
+ // 没有该样式,添加
+ spannable.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ setSelection(start, end);
+ }
+
+ /**
+ * 应用Span
+ */
+ private void applySpan(Object span) {
+ Spannable spannable = getText();
+ if (spannable == null) return;
+
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ if (start == end) return;
+
+ spannable.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ setSelection(start, end);
+ }
+
+ /**
+ * 切换Span
+ */
+ private void toggleSpan(Class> spanClass) {
+ Spannable spannable = getText();
+ if (spannable == null) return;
+
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ if (start == end) return;
+
+ // 检查当前是否有该Span
+ boolean hasSpan = false;
+ try {
+ Object[] spans = spannable.getSpans(start, end, spanClass);
+ for (Object span : spans) {
+ hasSpan = true;
+ spannable.removeSpan(span);
+ }
+
+ if (!hasSpan) {
+ // 没有该Span,添加
+ try {
+ // 创建该Span的实例
+ Object newSpan = spanClass.getDeclaredConstructor().newInstance();
+ spannable.setSpan(newSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } catch (Exception e) {
+ // 捕获所有异常,避免崩溃
+ e.printStackTrace();
+ }
+ }
+ } catch (Exception e) {
+ // 捕获所有异常,避免崩溃
+ e.printStackTrace();
+ }
+
+ setSelection(start, end);
+ }
+}
+
diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java b/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java
index 653b7fc..8c236df 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java
@@ -14,372 +14,258 @@
* limitations under the License.
*/
-// 包声明:归属小米便签UI模块,为列表页提供标准化的便签/文件夹数据模型,封装数据库游标解析逻辑
package net.micode.notes.ui;
-// 安卓上下文:提供系统服务访问能力,用于联系人查询、内容解析器获取
import android.content.Context;
-// 安卓数据库游标:承载数据库查询结果集,是本类核心的数据解析源
import android.database.Cursor;
-// 安卓文本工具类:提供字符串判空、文本处理等安全操作方法
import android.text.TextUtils;
-// 联系人数据工具类:根据手机号匹配系统通讯录中的联系人姓名
import net.micode.notes.data.Contact;
-// 便签核心常量类:定义便签类型、文件夹ID、字段名等全局常量
import net.micode.notes.data.Notes;
-// 便签数据库列名常量:数据库表的字段名称枚举,避免硬编码字符串
import net.micode.notes.data.Notes.NoteColumns;
-// 便签数据工具类:提供通话记录手机号查询、数据格式化等通用能力
import net.micode.notes.tool.DataUtils;
+
/**
- * 便签列表项核心数据模型类【小米便签核心类】
- * 核心定位:MVC架构中的Model层,纯数据载体,无任何UI渲染逻辑
- * 核心职责:
- * 1. 定义数据库查询的投影字段,精准指定查询列,规避全表查询的性能损耗;
- * 2. 从数据库Cursor中解析并封装所有便签/文件夹的业务数据;
- * 3. 对通话记录类便签做专属适配,自动关联手机号和联系人名称;
- * 4. 智能判断列表项的位置状态,为UI背景样式渲染提供数据支撑;
- * 5. 提供只读的getter方法和业务判断方法,对外屏蔽数据细节,保证数据安全;
- * 设计原则:单一职责 + 完全封装 + 只读数据 + 业务内聚,解耦性极强
+ * 便签列表数据传输对象
+ *
+ * 该类是专为列表页(NotesListActivity)设计的轻量级数据传输对象,
+ * 用于从Cursor中提取数据并驱动列表渲染。它包含了便签的基本信息,
+ * 如ID、类型、内容摘要、修改时间、背景颜色等。
*/
public class NoteItemData {
- /**
- * 数据库查询投影数组:指定本次查询需要返回的数据库列
- * 投影设计原则:按需查询,只获取业务需要的字段,减少内存占用和IO开销
- * 字段覆盖:便签/文件夹的基础属性、时间属性、关联属性、扩展属性四大类
- */
- static final String[] PROJECTION = new String[]{
- NoteColumns.ID, // 0: 便签/文件夹的唯一主键ID
- NoteColumns.ALERTED_DATE, // 1: 提醒时间戳(0代表无提醒)
- NoteColumns.BG_COLOR_ID, // 2: 背景色ID,用于列表项背景着色
- NoteColumns.CREATED_DATE, // 3: 创建时间戳,UTC毫秒值
- NoteColumns.HAS_ATTACHMENT, // 4: 是否包含附件 0-无 1-有(图片/音频等)
- NoteColumns.MODIFIED_DATE, // 5: 最后修改时间戳,列表页优先展示该时间
- NoteColumns.NOTES_COUNT, // 6: 文件夹内包含的便签数量,仅文件夹类型有效
- NoteColumns.PARENT_ID, // 7: 父文件夹ID,根目录为Notes.ID_ROOT_FOLDER
- NoteColumns.SNIPPET, // 8: 便签内容摘要/文件夹名称,短文本展示用
- NoteColumns.TYPE, // 9: 数据类型,区分便签/文件夹/系统项
- NoteColumns.WIDGET_ID, // 10: 关联的桌面小部件ID,无效则为默认值
- NoteColumns.WIDGET_TYPE // 11: 关联小部件尺寸类型 2x/4x
+ static final String [] PROJECTION = new String [] {
+ NoteColumns.ID,
+ NoteColumns.ALERTED_DATE,
+ NoteColumns.BG_COLOR_ID,
+ NoteColumns.CREATED_DATE,
+ NoteColumns.HAS_ATTACHMENT,
+ NoteColumns.MODIFIED_DATE,
+ NoteColumns.NOTES_COUNT,
+ NoteColumns.PARENT_ID,
+ NoteColumns.SNIPPET,
+ NoteColumns.TITLE,
+ NoteColumns.TYPE,
+ NoteColumns.WIDGET_ID,
+ NoteColumns.WIDGET_TYPE,
+ NoteColumns.PINNED,
+ NoteColumns.SORT_ORDER,
+ NoteColumns.LOCKED,
+ NoteColumns.PUBLIC,
};
- /**
- * 投影数组列索引常量:与PROJECTION数组字段一一对应
- * 设计目的:彻底避免硬编码数字索引,提升代码可读性+可维护性
- * 核心价值:修改投影数组字段顺序时,仅需同步修改此处索引,无需改动业务解析代码
- */
- private static final int ID_COLUMN = 0;
- private static final int ALERTED_DATE_COLUMN = 1;
- private static final int BG_COLOR_ID_COLUMN = 2;
- private static final int CREATED_DATE_COLUMN = 3;
- private static final int HAS_ATTACHMENT_COLUMN = 4;
- private static final int MODIFIED_DATE_COLUMN = 5;
- private static final int NOTES_COUNT_COLUMN = 6;
- private static final int PARENT_ID_COLUMN = 7;
- private static final int SNIPPET_COLUMN = 8;
- private static final int TYPE_COLUMN = 9;
- private static final int WIDGET_ID_COLUMN = 10;
- private static final int WIDGET_TYPE_COLUMN = 11;
-
- // ===================== 核心业务数据字段 - 与投影数组一一映射 =====================
- // 访问规则:全部私有,仅通过getter方法访问,无setter,数据只读,保证一致性
- private long mId; // 便签/文件夹唯一ID,数据库主键
- private long mAlertDate; // 提醒时间戳,>0表示该便签设置了提醒
- private int mBgColorId; // 背景色ID,对应预设的颜色值,列表项渲染用
- private long mCreatedDate; // 便签创建时间戳
- private boolean mHasAttachment; // 是否包含附件,数据库int转Java布尔值,贴合业务语义
- private long mModifiedDate; // 最后修改时间戳,列表页展示的核心时间字段
- private int mNotesCount; // 文件夹内便签数量,仅TYPE_FOLDER类型有效
- private long mParentId; // 父文件夹ID,通话记录便签固定为通话记录文件夹ID
- private String mSnippet; // 便签纯文本摘要/文件夹名称,已清理勾选标记
- private int mType; // 数据类型:Notes.TYPE_NOTE/文件夹/系统项
- private int mWidgetId; // 关联桌面小部件ID,无效则为INVALID_APPWIDGET_ID
- private int mWidgetType; // 关联小部件尺寸类型,2x/4x两种规格
-
- // ===================== 通话记录专属扩展字段 =====================
- private String mName; // 通话记录联系人姓名,无则显示手机号
- private String mPhoneNumber; // 通话记录对应的手机号,仅通话便签有值
-
- // ===================== 列表位置状态字段 - 纯UI渲染支撑 =====================
- // 作用:标记当前项在列表中的位置,用于适配不同的背景样式(圆角/分割线/边距等)
- private boolean mIsLastItem; // 是否为列表最后一条数据
- private boolean mIsFirstItem; // 是否为列表第一条数据
- private boolean mIsOnlyOneItem; // 是否为列表中唯一的一条数据
- private boolean mIsOneNoteFollowingFolder; // 文件夹后的唯一一条便签项
- private boolean mIsMultiNotesFollowingFolder;// 文件夹后的第一条便签(后续还有更多项)
-
- /**
- * 唯一构造方法:私有化核心初始化逻辑,从Cursor解析所有数据并完成对象初始化
- * 设计特点:全参构造,一次性完成所有数据赋值,无空构造,保证对象完整性
- * @param context 应用上下文,用于获取内容解析器、查询联系人信息,不可为空
- * @param cursor 数据库查询结果游标,已移动到目标行,不可为空/已关闭
- */
+ public static final int ID_COLUMN = 0;
+ private static final int ALERTED_DATE_COLUMN = 1;
+ private static final int BG_COLOR_ID_COLUMN = 2;
+ private static final int CREATED_DATE_COLUMN = 3;
+ private static final int HAS_ATTACHMENT_COLUMN = 4;
+ private static final int MODIFIED_DATE_COLUMN = 5;
+ private static final int NOTES_COUNT_COLUMN = 6;
+ private static final int PARENT_ID_COLUMN = 7;
+ private static final int SNIPPET_COLUMN = 8;
+ private static final int TITLE_COLUMN = 9;
+ private static final int TYPE_COLUMN = 10;
+ private static final int WIDGET_ID_COLUMN = 11;
+ private static final int WIDGET_TYPE_COLUMN = 12;
+ private static final int PINNED_COLUMN = 13;
+ private static final int SORT_ORDER_COLUMN = 14;
+ private static final int LOCKED_COLUMN = 15;
+ private static final int PUBLIC_COLUMN = 16;
+
+ private long mId;
+ private long mAlertDate;
+ private int mBgColorId;
+ private long mCreatedDate;
+ private boolean mHasAttachment;
+ private long mModifiedDate;
+ private int mNotesCount;
+ private long mParentId;
+ private String mSnippet;
+ private String mTitle;
+ private int mType;
+ private int mWidgetId;
+ private int mWidgetType;
+ private boolean mPinned;
+ private int mSortOrder;
+ private boolean mLocked;
+ private boolean mPublic;
+ private String mName;
+ private String mPhoneNumber;
+
+ private boolean mIsLastItem;
+ private boolean mIsFirstItem;
+ private boolean mIsOnlyOneItem;
+ private boolean mIsOneNoteFollowingFolder;
+ private boolean mIsMultiNotesFollowingFolder;
+
public NoteItemData(Context context, Cursor cursor) {
- // 第一步:解析数据库核心字段,与投影数组索引一一对应,基础数据初始化
mId = cursor.getLong(ID_COLUMN);
mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN);
mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN);
mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN);
- // 数据库存储为int(0/1),转换为业务语义更清晰的boolean类型
- mHasAttachment = cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0;
+ mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false;
mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN);
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN);
mParentId = cursor.getLong(PARENT_ID_COLUMN);
- // 解析摘要文本,并清理勾选框标记标签,仅保留纯文本内容,优化展示效果
mSnippet = cursor.getString(SNIPPET_COLUMN);
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, "");
+ mTitle = cursor.getString(TITLE_COLUMN);
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
+ mPinned = (cursor.getInt(PINNED_COLUMN) > 0) ? true : false;
+ mSortOrder = cursor.getInt(SORT_ORDER_COLUMN);
+ mLocked = (cursor.getInt(LOCKED_COLUMN) > 0) ? true : false;
+ mPublic = (cursor.getInt(PUBLIC_COLUMN) > 0) ? true : false;
- // 第二步:通话记录便签专属解析逻辑,仅父文件夹为通话记录文件夹时触发
mPhoneNumber = "";
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
- // 根据便签ID查询关联的通话手机号
mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId);
- // 手机号非空时,查询系统通讯录匹配联系人姓名
if (!TextUtils.isEmpty(mPhoneNumber)) {
mName = Contact.getContact(context, mPhoneNumber);
- // 无匹配联系人时,手机号作为联系人名称兜底展示
if (mName == null) {
mName = mPhoneNumber;
}
}
}
- // 第三步:空值兜底处理,防止空指针异常,生产级代码必备容错逻辑
if (mName == null) {
mName = "";
}
-
- // 第四步:自动判断当前项在列表中的位置状态,为UI渲染提供数据支撑
checkPostion(cursor);
}
- /**
- * 私有核心方法:解析当前Cursor所在行的列表位置状态,初始化位置标记字段
- * 核心作用:为列表项的背景样式提供精准的状态标记,不同位置展示不同样式
- * 设计亮点:Cursor位置移动后必回位,防止游标位置错乱导致后续解析异常
- * @param cursor 数据库游标,已定位到当前数据行
- */
private void checkPostion(Cursor cursor) {
- // 初始化基础位置状态:首项、尾项、唯一项
- mIsLastItem = cursor.isLast();
- mIsFirstItem = cursor.isFirst();
- mIsOnlyOneItem = cursor.getCount() == 1;
-
- // 初始化文件夹子项状态为默认值false
+ mIsLastItem = cursor.isLast() ? true : false;
+ mIsFirstItem = cursor.isFirst() ? true : false;
+ mIsOnlyOneItem = (cursor.getCount() == 1);
mIsMultiNotesFollowingFolder = false;
mIsOneNoteFollowingFolder = false;
- // 核心业务判断:仅处理【普通便签】且【非列表首项】的场景
if (mType == Notes.TYPE_NOTE && !mIsFirstItem) {
- // 记录当前游标位置,用于后续回位,防止位置丢失
- int currentPosition = cursor.getPosition();
- // 游标上移一行,判断上一项是否为【文件夹】或【系统项】
+ int position = cursor.getPosition();
if (cursor.moveToPrevious()) {
- int prevItemType = cursor.getInt(TYPE_COLUMN);
- if (prevItemType == Notes.TYPE_FOLDER || prevItemType == Notes.TYPE_SYSTEM) {
- // 上一项是文件夹/系统项 → 当前项是文件夹的子项第一条
- if (cursor.getCount() > currentPosition + 1) {
- // 后续还有更多数据 → 标记为多子项的第一条
+ if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER
+ || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) {
+ if (cursor.getCount() > (position + 1)) {
mIsMultiNotesFollowingFolder = true;
} else {
- // 后续无数据 → 标记为文件夹后的唯一子项
mIsOneNoteFollowingFolder = true;
}
}
- // 游标回位到原位置,必须执行,否则会导致后续数据解析错位
if (!cursor.moveToNext()) {
- // 回位失败时主动抛异常,快速定位问题,避免隐性bug
throw new IllegalStateException("cursor move to previous but can't move back");
}
}
}
}
- /**
- * 业务判断方法:当前项是否为【文件夹后的唯一便签项】
- * @return true-是 false-否
- */
public boolean isOneFollowingFolder() {
return mIsOneNoteFollowingFolder;
}
- /**
- * 业务判断方法:当前项是否为【文件夹后的第一条便签(后续还有项)】
- * @return true-是 false-否
- */
public boolean isMultiFollowingFolder() {
return mIsMultiNotesFollowingFolder;
}
- /**
- * 位置判断方法:当前项是否为列表最后一项
- * @return true-是 false-否
- */
public boolean isLast() {
return mIsLastItem;
}
- /**
- * 数据访问方法:获取通话记录的联系人名称(无则返回手机号)
- * @return 联系人姓名/手机号 空则返回空字符串
- */
public String getCallName() {
return mName;
}
- /**
- * 位置判断方法:当前项是否为列表第一项
- * @return true-是 false-否
- */
public boolean isFirst() {
return mIsFirstItem;
}
- /**
- * 位置判断方法:当前项是否为列表中唯一的一项
- * @return true-是 false-否
- */
public boolean isSingle() {
return mIsOnlyOneItem;
}
- /**
- * 基础数据访问:获取便签/文件夹唯一ID
- * @return 数据库主键ID,long类型
- */
public long getId() {
return mId;
}
- /**
- * 基础数据访问:获取提醒时间戳
- * @return 提醒时间UTC毫秒值,0表示无提醒
- */
public long getAlertDate() {
return mAlertDate;
}
- /**
- * 基础数据访问:获取创建时间戳
- * @return 创建时间UTC毫秒值
- */
public long getCreatedDate() {
return mCreatedDate;
}
- /**
- * 业务判断方法:当前便签是否包含附件(图片/音频等)
- * @return true-有附件 false-无附件
- */
public boolean hasAttachment() {
return mHasAttachment;
}
- /**
- * 基础数据访问:获取最后修改时间戳(列表页优先展示)
- * @return 修改时间UTC毫秒值
- */
public long getModifiedDate() {
return mModifiedDate;
}
- /**
- * 基础数据访问:获取背景色ID
- * @return 预设的颜色ID值,用于列表项背景渲染
- */
public int getBgColorId() {
return mBgColorId;
}
- /**
- * 基础数据访问:获取父文件夹ID
- * @return 父文件夹主键ID,通话记录为固定常量ID
- */
public long getParentId() {
return mParentId;
}
- /**
- * 基础数据访问:获取文件夹内便签数量
- * @return 数量值,仅文件夹类型有效,便签类型返回无意义值
- */
public int getNotesCount() {
return mNotesCount;
}
- /**
- * 兼容适配方法:等价于getParentId(),适配外部旧调用逻辑
- * 设计目的:向下兼容,无侵入式修改原有业务代码
- * @return 父文件夹主键ID
- */
- public long getFolderId() {
+ public long getFolderId () {
return mParentId;
}
- /**
- * 基础数据访问:获取数据类型
- * @return 类型值:Notes.TYPE_NOTE/文件夹/系统项
- */
public int getType() {
return mType;
}
- /**
- * 基础数据访问:获取关联小部件类型
- * @return 小部件尺寸类型 2x/4x
- */
- public int getWidgetType() {
- return mWidgetType;
- }
-
- /**
- * 基础数据访问:获取关联小部件ID
- * @return 小部件ID,无效则为INVALID_APPWIDGET_ID
- */
- public int getWidgetId() {
- return mWidgetId;
- }
-
- /**
- * 基础数据访问:获取清理后的纯文本摘要/文件夹名称
- * @return 无勾选标记的纯文本短内容
- */
+ public int getWidgetType() {
+ return mWidgetType;
+ }
+
+ public int getWidgetId() {
+ return mWidgetId;
+ }
+
public String getSnippet() {
return mSnippet;
}
- /**
- * 核心业务判断方法:当前便签是否设置了提醒
- * 封装细节:屏蔽「时间戳判0」的技术细节,对外提供业务语义
- * @return true-有提醒 false-无提醒
- */
public boolean hasAlert() {
- return mAlertDate > 0;
+ return (mAlertDate > 0);
}
- /**
- * 核心业务判断方法:当前项是否为【通话记录便签】
- * 判断条件:父文件夹是通话记录文件夹 + 手机号非空,双重校验保证准确性
- * @return true-通话记录便签 false-普通便签/文件夹
- */
public boolean isCallRecord() {
- return mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber);
+ return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
+ }
+
+ public boolean isPinned() {
+ return mPinned;
+ }
+
+ public int getSortOrder() {
+ return mSortOrder;
+ }
+
+ public boolean isLocked() {
+ return mLocked;
+ }
+
+ public boolean isPublic() {
+ return mPublic;
+ }
+
+ public String getTitle() {
+ return mTitle;
}
- /**
- * 静态工具方法:无需创建对象,快速从Cursor获取便签类型
- * 性能优化点:避免为了单个字段创建完整对象实例,适配列表适配器高频调用场景
- * 对外赋能:给外部适配器提供轻量级的类型查询能力
- * @param cursor 数据库游标,已定位到目标行
- * @return 便签类型值
- */
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java b/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java
index 462e30a..4234f16 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java
@@ -22,6 +22,7 @@ import android.app.Dialog;
import android.appwidget.AppWidgetManager;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
+import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
@@ -49,25 +50,37 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
import android.view.View.OnTouchListener;
+import android.view.KeyEvent;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.Button;
import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.database.sqlite.SQLiteDatabase;
import net.micode.notes.R;
+import net.micode.notes.data.Messages;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
+import net.micode.notes.data.NotesDatabaseHelper;
+import net.micode.notes.data.Users;
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.ResourceParser;
+import net.micode.notes.tool.UserManager;
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;
@@ -76,159 +89,177 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.util.ArrayList;
import java.util.HashSet;
/**
- * 小米便签 核心主页面 - 便签列表页
+ * 便签列表活动
*
- * 继承安卓原生{Activity},是小米便签应用的**核心门户页面**,隶属于MVC架构的UI层业务页面,承载应用所有核心交互能力;
- * 核心设计定位:作为应用的入口与中枢,统一管理便签/文件夹的所有核心业务操作,封装完整的页面状态管理、事件分发、异步数据处理、交互反馈逻辑,
- * 是连接便签编辑页、设置页、小组件、数据工具类的核心桥梁;
- * 核心业务职责:
- * 1. 数据展示:分页展示便签列表、文件夹列表、通话记录专属文件夹及内容,适配不同文件夹层级的视图切换;
- * 2. 便签操作:新建普通便签、打开已有便签编辑、批量删除便签、批量移动便签到指定文件夹,适配同步/非同步模式的差异化删除逻辑;
- * 3. 文件夹管理:创建文件夹、重命名文件夹、删除文件夹、进入文件夹查看子项,校验文件夹名称唯一性,维护文件夹层级关系;
- * 4. 多选模式:完整支撑便签的批量操作,包含全选/取消全选、选中状态维护、选中数量统计、多选菜单适配;
- * 5. 初始化引导:首次打开应用自动创建引导便签,展示应用核心功能与使用说明;
- * 6. 数据交互:异步执行数据库查询/更新/删除,避免主线程阻塞,保证页面流畅性;导出便签为文本文件,支持本地备份;
- * 7. 同步适配:区分同步模式/非同步模式,同步模式下删除便签移至回收站,支持手动触发/取消同步,跳转同步设置页;
- * 8. 小组件联动:便签数据变更后,自动发送广播更新关联的2x/4x桌面小组件内容,保证数据一致性;
- * 9. 交互优化:处理新建按钮透明区域的事件透传、长按震动反馈、软键盘的显隐控制、上下文菜单的创建与销毁;
- * 技术实现特点:
- * - 基于{AsyncQueryHandler}实现数据库异步操作,解决主线程阻塞问题,提升页面滑动与操作流畅度;
- * - 基于{NotesListAdapter}实现列表数据与视图的解耦,统一管理列表项的渲染与选中状态;
- * - 通过枚举{ListEditState}维护页面状态,精准控制不同状态下的视图展示与功能开放;
- * - 大量使用{AsyncTask}处理耗时操作(删除/导出),保证UI线程不被阻塞;
- * - 完整的异常边界处理,包含空数据校验、无效ID过滤、日志输出,提升应用稳定性;
- * - 精细化的交互体验优化,包含触摸事件分发、震动反馈、Toast提示、对话框确认,兼顾功能完整性与用户体验。
- *
+ * 该类是便签列表的核心界面,负责显示便签列表、处理便签的选择、删除、移动等操作。
+ * 它使用AsyncQueryHandler异步加载数据,支持便签的批量操作和小部件的管理。
+ *
+ * [2025 新特性]: 支持基于AI颜色分类的智能排序功能。
*/
public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener {
- // ===================== 异步查询任务标识常量 - 核心区分不同类型的异步数据库查询 =====================
- /** 异步查询Token:查询指定文件夹下的所有便签/子文件夹数据,是页面最核心的查询任务 */
private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0;
- /** 异步查询Token:查询可用于移动便签的目标文件夹列表,仅在批量移动便签时触发 */
+
private static final int FOLDER_LIST_QUERY_TOKEN = 1;
- // ===================== 文件夹上下文菜单操作ID - 区分文件夹的右键菜单功能 =====================
- /** 上下文菜单ID:删除当前文件夹 */
private static final int MENU_FOLDER_DELETE = 0;
- /** 上下文菜单ID:进入当前文件夹查看子项 */
+
private static final int MENU_FOLDER_VIEW = 1;
- /** 上下文菜单ID:修改当前文件夹的名称 */
+
private static final int MENU_FOLDER_CHANGE_NAME = 2;
- // ===================== 偏好设置存储键值 - 持久化标记应用初始化状态 =====================
- /** SharedPreference存储Key:标记是否已为新用户创建首次使用引导便签,避免重复创建 */
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, TRASH_FOLDER
};
- // ===================== 页面核心成员变量 =====================
- /** 当前页面的状态标识:控制视图展示与功能逻辑分支,默认值为NOTE_LIST根目录状态 */
private ListEditState mState;
- /** 异步数据库查询处理器:核心工具类,所有数据库的增删改查均通过该类异步执行,避免主线程阻塞 */
+
private BackgroundQueryHandler mBackgroundQueryHandler;
- /** 列表核心适配器:绑定数据库Cursor数据与ListView,管理列表项渲染、选中状态、数据统计,页面核心数据桥梁 */
+
private NotesListAdapter mNotesListAdapter;
- /** 核心列表控件:承载所有便签/文件夹的展示,是页面的核心视图容器 */
+
private ListView mNotesListView;
- /** 新建便签按钮:页面底部悬浮按钮,点击快速创建新便签,包含透明区域事件透传逻辑 */
+ private GridView mNotesGridView;
+
private Button mAddNewNote;
- /** 事件分发标记:标记是否需要将新建按钮的触摸事件透传给下方的ListView,处理透明区域点击 */
+
private boolean mDispatch;
- /** 触摸原始坐标Y:记录新建按钮触摸事件的初始Y坐标,用于事件透传时的坐标校准 */
+
private int mOriginY;
- /** 触摸分发坐标Y:记录事件透传后的目标Y坐标,保证ListView接收的坐标准确性 */
+
private int mDispatchY;
- /** 标题栏文本控件:子文件夹/通话记录文件夹状态下显示,展示当前文件夹名称,根目录状态下隐藏 */
+
private TextView mTitleBar;
- /** 当前选中的文件夹ID:标记用户当前浏览的文件夹层级,默认值为根文件夹ID,用于数据库查询条件拼接 */
+
private long mCurrentFolderId;
- /** 内容解析器:应用与ContentProvider的核心交互工具,所有数据库操作均通过该类执行 */
+
private ContentResolver mContentResolver;
- /** 多选模式回调处理器:封装多选模式的创建、菜单适配、选中状态变更、批量操作逻辑,是多选功能的核心 */
+
private ModeCallback mModeCallBack;
- /** 日志输出标签:页面所有日志的统一标识,便于日志过滤与问题定位 */
+
+ // 搜索相关组件
+ private EditText mSearchEditText;
+ private ImageView mSearchImageView;
+ private ImageView mCancelImageView;
+ private LinearLayout mSearchBar;
+ private String mSearchQuery;
+ private boolean mIsSearching;
+
+ // 显示模式常量
+ private static final int DISPLAY_MODE_LIST = 0;
+ private static final int DISPLAY_MODE_GRID = 1;
+
+ // 当前显示模式
+ private int mDisplayMode = DISPLAY_MODE_LIST;
+
+ // 排序方式常量
+ private static final String SORT_BY_CREATE_DATE = NoteColumns.CREATED_DATE + " DESC";
+ private static final String SORT_BY_MODIFIED_DATE = NoteColumns.MODIFIED_DATE + " DESC";
+
+ // 当前排序方式
+ private String mCurrentSortOrder = SORT_BY_MODIFIED_DATE;
+
private static final String TAG = "NotesListActivity";
- /** 列表滚动速率常量:自定义列表滚动行为的速率参数,优化滚动流畅度 */
+
public static final int NOTES_LISTVIEW_SCROLL_RATE = 30;
- /** 当前聚焦的数据项:记录长按选中的便签/文件夹数据,用于多选模式初始化、文件夹上下文菜单操作 */
+
private NoteItemData mFocusNoteDataItem;
- // ===================== 数据库查询条件常量 - 区分根文件夹与普通文件夹的查询逻辑 =====================
- /** 普通文件夹查询条件:精准查询指定父文件夹下的所有子项,适用于子文件夹层级的数据加载 */
+ // 拖拽相关变量
+ private boolean mIsDragging = false;
+ private int mDragStartPosition = -1;
+ private int mDragCurrentPosition = -1;
+ private View mDraggingView = null;
+ private int mDraggingViewHeight = 0;
+ private float mDragStartY = 0;
+ private float mDragOffsetY = 0;
+ private NoteItemData[] mDragTempData = null; // 拖拽过程中的临时数据列表
+
private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?";
- /** 根文件夹查询条件:特殊复合条件,排除系统文件夹 + 仅展示有内容的通话记录文件夹,保证根目录视图整洁 */
- private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>"
- + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR ("
+
+ private static final String ROOT_FOLDER_SELECTION = "( " + NoteColumns.TYPE + "<>"
+ + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?) OR ( "
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND "
- + NoteColumns.NOTES_COUNT + ">0)";
+ + NoteColumns.NOTES_COUNT + ">0)" + " OR ( "
+ + NoteColumns.ID + "=" + Notes.ID_TRASH_FOLER + ")"; // 始终显示回收站,不管里面有没有便签
- // ===================== Activity跳转请求码 - 区分不同场景的页面跳转与结果回调 =====================
- /** 请求码:打开已有便签编辑的跳转标识,用于onActivityResult区分回调场景 */
private final static int REQUEST_CODE_OPEN_NODE = 102;
- /** 请求码:新建便签的跳转标识,用于onActivityResult区分回调场景 */
private final static int REQUEST_CODE_NEW_NODE = 103;
- /**
- * Activity生命周期 - 页面创建阶段【核心初始化】
- * 执行时机:页面首次启动时调用,仅执行一次
- * 核心逻辑:加载页面布局、初始化所有核心资源与控件、绑定适配器与监听器、执行首次使用引导逻辑,为页面就绪做准备
- * @param savedInstanceState 页面重建时的状态数据,当前页面未使用该参数
- */
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- setContentView(R.layout.note_list); // 加载页面核心布局
- initResources(); // 初始化所有控件、适配器、监听器、状态变量,核心初始化方法
- setAppInfoFromRawRes(); // 执行首次使用引导,创建引导便签(仅首次打开应用触发)
+ setContentView(R.layout.note_list);
+ initResources();
+
+ /**
+ * Insert an introduction when user firstly use this application
+ */
+ setAppInfoFromRawRes();
+
+ // 加载保存的背景图片
+ loadSavedBackground();
}
- /**
- * Activity生命周期 - 页面返回结果回调
- * 执行时机:从{NoteEditActivity}编辑/新建便签返回当前页面时触发
- * 核心逻辑:判断返回结果为成功时,清空列表适配器的Cursor数据,触发重新查询,保证列表数据与最新编辑结果一致
- * @param requestCode 页面跳转时传入的请求码,区分新建/打开便签场景
- * @param resultCode 目标页面返回的结果码,RESULT_OK表示操作成功
- * @param data 目标页面返回的Intent数据,当前页面未使用该参数
- */
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- // 新建/打开便签操作成功返回,刷新列表数据
if (resultCode == RESULT_OK
&& (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) {
mNotesListAdapter.changeCursor(null);
} else {
+ // 处理背景更换结果
+ Log.d(TAG, "onActivityResult called, requestCode: " + requestCode + ", resultCode: " + resultCode);
+
+ if (resultCode == RESULT_OK) {
+ String imagePath = null;
+
+ switch (requestCode) {
+ case 1: // 从相册选择
+ if (data != null && data.getData() != null) {
+ // 获取图片路径
+ imagePath = getPathFromUri(data.getData());
+ Log.d(TAG, "Image path from gallery: " + imagePath);
+ }
+ break;
+ case 2: // 拍照
+ if (data != null && data.getExtras() != null) {
+ // 获取拍照的图片
+ android.graphics.Bitmap bitmap = (android.graphics.Bitmap) data.getExtras().get("data");
+ // 保存图片到本地
+ imagePath = saveBitmap(bitmap);
+ Log.d(TAG, "Image path from camera: " + imagePath);
+ }
+ break;
+ }
+
+ if (imagePath != null) {
+ // 保存图片路径到SharedPreferences
+ SharedPreferences sharedPreferences = getSharedPreferences("background", MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putString("background_path", imagePath);
+ editor.apply();
+
+ // 设置背景
+ setBackground(imagePath);
+ }
+ }
super.onActivityResult(requestCode, resultCode, data);
}
}
- /**
- * 核心初始化引导逻辑 - 首次使用应用创建引导便签
- * 设计意图:降低新用户使用门槛,自动展示应用核心功能与使用说明,提升用户体验
- * 核心规则:通过SharedPreference持久化标记,仅在应用首次打开时执行一次,避免重复创建引导便签
- * 实现逻辑:读取raw目录下的引导文本文件 → 封装为WorkingNote数据模型 → 保存到数据库 → 标记已创建引导
- */
private void setAppInfoFromRawRes() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
- // 已创建过引导便签,直接返回,避免重复执行
if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) {
StringBuilder sb = new StringBuilder();
InputStream in = null;
try {
- // 打开raw资源目录下的引导文本文件,该文件存储应用使用说明
in = getResources().openRawResource(R.raw.introduction);
if (in != null) {
- // 流式读取文本内容,拼接为完整的引导文案
InputStreamReader isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
char [] buf = new char[1024];
@@ -244,22 +275,20 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
e.printStackTrace();
return;
} finally {
- // 关闭输入流,释放系统资源,避免内存泄漏
if(in != null) {
try {
in.close();
} catch (IOException e) {
+ // TODO Auto-generated catch block
e.printStackTrace();
}
}
}
- // 创建空的工作便签模型:归属根文件夹、无小组件关联、红色背景(醒目)
WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER,
AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE,
ResourceParser.RED);
- note.setWorkingText(sb.toString()); // 为便签赋值引导文本内容
- // 保存便签到数据库成功后,标记已创建引导,持久化到偏好设置
+ note.setWorkingText(sb.toString());
if (note.saveNote()) {
sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit();
} else {
@@ -269,226 +298,512 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
}
- /**
- * Activity生命周期 - 页面启动阶段
- * 执行时机:页面创建完成后、用户可见前调用,每次页面恢复可见时都会执行
- * 核心逻辑:触发异步数据库查询,加载当前文件夹下的便签/文件夹列表数据,保证页面展示最新数据
- */
@Override
protected void onStart() {
super.onStart();
- startAsyncNotesListQuery(); // 启动异步查询,加载列表核心数据
+ startAsyncNotesListQuery();
}
- /**
- * 页面核心初始化方法 - 集中管理所有资源与控件的初始化
- * 设计意图:将所有初始化逻辑集中到该方法,避免分散在多个生命周期方法中,便于维护与扩展
- * 核心逻辑:获取内容解析器、初始化异步查询处理器、绑定核心控件、设置列表适配器与监听器、初始化状态变量
- */
private void initResources() {
- mContentResolver = this.getContentResolver(); // 获取应用的内容解析器,用于数据库操作
- mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); // 初始化异步查询处理器
- mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 默认选中根文件夹,展示根目录数据
- mNotesListView = (ListView) findViewById(R.id.notes_list); // 绑定核心列表控件
-
- // 为列表添加底部空白视图,避免列表数据为空时ListView塌陷,保证页面布局完整性
+ mContentResolver = this.getContentResolver();
+ mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver());
+ mCurrentFolderId = Notes.ID_ROOT_FOLDER;
+ mNotesListView = (ListView) findViewById(R.id.notes_list);
mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null),
null, false);
- mNotesListView.setOnItemClickListener(new OnListItemClickListener()); // 设置列表项点击监听器
- mNotesListView.setOnItemLongClickListener(this); // 设置列表项长按监听器
-
- mNotesListAdapter = new NotesListAdapter(this); // 初始化列表核心适配器
- mNotesListView.setAdapter(mNotesListAdapter); // 将适配器绑定到列表控件
-
- // 绑定新建便签按钮,设置点击与触摸监听器
+ mNotesListView.setOnItemClickListener(new OnListItemClickListener());
+ mNotesListView.setOnItemLongClickListener(this);
+ mNotesListView.setOnTouchListener(new OnDragTouchListener());
+
+ // 初始化宫格视图
+ mNotesGridView = (GridView) findViewById(R.id.notes_grid);
+ mNotesGridView.setOnItemClickListener(new OnListItemClickListener());
+ mNotesGridView.setOnItemLongClickListener(this);
+ mNotesGridView.setOnTouchListener(new OnDragTouchListener());
+
+ mNotesListAdapter = new NotesListAdapter(this);
+ mNotesListView.setAdapter(mNotesListAdapter);
mAddNewNote = (Button) findViewById(R.id.btn_new_note);
- mAddNewNote.setOnClickListener(this); // 点击事件:创建新便签
- mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); // 触摸事件:处理透明区域事件透传
-
- // 初始化事件分发相关变量,默认关闭事件透传
+ mAddNewNote.setOnClickListener(this);
+ mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener());
mDispatch = false;
mDispatchY = 0;
mOriginY = 0;
- mTitleBar = (TextView) findViewById(R.id.tv_title_bar); // 绑定标题栏控件
- mState = ListEditState.NOTE_LIST; // 初始化页面状态为根目录普通便签列表
- mModeCallBack = new ModeCallback(); // 初始化多选模式回调处理器
+ mTitleBar = (TextView) findViewById(R.id.tv_title_bar);
+ mState = ListEditState.NOTE_LIST;
+ mModeCallBack = new ModeCallback();
+
+ // 初始化搜索相关组件
+ mSearchBar = (LinearLayout) findViewById(R.id.search_bar);
+ mSearchEditText = (EditText) findViewById(R.id.et_search);
+ mSearchImageView = (ImageView) findViewById(R.id.iv_search);
+ mCancelImageView = (ImageView) findViewById(R.id.iv_cancel);
+
+ // 初始化省略号按钮
+ ImageView mMenuMoreImageView = (ImageView) findViewById(R.id.iv_menu_more);
+
+ // 设置搜索按钮点击事件
+ mSearchImageView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startSearch();
+ }
+ });
+
+ // 设置取消按钮点击事件
+ mCancelImageView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ cancelSearch();
+ }
+ });
+
+ // 设置省略号按钮点击事件
+ mMenuMoreImageView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showMoreOptionsMenu(v);
+ }
+ });
+
+ // 设置搜索框文本变化监听
+ mSearchEditText.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) {
+ mSearchQuery = s.toString();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+
+ // 设置搜索框回车键监听
+ mSearchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_SEARCH ||
+ (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN)) {
+ performSearch();
+ return true;
+ }
+ return false;
+ }
+ });
+
+ mIsSearching = false;
}
- /**
- * 内部核心回调类 - 列表多选模式完整处理器
- * 实现{ListView.MultiChoiceModeListener}:监听多选模式的创建、准备、销毁、选中状态变更;
- * 实现{OnMenuItemClickListener}:监听多选菜单的点击事件,处理批量删除/移动操作;
- * 核心职责:封装多选模式的所有逻辑,包括菜单加载、选中状态维护、全选/取消全选、批量操作执行、视图状态切换,
- * 是页面批量操作功能的核心实现类,与列表适配器联动完成所有多选相关逻辑。
- */
private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener {
- /** 多选模式顶部下拉菜单:封装全选/取消全选功能,展示当前选中数量,提升交互便捷性 */
private DropdownMenu mDropDownMenu;
- /** 系统多选模式ActionMode:控制顶部操作栏的创建与销毁,是多选模式的核心载体 */
private ActionMode mActionMode;
- /** 批量移动菜单项:根据当前场景控制显隐,通话记录文件夹下/无用户文件夹时隐藏该菜单 */
private MenuItem mMoveMenu;
- /**
- * 多选模式创建回调 - 多选功能初始化入口
- * 执行时机:用户长按便签触发多选模式时调用,仅执行一次
- * 核心逻辑:加载多选操作菜单、设置菜单点击监听、适配移动菜单显隐、创建自定义顶部视图、初始化下拉菜单、
- * 切换适配器多选状态、隐藏新建按钮,完成多选模式的所有初始化配置
- * @param mode 当前创建的多选模式ActionMode对象
- * @param menu 多选模式的操作菜单对象
- * @return boolean true表示创建成功,展示多选菜单
- */
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.delete).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.pin).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.unpin).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.lock).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.unlock).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.make_public).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.make_private).setOnMenuItemClickListener(this);
mMoveMenu = menu.findItem(R.id.move);
+
+ // 根据便签状态显示/隐藏锁和解锁菜单项
+ MenuItem lockItem = menu.findItem(R.id.lock);
+ MenuItem unlockItem = menu.findItem(R.id.unlock);
+
+ if (mFocusNoteDataItem != null && mFocusNoteDataItem.isLocked()) {
+ // 便签已锁定,显示解锁选项,隐藏加锁选项
+ lockItem.setVisible(false);
+ unlockItem.setVisible(true);
+ } else {
+ // 便签未锁定,显示加锁选项,隐藏解锁选项
+ lockItem.setVisible(true);
+ unlockItem.setVisible(false);
+ }
- // 移动菜单显隐规则:通话记录文件夹下的便签不可移动 + 无用户文件夹时无需移动,两种场景均隐藏
- if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER
- || DataUtils.getUserFolderCount(mContentResolver) == 0) {
+ // 根据便签状态显示/隐藏开放和取消开放菜单项
+ MenuItem makePublicItem = menu.findItem(R.id.make_public);
+ MenuItem makePrivateItem = menu.findItem(R.id.make_private);
+
+ boolean isPublic = mFocusNoteDataItem != null && mFocusNoteDataItem.isPublic();
+ makePublicItem.setVisible(!isPublic);
+ makePrivateItem.setVisible(isPublic);
+
+ // 在回收站中,显示"恢复"选项而不是"移动"选项
+ if (mState == ListEditState.TRASH_FOLDER) {
+ // 隐藏"移动"选项
mMoveMenu.setVisible(false);
+ // 添加"恢复"选项
+ MenuItem restoreMenu = menu.add(0, 100, 0, "恢复");
+ restoreMenu.setOnMenuItemClickListener(this);
} else {
- mMoveMenu.setVisible(true);
- mMoveMenu.setOnMenuItemClickListener(this); // 设置移动菜单点击监听
+ // 不在回收站中,使用原来的逻辑
+ if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER
+ || DataUtils.getUserFolderCount(mContentResolver) == 0) {
+ mMoveMenu.setVisible(false);
+ } else {
+ mMoveMenu.setVisible(true);
+ mMoveMenu.setOnMenuItemClickListener(this);
+ }
}
+ // 根据便签是否已置顶,显示或隐藏置顶/取消置顶菜单项
+ boolean isPinned = mFocusNoteDataItem.isPinned();
+ menu.findItem(R.id.pin).setVisible(!isPinned);
+ menu.findItem(R.id.unpin).setVisible(isPinned);
+
+ // 添加"发送给"菜单项
+ MenuItem sendToMenu = menu.add(0, 101, 0, "发送给");
+ sendToMenu.setOnMenuItemClickListener(this);
+
mActionMode = mode;
- mNotesListAdapter.setChoiceMode(true); // 通知适配器切换到多选模式,展示勾选框
- mNotesListView.setLongClickable(false); // 多选模式下禁用长按,避免重复触发多选逻辑
- mAddNewNote.setVisibility(View.GONE); // 隐藏新建按钮,避免干扰多选操作
+ mNotesListAdapter.setChoiceMode(true);
+ mNotesListView.setLongClickable(false);
+ mAddNewNote.setVisibility(View.GONE);
- // 加载自定义多选模式顶部视图,包含全选/取消全选的下拉菜单,提升交互体验
View customView = LayoutInflater.from(NotesListActivity.this).inflate(
R.layout.note_list_dropdown_menu, null);
mode.setCustomView(customView);
- // 初始化下拉菜单,绑定按钮与菜单资源
mDropDownMenu = new DropdownMenu(NotesListActivity.this,
(Button) customView.findViewById(R.id.selection_menu),
R.menu.note_list_dropdown);
- // 设置下拉菜单点击监听,处理全选/取消全选逻辑
mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
public boolean onMenuItemClick(MenuItem item) {
- // 切换全选状态:已全选则取消全选,未全选则执行全选
mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected());
- updateMenu(); // 同步更新菜单标题与按钮文本
+ updateMenu();
return true;
}
+
});
return true;
}
- /**
- * 多选菜单更新方法 - 同步选中状态与菜单展示内容
- * 核心逻辑:获取当前选中的便签数量,更新下拉菜单标题为「已选择 X 项」;根据全选状态,切换按钮文本为「全选」/「取消全选」,
- * 保证菜单展示内容与实际选中状态一致,提升交互准确性。
- */
private void updateMenu() {
int selectedCount = mNotesListAdapter.getSelectedCount();
- // 格式化选中数量文本,更新下拉菜单标题
+ // Update dropdown menu
String format = getResources().getString(R.string.menu_select_title, selectedCount);
mDropDownMenu.setTitle(format);
- // 根据全选状态切换按钮文本
MenuItem item = mDropDownMenu.findItem(R.id.action_select_all);
if (item != null) {
if (mNotesListAdapter.isAllSelected()) {
item.setChecked(true);
- item.setTitle(R.string.menu_deselect_all); // 已全选 → 显示取消全选
+ item.setTitle(R.string.menu_deselect_all);
} else {
item.setChecked(false);
- item.setTitle(R.string.menu_select_all); // 未全选 → 显示全选
+ item.setTitle(R.string.menu_select_all);
}
}
}
- /**
- * 多选模式准备回调 - 预留扩展接口
- * 执行时机:多选模式创建后、每次菜单刷新前调用,当前页面无额外逻辑,返回false即可
- */
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ // TODO Auto-generated method stub
return false;
}
- /**
- * 多选菜单项点击回调 - 预留扩展接口
- * 注:实际的菜单点击逻辑在{onMenuItemClickListener}中实现,当前方法仅返回false
- */
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ // TODO Auto-generated method stub
return false;
}
- /**
- * 多选模式销毁回调 - 恢复页面默认状态
- * 执行时机:用户退出多选模式(点击返回键/完成按钮)时调用
- * 核心逻辑:通知适配器退出多选模式、恢复列表项长按功能、重新显示新建便签按钮,将页面恢复到普通浏览状态
- */
public void onDestroyActionMode(ActionMode mode) {
- mNotesListAdapter.setChoiceMode(false); // 适配器退出多选模式,隐藏勾选框
- mNotesListView.setLongClickable(true); // 恢复列表项长按功能,允许再次触发多选
- mAddNewNote.setVisibility(View.VISIBLE); // 重新显示新建便签按钮
+ mNotesListAdapter.setChoiceMode(false);
+ mNotesListView.setLongClickable(true);
+ mAddNewNote.setVisibility(View.VISIBLE);
}
- /**
- * 主动结束多选模式方法 - 对外提供的退出接口
- * 核心作用:批量操作完成后(删除/移动),主动关闭多选模式,恢复页面默认状态,提升交互连贯性
- */
public void finishActionMode() {
mActionMode.finish();
}
- /**
- * 列表项选中状态变更回调 - 多选模式核心状态同步
- * 执行时机:用户勾选/取消勾选列表项时调用
- * 核心逻辑:将选中状态同步到列表适配器,更新适配器的选中状态映射,然后刷新菜单展示内容,
- * 保证视图展示的选中状态与实际数据一致。
- * @param mode 当前的多选模式ActionMode对象
- * @param position 发生状态变更的列表项位置
- * @param id 列表项对应的便签ID
- * @param checked 变更后的选中状态:true=选中,false=取消选中
- */
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
- boolean checked) {
- mNotesListAdapter.setCheckedItem(position, checked); // 同步选中状态到适配器
- updateMenu(); // 刷新菜单标题与按钮文本
+ boolean checked) {
+ mNotesListAdapter.setCheckedItem(position, checked);
+ updateMenu();
}
- /**
- * 多选菜单点击事件处理 - 批量操作核心执行逻辑
- * 核心职责:处理「删除」「移动」两个核心批量操作,包含前置校验、用户确认、业务逻辑执行,
- * 是多选模式的核心功能出口。
- * @param item 被点击的菜单项对象
- * @return boolean true表示事件已处理完成
- */
public boolean onMenuItemClick(MenuItem item) {
- // 前置校验:无选中项时,弹出Toast提示用户选择便签,避免无效操作
if (mNotesListAdapter.getSelectedCount() == 0) {
Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none),
Toast.LENGTH_SHORT).show();
return true;
}
+ // 获取选中的便签ID列表
+ HashSet selectedIds = mNotesListAdapter.getSelectedItemIds();
+
switch (item.getItemId()) {
+ case R.id.pin:
+ // 处理置顶逻辑
+ try {
+ // 获取当前最小的SORT_ORDER值,用于新置顶的便签
+ long currentTime = System.currentTimeMillis();
+
+ // 更新每个选中的便签
+ for (Long id : selectedIds) {
+ ContentValues pinValues = new ContentValues();
+ pinValues.put(NoteColumns.PINNED, 1);
+ // 设置为当前时间戳的负数,确保最新置顶的便签有最小的SORT_ORDER值,显示在最前面
+ pinValues.put(NoteColumns.SORT_ORDER, -currentTime);
+
+ int updatedRows = mContentResolver.update(
+ Notes.CONTENT_NOTE_URI,
+ pinValues,
+ "_id=?",
+ new String[]{String.valueOf(id)});
+ Log.d(TAG, "Updated " + updatedRows + " rows for pinning id: " + id);
+
+ // 为下一个便签设置更小的SORT_ORDER值
+ currentTime--;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error in pin operation: " + e.getMessage());
+ e.printStackTrace();
+ }
+
+ // 刷新列表
+ mModeCallBack.finishActionMode();
+ startAsyncNotesListQuery();
+ break;
+ case R.id.unpin:
+ // 处理取消置顶逻辑
+ try {
+ // 更新每个选中的便签
+ for (Long id : selectedIds) {
+ ContentValues unpinValues = new ContentValues();
+ unpinValues.put(NoteColumns.PINNED, 0);
+ // 重置SORT_ORDER为0,确保取消置顶后显示在下面
+ unpinValues.put(NoteColumns.SORT_ORDER, 0);
+
+ int updatedRows = mContentResolver.update(
+ Notes.CONTENT_NOTE_URI,
+ unpinValues,
+ "_id=?",
+ new String[]{String.valueOf(id)});
+ Log.d(TAG, "Updated " + updatedRows + " rows for unpinning id: " + id);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error in unpin operation: " + e.getMessage());
+ e.printStackTrace();
+ }
+
+ // 刷新列表
+ mModeCallBack.finishActionMode();
+ startAsyncNotesListQuery();
+ break;
case R.id.delete:
- // 批量删除:弹出确认对话框,提示删除数量,用户确认后执行删除逻辑,防止误删
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
builder.setTitle(getString(R.string.alert_title_delete));
builder.setIcon(android.R.drawable.ic_dialog_alert);
- builder.setMessage(getString(R.string.alert_message_delete_notes,
- mNotesListAdapter.getSelectedCount()));
- builder.setPositiveButton(android.R.string.ok,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog,
- int which) {
- batchDelete(); // 执行异步批量删除逻辑
- }
- });
- builder.setNegativeButton(android.R.string.cancel, null);
+
+ if (mState == ListEditState.TRASH_FOLDER) {
+ // 在回收站中,直接彻底删除
+ builder.setMessage(getString(R.string.alert_message_delete_notes,
+ mNotesListAdapter.getSelectedCount()));
+ builder.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ // 直接彻底删除
+ DataUtils.batchDeleteNotes(getContentResolver(), mNotesListAdapter.getSelectedItemIds());
+ mModeCallBack.finishActionMode();
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ } else {
+ // 不在回收站中,让用户选择删除方式
+ builder.setMessage("请选择删除方式");
+ builder.setPositiveButton("移动到回收站",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ batchDelete();
+ }
+ });
+ builder.setNeutralButton("直接删除",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ // 直接彻底删除
+ DataUtils.batchDeleteNotes(getContentResolver(), mNotesListAdapter.getSelectedItemIds());
+ mModeCallBack.finishActionMode();
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ }
builder.show();
break;
case R.id.move:
- // 批量移动:触发目标文件夹查询,展示文件夹选择对话框,用户选择后执行移动逻辑
startQueryDestinationFolders();
break;
+ case R.id.lock:
+ // 处理加锁逻辑
+ if (!NotesPreferenceActivity.isPasswordSet(NotesListActivity.this)) {
+ Toast.makeText(NotesListActivity.this,
+ R.string.preferences_password_empty,
+ Toast.LENGTH_SHORT).show();
+ // 引导用户设置密码
+ Intent intent = new Intent(NotesListActivity.this, NotesPreferenceActivity.class);
+ startActivity(intent);
+ break;
+ }
+
+ // 更新每个选中的便签
+ for (Long id : selectedIds) {
+ ContentValues lockValues = new ContentValues();
+ lockValues.put(NoteColumns.LOCKED, 1);
+
+ int updatedRows = mContentResolver.update(
+ Notes.CONTENT_NOTE_URI,
+ lockValues,
+ "_id=?",
+ new String[]{String.valueOf(id)});
+ Log.d(TAG, "Updated " + updatedRows + " rows for locking id: " + id);
+ }
+
+ // 刷新列表
+ mModeCallBack.finishActionMode();
+ startAsyncNotesListQuery();
+ break;
+ case R.id.unlock:
+ // 处理解锁逻辑
+ if (!NotesPreferenceActivity.isPasswordSet(NotesListActivity.this)) {
+ // 没有密码直接解锁
+ for (Long id : selectedIds) {
+ ContentValues unlockValues = new ContentValues();
+ unlockValues.put(NoteColumns.LOCKED, 0);
+
+ int updatedRows = mContentResolver.update(
+ Notes.CONTENT_NOTE_URI,
+ unlockValues,
+ "_id=?",
+ new String[]{String.valueOf(id)});
+ Log.d(TAG, "Updated " + updatedRows + " rows for unlocking id: " + id);
+ }
+
+ // 刷新列表
+ mModeCallBack.finishActionMode();
+ startAsyncNotesListQuery();
+ break;
+ }
+
+ // 显示密码输入对话框
+ View passwordView = LayoutInflater.from(NotesListActivity.this).inflate(R.layout.password_input_dialog, null);
+ final EditText passwordEdit = (EditText) passwordView.findViewById(R.id.password_input);
+
+ AlertDialog.Builder passwordDialogBuilder = new AlertDialog.Builder(NotesListActivity.this);
+ passwordDialogBuilder.setTitle(R.string.note_lock_password_prompt);
+ passwordDialogBuilder.setView(passwordView);
+
+ passwordDialogBuilder.setPositiveButton(getString(R.string.preferences_button_confirm), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ String password = passwordEdit.getText().toString();
+
+ if (password.equals(NotesPreferenceActivity.getPassword(NotesListActivity.this))) {
+ // 密码正确,解锁便签
+ for (Long id : selectedIds) {
+ ContentValues unlockValues = new ContentValues();
+ unlockValues.put(NoteColumns.LOCKED, 0);
+
+ int updatedRows = mContentResolver.update(
+ Notes.CONTENT_NOTE_URI,
+ unlockValues,
+ "_id=?",
+ new String[]{String.valueOf(id)});
+ Log.d(TAG, "Updated " + updatedRows + " rows for unlocking id: " + id);
+ }
+
+ // 刷新列表
+ mModeCallBack.finishActionMode();
+ startAsyncNotesListQuery();
+ } else {
+ // 密码错误,显示提示
+ Toast.makeText(NotesListActivity.this, R.string.note_lock_password_incorrect, Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ passwordDialogBuilder.setNegativeButton(android.R.string.cancel, null);
+ passwordDialogBuilder.show();
+ break;
+ case R.id.make_public:
+ // 处理开放便签逻辑
+ try {
+ // 更新每个选中的便签
+ for (Long id : selectedIds) {
+ ContentValues publicValues = new ContentValues();
+ publicValues.put(NoteColumns.PUBLIC, 1);
+
+ int updatedRows = mContentResolver.update(
+ Notes.CONTENT_NOTE_URI,
+ publicValues,
+ "_id=?",
+ new String[]{String.valueOf(id)});
+ Log.d(TAG, "Updated " + updatedRows + " rows for making public id: " + id);
+ }
+
+ // 刷新列表
+ mModeCallBack.finishActionMode();
+ startAsyncNotesListQuery();
+ } catch (Exception e) {
+ Log.e(TAG, "Error in make_public operation: " + e.getMessage());
+ e.printStackTrace();
+ }
+ break;
+ case R.id.make_private:
+ // 处理取消开放便签逻辑
+ try {
+ // 更新每个选中的便签
+ for (Long id : selectedIds) {
+ ContentValues privateValues = new ContentValues();
+ privateValues.put(NoteColumns.PUBLIC, 0);
+
+ int updatedRows = mContentResolver.update(
+ Notes.CONTENT_NOTE_URI,
+ privateValues,
+ "_id=?",
+ new String[]{String.valueOf(id)});
+ Log.d(TAG, "Updated " + updatedRows + " rows for making private id: " + id);
+ }
+
+ // 刷新列表
+ mModeCallBack.finishActionMode();
+ startAsyncNotesListQuery();
+ } catch (Exception e) {
+ Log.e(TAG, "Error in make_private operation: " + e.getMessage());
+ e.printStackTrace();
+ }
+ break;
+ case 100:
+ // 恢复选中的便签
+ // 恢复便签到原始文件夹
+ for (long noteId : selectedIds) {
+ // 查询原始文件夹ID
+ Cursor cursor = getContentResolver().query(
+ ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
+ new String[]{NoteColumns.ORIGIN_PARENT_ID},
+ null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ long originParentId = cursor.getLong(0);
+ cursor.close();
+ // 如果原始文件夹ID有效,则恢复到原始文件夹,否则恢复到根文件夹
+ long targetFolderId = (originParentId > 0) ? originParentId : Notes.ID_ROOT_FOLDER;
+ DataUtils.batchMoveToFolder(getContentResolver(), selectedIds, targetFolderId);
+ break; // 只需要处理一次,因为所有选中的便签都使用相同的targetFolderId
+ }
+ }
+ mModeCallBack.finishActionMode();
+ break;
+ case 101:
+ // 处理"发送给"功能
+ showSendToFriendsDialog(selectedIds);
+ break;
default:
return false;
}
@@ -496,52 +811,47 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
}
- /**
- * 内部触摸事件监听器 - 新建按钮透明区域事件透传处理器
- * 核心设计意图:新建便签按钮为悬浮半透明样式,包含大量透明区域,用户点击透明区域时,希望事件能透传到下方的ListView,
- * 支持列表滚动/点击列表项,提升交互体验,解决「透明区域点击无响应」的问题。
- * 核心逻辑:根据触摸坐标判断是否点击了透明区域 → 若是则校准坐标并将事件分发给ListView → 否则交由按钮默认处理,
- * 完美兼容按钮点击与列表交互。
- */
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();
-
- // 子文件夹状态下,扣除标题栏高度,校准触摸坐标,保证判断准确性
+ /**
+ * Minus TitleBar's height
+ */
if (mState == ListEditState.SUB_FOLDER) {
eventY -= mTitleBar.getHeight();
start -= mTitleBar.getHeight();
}
-
/**
- * 核心透明区域判断逻辑:基于UI设计的像素公式 y=-0.12x+94,判断触摸点是否在按钮的透明区域内
- * 该公式由UI设计稿确定,若按钮背景样式变更,需同步调整该公式
+ * HACKME:When click the transparent part of "New Note" button, dispatch
+ * the event to the list view behind this button. The transparent part of
+ * "New Note" button could be expressed by formula y=-0.12x+94(Unit:pixel)
+ * and the line top of the button. The coordinate based on left of the "New
+ * Note" button. The 94 represents maximum height of the transparent part.
+ * Notice that, if the background of the button changes, the formula should
+ * also change. This is very bad, just for the UI designer's strong requirement.
*/
if (event.getY() < (event.getX() * (-0.12) + 94)) {
- // 获取列表最后一个可见项(排除底部footer),判断是否在透明区域范围内
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); // 将事件分发给ListView
+ mOriginY = (int) event.getY();
+ mDispatchY = eventY;
+ event.setLocation(event.getX(), mDispatchY);
+ mDispatch = true;
+ return mNotesListView.dispatchTouchEvent(event);
}
}
break;
}
case MotionEvent.ACTION_MOVE: {
- // 事件分发状态下,同步移动坐标,继续将触摸事件分发给ListView,支持列表滑动
if (mDispatch) {
mDispatchY += (int) event.getY() - mOriginY;
event.setLocation(event.getX(), mDispatchY);
@@ -550,7 +860,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
break;
}
default: {
- // 触摸事件结束(抬起/取消),分发最后一次事件,重置分发标记,恢复默认状态
if (mDispatch) {
event.setLocation(event.getX(), mDispatchY);
mDispatch = false;
@@ -559,53 +868,84 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
break;
}
}
- return false; // 非透明区域点击,交由按钮默认处理(触发新建便签)
+ return false;
}
+
};
- /**
- * 核心异步查询方法 - 加载当前文件夹下的便签/文件夹列表
- * 设计意图:所有列表数据均通过该方法异步查询,避免主线程阻塞,保证页面滑动流畅,是页面数据加载的核心入口
- * 核心逻辑:根据当前文件夹是否为根目录,选择对应的查询条件 → 调用异步查询处理器执行查询 → 查询结果在回调中更新到适配器
- */
+ // 开始搜索
+ private void startSearch() {
+ mIsSearching = true;
+ mTitleBar.setVisibility(View.GONE);
+ mSearchEditText.setVisibility(View.VISIBLE);
+ mCancelImageView.setVisibility(View.VISIBLE);
+ mSearchImageView.setVisibility(View.GONE);
+ mSearchEditText.requestFocus();
+ showSoftInput();
+ }
+
+ // 取消搜索
+ private void cancelSearch() {
+ mIsSearching = false;
+ mSearchQuery = null;
+ mSearchEditText.setText("");
+ mSearchEditText.setVisibility(View.GONE);
+ mCancelImageView.setVisibility(View.GONE);
+ mSearchImageView.setVisibility(View.VISIBLE);
+ mTitleBar.setVisibility(mState == ListEditState.NOTE_LIST ? View.GONE : View.VISIBLE);
+ hideSoftInput(mSearchEditText);
+ mNotesListAdapter.setSearchQuery(null);
+ startAsyncNotesListQuery();
+ }
+
+ // 执行搜索
+ private void performSearch() {
+ if (!TextUtils.isEmpty(mSearchQuery)) {
+ hideSoftInput(mSearchEditText);
+ mNotesListAdapter.setSearchQuery(mSearchQuery);
+ startAsyncNotesListQuery();
+ }
+ }
+
private void startAsyncNotesListQuery() {
- // 拼接查询条件:根目录使用特殊复合条件,普通文件夹使用精准匹配条件
- String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION
- : NORMAL_SELECTION;
- // 启动异步数据库查询:指定查询标识、查询URI、查询字段、查询条件、条件参数、排序规则
+ String selection;
+ String[] selectionArgs;
+
+ if (mIsSearching && !TextUtils.isEmpty(mSearchQuery)) {
+ // 搜索模式:查询包含搜索关键词的便签
+ selection = NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " +
+ NoteColumns.SNIPPET + " LIKE ?";
+ String searchQuery = "%" + mSearchQuery + "%";
+ selectionArgs = new String[] { searchQuery };
+ } else {
+ // 普通模式:根据当前文件夹查询
+ selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_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.MODIFIED_DATE + " DESC");
+ Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, selectionArgs,
+ "CASE WHEN " + NoteColumns.ID + " = " + Notes.ID_TRASH_FOLER + " THEN 0 ELSE 1 END ASC, " +
+ NoteColumns.PINNED + " DESC," + NoteColumns.SORT_ORDER + " ASC," + NoteColumns.TYPE + " DESC," + mCurrentSortOrder);
}
- /**
- * 内部核心异步处理器 - 数据库异步操作完整封装
- * 继承安卓原生{AsyncQueryHandler},核心职责:封装数据库的异步查询、插入、更新、删除操作,
- * 将耗时的数据库操作放到子线程执行,查询结果通过回调返回主线程,避免主线程阻塞,是页面流畅性的核心保障。
- * 所有数据库操作均通过该类执行,包含列表数据查询、文件夹列表查询。
- */
private final class BackgroundQueryHandler extends AsyncQueryHandler {
public BackgroundQueryHandler(ContentResolver contentResolver) {
super(contentResolver);
}
- /**
- * 异步查询完成回调 - 数据库查询结果处理核心方法
- * 执行时机:异步查询任务完成后,由系统自动调用,运行在主线程
- * 核心逻辑:根据查询标识(Token)区分不同的查询任务 → 列表数据查询:更新适配器Cursor → 文件夹列表查询:展示文件夹选择菜单
- * @param token 异步查询任务的标识,区分不同查询类型
- * @param cookie 异步查询的附加参数,当前页面未使用
- * @param cursor 数据库查询结果游标,封装了查询到的所有数据
- */
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
switch (token) {
case FOLDER_NOTE_LIST_QUERY_TOKEN:
- mNotesListAdapter.changeCursor(cursor); // 更新列表适配器数据,刷新视图
+ mNotesListAdapter.changeCursor(cursor);
+ // 如果当前显示的是宫格模式,确保GridView也更新数据
+ if (mDisplayMode == DISPLAY_MODE_GRID) {
+ mNotesGridView.setAdapter(mNotesListAdapter);
+ }
break;
case FOLDER_LIST_QUERY_TOKEN:
- // 文件夹列表查询完成,展示文件夹选择对话框,供用户选择目标文件夹
if (cursor != null && cursor.getCount() > 0) {
showFolderListMenu(cursor);
} else {
@@ -618,196 +958,319 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
}
- /**
- * 文件夹选择对话框展示方法 - 批量移动便签的目标文件夹选择
- * 核心逻辑:将查询到的文件夹列表封装为适配器 → 展示对话框供用户选择 → 用户选择后执行批量移动逻辑 → 提示移动成功 → 退出多选模式
- * @param cursor 数据库查询到的文件夹列表游标,封装了所有可选择的文件夹数据
- */
private void showFolderListMenu(Cursor cursor) {
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
- builder.setTitle(R.string.menu_title_select_folder); // 设置对话框标题
- final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); // 初始化文件夹列表适配器
+ builder.setTitle(R.string.menu_title_select_folder);
+ final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor);
builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
+
public void onClick(DialogInterface dialog, int which) {
- // 执行批量移动逻辑:将选中的便签移至目标文件夹
DataUtils.batchMoveToFolder(mContentResolver,
mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which));
- // 弹出Toast提示,展示移动数量与目标文件夹名称,提升用户感知
Toast.makeText(
NotesListActivity.this,
getString(R.string.format_move_notes_to_folder,
mNotesListAdapter.getSelectedCount(),
adapter.getFolderName(NotesListActivity.this, which)),
Toast.LENGTH_SHORT).show();
- mModeCallBack.finishActionMode(); // 移动完成,退出多选模式
+ mModeCallBack.finishActionMode();
}
});
- builder.show(); // 显示文件夹选择对话框
+ builder.show();
}
- /**
- * 新建便签核心方法 - 跳转编辑页创建新便签
- * 核心逻辑:构建跳转Intent,标记为「新建/编辑」模式 → 传递当前文件夹ID,保证新便签归属当前文件夹 → 启动编辑页并等待返回结果
- * 设计意图:新便签默认归属用户当前浏览的文件夹,保持文件夹上下文一致性,提升用户体验
- */
private void createNewNote() {
Intent intent = new Intent(this, NoteEditActivity.class);
- intent.setAction(Intent.ACTION_INSERT_OR_EDIT); // 标记为新建/编辑模式
- intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); // 传递当前文件夹ID
- this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); // 启动编辑页,接收返回结果
+ intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
+ intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId);
+ this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE);
}
- /**
- * 批量删除便签核心方法 - 异步执行差异化删除逻辑
- * 核心设计:通过{AsyncTask}异步执行删除操作,避免主线程阻塞;区分同步/非同步模式,执行差异化删除逻辑,保证数据安全;
- * 删除完成后自动更新关联的桌面小组件,保证数据一致性。
- * 核心规则:非同步模式 → 直接物理删除便签;同步模式 → 将便签移至回收站,支持恢复,防止误删导致数据丢失。
- */
private void batchDelete() {
new AsyncTask>() {
- /**
- * 后台执行方法 - 耗时删除逻辑处理,运行在子线程
- * 核心逻辑:获取选中便签关联的小组件信息 → 执行差异化删除逻辑 → 返回小组件信息供后续更新
- * @param unused 无传入参数
- * @return HashSet 选中便签关联的所有小组件属性集合
- */
protected HashSet doInBackground(Void... unused) {
- HashSet widgets = mNotesListAdapter.getSelectedWidget();
- // 非同步模式:直接物理删除选中的便签
- if (!isSyncMode()) {
- if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter
- .getSelectedItemIds())) {
- } else {
- Log.e(TAG, "Delete notes error, should not happens");
+ HashSet selectedIds = mNotesListAdapter.getSelectedItemIds();
+ HashSet allWidgets = new HashSet<>();
+
+ // 分离便签和文件夹
+ HashSet noteIds = new HashSet<>();
+ HashSet folderIds = new HashSet<>();
+
+ Cursor cursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
+ new String[]{NoteColumns.ID, NoteColumns.TYPE},
+ NoteColumns.ID + " IN (" + TextUtils.join(",", selectedIds) + ")",
+ null, null);
+
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ int type = cursor.getInt(1);
+ if (type == Notes.TYPE_NOTE) {
+ noteIds.add(id);
+ } else if (type == Notes.TYPE_FOLDER) {
+ folderIds.add(id);
+ }
}
- } else {
- // 同步模式:将便签移至回收站,而非直接删除,适配同步逻辑,支持数据恢复
- if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter
- .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) {
+ cursor.close();
+ }
+
+ // 处理便签:直接移至回收站
+ if (!noteIds.isEmpty()) {
+ if (!DataUtils.batchMoveToFolder(mContentResolver, noteIds, Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happens");
}
+ allWidgets.addAll(mNotesListAdapter.getSelectedWidget());
+ }
+
+ // 处理文件夹
+ for (long folderId : folderIds) {
+ // 查询文件夹中的便签数量
+ Cursor folderCursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
+ new String[]{"COUNT(*)"},
+ NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "=?",
+ new String[]{String.valueOf(folderId), String.valueOf(Notes.TYPE_NOTE)},
+ null);
+
+ int noteCount = 0;
+ if (folderCursor != null) {
+ if (folderCursor.moveToFirst()) {
+ noteCount = folderCursor.getInt(0);
+ }
+ folderCursor.close();
+ }
+
+ if (noteCount > 0) {
+ // 文件夹中有便签,将便签移至回收站
+ Cursor noteCursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
+ new String[]{NoteColumns.ID},
+ NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "=?",
+ new String[]{String.valueOf(folderId), String.valueOf(Notes.TYPE_NOTE)},
+ null);
+
+ HashSet folderNoteIds = new HashSet<>();
+ if (noteCursor != null) {
+ while (noteCursor.moveToNext()) {
+ folderNoteIds.add(noteCursor.getLong(0));
+ }
+ noteCursor.close();
+ }
+
+ // 移动便签到回收站
+ if (!DataUtils.batchMoveToFolder(mContentResolver, folderNoteIds, Notes.ID_TRASH_FOLER)) {
+ Log.e(TAG, "Move folder notes to trash error");
+ }
+
+ // 获取文件夹中便签的widget信息
+ HashSet folderWidgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId);
+ if (folderWidgets != null) {
+ allWidgets.addAll(folderWidgets);
+ }
+
+ // 删除空文件夹
+ HashSet emptyFolderIds = new HashSet<>();
+ emptyFolderIds.add(folderId);
+ DataUtils.batchDeleteNotes(mContentResolver, emptyFolderIds);
+ } else {
+ // 文件夹为空,直接删除
+ HashSet emptyFolderIds = new HashSet<>();
+ emptyFolderIds.add(folderId);
+ DataUtils.batchDeleteNotes(mContentResolver, emptyFolderIds);
+ }
}
- return widgets;
+
+ return allWidgets;
}
- /**
- * 主线程回调方法 - 删除完成后的UI更新与联动,运行在主线程
- * 核心逻辑:遍历关联的小组件,发送广播更新小组件内容 → 退出多选模式,恢复页面默认状态
- * @param widgets 后台方法返回的小组件属性集合
- */
@Override
protected void onPostExecute(HashSet widgets) {
if (widgets != null) {
for (AppWidgetAttribute widget : widgets) {
if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
- updateWidget(widget.widgetId, widget.widgetType); // 更新桌面小组件
+ updateWidget(widget.widgetId, widget.widgetType);
}
}
}
- mModeCallBack.finishActionMode(); // 退出多选模式
+ mModeCallBack.finishActionMode();
}
}.execute();
}
- /**
- * 删除文件夹核心方法 - 适配同步模式的文件夹删除逻辑
- * 核心规则:根文件夹不可删除(系统保护);非同步模式 → 直接删除文件夹及子项;同步模式 → 移至回收站;
- * 删除后自动更新关联的桌面小组件,保证数据一致性。
- * @param folderId 要删除的文件夹ID
- */
private void deleteFolder(long folderId) {
- // 系统保护:根文件夹为核心目录,禁止删除,防止应用数据异常
if (folderId == Notes.ID_ROOT_FOLDER) {
Log.e(TAG, "Wrong folder id, should not happen " + folderId);
return;
}
- // 构建待删除的文件夹ID集合
- HashSet ids = new HashSet();
- ids.add(folderId);
- // 获取该文件夹下所有便签关联的桌面小组件信息,用于后续更新
- HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver,
- folderId);
-
- // 非同步模式:直接物理删除文件夹
- if (!isSyncMode()) {
- DataUtils.batchDeleteNotes(mContentResolver, ids);
- } else {
- // 同步模式:将文件夹移至回收站,适配同步逻辑
- DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER);
- }
-
- // 更新关联的桌面小组件,保证小组件数据与页面一致
- if (widgets != null) {
- for (AppWidgetAttribute widget : widgets) {
- if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
- && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
- updateWidget(widget.widgetId, widget.widgetType);
- }
- }
+ // 查询文件夹中的便签数量
+ Cursor folderCursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
+ new String[]{"COUNT(*)"},
+ NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "=?",
+ new String[]{String.valueOf(folderId), String.valueOf(Notes.TYPE_NOTE)},
+ null);
+
+ final int noteCount = folderCursor != null && folderCursor.moveToFirst() ? folderCursor.getInt(0) : 0;
+ if (folderCursor != null) {
+ folderCursor.close();
}
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(getString(R.string.alert_title_delete));
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setMessage("请选择删除方式");
+
+ // 移动到回收站选项
+ builder.setPositiveButton("移动到回收站",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (noteCount > 0) {
+ // 文件夹中有便签,将便签移至回收站
+ Cursor noteCursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
+ new String[]{NoteColumns.ID},
+ NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "=?",
+ new String[]{String.valueOf(folderId), String.valueOf(Notes.TYPE_NOTE)},
+ null);
+
+ HashSet folderNoteIds = new HashSet<>();
+ if (noteCursor != null) {
+ while (noteCursor.moveToNext()) {
+ folderNoteIds.add(noteCursor.getLong(0));
+ }
+ noteCursor.close();
+ }
+
+ // 移动便签到回收站
+ if (!DataUtils.batchMoveToFolder(mContentResolver, folderNoteIds, Notes.ID_TRASH_FOLER)) {
+ Log.e(TAG, "Move folder notes to trash error");
+ }
+
+ // 获取文件夹中便签的widget信息
+ HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId);
+ if (widgets != null) {
+ for (AppWidgetAttribute widget : widgets) {
+ if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
+ && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
+ updateWidget(widget.widgetId, widget.widgetType);
+ }
+ }
+ }
+ }
+
+ // 删除空文件夹
+ HashSet emptyFolderIds = new HashSet<>();
+ emptyFolderIds.add(folderId);
+ DataUtils.batchDeleteNotes(mContentResolver, emptyFolderIds);
+ }
+ });
+
+ // 直接删除选项
+ builder.setNeutralButton("直接删除",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (noteCount > 0) {
+ // 文件夹中有便签,先删除所有便签
+ Cursor noteCursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
+ new String[]{NoteColumns.ID},
+ NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "=?",
+ new String[]{String.valueOf(folderId), String.valueOf(Notes.TYPE_NOTE)},
+ null);
+
+ HashSet folderNoteIds = new HashSet<>();
+ if (noteCursor != null) {
+ while (noteCursor.moveToNext()) {
+ folderNoteIds.add(noteCursor.getLong(0));
+ }
+ noteCursor.close();
+ }
+
+ // 直接彻底删除便签
+ DataUtils.batchDeleteNotes(mContentResolver, folderNoteIds);
+ }
+
+ // 删除文件夹
+ HashSet folderIds = new HashSet<>();
+ folderIds.add(folderId);
+ DataUtils.batchDeleteNotes(mContentResolver, folderIds);
+ }
+ });
+
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.show();
}
- /**
- * 打开便签核心方法 - 跳转编辑页查看/编辑已有便签
- * 核心逻辑:构建跳转Intent,标记为「查看」模式 → 传递便签唯一ID → 启动编辑页并等待返回结果,
- * 编辑完成后列表会自动刷新,保证数据一致性。
- * @param data 要打开的便签数据模型,包含便签ID等核心信息
- */
private void openNode(NoteItemData data) {
- Intent intent = new Intent(this, NoteEditActivity.class);
- intent.setAction(Intent.ACTION_VIEW); // 标记为查看模式,区别于新建便签
- intent.putExtra(Intent.EXTRA_UID, data.getId()); // 传递便签唯一ID
- this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); // 启动编辑页
+ if (data.isLocked()) {
+ // 便签被锁定,需要密码验证
+ View passwordView = LayoutInflater.from(this).inflate(R.layout.password_input_dialog, null);
+ final EditText passwordEdit = (EditText) passwordView.findViewById(R.id.password_input);
+ final NoteItemData noteData = data;
+
+ AlertDialog.Builder passwordDialogBuilder = new AlertDialog.Builder(this);
+ passwordDialogBuilder.setTitle(R.string.note_lock_password_prompt);
+ passwordDialogBuilder.setView(passwordView);
+
+ passwordDialogBuilder.setPositiveButton(getString(R.string.preferences_button_confirm), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ String password = passwordEdit.getText().toString();
+
+ if (password.equals(NotesPreferenceActivity.getPassword(NotesListActivity.this))) {
+ // 密码正确,打开便签
+ Intent intent = new Intent(NotesListActivity.this, NoteEditActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.putExtra(Intent.EXTRA_UID, noteData.getId());
+ NotesListActivity.this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE);
+ } else {
+ // 密码错误
+ Toast.makeText(NotesListActivity.this,
+ R.string.note_lock_password_incorrect,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ passwordDialogBuilder.setNegativeButton(getString(R.string.preferences_button_cancel), null);
+ passwordDialogBuilder.show();
+ } else {
+ // 便签未锁定,直接打开
+ Intent intent = new Intent(this, NoteEditActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.putExtra(Intent.EXTRA_UID, data.getId());
+ this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE);
+ }
}
- /**
- * 打开文件夹核心方法 - 进入文件夹层级,加载子项数据
- * 核心逻辑:更新当前文件夹ID → 异步查询该文件夹下的子项 → 切换页面状态 → 适配视图展示(标题栏、新建按钮)→ 更新标题栏文本,
- * 完成文件夹层级的切换,是页面层级导航的核心方法。
- * @param data 要打开的文件夹数据模型,包含文件夹ID、名称等核心信息
- */
private void openFolder(NoteItemData data) {
- mCurrentFolderId = data.getId(); // 更新当前文件夹ID,标记用户浏览层级
- startAsyncNotesListQuery(); // 异步查询该文件夹下的所有子项
-
- // 切换页面状态:通话记录文件夹为专属状态,其他文件夹为子文件夹状态
+ mCurrentFolderId = data.getId();
+ startAsyncNotesListQuery();
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mState = ListEditState.CALL_RECORD_FOLDER;
- mAddNewNote.setVisibility(View.GONE); // 通话记录文件夹下隐藏新建按钮,禁止创建普通便签
+ mAddNewNote.setVisibility(View.GONE);
+ } else if (data.getId() == Notes.ID_TRASH_FOLER) {
+ mState = ListEditState.TRASH_FOLDER;
+ mAddNewNote.setVisibility(View.GONE);
} else {
mState = ListEditState.SUB_FOLDER;
}
-
- // 更新标题栏展示:通话记录文件夹显示固定名称,其他文件夹显示自定义名称
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mTitleBar.setText(R.string.call_record_folder_name);
+ } else if (data.getId() == Notes.ID_TRASH_FOLER) {
+ mTitleBar.setText("回收站");
} else {
mTitleBar.setText(data.getSnippet());
}
- mTitleBar.setVisibility(View.VISIBLE); // 显示标题栏,展示当前文件夹名称
+ mTitleBar.setVisibility(View.VISIBLE);
}
- /**
- * 页面点击事件统一处理 - 实现{OnClickListener}接口
- * 核心职责:处理页面所有控件的点击事件,当前仅处理新建便签按钮的点击,触发新建便签逻辑
- * @param v 被点击的视图控件对象
- */
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_new_note:
- createNewNote(); // 点击新建按钮,创建新便签
+ createNewNote();
break;
default:
break;
}
}
- /**
- * 软键盘强制显示方法 - 用于文件夹名称编辑场景
- * 核心逻辑:通过系统输入法服务,强制弹出软键盘,无需用户手动点击输入框,提升交互便捷性,适用于对话框打开后自动聚焦输入的场景。
- */
private void showSoftInput() {
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null) {
@@ -815,102 +1278,300 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
}
- /**
- * 软键盘隐藏方法 - 适配指定视图的软键盘关闭
- * 核心逻辑:通过视图的WindowToken,精准关闭当前页面的软键盘,避免影响其他应用,适用于输入完成后自动关闭软键盘的场景。
- * @param view 用于获取WindowToken的视图,通常为输入框控件
- */
private void hideSoftInput(View view) {
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
- /**
- * 文件夹创建/重命名对话框展示方法 - 核心文件夹编辑逻辑
- * 核心职责:统一处理文件夹的创建与重命名,包含输入框初始化、名称唯一性校验、数据库操作、软键盘控制,
- * 是文件夹管理的核心交互入口。
- * @param create boolean true=创建新文件夹,false=修改已有文件夹名称
- */
- private void showCreateOrModifyFolderDialog(final boolean create) {
- final AlertDialog.Builder builder = new AlertDialog.Builder(this);
- // 加载对话框布局,包含输入框用于输入文件夹名称
- View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null);
- final EditText etName = (EditText) view.findViewById(R.id.et_foler_name);
- showSoftInput(); // 打开对话框后,强制弹出软键盘,提升输入便捷性
-
- // 重命名文件夹场景:初始化输入框为当前文件夹名称,设置对话框标题为「重命名文件夹」
- if (!create) {
- if (mFocusNoteDataItem != null) {
- etName.setText(mFocusNoteDataItem.getSnippet());
+ // 显示更多选项菜单
+ private void showMoreOptionsMenu(View anchorView) {
+ PopupMenu popupMenu = new PopupMenu(this, anchorView);
+
+ // 添加菜单项,移除分隔线以消除空白项
+ MenuItem listModeItem = popupMenu.getMenu().add(0, DISPLAY_MODE_LIST, 1, "列表式呈现");
+ MenuItem gridModeItem = popupMenu.getMenu().add(0, DISPLAY_MODE_GRID, 2, "宫格图呈现");
+ MenuItem sortByCreateDateItem = popupMenu.getMenu().add(0, 3, 3, "按照创建时间排序");
+ MenuItem sortByModifiedDateItem = popupMenu.getMenu().add(0, 4, 4, "按照修改日期排序");
+ MenuItem friendItem = popupMenu.getMenu().add(0, 7, 5, "好友");
+ MenuItem switchAccountItem = popupMenu.getMenu().add(0, 6, 6, "切换账号");
+
+ // 根据当前状态设置菜单项的勾选状态
+ listModeItem.setChecked(mDisplayMode == DISPLAY_MODE_LIST);
+ gridModeItem.setChecked(mDisplayMode == DISPLAY_MODE_GRID);
+ listModeItem.setCheckable(true);
+ gridModeItem.setCheckable(true);
+
+ // 设置菜单项点击监听器
+ popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ switch (item.getItemId()) {
+ case DISPLAY_MODE_LIST:
+ // 切换到列表模式
+ mDisplayMode = DISPLAY_MODE_LIST;
+ updateDisplayMode();
+ item.setChecked(true);
+ break;
+ case DISPLAY_MODE_GRID:
+ // 切换到宫格模式
+ mDisplayMode = DISPLAY_MODE_GRID;
+ updateDisplayMode();
+ item.setChecked(true);
+ break;
+ case 3:
+ // 按照创建时间排序
+ mCurrentSortOrder = SORT_BY_CREATE_DATE;
+ startAsyncNotesListQuery();
+ break;
+ case 4:
+ // 按照修改日期排序
+ mCurrentSortOrder = SORT_BY_MODIFIED_DATE;
+ startAsyncNotesListQuery();
+ break;
+ case 6:
+ // 处理切换账号
+ showSwitchAccountDialog();
+ break;
+ case 7:
+ // 处理好友功能
+ Intent friendIntent = new Intent(NotesListActivity.this, net.micode.notes.ui.FriendManagementActivity.class);
+ startActivity(friendIntent);
+ break;
+ }
+ return true;
+ }
+ });
+
+ // 显示弹出菜单
+ popupMenu.show();
+ }
+
+ /**
+ * 显示切换账号对话框
+ */
+ private void showSwitchAccountDialog() {
+ // 准备用户列表
+ final ArrayList userIds = new ArrayList<>();
+ final ArrayList usernames = new ArrayList<>();
+
+ try {
+ // 直接在主线程中查询所有用户,数据库操作简单,不会导致明显卡顿
+ net.micode.notes.data.NotesDatabaseHelper helper = net.micode.notes.data.NotesDatabaseHelper.getInstance(getApplicationContext());
+ if (helper == null) {
+ Toast.makeText(this, "数据库初始化失败", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ android.database.sqlite.SQLiteDatabase db = helper.getReadableDatabase();
+ if (db == null || !db.isOpen()) {
+ Toast.makeText(this, "数据库连接失败", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ android.database.Cursor cursor = null;
+ try {
+ // 查询所有用户
+ cursor = db.query(
+ net.micode.notes.data.NotesDatabaseHelper.TABLE.USER,
+ new String[]{net.micode.notes.data.Users.UserColumns.ID, net.micode.notes.data.Users.UserColumns.USERNAME},
+ null,
+ null,
+ null,
+ null,
+ null
+ );
+
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ userIds.add(cursor.getLong(0));
+ usernames.add(cursor.getString(1));
+ } while (cursor.moveToNext());
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "查询账号失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
+ return;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "获取账号列表失败", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (userIds.isEmpty()) {
+ Toast.makeText(this, "没有可用的账号", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 显示用户列表对话框
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("选择账号");
+ builder.setItems(usernames.toArray(new String[0]), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ long selectedUserId = userIds.get(which);
+ String selectedUsername = usernames.get(which);
+
+ // 获取当前用户ID
+ long currentUserId = net.micode.notes.tool.UserManager.getInstance(NotesListActivity.this).getCurrentUserId();
+
+ if (selectedUserId == currentUserId) {
+ // 如果选择的是当前账号,显示提示
+ Toast.makeText(NotesListActivity.this, "您已经在" + selectedUsername + "账号上", Toast.LENGTH_SHORT).show();
+ } else {
+ // 否则,显示密码输入对话框
+ showPasswordInputDialog(selectedUserId, selectedUsername);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(NotesListActivity.this, "账号选择失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ builder.show();
+ }
+
+ /**
+ * 显示密码输入对话框
+ */
+ private void showPasswordInputDialog(final long userId, final String username) {
+ try {
+ // 加载密码输入对话框布局
+ View passwordView = LayoutInflater.from(this).inflate(R.layout.password_input_dialog, null);
+ final EditText passwordEdit = (EditText) passwordView.findViewById(R.id.password_input);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("输入密码");
+ builder.setMessage("请输入" + username + "的密码");
+ builder.setView(passwordView);
+
+ // 设置确定按钮点击事件
+ builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ try {
+ String password = passwordEdit.getText().toString();
+
+ // 验证密码
+ if (net.micode.notes.tool.UserManager.getInstance(NotesListActivity.this).validatePassword(userId, password)) {
+ // 密码正确,切换账号
+ net.micode.notes.tool.UserManager.getInstance(NotesListActivity.this).setCurrentUser(userId);
+
+ // 刷新便签列表
+ startAsyncNotesListQuery();
+
+ // 显示切换成功提示
+ Toast.makeText(NotesListActivity.this, "已切换到" + username + "账号", Toast.LENGTH_SHORT).show();
+ } else {
+ // 密码错误,显示提示
+ Toast.makeText(NotesListActivity.this, "密码错误", Toast.LENGTH_SHORT).show();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(NotesListActivity.this, "切换账号失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ // 设置取消按钮点击事件
+ builder.setNegativeButton(android.R.string.cancel, null);
+
+ // 显示对话框
+ builder.show();
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "打开密码输入框失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ // 更新显示模式
+ private void updateDisplayMode() {
+ if (mDisplayMode == DISPLAY_MODE_LIST) {
+ // 切换到列表模式
+ mNotesListView.setVisibility(View.VISIBLE);
+ mNotesGridView.setVisibility(View.GONE);
+ } else {
+ // 切换到宫格模式
+ mNotesListView.setVisibility(View.GONE);
+ mNotesGridView.setVisibility(View.VISIBLE);
+ mNotesGridView.setAdapter(mNotesListAdapter);
+ }
+ }
+
+ private void showCreateOrModifyFolderDialog(final boolean create) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null);
+ final EditText etName = (EditText) view.findViewById(R.id.et_foler_name);
+ showSoftInput();
+ if (!create) {
+ if (mFocusNoteDataItem != null) {
+ etName.setText(mFocusNoteDataItem.getSnippet());
builder.setTitle(getString(R.string.menu_folder_change_name));
} else {
Log.e(TAG, "The long click data item is null");
return;
}
} else {
- // 创建文件夹场景:清空输入框,设置对话框标题为「创建文件夹」
etName.setText("");
builder.setTitle(this.getString(R.string.menu_create_folder));
}
- // 设置对话框按钮:确定按钮先不绑定点击事件(自定义逻辑),取消按钮点击时隐藏软键盘
builder.setPositiveButton(android.R.string.ok, null);
builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
- hideSoftInput(etName); // 取消操作,隐藏软键盘
+ hideSoftInput(etName);
}
});
- // 显示对话框,并自定义确定按钮的点击逻辑,避免默认点击直接关闭对话框
final Dialog dialog = builder.setView(view).show();
final Button positive = (Button)dialog.findViewById(android.R.id.button1);
positive.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
- hideSoftInput(etName); // 输入完成,隐藏软键盘
+ hideSoftInput(etName);
String name = etName.getText().toString();
-
- // 核心校验:文件夹名称唯一性校验,已存在则提示用户并选中输入框文本,避免重复创建同名文件夹
if (DataUtils.checkVisibleFolderName(mContentResolver, name)) {
Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name),
Toast.LENGTH_LONG).show();
- etName.setSelection(0, etName.length()); // 选中输入框文本,便于用户修改
+ etName.setSelection(0, etName.length());
return;
}
-
- // 重命名文件夹逻辑:名称非空时,更新数据库中该文件夹的名称与本地修改标记
if (!create) {
if (!TextUtils.isEmpty(name)) {
ContentValues values = new ContentValues();
- values.put(NoteColumns.SNIPPET, name); // 更新文件夹名称
- values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); // 标记为文件夹类型
- values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记本地修改,适配同步逻辑
- // 执行数据库更新操作,精准更新指定ID的文件夹
+ values.put(NoteColumns.SNIPPET, name);
+ values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
+ values.put(NoteColumns.LOCAL_MODIFIED, 1);
mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID
+ "=?", new String[] {
- String.valueOf(mFocusNoteDataItem.getId())
+ String.valueOf(mFocusNoteDataItem.getId())
});
}
} else if (!TextUtils.isEmpty(name)) {
- // 创建文件夹逻辑:名称非空时,插入新文件夹到数据库,归属根目录
ContentValues values = new ContentValues();
- values.put(NoteColumns.SNIPPET, name); // 设置文件夹名称
- values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); // 标记为文件夹类型
- mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); // 插入新文件夹
+ values.put(NoteColumns.SNIPPET, name);
+ values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
+ mContentResolver.insert(Notes.CONTENT_NOTE_URI, values);
}
- dialog.dismiss(); // 操作完成,关闭对话框
+ dialog.dismiss();
}
});
- // 初始化确定按钮状态:输入框为空时禁用,避免创建空名称的文件夹
if (TextUtils.isEmpty(etName.getText())) {
positive.setEnabled(false);
}
/**
- * 输入框文本变化监听:实时校验输入内容,为空时禁用确定按钮,非空时启用,
- * 从交互层面避免用户输入空名称,提升数据有效性。
+ * When the name edit text is null, disable the positive button
*/
etName.addTextChangedListener(new TextWatcher() {
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // TODO Auto-generated method stub
+
+ }
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (TextUtils.isEmpty(etName.getText())) {
@@ -920,28 +1581,25 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
}
- public void afterTextChanged(Editable s) {}
+ public void afterTextChanged(Editable s) {
+ // TODO Auto-generated method stub
+
+ }
});
}
- /**
- * 返回键事件重写 - 适配文件夹层级的返回逻辑
- * 核心设计意图:用户点击返回键时,根据当前页面状态执行差异化逻辑,子文件夹返回根目录,根目录退出应用,
- * 实现「层级导航」的交互逻辑,符合用户使用习惯。
- * 核心规则:子文件夹状态 → 返回根目录;通话记录文件夹 → 返回根目录并恢复新建按钮;根目录 → 执行系统默认返回逻辑。
- */
@Override
public void onBackPressed() {
switch (mState) {
case SUB_FOLDER:
- // 子文件夹状态:返回根目录,重置状态,刷新列表,隐藏标题栏
+ case TRASH_FOLDER:
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
startAsyncNotesListQuery();
mTitleBar.setVisibility(View.GONE);
+ mAddNewNote.setVisibility(View.VISIBLE);
break;
case CALL_RECORD_FOLDER:
- // 通话记录文件夹状态:返回根目录,重置状态,恢复新建按钮,隐藏标题栏,刷新列表
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
mAddNewNote.setVisibility(View.VISIBLE);
@@ -949,7 +1607,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
startAsyncNotesListQuery();
break;
case NOTE_LIST:
- // 根目录状态:执行系统默认返回逻辑,退出应用
super.onBackPressed();
break;
default:
@@ -957,16 +1614,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
}
- /**
- * 桌面小组件更新核心方法 - 发送广播同步小组件数据
- * 核心设计意图:便签数据变更后,必须同步更新关联的桌面小组件,保证应用内数据与桌面展示数据一致,是小组件联动的核心方法。
- * 核心逻辑:根据小组件类型,指定对应的广播接收者 → 封装要更新的小组件ID → 发送广播通知小组件刷新内容。
- * @param appWidgetId 要更新的小组件系统唯一标识ID
- * @param appWidgetType 小组件类型:2x/4x,对应不同的广播接收者
- */
private void updateWidget(int appWidgetId, int appWidgetType) {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
- // 根据小组件类型,绑定对应的广播接收者,保证精准更新
if (appWidgetType == Notes.TYPE_WIDGET_2X) {
intent.setClass(this, NoteWidgetProvider_2x.class);
} else if (appWidgetType == Notes.TYPE_WIDGET_4X) {
@@ -976,37 +1625,25 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
return;
}
- // 传递要更新的小组件ID数组,支持批量更新
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {
- appWidgetId
+ appWidgetId
});
- sendBroadcast(intent); // 发送广播,通知小组件执行刷新
- setResult(RESULT_OK, intent); // 设置返回结果,标记更新成功
+ sendBroadcast(intent);
+ setResult(RESULT_OK, intent);
}
- /**
- * 文件夹上下文菜单创建监听器 - 长按文件夹时展示右键菜单
- * 核心职责:创建文件夹的右键菜单,包含「查看」「删除」「重命名」三个核心操作,菜单标题为文件夹名称,
- * 是文件夹的核心右键交互入口。
- */
private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() {
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
if (mFocusNoteDataItem != null) {
- menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); // 菜单标题为文件夹名称
- menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); // 查看文件夹
- 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); // 重命名文件夹
+ menu.setHeaderTitle(mFocusNoteDataItem.getSnippet());
+ menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view);
+ 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);
}
}
};
- /**
- * 上下文菜单关闭回调 - 内存泄漏防护处理
- * 执行时机:上下文菜单关闭时调用
- * 核心逻辑:清空列表的上下文菜单创建监听器,避免监听器持有页面引用导致内存泄漏,提升应用稳定性。
- * @param menu 被关闭的上下文菜单对象
- */
@Override
public void onContextMenuClosed(Menu menu) {
if (mNotesListView != null) {
@@ -1015,26 +1652,17 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
super.onContextMenuClosed(menu);
}
- /**
- * 上下文菜单项选择处理 - 文件夹右键菜单的核心逻辑执行
- * 核心职责:处理文件夹右键菜单的「查看」「删除」「重命名」操作,包含前置校验、用户确认、业务逻辑执行,
- * 是文件夹右键交互的核心出口。
- * @param item 被选中的菜单项对象
- * @return boolean true表示事件已处理完成
- */
@Override
public boolean onContextItemSelected(MenuItem item) {
- // 前置校验:无聚焦的文件夹数据,直接返回,避免空指针异常
if (mFocusNoteDataItem == null) {
Log.e(TAG, "The long click data item is null");
return false;
}
switch (item.getItemId()) {
case MENU_FOLDER_VIEW:
- openFolder(mFocusNoteDataItem); // 查看文件夹 → 进入该文件夹层级
+ openFolder(mFocusNoteDataItem);
break;
case MENU_FOLDER_DELETE:
- // 删除文件夹:弹出确认对话框,防止误删,用户确认后执行删除逻辑
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.alert_title_delete));
builder.setIcon(android.R.drawable.ic_dialog_alert);
@@ -1049,197 +1677,200 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
builder.show();
break;
case MENU_FOLDER_CHANGE_NAME:
- showCreateOrModifyFolderDialog(false); // 重命名文件夹 → 打开修改对话框
+ showCreateOrModifyFolderDialog(false);
break;
default:
break;
}
+
return true;
}
- /**
- * 选项菜单准备回调 - 动态加载不同状态的菜单资源
- * 执行时机:页面顶部菜单展示前调用,每次菜单刷新都会执行
- * 核心逻辑:清空原有菜单 → 根据当前页面状态加载对应的菜单资源 → 更新同步按钮的文本(同步/取消同步),
- * 保证不同页面状态下展示的菜单功能与当前场景匹配。
- * @param menu 要初始化的选项菜单对象
- * @return boolean true表示菜单初始化成功,展示菜单
- */
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
- menu.clear(); // 清空原有菜单,避免重复加载导致菜单异常
- // 根据页面状态加载对应的菜单资源
+ menu.clear();
if (mState == ListEditState.NOTE_LIST) {
- getMenuInflater().inflate(R.menu.note_list, menu); // 根目录加载完整菜单
- // 更新同步按钮文本:同步中显示「取消同步」,未同步显示「同步」
+ getMenuInflater().inflate(R.menu.note_list, menu);
+ // set sync or sync_cancel
menu.findItem(R.id.menu_sync).setTitle(
GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync);
} else if (mState == ListEditState.SUB_FOLDER) {
- getMenuInflater().inflate(R.menu.sub_folder, menu); // 子文件夹加载精简菜单
+ getMenuInflater().inflate(R.menu.sub_folder, menu);
} else if (mState == ListEditState.CALL_RECORD_FOLDER) {
- getMenuInflater().inflate(R.menu.call_record_folder, menu); // 通话记录文件夹加载专属菜单
+ getMenuInflater().inflate(R.menu.call_record_folder, menu);
+ } else if (mState == ListEditState.TRASH_FOLDER) {
+ // 为回收站添加特定菜单
+ menu.add(0, 100, 0, "恢复");
+ menu.add(0, 101, 0, "清空回收站");
} else {
Log.e(TAG, "Wrong state:" + mState);
}
return true;
}
- /**
- * 选项菜单点击事件处理 - 页面顶部菜单的核心交互逻辑
- * 核心职责:处理顶部菜单的所有点击事件,包含「新建文件夹」「导出文本」「同步」「设置」「新建便签」「搜索」,
- * 是页面顶部功能的核心出口。
- * @param item 被选中的菜单项对象
- * @return boolean true表示事件已处理完成
- */
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
- case R.id.menu_new_folder:
- showCreateOrModifyFolderDialog(true); // 新建文件夹 → 打开创建对话框
+ case R.id.menu_new_folder: {
+ showCreateOrModifyFolderDialog(true);
break;
- case R.id.menu_export_text:
- exportNoteToText(); // 导出文本 → 执行便签导出逻辑,保存为本地文件
+ }
+ case R.id.menu_export_text: {
+ exportNoteToText();
break;
- case R.id.menu_sync:
- // 同步操作:区分同步模式与非同步模式,执行差异化逻辑
+ }
+ case R.id.menu_sync: {
if (isSyncMode()) {
- // 同步模式下:点击「同步」启动同步,点击「取消同步」终止同步
if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) {
GTaskSyncService.startSync(this);
} else {
GTaskSyncService.cancelSync(this);
}
} else {
- // 非同步模式下:跳转至设置页面,引导用户配置同步账号
startPreferenceActivity();
}
break;
- case R.id.menu_setting:
- startPreferenceActivity(); // 设置 → 跳转至应用偏好设置页面
+ }
+ case R.id.menu_setting: {
+ startPreferenceActivity();
break;
- case R.id.menu_new_note:
- createNewNote(); // 新建便签 → 执行新建逻辑
+ }
+ case R.id.menu_new_note: {
+ createNewNote();
break;
+ }
case R.id.menu_search:
- onSearchRequested(); // 搜索 → 触发系统全局搜索功能
+ onSearchRequested();
break;
+ case R.id.menu_change_background:
+ // 直接在主界面更换背景
+ changeBackground();
+ break;
+
+ case 100: {
+ // 恢复选中的便签(兼容旧版本)
+ if (mNotesListAdapter.getSelectedCount() == 0) {
+ Toast.makeText(this, getString(R.string.menu_select_none),
+ Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ // 将便签恢复到根文件夹
+ DataUtils.batchMoveToFolder(getContentResolver(), mNotesListAdapter.getSelectedItemIds(), Notes.ID_ROOT_FOLDER);
+ mModeCallBack.finishActionMode();
+ break;
+ }
+ case 101: {
+ // 清空回收站(兼容旧版本)
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(getString(R.string.alert_title_delete));
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setMessage(getString(R.string.alert_message_delete_notes, "所有"));
+ builder.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ // 查询回收站中的所有便签并彻底删除
+ Cursor cursor = getContentResolver().query(Notes.CONTENT_NOTE_URI,
+ new String[] { NoteColumns.ID },
+ NoteColumns.PARENT_ID + "=?",
+ new String[] { String.valueOf(Notes.ID_TRASH_FOLER) },
+ null);
+ if (cursor != null) {
+ HashSet ids = new HashSet();
+ while (cursor.moveToNext()) {
+ ids.add(cursor.getLong(0));
+ }
+ cursor.close();
+ // 彻底删除
+ DataUtils.batchDeleteNotes(getContentResolver(), ids);
+ }
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.show();
+ break;
+ }
default:
break;
}
return true;
}
- /**
- * 系统搜索触发方法 - 重写原生方法
- * 核心逻辑:调用系统搜索接口,启动应用内搜索页面,支持便签内容的全局搜索,提升数据查找效率。
- * @return boolean true表示搜索请求已成功触发
- */
@Override
public boolean onSearchRequested() {
- startSearch(null, false, null, false);
+ startSearch(null, false, null /* appData */, false);
return true;
}
- /**
- * 便签导出核心方法 - 异步导出便签为文本文件
- * 核心设计:通过{AsyncTask}异步执行导出操作,避免主线程阻塞;适配SD卡状态、导出结果,展示对应的提示对话框,
- * 支持本地备份,提升数据安全性。
- * 核心逻辑:调用备份工具类执行导出 → 根据导出状态码展示不同提示 → 成功则显示文件路径,失败则提示原因。
- */
- private void exportNoteToText() {
- final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this);
- new AsyncTask() {
-
- @Override
- protected Integer doInBackground(Void... unused) {
- return backup.exportToText(); // 后台执行导出逻辑,返回状态码
- }
-
- @Override
- protected void onPostExecute(Integer result) {
- // 根据导出状态码,展示对应的提示对话框
- if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) {
- // SD卡未挂载,导出失败,提示用户挂载SD卡
- AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
- builder.setTitle(NotesListActivity.this
- .getString(R.string.failed_sdcard_export));
- builder.setMessage(NotesListActivity.this
- .getString(R.string.error_sdcard_unmounted));
- builder.setPositiveButton(android.R.string.ok, null);
- builder.show();
- } else if (result == BackupUtils.STATE_SUCCESS) {
- // 导出成功,提示用户文件路径与名称
- AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
- builder.setTitle(NotesListActivity.this
- .getString(R.string.success_sdcard_export));
- builder.setMessage(NotesListActivity.this.getString(
- R.string.format_exported_file_location, backup
- .getExportedTextFileName(), backup.getExportedTextFileDir()));
- builder.setPositiveButton(android.R.string.ok, null);
- builder.show();
- } else if (result == BackupUtils.STATE_SYSTEM_ERROR) {
- // 系统异常,导出失败,提示用户重试
- alertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
- builder.setTitle(NotesListActivity.this
- .getString(R.string.failed_sdcard_export));
- builder.setMessage(NotesListActivity.this
- .getString(R.string.error_sdcard_export));
- builder.setPositiveButton(android.R.string.ok, null);
- builder.show();
- }
- }
- }.execute();
- }
+ private void exportNoteToText() {
+ final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this);
+ new AsyncTask() {
+
+ @Override
+ protected Integer doInBackground(Void... unused) {
+ return backup.exportToText();
+ }
+
+ @Override
+ protected void onPostExecute(Integer result) {
+ if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
+ builder.setTitle(NotesListActivity.this
+ .getString(R.string.failed_sdcard_export));
+ builder.setMessage(NotesListActivity.this
+ .getString(R.string.error_sdcard_unmounted));
+ builder.setPositiveButton(android.R.string.ok, null);
+ builder.show();
+ } else if (result == BackupUtils.STATE_SUCCESS) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
+ builder.setTitle(NotesListActivity.this
+ .getString(R.string.success_sdcard_export));
+ builder.setMessage(NotesListActivity.this.getString(
+ R.string.format_exported_file_location, backup
+ .getExportedTextFileName(), backup.getExportedTextFileDir()));
+ builder.setPositiveButton(android.R.string.ok, null);
+ builder.show();
+ } else if (result == BackupUtils.STATE_SYSTEM_ERROR) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
+ builder.setTitle(NotesListActivity.this
+ .getString(R.string.failed_sdcard_export));
+ builder.setMessage(NotesListActivity.this
+ .getString(R.string.error_sdcard_export));
+ builder.setPositiveButton(android.R.string.ok, null);
+ builder.show();
+ }
+ }
+
+ }.execute();
+ }
- /**
- * 同步模式判断方法 - 核心模式区分依据
- * 核心规则:通过判断是否配置了同步账号,区分同步模式与非同步模式,同步模式下所有删除操作均移至回收站,非同步模式直接物理删除,
- * 是页面差异化业务逻辑的核心判断依据。
- * @return boolean true=已配置同步账号(同步模式),false=未配置同步账号(非同步模式)
- */
private boolean isSyncMode() {
return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0;
}
- /**
- * 偏好设置页面启动方法 - 适配嵌套Activity场景
- * 核心逻辑:判断当前页面是否有父Activity,有则通过父Activity启动,无则直接启动,适配应用的嵌套页面结构,
- * 保证设置页面正常打开。
- */
private void startPreferenceActivity() {
Activity from = getParent() != null ? getParent() : this;
Intent intent = new Intent(from, NotesPreferenceActivity.class);
from.startActivityIfNeeded(intent, -1);
}
- /**
- * 内部列表项点击监听器 - 列表项核心交互逻辑
- * 实现{OnItemClickListener}接口,核心职责:区分多选模式与普通模式,处理列表项的点击事件,
- * 多选模式下切换选中状态,普通模式下打开便签/文件夹,是列表项点击交互的核心实现类。
- */
private class OnListItemClickListener implements OnItemClickListener {
public void onItemClick(AdapterView> parent, View view, int position, long id) {
- // 仅处理自定义的NotesListItem列表项,避免类型错误
if (view instanceof NotesListItem) {
NoteItemData item = ((NotesListItem) view).getItemData();
-
- // 多选模式下的点击逻辑:仅处理普通便签,切换选中状态,文件夹不可选
if (mNotesListAdapter.isInChoiceMode()) {
- if (item.getType() == Notes.TYPE_NOTE) {
- // 校准列表项位置,排除头部视图的影响
+ // 允许在多选模式下选中文件夹和便签,但禁止选中回收站
+ if (item.getId() != Notes.ID_TRASH_FOLER) {
position = position - mNotesListView.getHeaderViewsCount();
- // 切换当前项的选中状态:选中→取消,取消→选中
mModeCallBack.onItemCheckedStateChanged(null, position, id,
!mNotesListAdapter.isSelectedItem(position));
}
return;
}
- // 普通模式下的点击逻辑:根据当前页面状态,执行差异化操作
switch (mState) {
case NOTE_LIST:
- // 根目录状态:文件夹/系统文件夹→打开文件夹,普通便签→打开便签编辑
if (item.getType() == Notes.TYPE_FOLDER
|| item.getType() == Notes.TYPE_SYSTEM) {
openFolder(item);
@@ -1251,11 +1882,11 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
break;
case SUB_FOLDER:
case CALL_RECORD_FOLDER:
- // 子文件夹/通话记录文件夹状态:仅处理普通便签,打开编辑,无文件夹层级
+ case TRASH_FOLDER:
if (item.getType() == Notes.TYPE_NOTE) {
openNode(item);
} else {
- Log.e(TAG, "Wrong note type in SUB_FOLDER");
+ Log.e(TAG, "Wrong note type in folder view");
}
break;
default:
@@ -1263,21 +1894,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
}
}
+
}
- /**
- * 目标文件夹查询启动方法 - 批量移动便签的前置查询
- * 核心设计意图:查询所有可用于移动便签的目标文件夹,构建差异化的查询条件,排除无效文件夹,保证移动逻辑的准确性。
- * 核心查询规则:包含所有用户文件夹 → 排除回收站、当前文件夹 → 子文件夹状态下额外包含根文件夹(允许移回根目录)。
- */
private void startQueryDestinationFolders() {
- // 基础查询条件:类型为文件夹 + 父文件夹不是回收站 + 文件夹ID不是当前文件夹
String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?";
- // 子文件夹状态下,额外包含根文件夹,允许用户将便签移回根目录
selection = (mState == ListEditState.NOTE_LIST) ? selection:
- "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")";
+ "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")";
- // 启动异步查询,获取目标文件夹列表,用于展示选择对话框
mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN,
null,
Notes.CONTENT_NOTE_URI,
@@ -1290,34 +1914,770 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
},
NoteColumns.MODIFIED_DATE + " DESC");
}
+
+ /**
+ * 显示选择好友的对话框,用于发送便签
+ */
+ private void showSendToFriendsDialog(final HashSet selectedNoteIds) {
+ try {
+ // 查询所有好友
+ final ArrayList friendIds = new ArrayList<>();
+ final ArrayList friendUsernames = new ArrayList<>();
+ final boolean[] selectedFriends = new boolean[0];
+
+ // 获取当前用户ID
+ long currentUserId = UserManager.getInstance(this).getCurrentUserId();
+
+ // 获取数据库实例
+ SQLiteDatabase db = NotesDatabaseHelper.getInstance(this).getReadableDatabase();
+
+ // 查询除当前用户以外的所有用户
+ Cursor cursor = db.query(
+ NotesDatabaseHelper.TABLE.USER,
+ new String[]{Users.UserColumns.ID, Users.UserColumns.USERNAME},
+ Users.UserColumns.ID + " != ?",
+ new String[]{String.valueOf(currentUserId)},
+ null, null, null);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ long friendId = cursor.getLong(cursor.getColumnIndexOrThrow(Users.UserColumns.ID));
+ String friendUsername = cursor.getString(cursor.getColumnIndexOrThrow(Users.UserColumns.USERNAME));
+ friendIds.add(friendId);
+ friendUsernames.add(friendUsername);
+ } while (cursor.moveToNext());
+ cursor.close();
+ }
+
+ if (friendIds.isEmpty()) {
+ Toast.makeText(this, "没有可用的好友", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 创建选择数组
+ final boolean[] isFriendSelected = new boolean[friendIds.size()];
+
+ // 创建对话框
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("选择好友");
+
+ // 设置多选项
+ builder.setMultiChoiceItems(
+ friendUsernames.toArray(new String[0]),
+ isFriendSelected,
+ new DialogInterface.OnMultiChoiceClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which, boolean isChecked) {
+ isFriendSelected[which] = isChecked;
+ }
+ });
+
+ // 设置确定按钮
+ builder.setPositiveButton("发送", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // 获取选中的好友
+ ArrayList selectedFriendIds = new ArrayList<>();
+ for (int i = 0; i < isFriendSelected.length; i++) {
+ if (isFriendSelected[i]) {
+ selectedFriendIds.add(friendIds.get(i));
+ }
+ }
+
+ if (selectedFriendIds.isEmpty()) {
+ Toast.makeText(NotesListActivity.this, "请选择至少一个好友", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 发送便签到选中的好友
+ sendNotesToFriends(selectedNoteIds, selectedFriendIds);
+
+ // 结束操作模式
+ mModeCallBack.finishActionMode();
+ }
+ });
+
+ // 设置取消按钮
+ builder.setNegativeButton("取消", null);
+
+ // 显示对话框
+ builder.show();
+ } catch (Exception e) {
+ Log.e(TAG, "Error in showSendToFriendsDialog: " + e.getMessage(), e);
+ Toast.makeText(this, "显示好友列表失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 发送便签到选中的好友
+ */
+ private void sendNotesToFriends(HashSet noteIds, ArrayList friendIds) {
+ try {
+ SQLiteDatabase db = NotesDatabaseHelper.getInstance(this).getWritableDatabase();
+ long currentUserId = UserManager.getInstance(this).getCurrentUserId();
+
+ // 遍历每个选中的便签
+ for (Long noteId : noteIds) {
+ // 查询便签内容
+ Cursor noteCursor = db.query(
+ NotesDatabaseHelper.TABLE.NOTE,
+ new String[]{NoteColumns.TITLE, NoteColumns.SNIPPET},
+ NoteColumns.ID + " = ?",
+ new String[]{String.valueOf(noteId)},
+ null, null, null);
+
+ if (noteCursor != null && noteCursor.moveToFirst()) {
+ String noteTitle = noteCursor.getString(noteCursor.getColumnIndexOrThrow(NoteColumns.TITLE));
+ String noteContent = noteCursor.getString(noteCursor.getColumnIndexOrThrow(NoteColumns.SNIPPET));
+ noteCursor.close();
+
+ // 便签内容格式:标题|内容|便签ID
+ String noteData = noteTitle + "|" + noteContent + "|" + noteId;
+
+ // 遍历每个选中的好友,发送便签
+ for (Long friendId : friendIds) {
+ ContentValues values = new ContentValues();
+ values.put(Messages.MessageColumns.SENDER_ID, currentUserId);
+ values.put(Messages.MessageColumns.RECEIVER_ID, friendId);
+ values.put(Messages.MessageColumns.CONTENT, noteData);
+ values.put(Messages.MessageColumns.MESSAGE_TYPE, Messages.MessageType.NOTE);
+ values.put(Messages.MessageColumns.CREATED_DATE, System.currentTimeMillis());
+ values.put(Messages.MessageColumns.IS_READ, 0);
+
+ // 插入消息到数据库
+ long messageId = db.insert(NotesDatabaseHelper.TABLE.MESSAGE, null, values);
+ if (messageId != -1) {
+ Log.d(TAG, "Note sent to friend " + friendId + ", messageId: " + messageId);
+ } else {
+ Log.e(TAG, "Failed to send note to friend " + friendId);
+ }
+ }
+ } else {
+ if (noteCursor != null) {
+ noteCursor.close();
+ }
+ Log.e(TAG, "Failed to get note content for note " + noteId);
+ }
+ }
+
+ Toast.makeText(this, "便签发送成功", Toast.LENGTH_SHORT).show();
+ } catch (Exception e) {
+ Log.e(TAG, "Error in sendNotesToFriends: " + e.getMessage(), e);
+ Toast.makeText(this, "发送便签失败", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 更换背景方法
+ */
+ private void changeBackground() {
+ Log.d(TAG, "changeBackground called");
+
+ // 创建选择背景的对话框
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("更换背景");
+
+ // 创建列表项
+ String[] items = {"从相册选择", "拍照"};
+
+ // 设置列表项点击事件
+ builder.setItems(items, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent;
+ switch (which) {
+ case 0: // 从相册选择
+ intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ intent.setType("image/*");
+ startActivityForResult(intent, 1);
+ break;
+ case 1: // 拍照
+ intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
+ startActivityForResult(intent, 2);
+ break;
+ }
+ }
+ });
+
+ // 显示对话框
+ builder.show();
+ Log.d(TAG, "Dialog shown");
+ }
+
+
+ /**
+ * 根据Uri获取图片路径,适配不同Android版本
+ */
+ private String getPathFromUri(android.net.Uri uri) {
+ Log.d(TAG, "Getting path from Uri: " + uri.toString() + ", scheme: " + uri.getScheme());
+
+ String path = null;
+
+ try {
+ // 检查Uri scheme
+ if ("content".equals(uri.getScheme())) {
+ // 处理content://类型的Uri
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
+ // Android 10及以上,使用MediaStore API
+ path = getPathFromContentUriQ(uri);
+ } else {
+ // Android 9及以下,使用传统方式
+ path = getPathFromContentUriLegacy(uri);
+ }
+ } else if ("file".equals(uri.getScheme())) {
+ // 处理file://类型的Uri
+ path = uri.getPath();
+ Log.d(TAG, "File scheme Uri, path: " + path);
+ }
+
+ Log.d(TAG, "Final path from Uri: " + path);
+ } catch (Exception e) {
+ Log.e(TAG, "Error getting path from Uri: " + e.getMessage(), e);
+ }
+
+ return path;
+ }
+
+ /**
+ * Android 9及以下,根据Content Uri获取图片路径
+ */
+ private String getPathFromContentUriLegacy(android.net.Uri uri) {
+ String path = null;
+ String[] projection = {android.provider.MediaStore.Images.Media.DATA};
+ Cursor cursor = getContentResolver().query(uri, projection, null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ int columnIndex = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA);
+ path = cursor.getString(columnIndex);
+ Log.d(TAG, "Legacy path: " + path);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error in legacy path retrieval: " + e.getMessage(), e);
+ } finally {
+ cursor.close();
+ }
+ }
+ return path;
+ }
+
/**
- * 列表项长按事件处理 - 实现{OnItemLongClickListener}接口,页面核心长按交互逻辑
- * 核心职责:区分长按的是便签还是文件夹,执行差异化逻辑 → 长按便签:触发多选模式,选中当前项并震动反馈;
- * 长按文件夹:绑定上下文菜单监听器,展示右键菜单,是页面长按交互的核心入口。
- * @param parent 列表控件对象
- * @param view 被长按的列表项视图
- * @param position 列表项位置
- * @param id 列表项对应的便签/文件夹ID
- * @return boolean false表示不消费事件,允许后续逻辑执行
+ * Android 10及以上,根据Content Uri获取图片路径
*/
+ private String getPathFromContentUriQ(android.net.Uri uri) {
+ String path = null;
+
+ // 对于Android Q及以上,我们可以直接使用Uri打开InputStream,而不需要获取真实路径
+ // 这里我们创建一个临时文件来保存图片
+ try {
+ // 创建临时文件
+ java.io.File tempFile = java.io.File.createTempFile("notes_bg", ".jpg", getExternalCacheDir());
+ tempFile.deleteOnExit();
+
+ // 从Uri复制到临时文件
+ java.io.InputStream inputStream = getContentResolver().openInputStream(uri);
+ if (inputStream != null) {
+ java.io.FileOutputStream outputStream = new java.io.FileOutputStream(tempFile);
+
+ // 复制文件
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, length);
+ }
+
+ outputStream.close();
+ inputStream.close();
+
+ path = tempFile.getAbsolutePath();
+ Log.d(TAG, "Q path: " + path);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error in Q path retrieval: " + e.getMessage(), e);
+ }
+
+ return path;
+ }
+
+ /**
+ * 保存Bitmap到本地
+ */
+ private String saveBitmap(android.graphics.Bitmap bitmap) {
+ String path = android.os.Environment.getExternalStorageDirectory().getAbsolutePath() + "/notes_background.jpg";
+ try {
+ java.io.FileOutputStream fos = new java.io.FileOutputStream(path);
+ bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 100, fos);
+ fos.flush();
+ fos.close();
+ } catch (java.io.IOException e) {
+ e.printStackTrace();
+ path = null;
+ }
+ return path;
+ }
+
+ /**
+ * 设置背景
+ */
+ private void setBackground(String imagePath) {
+ Log.d(TAG, "Setting background with path: " + imagePath);
+
+ try {
+ // 首先检查图片文件是否存在
+ java.io.File imageFile = new java.io.File(imagePath);
+ if (!imageFile.exists()) {
+ Log.e(TAG, "Image file not found: " + imagePath);
+ Toast.makeText(this, "图片文件不存在", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Log.d(TAG, "Image file exists, size: " + imageFile.length() + " bytes");
+
+ // 1. 尝试直接获取note_list.xml中的根FrameLayout
+ FrameLayout rootFrameLayout = findViewById(R.id.root_layout);
+ if (rootFrameLayout != null) {
+ Log.d(TAG, "Found root_layout (note_list.xml root), setting background directly");
+
+ // 尝试加载图片
+ android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeFile(imagePath);
+ if (bitmap != null) {
+ Log.d(TAG, "Bitmap loaded successfully, width: " + bitmap.getWidth() + ", height: " + bitmap.getHeight());
+
+ // 创建Drawable
+ android.graphics.drawable.Drawable drawable;
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ drawable = new android.graphics.drawable.BitmapDrawable(getResources(), bitmap);
+ } else {
+ drawable = new android.graphics.drawable.BitmapDrawable(bitmap);
+ }
+
+ // 直接设置根FrameLayout的背景
+ rootFrameLayout.setBackground(drawable);
+ Log.d(TAG, "Root layout background set successfully");
+ Toast.makeText(this, "背景设置成功", Toast.LENGTH_SHORT).show();
+ } else {
+ Log.e(TAG, "Failed to load bitmap from path: " + imagePath);
+ Toast.makeText(this, "图片加载失败", Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ Log.d(TAG, "root_layout not found, trying other methods");
+
+ // 2. 尝试获取Activity的根View
+ View rootView = getWindow().getDecorView().findViewById(android.R.id.content);
+ if (rootView != null) {
+ Log.d(TAG, "Root view found: " + rootView.getClass().getName());
+
+ // 3. 如果是ViewGroup,尝试设置其所有子View的背景为透明,然后设置自身背景
+ if (rootView instanceof ViewGroup) {
+ Log.d(TAG, "Root view is ViewGroup, clearing child backgrounds");
+ clearChildBackgrounds((ViewGroup) rootView);
+ }
+
+ // 尝试加载图片
+ android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeFile(imagePath);
+ if (bitmap != null) {
+ Log.d(TAG, "Bitmap loaded successfully, width: " + bitmap.getWidth() + ", height: " + bitmap.getHeight());
+
+ // 创建Drawable
+ android.graphics.drawable.Drawable drawable;
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ drawable = new android.graphics.drawable.BitmapDrawable(getResources(), bitmap);
+ } else {
+ drawable = new android.graphics.drawable.BitmapDrawable(bitmap);
+ }
+
+ // 设置背景
+ rootView.setBackground(drawable);
+ Log.d(TAG, "Root view background set successfully");
+ Toast.makeText(this, "背景设置成功", Toast.LENGTH_SHORT).show();
+ } else {
+ Log.e(TAG, "Failed to load bitmap from path: " + imagePath);
+ Toast.makeText(this, "图片加载失败", Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ Log.e(TAG, "Root view not found");
+ Toast.makeText(this, "无法获取根布局", Toast.LENGTH_SHORT).show();
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error setting background: " + e.getMessage(), e);
+ Toast.makeText(this, "设置背景时出错: " + e.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 清除ViewGroup中所有子View的背景
+ */
+ private void clearChildBackgrounds(ViewGroup viewGroup) {
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
+ View child = viewGroup.getChildAt(i);
+ if (child instanceof ViewGroup) {
+ // 递归清除子ViewGroup的背景
+ clearChildBackgrounds((ViewGroup) child);
+ }
+ // 设置子View背景为透明
+ child.setBackgroundColor(getResources().getColor(android.R.color.transparent));
+ }
+ }
+
+ /**
+ * 加载保存的背景图片
+ */
+ private void loadSavedBackground() {
+ // 从SharedPreferences获取保存的图片路径
+ SharedPreferences sharedPreferences = getSharedPreferences("background", MODE_PRIVATE);
+ String imagePath = sharedPreferences.getString("background_path", null);
+
+ if (imagePath != null) {
+ // 设置背景
+ setBackground(imagePath);
+ }
+ }
+
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 (mFocusNoteDataItem.getId() == Notes.ID_TRASH_FOLER) {
+ return false;
+ }
+ if (!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
+
+ // 拖拽触摸监听器
+ private class OnDragTouchListener implements OnTouchListener {
+ private static final long LONG_PRESS_DURATION = 500; // 长按检测时长
+ private boolean mIsLongPress = false;
+ private long mPressStartTime = 0;
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (mIsDragging) {
+ handleDragEvent(event);
+ return true;
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mPressStartTime = System.currentTimeMillis();
+ mIsLongPress = false;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (!mIsLongPress && System.currentTimeMillis() - mPressStartTime > LONG_PRESS_DURATION) {
+ mIsLongPress = true;
+ startDrag(v, event);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mIsLongPress = false;
+ break;
+ }
+ return false;
+ }
+ }
+
+ // 开始拖拽
+ private void startDrag(View listView, MotionEvent event) {
+ try {
+ if (listView instanceof ListView) {
+ ListView lv = (ListView) listView;
+ int position = lv.pointToPosition((int) event.getX(), (int) event.getY());
+ int listCount = lv.getCount();
+ if (position >= 0 && position < listCount) {
+ // 保存当前列表数据
+ Cursor cursor = mNotesListAdapter.getCursor();
+ if (cursor != null && cursor.getCount() > 0) {
+ try {
+ cursor.moveToFirst();
+ int cursorCount = cursor.getCount();
+ mDragTempData = new NoteItemData[cursorCount];
+ for (int i = 0; i < cursorCount; i++) {
+ if (!cursor.isAfterLast()) {
+ mDragTempData[i] = new NoteItemData(this, cursor);
+ cursor.moveToNext();
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error saving list data: " + e.getMessage(), e);
+ mDragTempData = null;
+ } finally {
+ if (!cursor.isClosed()) {
+ try {
+ cursor.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Error closing cursor: " + e.getMessage(), e);
+ }
+ }
+ }
+ }
+
+ mDragStartPosition = position;
+ mDragCurrentPosition = position;
+ mDragStartY = event.getY();
+ mDragOffsetY = event.getY();
+ mIsDragging = true;
+
+ // 获取拖拽视图
+ int firstVisiblePosition = lv.getFirstVisiblePosition();
+ int childIndex = position - firstVisiblePosition;
+ if (childIndex >= 0 && childIndex < lv.getChildCount()) {
+ View itemView = lv.getChildAt(childIndex);
+ if (itemView != null) {
+ // 创建拖拽视图的副本
+ mDraggingView = itemView;
+ mDraggingViewHeight = itemView.getHeight();
+ // 显示拖拽效果
+ mDraggingView.setAlpha(0.5f);
+ mDraggingView.setScaleX(1.1f);
+ mDraggingView.setScaleY(1.1f);
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error starting drag: " + e.getMessage(), e);
+ // 发生异常时,重置拖拽状态
+ mIsDragging = false;
+ mDragStartPosition = -1;
+ mDragCurrentPosition = -1;
+ mDraggingView = null;
+ mDragTempData = null;
+ }
+ }
+
+ // 处理拖拽事件
+ private void handleDragEvent(MotionEvent event) {
+ try {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_MOVE:
+ // 更新拖拽视图位置
+ if (mDraggingView != null) {
+ float deltaY = event.getY() - mDragOffsetY;
+ mDraggingView.setTranslationY(deltaY);
+ }
+
+ // 计算当前拖拽到的位置
+ ListView lv = mNotesListView;
+ if (lv == null) {
+ break;
+ }
+
+ int firstVisiblePosition = lv.getFirstVisiblePosition();
+ int lastVisiblePosition = lv.getLastVisiblePosition();
+ int childCount = lv.getChildCount();
+
+ // 计算相对于列表顶部的Y坐标
+ int listTop = lv.getTop();
+ int relativeY = (int) event.getY() + listTop;
+
+ // 计算拖拽到的项索引
+ int newPosition = -1;
+
+ // 遍历可见项,计算当前Y坐标对应的项
+ if (childCount > 0) {
+ for (int i = 0; i < childCount; i++) {
+ View child = lv.getChildAt(i);
+ if (child == null) continue;
+
+ int childTop = child.getTop() + listTop;
+ int childBottom = childTop + child.getHeight();
+
+ if (relativeY >= childTop && relativeY <= childBottom) {
+ // 找到了对应的项
+ newPosition = firstVisiblePosition + i;
+ break;
+ }
+ }
+ }
+
+ // 处理边界情况
+ if (newPosition == -1) {
+ if (childCount > 0) {
+ View firstVisibleItem = lv.getChildAt(0);
+ View lastVisibleItem = lv.getChildAt(childCount - 1);
+
+ if (firstVisibleItem != null && event.getY() < firstVisibleItem.getTop()) {
+ // 向上拖拽到可见区域外,应该插入到第一个可见项之前
+ newPosition = firstVisiblePosition;
+ } else if (lastVisibleItem != null && event.getY() > lastVisibleItem.getBottom()) {
+ // 向下拖拽到可见区域外,应该插入到最后一个可见项之后
+ newPosition = lastVisiblePosition + 1;
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+
+ // 确保新位置在有效范围内
+ int listCount = lv.getCount();
+ if (listCount > 0) {
+ newPosition = Math.max(0, Math.min(newPosition, listCount - 1));
+ } else {
+ break;
+ }
+
+ if (newPosition != mDragCurrentPosition && mDragTempData != null) {
+ // 只更新临时数据列表,不立即更新数据库
+ // 这样可以避免在拖拽过程中频繁更新数据库导致的性能问题
+ if (mDragCurrentPosition >= 0 && mDragCurrentPosition < mDragTempData.length) {
+ NoteItemData draggedItem = mDragTempData[mDragCurrentPosition];
+ if (draggedItem != null && newPosition >= 0 && newPosition < mDragTempData.length) {
+ if (newPosition > mDragCurrentPosition) {
+ // 向下移动
+ for (int i = mDragCurrentPosition; i < newPosition; i++) {
+ if (i + 1 < mDragTempData.length) {
+ mDragTempData[i] = mDragTempData[i + 1];
+ }
+ }
+ } else if (newPosition < mDragCurrentPosition) {
+ // 向上移动
+ for (int i = mDragCurrentPosition; i > newPosition; i--) {
+ if (i - 1 >= 0) {
+ mDragTempData[i] = mDragTempData[i - 1];
+ }
+ }
+ }
+ // 将被拖拽的项放到新位置
+ mDragTempData[newPosition] = draggedItem;
+ // 更新当前位置
+ mDragCurrentPosition = newPosition;
+ }
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ stopDrag();
+ break;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error in handleDragEvent: " + e.getMessage(), e);
+ // 发生异常时,停止拖拽
+ stopDrag();
+ }
+ }
+
+ // 更新临时数据列表的顺序
+ private void updateTempDataOrder(int newPosition) {
+ if (mDragTempData == null || mDragStartPosition < 0 || mDragStartPosition >= mDragTempData.length) {
+ return;
+ }
+
+ // 保存当前拖拽位置
+ int currentPosition = mDragCurrentPosition;
+
+ // 先更新当前位置
+ mDragCurrentPosition = newPosition;
+
+ // 保存被拖拽的项
+ NoteItemData draggedItem = mDragTempData[currentPosition];
+
+ // 移动数组中的元素
+ if (newPosition > currentPosition) {
+ // 向下移动
+ for (int i = currentPosition; i < newPosition; i++) {
+ mDragTempData[i] = mDragTempData[i + 1];
+ }
+ } else if (newPosition < currentPosition) {
+ // 向上移动
+ for (int i = currentPosition; i > newPosition; i--) {
+ mDragTempData[i] = mDragTempData[i - 1];
+ }
+ }
+
+ // 将被拖拽的项放到新位置
+ mDragTempData[newPosition] = draggedItem;
+
+ // 更新起始位置为当前新位置
+ mDragStartPosition = newPosition;
+
+ // 立即更新数据库中的排序
+ updateSortOrderInDatabase();
+ }
+
+ // 更新数据库中的排序
+ private void updateSortOrderInDatabase() {
+ if (mDragTempData == null || mDragTempData.length == 0) {
+ return;
+ }
+
+ try {
+ // 使用ContentResolver批量更新,不需要直接操作数据库
+ ContentResolver resolver = getContentResolver();
+ if (resolver == null) {
+ return;
+ }
+
+ for (int i = 0; i < mDragTempData.length; i++) {
+ NoteItemData item = mDragTempData[i];
+ if (item != null && item.getType() == Notes.TYPE_NOTE) {
+ ContentValues values = new ContentValues();
+ values.put(NoteColumns.SORT_ORDER, i);
+ resolver.update(Notes.CONTENT_NOTE_URI,
+ values,
+ "_id=?",
+ new String[]{String.valueOf(item.getId())});
+ }
+ }
+
+ // 刷新列表
+ startAsyncNotesListQuery();
+ } catch (Exception e) {
+ Log.e(TAG, "Error updating sort order: " + e.getMessage(), e);
+ }
+ }
+
+ // 停止拖拽
+ private void stopDrag() {
+ try {
+ if (mDraggingView != null) {
+ // 恢复拖拽视图
+ try {
+ mDraggingView.setAlpha(1.0f);
+ mDraggingView.setScaleX(1.0f);
+ mDraggingView.setScaleY(1.0f);
+ mDraggingView.setTranslationY(0);
+ } catch (Exception e) {
+ Log.e(TAG, "Error resetting dragging view: " + e.getMessage(), e);
+ }
+ }
+
+ // 在拖拽结束时更新数据库
+ if (mDragTempData != null) {
+ updateSortOrderInDatabase();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error in stopDrag: " + e.getMessage(), e);
+ } finally {
+ // 无论如何都重置拖拽状态
+ mIsDragging = false;
+ mDragStartPosition = -1;
+ mDragCurrentPosition = -1;
+ mDraggingView = null;
+ mDragTempData = null;
+ }
+ }
+
+ // 获取指定位置的便签ID
+ private long getItemId(int position) {
+ Cursor cursor = mNotesListAdapter.getCursor();
+ if (cursor != null && cursor.moveToPosition(position)) {
+ return cursor.getLong(0); // ID_COLUMN 是 0
+ }
+ return -1;
+ }
+}
+
diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java b/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java
index d8720c9..fcdee3f 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java
@@ -14,231 +14,133 @@
* limitations under the License.
*/
-// 包声明:小米便签 核心UI模块,该包承载应用所有可视化交互页面及配套适配器,本类为列表页核心数据适配桥梁
package net.micode.notes.ui;
-// -------------------------- 安卓系统核心依赖包 - 上下文/数据库/日志/视图/适配器能力 --------------------------
-// 安卓应用全局上下文:提供资源访问、视图创建、数据解析等基础能力,适配器必备依赖
import android.content.Context;
-// 安卓数据库游标核心类:封装数据库查询结果集,承载便签数据表的查询数据,列表展示的核心数据来源
import android.database.Cursor;
-// 安卓系统日志工具类:输出适配器运行的调试/错误日志,便于问题定位与线上排查
import android.util.Log;
-// 安卓视图体系核心类:视图创建、视图容器的核心父类,适配列表项的创建与挂载
import android.view.View;
import android.view.ViewGroup;
-// 安卓游标适配器基类:专为Cursor数据设计的列表适配器,封装数据绑定、视图复用、数据变化监听等核心能力,本类的核心父类
import android.widget.CursorAdapter;
-// -------------------------- 小米便签业务层核心依赖 - 数据常量/集合工具 --------------------------
-// 小米便签数据层核心常量类:定义便签/文件夹/小部件的类型、特殊ID、业务状态等全局核心常量
import net.micode.notes.data.Notes;
-// Java集合框架相关类:封装选中项状态的存储、遍历、统计,支撑批量选择模式的核心逻辑
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+
/**
- * 小米便签 列表页核心数据适配器
+ * 便签列表适配器
*
- * 继承安卓系统{CursorAdapter}游标适配器,隶属于MVC架构的UI层适配桥梁,是{NotesListActivity}与数据库/Cursor数据、{NotesListItem}列表项之间的核心纽带;
- * 核心设计定位:封装Cursor数据库数据与列表项视图的绑定逻辑,统一管理列表的展示规则、选择模式、数据变化监听,解耦列表页的业务逻辑与数据渲染逻辑;
- * 核心业务职责:创建并复用自定义列表项视图、将Cursor数据映射为业务模型并绑定至列表项、完整支撑批量选择模式的所有操作(单选/全选/取消全选/选中状态维护)、
- * 统计选中项数据与普通便签总数、提取选中项的小部件关联属性、监听数据库数据变化并实时刷新视图;
- * 技术实现特点:基于安卓原生CursorAdapter实现视图复用,提升列表滑动性能;通过HashMap维护选中项状态,保证选择操作的高效性;
- * 封装数据统计与业务属性提取逻辑,对外提供简洁的调用接口;自动监听数据库数据变化,保证视图与数据的一致性。
- *
+ * 该类是便签列表的适配器,用于将数据库中的便签数据绑定到列表项视图上。
+ * 它支持选择模式、搜索高亮和小部件属性管理等功能。
*/
public class NotesListAdapter extends CursorAdapter {
- /** 日志常量标签:适配器相关日志的统一标识,便于日志过滤与问题定位 */
private static final String TAG = "NotesListAdapter";
- /** 应用上下文对象:用于创建列表项视图、解析业务数据,全局复用避免多次创建 */
private Context mContext;
- /** 选中项状态映射容器:核心数据结构,Key=列表项的索引位置,Value=该位置的选中状态,支撑选择模式的核心存储 */
private HashMap mSelectedIndex;
- /** 普通便签数量统计:仅统计{Notes.TYPE_NOTE}类型的项,排除文件夹/通话记录等类型,用于全选状态的判定依据 */
private int mNotesCount;
- /** 选择模式状态标记:true=开启批量选择模式(批量操作),false=关闭选择模式(普通浏览),控制列表项勾选框的显隐 */
private boolean mChoiceMode;
+ private String mSearchQuery;
/**
- * 内部静态数据载体类:便签关联的桌面小部件属性封装
- * 核心设计目的:批量操作场景下,统一封装选中便签所绑定的桌面小部件核心属性,便于后续同步更新小部件数据,
- * 静态类设计减少内存开销,无上下文引用避免内存泄漏
+ * 小部件属性类
+ *
+ * 用于存储小部件的ID和类型信息
*/
public static class AppWidgetAttribute {
- /** 小部件系统唯一标识ID:桌面小部件的注册ID,用于精准定位目标小部件 */
public int widgetId;
- /** 小部件类型标识:区分2x/4x两种尺寸的便签小部件,对应{Notes.TYPE_WIDGET_2X}/{Notes.TYPE_WIDGET_4X} */
public int widgetType;
};
- /**
- * 构造方法:适配器的初始化入口,完成核心成员变量的初始化配置
- * 核心初始化逻辑:调用父类构造方法完成基础配置、初始化选中状态映射容器、保存上下文引用、重置普通便签统计数,
- * 初始无绑定Cursor,后续通过{changeCursor}方法动态绑定数据库查询结果
- * @param context 应用上下文对象,传递至父类并全局保存
- */
public NotesListAdapter(Context context) {
- // 父类构造:传入上下文+空Cursor,Cursor数据后续动态绑定,保证初始化灵活性
super(context, null);
- // 初始化选中项状态映射容器,空HashMap保证初始无选中项
mSelectedIndex = new HashMap();
- // 保存应用上下文引用,供后续视图创建与数据解析使用
mContext = context;
- // 重置普通便签统计数量为0,初始无数据状态
mNotesCount = 0;
+ mSearchQuery = null;
+ }
+
+ // 设置搜索查询
+ public void setSearchQuery(String query) {
+ mSearchQuery = query;
+ notifyDataSetChanged();
}
- /**
- * 重写父类核心方法:创建新的列表项视图
- * 执行时机:列表首次加载、滑动时需要创建新视图的场景,遵循安卓视图复用机制
- * 核心逻辑:创建自定义的{NotesListItem}列表项视图并返回,该方法仅负责视图创建,不负责数据绑定
- * @param context 应用上下文对象
- * @param cursor 当前位置的数据库游标(本方法未使用,仅遵循父类接口规范)
- * @param parent 列表项的父容器,即承载所有列表项的ListView
- * @return View 创建完成的、未绑定数据的自定义列表项视图
- */
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
- // 创建小米便签自定义列表项视图,作为列表的最小展示单元
return new NotesListItem(context);
}
- /**
- * 重写父类核心方法:数据与视图的绑定核心实现
- * 执行时机:列表首次加载、视图复用、数据变化刷新时调用,是适配器的核心业务方法
- * 核心逻辑:将指定位置的Cursor数据库数据,解析为业务数据模型{NoteItemData},并调用列表项的绑定方法,
- * 完成数据渲染、选择模式适配、选中状态赋值,实现「数据→模型→视图」的完整映射
- * @param view 待绑定数据的列表项视图,为{NotesListItem}类型,支持视图复用
- * @param context 应用上下文对象
- * @param cursor 当前列表位置的数据库游标,封装了该位置的完整便签数据
- */
@Override
public void bindView(View view, Context context, Cursor cursor) {
- // 类型安全校验:仅处理自定义的NotesListItem视图,防止视图类型错误导致崩溃
if (view instanceof NotesListItem) {
- // 将Cursor数据库数据解析为业务数据模型,封装所有展示所需的业务字段
NoteItemData itemData = new NoteItemData(context, cursor);
- // 调用列表项的核心绑定方法,完成数据渲染+选择模式适配+选中状态赋值
((NotesListItem) view).bind(context, itemData, mChoiceMode,
- isSelectedItem(cursor.getPosition()));
+ isSelectedItem(cursor.getPosition()), mSearchQuery);
}
}
- /**
- * 公开业务方法:设置指定位置列表项的选中状态
- * 核心作用:单选操作的核心接口,支持选择模式下的单个列表项勾选/取消勾选,
- * 更新状态后主动通知列表刷新视图,保证选中状态的实时展示
- * @param position 待设置状态的列表项索引位置
- * @param checked 目标选中状态,true=勾选,false=取消勾选
- */
public void setCheckedItem(final int position, final boolean checked) {
- // 更新选中状态映射容器,保存当前位置的选中状态
mSelectedIndex.put(position, checked);
- // 通知列表数据发生变化,触发视图刷新,展示最新的选中状态
notifyDataSetChanged();
}
- /**
- * 公开查询方法:获取当前列表的选择模式状态
- * @return boolean true=处于批量选择模式,false=处于普通浏览模式
- */
public boolean isInChoiceMode() {
return mChoiceMode;
}
- /**
- * 公开业务方法:开启/关闭列表的批量选择模式
- * 核心逻辑:切换模式时自动清空所有选中状态,避免模式切换后残留选中标记,保证视图展示的一致性,
- * 是选择模式的总开关接口
- * @param mode true=开启选择模式,false=关闭选择模式
- */
public void setChoiceMode(boolean mode) {
- // 清空所有选中项状态,重置选择容器为初始状态
mSelectedIndex.clear();
- // 更新选择模式标记,控制后续视图绑定的逻辑分支
mChoiceMode = mode;
}
- /**
- * 公开业务方法:全选/取消全选的批量操作
- * 核心规则:仅对{Notes.TYPE_NOTE}类型的普通便签生效,文件夹/通话记录等特殊类型不参与选择,
- * 避免用户误操作系统特殊项,保证业务数据的安全性
- * @param checked true=执行全选操作,false=执行取消全选操作
- */
public void selectAll(boolean checked) {
- // 获取当前绑定的数据库游标,遍历所有列表项数据
Cursor cursor = getCursor();
for (int i = 0; i < getCount(); i++) {
- // 移动游标到当前列表项的位置,匹配对应数据
if (cursor.moveToPosition(i)) {
- // 仅处理普通便签类型,过滤文件夹等非选择项
if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) {
- // 批量设置当前位置的选中状态
setCheckedItem(i, checked);
}
}
}
}
- /**
- * 公开业务方法:获取所有选中项的便签ID集合
- * 核心作用:批量删除/批量移动等业务操作的核心数据来源,返回去重的选中便签ID,
- * 自动过滤根文件夹等无效ID并输出日志提示,保证业务数据的有效性
- * @return HashSet 所有选中便签的ID集合,无选中项则返回空集合
- */
public HashSet getSelectedItemIds() {
HashSet itemSet = new HashSet();
- // 遍历所有已记录的选中状态位置
- for (Integer position : mSelectedIndex.keySet()) {
- // 仅处理选中状态为true的有效项
- if (mSelectedIndex.get(position) == true) {
- // 获取当前位置对应的便签ID
- Long id = getItemId(position);
- // 过滤系统根文件夹的无效ID,避免业务操作异常
- if (id == Notes.ID_ROOT_FOLDER) {
- Log.d(TAG, "Wrong item id, should not happen");
- } else {
- // 将有效ID添加至结果集合
- itemSet.add(id);
+ Cursor cursor = getCursor();
+ if (cursor != null) {
+ for (Integer position : mSelectedIndex.keySet()) {
+ if (mSelectedIndex.get(position) == true) {
+ if (cursor.moveToPosition(position)) {
+ long id = cursor.getLong(NoteItemData.ID_COLUMN);
+ if (id != Notes.ID_ROOT_FOLDER) {
+ itemSet.add(id);
+ }
+ }
}
}
}
return itemSet;
}
- /**
- * 公开业务方法:获取所有选中项关联的小部件属性集合
- * 核心作用:批量操作后同步更新桌面小部件的核心数据来源,提取选中便签所绑定的小部件ID与类型,
- * 自动校验游标有效性,异常时输出错误日志并返回null,保证数据安全性
- * @return HashSet 选中项的小部件属性集合,无效数据时返回null
- */
public HashSet getSelectedWidget() {
HashSet itemSet = new HashSet();
- // 遍历所有已记录的选中状态位置
for (Integer position : mSelectedIndex.keySet()) {
- // 仅处理选中状态为true的有效项
if (mSelectedIndex.get(position) == true) {
- // 获取当前位置对应的数据库游标
Cursor c = (Cursor) getItem(position);
if (c != null) {
- // 创建小部件属性对象,封装核心数据
AppWidgetAttribute widget = new AppWidgetAttribute();
- // 将游标数据解析为业务模型,提取小部件关联属性
NoteItemData item = new NoteItemData(mContext, c);
widget.widgetId = item.getWidgetId();
widget.widgetType = item.getWidgetType();
- // 添加至结果集合
itemSet.add(widget);
/**
- * 重要说明:此处不主动关闭Cursor
- * Cursor由CursorAdapter统一管理生命周期,外部关闭会导致列表数据异常、游标越界等崩溃问题
+ * Don't close cursor here, only the adapter could close it
*/
} else {
- // 游标无效时输出错误日志,返回null标记异常状态
Log.e(TAG, "Invalid cursor");
return null;
}
@@ -247,18 +149,11 @@ public class NotesListAdapter extends CursorAdapter {
return itemSet;
}
- /**
- * 公开业务方法:统计当前选中的普通便签数量
- * 核心作用:列表页展示选中数量、判定全选状态的核心依据,仅统计选中状态为true的有效项,保证计数准确性
- * @return int 选中的普通便签数量,无选中项则返回0
- */
public int getSelectedCount() {
- // 获取所有已记录的选中状态值集合
Collection values = mSelectedIndex.values();
if (null == values) {
return 0;
}
- // 遍历状态值,统计选中状态为true的项数
Iterator iter = values.iterator();
int count = 0;
while (iter.hasNext()) {
@@ -269,80 +164,42 @@ public class NotesListAdapter extends CursorAdapter {
return count;
}
- /**
- * 公开业务方法:判断当前是否处于「全选」状态
- * 核心判定规则:选中数量大于0 且 选中数量等于列表中普通便签的总数,双条件保证判定准确性,
- * 避免无数据时误判为全选、选中部分项时误判为全选
- * @return boolean true=已全选所有普通便签,false=未全选/无选中项/无普通便签
- */
public boolean isAllSelected() {
int checkedCount = getSelectedCount();
return (checkedCount != 0 && checkedCount == mNotesCount);
}
- /**
- * 公开查询方法:判断指定位置的列表项是否被选中
- * 核心规则:未记录状态的位置默认视为「未选中」,避免空指针异常,保证业务逻辑的健壮性
- * @param position 待查询的列表项索引位置
- * @return boolean true=该位置已选中,false=该位置未选中/无状态记录
- */
public boolean isSelectedItem(final int position) {
- // 状态容器中无该位置记录时,默认返回未选中
if (null == mSelectedIndex.get(position)) {
return false;
}
- // 返回该位置的实际选中状态
return mSelectedIndex.get(position);
}
- /**
- * 重写父类回调方法:监听数据库内容变化的核心回调
- * 触发时机:当适配器绑定的数据库表数据发生增/删/改操作时,系统自动调用该方法
- * 核心逻辑:执行父类默认刷新逻辑后,重新统计普通便签数量,保证全选状态判定的准确性,
- * 是视图与数据库数据一致性的核心保障
- */
@Override
protected void onContentChanged() {
super.onContentChanged();
- // 数据变化后重新统计普通便签数量
calcNotesCount();
}
- /**
- * 重写父类核心方法:更换适配器绑定的游标数据
- * 触发时机:列表页切换文件夹、刷新数据、查询条件变更时主动调用
- * 核心逻辑:执行父类游标更换逻辑后,重新统计普通便签数量,适配新数据集的全选判定规则
- * @param cursor 新的数据库游标,封装了新的查询结果集
- */
@Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
- // 更换游标后重新统计普通便签数量
calcNotesCount();
}
- /**
- * 私有核心方法:统计列表中普通便签的总数
- * 核心业务规则:仅遍历统计{Notes.TYPE_NOTE}类型的项,排除文件夹、通话记录等非便签类型,
- * 自动校验游标有效性,异常时终止统计并输出日志,保证计数准确性,是全选功能的核心支撑方法
- */
private void calcNotesCount() {
- // 重置计数为0,避免累计统计错误
mNotesCount = 0;
- // 遍历适配器绑定的所有列表项数据
for (int i = 0; i < getCount(); i++) {
- // 获取当前位置对应的数据库游标
Cursor c = (Cursor) getItem(i);
if (c != null) {
- // 仅统计普通便签类型的项
if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) {
mNotesCount++;
}
} else {
- // 游标无效时输出错误日志,终止统计避免数据异常
Log.e(TAG, "Invalid cursor");
return;
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java b/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java
index d890f6c..3554ce4 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java
@@ -14,88 +14,53 @@
* limitations under the License.
*/
-// 包声明:小米便签 核心UI模块,该包承载应用所有可视化交互页面及自定义UI组件,本类为列表页核心子项组件
package net.micode.notes.ui;
-// -------------------------- 安卓系统核心依赖包 - 上下文/视图/布局/基础控件能力 --------------------------
-// 安卓应用全局上下文:提供资源访问、样式加载、布局渲染等基础能力,自定义控件必备依赖
import android.content.Context;
-// 安卓系统时间格式化工具类:提供相对时间格式化能力,将时间戳转为「几分钟前/昨天/上周」等友好展示格式
import android.text.format.DateUtils;
-// 安卓视图体系核心父类:控制控件的显示/隐藏、可见性状态、视图属性等核心操作
import android.view.View;
-// 安卓复选框控件:列表选择模式下的核心勾选组件,用于便签的批量操作场景
import android.widget.CheckBox;
-// 安卓图片展示控件:承载提醒图标、通话记录图标等所有图片类展示内容
import android.widget.ImageView;
-// 安卓线性布局容器:本类的父布局,提供横向/纵向的线性控件排列能力,作为列表项的根布局
import android.widget.LinearLayout;
-// 安卓文本展示控件:承载所有文本类展示内容,如标题、时间、通话名称等
import android.widget.TextView;
-// -------------------------- 小米便签业务层核心依赖 - 资源/数据/工具适配 --------------------------
-// 小米便签资源常量类:统一管理布局、字符串、样式、图片等所有本地资源ID引用
import net.micode.notes.R;
-// 小米便签数据层核心常量类:定义便签/文件夹的类型、特殊ID、业务状态等全局核心常量
import net.micode.notes.data.Notes;
-// 小米便签数据格式化工具类:封装便签摘要文本的格式化处理逻辑,统一文本展示规则
import net.micode.notes.tool.DataUtils;
-// 小米便签资源解析工具类:封装便签/文件夹的背景资源适配逻辑,提供不同状态的背景资源获取能力
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
+
/**
- * 小米便签 列表页核心自定义列表项组件
+ * 便签列表项视图
*
- * 继承安卓系统{LinearLayout}线性布局,隶属于MVC架构的UI层视图组件,是{NotesListActivity}列表页的最小展示单元;
- * 核心设计定位:作为便签列表的标准化子项容器,封装所有列表项的UI渲染逻辑、数据绑定逻辑、样式适配逻辑,解耦列表页与子项展示逻辑;
- * 核心业务职责:统一承载所有类型便签/文件夹的差异化展示、选择模式的UI适配、提醒状态的图标展示、背景样式的精准适配、通话记录的专属布局;
- * 技术实现特点:基于数据驱动视图的设计思想,通过统一的bind方法完成数据与视图的绑定,内置多分支逻辑适配不同业务类型,
- * 封装背景设置的复杂逻辑,对外提供简洁的调用与数据获取接口,是列表页高性能展示的核心基础组件。
- *
+ * 该类是便签列表的列表项视图,负责显示单个便签的信息,包括标题、时间、提醒图标、
+ * 锁定图标、分类标签等。它还包含复杂的背景圆角计算逻辑,根据首项、末项、单项动态设置背景。
*/
public class NotesListItem extends LinearLayout {
- /** 功能图标控件:展示闹钟提醒、通话记录等业务类型图标,不同场景展示对应功能标识 */
private ImageView mAlert;
- /** 主标题文本控件:核心文本展示区,适配展示便签摘要、文件夹名称+数量、通话记录内容等核心信息 */
+ private ImageView mLock;
+ private ImageView mPublic;
private TextView mTitle;
- /** 时间文本控件:展示便签/文件夹的最后修改时间,统一格式化为相对友好时间格式 */
private TextView mTime;
- /** 通话名称文本控件:通话记录专属展示区,仅通话记录项展示来电/去电的联系人名称,其他场景隐藏 */
private TextView mCallName;
- /** 数据模型载体:保存当前列表项绑定的业务数据,用于后续视图刷新与数据获取 */
+ private TextView mCategory;
private NoteItemData mItemData;
- /** 选择复选框控件:批量操作模式专属控件,用于勾选待操作的便签,非选择模式下默认隐藏 */
private CheckBox mCheckBox;
- /**
- * 构造方法:自定义控件的初始化入口
- * 核心初始化逻辑:加载列表项的基础布局文件,完成所有子控件的视图绑定,为后续数据绑定做准备;
- * 该方法仅在列表项创建时执行一次,保证控件初始化的性能最优
- * @param context 应用上下文对象,用于加载布局资源与查找子控件
- */
public NotesListItem(Context context) {
super(context);
- // 加载列表项的基础布局文件,将xml布局解析为当前线性布局的子视图
inflate(context, R.layout.note_item, this);
- // 绑定布局内所有子控件,通过ID精准获取并赋值给成员变量
mAlert = (ImageView) findViewById(R.id.iv_alert_icon);
+ mLock = (ImageView) findViewById(R.id.iv_lock_icon);
+ mPublic = (ImageView) findViewById(R.id.iv_public_icon);
mTitle = (TextView) findViewById(R.id.tv_title);
mTime = (TextView) findViewById(R.id.tv_time);
mCallName = (TextView) findViewById(R.id.tv_name);
+ mCategory = (TextView) findViewById(R.id.tv_category);
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
}
- /**
- * 核心公开绑定方法:数据与视图的统一绑定入口,列表项所有展示逻辑的核心处理方法
- * 核心业务能力:接收业务数据模型,根据数据类型/状态/模式完成所有视图的差异化渲染,包括控件显隐、文本赋值、图标切换、样式适配、勾选状态设置;
- * 该方法为列表项的核心对外接口,列表页通过调用此方法完成所有子项的内容展示
- * @param context 应用上下文,用于资源加载、样式设置、字符串格式化
- * @param data 当前列表项对应的业务数据模型,承载所有展示所需的业务字段
- * @param choiceMode 是否开启列表选择模式:true=批量操作模式,false=普通浏览模式
- * @param checked 选择模式下当前项的勾选状态:true=已勾选,false=未勾选
- */
- public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
- // 选择模式适配逻辑:仅普通便签在选择模式下展示勾选框,其他类型/模式均隐藏,防止误操作文件夹
+ public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked, String searchQuery) {
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
mCheckBox.setVisibility(View.VISIBLE);
mCheckBox.setChecked(checked);
@@ -103,106 +68,146 @@ public class NotesListItem extends LinearLayout {
mCheckBox.setVisibility(View.GONE);
}
- // 缓存当前绑定的业务数据模型,供后续背景设置与外部数据获取使用
mItemData = data;
-
- // ===== 分支一:通话记录专属文件夹(系统特殊固定ID),独立的展示样式 =====
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mCallName.setVisibility(View.GONE);
mAlert.setVisibility(View.VISIBLE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
- // 标题展示规则:固定文件夹名称 + 文件夹内的通话记录数量,格式化展示
mTitle.setText(context.getString(R.string.call_record_folder_name)
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
- // 展示通话记录专属图标,明确业务类型标识
mAlert.setImageResource(R.drawable.call_record);
- }
- // ===== 分支二:通话记录子项(归属通话记录文件夹),通话类专属展示样式 =====
- else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
+ } else if (data.getId() == Notes.ID_TRASH_FOLER) {
+ mCallName.setVisibility(View.GONE);
+ mAlert.setVisibility(View.VISIBLE);
+ mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
+ mTitle.setText("回收站"
+ + context.getString(R.string.format_folder_files_count, data.getNotesCount()));
+ // 使用现有的clock图标作为临时垃圾桶图标
+ mAlert.setImageResource(R.drawable.clock);
+ } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
mCallName.setVisibility(View.VISIBLE);
mCallName.setText(data.getCallName());
mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem);
- // 标题展示格式化后的通话记录摘要内容,保证文本展示的规范性
- mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
- // 提醒状态适配:有闹钟提醒则展示闹钟图标,无则隐藏
+ String formattedSnippet = DataUtils.getFormattedSnippet(data.getSnippet());
+ mTitle.setText(highlightText(formattedSnippet, searchQuery, context));
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
} else {
mAlert.setVisibility(View.GONE);
}
- }
- // ===== 分支三:普通业务类型(普通文件夹/普通便签),通用展示样式 =====
- else {
- mCallName.setVisibility(View.GONE);
- mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
+ } else {
+ mCallName.setVisibility(View.GONE);
+ mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
- // 子分支1:普通文件夹类型,展示文件夹名称+包含便签数量
- if (data.getType() == Notes.TYPE_FOLDER) {
- mTitle.setText(data.getSnippet()
- + context.getString(R.string.format_folder_files_count,
- data.getNotesCount()));
- mAlert.setVisibility(View.GONE);
- }
- // 子分支2:普通便签类型,展示便签核心摘要内容
- else {
- mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
- // 提醒状态适配:有闹钟提醒展示闹钟图标,无则隐藏
- if (data.hasAlert()) {
- mAlert.setImageResource(R.drawable.clock);
- mAlert.setVisibility(View.VISIBLE);
- } else {
+ if (data.getType() == Notes.TYPE_FOLDER) {
+ mTitle.setText(data.getSnippet()
+ + context.getString(R.string.format_folder_files_count,
+ data.getNotesCount()));
mAlert.setVisibility(View.GONE);
+ } else {
+ // 显示标题,如果标题为空则显示内容摘要
+ String displayText;
+ if (data.getTitle() != null && !data.getTitle().isEmpty()) {
+ displayText = data.getTitle();
+ } else {
+ String formattedSnippet = DataUtils.getFormattedSnippet(data.getSnippet());
+ displayText = formattedSnippet;
+ }
+ mTitle.setText(highlightText(displayText, searchQuery, context));
+ if (data.hasAlert()) {
+ mAlert.setImageResource(R.drawable.clock);
+ mAlert.setVisibility(View.VISIBLE);
+ } else {
+ mAlert.setVisibility(View.GONE);
+ }
}
}
- }
- // 统一设置最后修改时间:将时间戳转为「几分钟前/昨天」等相对友好的时间格式,提升用户体验
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
- // 根据当前绑定的数据模型,完成列表项背景样式的精准适配
+ // 设置分类标签,优先使用标题进行分类
+ String contentForCategory = data.getTitle();
+ if (contentForCategory == null || contentForCategory.isEmpty()) {
+ contentForCategory = data.getSnippet();
+ }
+ String category = net.micode.notes.tool.CategoryUtil.autoCategorize(contentForCategory);
+ mCategory.setText(category);
+
+ // 处理锁定图标
+ if (data.isLocked() && data.getType() == Notes.TYPE_NOTE) {
+ mLock.setVisibility(View.VISIBLE);
+ mLock.setImageResource(R.drawable.clock); // 使用clock图标作为锁图标
+ } else {
+ mLock.setVisibility(View.GONE);
+ }
+
+ // 处理公开图标
+ if (data.isPublic() && data.getType() == Notes.TYPE_NOTE) {
+ mPublic.setVisibility(View.VISIBLE);
+ mPublic.setImageResource(R.drawable.call_record); // 使用call_record图标作为公开图标,与置顶图标区分
+ } else {
+ mPublic.setVisibility(View.GONE);
+ }
+
+ // 处理置顶和提醒图标
+ if (data.isPinned() && data.getType() == Notes.TYPE_NOTE) {
+ mAlert.setVisibility(View.VISIBLE);
+ mAlert.setImageResource(R.drawable.selected); // 使用selected图标作为置顶图标
+ } else if (data.hasAlert() && data.getType() == Notes.TYPE_NOTE) {
+ mAlert.setVisibility(View.VISIBLE);
+ mAlert.setImageResource(R.drawable.call_record); // 使用call_record图标作为提醒图标
+ } else {
+ mAlert.setVisibility(View.GONE);
+ }
+
setBackground(data);
}
- /**
- * 私有核心方法:列表项背景样式的统一适配处理
- * 核心业务逻辑:封装复杂的背景适配规则,区分普通便签与文件夹的不同背景策略;
- * 普通便签根据列表中的位置(首项/中项/尾项/单独项)+ 背景色ID,匹配对应的背景资源;
- * 文件夹统一使用固定背景,简化适配逻辑,保证列表样式的统一性与美观性
- * @param data 当前列表项绑定的业务数据模型,提供背景色ID、类型、列表位置等适配所需字段
- */
private void setBackground(NoteItemData data) {
- // 获取当前数据模型的背景色标识ID,作为背景资源匹配的核心依据
int id = data.getBgColorId();
-
- // 普通便签的背景适配逻辑:多场景精准匹配,保证列表连贯的视觉效果
if (data.getType() == Notes.TYPE_NOTE) {
if (data.isSingle() || data.isOneFollowingFolder()) {
- // 场景1:列表中唯一项 / 文件夹下的唯一项 → 使用独立完整背景
setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id));
} else if (data.isLast()) {
- // 场景2:列表中的最后一项 → 使用底部收尾样式背景
setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id));
} else if (data.isFirst() || data.isMultiFollowingFolder()) {
- // 场景3:列表中的第一项 / 文件夹下的多项首项 → 使用顶部起始样式背景
setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id));
} else {
- // 场景4:列表中的中间项 → 使用标准连贯样式背景
setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id));
}
- }
- // 文件夹类型统一适配逻辑:所有文件夹(含通话记录文件夹)使用固定背景样式
- else {
+ } else {
setBackgroundResource(NoteItemBgResources.getFolderBgRes());
}
}
- /**
- * 公开数据获取方法:获取当前列表项绑定的业务数据模型
- * 核心作用:为列表页提供数据访问接口,列表页可通过该方法获取选中项、点击项的业务数据,完成跳转/删除/编辑等后续操作;
- * 是视图组件与业务逻辑之间的核心数据桥梁
- * @return NoteItemData 当前列表项绑定的完整业务数据模型
- */
+ // 高亮匹配的文本
+ private CharSequence highlightText(String text, String searchQuery, Context context) {
+ if (text == null || searchQuery == null || searchQuery.isEmpty()) {
+ return text;
+ }
+
+ android.text.SpannableString spannable = new android.text.SpannableString(text);
+ try {
+ String lowerText = text.toLowerCase();
+ String lowerQuery = searchQuery.toLowerCase();
+ int startIndex = lowerText.indexOf(lowerQuery);
+
+ while (startIndex != -1) {
+ int endIndex = startIndex + searchQuery.length();
+ spannable.setSpan(
+ new android.text.style.BackgroundColorSpan(context.getResources().getColor(R.color.user_query_highlight)),
+ startIndex, endIndex, android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ startIndex = lowerText.indexOf(lowerQuery, endIndex);
+ }
+ } catch (Exception e) {
+ // 处理可能的异常,比如空指针或索引越界
+ return text;
+ }
+
+ return spannable;
+ }
+
public NoteItemData getItemData() {
return mItemData;
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java b/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java
index 0151e15..3183386 100644
--- a/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java
+++ b/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java
@@ -14,176 +14,184 @@
* limitations under the License.
*/
-// 包声明:小米便签 核心UI业务模块,承载应用所有可视化交互页面,本类为应用设置页核心实现
package net.micode.notes.ui;
-// -------------------------- 安卓系统核心依赖包 - 账号/页面/弹窗/广播/数据存储能力 --------------------------
-// 安卓账号体系核心类:封装Google账号的账号名、账号类型等基础信息载体
import android.accounts.Account;
-// 安卓账号管理核心服务类:系统级账号管理器,负责设备上所有账号的查询、管理、鉴权等操作
import android.accounts.AccountManager;
-// 安卓顶部导航栏核心类:配置页面导航样式、返回按钮、标题等ActionBar相关属性
import android.app.ActionBar;
-// 安卓系统弹窗核心类:构建标准化的对话框,承载账号选择、确认提示等交互弹窗
import android.app.AlertDialog;
-// 安卓广播核心组件:监听系统/应用内部的广播消息,本类用于监听同步服务状态变更广播
import android.content.BroadcastReceiver;
-// 安卓数据封装类:封装键值对数据,用于ContentProvider执行数据库字段更新操作
import android.content.ContentValues;
-// 安卓应用全局上下文:提供资源访问、组件通信、偏好设置读写等核心基础能力
import android.content.Context;
-// 安卓对话框交互核心接口:监听弹窗选项的点击事件,处理用户选择逻辑
import android.content.DialogInterface;
-// 安卓组件通信核心类:封装页面跳转指令、广播指令、参数传递,实现跨组件通信
import android.content.Intent;
-// 安卓广播过滤核心类:筛选需要监听的广播动作,精准匹配目标广播消息
import android.content.IntentFilter;
-// 安卓轻量级存储核心类:键值对持久化存储,用于保存应用偏好配置,非数据库存储
import android.content.SharedPreferences;
-// 安卓页面状态存储类:保存页面销毁重建时的临时数据,保证页面状态不丢失
import android.os.Bundle;
-// 安卓系统偏好设置组件:构建设置页面的标准化UI控件,封装设置项的展示与交互逻辑
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
-// 安卓文本工具类:封装字符串判空、内容对比等常用操作,避免空指针与硬编码判断
import android.text.TextUtils;
-// 安卓系统日期格式化工具类:标准化格式化时间戳为指定样式的字符串,适配多语言展示
import android.text.format.DateFormat;
-// 安卓布局加载核心类:将xml布局文件解析为Java视图对象,加载自定义布局与弹窗样式
import android.view.LayoutInflater;
-// 安卓页面菜单核心类:配置ActionBar右侧菜单的创建与点击事件处理
import android.view.Menu;
import android.view.MenuItem;
-// 安卓视图体系核心类:操作所有可视化控件的父类,实现控件的点击、赋值、显隐等操作
import android.view.View;
import android.widget.Button;
+import android.widget.EditText;
import android.widget.TextView;
-// 安卓轻量级提示组件:展示短时操作结果提示,无焦点不阻塞用户交互
import android.widget.Toast;
-// -------------------------- 小米便签业务层核心依赖 - 资源/数据/同步服务 --------------------------
-// 小米便签资源常量类:统一管理布局、字符串、颜色、样式等所有本地资源ID引用
import net.micode.notes.R;
-// 小米便签数据层核心常量类:定义便签ContentProvider URI、数据表字段、业务常量等核心配置
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
-// 小米便签核心同步服务类:封装便签与Google Task的双向同步逻辑,提供同步启停、状态查询等核心能力
import net.micode.notes.gtask.remote.GTaskSyncService;
+
/**
- * 小米便签 应用核心设置页面Activity
- *
- * 继承安卓系统{PreferenceActivity},隶属于MVC架构的UI层核心业务页面,是便签应用的全局配置中心;
- * 核心设计定位:统一承载应用所有用户可配置项,聚焦「Google账号绑定与GTask云同步」核心能力,兼顾基础偏好配置;
- * 核心业务职责:Google账号的绑定/切换/移除管理、手动同步/取消同步操作、同步状态实时展示、同步时间持久化、
- * 偏好配置读写、同步服务广播监听、页面导航与交互反馈,是便签应用云同步能力的唯一配置入口;
- * 技术实现特点:通过SharedPreferences实现轻量级配置持久化,通过广播接收器实现同步状态实时刷新,
- * 通过异步线程处理数据库更新避免主线程阻塞,通过系统账号管理器实现Google账号的标准化管理。
- *
+ * 便签设置活动类
+ * 负责管理应用的各种设置选项,包括账户同步、密码设置、好友管理和背景更换等功能
+ * 提供用户界面来配置应用行为和个性化选项
+ *
+ * 架构设计:
+ * - 继承自PreferenceActivity,使用传统偏好设置界面
+ * - 支持Google账户同步配置
+ * - 集成密码设置功能,保护用户隐私
+ * - 提供好友管理入口
+ * - 支持背景更换功能
+ *
+ * 核心功能:
+ * - 账户同步管理:添加、修改、删除Google账户
+ * - 密码设置:设置和修改应用密码
+ * - 好友管理:进入好友管理界面
+ * - 背景更换:进入背景设置界面
+ * - 同步状态显示:显示上次同步时间和同步进度
*/
public class NotesPreferenceActivity extends PreferenceActivity {
/**
- * 全局常量:SharedPreferences偏好设置存储文件名,应用所有配置项均持久化存储在该文件中,单文件统一管理
+ * 偏好设置文件名
*/
public static final String PREFERENCE_NAME = "notes_preferences";
+
/**
- * 全局常量:偏好配置存储键 - 当前绑定的Google同步账号名,核心云同步关联标识
+ * 同步账户名偏好键
*/
public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name";
+
/**
- * 全局常量:偏好配置存储键 - 最后一次同步成功的时间戳,用于展示同步历史记录
+ * 上次同步时间偏好键
*/
public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time";
+
/**
- * 全局常量:偏好配置存储键 - 便签背景色随机展示开关,控制新建便签的背景色展示策略
+ * 背景颜色设置偏好键
*/
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
/**
- * 私有常量:偏好设置分类标识 - 账号同步相关配置项的分类容器Key,用于页面UI组件定位
+ * 密码偏好键
*/
- private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
+ public static final String PREFERENCE_PASSWORD_KEY = "pref_key_password";
+
/**
- * 私有常量:系统账号过滤标识 - 添加新账号时的权限过滤关键字,用于精准筛选Google账号类型
+ * 密码设置状态偏好键
*/
+ public static final String PREFERENCE_PASSWORD_SET_KEY = "pref_key_password_set";
+
+ private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
+ private static final String PREFERENCE_PASSWORD_SETTING_KEY = "pref_password_setting_key";
+
private static final String AUTHORITIES_FILTER_KEY = "authorities";
- /**
- * 页面核心控件:账号同步相关的偏好设置分类容器,承载账号选择配置项的父组件
- */
private PreferenceCategory mAccountCategory;
- /**
- * 核心广播接收器:监听GTaskSyncService的同步状态广播,实现同步中/同步完成的UI实时刷新
- */
+
private GTaskReceiver mReceiver;
- /**
- * 账号缓存数组:存储操作前的设备Google账号列表,用于对比判断是否新增账号
- */
+
private Account[] mOriAccounts;
- /**
- * 状态标记位:标记是否触发了系统添加账号的操作,用于页面恢复时的账号自动绑定逻辑
- */
+
private boolean mHasAddedAccount;
- /**
- * 重写页面生命周期:页面创建初始化入口方法
- * 执行时机:页面第一次被创建时调用,仅执行一次
- * 核心初始化逻辑:配置ActionBar导航样式、加载偏好设置页面布局、初始化核心控件、注册同步状态广播接收器、
- * 添加页面自定义头部布局,完成页面所有基础初始化工作,为后续交互做准备
- * @param icicle 页面状态存储Bundle,恢复重建时的临时数据载体
- */
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
- /* 配置ActionBar导航:启用左上角返回按钮,跳转回便签列表页 */
+ /* using the app icon for navigation */
getActionBar().setDisplayHomeAsUpEnabled(true);
- // 从xml资源加载设置页面的标准化偏好配置UI结构,页面主体内容初始化
addPreferencesFromResource(R.xml.preferences);
- // 根据标识获取账号同步分类容器控件,完成核心控件绑定
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
- // 初始化同步状态广播接收器,注册监听同步服务的状态变更广播
+
+ // Add password setting preference
+ Preference passwordPreference = new Preference(this);
+ passwordPreference.setTitle(getString(R.string.preferences_password_title));
+ passwordPreference.setSummary(getString(R.string.preferences_password_summary));
+ passwordPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ showPasswordSettingDialog();
+ return true;
+ }
+ });
+
+ // Add friend management preference
+ Preference friendPreference = new Preference(this);
+ friendPreference.setTitle("好友");
+ friendPreference.setSummary("管理和查看好友的公开便签");
+ friendPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ // 启动好友管理活动
+ Intent intent = new Intent(NotesPreferenceActivity.this, FriendManagementActivity.class);
+ startActivity(intent);
+ return true;
+ }
+ });
+
+ // Add change background preference
+ Preference changeBackgroundPreference = new Preference(this);
+ changeBackgroundPreference.setTitle("更换背景");
+ changeBackgroundPreference.setSummary("更换便签界面的背景图片");
+ changeBackgroundPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ // 直接启动NoteEditActivity并传递更换背景的标志
+ Intent intent = new Intent(NotesPreferenceActivity.this, NoteEditActivity.class);
+ intent.putExtra("CHANGE_BACKGROUND", true);
+ startActivity(intent);
+ return true;
+ }
+ });
+
+ PreferenceCategory generalCategory = (PreferenceCategory) getPreferenceScreen().getPreference(1);
+ generalCategory.addPreference(passwordPreference);
+ generalCategory.addPreference(friendPreference);
+ generalCategory.addPreference(changeBackgroundPreference);
+
mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
registerReceiver(mReceiver, filter);
- // 初始化账号缓存数组与状态标记位,默认无账号无新增操作
mOriAccounts = null;
- mHasAddedAccount = false;
- // 加载自定义页面头部布局并添加至列表顶部,丰富页面视觉层级
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
getListView().addHeaderView(header, null, true);
}
- /**
- * 重写页面生命周期:页面恢复可见状态回调方法
- * 执行时机:页面从后台切回前台、弹窗关闭后、页面创建后首次展示时调用
- * 核心业务逻辑:检测是否触发过添加账号操作,若有则自动绑定新增的Google账号;刷新页面所有UI控件状态,
- * 保证页面展示的账号信息、同步按钮、同步状态均为最新数据,是页面数据一致性的核心保障
- */
@Override
protected void onResume() {
super.onResume();
- // 检测添加账号标记位,处理新增账号的自动绑定逻辑
+ // need to set sync account automatically if user has added a new
+ // account
if (mHasAddedAccount) {
Account[] accounts = getGoogleAccounts();
- // 对比操作前后的账号数量,判断是否有新账号添加成功
if (mOriAccounts != null && accounts.length > mOriAccounts.length) {
for (Account accountNew : accounts) {
boolean found = false;
- // 遍历原始账号列表,过滤已存在的账号
for (Account accountOld : mOriAccounts) {
if (TextUtils.equals(accountOld.name, accountNew.name)) {
found = true;
break;
}
}
- // 匹配到新增账号,自动完成绑定并终止遍历
if (!found) {
setSyncAccount(accountNew.name);
break;
@@ -192,53 +200,36 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
}
- // 统一刷新页面所有UI组件状态,保证数据与视图一致
refreshUI();
}
- /**
- * 重写页面生命周期:页面销毁回收资源回调方法
- * 执行时机:页面退出、被销毁时调用,仅执行一次
- * 核心优化逻辑:注销已注册的广播接收器,释放系统资源,防止内存泄漏,是安卓组件开发的必做优化项
- */
@Override
protected void onDestroy() {
- // 安全注销广播接收器,判空避免空指针异常
if (mReceiver != null) {
unregisterReceiver(mReceiver);
}
super.onDestroy();
}
- /**
- * 私有核心方法:加载并初始化账号同步偏好配置项
- * 核心业务逻辑:动态构建账号选择配置项,绑定点击事件,实现「未绑定账号展示选择弹窗、已绑定账号展示变更弹窗」的交互逻辑,
- * 同步中禁用账号操作防止数据异常,是账号管理能力的核心实现
- */
private void loadAccountPreference() {
- // 清空分类容器内原有配置项,避免重复添加导致的UI重复展示问题
mAccountCategory.removeAll();
- // 新建账号选择偏好配置项,初始化展示文案与交互行为
Preference accountPref = new Preference(this);
final String defaultAccount = getSyncAccountName(this);
accountPref.setTitle(getString(R.string.preferences_account_title));
accountPref.setSummary(getString(R.string.preferences_account_summary));
-
- // 绑定配置项点击事件,处理账号选择与变更的核心交互逻辑
accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
- // 同步中禁止账号操作,防止同步数据与账号信息不一致导致异常
if (!GTaskSyncService.isSyncing()) {
if (TextUtils.isEmpty(defaultAccount)) {
- // 未绑定账号:展示账号选择弹窗,供用户选择绑定
+ // the first time to set account
showSelectAccountAlertDialog();
} else {
- // 已绑定账号:展示账号变更确认弹窗,提示风险并提供操作选项
+ // if the account has already been set, we need to promp
+ // user about the risk
showChangeAccountConfirmAlertDialog();
}
} else {
- // 同步中操作拦截,展示友好的吐司提示
Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show();
@@ -247,23 +238,15 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
});
- // 将配置项添加至分类容器,完成UI渲染
mAccountCategory.addPreference(accountPref);
}
- /**
- * 私有核心方法:加载并初始化同步按钮与同步状态展示控件
- * 核心业务逻辑:根据同步服务的运行状态,动态切换按钮文本与点击事件(立即同步/取消同步);
- * 展示同步进度或最后同步时间;无绑定账号时禁用同步按钮,是同步能力的核心交互入口实现
- */
private void loadSyncButton() {
- // 获取页面同步按钮与同步状态文本控件,完成视图绑定
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
- // 根据同步服务状态,动态配置按钮行为与文本
+ // set button state
if (GTaskSyncService.isSyncing()) {
- // 同步中状态:按钮文本为取消同步,点击触发同步终止逻辑
syncButton.setText(getString(R.string.preferences_button_sync_cancel));
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
@@ -271,7 +254,6 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
});
} else {
- // 未同步状态:按钮文本为立即同步,点击触发同步启动逻辑
syncButton.setText(getString(R.string.preferences_button_sync_immediately));
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
@@ -279,16 +261,13 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
});
}
- // 无绑定账号时禁用同步按钮,避免无账号同步的无效操作
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
- // 配置同步状态文本展示逻辑,区分同步中与历史同步记录
+ // set last sync time
if (GTaskSyncService.isSyncing()) {
- // 同步中:展示实时同步进度文案
lastSyncTimeView.setText(GTaskSyncService.getProgressString());
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
- // 未同步:读取最后同步时间戳,展示格式化的历史同步记录
long lastSyncTime = getLastSyncTime(this);
if (lastSyncTime != 0) {
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
@@ -296,185 +275,137 @@ public class NotesPreferenceActivity extends PreferenceActivity {
lastSyncTime)));
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
- // 无同步记录:隐藏状态文本,简化页面展示
lastSyncTimeView.setVisibility(View.GONE);
}
}
}
- /**
- * 私有统一刷新方法:页面UI状态刷新总入口
- * 核心职责:统一调用账号配置项与同步按钮的加载方法,实现页面所有核心控件的状态刷新,
- * 简化多处刷新逻辑的调用成本,保证刷新行为的一致性
- */
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
- /**
- * 私有弹窗方法:展示Google账号选择对话框
- * 核心业务逻辑:展示设备上已有的Google账号列表供用户选择绑定;提供添加新账号入口跳转系统设置;
- * 选择账号后自动完成绑定并刷新页面,是账号绑定的核心交互弹窗实现
- */
- private void showSelectAccountAlertDialog() {
- AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
-
- // 加载自定义弹窗标题布局,初始化弹窗头部文案
- View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
- TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
- titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
- TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
- subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
-
- dialogBuilder.setCustomTitle(titleView);
- dialogBuilder.setPositiveButton(null, null);
-
- // 获取设备上所有Google账号,构建账号选择列表
- Account[] accounts = getGoogleAccounts();
- String defAccount = getSyncAccountName(this);
-
- // 缓存当前账号列表,用于后续新增账号判断
- mOriAccounts = accounts;
- mHasAddedAccount = false;
-
- // 存在Google账号时,构建单选列表供用户选择
- if (accounts.length > 0) {
- CharSequence[] items = new CharSequence[accounts.length];
- final CharSequence[] itemMapping = items;
- int checkedItem = -1;
- int index = 0;
- for (Account account : accounts) {
- if (TextUtils.equals(account.name, defAccount)) {
- checkedItem = index;
- }
- items[index++] = account.name;
- }
- // 绑定列表选择事件,选择后完成账号绑定并关闭弹窗
- dialogBuilder.setSingleChoiceItems(items, checkedItem,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- setSyncAccount(itemMapping[which].toString());
- dialog.dismiss();
- refreshUI();
- }
- });
- }
-
- // 加载添加新账号的自定义视图,提供账号新增入口
- View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
- dialogBuilder.setView(addAccountView);
-
- // 展示弹窗并绑定添加账号点击事件,跳转系统账号添加页面
- final AlertDialog dialog = dialogBuilder.show();
- addAccountView.setOnClickListener(new View.OnClickListener() {
- public void onClick(View v) {
- mHasAddedAccount = true;
- Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
- intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
- "gmail-ls"
- });
- startActivityForResult(intent, -1);
- dialog.dismiss();
- }
- });
- }
+ private void showSelectAccountAlertDialog() {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
- /**
- * 私有弹窗方法:展示账号变更/移除确认对话框
- * 核心业务逻辑:针对已绑定账号的场景,展示风险提示与操作选项(更改账号/移除账号/取消);
- * 处理账号变更与解绑逻辑,是账号管理的核心确认交互弹窗实现
- */
- private void showChangeAccountConfirmAlertDialog() {
- AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
-
- // 加载自定义弹窗标题布局,展示当前绑定账号与风险提示
- View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
- TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
- titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
- getSyncAccountName(this)));
- TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
- subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
- dialogBuilder.setCustomTitle(titleView);
-
- // 构建弹窗操作选项,绑定点击事件处理不同操作逻辑
- CharSequence[] menuItemArray = new CharSequence[] {
- getString(R.string.preferences_menu_change_account),
- getString(R.string.preferences_menu_remove_account),
- getString(R.string.preferences_menu_cancel)
- };
- dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- if (which == 0) {
- // 选择更改账号:跳转账号选择弹窗
- showSelectAccountAlertDialog();
- } else if (which == 1) {
- // 选择移除账号:执行账号解绑逻辑
- removeSyncAccount();
- refreshUI();
- }
- }
- });
- dialogBuilder.show();
- }
+ View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
+ TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
+ titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
+ TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
+ subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
+
+ dialogBuilder.setCustomTitle(titleView);
+ dialogBuilder.setPositiveButton(null, null);
+
+ Account[] accounts = getGoogleAccounts();
+ String defAccount = getSyncAccountName(this);
+
+ mOriAccounts = accounts;
+ mHasAddedAccount = false;
+
+ if (accounts.length > 0) {
+ CharSequence[] items = new CharSequence[accounts.length];
+ final CharSequence[] itemMapping = items;
+ int checkedItem = -1;
+ int index = 0;
+ for (Account account : accounts) {
+ if (TextUtils.equals(account.name, defAccount)) {
+ checkedItem = index;
+ }
+ items[index++] = account.name;
+ }
+ dialogBuilder.setSingleChoiceItems(items, checkedItem,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ setSyncAccount(itemMapping[which].toString());
+ dialog.dismiss();
+ refreshUI();
+ }
+ });
+ }
+
+ View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
+ dialogBuilder.setView(addAccountView);
+
+ final AlertDialog dialog = dialogBuilder.show();
+ addAccountView.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ mHasAddedAccount = true;
+ Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
+ intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
+ "gmail-ls"
+ });
+ startActivityForResult(intent, -1);
+ dialog.dismiss();
+ }
+ });
+ }
+
+ private void showChangeAccountConfirmAlertDialog() {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
+
+ View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
+ TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
+ titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
+ getSyncAccountName(this)));
+ TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
+ subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
+ dialogBuilder.setCustomTitle(titleView);
+
+ CharSequence[] menuItemArray = new CharSequence[] {
+ getString(R.string.preferences_menu_change_account),
+ getString(R.string.preferences_menu_remove_account),
+ getString(R.string.preferences_menu_cancel)
+ };
+ dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == 0) {
+ showSelectAccountAlertDialog();
+ } else if (which == 1) {
+ removeSyncAccount();
+ refreshUI();
+ }
+ }
+ });
+ dialogBuilder.show();
+ }
- /**
- * 私有工具方法:获取设备上所有已登录的Google账号列表
- * 核心实现:通过系统账号管理器,根据Google账号类型精准筛选,返回纯净的Google账号数组,
- * 是账号管理能力的基础数据来源
- * @return Account[] 设备上所有com.google类型的账号数组,无账号则返回空数组
- */
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google");
}
- /**
- * 私有核心方法:设置当前绑定的Google同步账号
- * 核心业务逻辑:更新偏好配置中的账号信息、清空历史同步时间、异步清理本地便签的同步关联字段、
- * 展示操作成功提示;账号未变化时不执行任何操作,避免无效处理,是账号绑定的核心业务逻辑实现
- * @param account 待绑定的Google账号名
- */
- private void setSyncAccount(String account) {
- // 账号未发生变化时,直接返回避免无效操作
- if (!getSyncAccountName(this).equals(account)) {
- // 写入偏好配置,持久化存储绑定的账号名
- SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
- SharedPreferences.Editor editor = settings.edit();
- if (account != null) {
- editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
- } else {
- editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
- }
- editor.commit();
-
- // 账号变更后清空历史同步时间,保证同步记录的准确性
- setLastSyncTime(this, 0);
-
- // 异步线程清理本地便签的同步关联字段,避免主线程阻塞导致页面卡顿
- new Thread(new Runnable() {
- public void run() {
- ContentValues values = new ContentValues();
- values.put(NoteColumns.GTASK_ID, "");
- values.put(NoteColumns.SYNC_ID, 0);
- getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
- }
- }).start();
-
- // 展示账号绑定成功的友好提示
- Toast.makeText(NotesPreferenceActivity.this,
- getString(R.string.preferences_toast_success_set_accout, account),
- Toast.LENGTH_SHORT).show();
- }
- }
+ private void setSyncAccount(String account) {
+ if (!getSyncAccountName(this).equals(account)) {
+ SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = settings.edit();
+ if (account != null) {
+ editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
+ } else {
+ editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
+ }
+ editor.commit();
+
+ // clean up last sync time
+ setLastSyncTime(this, 0);
+
+ // clean up local gtask related info
+ new Thread(new Runnable() {
+ public void run() {
+ ContentValues values = new ContentValues();
+ values.put(NoteColumns.GTASK_ID, "");
+ values.put(NoteColumns.SYNC_ID, 0);
+ getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
+ }
+ }).start();
+
+ Toast.makeText(NotesPreferenceActivity.this,
+ getString(R.string.preferences_toast_success_set_accout, account),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
- /**
- * 私有核心方法:移除当前绑定的Google同步账号,完成账号解绑
- * 核心业务逻辑:清理偏好配置中的账号名与同步时间、异步清理本地便签的同步关联字段,
- * 彻底解除本地便签与原账号的云同步关联,是账号解绑的核心业务逻辑实现
- */
private void removeSyncAccount() {
- // 读取偏好配置并清理账号与同步时间相关配置项
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) {
@@ -485,7 +416,7 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
editor.commit();
- // 异步线程清理本地便签的同步关联字段,释放系统资源
+ // clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
@@ -496,24 +427,12 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}).start();
}
- /**
- * 公开静态工具方法:获取当前绑定的Google同步账号名
- * 全局通用能力:供应用其他组件调用,获取同步账号信息,解耦配置读取逻辑
- * @param context 应用上下文,用于访问偏好配置
- * @return String 当前绑定的账号名,无绑定则返回空字符串
- */
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
- /**
- * 公开静态工具方法:设置最后一次同步的时间戳
- * 全局通用能力:供同步服务调用,持久化同步完成时间,解耦配置写入逻辑
- * @param context 应用上下文,用于访问偏好配置
- * @param time 同步完成的时间戳(毫秒值)
- */
public static void setLastSyncTime(Context context, long time) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
@@ -522,37 +441,104 @@ public class NotesPreferenceActivity extends PreferenceActivity {
editor.commit();
}
- /**
- * 公开静态工具方法:获取最后一次同步的时间戳
- * 全局通用能力:供应用其他组件调用,读取同步历史时间,解耦配置读取逻辑
- * @param context 应用上下文,用于访问偏好配置
- * @return long 最后同步时间戳,无记录则返回0
- */
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
- /**
- * 内部私有广播接收器类:监听GTask同步服务的状态变更广播
- * 核心设计定位:页面与同步服务的通信桥梁,无耦合监听同步状态变化;
- * 核心职责:接收同步中/同步完成的广播消息,触发页面UI刷新,展示实时同步进度,
- * 是同步状态实时更新的核心实现,生命周期与宿主页面一致
- */
+ public static boolean isPasswordSet(Context context) {
+ SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
+ Context.MODE_PRIVATE);
+ return settings.getBoolean(PREFERENCE_PASSWORD_SET_KEY, false);
+ }
+
+ public static String getPassword(Context context) {
+ SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
+ Context.MODE_PRIVATE);
+ return settings.getString(PREFERENCE_PASSWORD_KEY, "");
+ }
+
+ private void setPassword(String password) {
+ SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = settings.edit();
+ editor.putString(PREFERENCE_PASSWORD_KEY, password);
+ editor.putBoolean(PREFERENCE_PASSWORD_SET_KEY, !TextUtils.isEmpty(password));
+ editor.commit();
+ }
+
+ private void showPasswordSettingDialog() {
+ final boolean hasPassword = isPasswordSet(this);
+
+ View view = LayoutInflater.from(this).inflate(R.layout.password_setting_dialog, null);
+ final EditText currentPasswordEdit = (EditText) view.findViewById(R.id.current_password);
+ final EditText newPasswordEdit = (EditText) view.findViewById(R.id.new_password);
+ final EditText confirmPasswordEdit = (EditText) view.findViewById(R.id.confirm_password);
+
+ final TextView currentPasswordLabel = (TextView) view.findViewById(R.id.current_password_label);
+
+ if (!hasPassword) {
+ currentPasswordLabel.setVisibility(View.GONE);
+ currentPasswordEdit.setVisibility(View.GONE);
+ }
+
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
+ dialogBuilder.setTitle(hasPassword ? getString(R.string.preferences_password_change_title) : getString(R.string.preferences_password_set_title));
+ dialogBuilder.setView(view);
+
+ dialogBuilder.setPositiveButton(getString(R.string.preferences_button_confirm), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ String currentPassword = currentPasswordEdit.getText().toString();
+ String newPassword = newPasswordEdit.getText().toString();
+ String confirmPassword = confirmPasswordEdit.getText().toString();
+
+ if (hasPassword) {
+ // 验证当前密码
+ if (!currentPassword.equals(getPassword(NotesPreferenceActivity.this))) {
+ Toast.makeText(NotesPreferenceActivity.this,
+ getString(R.string.preferences_password_incorrect),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+
+ // 验证新密码和确认密码
+ if (TextUtils.isEmpty(newPassword)) {
+ Toast.makeText(NotesPreferenceActivity.this,
+ getString(R.string.preferences_password_empty),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (!newPassword.equals(confirmPassword)) {
+ Toast.makeText(NotesPreferenceActivity.this,
+ getString(R.string.preferences_password_not_match),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 设置密码
+ setPassword(newPassword);
+ Toast.makeText(NotesPreferenceActivity.this,
+ getString(R.string.preferences_password_set_success),
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ dialogBuilder.setNegativeButton(getString(R.string.preferences_button_cancel), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+
+ dialogBuilder.show();
+ }
+
private class GTaskReceiver extends BroadcastReceiver {
- /**
- * 重写广播接收回调方法:处理同步服务的状态广播
- * 核心逻辑:接收到广播后立即刷新页面UI,同步中时更新进度文本,保证页面状态与同步服务一致
- * @param context 广播上下文对象
- * @param intent 携带同步状态与进度的广播意图
- */
@Override
public void onReceive(Context context, Intent intent) {
- // 刷新页面所有UI控件,同步最新状态
refreshUI();
- // 同步中状态:更新实时同步进度文本
if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) {
TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
syncStatus.setText(intent
@@ -562,17 +548,9 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
}
- /**
- * 重写ActionBar菜单点击事件处理方法
- * 核心业务逻辑:处理左上角返回按钮的点击事件,跳转回便签列表主页面并清除顶部Activity栈,
- * 避免返回时重复创建页面,优化用户导航体验,是页面导航的核心实现
- * @param item 被点击的菜单项对象
- * @return boolean 是否成功处理该点击事件
- */
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
- // 构建返回主页面的意图,清除顶部栈保证页面唯一性
Intent intent = new Intent(this, NotesListActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
@@ -581,4 +559,4 @@ public class NotesPreferenceActivity extends PreferenceActivity {
return false;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/ui/SplashActivity.java b/src/Notes-master/src/net/micode/notes/ui/SplashActivity.java
new file mode 100644
index 0000000..35a3b4a
--- /dev/null
+++ b/src/Notes-master/src/net/micode/notes/ui/SplashActivity.java
@@ -0,0 +1,62 @@
+package net.micode.notes.ui;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.widget.LinearLayout;
+
+import net.micode.notes.R;
+
+/**
+ * 开机动画活动
+ *
+ * 该类负责显示应用启动时的开机动画,使用淡入淡出动画效果展示应用Logo,
+ * 然后延迟跳转到登录界面。
+ */
+public class SplashActivity extends Activity {
+
+ private static final int SPLASH_DISPLAY_DURATION = 2000; // 2秒
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_splash);
+
+ // 找到小米logo容器
+ LinearLayout miLogo = findViewById(R.id.mi_logo);
+
+ // 创建淡入淡出动画
+ AlphaAnimation animation = new AlphaAnimation(0.0f, 1.0f);
+ animation.setDuration(1500); // 1.5秒
+ animation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ // 动画开始时的回调
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ // 动画结束后跳转到主界面
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ Intent mainIntent = new Intent(SplashActivity.this, NotesListActivity.class);
+ SplashActivity.this.startActivity(mainIntent);
+ SplashActivity.this.finish();
+ }
+ }, 500); // 延迟500毫秒后跳转
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ // 动画重复时的回调
+ }
+ });
+
+ // 应用动画到小米logo
+ miLogo.startAnimation(animation);
+ }
+}
\ No newline at end of file
diff --git a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider.java b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider.java
index fd6f02f..3ad7508 100644
--- a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider.java
+++ b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider.java
@@ -14,88 +14,61 @@
* limitations under the License.
*/
-// 包声明:小米便签 桌面小部件功能模块,该包统管所有小部件基类与不同规格的实现子类
package net.micode.notes.widget;
-
-// -------------------------- 安卓系统核心依赖包 - 桌面小部件基础能力 --------------------------
-// 安卓延迟意图类:小部件跨进程点击事件核心载体,桌面进程通过该类触发应用内页面跳转,异步执行意图逻辑
import android.app.PendingIntent;
-// 安卓系统桌面小部件核心管理类:负责小部件的创建、更新、销毁、状态维护等全生命周期系统调度
import android.appwidget.AppWidgetManager;
-// 安卓系统小部件基类:所有桌面小部件的标准父类,封装系统层小部件生命周期回调方法
import android.appwidget.AppWidgetProvider;
-// 安卓数据封装类:封装ContentProvider的更新数据键值对,用于便签数据的字段更新操作
import android.content.ContentValues;
-// 安卓应用全局上下文:提供资源访问、ContentResolver获取、系统服务调用等核心能力,小部件核心依赖
import android.content.Context;
-// 安卓组件通信核心类:封装页面跳转指令与传递参数,用于小部件点击后的页面跳转逻辑
import android.content.Intent;
-// 安卓数据库游标类:承载ContentProvider的查询结果集,按需读取便签数据,用完需手动释放资源
import android.database.Cursor;
-// 安卓系统日志工具类:输出调试与异常日志,便于小部件功能的问题定位与线上排查
import android.util.Log;
-// 安卓远程视图类:小部件核心UI载体,因小部件运行在桌面系统进程,需通过该类跨进程渲染UI布局与数据
import android.widget.RemoteViews;
-// -------------------------- 小米便签业务层核心依赖 - 资源/数据/页面/工具 --------------------------
-// 小米便签资源常量类:统一管理布局、字符串、图片、颜色等所有本地资源ID引用
import net.micode.notes.R;
-// 小米便签数据层核心常量类:定义便签URI、意图参数、业务状态、小部件类型等全局核心常量,数据层与UI层通用
import net.micode.notes.data.Notes;
-// 小米便签数据表字段子类:简化便签数据库表的列名引用,避免硬编码,提升代码可维护性
import net.micode.notes.data.Notes.NoteColumns;
-// 小米便签资源解析工具类:封装多规格小部件背景资源的映射适配逻辑,统一提供不同尺寸的背景资源获取能力
import net.micode.notes.tool.ResourceParser;
-// 小米便签核心业务页面:便签新建/编辑页面,小部件点击跳转的核心目标页面
import net.micode.notes.ui.NoteEditActivity;
-// 小米便签核心业务页面:便签列表展示页面,隐私模式下小部件点击的跳转目标页面
-
import net.micode.notes.ui.NotesListActivity;
/**
- * 小米便签 桌面小部件通用抽象基类
+ * 便签小部件提供者抽象基类
*
- * 继承安卓系统标准基类{AppWidgetProvider},隶属于MVC架构的UI层核心组件,是所有尺寸规格便签小部件的父类;
- * 核心设计理念:采用「模板方法设计模式」,封装所有小部件的通用核心业务逻辑与生命周期管理,下沉共性能力,
- * 把与尺寸强相关的差异化适配逻辑抽离为抽象方法,交由子类{NoteWidgetProvider_2x}/{NoteWidgetProvider_4x}实现;
- * 统一承载能力:小部件删除时的关联数据清理、小部件更新时的通用数据查询/UI渲染/点击事件绑定、隐私模式适配、资源释放等;
- * 本类为抽象类,无法实例化,仅作为子类的标准化模板与能力基座。
- *
+ * 该类是所有便签小部件的基类,定义了小部件的基本行为和抽象方法。
+ * 它继承自AppWidgetProvider,负责处理小部件的更新、删除等操作。
*/
public abstract class NoteWidgetProvider extends AppWidgetProvider {
/**
- * 数据库查询投影数组:指定查询便签表的核心业务字段,按需查询,减少无效字段的内存占用与查询耗时
- * 投影字段严格匹配业务需求,仅查询小部件展示所需的核心数据,无冗余字段
+ * 查询小部件信息的投影列
*/
public static final String [] PROJECTION = new String [] {
- NoteColumns.ID, // 便签数据表-主键ID,唯一标识单条便签数据
- NoteColumns.BG_COLOR_ID, // 便签数据表-背景色标识ID,用于匹配小部件对应背景资源
- NoteColumns.SNIPPET // 便签数据表-内容摘要,小部件UI上展示的核心文本内容
+ NoteColumns.ID,
+ NoteColumns.BG_COLOR_ID,
+ NoteColumns.SNIPPET
};
- // 投影数组对应的列索引常量:固化查询结果集的字段下标,简化Cursor取值逻辑,避免硬编码索引值导致的错误
- public static final int COLUMN_ID = 0; // 投影数组中-便签ID的列索引
- public static final int COLUMN_BG_COLOR_ID = 1; // 投影数组中-背景色ID的列索引
- public static final int COLUMN_SNIPPET = 2; // 投影数组中-便签摘要的列索引
+ /**
+ * 投影列索引定义
+ */
+ public static final int COLUMN_ID = 0;
+ public static final int COLUMN_BG_COLOR_ID = 1;
+ public static final int COLUMN_SNIPPET = 2;
- // 日志统一标签:小部件模块所有日志输出的固定标识,便于日志过滤与问题精准定位
private static final String TAG = "NoteWidgetProvider";
/**
- * 重写系统小部件生命周期回调:小部件删除触发方法
- * 触发时机:用户在桌面手动删除任意规格的便签小部件实例时由系统回调
- * 核心业务逻辑:清理当前小部件与便签的关联关系,将关联便签的WIDGET_ID字段置为无效值,
- * 保证数据层的关联关系一致性,防止出现无效的脏数据关联
- * @param context 应用全局上下文对象,提供ContentResolver数据操作能力
- * @param appWidgetIds 被用户删除的小部件ID数组,支持批量删除处理
+ * 当小部件被删除时调用
+ *
+ * 更新数据库中对应小部件的ID为无效值
+ *
+ * @param context 上下文对象
+ * @param appWidgetIds 被删除的小部件ID数组
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
- // 构建数据更新载体:封装需要更新的字段与对应值,将关联标识置为系统无效值
ContentValues values = new ContentValues();
values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
-
- // 遍历所有待删除的小部件ID,逐一对关联便签执行数据更新操作
for (int i = 0; i < appWidgetIds.length; i++) {
context.getContentResolver().update(Notes.CONTENT_NOTE_URI,
values,
@@ -105,11 +78,13 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider {
}
/**
- * 私有核心工具方法:根据小部件ID,查询其关联的有效便签数据信息
- * 核心过滤条件:匹配当前小部件ID + 排除回收站中的便签数据,保证查询结果为有效展示的便签
- * @param context 应用上下文,用于获取ContentResolver执行数据查询
- * @param widgetId 目标小部件的唯一标识ID
- * @return Cursor 匹配条件的便签数据结果集,无匹配数据时返回null;结果集需调用方手动关闭释放资源
+ * 获取小部件关联的便签信息
+ *
+ * 通过ContentResolver查询数据库,获取与指定小部件ID关联的便签信息
+ *
+ * @param context 上下文对象
+ * @param widgetId 小部件ID
+ * @return 查询结果游标
*/
private Cursor getNoteWidgetInfo(Context context, int widgetId) {
return context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
@@ -120,125 +95,105 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider {
}
/**
- * 受保护的重载更新方法:对外暴露的小部件通用更新入口
- * 业务适配:默认以【非隐私模式】执行小部件的UI渲染与数据绑定,为子类提供极简的调用入口
- * @param context 应用全局上下文
- * @param appWidgetManager 系统小部件管理器,负责最终的UI更新调度
- * @param appWidgetIds 需要执行更新操作的小部件ID数组,支持批量更新
+ * 更新小部件
+ *
+ * 调用带privacyMode参数的update方法,默认privacyMode为false
+ *
+ * @param context 上下文对象
+ * @param appWidgetManager 小部件管理器
+ * @param appWidgetIds 需要更新的小部件ID数组
*/
protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
update(context, appWidgetManager, appWidgetIds, false);
}
/**
- * 私有核心业务方法:小部件通用更新逻辑的总入口,封装所有规格小部件的完整更新流程
- * 核心职责:统一实现 数据查询 → 数据解析 → 视图初始化 → 内容赋值 → 点击事件绑定 → UI刷新 全链路逻辑,
- * 兼容普通模式/隐私模式双场景,处理有无关联便签的分支逻辑,是整个小部件的核心业务实现方法
- * @param context 应用全局上下文,支撑资源访问与数据操作
- * @param appWidgetManager 系统小部件管理器,提供小部件UI更新的核心能力
- * @param appWidgetIds 需要更新的小部件ID数组,支持多实例批量处理
- * @param privacyMode 是否启用隐私模式:true=隐私模式(隐藏内容),false=普通模式(展示内容)
+ * 更新小部件
+ *
+ * 遍历所有需要更新的小部件,根据小部件ID获取关联的便签信息,
+ * 然后更新小部件的显示内容和点击事件
+ *
+ * @param context 上下文对象
+ * @param appWidgetManager 小部件管理器
+ * @param appWidgetIds 需要更新的小部件ID数组
+ * @param privacyMode 是否为隐私模式
*/
private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds,
boolean privacyMode) {
- // 遍历所有待更新的小部件ID,逐个完成独立的更新逻辑处理
for (int i = 0; i < appWidgetIds.length; i++) {
- // 过滤无效的小部件ID,跳过无意义的更新操作,提升执行效率
if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) {
- // 初始化默认背景ID:从资源工具类获取应用全局默认的便签背景标识
int bgId = ResourceParser.getDefaultBgId(context);
- // 初始化便签摘要:默认空字符串,防止空指针异常
String snippet = "";
-
- // 构建默认跳转意图:无关联便签时,跳转至便签编辑页执行新建操作
Intent intent = new Intent(context, NoteEditActivity.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); // 启动模式:栈顶复用,避免重复创建页面实例
- intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); // 携带小部件ID参数,供编辑页关联使用
- intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); // 携带小部件类型,由子类实现差异化适配
+ intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]);
+ intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType());
- // 查询当前小部件关联的有效便签数据
Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]);
if (c != null && c.moveToFirst()) {
- // 异常日志埋点:同一小部件ID关联多条便签数据,属于数据异常场景,输出错误日志便于排查
if (c.getCount() > 1) {
Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]);
c.close();
return;
}
- // 从结果集中解析业务数据,赋值给本地变量用于后续视图渲染
snippet = c.getString(COLUMN_SNIPPET);
bgId = c.getInt(COLUMN_BG_COLOR_ID);
- // 携带便签ID参数,跳转编辑页时直接定位到当前关联的便签内容
intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID));
- // 修改意图动作:由「新建」改为「查看/编辑」已有便签
intent.setAction(Intent.ACTION_VIEW);
} else {
- // 无关联便签数据时:展示应用默认的空内容提示文本
snippet = context.getResources().getString(R.string.widget_havenot_content);
- // 修改意图动作:执行新建便签的业务逻辑
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
}
- // 安全关闭游标,释放数据库连接资源,防止内存泄漏
if (c != null) {
c.close();
}
- // 创建远程视图实例:加载子类实现的规格专属布局,完成跨进程UI初始化
RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId());
- // 为远程视图设置背景资源:加载子类实现的规格专属背景,完成尺寸与背景的精准适配
rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId));
- // 携带背景ID参数,跳转编辑页时同步使用当前小部件的背景样式
intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId);
-
/**
- * 构建小部件点击的延迟意图:区分隐私模式与普通模式,实现差异化的交互逻辑
- * PendingIntent为跨进程意图载体,是小部件点击事件的核心实现方式,保证桌面进程能触发应用内逻辑
+ * 生成启动便签编辑活动的PendingIntent
*/
PendingIntent pendingIntent = null;
if (privacyMode) {
- // 隐私模式逻辑:隐藏便签真实内容,展示隐私提示文本
rv.setTextViewText(R.id.widget_text,
context.getString(R.string.widget_under_visit_mode));
- // 隐私模式跳转:点击后跳转至便签列表页面,而非编辑页
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent(
context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
} else {
- // 普通模式逻辑:展示解析后的便签摘要文本
rv.setTextViewText(R.id.widget_text, snippet);
- // 普通模式跳转:点击后跳转至便签编辑页,携带完整业务参数
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
- // 为小部件核心文本区域绑定点击事件,触发上述构建的延迟意图
rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent);
- // 通过系统管理器完成最终的UI刷新,将渲染完成的远程视图同步至桌面小部件
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
}
}
}
/**
- * 抽象方法:获取小部件规格专属的背景资源ID
- * 由子类根据自身尺寸规格实现,完成背景资源与小部件尺寸的精准适配,避免背景拉伸/变形
- * @param bgId 便签数据表中定义的背景色标识ID,为全局统一的背景风格常量
- * @return int 对应尺寸规格的背景Drawable资源ID
+ * 获取背景资源ID
+ *
+ * 根据背景颜色ID获取对应的背景资源ID
+ *
+ * @param bgId 背景颜色ID
+ * @return 背景资源ID
*/
protected abstract int getBgResourceId(int bgId);
/**
- * 抽象方法:获取小部件规格专属的布局资源ID
- * 由子类根据自身尺寸规格实现,加载与桌面占位大小匹配的专属布局,保证UI展示效果
- * @return int 对应尺寸规格的布局资源ID
+ * 获取布局ID
+ *
+ * @return 布局资源ID
*/
protected abstract int getLayoutId();
/**
- * 抽象方法:获取小部件的标准化业务类型标识
- * 由子类根据自身尺寸规格实现,返回数据层统一定义的类型常量;
- * 该标识是数据层识别小部件规格的核心依据,用于数据关联、筛选与同步,保证数据与视图的一致性
- * @return int 小部件业务类型常量,取值为Notes.TYPE_WIDGET_2X / Notes.TYPE_WIDGET_4X
+ * 获取小部件类型
+ *
+ * @return 小部件类型
*/
protected abstract int getWidgetType();
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_2x.java b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_2x.java
index 78af6c9..0ee0643 100644
--- a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_2x.java
+++ b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_2x.java
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-// 包声明:小米便签 桌面小部件功能模块,该包下统管所有尺寸规格的便签桌面小组件实现类
package net.micode.notes.widget;
import android.appwidget.AppWidgetManager;
@@ -24,20 +23,22 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.ResourceParser;
+
/**
- * 小米便签 2X规格桌面小部件提供者核心实现类
- * 继承抽象基类NoteWidgetProvider,作为MVC架构中UI层的桌面可视化组件,仅负责2X规格专属适配逻辑;
- * 核心职责:实现基类抽象方法,为2X尺寸小部件提供专属布局、匹配的背景资源、标准化业务类型标识;
- * 设计原则:通用的小部件生命周期管理、数据更新、视图渲染逻辑完全复用父类,子类只做规格差异化实现,解耦通用逻辑与尺寸适配逻辑。
+ * 2x2 大小的便签小部件提供者
+ *
+ * 该类继承自NoteWidgetProvider,实现了2x2大小的便签小部件。
+ * 它提供了小部件的布局、背景资源和类型等信息。
*/
public class NoteWidgetProvider_2x extends NoteWidgetProvider {
/**
- * 重写系统小部件生命周期的更新回调方法
- * 触发场景:小部件首次添加至桌面、系统触发定时刷新、便签数据变更主动同步时调用
- * 方法逻辑:无差异化业务处理,直接调用父类统一的update核心方法,完成2X小部件的视图刷新与便签数据绑定
- * @param context 全局上下文对象,提供系统服务调用、应用资源访问的基础能力
- * @param appWidgetManager 系统桌面小部件管理器,负责小部件的创建、更新、销毁等全生命周期调度
- * @param appWidgetIds 待更新的2X规格小部件ID数组,支持多实例批量更新操作
+ * 当小部件需要更新时调用
+ *
+ * 调用父类的update方法更新小部件
+ *
+ * @param context 上下文对象
+ * @param appWidgetManager 小部件管理器
+ * @param appWidgetIds 需要更新的小部件ID数组
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
@@ -45,9 +46,11 @@ public class NoteWidgetProvider_2x extends NoteWidgetProvider {
}
/**
- * 重写父类抽象方法,获取2X规格小部件的专属布局资源ID
- * 布局文件为固定尺寸适配,与2X桌面占位大小完全匹配,保证便签内容在该规格下的完整展示
- * @return int 2X小部件专属布局资源ID,对应布局文件:R.layout.widget_2x
+ * 获取布局ID
+ *
+ * 返回2x2小部件的布局资源ID
+ *
+ * @return 布局资源ID
*/
@Override
protected int getLayoutId() {
@@ -55,11 +58,12 @@ public class NoteWidgetProvider_2x extends NoteWidgetProvider {
}
/**
- * 重写父类抽象方法,获取2X规格小部件对应的背景资源ID
- * 核心适配说明:不同尺寸的小部件背景资源为独立资源文件,需根据背景主题ID精准匹配对应规格的背景;
- * 避免因尺寸不一致导致的背景拉伸、变形等UI兼容问题,通过工具类封装所有背景资源的映射关系,统一管理。
- * @param bgId 背景主题标识ID,为Notes模块统一定义的背景风格常量,与具体尺寸解耦
- * @return int 适配2X规格的背景Drawable资源ID
+ * 获取背景资源ID
+ *
+ * 根据背景颜色ID获取对应的2x2小部件背景资源ID
+ *
+ * @param bgId 背景颜色ID
+ * @return 背景资源ID
*/
@Override
protected int getBgResourceId(int bgId) {
@@ -67,13 +71,14 @@ public class NoteWidgetProvider_2x extends NoteWidgetProvider {
}
/**
- * 重写父类抽象方法,获取当前小部件的标准化业务类型标识
- * 该标识为数据层核心常量,用于NotesProvider数据持久化、便签数据同步、小部件类型筛选的核心依据;
- * 保证数据层能精准识别当前小部件的尺寸规格,返回对应规格的便签数据,实现「数据-视图」的规格一致性适配。
- * @return int 2X规格小部件的业务类型常量,定值:Notes.TYPE_WIDGET_2X
+ * 获取小部件类型
+ *
+ * 返回2x2小部件的类型
+ *
+ * @return 小部件类型
*/
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_2X;
}
-}
\ No newline at end of file
+}
diff --git a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_4x.java b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_4x.java
index c339503..e4fe4fc 100644
--- a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_4x.java
+++ b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_4x.java
@@ -14,35 +14,31 @@
* limitations under the License.
*/
-// 包声明:小米便签 桌面小部件功能模块,该包统一承载所有尺寸规格的便签桌面小组件实现类
package net.micode.notes.widget;
-// 安卓系统核心依赖:桌面小部件全生命周期调度的核心管理类,负责小部件的创建、更新、销毁等系统操作
import android.appwidget.AppWidgetManager;
-// 安卓系统核心依赖:应用全局上下文,提供资源访问、系统服务调用、组件通信的基础能力,小部件初始化必备
import android.content.Context;
-// 小米便签业务依赖:应用资源常量类,统一管理所有布局、样式等资源ID引用
import net.micode.notes.R;
-// 小米便签业务依赖:数据层核心常量类,定义便签及小部件的类型、状态等全局业务枚举常量
import net.micode.notes.data.Notes;
-// 小米便签业务依赖:资源解析工具类,封装多规格小部件背景资源的映射适配逻辑,统一提供背景资源获取能力
import net.micode.notes.tool.ResourceParser;
+
/**
- * 小米便签 4X规格桌面小部件提供者核心实现类
- * 继承抽象基类NoteWidgetProvider,隶属于MVC架构的UI层桌面可视化组件,专注4X规格的差异化适配;
- * 核心设计职责:实现基类定义的抽象方法,为4X尺寸小部件提供专属的布局资源、匹配的背景样式资源、标准化业务类型标识;
- * 设计思想遵循:父类封装所有小部件通用的生命周期、数据更新、视图渲染核心逻辑,子类仅实现当前规格的差异化配置,解耦通用逻辑与尺寸适配逻辑,提升扩展性。
+ * 4x4 大小的便签小部件提供者
+ *
+ * 该类继承自NoteWidgetProvider,实现了4x4大小的便签小部件。
+ * 它提供了小部件的布局、背景资源和类型等信息。
*/
public class NoteWidgetProvider_4x extends NoteWidgetProvider {
/**
- * 重写系统小部件生命周期的更新回调方法
- * 触发场景:4X小部件首次添加至桌面、系统执行定时刷新任务、便签数据变更触发主动同步时调用
- * 方法核心逻辑:无4X规格的差异化业务处理,直接复用父类统一的update核心方法,完成4X小部件的视图刷新与最新便签数据绑定
- * @param context 应用全局上下文对象,支撑小部件的资源访问与系统服务调用
- * @param appWidgetManager 系统桌面小部件管理器,统一调度所有小部件实例的更新操作
- * @param appWidgetIds 待执行更新的4X规格小部件ID数组,原生支持多小部件实例的批量更新
+ * 当小部件需要更新时调用
+ *
+ * 调用父类的update方法更新小部件
+ *
+ * @param context 上下文对象
+ * @param appWidgetManager 小部件管理器
+ * @param appWidgetIds 需要更新的小部件ID数组
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
@@ -50,20 +46,24 @@ public class NoteWidgetProvider_4x extends NoteWidgetProvider {
}
/**
- * 实现父类抽象方法:获取4X规格小部件的专属布局资源ID
- * 该布局为小米便签定制化的4X尺寸UI布局,与桌面4格占位大小精准匹配,保障便签内容完整展示无适配问题
- * @return int 4X小部件专属布局资源ID,对应固定布局文件:R.layout.widget_4x
+ * 获取布局ID
+ *
+ * 返回4x4小部件的布局资源ID
+ *
+ * @return 布局资源ID
*/
+ @Override
protected int getLayoutId() {
return R.layout.widget_4x;
}
/**
- * 重写父类抽象方法:根据背景主题标识ID,获取4X规格小部件的专属背景资源ID
- * 核心适配逻辑:不同尺寸规格的小部件对应独立的背景资源文件,避免尺寸不匹配导致的背景拉伸、变形等UI兼容问题;
- * 背景资源的映射关系由工具类统一封装管理,实现业务逻辑与资源适配的解耦,便于后续扩展新的背景样式。
- * @param bgId 背景主题枚举标识ID,为项目统一定义的小部件背景风格常量,与具体尺寸规格解耦
- * @return int 精准匹配4X规格的背景Drawable资源ID
+ * 获取背景资源ID
+ *
+ * 根据背景颜色ID获取对应的4x4小部件背景资源ID
+ *
+ * @param bgId 背景颜色ID
+ * @return 背景资源ID
*/
@Override
protected int getBgResourceId(int bgId) {
@@ -71,13 +71,14 @@ public class NoteWidgetProvider_4x extends NoteWidgetProvider {
}
/**
- * 重写父类抽象方法:获取当前4X规格小部件的标准化业务类型标识
- * 该标识为数据层核心业务常量,是NotesProvider数据持久化、便签数据同步、小部件类型筛选的核心依据;
- * 作用是让数据层能够精准识别当前小部件的尺寸规格,返回对应规格的适配数据,实现「业务数据-桌面视图」的规格一致性闭环。
- * @return int 4X规格小部件的业务类型常量,定值:Notes.TYPE_WIDGET_4X
+ * 获取小部件类型
+ *
+ * 返回4x4小部件的类型
+ *
+ * @return 小部件类型
*/
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_4X;
}
-}
\ No newline at end of file
+}