更新完善

master
刘焱 3 months ago
parent efc373cd74
commit 76ff74ca12

@ -25,61 +25,104 @@ import android.util.Log;
import java.util.HashMap;
/**
* (Data Layer - External Data Access)
* <p>
*
* 访便ContentProvider
*
*/
public class Contact {
private static HashMap<String, String> sContactCache;//定义sContactCache是一个缓存电话号码和相应联系人名字的哈希表
private static final String TAG = "Contact";//定义用于日志输出的标识TAG
/**
* ( -> )
* <p>
* 使HashMap
*
*/
private static HashMap<String, String> sContactCache;
private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER
+ ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'"
+ " AND " + Data.RAW_CONTACT_ID + " IN "
+ "(SELECT raw_contact_id "
+ " FROM phone_lookup"
+ " WHERE min_match = '+')";
//用于构建数据库查询条件的字符串常量
public static String getContact(Context context, String phoneNumber)
//参数:Context对象:用于访问系统服务和应用资源 phoneNumber:需要查询的联系人电话号码
{
if(sContactCache == null) {
sContactCache = new HashMap<String, String>();
/**
* 使TAG
*/
private static final String TAG = "Contact";
/**
* SQL Selection
* <p>
*
* 1. PHONE_NUMBERS_EQUAL使Android
* 2. MIMETYPE='phone'
* 3. RAW_CONTACT_ID IN phone_lookupphone_lookup
*
* "+"
*/
private static final String CALLER_ID_SELECTION =
"PHONE_NUMBERS_EQUAL(" + Phone.NUMBER + ",?) AND " +
Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'" +
" AND " + Data.RAW_CONTACT_ID + " IN " +
"(SELECT raw_contact_id " +
" FROM phone_lookup" +
" WHERE min_match = '+')";
// 没映射表就建表,有就查缓存中有没有这个联系人
/**
* ()
* <p>
*
* HashMap
*
*
* @param context 访ContentResolver
* @param phoneNumber
* @return null
*/
public static String getContact(Context context, String phoneNumber) {
// 1. 延迟初始化缓存:首次调用时创建缓存容器
if (sContactCache == null) {
sContactCache = new HashMap<String, String>();
}
if(sContactCache.containsKey(phoneNumber)) {
// 2. 缓存命中检查:如果缓存中存在该电话号码,直接返回缓存的姓名
if (sContactCache.containsKey(phoneNumber)) {
return sContactCache.get(phoneNumber);
}
//返回从表里查询到的对应名字
// 3. 构建查询条件:将占位符"+"替换为电话号码的最小匹配长度
// 最小匹配长度是Android电话号码匹配算法的一部分用于处理不同格式的电话号码
String selection = CALLER_ID_SELECTION.replace("+",
PhoneNumberUtils.toCallerIDMinMatch(phoneNumber));
//构造一个SQL查询条件CALLER_ID_SELECTION中的"+"被替换为电话号码的最小匹配值
//然后执行查询语句
// 4. DB操作查询系统联系人数据库
Cursor cursor = context.getContentResolver().query(
Data.CONTENT_URI,
new String [] { Phone.DISPLAY_NAME },
selection,
new String[] { phoneNumber },
null);
//判断查询结果:
//查询结果不为空,且能够移动到第一条记录:
// 那么就尝试从Cursor中获取联系人姓名并将其存入缓存sContactCache。然后返回联系人姓名。
// 异常情况如果在获取字符串时发生数组越界异常则记录一个错误日志并返回null。
// 最后都要确保关闭Cursor对象以避免内存泄漏。
//如果查询结果为空或者没有记录可以移动到(即没有找到匹配的联系人):
// 则记录一条调试日志并返回null
Data.CONTENT_URI, // URI系统联系人的数据表
new String[] { Phone.DISPLAY_NAME }, // Projection仅查询显示名称列
selection, // Selection动态构建的查询条件
new String[] { phoneNumber }, // SelectionArgs查询参数
null // SortOrder无需排序
);
// 5. 结果处理:如果查询到匹配的联系人,提取姓名并更新缓存
if (cursor != null && cursor.moveToFirst()) {
try {
// 从Cursor的第一列获取联系人姓名
String name = cursor.getString(0);
// 更新缓存:将电话号码-姓名映射加入缓存
sContactCache.put(phoneNumber, name);
Log.d(TAG, "Contact found for number: " + phoneNumber + ", name: " + name);
return name;
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, " Cursor get string error " + e.toString());
// 异常保护当Cursor列索引越界时记录错误日志
Log.e(TAG, "Cursor get string error: " + e.toString());
return null;
} finally {
// 资源清理无论是否发生异常都确保关闭Cursor
cursor.close();
}
} else {
Log.d(TAG, "No contact matched with number:" + phoneNumber);
// 查询结果为空:未找到匹配的联系人
Log.d(TAG, "No contact matched with number: " + phoneNumber);
return null;
}
}
}
}

@ -17,34 +17,56 @@
package net.micode.notes.data;
import android.net.Uri;
/**
* 便 (Data Layer - Schema Definition)
* <p>
* 便ContentProvider URI
*
* 使便
*/
public class Notes {
//用于表示笔记应用中的各种类型、标识符以及Intent的额外数据
/**
* ContentProvider
* <p>
* ContentProvider URI AndroidManifest.xml
*/
public static final String AUTHORITY = "micode_notes";
/**
* 使TAG
*/
public static final String TAG = "Notes";
//对NoteColumns.TYPE的值进行设置时使用
//即不同种类:笔记、文件夹和系统文件夹
public static final int TYPE_NOTE = 0;
public static final int TYPE_FOLDER = 1;
public static final int TYPE_SYSTEM = 2;
/**
* Following IDs are system folders' identifiers
* {@link Notes#ID_ROOT_FOLDER } is default folder
* {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder
* {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records
* 便
* <p>
* 便
* {@code type}
*/
public static final int TYPE_NOTE = 0; // 普通便签
public static final int TYPE_FOLDER = 1; // 文件夹(用于组织便签)
public static final int TYPE_SYSTEM = 2; // 系统文件夹(特殊用途)
/**
* (System Folder Identifiers)
* <p>
* ID
*
*
*/
public static final int ID_ROOT_FOLDER = 0; // 根文件夹:默认文件夹,显示所有便签
public static final int ID_TEMPARAY_FOLDER = -1; // 临时文件夹:存储未分类的便签
public static final int ID_CALL_RECORD_FOLDER = -2; // 通话记录文件夹:存储来自通话的便签
public static final int ID_TRASH_FOLER = -3; // 回收站:软删除的便签临时存放处
/**
* Intent (Intent Extra Keys)
* <p>
* Activity
* 使
*/
//以下id是系统文件夹的标识符即系统文件夹的分类
//ID_ROOT_FOLDER默认文件夹
//ID_TEMPARAY_FOLDER不属于文件夹的笔记
//ID_CALL_RECORD_FOLDER用于存储通话记录以便返回
//ID_TRASH_FOLER垃圾回收站
public static final int ID_ROOT_FOLDER = 0;
public static final int ID_TEMPARAY_FOLDER = -1;
public static final int ID_CALL_RECORD_FOLDER = -2;
public static final int ID_TRASH_FOLER = -3;
// 额外的数据键个人理解为就是定义一些布局的ID
// 这部分就是用于设置UI界面的一些布局或小组件的id给它定义成常量了。
// 这样的封装性可能比较好因为如果有部分要修改则直接来这边修改即可不用在activity部分一个一个修改。
public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date";
public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id";
public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id";
@ -52,258 +74,332 @@ public class Notes {
public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id";
public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date";
public static final int TYPE_WIDGET_INVALIDE = -1;
public static final int TYPE_WIDGET_2X = 0;
public static final int TYPE_WIDGET_4X = 1;
// 数据常量:里面定义了两种类型:文本便签和通话记录
/**
*
* <p>
* 便
* TYPE_WIDGET_INVALIDE
*/
public static final int TYPE_WIDGET_INVALIDE = -1; // 无效的小部件类型
public static final int TYPE_WIDGET_2X = 0; // 2x2 小部件尺寸
public static final int TYPE_WIDGET_4X = 1; // 4x4 小部件尺寸
/**
*
* <p>
* 便MIME
* ContentProviderMIME_TYPE便
*/
public static class DataConstants {
public static final String NOTE = TextNote.CONTENT_ITEM_TYPE;
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE;
public static final String NOTE = TextNote.CONTENT_ITEM_TYPE; // 普通文本便签
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE; // 通话记录便签
}
//定义一堆访问笔记和文件的uri
//GPTAndroid开发中常见的用于定义内容提供者Content ProviderURI
//内容提供者是一种Android组件它允许应用程序共享和存储数据。这里定义了一个URI来查询数据
/**
* Uri to query all notes and folders
* ContentProvider URI
* <p>
* URI访
* ContentProviderURI便
*/
/**
* 便URI
* <p>
* notes便
*/
public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note");
/**
* Uri to query data
* 便URI
* <p>
* data便
*/
public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data");
/**
* Notes (Database Schema - Notes Table)
* <p>
* notes
*
*/
public interface NoteColumns {
/**
* The unique ID for a row
* ID
* <P> Type: INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* The parent's id for note or folder
* ID
* <P> Type: INTEGER (long) </P>
*/
public static final String PARENT_ID = "parent_id";
/**
* Created data for note or folder
*
* <P> Type: INTEGER (long) </P>
*/
public static final String CREATED_DATE = "created_date";
/**
* Latest modified date
*
* <P> Type: INTEGER (long) </P>
*/
public static final String MODIFIED_DATE = "modified_date";
/**
* Alert date
*
* <P> Type: INTEGER (long) </P>
*/
public static final String ALERTED_DATE = "alert_date";
/**
* Folder's name or text content of note
* 便
* <P> Type: TEXT </P>
* <p>
* 便
*
*/
// 摘要
public static final String SNIPPET = "snippet";
/**
* Note's widget id
* ID
* <P> Type: INTEGER (long) </P>
*/
public static final String WIDGET_ID = "widget_id";
/**
* Note's widget type
* 2x24x4
* <P> Type: INTEGER (long) </P>
*/
public static final String WIDGET_TYPE = "widget_type";
/**
* Note's background color's id
* 便ID
* <P> Type: INTEGER (long) </P>
* <p>
* [2025 AI]
*
* AI
*/
public static final String BG_COLOR_ID = "bg_color_id";
/**
* For text note, it doesn't has attachment, for multi-media
* note, it has at least one attachment
*
* <P> Type: INTEGER </P>
* <p>
* 01
* 便
*/
public static final String HAS_ATTACHMENT = "has_attachment";
/**
* Folder's count of notes
* 便
* <P> Type: INTEGER (long) </P>
*/
public static final String NOTES_COUNT = "notes_count";
/**
* The file type: folder or note
* 便//
* <P> Type: INTEGER </P>
* <p>
* {@link Notes#TYPE_NOTE}, {@link Notes#TYPE_FOLDER}, {@link Notes#TYPE_SYSTEM}
*/
public static final String TYPE = "type";
/**
* The last sync id
* ID
* <P> Type: INTEGER (long) </P>
* <p>
*
*/
//在数据同步过程中这个ID可能用来跟踪和识别每次同步操作的唯一性确保数据的一致性。
public static final String SYNC_ID = "sync_id";
/**
* Sign to indicate local modified or not
*
* <P> Type: INTEGER </P>
* <p>
* 01
*
*/
public static final String LOCAL_MODIFIED = "local_modified";
/**
* Original parent id before moving into temporary folder
* ID
* <P> Type : INTEGER </P>
* <p>
* 便ID
* 便
*/
public static final String ORIGIN_PARENT_ID = "origin_parent_id";
/**
* The gtask id
* GoogleIDGoogle Tasks
* <P> Type : TEXT </P>
*/
public static final String GTASK_ID = "gtask_id";
/**
* The version code
*
* <P> Type : INTEGER (long) </P>
* <p>
*
*/
public static final String VERSION = "version";
}
/**
* Data (Database Schema - Data Table)
* <p>
* data
* data便MIME
*/
public interface DataColumns {
// DataColumns的接口这个接口包含了一系列静态常量这些常量代表了数据库表中用于存储数据的列名。
// 每个常量都有相应的注释,说明该列的作用和数据类型。
/**
* The unique ID for a row
* ID
* <P> Type: INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* The MIME type of the item represented by this row.
* MIME
* <P> Type: Text </P>
* <p>
* MIME便
* 便便
*/
//MIME类型是一种标准用于标识文档、文件或字节流的性质和格式。在数据库中这个字段可以用来识别不同类型的数据例如文本、图片、音频或视频等。
public static final String MIME_TYPE = "mime_type";
/**
* The reference id to note that this data belongs to
* 便IDnotes
* <P> Type: INTEGER (long) </P>
*/
//归属的Note的ID
public static final String NOTE_ID = "note_id";
/**
* Created data for note or folder
*
* <P> Type: INTEGER (long) </P>
*/
//创建日期
public static final String CREATED_DATE = "created_date";
/**
* Latest modified date
*
* <P> Type: INTEGER (long) </P>
*/
//最近修改日期
public static final String MODIFIED_DATE = "modified_date";
/**
* Data's content
* 便
* <P> Type: TEXT </P>
*/
//数据内容
public static final String CONTENT = "content";
// 以下5个是通用数据列它们的具体意义取决于MIME类型由MIME_TYPE字段指定
// 不同的MIME类型可能需要存储不同类型的数据这五个字段提供了灵活性允许根据MIME类型来存储相应的数据。
// 读后面的代码感觉这部分是在表示内容的不同状态
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* integer data type
* 1MIME
* <P> Type: INTEGER </P>
* <p>
*
* MIME_TYPE
*/
public static final String DATA1 = "data1";
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* integer data type
* 2MIME
* <P> Type: INTEGER </P>
*/
public static final String DATA2 = "data2";
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* TEXT data type
* 3MIME
* <P> Type: TEXT </P>
*/
public static final String DATA3 = "data3";
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* TEXT data type
* 4MIME
* <P> Type: TEXT </P>
*/
public static final String DATA4 = "data4";
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* TEXT data type
* 5MIME
* <P> Type: TEXT </P>
*/
public static final String DATA5 = "data5";
}
//以下是文本便签的定义
/**
* 便 (Text Note Data Definition)
* <p>
* 便
* 使DataColumns
*/
public static final class TextNote implements DataColumns {
/**
* Mode to indicate the text in check list mode or not
* 便
* <P> Type: Integer 1:check list mode 0: normal mode </P>
* <p>
* DATA1
*/
public static final String MODE = DATA1;//模式这个被存在DATA1列中
public static final String MODE = DATA1;
public static final int MODE_CHECK_LIST = 1;//所处检查列表模式
/**
*
*/
public static final int MODE_CHECK_LIST = 1;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note";// 定义了MIME类型用于标识文本标签的目录
/**
* 便MIME
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note";// 定义了MIME类型用于标识文本标签的单个项
/**
* 便MIME
*/
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");//文本标签内容提供者Content Provider的URI用于访问文本标签数据
/**
* 便URI
*/
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");
}
// 通话记录的定义
/**
* 便 (Call Note Data Definition)
* <p>
* 便
* 使DataColumns
*/
public static final class CallNote implements DataColumns {
/**
* Call date for this record
*
* <P> Type: INTEGER (long) </P>
* <p>
* DATA1
*/
public static final String CALL_DATE = DATA1;//一个字符串常量,表示通话记录的日期
public static final String CALL_DATE = DATA1;
/**
* Phone number for this record
*
* <P> Type: TEXT </P>
* <p>
* DATA3
*/
public static final String PHONE_NUMBER = DATA3;//意味着在数据库表中这个电话号码信息将被存储在DATA3列中
public static final String PHONE_NUMBER = DATA3;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note";// 同样定义了MIME类型是用于标识通话记录的目录。
/**
* 便MIME
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";// 同样定义了MIME类型是用于标识通话记录的单个项
/**
* 便MIME
*/
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");//定义了通话记录内容提供者的URI用于访问通话记录数据
/**
* 便URI
*/
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");
}
}
}

@ -10,30 +10,63 @@ import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
/**
* 便 (Data Layer - Database Management)
* <p>
* 便SQLite
*
* 使SQLiteOpenHelper
*
*/
public class NotesDatabaseHelper extends SQLiteOpenHelper {
// 数据库帮助类,用于管理名为 note.db 的 SQLite 数据库。
// 它继承自 SQLiteOpenHelper 类,这是 Android提供的一个方便的工具类用于管理数据库的创建和版本更新.
// 数据库的基本信息;数据库名称和版本信息(在创建实例对象时会用到)
/**
*
*/
private static final String DB_NAME = "note.db";
/**
*
* <p>
*
* onUpgrade
*/
private static final int DB_VERSION = 4;
//内部接口个人理解为两个表名一个note一个data
/**
*
*/
public interface TABLE {
/**
* 便便
*/
public static final String NOTE = "note";
/**
* 便便
*/
public static final String DATA = "data";
}
//一个标签,方便日志输出时识别出信息来自哪里
/**
* 使TAG
*/
private static final String TAG = "NotesDatabaseHelper";
//静态所有变量,提供一个全局访问点来获取数据库辅助类的唯一实例,使得在应用的任何地方都可以方便地使用它
/**
*
* <p>
* 使
*
*/
private static NotesDatabaseHelper mInstance;
/* 以下都是一些SQL语句辅助我们来对数据库进行操作 */
//创建note表的语句这里的NoteColumns就是我们刚刚在Notes中定义的一个接口里面定义了一系列静态的数据库表中的列名
/**
* NOTESQL
* <p>
* NOTE
*
*/
private static final String CREATE_NOTE_TABLE_SQL =
"CREATE TABLE " + TABLE.NOTE + "(" +
NoteColumns.ID + " INTEGER PRIMARY KEY," +
@ -55,7 +88,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
")";
//同上创建data表的语句这里的DataColumns就是我们刚刚在Notes中定义的一个接口里面定义了一系列静态的数据库表中的列名
/**
* DATASQL
* <p>
* DATA便
* NOTE_IDNOTE
*/
private static final String CREATE_DATA_TABLE_SQL =
"CREATE TABLE " + TABLE.DATA + "(" +
DataColumns.ID + " INTEGER PRIMARY KEY," +
@ -71,31 +109,22 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
")";
// 功能简介:
// 创建一个以note的ID为索引
// 解读:
// 用于在TABLE.DATA表上创建一个名为note_id_index的索引。
// 这个索引是基于DataColumns.NOTE_ID列的。IF NOT EXISTS确保了如果索引已经存在那么就不会尝试重新创建它避免了可能的错误。
// 索引通常用于提高查询性能,特别是在对某个字段进行频繁查询时。
/**
* DATANOTE_IDSQL
* <p>
* DATANOTE_ID便ID
* IF NOT EXISTS
*/
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
"CREATE INDEX IF NOT EXISTS note_id_index ON " +
TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
/* 以下是一些对便签增删改定义的触发器 */
/*
* NOTEDATA
* NOTE
* */
/**
* Increase folder's note count when move note to the folder
* NOTE
* <p>
* 便IDPARENT_ID便
* 使AFTER UPDATE
*/
// 功能简介:
// 添加触发器:增加文件夹的便签个数记录(因为我们会移动便签进入文件夹,这时候文件夹的计数要进行更新)
// 解读:
// 定义了一个SQL触发器increase_folder_count_on_update。
// 触发器是一种特殊的存储过程它会在指定表上的指定事件如INSERT、UPDATE、DELETE发生时自动执行。
// 这个触发器会在TABLE.NOTE表的NoteColumns.PARENT_ID字段更新后执行。
// 触发器的逻辑是当某个笔记的PARENT_ID即父文件夹ID被更新时它会找到对应的文件夹通过新的PARENT_ID并将该文件夹的NOTES_COUNT即笔记数增加1。
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_update "+
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
@ -106,10 +135,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Decrease folder's note count when move note from folder
* NOTE
* <p>
* 便IDPARENT_ID便
* AND
*/
// 功能简介:(触发器和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
// 添加触发器:减少文件夹的便签个数记录(因为我们会移动便签移出文件夹,这时候文件夹的计数要进行更新)
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_update " +
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
@ -121,10 +151,10 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Increase folder's note count when insert new note to the folder
* NOTE
* <p>
* 便便
*/
// 功能简介:(触发器原理和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
// 添加触发器:当我们在文件夹插入便签时,增加文件夹的便签个数记录
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_insert " +
" AFTER INSERT ON " + TABLE.NOTE +
@ -135,10 +165,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Decrease folder's note count when delete note from the folder
* NOTE
* <p>
* 便便
* AND
*/
// 功能简介:(触发器原理和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
// 添加触发器:当我们在文件夹删除便签时,减少文件夹的便签个数记录
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
@ -150,13 +181,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Update note's content when insert data with type {@link DataConstants#NOTE}
* DATA便
* <p>
* DATA便NOTE
* NOTEDATA
*/
// 功能简介:
// 添加触发器当向DATA表中插入类型为NOTE便签的数据时更新note表对应的笔记内容。
// 解读:
// 在DATA表上进行INSERT操作后如果新插入的数据的MIME_TYPE为NOTE则触发此操作。
// 它会更新NOTE表将与新插入数据相关联的标签的SNIPPET摘要字段设置为新插入数据的CONTENT字段的值
private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER =
"CREATE TRIGGER update_note_content_on_insert " +
" AFTER INSERT ON " + TABLE.DATA +
@ -168,13 +197,10 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Update note's content when data with {@link DataConstants#NOTE} type has changed
* DATA便
* <p>
* DATA便NOTE
*/
// 功能简介:
// 添加触发器当DATA表中类型为NOTE便签的数据更改时更新note表对应的笔记内容。
// 解读:
// 在DATA表上进行UPDATE操作后如果更新前的数据的MIME_TYPE为NOTE则触发此操作。
// 它会更新NOTE表将与更新后的数据相关联的笔记的SNIPPET字段设置为新数据的CONTENT字段的值
private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER update_note_content_on_update " +
" AFTER UPDATE ON " + TABLE.DATA +
@ -186,13 +212,10 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Update note's content when data with {@link DataConstants#NOTE} type has deleted
* DATA便
* <p>
* DATA便NOTE
*/
// 功能简介:
// 添加触发器当DATA表中类型为NOTE便签的数据删除时更新note表对应的笔记内容置空
// 解读:
// 在DATA表上进行DELETE操作后如果删除的数据的MIME_TYPE为NOTE则触发此操作。
// 它会更新NOTE表将与删除的数据相关联的笔记的SNIPPET字段设置为空字符串。
private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER =
"CREATE TRIGGER update_note_content_on_delete " +
" AFTER delete ON " + TABLE.DATA +
@ -204,13 +227,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Delete datas belong to note which has been deleted
* NOTE
* <p>
* NOTE便DATA
*
*/
// 功能简介:
// 添加触发器当从NOTE表中删除笔记时删除与该笔记相关联的数据就是删除data表中为该note的数据
// 解读:
// 在NOTE表上进行DELETE操作后此触发器被激活。
// 它会从DATA表中删除所有与已删除的笔记由old.ID表示相关联的数据行通过比较DATA表中的NOTE_ID字段与已删除笔记的ID来实现
private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER =
"CREATE TRIGGER delete_data_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
@ -220,13 +241,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Delete notes belong to folder which has been deleted
* NOTE便
* <p>
* 便
* 便
*/
// 功能简介:
// 添加触发器当从NOTE表中删除一个文件夹时删除该文件夹下的所有笔记。
// 解读:
// 在NOTE表上进行DELETE操作后如果删除的是一个文件夹由old.ID表示
// 触发器会删除所有以该文件夹为父级PARENT_ID的笔记通过比较NOTE表中的PARENT_ID字段与已删除文件夹的ID来实现
private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER =
"CREATE TRIGGER folder_delete_notes_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
@ -236,13 +255,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* Move notes belong to folder which has been moved to trash folder
* NOTE便
* <p>
* 便
*
*/
// 功能简介:
// 添加触发器:当某个文件夹被移动到回收站时,移动该文件夹下的所有笔记到回收站
// 解读:
// 在NOTE表上进行UPDATE操作后如果某个文件夹的新PARENT_ID字段值等于回收站的IDNotes.ID_TRASH_FOLER
// 触发器会更新所有以该文件夹为父级PARENT_ID的笔记将它们也移动到回收站。
private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER =
"CREATE TRIGGER folder_move_notes_on_trash " +
" AFTER UPDATE ON " + TABLE.NOTE +
@ -253,23 +270,46 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
// 构造器
/**
*
*
* @param context
*/
public NotesDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
// 创建note标签
/**
* NOTE
* <p>
* DBCREATE TABLENOTE
*
*
* @param db SQLite
*/
public void createNoteTable(SQLiteDatabase db) {
// 1. 创建NOTE表
db.execSQL(CREATE_NOTE_TABLE_SQL);
// 2. 重建NOTE表的所有触发器
reCreateNoteTableTriggers(db);
// 3. 创建系统文件夹(根文件夹、临时文件夹、回收站等)
createSystemFolder(db);
Log.d(TAG, "note table has been created");
}
// 重新创建或更新与笔记表相关的触发器。
// 首先使用DROP TRIGGER IF EXISTS语句删除已存在的触发器。确保在重新创建触发器之前不存在同名的触发器。
// 然后使用db.execSQL()方法执行预定义的SQL语句这些语句用于创建新的触发器。
/**
* NOTE
* <p>
* DBNOTE
*
*
* @param db SQLite
*/
private void reCreateNoteTableTriggers(SQLiteDatabase db) {
// 1. 删除所有已存在的NOTE表触发器幂等性设计
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete");
@ -278,6 +318,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash");
// 2. 重新创建所有NOTE表触发器
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER);
@ -287,89 +328,103 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER);
}
/* 以下部分是操作SQLite数据库部分 */
// 功能简介:
// 创建通话记录文件夹、默认文件夹、临时文件夹和回收站,并插入相关数据
// 具体解读:
// ContentValues是一个用于存储键值对的类常用于SQLite数据库的插入操作
// values.put方法可以向ContentValues对象中添加数据。
// NoteColumns.ID是存储文件夹ID的列名Notes.ID_CALL_RECORD_FOLDER是通话记录文件夹的ID。
// NoteColumns.TYPE是存储文件夹类型的列名Notes.TYPE_SYSTEM表示这是一个系统文件夹。
// 使用db.insert方法将values中的数据插入到TABLE.NOTE即标签表中。
// 每次插入新数据前都使用values.clear()方法清除ContentValues对象中的旧数据确保不会重复插入旧数据。
// 然后分别创建默认文件夹、临时文件夹和回收站,并以同样的方法插入数据。
/**
*
* <p>
* DBNOTE
* ID
*
* @param db SQLite
*/
private void createSystemFolder(SQLiteDatabase db) {
ContentValues values = new ContentValues();
/**
* call record foler for call notes
* 1.
* 便
*/
values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* root folder which is default folder
* 2.
* 便
*/
// 创建默认文件夹:重复上述步骤,但这次是为根文件夹插入数据。
values.clear();
values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* temporary folder which is used for moving note
* 3.
* 便
*/
// 创建“临时”文件夹:同样地,为临时文件夹插入数据。
values.clear();
values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* create trash folder
* 4.
* 便
*/
// 创建“回收站”文件夹:最后,为回收站文件夹插入数据。
values.clear();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
//功能简介:
//创建data数据
//解读:
//这个方法用于创建数据表,以及与之相关的触发器。
//创建数据表使用db.execSQL方法执行预定义的SQL语句CREATE_DATA_TABLE_SQL用于创建数据表。
//重新创建数据表触发器调用reCreateDataTableTriggers方法用于删除并重新创建与数据表相关的触发器。
//创建索引使用db.execSQL方法执行CREATE_DATA_NOTE_ID_INDEX_SQL语句为数据表创建索引。
//记录日志使用Log.d方法记录一条调试级别的日志表示数据表已经创建。
/**
* DATA
* <p>
* DBCREATE TABLEDATA
*
*
* @param db SQLite
*/
public void createDataTable(SQLiteDatabase db) {
// 1. 创建DATA表
db.execSQL(CREATE_DATA_TABLE_SQL);
// 2. 重建DATA表的所有触发器
reCreateDataTableTriggers(db);
// 3. 创建NOTE_ID索引提升查询性能
db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL);
Log.d(TAG, "data table has been created");
}
//和上面的note表的reCreate...同理
//重新创建或更新与笔记表相关的触发器。
//首先使用DROP TRIGGER IF EXISTS语句删除已存在的触发器。确保在重新创建触发器之前不存在同名的触发器。
//然后使用db.execSQL()方法执行预定义的SQL语句这些语句用于创建新的触发器。
/**
* DATA
* <p>
* DBDATA
*
*
* @param db SQLite
*/
private void reCreateDataTableTriggers(SQLiteDatabase db) {
// 1. 删除所有已存在的DATA表触发器幂等性设计
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete");
// 2. 重新创建所有DATA表触发器
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER);
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER);
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
//解读:
//synchronized关键字确保在多线程环境下只有一个线程能够进入这个方法防止了同时创建多个实例的情况
//getInstance(Context context)方法使用了单例模式来确保整个应用程序中只有一个NotesDatabaseHelper实例。
//它首先检查mInstance类的静态成员变量没有在代码片段中显示是否为null。
//如果是null则创建一个新的NotesDatabaseHelper实例并将其赋值给mInstance。最后返回mInstance。
/**
* NotesDatabaseHelper
* <p>
* 使线
*
*
* @param context
* @return NotesDatabaseHelper
*/
static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
@ -377,90 +432,125 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
return mInstance;
}
//功能简介:
//当数据库首次创建时onCreate方法会被调用。
//这里重写onCreate方法它调用了上述createNoteTable(db)和createDataTable(db)两个方法
//这样首次创建数据库时就多出了两张表。
/**
*
* <p>
* SQLiteOpenHelper
* DB
*
* @param db SQLite
*/
@Override
public void onCreate(SQLiteDatabase db) {
// 1. 创建NOTE表及相关对象
createNoteTable(db);
// 2. 创建DATA表及相关对象
createDataTable(db);
}
//功能简介:
//当数据库需要升级时即数据库的版本号改变onUpgrade方法会被调用。
//该方法会根据当前的oldVersion和新的newVersion来执行相应的升级操作
/**
*
* <p>
* SQLiteOpenHelper
*
* 使
*
* @param db SQLite
* @param oldVersion
* @param newVersion
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
boolean reCreateTriggers = false;
boolean skipV2 = false;
// 版本1 -> 版本2的升级逻辑
if (oldVersion == 1) {
upgradeToV2(db);
skipV2 = true; // this upgrade including the upgrade from v2 to v3
skipV2 = true; // 此升级已包含版本2到版本3的升级
oldVersion++;
}
// 版本2 -> 版本3的升级逻辑如果未跳过
if (oldVersion == 2 && !skipV2) {
upgradeToV3(db);
reCreateTriggers = true;
oldVersion++;
}
// 版本3 -> 版本4的升级逻辑
if (oldVersion == 3) {
upgradeToV4(db);
oldVersion++;
}
// 如果需要,重新创建触发器
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
}
if (oldVersion != newVersion) { //数据库升级失败,抛出一个异常,表示数据库升级失败
// 版本号检查:确保升级过程正确执行
if (oldVersion != newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion
+ "fails");
}
}
//功能简介:
// 将数据库从版本1升级到版本2。
//解读:
// 首先它删除了已经存在的NOTE和DATA表如果存在的话。DROP TABLE IF EXISTS语句确保了即使这些表不存在也不会抛出错误。
// 然后它调用了createNoteTable(db)和createDataTable(db)方法来重新创建这两个表。这意味着在升级到版本2时这两个表的内容会被完全清除并重新创建新的空表。
/**
* 12
* <p>
* DB
*
*
* @param db SQLite
*/
private void upgradeToV2(SQLiteDatabase db) {
// 1. 删除旧表(如果存在)
db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
// 2. 重新创建所有表
createNoteTable(db);
createDataTable(db);
}
//功能简介:
// 将数据库从版本2或可能是跳过版本2的某个状态升级到版本3。
//解读:
// 首先,删除了三个不再使用的触发器(如果存在的话)。触发器是数据库中的一种对象,可以在插入、更新或删除记录时自动执行某些操作。
// 然后使用ALTER TABLE语句修改表结构向NOTE表中添加了一个名为GTASK_ID的新列并设置默认值为空字符串。
// 最后向NOTE表中插入了一条新的系统文件夹记录表示一个名为“trash folder”的系统文件夹。这可能是用于存储已删除笔记的回收站功能。
/**
* 23
* <p>
* DB
* Google Tasks
*
* @param db SQLite
*/
private void upgradeToV3(SQLiteDatabase db) {
// drop unused triggers
// 1. 删除不再使用的旧触发器
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update");
// add a column for gtask id
// 2. 向NOTE表添加GTASK_ID列用于Google Tasks集成
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID
+ " TEXT NOT NULL DEFAULT ''");
// add a trash system folder
// 3. 向NOTE表插入回收站系统文件夹
ContentValues values = new ContentValues();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
//功能简介:
// 这个方法负责将数据库从版本3升级到版本4。
//解读:
// 它向NOTE表中添加了一个名为VERSION的新列并设置了默认值为0。这个新列用于记录标签版本信息。
/**
* 34
* <p>
* DBNOTEVERSION
*
*
* @param db SQLite
*/
private void upgradeToV4(SQLiteDatabase db) {
// 向NOTE表添加VERSION列用于乐观锁控制
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}

@ -1,6 +1,5 @@
package net.micode.notes.data;
import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentUris;
@ -18,156 +17,189 @@ import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
/**
* 便 (Data Layer - Content Provider)
* <p>
* Android ContentProvider便访
* CRUD
* MVCContentResolverUI访
* URI便Android
*/
public class NotesProvider extends ContentProvider {
// Android 应用程序中的一部分内容提供者ContentProvider
// 内容提供者是 Android 四大组件之一,它允许应用程序之间共享数据。
//概述:
//NotesProvider的主要功能是作为一个内容提供者为其他应用程序或组件提供对“Notes”数据的访问。
//它允许其他应用程序查询、插入、更新或删除标签数据。
//通过URI匹配NotesProvider能够区分对哪种数据类型的请求例如单独的标签、标签的数据、文件夹操作等并执行相应的操作。
//用于匹配不同URI的UriMatcher对象通常用于解析传入的URI并确定应该执行哪种操作。
/**
* URIURI
* <p>
* URIURI
* 便访
*/
private static final UriMatcher mMatcher;
//NotesDatabaseHelper实类用来操作SQLite数据库负责创建、更新和查询数据库。
/**
*
*/
private NotesDatabaseHelper mHelper;
//标签,输出日志时用来表示是该类发出的消息
/**
* 使TAG
*/
private static final String TAG = "NotesProvider";
//6个URI的匹配码用于区分不同的URI类型
private static final int URI_NOTE = 1;
private static final int URI_NOTE_ITEM = 2;
private static final int URI_DATA = 3;
private static final int URI_DATA_ITEM = 4;
private static final int URI_SEARCH = 5;
private static final int URI_SEARCH_SUGGEST = 6;
/**
* URI
* <p>
* 访
* 便
*/
private static final int URI_NOTE = 1; // 便签表(集合)
private static final int URI_NOTE_ITEM = 2; // 单个便签项
private static final int URI_DATA = 3; // 数据表(集合)
private static final int URI_DATA_ITEM = 4; // 单个数据项
private static final int URI_SEARCH = 5; // 便签搜索
private static final int URI_SEARCH_SUGGEST = 6; // 搜索建议
//进一步定义了URI匹配规则和搜索查询的投影
//功能概述:
//初始化了一个UriMatcher对象mMatcher并添加了一系列的URI匹配规则。
//解读:
/**
* URI
* <p>
* URIURI
*
*/
static {
//创建了一个UriMatcher实例并设置默认匹配码为NO_MATCH表示如果没有任何URI匹配则返回这个码。
// 创建UriMatcher实例设置默认匹配码为NO_MATCH
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//添加规则当URI的authority为Notes.AUTHORITY路径为note时返回匹配码URI_NOTE。
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE);
//添加规则当URI的authority为Notes.AUTHORITY路径为note/后跟一个数字(#代表数字返回匹配码URI_NOTE_ITEM。
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM);
//和上面两句同理但用于匹配数据相关的URI
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA);
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM);
//用于匹配搜索相关的URI
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH);
//这两行用于匹配搜索建议相关的URI
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST);
// 添加便签表相关URI规则
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); // content://micode_notes/note
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); // content://micode_notes/note/{id}
// 添加数据表相关URI规则
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); // content://micode_notes/data
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); // content://micode_notes/data/{id}
// 添加搜索相关URI规则
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); // content://micode_notes/search
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); // content://micode_notes/search_suggest_query
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); // content://micode_notes/search_suggest_query/*
}
/**
* x'0A' represents the '\n' character in sqlite. For title and content in the search result,
* we will trim '\n' and white space in order to show more information.
* 便
* <p>
* Android
* SearchManager
*
*
* 1. 使SQLiteREPLACE
* 2. TRIM
* 3. Intent
*
* x'0A'SQLite'\n'
*/
private static final String NOTES_SEARCH_PROJECTION =
NoteColumns.ID + "," // 便签ID
+ NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + "," // Intent额外数据
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + "," // 建议文本行1
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + "," // 建议文本行2
+ R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + "," // 建议图标
+ "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + "," // Intent动作
+ "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA; // Intent数据类型
/**
* 便SQL
* <p>
* 便Android
*
* 1. 使LIKE
* 2. 便
* 3. 便
*/
private static String NOTES_SNIPPET_SEARCH_QUERY =
"SELECT " + NOTES_SEARCH_PROJECTION
+ " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.SNIPPET + " LIKE ?"
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
/**
* ContentProvider
* <p>
* ContentProvider
*
*
* @return true
*/
//功能概述:
//一个 SQL 查询的投影部分,用于定义查询返回的结果集中应该包含哪些列。
//解读:(每行对应)
//返回笔记的 ID。
//笔记的 ID 也被重命名为 SUGGEST_COLUMN_INTENT_EXTRA_DATA这通常用于 Android 的搜索建议中,作为传递给相关 Intent 的额外数据。
//对 SNIPPET 列的处理:首先使用 REPLACE 函数将 x'0A'(即换行符 \n替换为空字符串然后使用 TRIM 函数删除前后的空白字符,处理后的结果分别重命名为 SUGGEST_COLUMN_TEXT_1
//对 SNIPPET 列的处理:首先使用 REPLACE 函数将 x'0A'(即换行符 \n替换为空字符串然后使用 TRIM 函数删除前后的空白字符,处理后的结果分别重命名为 SUGGEST_COLUMN_TEXT_2
//返回一个用于搜索建议图标的资源 ID并命名为 SUGGEST_COLUMN_ICON_1。
//返回一个固定的 Intent 动作 ACTION_VIEW并命名为 SUGGEST_COLUMN_INTENT_ACTION。
//返回一个内容类型,并命名为 SUGGEST_COLUMN_INTENT_DATA。
private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," //返回笔记的 ID
+ NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + ","
+ R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + ","
+ "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + ","
+ "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA;
//功能概述:
//完整的 SQL 查询语句,用于从 TABLE.NOTE 表中检索信息
//解读:
// 使用上面定义的投影来选择数据。
// 并指定从哪个表中选择数据。
//WHERE子句包含三个条件
// ①搜索 SNIPPET 列中包含特定模式的行(? 是一个占位符,实际查询时会用具体的值替换)。
// ②父ID不为回收站的ID排除那些父 ID 为回收站的行。
// ③只选择类型为note标签的行。
private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION
+ " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.SNIPPET + " LIKE ?"
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
//重写onCreate方法
//getContext() 方法被调用以获取当前组件的上下文Context以便 NotesDatabaseHelper 能够访问应用程序的资源和其他功能
//mHelper用于存储从 NotesDatabaseHelper.getInstance 方法返回的实例。这样,该实例就可以在整个组件的其他方法中被访问和使用。
@Override
public boolean onCreate() {
// 获取NotesDatabaseHelper单例实例
mHelper = NotesDatabaseHelper.getInstance(getContext());
return true;
}
//功能:查询数据
/**
* ()
* <p>
* 便
* URI
*
* @param uri URI
* @param projection null
* @param selection WHEREnull
* @param selectionArgs WHEREselection'?'
* @param sortOrder null使
* @return Cursornull
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
//初始化变量:
//Cursor对象 c用来存储查询结果
//使用 NotesDatabaseHelper 的实例 mHelper来获取一个可读的数据库实例
//定义一个字符串id用来存储从URI中解析出的ID
Cursor c = null;
SQLiteDatabase db = mHelper.getReadableDatabase();
String id = null;
//根据匹配不同的URI来进行不同的查询
// 根据URI匹配码执行不同的查询逻辑
switch (mMatcher.match(uri)) {
// URI_NOTE查询整个 NOTE 表。
// URI_NOTE_ITEM查询 NOTE 表中的特定项。ID 从 URI 的路径段中获取,并添加到查询条件中。
// URI_DATA查询整个 DATA 表。
// URI_DATA_ITEM查询 DATA 表中的特定项。ID 的获取和处理方式与 URI_NOTE_ITEM 相同。
case URI_NOTE:
// 查询便签表(集合查询)
c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
// 查询单个便签项ID查询
id = uri.getPathSegments().get(1); // 从URI路径中提取ID
c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_DATA:
// 查询数据表(集合查询)
c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_DATA_ITEM:
id = uri.getPathSegments().get(1);
// 查询单个数据项ID查询
id = uri.getPathSegments().get(1); // 从URI路径中提取ID
c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
//URI_SEARCH 和 URI_SEARCH_SUGGEST处理搜索查询。
// 代码首先检查是否提供了不应与搜索查询一起使用的参数(如 sortOrder, selection, selectionArgs, 或 projection
// 如果提供了这些参数,则抛出一个 IllegalArgumentException。
// 根据 URI 类型,从 URI 的路径段或查询参数中获取搜索字符串 searchString。
// 如果 searchString 为空或无效,则返回 null表示没有搜索结果。
case URI_SEARCH:
case URI_SEARCH_SUGGEST:
// 处理搜索和搜索建议查询
if (sortOrder != null || projection != null) {
throw new IllegalArgumentException(
"do not specify sortOrder, selection, selectionArgs, or projection" + "with this query");
"do not specify sortOrder, selection, selectionArgs, or projection" +
"with this query");
}
String searchString = null;
if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) {
// 搜索建议从URI路径段获取搜索词
if (uri.getPathSegments().size() > 1) {
searchString = uri.getPathSegments().get(1);
}
} else {
// 普通搜索:从查询参数获取搜索模式
searchString = uri.getQueryParameter("pattern");
}
@ -175,9 +207,9 @@ public class NotesProvider extends ContentProvider {
return null;
}
//字符串格式化:格式化后的字符串就会是 "%s%"即包含s是任何文本
//然后执行原始SQL查询
// DB操作执行原始SQL查询进行模糊匹配
try {
// 格式化为SQL LIKE模式的搜索字符串
searchString = String.format("%%%s%%", searchString);
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
new String[] { searchString });
@ -186,92 +218,104 @@ public class NotesProvider extends ContentProvider {
}
break;
//未知URI处理
default:
// 未知URI类型抛出异常
throw new IllegalArgumentException("Unknown URI " + uri);
}
//如果查询结果不为空(即 Cursor 对象 c 不是 null则为其设置一个通知 URI。
//这意味着当与这个 URI 关联的数据发生变化时,任何注册了监听这个 URI 的 ContentObserver 都会被通知。
// 注册内容观察当查询结果对应的数据发生变化时Cursor会自动收到通知
if (c != null) {
c.setNotificationUri(getContext().getContentResolver(), uri);
}
return c;
}
//功能:插入数据
//参数Uri 用来标识要插入数据的表ContentValues对象包含要插入的键值对
/**
* ()
* <p>
* 便
* UI
*
* @param uri URI
* @param values
* @return URIID
*/
@Override
public Uri insert(Uri uri, ContentValues values) {
//获取数据库
//三个长整型变量分别用来存储数据项ID、便签ID 和插入行的ID
SQLiteDatabase db = mHelper.getWritableDatabase();
long dataId = 0, noteId = 0, insertedId = 0;
//对于 URI_NOTE将values插入到 TABLE.NOTE 表中,并返回插入行的 ID。
//对于 URI_DATA首先检查values是否包含 DataColumns.NOTE_ID如果包含则获取其值。如果不包含记录一条日志信息。然后将 values 插入到 TABLE.DATA 表中,并返回插入行的 ID。
//如果 uri 不是已知的 URI 类型,则抛出一个 IllegalArgumentException。
// 根据URI匹配码执行不同的插入逻辑
switch (mMatcher.match(uri)) {
case URI_NOTE:
// DB操作向便签表插入新记录
insertedId = noteId = db.insert(TABLE.NOTE, null, values);
break;
case URI_DATA:
// 检查是否包含关联的便签ID
if (values.containsKey(DataColumns.NOTE_ID)) {
noteId = values.getAsLong(DataColumns.NOTE_ID);
} else {
Log.d(TAG, "Wrong data format without note id:" + values.toString());
}
// DB操作向数据表插入新记录
insertedId = dataId = db.insert(TABLE.DATA, null, values);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
//功能:通知变化
//如果noteId 或 dataId 大于 0即成功插入了数据则使用 ContentResolver 的 notifyChange 方法通知监听这些 URI 的观察者,告知数据已经改变。
//ContentUris.withAppendedId 方法用于在基本 URI 后面追加一个 ID形成完整的 URI。
// Notify the note uri
// 数据变更通知机制:通知所有观察者数据已更新
// 通知便签URI的观察者
if (noteId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
}
// Notify the data uri
// 通知数据URI的观察者
if (dataId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
}
//返回包含新插入数据项ID 的 Uri。允许调用者知道新插入的数据项的位置
// 返回新插入数据的完整URI
return ContentUris.withAppendedId(uri, insertedId);
}
//功能:删除数据项
//参数uri标识要删除数据的表或数据项。 selection一个可选的 WHERE 子句,用于指定删除条件。 selectionArgs一个可选的字符串数组用于替换 selection 中的占位符
/**
* ()
* <p>
* 便ID
* ID<=0
*
* @param uri URI
* @param selection WHEREnull
* @param selectionArgs WHERE
* @return
*/
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
//count记录被删除的行数。
//id用于存储从 URI 中解析出的数据项 ID。
//db可写的数据库对象用于执行删除操作。
//deleteData一个布尔值用于标记是否删除了 DATA 表中的数据。
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean deleteData = false;
// 根据URI匹配码执行不同的删除逻辑
switch (mMatcher.match(uri)) {
//URI_NOTE: 修改 selection 语句:确保只删除 ID 大于 0 的笔记。然后执行删除操作并返回被删除的行数。
//URI_NOTE_ITEM: 从 URI 中解析出 ID。检查 ID 是否小于等于 0如果是则不执行删除操作否则执行删除操作并返回被删除的行数
//URI_DATA 执行删除操作并返回被删除的行数。设置 deleteData 为 true表示删除了 DATA 表中的数据。
//URI_DATA_ITEM 先从 URI 中解析出 ID然后执行删除操作并返回被删除的行数并设置 deleteData 为 true表示删除了 DATA 表中的数据。
case URI_NOTE:
// 便签表集合删除确保不删除系统文件夹ID>0
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
count = db.delete(TABLE.NOTE, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
// 单个便签项删除
id = uri.getPathSegments().get(1);
/**
* ID that smaller than 0 is system folder which is not allowed to
* trash
* ID0
*
*/
long noteId = Long.valueOf(id);
if (noteId <= 0) {
@ -280,94 +324,126 @@ public class NotesProvider extends ContentProvider {
count = db.delete(TABLE.NOTE,
NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
break;
case URI_DATA:
// 数据表集合删除
count = db.delete(TABLE.DATA, selection, selectionArgs);
deleteData = true;
break;
case URI_DATA_ITEM:
// 单个数据项删除
id = uri.getPathSegments().get(1);
count = db.delete(TABLE.DATA,
DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
deleteData = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
//如果 count 大于 0说明有数据被删除。
//如果 deleteData 为 true则通知监听 Notes.CONTENT_NOTE_URI 的观察者,数据已改变。
//通知监听传入 uri 的观察者数据已改变。
// 数据变更通知机制
if (count > 0) {
if (deleteData) {
// 数据表删除需要通知便签观察者,因为便签摘要可能受影响
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
// 通知当前URI的观察者
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
//功能:更新数据库的数据
//参数uri标识要更新数据的表或数据项。 values一个包含新值的键值对集合。
// selection一个可选的 WHERE 子句,用于指定更新条件。 selectionArgs一个可选的字符串数组用于替换 selection 中的占位符。
/**
* ()
* <p>
* 便
* 便
*
* @param uri URI
* @param values
* @param selection WHEREnull
* @param selectionArgs WHERE
* @return
*/
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
//count记录被更新的行数。
//id用于存储从 URI 中解析出的数据项 ID。
//db可写的 SQLite 数据库对象,用于执行更新操作。
//updateData用于标记是否更新了 data 表中的数据。
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean updateData = false;
// 根据URI匹配码执行不同的更新逻辑
switch (mMatcher.match(uri)) {
//URI_NOTE调用 increaseNoteVersion 方法用于增加便签版本然后在note表执行更新操作并返回被更新的行数。
//URI_NOTE_ITEM从 URI 中解析出 ID并调用 increaseNoteVersion 方法,传入解析出的 ID最后在note表执行更新操作并返回被更新的行数。
//URI_DATA在data表执行更新操作并返回被更新的行数。设置 updateData 为 true表示更新了 DATA 表中的数据。
//URI_DATA_ITEM从 URI 中解析出 ID。执行更新操作并返回被更新的行数。置 updateData 为 true表示更新了 DATA 表中的数据。
case URI_NOTE:
// 便签表集合更新:增加版本号并执行更新
increaseNoteVersion(-1, selection, selectionArgs);
count = db.update(TABLE.NOTE, values, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
// 单个便签项更新:增加版本号并执行更新
id = uri.getPathSegments().get(1);
increaseNoteVersion(Long.valueOf(id), selection, selectionArgs);
count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
break;
case URI_DATA:
// 数据表集合更新
count = db.update(TABLE.DATA, values, selection, selectionArgs);
updateData = true;
break;
case URI_DATA_ITEM:
// 单个数据项更新
id = uri.getPathSegments().get(1);
count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
updateData = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
//如果 count 大于 0说明有数据被更新。
//如果 updateData 为 true则通知监听 Notes.CONTENT_NOTE_URI 的观察者数据已改变。
//通知监听传入 uri 的观察者数据已改变。
// 数据变更通知机制
if (count > 0) {
if (updateData) {
// 数据表更新需要通知便签观察者
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
// 通知当前URI的观察者
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
//解析传入的条件语句:一个 SQL WHERE 子句的一部分
/**
* ANDWHERE
* <p>
* IDWHERE
* " AND (条件)"
*
* @param selection
* @return WHERE
*/
private String parseSelection(String selection) {
return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
}
//更新note表的version列将其值增加 1。
/**
* 便 ()
* <p>
* 便
*
*
* @param id 便ID-1使selection
* @param selection
* @param selectionArgs
*/
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
@ -376,6 +452,7 @@ public class NotesProvider extends ContentProvider {
sql.append(NoteColumns.VERSION);
sql.append("=" + NoteColumns.VERSION + "+1 ");
// 构建WHERE子句
if (id > 0 || !TextUtils.isEmpty(selection)) {
sql.append(" WHERE ");
}
@ -383,6 +460,7 @@ public class NotesProvider extends ContentProvider {
sql.append(NoteColumns.ID + "=" + String.valueOf(id));
}
if (!TextUtils.isEmpty(selection)) {
// 处理参数替换
String selectString = id > 0 ? parseSelection(selection) : selection;
for (String args : selectionArgs) {
selectString = selectString.replaceFirst("\\?", args);
@ -390,13 +468,24 @@ public class NotesProvider extends ContentProvider {
sql.append(selectString);
}
// DB操作执行版本号更新
mHelper.getWritableDatabase().execSQL(sql.toString());
}
/**
* URIMIME
* <p>
* URIIntent
* null
*
* @param uri URI
* @return URIMIMEnull
*/
@Override
public String getType(Uri uri) {
// TODO Auto-generated method stub
// TODO: 根据URI返回相应的MIME类型
// 例如对于单个便签返回vnd.android.cursor.item/text_note
// 对于便签列表返回vnd.android.cursor.dir/text_note
return null;
}
}

@ -16,86 +16,111 @@
package net.micode.notes.gtask.data;
import android.database.Cursor; // 数据库查询游标:父类方法参数,此类未实际使用
import android.util.Log; // 日志工具:打印错误/警告日志
import android.database.Cursor;
import android.util.Log;
import net.micode.notes.tool.GTaskStringUtils; // GTask字符串工具类存储元数据相关常量如Gid字段名
import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONException; // JSON解析异常JSON操作失败时抛出
import org.json.JSONObject; // JSON对象存储和解析元数据键值对
import org.json.JSONException;
import org.json.JSONObject;
/**
* GTask
* GTaskGTaskTask
* 便GTaskGTaskID
* JSONID
* GTask (Data Layer - Google Tasks Integration)
* <p>
* 便Google Tasks
* 便Google Tasks
* TaskGTask
* JSONnotes
* 使便
*/
public class MetaData extends Task {
// 日志标签:使用类名作为标签,方便定位该类的日志信息
/**
* 使TAG
*/
private final static String TAG = MetaData.class.getSimpleName();
// 关联的远程GTask唯一ID用来绑定本地便签和远程GTask任务null表示未关联
/**
* Google TasksID
* <p>
* Google Tasks
* 便
*/
private String mRelatedGid = null;
/**
* GTask ID
* GTaskIDJSONJSON
* 便GTask
* @param gid GTaskID
* @param metaInfo JSON
*
* <p>
* Google TasksIDJSON
* notes
* notesGTask
*
* @param gid Google Tasks
* @param metaInfo JSON
*/
public void setMeta(String gid, JSONObject metaInfo) {
try {
// 将远程GTask ID存入JSON对象键为元数据头部GTask ID常量
// 将Google Tasks任务ID添加到元信息JSON对象中
metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid);
} catch (JSONException e) {
// JSON存入失败打印错误日志
// 异常处理JSON操作失败记录错误日志
Log.e(TAG, "failed to put related gid");
}
// 将元信息JSON对象转为字符串设置为当前任务的备注存储元数据
// 将JSON对象序列化为字符串存储到notes字段
setNotes(metaInfo.toString());
// 将任务名称设为元数据专用名称,标识这是元数据任务
// 设置任务名称为元数据标识便于GTask服务器识别
setName(GTaskStringUtils.META_NOTE_NAME);
}
/**
* GTaskID
* 便GTaskID
* @return GTask IDnull
* Google TasksID
* <p>
* 便
*
*
* @return Google TasksIDnull
*/
public String getRelatedGid() {
return mRelatedGid;
}
/**
*
* JSONGTask
* @return true=false=
* Google Tasks
* <p>
*
* notesJSON
* Google Tasks
*
* @return truefalse
*/
@Override
public boolean isWorthSaving() {
// 只要有元数据内容就值得保存
return getNotes() != null;
}
/**
* GTaskJSON
* GTaskGTask ID
* @param js GTaskJSON
* Google Tasks JSON
* <p>
* Task
* notesJSONGoogle TasksID
*
* @param js Google TasksJSON
*/
@Override
public void setContentByRemoteJSON(JSONObject js) {
// 先调用父类方法解析通用的GTask任务内容如名称、备注等
// 1. 调用父类方法解析基础任务信息
super.setContentByRemoteJSON(js);
// 如果任务备注元数据JSON字符串不为空
// 2. 如果notes字段包含元数据解析其中的关联ID
if (getNotes() != null) {
try {
// 将备注字符串转为JSON对象去除首尾空白字符避免解析失败
// JSON解析将notes字符串转换为JSON对象去除首尾空格
JSONObject metaInfo = new JSONObject(getNotes().trim());
// 从JSON对象中提取关联的远程GTask ID
// 提取关联的Google Tasks任务ID
mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID);
} catch (JSONException e) {
// JSON解析失败打印警告日志并将关联ID置为null
// 异常处理JSON解析失败清除关联ID并记录警告日志
Log.w(TAG, "failed to get related gid");
mRelatedGid = null;
}
@ -103,37 +128,51 @@ public class MetaData extends Task {
}
/**
* JSON
* MetaDataGTaskJSON
* 访
* JSON
* <p>
* JSON
* MetaDataGoogle Tasks
*
*
* @param js JSON
* @throws IllegalAccessError
*/
@Override
public void setContentByLocalJSON(JSONObject js) {
// 抛出非法访问错误,提示该方法不应该被调用
// 防御性设计:防止错误的调用路径
throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called");
}
/**
* JSON
* MetaDataJSON访
* @return
* JSON
* <p>
* JSON
* MetaData
* setMetaJSON
*
* @return
* @throws IllegalAccessError
*/
@Override
public JSONObject getLocalJSONFromContent() {
// 抛出非法访问错误,提示该方法不应该被调用
// 防御性设计:防止错误的调用路径
throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called");
}
/**
*
* MetaData访
* @param c 使
* @return
*
* <p>
*
* MetaData
*
*
* @param c 使
* @return
* @throws IllegalAccessError
*/
@Override
public int getSyncAction(Cursor c) {
// 抛出非法访问错误,提示该方法不应该被调用
// 防御性设计:防止错误的调用路径
throw new IllegalAccessError("MetaData:getSyncAction should not be called");
}
}

@ -16,147 +16,259 @@
package net.micode.notes.gtask.data;
import android.database.Cursor; // 数据库查询游标:用于子类查询本地数据库,判断同步动作
import android.database.Cursor;
import org.json.JSONObject; // JSON对象用于封装GTask接口所需参数或存储本地数据
import org.json.JSONObject;
/**
* GTask
* GTask
* ID
* GTask
* GTask (Data Layer - Google Tasks Synchronization)
* <p>
* Google Tasks
* 便Google Tasks
* GTask
*
*
*/
public abstract class Node {
// 同步动作常量:表示无任何同步操作(本地和远程数据一致)
/**
*
*/
public static final int SYNC_ACTION_NONE = 0;
// 同步动作常量往远程GTask服务器添加新数据本地有新增远程没有
/**
*
*/
public static final int SYNC_ACTION_ADD_REMOTE = 1;
// 同步动作常量:往本地数据库添加新数据(远程有新增,本地没有)
/**
*
*/
public static final int SYNC_ACTION_ADD_LOCAL = 2;
// 同步动作常量删除远程GTask服务器上的数据本地已删除远程还存在
/**
*
*/
public static final int SYNC_ACTION_DEL_REMOTE = 3;
// 同步动作常量:删除本地数据库里的数据(远程已删除,本地还存在)
/**
*
*/
public static final int SYNC_ACTION_DEL_LOCAL = 4;
// 同步动作常量更新远程GTask服务器上的数据本地修改比远程新
/**
*
*/
public static final int SYNC_ACTION_UPDATE_REMOTE = 5;
// 同步动作常量:更新本地数据库里的数据(远程修改比本地新)
/**
*
*/
public static final int SYNC_ACTION_UPDATE_LOCAL = 6;
// 同步动作常量:同步冲突(本地和远程都有修改,无法自动合并)
/**
*
*/
public static final int SYNC_ACTION_UPDATE_CONFLICT = 7;
// 同步动作常量:同步异常(同步过程中出现错误,无法判断动作类型)
/**
*
*/
public static final int SYNC_ACTION_ERROR = 8;
// 远程GTask唯一标识Gid每个同步数据在GTask服务器上的唯一IDnull表示未上传到远程
/**
* Google TasksGID
* <p>
* Google Tasks
*
* null
*/
private String mGid;
// 节点名称:同步数据的名称(比如便签标题、文件夹名称)
/**
*
* <p>
* 便
*
*/
private String mName;
// 最后修改时间戳:数据最后一次被修改的时间(用于判断本地和远程哪个更新)
/**
*
* <p>
*
*
*/
private long mLastModified;
// 删除标记是否被标记为删除true=已删除,需要同步删除操作)
/**
*
* <p>
*
*
*/
private boolean mDeleted;
/**
*
* Node
*
* <p>
*
*
*/
public Node() {
mGid = null; // 默认未关联远程GTask无Gid
mName = ""; // 默认名称为空字符串
mLastModified = 0; // 默认最后修改时间为0未修改
mDeleted = false; // 默认未删除
mGid = null; // 默认未关联云端
mName = ""; // 默认名称为空
mLastModified = 0; // 默认从未修改
mDeleted = false; // 默认未删除
}
/**
* JSON
* GTaskJSON
* GTask便
* @param actionId IDGTask
* @return JSON
* JSON
* <p>
*
* Google Tasks APIJSON
*
*
* @param actionId Google Tasks API
* @return Google Tasks APIJSON
*/
public abstract JSONObject getCreateAction(int actionId);
/**
* JSON
* GTaskJSON
* GTask便
* @param actionId IDGTask
* @return JSON
* JSON
* <p>
*
*
*
* @param actionId Google Tasks API
* @return Google Tasks APIJSON
*/
public abstract JSONObject getUpdateAction(int actionId);
/**
* GTaskJSON
* GTask
* JSON便
* @param js GTaskJSON
* JSON
* <p>
* JSON
* Google Tasks API便
*
* @param js Google Tasks APIJSON
*/
public abstract void setContentByRemoteJSON(JSONObject js);
/**
* JSON
* JSON
* JSON便
* @param js JSON
* JSON
* <p>
* JSON
*
*
* @param js JSON
*/
public abstract void setContentByLocalJSON(JSONObject js);
/**
* JSON
* 便
* JSON便
* @return JSON
* JSON
* <p>
* JSON
*
* JSONsetContentByLocalJSON
*
* @return JSON
*/
public abstract JSONObject getLocalJSONFromContent();
/**
*
* Cursor
*
* @param c
* @return SYNC_ACTION_XXX
*
* <p>
*
*
*
*
*
* @param c
* @return SYNC_ACTION_XXX
*/
public abstract int getSyncAction(Cursor c);
// 以下是通用属性的Setter方法设置对应的属性值供子类或外部调用
/** 设置远程GTask的唯一标识Gid */
// ==================== 通用属性访问器 ====================
/**
* Google Tasks
* <p>
*
*
*
* @param gid Google Tasks
*/
public void setGid(String gid) {
this.mGid = gid;
}
/** 设置节点名称(如便签标题、文件夹名称) */
/**
*
*
* @param name
*/
public void setName(String name) {
this.mName = name;
}
/** 设置最后修改时间戳(用于同步对比新旧) */
/**
*
* <p>
*
*
*
* @param lastModified
*/
public void setLastModified(long lastModified) {
this.mLastModified = lastModified;
}
/** 设置删除标记(标记是否需要同步删除操作) */
/**
*
* <p>
*
*
*
* @param deleted true
*/
public void setDeleted(boolean deleted) {
this.mDeleted = deleted;
}
// 以下是通用属性的Getter方法获取对应的属性值供子类或外部调用
/** 获取远程GTask的唯一标识Gid */
/**
* Google Tasks
*
* @return Google Tasksnull
*/
public String getGid() {
return this.mGid;
}
/** 获取节点名称(如便签标题、文件夹名称) */
/**
*
*
* @return
*/
public String getName() {
return this.mName;
}
/** 获取最后修改时间戳(用于同步对比新旧) */
/**
*
*
* @return
*/
public long getLastModified() {
return this.mLastModified;
}
/** 获取删除标记(判断是否需要同步删除操作) */
/**
*
*
* @return true
*/
public boolean getDeleted() {
return this.mDeleted;
}
}

@ -34,259 +34,306 @@ import net.micode.notes.gtask.exception.ActionFailureException;
import org.json.JSONException;
import org.json.JSONObject;
/**
* 便
* 便
*
* 便 (Data Layer - Note Details Management)
* <p>
* 便dataCRUDJSON
* 便
* 访
* 访
*
*/
public class SqlData {
// 日志标签:打印这个类的日志时,用来标识日志来源
/**
* 使TAG
*/
private static final String TAG = SqlData.class.getSimpleName();
// 无效的编号用来标记数据还没有有效的唯一ID
/**
* ID
*/
private static final int INVALID_ID = -99999;
// 查询数据明细表时,要获取的字段清单(相当于要查的列名)
/**
*
* <p>
* data
*
*/
public static final String[] PROJECTION_DATA = new String[] {
DataColumns.ID, // 数据唯一ID
DataColumns.MIME_TYPE, // 数据类型(比如普通便签、通话便签)
DataColumns.CONTENT, // 数据内容(比如便签正文)
DataColumns.DATA1, // 扩展字段1存一些额外信息比如便签模式
DataColumns.DATA3 // 扩展字段3存一些额外信息
DataColumns.ID, // 数据记录ID
DataColumns.MIME_TYPE, // 数据类型标识
DataColumns.CONTENT, // 便签核心内容
DataColumns.DATA1, // 扩展字段1用于存储模式等整型数据
DataColumns.DATA3 // 扩展字段3用于存储电话号码等文本数据
};
// 上面字段清单对应的索引位置,方便从查询结果里快速取值
public static final int DATA_ID_COLUMN = 0; // 数据ID在查询结果里的位置
public static final int DATA_MIME_TYPE_COLUMN = 1; // 数据类型在查询结果里的位置
public static final int DATA_CONTENT_COLUMN = 2; // 数据内容在查询结果里的位置
public static final int DATA_CONTENT_DATA_1_COLUMN = 3; // 扩展字段1在查询结果里的位置
public static final int DATA_CONTENT_DATA_3_COLUMN = 4; // 扩展字段3在查询结果里的位置
/**
*
* <p>
* PROJECTION_DATA
*
*/
public static final int DATA_ID_COLUMN = 0;
public static final int DATA_MIME_TYPE_COLUMN = 1;
public static final int DATA_CONTENT_COLUMN = 2;
public static final int DATA_CONTENT_DATA_1_COLUMN = 3;
public static final int DATA_CONTENT_DATA_3_COLUMN = 4;
// 数据库操作工具:用来和本地便签数据库打交道(增删改查)
/**
* ContentResolver访ContentProvider
*/
private ContentResolver mContentResolver;
// 是否是新数据true=新创建的还没存到数据库false=已存在的(从数据库加载的)
/**
* true
*/
private boolean mIsCreate;
// 这条数据的唯一ID
/**
* ID
*/
private long mDataId;
// 这条数据的类型(比如普通便签、通话便签)
/**
* MIME
*/
private String mDataMimeType;
// 这条数据的内容(比如便签的正文文字)
/**
* 便
*/
private String mDataContent;
// 扩展字段1的值存额外信息比如便签是普通模式还是清单模式
/**
* 1
*/
private long mDataContentData1;
// 扩展字段3的值存额外信息
/**
* 3
*/
private String mDataContentData3;
// 数据变更记录:用来存和原来不一样的内容(只更新有变化的部分,提高效率)
/**
*
* <p>
*
*
*/
private ContentValues mDiffDataValues;
/**
*
* @param context
*
* <p>
*
* 便
*
* @param context ContentResolver
*/
public SqlData(Context context) {
// 获取数据库操作工具
// 获取ContentResolver用于数据库访问
mContentResolver = context.getContentResolver();
// 标记为新数据(还没存数据库)
mIsCreate = true;
// 初始化为无效ID还没有数据库分配的唯一ID
mDataId = INVALID_ID;
// 初始数据类型为普通便签
mDataMimeType = DataConstants.NOTE;
// 初始内容为空字符串
mDataContent = "";
// 初始扩展字段1的值为0
mDataContentData1 = 0;
// 初始扩展字段3的值为空字符串
mDataContentData3 = "";
// 初始化数据变更记录容器
mDiffDataValues = new ContentValues();
// 设置初始状态
mIsCreate = true; // 标记为新建数据
mDataId = INVALID_ID; // ID设为无效值未保存
mDataMimeType = DataConstants.NOTE; // 默认类型为普通便签
mDataContent = ""; // 默认内容为空
mDataContentData1 = 0; // 扩展字段1默认值
mDataContentData3 = ""; // 扩展字段3默认值
mDiffDataValues = new ContentValues(); // 初始化差异容器
}
/**
*
* @param context
* @param c
*
* <p>
*
* 便
*
* @param context
* @param c data
*/
public SqlData(Context context, Cursor c) {
// 获取数据库操作工具
// 获取ContentResolver用于数据库访问
mContentResolver = context.getContentResolver();
// 标记为已有数据(不是新创建的)
mIsCreate = false;
// 从查询结果里加载数据到当前对象
// 设置初始状态
mIsCreate = false; // 标记为已存在数据
mDiffDataValues = new ContentValues(); // 初始化差异容器
// 从游标加载数据
loadFromCursor(c);
// 初始化数据变更记录容器
mDiffDataValues = new ContentValues();
}
/**
*
*
* @param c
*
* <p>
* DBCursordata
*
*
* @param c PROJECTION_DATA
*/
private void loadFromCursor(Cursor c) {
// 从查询结果里取数据ID
// 提取并设置所有字段值
mDataId = c.getLong(DATA_ID_COLUMN);
// 从查询结果里取数据类型
mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN);
// 从查询结果里取数据内容
mDataContent = c.getString(DATA_CONTENT_COLUMN);
// 从查询结果里取扩展字段1的值
mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN);
// 从查询结果里取扩展字段3的值
mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN);
}
/**
* JSON
* JSON
*
* @param js JSON
* @throws JSONException JSON
* JSON
* <p>
* JSON
*
* GTask
*
* @param js JSON
* @throws JSONException JSON
*/
public void setContent(JSONObject js) throws JSONException {
// 从JSON里取数据ID没有的话就用无效ID
// 1. 处理ID字段
long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID;
// 如果是新数据或者当前ID和JSON里的ID不一样就记录这个变更
if (mIsCreate || mDataId != dataId) {
mDiffDataValues.put(DataColumns.ID, dataId);
}
// 更新当前数据ID
mDataId = dataId;
// 从JSON里取数据类型没有的话就默认是普通便签
String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE)
: DataConstants.NOTE;
// 如果是新数据或者当前类型和JSON里的类型不一样就记录这个变更
// 2. 处理MIME类型字段
String dataMimeType = js.has(DataColumns.MIME_TYPE) ?
js.getString(DataColumns.MIME_TYPE) : DataConstants.NOTE;
if (mIsCreate || !mDataMimeType.equals(dataMimeType)) {
mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType);
}
// 更新当前数据类型
mDataMimeType = dataMimeType;
// 从JSON里取数据内容没有的话就为空字符串
String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : "";
// 如果是新数据或者当前内容和JSON里的内容不一样就记录这个变更
// 3. 处理内容字段
String dataContent = js.has(DataColumns.CONTENT) ?
js.getString(DataColumns.CONTENT) : "";
if (mIsCreate || !mDataContent.equals(dataContent)) {
mDiffDataValues.put(DataColumns.CONTENT, dataContent);
}
// 更新当前数据内容
mDataContent = dataContent;
// 从JSON里取扩展字段1的值没有的话就为0
long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0;
// 如果是新数据或者当前扩展字段1和JSON里的不一样就记录这个变更
// 4. 处理扩展字段1通常用于存储模式等整型数据
long dataContentData1 = js.has(DataColumns.DATA1) ?
js.getLong(DataColumns.DATA1) : 0;
if (mIsCreate || mDataContentData1 != dataContentData1) {
mDiffDataValues.put(DataColumns.DATA1, dataContentData1);
}
// 更新当前扩展字段1的值
mDataContentData1 = dataContentData1;
// 从JSON里取扩展字段3的值没有的话就为空字符串
String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : "";
// 如果是新数据或者当前扩展字段3和JSON里的不一样就记录这个变更
// 5. 处理扩展字段3通常用于存储电话号码等文本数据
String dataContentData3 = js.has(DataColumns.DATA3) ?
js.getString(DataColumns.DATA3) : "";
if (mIsCreate || !mDataContentData3.equals(dataContentData3)) {
mDiffDataValues.put(DataColumns.DATA3, dataContentData3);
}
// 更新当前扩展字段3的值
mDataContentData3 = dataContentData3;
}
/**
* JSON
* JSON便使
* @return JSON
* @throws JSONException JSON
* JSON
* <p>
* JSON
* GTask
*
* @return JSON
* @throws JSONException JSON
*/
public JSONObject getContent() throws JSONException {
// 如果是新数据还没存到数据库打印错误日志并返回null
// 安全检查新建数据不能转换为JSON未保存到数据库
if (mIsCreate) {
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
// 创建空的JSON数据包
// 构建JSON对象包含所有数据字段
JSONObject js = new JSONObject();
// 把数据ID存入JSON
js.put(DataColumns.ID, mDataId);
// 把数据类型存入JSON
js.put(DataColumns.MIME_TYPE, mDataMimeType);
// 把数据内容存入JSON
js.put(DataColumns.CONTENT, mDataContent);
// 把扩展字段1的值存入JSON
js.put(DataColumns.DATA1, mDataContentData1);
// 把扩展字段3的值存入JSON
js.put(DataColumns.DATA3, mDataContentData3);
// 返回打包好的JSON
return js;
}
/**
*
*
* @param noteId 便ID便
* @param validateVersion 便
* @param version 便
*
* <p>
* DB
*
*
*
* @param noteId 便ID
* @param validateVersion
* @param version validateVersiontrue
*/
public void commit(long noteId, boolean validateVersion, long version) {
// 如果是新数据(要插入数据库)
if (mIsCreate) {
// 如果数据ID是无效的就把ID从变更记录里移除数据库会自动生成唯一ID
// Case A: 新建数据,执行插入操作
// 如果ID为无效值且差异容器中包含ID移除ID让数据库自动生成
if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) {
mDiffDataValues.remove(DataColumns.ID);
}
// 把所属的便签ID存入变更记录关联到便签主表
// 设置关联的便签ID
mDiffDataValues.put(DataColumns.NOTE_ID, noteId);
// 把变更记录里的内容插入到数据明细表返回新数据的URI
// DB操作插入新记录
Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues);
try {
// 从返回的URI里提取数据库分配的唯一数据ID
// 从返回的URI中提取新生成的ID
mDataId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
// 提取ID失败打印错误日志并抛出异常
// 异常处理:插入失败,记录错误并抛出业务异常
Log.e(TAG, "Get note id error :" + e.toString());
throw new ActionFailureException("create note failed");
}
} else {
// 如果是已有数据(要更新数据库),且有变更内容
// Case B: 更新已有数据,执行更新操作
// 只有当有字段变化时才执行更新
if (mDiffDataValues.size() > 0) {
int result = 0;
// 如果不需要验证便签版本,直接更新数据
if (!validateVersion) {
// 无需版本验证,直接更新
result = mContentResolver.update(
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, mDataId),
mDiffDataValues, null, null);
} else {
// 如果需要验证便签版本,只有版本匹配时才更新(避免冲突
// 需要版本验证:只有版本匹配时才更新(乐观锁控制
result = mContentResolver.update(
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, mDataId),
mDiffDataValues,
// 子查询:检查关联的便签版本是否匹配
" ? in (SELECT " + NoteColumns.ID + " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.VERSION + "=?)",
new String[] { String.valueOf(noteId), String.valueOf(version) });
}
// 如果更新结果为0说明没有更新成功可能版本不匹配或数据已被修改
// 更新结果检查0表示没有记录被更新可能版本不匹配或数据已被删除
if (result == 0) {
Log.w(TAG, "there is no update. maybe user updates note when syncing");
}
}
}
// 清空变更记录(本次提交完成,下次变更重新记录)
// 重置状态:清空差异容器,标记为已存在数据
mDiffDataValues.clear();
// 标记为非新数据(就算是刚插入的,现在也已经存到数据库了)
mIsCreate = false;
}
/**
* ID
* @return ID
* ID
*
* @return ID
*/
public long getId() {
return mDataId;

@ -37,304 +37,369 @@ import org.json.JSONObject;
import java.util.ArrayList;
/**
* 便
* 便便
* 便便
* 便便
* 便 (Data Layer - Note Master Table Management)
* <p>
* 便noteCRUD便
* 便Widget
* 便JSON/
* GTask
* 线
*/
public class SqlNote {
// 日志标签:打印这个类的日志时,用来标识日志来自这个类
/**
* 使TAG
*/
private static final String TAG = SqlNote.class.getSimpleName();
// 无效的便签ID用来标记便签还没有有效的唯一编号
/**
* ID便
*/
private static final int INVALID_ID = -99999;
// 查询便签主表时,要获取的字段清单(相当于要查的列名,对应便签的各项基本信息)
/**
* 便
* <p>
* note
* 便
*/
public static final String[] PROJECTION_NOTE = new String[] {
NoteColumns.ID, // 便签唯一ID
NoteColumns.ALERTED_DATE, // 提醒时间
NoteColumns.BG_COLOR_ID, // 背景颜色ID
NoteColumns.CREATED_DATE, // 创建时间
NoteColumns.HAS_ATTACHMENT, // 是否有附件0=没有1=有)
NoteColumns.MODIFIED_DATE, // 最后修改时间
NoteColumns.NOTES_COUNT, // 子便签数量
NoteColumns.PARENT_ID, // 所属文件夹ID
NoteColumns.SNIPPET, // 便签摘要(标题/部分正文)
NoteColumns.TYPE, // 便签类型(普通便签/文件夹/系统文件夹)
NoteColumns.WIDGET_ID, // 桌面小组件ID
NoteColumns.WIDGET_TYPE, // 桌面小组件类型
NoteColumns.SYNC_ID, // 同步编号
NoteColumns.LOCAL_MODIFIED, // 本地修改标记
NoteColumns.ORIGIN_PARENT_ID, // 原始所属文件夹ID
NoteColumns.GTASK_ID, // 对应云端GTask的ID
NoteColumns.VERSION // 便签版本号(用来避免同步冲突)
NoteColumns.ID, // 便签ID
NoteColumns.ALERTED_DATE, // 提醒时间
NoteColumns.BG_COLOR_ID, // 背景颜色ID
NoteColumns.CREATED_DATE, // 创建时间
NoteColumns.HAS_ATTACHMENT, // 附件标记
NoteColumns.MODIFIED_DATE, // 最后修改时间
NoteColumns.NOTES_COUNT, // 子便签数量
NoteColumns.PARENT_ID, // 父文件夹ID
NoteColumns.SNIPPET, // 便签摘要
NoteColumns.TYPE, // 便签类型
NoteColumns.WIDGET_ID, // 桌面Widget ID
NoteColumns.WIDGET_TYPE, // 桌面Widget类型
NoteColumns.SYNC_ID, // 同步ID
NoteColumns.LOCAL_MODIFIED, // 本地修改标记
NoteColumns.ORIGIN_PARENT_ID, // 原始父文件夹ID
NoteColumns.GTASK_ID, // Google Tasks关联ID
NoteColumns.VERSION // 数据版本号
};
// 上面字段清单对应的索引位置,方便从查询结果里快速取值
public static final int ID_COLUMN = 0; // 便签ID在查询结果里的位置
public static final int ALERTED_DATE_COLUMN = 1; // 提醒时间在查询结果里的位置
public static final int BG_COLOR_ID_COLUMN = 2; // 背景颜色ID在查询结果里的位置
public static final int CREATED_DATE_COLUMN = 3; // 创建时间在查询结果里的位置
public static final int HAS_ATTACHMENT_COLUMN = 4; // 是否有附件在查询结果里的位置
public static final int MODIFIED_DATE_COLUMN = 5; // 最后修改时间在查询结果里的位置
public static final int NOTES_COUNT_COLUMN = 6; // 子便签数量在查询结果里的位置
public static final int PARENT_ID_COLUMN = 7; // 所属文件夹ID在查询结果里的位置
public static final int SNIPPET_COLUMN = 8; // 便签摘要在查询结果里的位置
public static final int TYPE_COLUMN = 9; // 便签类型在查询结果里的位置
public static final int WIDGET_ID_COLUMN = 10; // 桌面小组件ID在查询结果里的位置
public static final int WIDGET_TYPE_COLUMN = 11; // 桌面小组件类型在查询结果里的位置
public static final int SYNC_ID_COLUMN = 12; // 同步编号在查询结果里的位置
public static final int LOCAL_MODIFIED_COLUMN = 13; // 本地修改标记在查询结果里的位置
public static final int ORIGIN_PARENT_ID_COLUMN = 14; // 原始所属文件夹ID在查询结果里的位置
public static final int GTASK_ID_COLUMN = 15; // 云端GTaskID在查询结果里的位置
public static final int VERSION_COLUMN = 16; // 便签版本号在查询结果里的位置
// 上下文:用来获取数据库操作工具、默认配置等
/**
*
*/
public static final int ID_COLUMN = 0;
public static final int ALERTED_DATE_COLUMN = 1;
public static final int BG_COLOR_ID_COLUMN = 2;
public static final int CREATED_DATE_COLUMN = 3;
public static final int HAS_ATTACHMENT_COLUMN = 4;
public static final int MODIFIED_DATE_COLUMN = 5;
public static final int NOTES_COUNT_COLUMN = 6;
public static final int PARENT_ID_COLUMN = 7;
public static final int SNIPPET_COLUMN = 8;
public static final int TYPE_COLUMN = 9;
public static final int WIDGET_ID_COLUMN = 10;
public static final int WIDGET_TYPE_COLUMN = 11;
public static final int SYNC_ID_COLUMN = 12;
public static final int LOCAL_MODIFIED_COLUMN = 13;
public static final int ORIGIN_PARENT_ID_COLUMN = 14;
public static final int GTASK_ID_COLUMN = 15;
public static final int VERSION_COLUMN = 16;
/**
* 访
*/
private Context mContext;
// 数据库操作工具:用来和本地便签数据库打交道(增删改查)
/**
* ContentResolver访ContentProvider
*/
private ContentResolver mContentResolver;
// 是否是新便签true=新创建的还没存到数据库false=已存在的(从数据库加载的)
/**
* true便
*/
private boolean mIsCreate;
// 便签唯一ID
/**
* 便ID
*/
private long mId;
// 提醒时间比如设置了早上8点提醒这里存的是对应的时间戳
/**
*
*/
private long mAlertDate;
// 背景颜色ID对应不同的便签背景色比如白色、黄色
/**
* ID
*/
private int mBgColorId;
// 创建时间(存的是时间戳,记录便签什么时候被创建)
/**
*
*/
private long mCreatedDate;
// 是否有附件0=没有附件1=有附件)
/**
* 0=1=
*/
private int mHasAttachment;
// 最后修改时间(存的是时间戳,记录便签最后一次被修改的时间)
/**
*
*/
private long mModifiedDate;
// 所属文件夹ID标记这个便签在哪个文件夹里
/**
* ID
*/
private long mParentId;
// 便签摘要(一般是便签的标题,或者正文的前几句)
/**
* 便
*/
private String mSnippet;
// 便签类型(普通便签/文件夹/系统文件夹)
/**
* 便TYPE_NOTE/TYPE_FOLDER/TYPE_SYSTEM
*/
private int mType;
// 桌面小组件ID如果这个便签添加到桌面这里存小组件的编号
/**
* Widget ID
*/
private int mWidgetId;
// 桌面小组件类型(标记桌面小组件的样式)
/**
* Widget
*/
private int mWidgetType;
// 原始所属文件夹ID记录便签最初在哪个文件夹里
/**
* ID
*/
private long mOriginParent;
// 便签版本号(每次修改都会递增,用来避免同步时多人同时修改导致冲突)
/**
*
*/
private long mVersion;
// 便签主表的变更记录:只存和原来不一样的内容,后续只更新这些变更,提高效率
/**
* 便
*/
private ContentValues mDiffNoteValues;
// 便签的明细数据列表比如普通便签的正文内容用SqlData对象存储
/**
* 便data
*/
private ArrayList<SqlData> mDataList;
/**
* 便
* @param context
* 便
* <p>
* 便
*
*
* @param context
*/
public SqlNote(Context context) {
mContext = context;
// 获取数据库操作工具
mContentResolver = context.getContentResolver();
// 标记为新便签(还没存数据库)
// 初始化默认值
mIsCreate = true;
// 初始化为无效ID还没有数据库分配的唯一ID
mId = INVALID_ID;
// 初始提醒时间为0没有设置提醒
mAlertDate = 0;
// 初始背景色为系统默认背景色
mBgColorId = ResourceParser.getDefaultBgId(context);
// 初始创建时间为当前时间(获取系统当前时间戳)
mCreatedDate = System.currentTimeMillis();
// 初始没有附件
mHasAttachment = 0;
// 初始最后修改时间为当前时间
mModifiedDate = System.currentTimeMillis();
// 初始所属文件夹ID为0默认在根目录
mParentId = 0;
// 初始摘要为空字符串
mSnippet = "";
// 初始类型为普通便签
mType = Notes.TYPE_NOTE;
// 初始桌面小组件ID为无效ID没有添加到桌面
mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
// 初始桌面小组件类型为无效类型
mWidgetType = Notes.TYPE_WIDGET_INVALIDE;
// 初始原始所属文件夹ID为0
mOriginParent = 0;
// 初始版本号为0
mVersion = 0;
// 初始化便签主表的变更记录容器
mDiffNoteValues = new ContentValues();
// 初始化明细数据列表
mDataList = new ArrayList<SqlData>();
}
/**
* 便
* @param context
* @param c 便
* 便
* <p>
* 便
*
* @param context
* @param c note
*/
public SqlNote(Context context, Cursor c) {
mContext = context;
// 获取数据库操作工具
mContentResolver = context.getContentResolver();
// 标记为已有便签(不是新创建的)
mIsCreate = false;
// 从查询结果里加载便签基本信息
loadFromCursor(c);
// 初始化明细数据列表
mDataList = new ArrayList<SqlData>();
// 如果是普通便签,加载对应的明细内容(比如正文)
if (mType == Notes.TYPE_NOTE)
loadDataContent();
// 初始化便签主表的变更记录容器
mDiffNoteValues = new ContentValues();
// 加载基础数据
loadFromCursor(c);
// 如果是普通便签,加载详细数据内容
if (mType == Notes.TYPE_NOTE) {
loadDataContent();
}
}
/**
* 便ID便
* @param context
* @param id 便ID
* 便ID便
* <p>
* 便ID便
* ID便
*
* @param context
* @param id 便ID
*/
public SqlNote(Context context, long id) {
mContext = context;
// 获取数据库操作工具
mContentResolver = context.getContentResolver();
// 标记为已有便签(不是新创建的)
mIsCreate = false;
// 通过便签ID查询数据库再加载便签基本信息
loadFromCursor(id);
// 初始化明细数据列表
mDataList = new ArrayList<SqlData>();
// 如果是普通便签,加载对应的明细内容(比如正文)
if (mType == Notes.TYPE_NOTE)
loadDataContent();
// 初始化便签主表的变更记录容器
mDiffNoteValues = new ContentValues();
// 通过ID加载便签
loadFromCursor(id);
// 如果是普通便签,加载详细数据内容
if (mType == Notes.TYPE_NOTE) {
loadDataContent();
}
}
/**
* 便ID便
* @param id 便ID
* 便ID
* <p>
* DB便IDloadFromCursor(Cursor)
*
*
* @param id 便ID
*/
private void loadFromCursor(long id) {
Cursor c = null; // 数据库查询结果容器
Cursor c = null;
try {
// 根据便签ID查询便签主表,获取该便签的基本信息
// DB操作根据ID查询便签
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)",
new String[] { String.valueOf(id) }, null);
if (c != null) {
// 移动到查询结果的第一条因为ID是唯一的只有一条结果
c.moveToNext();
// 从查询结果里加载数据到当前对象
loadFromCursor(c);
if (c.moveToNext()) {
loadFromCursor(c);
} else {
Log.w(TAG, "loadFromCursor: cursor = null");
}
} else {
// 查询结果为空,打印警告日志
Log.w(TAG, "loadFromCursor: cursor = null");
}
} finally {
// 不管查询成功与否,最后都关闭查询结果,避免占用资源
if (c != null)
if (c != null) {
c.close();
}
}
}
/**
* 便
* 便
* @param c
* 便
* <p>
* DBCursornote
*
*
* @param c PROJECTION_NOTE
*/
private void loadFromCursor(Cursor c) {
mId = c.getLong(ID_COLUMN); // 取便签ID
mAlertDate = c.getLong(ALERTED_DATE_COLUMN); // 取提醒时间
mBgColorId = c.getInt(BG_COLOR_ID_COLUMN); // 取背景颜色ID
mCreatedDate = c.getLong(CREATED_DATE_COLUMN); // 取创建时间
mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN); // 取是否有附件
mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN); // 取最后修改时间
mParentId = c.getLong(PARENT_ID_COLUMN); // 取所属文件夹ID
mSnippet = c.getString(SNIPPET_COLUMN); // 取便签摘要
mType = c.getInt(TYPE_COLUMN); // 取便签类型
mWidgetId = c.getInt(WIDGET_ID_COLUMN); // 取桌面小组件ID
mWidgetType = c.getInt(WIDGET_TYPE_COLUMN); // 取桌面小组件类型
mVersion = c.getLong(VERSION_COLUMN); // 取便签版本号
mId = c.getLong(ID_COLUMN);
mAlertDate = c.getLong(ALERTED_DATE_COLUMN);
mBgColorId = c.getInt(BG_COLOR_ID_COLUMN);
mCreatedDate = c.getLong(CREATED_DATE_COLUMN);
mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN);
mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN);
mParentId = c.getLong(PARENT_ID_COLUMN);
mSnippet = c.getString(SNIPPET_COLUMN);
mType = c.getInt(TYPE_COLUMN);
mWidgetId = c.getInt(WIDGET_ID_COLUMN);
mWidgetType = c.getInt(WIDGET_TYPE_COLUMN);
mVersion = c.getLong(VERSION_COLUMN);
}
/**
* 便便
* 便IDmDataList
* 便
* <p>
* DBdata便
* 便notedata
*/
private void loadDataContent() {
Cursor c = null; // 数据库查询结果容器
// 先清空现有的明细数据列表
Cursor c = null;
mDataList.clear();
try {
// 根据便签ID查询数据明细表获取该便签的明细内容
// DB操作查询关联的详细数据
c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA,
"(note_id=?)", new String[] { String.valueOf(mId) }, null);
if (c != null) {
// 如果没有查询到明细数据,打印警告日志并返回
if (c.getCount() == 0) {
Log.w(TAG, "it seems that the note has not data");
return;
}
// 遍历所有明细数据逐个加载到mDataList
while (c.moveToNext()) {
SqlData data = new SqlData(mContext, c); // 从查询结果创建明细数据对象
mDataList.add(data); // 添加到明细数据列表
SqlData data = new SqlData(mContext, c);
mDataList.add(data);
}
} else {
// 查询结果为空,打印警告日志
Log.w(TAG, "loadDataContent: cursor = null");
}
} finally {
// 不管查询成功与否,最后都关闭查询结果,避免占用资源
if (c != null)
if (c != null) {
c.close();
}
}
}
/**
* JSON便
* JSON便
*
* @param js 便JSON
* @return true=false=JSON
* JSON便
* <p>
* JSON
* 便便//
* GTask
*
* @param js 便JSON
* @return truefalse
*/
public boolean setContent(JSONObject js) {
try {
// 从JSON里取出便签基本信息的数据包
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
// 如果是系统文件夹,不允许修改,打印警告日志
// Case 1: 系统文件夹 - 禁止修改
if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
Log.w(TAG, "cannot set system folder");
}
// 如果是普通文件夹,只能更新摘要和类型
// Case 2: 文件夹 - 只能更新摘要和类型
else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
// 从JSON里取文件夹摘要没有的话为空字符串
String snippet = note.has(NoteColumns.SNIPPET) ? note
.getString(NoteColumns.SNIPPET) : "";
// 如果是新文件夹,或者摘要有变化,记录变更
String snippet = note.has(NoteColumns.SNIPPET) ?
note.getString(NoteColumns.SNIPPET) : "";
if (mIsCreate || !mSnippet.equals(snippet)) {
mDiffNoteValues.put(NoteColumns.SNIPPET, snippet);
}
// 更新当前文件夹摘要
mSnippet = snippet;
// 从JSON里取文件夹类型没有的话默认是普通便签
int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE)
: Notes.TYPE_NOTE;
// 如果是新文件夹,或者类型有变化,记录变更
int type = note.has(NoteColumns.TYPE) ?
note.getInt(NoteColumns.TYPE) : Notes.TYPE_NOTE;
if (mIsCreate || mType != type) {
mDiffNoteValues.put(NoteColumns.TYPE, type);
}
// 更新当前文件夹类型
mType = type;
}
// 如果是普通便签,更新所有基本信息和明细内容
// Case 3: 普通便签 - 更新所有字段和详细数据
else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) {
// 取出明细数据的JSON数组比如正文内容
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
// 处理便签ID
@ -344,149 +409,149 @@ public class SqlNote {
}
mId = id;
// 处理提醒时间
long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note
.getLong(NoteColumns.ALERTED_DATE) : 0;
// 处理提醒日期
long alertDate = note.has(NoteColumns.ALERTED_DATE) ?
note.getLong(NoteColumns.ALERTED_DATE) : 0;
if (mIsCreate || mAlertDate != alertDate) {
mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate);
}
mAlertDate = alertDate;
// 处理背景颜色ID
int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note
.getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext);
int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ?
note.getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext);
if (mIsCreate || mBgColorId != bgColorId) {
mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId);
}
mBgColorId = bgColorId;
// 处理创建时间
long createDate = note.has(NoteColumns.CREATED_DATE) ? note
.getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis();
// 处理创建日期
long createDate = note.has(NoteColumns.CREATED_DATE) ?
note.getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis();
if (mIsCreate || mCreatedDate != createDate) {
mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate);
}
mCreatedDate = createDate;
// 处理是否有附件
int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note
.getInt(NoteColumns.HAS_ATTACHMENT) : 0;
// 处理附件标记
int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ?
note.getInt(NoteColumns.HAS_ATTACHMENT) : 0;
if (mIsCreate || mHasAttachment != hasAttachment) {
mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment);
}
mHasAttachment = hasAttachment;
// 处理最后修改时间
long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note
.getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis();
// 处理修改日期
long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ?
note.getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis();
if (mIsCreate || mModifiedDate != modifiedDate) {
mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate);
}
mModifiedDate = modifiedDate;
// 处理所属文件夹ID
long parentId = note.has(NoteColumns.PARENT_ID) ? note
.getLong(NoteColumns.PARENT_ID) : 0;
// 处理文件夹ID
long parentId = note.has(NoteColumns.PARENT_ID) ?
note.getLong(NoteColumns.PARENT_ID) : 0;
if (mIsCreate || mParentId != parentId) {
mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId);
}
mParentId = parentId;
// 处理便签摘要
String snippet = note.has(NoteColumns.SNIPPET) ? note
.getString(NoteColumns.SNIPPET) : "";
String snippet = note.has(NoteColumns.SNIPPET) ?
note.getString(NoteColumns.SNIPPET) : "";
if (mIsCreate || !mSnippet.equals(snippet)) {
mDiffNoteValues.put(NoteColumns.SNIPPET, snippet);
}
mSnippet = snippet;
// 处理便签类型
int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE)
: Notes.TYPE_NOTE;
int type = note.has(NoteColumns.TYPE) ?
note.getInt(NoteColumns.TYPE) : Notes.TYPE_NOTE;
if (mIsCreate || mType != type) {
mDiffNoteValues.put(NoteColumns.TYPE, type);
}
mType = type;
// 处理桌面小组件ID
int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID)
: AppWidgetManager.INVALID_APPWIDGET_ID;
// 处理Widget ID
int widgetId = note.has(NoteColumns.WIDGET_ID) ?
note.getInt(NoteColumns.WIDGET_ID) : AppWidgetManager.INVALID_APPWIDGET_ID;
if (mIsCreate || mWidgetId != widgetId) {
mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId);
}
mWidgetId = widgetId;
// 处理桌面小组件类型
int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note
.getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE;
// 处理Widget类型
int widgetType = note.has(NoteColumns.WIDGET_TYPE) ?
note.getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE;
if (mIsCreate || mWidgetType != widgetType) {
mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType);
}
mWidgetType = widgetType;
// 处理原始所属文件夹ID
long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note
.getLong(NoteColumns.ORIGIN_PARENT_ID) : 0;
// 处理原始文件夹ID
long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ?
note.getLong(NoteColumns.ORIGIN_PARENT_ID) : 0;
if (mIsCreate || mOriginParent != originParent) {
mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent);
}
mOriginParent = originParent;
// 处理明细数据(比如正文内容)
// 处理详细数据
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
SqlData sqlData = null;
// 如果明细数据有ID先在现有列表里找对应的明细对象
// 查找已存在的详细数据
if (data.has(DataColumns.ID)) {
long dataId = data.getLong(DataColumns.ID);
for (SqlData temp : mDataList) {
if (dataId == temp.getId()) {
sqlData = temp;
break;
}
}
}
// 如果没找到对应的明细对象,创建新的并加入列表
// 如果未找到,创建新的详细数据
if (sqlData == null) {
sqlData = new SqlData(mContext);
mDataList.add(sqlData);
}
// 给明细对象设置内容,并记录变更
// 设置详细数据内容
sqlData.setContent(data);
}
}
} catch (JSONException e) {
// JSON解析出错打印错误日志并返回false
Log.e(TAG, e.toString());
e.printStackTrace();
return false;
}
// 设置成功返回true
return true;
}
/**
* 便JSON
* 便JSON便
* @return 便JSON便/null
* 便JSON
* <p>
* JSON
* GTask
*
* @return 便JSONnull
*/
public JSONObject getContent() {
try {
// 创建空的JSON数据包
JSONObject js = new JSONObject();
// 如果是新便签还没存到数据库打印错误日志并返回null
// 安全检查新建便签不能转换为JSON
if (mIsCreate) {
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
// 创建存储便签基本信息的JSON对象
JSONObject note = new JSONObject();
// 如果是普通便签,打包所有基本信息和明细内容
// Case 1: 普通便签 - 包含所有字段和详细数据
if (mType == Notes.TYPE_NOTE) {
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.ALERTED_DATE, mAlertDate);
@ -500,44 +565,41 @@ public class SqlNote {
note.put(NoteColumns.WIDGET_ID, mWidgetId);
note.put(NoteColumns.WIDGET_TYPE, mWidgetType);
note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent);
// 把便签基本信息存入总JSON
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
// 创建存储明细数据的JSON数组
JSONArray dataArray = new JSONArray();
for (SqlData sqlData : mDataList) {
// 把每个明细数据打包成JSON
JSONObject data = sqlData.getContent();
if (data != null) {
dataArray.put(data);
}
}
// 把明细数据数组存入总JSON
js.put(GTaskStringUtils.META_HEAD_DATA, dataArray);
}
// 如果是文件夹/系统文件夹,只打包ID、类型和摘要
// Case 2: 文件夹或系统文件夹 - 仅包含ID、类型和摘要
else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) {
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.TYPE, mType);
note.put(NoteColumns.SNIPPET, mSnippet);
// 把文件夹信息存入总JSON
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
}
// 返回打包好的JSON
return js;
} catch (JSONException e) {
// JSON打包出错打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
return null;
}
// 打包失败返回null
return null;
}
/**
* 便ID
* @param id ID
* 便ID
* <p>
* 便
*
* @param id ID
*/
public void setParentId(long id) {
mParentId = id;
@ -545,30 +607,39 @@ public class SqlNote {
}
/**
* 便GTask ID
* @param gid GTaskID
* Google TasksID
* <p>
* 便Google Tasks
*
* @param gid Google TasksID
*/
public void setGtaskId(String gid) {
mDiffNoteValues.put(NoteColumns.GTASK_ID, gid);
}
/**
* 便
* @param syncId
* ID
* <p>
* 便
*
* @param syncId ID
*/
public void setSyncId(long syncId) {
mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId);
}
/**
*
*
* <p>
* 便"已同步"
*/
public void resetLocalModified() {
mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0);
}
/**
* 便ID
* 便ID
*
* @return 便ID
*/
public long getId() {
@ -576,15 +647,17 @@ public class SqlNote {
}
/**
* 便ID
* @return ID
* ID
*
* @return ID
*/
public long getParentId() {
return mParentId;
}
/**
* 便
* 便
*
* @return 便
*/
public String getSnippet() {
@ -592,82 +665,91 @@ public class SqlNote {
}
/**
* 便
* @return true=便false=/
* 便
*
* @return true便false
*/
public boolean isNoteType() {
return mType == Notes.TYPE_NOTE;
}
/**
* 便
* 便便
* 便
* @param validateVersion 便
* 便
* <p>
* DB便
*
*
*
* @param validateVersion
*/
public void commit(boolean validateVersion) {
// 如果是新便签(要插入数据库)
if (mIsCreate) {
// 如果便签ID是无效的就把ID从变更记录里移除数据库会自动生成唯一ID
// Case A: 新建便签,执行插入操作
// 移除无效ID让数据库自动生成
if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) {
mDiffNoteValues.remove(NoteColumns.ID);
}
// 把便签基本信息插入到便签主表返回新便签的URI
// DB操作插入新便签
Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues);
try {
// 从返回的URI里提取数据库分配的唯一便签ID
// 从返回的URI中提取新生成的ID
mId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
// 提取ID失败打印错误日志并抛出异常
Log.e(TAG, "Get note id error :" + e.toString());
throw new ActionFailureException("create note failed");
}
// 如果提取的ID为0说明创建失败抛出异常
// ID为0表示创建失败
if (mId == 0) {
throw new IllegalStateException("Create thread id failed");
}
// 如果是普通便签,把明细数据也插入到数据明细表
// 如果是普通便签,提交详细数据
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, false, -1);
}
}
}
// 如果是已有便签(要更新数据库)
else {
// 验证便签ID是否有效除了根文件夹和通话记录文件夹其他ID不能<=0
} else {
// Case B: 更新已有便签
// ID验证除了特殊系统文件夹ID必须大于0
if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) {
Log.e(TAG, "No such note");
throw new IllegalStateException("Try to update note with invalid id");
}
// 如果有变更内容,才执行更新操作
// 如果有字段变化才执行更新
if (mDiffNoteValues.size() > 0) {
// 版本号递增(每次修改都升级版本)
mVersion ++;
// 递增版本号
mVersion++;
int result = 0;
// 如果不需要验证版本,直接更新便签主表
if (!validateVersion) {
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "("
+ NoteColumns.ID + "=?)", new String[] { String.valueOf(mId) });
}
// 如果需要验证版本,只有版本匹配时才更新(避免同步冲突)
else {
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "("
+ NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)",
// 无需版本验证的直接更新
result = mContentResolver.update(
Notes.CONTENT_NOTE_URI,
mDiffNoteValues,
"(" + NoteColumns.ID + "=?)",
new String[] { String.valueOf(mId) });
} else {
// 需要版本验证的更新(乐观锁控制)
result = mContentResolver.update(
Notes.CONTENT_NOTE_URI,
mDiffNoteValues,
"(" + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)",
new String[] { String.valueOf(mId), String.valueOf(mVersion) });
}
// 如果更新结果为0说明没有更新成功可能版本不匹配或数据已被修改
// 更新结果为0表示没有记录被更新
if (result == 0) {
Log.w(TAG, "there is no update. maybe user updates note when syncing");
}
}
// 如果是普通便签,更新对应的明细数据
// 如果是普通便签,提交详细数据更新
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, validateVersion, mVersion);
@ -675,14 +757,14 @@ public class SqlNote {
}
}
// 刷新便签信息(重新从数据库加载最新数据)
// 重新加载数据,确保内存状态与数据库一致
loadFromCursor(mId);
if (mType == Notes.TYPE_NOTE)
if (mType == Notes.TYPE_NOTE) {
loadDataContent();
}
// 清空便签主表的变更记录(本次提交完成,下次变更重新记录)
// 重置状态
mDiffNoteValues.clear();
// 标记为非新便签(就算是刚插入的,现在也已经存到数据库了)
mIsCreate = false;
}
}

@ -31,175 +31,216 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* GTask便
* GTask便
* Node//
*
* Google Tasks (Data Layer - Google Tasks Task Management)
* <p>
* Google Tasks便
* 便
* NodeGoogle Tasks API
* 便JSON
*
*/
public class Task extends Node {
// 日志标签:打印这个类的日志时,用来标识日志来自这个类
/**
* 使TAG
*/
private static final String TAG = Task.class.getSimpleName();
// 任务是否完成true=已完成false=未完成)
/**
*
* <p>
* Google Tasks
* 便GTask
*/
private boolean mCompleted;
// 任务备注(额外的说明信息)
/**
* JSON
* <p>
*
* JSON
*/
private String mNotes;
// 本地便签的元信息存储便签的详细配置是个JSON数据包
/**
* 便
* <p>
* 便
*
*/
private JSONObject mMetaInfo;
// 上一个兄弟任务(当前任务在列表里的前一个任务,用来确定任务顺序)
/**
*
* <p>
*
* ID
*/
private Task mPriorSibling;
// 所属任务列表(对应本地的文件夹,当前任务在哪个列表/文件夹里)
/**
*
* <p>
* TaskList
*
*/
private TaskList mParent;
/**
*
*
*
* <p>
*
*/
public Task() {
super(); // 调用父类Node的构造方法初始化同步相关的默认属性
mCompleted = false; // 默认任务未完成
mNotes = null; // 默认没有任务备注
mPriorSibling = null; // 默认没有上一个兄弟任务
mParent = null; // 默认没有所属任务列表
mMetaInfo = null; // 默认没有本地便签元信息
super();
mCompleted = false;
mNotes = null;
mPriorSibling = null;
mParent = null;
mMetaInfo = null;
}
/**
*
* 便JSON
* @param actionId
* @return JSON
* JSON
* <p>
* Google Tasks API
*
*
* @param actionId Google Tasks API
* @return Google Tasks APIJSON
* @throws ActionFailureException JSON
*/
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject(); // 创建空的JSON数据包
JSONObject js = new JSONObject();
try {
// 1. 设置动作类型:告诉云端这次是“创建任务
// 1. 设置动作类型:创建任务
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// 2. 设置动作ID给这次创建操作一个唯一编号,方便云端识别
// 2. 设置动作ID确保请求幂等性
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// 3. 设置任务位置:告诉云端这个新任务在所属列表里的排序位置
// 3. 设置任务索引位置:在父列表中的顺序
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this));
// 4. 设置任务核心信息:打包任务的名称、创建者等信息
// 4. 构建任务实体数据
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 任务名称(对应便签摘要/内容)
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); // 创建者ID这里默认传null
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); // GTask API要求此字段
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_TASK); // 实体类型:告诉云端这是一个“任务”
if (getNotes() != null) { // 如果有任务备注,就把备注也传过去
GTaskStringUtils.GTASK_JSON_TYPE_TASK);
// 如果有备注,添加到实体数据
if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 把任务核心信息存入总JSON
// 5. 设置所属列表ID告诉云端这个任务属于哪个列表对应本地文件夹
js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid());
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
// 6. 设置所属列表类型:告诉云端父级是“任务列表”(文件夹类型)
// 5. 设置父列表信息
js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid());
js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
// 7. 设置列表ID和所属列表ID一致确认任务归属
js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid());
// 8. 设置上一个兄弟任务ID如果有前一个任务就传过去保证任务排序
// 6. 设置前一个兄弟任务ID用于确定任务顺序
if (mPriorSibling != null) {
js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid());
}
} catch (JSONException e) {
// JSON打包出错打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-create jsonobject");
}
return js; // 返回打包好的创建请求参数
return js;
}
/**
*
* 便JSON
* @param actionId
* @return JSON
* JSON
* <p>
* Google Tasks API
*
*
* @param actionId Google Tasks API
* @return Google Tasks APIJSON
* @throws ActionFailureException JSON
*/
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject(); // 创建空的JSON数据包
JSONObject js = new JSONObject();
try {
// 1. 设置动作类型:告诉云端这次是“更新任务
// 1. 设置动作类型:更新任务
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// 2. 设置动作ID:给这次更新操作一个唯一编号,方便云端识别
// 2. 设置动作ID
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// 3. 设置任务ID告诉云端要更新哪个任务用云端给的唯一ID
// 3. 设置任务ID指定要更新的任务
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// 4. 设置要更新的任务信息:打包修改后的名称、备注、删除状态
// 4. 构建更新实体数据
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 更新后的任务名称
if (getNotes() != null) { // 如果有备注,就更新备注
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); // 任务是否被删除(标记云端是否要删除该任务)
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 把更新信息存入总JSON
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
// JSON打包出错打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-update jsonobject");
}
return js; // 返回打包好的更新请求参数
return js;
}
/**
*
*
* @param js JSON
* JSON
* <p>
* Google Tasks APIJSON
*
*
* @param js Google Tasks APIJSON
* @throws ActionFailureException JSON
*/
@Override
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) { // 如果云端返回的数据不为空
if (js != null) {
try {
// 1. 提取任务ID云端给的唯一编号设置到当前对象
// 提取并设置所有可用字段
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
// 2. 提取任务最后修改时间,设置到当前对象
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// 3. 提取任务名称,设置到当前对象
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
// 4. 提取任务备注,设置到当前对象
if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) {
setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES));
}
// 5. 提取任务删除状态,设置到当前对象
if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) {
setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED));
}
// 6. 提取任务完成状态,设置到当前对象
if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) {
setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED));
}
} catch (JSONException e) {
// JSON解析出错打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get task content from jsonobject");
@ -208,95 +249,101 @@ public class Task extends Node {
}
/**
* 便JSON
* 便JSON便
* @param js 便JSON
* JSON
* <p>
* 便JSON便
* 线
*
* @param js 便JSON
*/
@Override
public void setContentByLocalJSON(JSONObject js) {
// 如果JSON数据为空或者缺少便签信息/明细内容,打印警告日志
// 数据验证确保JSON包含必要的结构
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)
|| !js.has(GTaskStringUtils.META_HEAD_DATA)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
Log.w(TAG, "setContentByLocalJSON: nothing is available");
}
try {
// 1. 提取本地便签的基本信息
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
// 2. 提取本地便签的明细内容(比如正文)
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
// 3. 如果不是普通便签类型,打印错误日志并返回
// 类型验证:只处理普通便签类型
if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) {
Log.e(TAG, "invalid type");
return;
}
// 4. 遍历明细内容,提取普通便签的正文作为任务名称
// 遍历数据数组,查找便签内容
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
// 判断是否是普通便签的明细内容
// 只处理普通便签类型的数据
if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) {
setName(data.getString(DataColumns.CONTENT)); // 把便签正文设为任务名称
break; // 找到后就停止遍历
// 将便签内容设置为任务名称
setName(data.getString(DataColumns.CONTENT));
break;
}
}
} catch (JSONException e) {
// JSON解析出错打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
/**
* 便JSON
* 便JSON便
* @return 便JSONnull
* JSON
* <p>
* 便JSON
*
*
* @return 便JSONnull
*/
@Override
public JSONObject getLocalJSONFromContent() {
String name = getName(); // 获取任务名称(对应便签内容)
String name = getName();
try {
// 情况1没有本地元信息云端新建的任务同步到本地)
// Case 1: 无本地元信息(云端新建任务同步到本地)
if (mMetaInfo == null) {
// 如果任务名称为空打印警告日志并返回null
// 名称验证:空任务不予同步
if (name == null) {
Log.w(TAG, "the note seems to be an empty one");
return null;
}
// 创建本地便签的JSON数据包
// 构建新的本地便签JSON结构
JSONObject js = new JSONObject();
JSONObject note = new JSONObject(); // 便签基本信息
JSONArray dataArray = new JSONArray(); // 便签明细内容
JSONObject data = new JSONObject(); // 明细内容(正文)
data.put(DataColumns.CONTENT, name); // 把任务名称设为便签正文
dataArray.put(data); // 把正文加入明细数组
js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); // 存入明细内容
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 标记为普通便签类型
js.put(GTaskStringUtils.META_HEAD_NOTE, note); // 存入便签基本信息
return js; // 返回打包好的本地便签JSON
}
// 情况2有本地元信息已同步过的任务更新本地内容
else {
// 提取已有的本地便签元信息
JSONObject note = new JSONObject();
JSONArray dataArray = new JSONArray();
JSONObject data = new JSONObject();
data.put(DataColumns.CONTENT, name);
dataArray.put(data);
js.put(GTaskStringUtils.META_HEAD_DATA, dataArray);
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
return js;
} else {
// Case 2: 有本地元信息(更新现有便签)
JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
// 遍历明细内容,更新便签正文(用最新的任务名称)
// 更新便签内容
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
// 找到普通便签的明细内容
if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) {
data.put(DataColumns.CONTENT, getName()); // 更新便签正文
break; // 找到后停止遍历
data.put(DataColumns.CONTENT, getName());
break;
}
}
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 确保是普通便签类型
return mMetaInfo; // 返回更新后的本地元信息
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
return mMetaInfo;
}
} catch (JSONException e) {
// JSON打包出错打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
return null;
@ -304,18 +351,19 @@ public class Task extends Node {
}
/**
* 便
* MetaData便JSON
* 便
* <p>
* MetaData便
* 便
*
* @param metaData 便
*/
public void setMetaInfo(MetaData metaData) {
// 如果元数据不为空,且包含便签信息
if (metaData != null && metaData.getNotes() != null) {
try {
// 把元数据里的便签信息转换成JSON对象存入mMetaInfo
// 将MetaData中的notes字段解析为JSON对象
mMetaInfo = new JSONObject(metaData.getNotes());
} catch (JSONException e) {
// 转换失败,打印警告日志,清空元信息
Log.w(TAG, e.toString());
mMetaInfo = null;
}
@ -323,64 +371,70 @@ public class Task extends Node {
}
/**
*
* 便
* @param c 便
* @return NodeSYNC_ACTION_XXX
*
* <p>
*
*
* 8
*
* @param c
* @return SYNC_ACTION_XXX
*/
@Override
public int getSyncAction(Cursor c) {
try {
JSONObject noteInfo = null;
// 提取本地便签的元信息(如果存在)
// 提取本地元信息中的便签信息
if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) {
noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
}
// 1. 如果本地元信息为空,说明本地便签已删除,返回“更新云端”(让云端也删除)
// Case 1: 本地元信息为空(本地便签已删除)
if (noteInfo == null) {
Log.w(TAG, "it seems that note meta has been deleted");
return SYNC_ACTION_UPDATE_REMOTE;
}
// 2. 如果元信息里没有便签ID说明本地便签无效返回“更新本地”用云端数据覆盖
// Case 2: 元信息中无便签ID云端任务关联的本地便签已删除
if (!noteInfo.has(NoteColumns.ID)) {
Log.w(TAG, "remote note id seems to be deleted");
return SYNC_ACTION_UPDATE_LOCAL;
}
// 3. 验证便签ID是否匹配本地便签ID和元信息里的ID不一致返回“更新本地”
// Case 3: 便签ID不匹配数据不一致
if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) {
Log.w(TAG, "note id doesn't match");
return SYNC_ACTION_UPDATE_LOCAL;
}
// 4. 判断本地是否有修改
// Case 4: 检查本地修改状态
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
// 本地没有修改
// 本地修改
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// 本地和云端都没有修改,返回“无需操作”
// 两端数据一致,无需同步
return SYNC_ACTION_NONE;
} else {
// 云端有修改,返回“更新本地”(用云端数据覆盖本地)
// 云端有修改,需要更新本地
return SYNC_ACTION_UPDATE_LOCAL;
}
} else {
// 本地有修改
// 验证云端任务ID是否匹配不匹配返回“同步异常”
// 验证GTask ID匹配
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "gtask id doesn't match");
return SYNC_ACTION_ERROR;
}
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// 只有本地修改,返回“更新云端”(把本地修改同步到云端)
// 只有本地修改,需要更新云端
return SYNC_ACTION_UPDATE_REMOTE;
} else {
// 本地和云端都有修改,返回“同步冲突”(需要用户手动选择)
// 两端都有修改,发生冲突
return SYNC_ACTION_UPDATE_CONFLICT;
}
}
} catch (Exception e) {
// 出现异常,打印错误日志,返回“同步异常”
Log.e(TAG, e.toString());
e.printStackTrace();
}
@ -389,78 +443,90 @@ public class Task extends Node {
}
/**
*
*
* @return true=//false=
*
* <p>
*
*
*
* @return true
*/
public boolean isWorthSaving() {
return mMetaInfo != null // 有本地元信息
|| (getName() != null && getName().trim().length() > 0) // 任务名称不为空
|| (getNotes() != null && getNotes().trim().length() > 0); // 任务备注不为空
return mMetaInfo != null
|| (getName() != null && getName().trim().length() > 0)
|| (getNotes() != null && getNotes().trim().length() > 0);
}
// ==================== 属性访问器 ====================
/**
*
* @param completed true=false=
*
*
* @param completed true
*/
public void setCompleted(boolean completed) {
this.mCompleted = completed;
}
/**
*
* @param notes
*
*
* @param notes
*/
public void setNotes(String notes) {
this.mNotes = notes;
}
/**
*
* @param priorSibling
*
*
* @param priorSibling
*/
public void setPriorSibling(Task priorSibling) {
this.mPriorSibling = priorSibling;
}
/**
*
* @param parent
*
*
* @param parent
*/
public void setParent(TaskList parent) {
this.mParent = parent;
}
/**
*
* @return true=false=
*
*
* @return true
*/
public boolean getCompleted() {
return this.mCompleted;
}
/**
*
* @return
*
*
* @return
*/
public String getNotes() {
return this.mNotes;
}
/**
*
* @return null
*
*
* @return null
*/
public Task getPriorSibling() {
return this.mPriorSibling;
}
/**
*
* @return null
*
*
* @return null
*/
public TaskList getParent() {
return this.mParent;
}
}

@ -29,131 +29,158 @@ import org.json.JSONObject;
import java.util.ArrayList;
/**
* GTask
* GTask便
* Node//
* 便
* Google Tasks (Data Layer - Google Tasks List Management)
* <p>
* Google Tasks便
*
* NodeGoogle Tasks API
* JSON
*
*/
public class TaskList extends Node {
// 日志标签:打印这个类的日志时,用来标识日志来自这个类
/**
* 使TAG
*/
private static final String TAG = TaskList.class.getSimpleName();
// 列表排序索引:用来确定这个任务列表在云端的显示顺序
/**
*
* <p>
* Google Tasks
*
*/
private int mIndex;
// 子任务列表:存储当前列表下的所有任务(对应本地文件夹里的普通便签)
/**
*
* <p>
*
*
*/
private ArrayList<Task> mChildren;
/**
*
*
*
* <p>
*
*/
public TaskList() {
super(); // 调用父类Node的构造方法初始化同步相关的默认属性
mChildren = new ArrayList<Task>(); // 初始化子任务列表(空列表)
mIndex = 1; // 默认排序索引为1控制列表在云端的显示顺序
super();
mChildren = new ArrayList<Task>();
mIndex = 1;
}
/**
*
* JSON
* @param actionId
* @return JSON
* JSON
* <p>
* Google Tasks API
*
*
* @param actionId Google Tasks API
* @return Google Tasks APIJSON
* @throws ActionFailureException JSON
*/
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject(); // 创建空的JSON数据包
JSONObject js = new JSONObject();
try {
// 1. 设置动作类型:告诉云端这次是“创建任务列表
// 1. 设置动作类型:创建任务列表
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// 2. 设置动作ID给这次创建操作一个唯一编号,方便云端识别
// 2. 设置动作ID确保请求幂等性
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// 3. 设置列表排序索引:告诉云端这个新列表在云端的显示顺序
// 3. 设置列表索引:在云端列表中的显示顺序
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex);
// 4. 设置列表核心信息:打包列表的名称、创建者等信息
// 4. 构建列表实体数据
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 列表名称(对应本地文件夹名称)
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); // 创建者ID这里默认传null
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP); // 实体类型:告诉云端这是一个“任务列表”(文件夹)
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 把列表核心信息存入总JSON
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
// JSON打包出错打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate tasklist-create jsonobject");
}
return js; // 返回打包好的创建请求参数
return js;
}
/**
*
* JSON
* @param actionId
* @return JSON
* JSON
* <p>
* Google Tasks API
*
*
* @param actionId Google Tasks API
* @return Google Tasks APIJSON
* @throws ActionFailureException JSON
*/
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject(); // 创建空的JSON数据包
JSONObject js = new JSONObject();
try {
// 1. 设置动作类型:告诉云端这次是“更新任务列表
// 1. 设置动作类型:更新任务列表
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// 2. 设置动作ID:给这次更新操作一个唯一编号,方便云端识别
// 2. 设置动作ID
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// 3. 设置列表ID告诉云端要更新哪个列表用云端给的唯一ID
// 3. 设置列表ID指定要更新的列表
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// 4. 设置要更新的列表信息:打包修改后的名称、删除状态
// 4. 构建更新实体数据
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 更新后的列表名称
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); // 列表是否被删除(标记云端是否要删除该列表)
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 把更新信息存入总JSON
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
// JSON打包出错打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate tasklist-update jsonobject");
}
return js; // 返回打包好的更新请求参数
return js;
}
/**
*
*
* @param js JSON
* JSON
* <p>
* Google Tasks APIJSON
*
*
* @param js Google Tasks APIJSON
* @throws ActionFailureException JSON
*/
@Override
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) { // 如果云端返回的数据不为空
if (js != null) {
try {
// 1. 提取列表ID云端给的唯一编号设置到当前对象
// 提取并设置所有可用字段
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
// 2. 提取列表最后修改时间,设置到当前对象
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// 3. 提取列表名称,设置到当前对象
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
} catch (JSONException e) {
// JSON解析出错打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get tasklist content from jsonobject");
@ -162,86 +189,85 @@ public class TaskList extends Node {
}
/**
* JSON
* JSON
* @param js JSON
* JSON
* <p>
* 便JSON
*
*
* @param js 便JSON
*/
@Override
public void setContentByLocalJSON(JSONObject js) {
// 如果JSON数据为空或者缺少文件夹信息打印警告日志
// 数据验证确保JSON包含必要的结构
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
Log.w(TAG, "setContentByLocalJSON: nothing is available");
}
try {
// 1. 提取本地文件夹的基本信息
JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
// 2. 如果是普通文件夹(用户自己创建的文件夹)
// Case 1: 普通文件夹 - 添加MIUI前缀
if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
// 提取文件夹名称加上MIUI专属前缀区分云端其他列表
String name = folder.getString(NoteColumns.SNIPPET);
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name);
}
// 3. 如果是系统文件夹(自带的根目录/通话记录文件夹)
// Case 2: 系统文件夹 - 特殊处理根目录和通话记录文件夹
else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
// 根目录文件夹:设置默认名称(加前缀)
if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER)
if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER) {
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT);
// 通话记录文件夹:设置通话便签文件夹名称(加前缀)
else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER)
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_CALL_NOTE);
// 其他系统文件夹:打印错误日志
else
} else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER) {
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE);
} else {
Log.e(TAG, "invalid system folder");
}
}
// 4. 无效类型:打印错误日志
// Case 3: 无效类型
else {
Log.e(TAG, "error type");
}
} catch (JSONException e) {
// JSON解析出错打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
/**
* JSON
* JSON便
* @return JSONnull
* JSON
* <p>
* 便JSON
* MIUI
*
* @return 便JSONnull
*/
@Override
public JSONObject getLocalJSONFromContent() {
try {
// 创建本地文件夹的JSON数据包
JSONObject js = new JSONObject();
JSONObject folder = new JSONObject(); // 文件夹基本信息
JSONObject folder = new JSONObject();
// 1. 提取云端列表名称去掉MIUI专属前缀还原成本地文件夹名称
// 去除MIUI前缀恢复原始文件夹名称
String folderName = getName();
if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX))
if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)) {
folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(),
folderName.length());
}
// 2. 设置文件夹名称(摘要)
// 设置文件夹名称(摘要)
folder.put(NoteColumns.SNIPPET, folderName);
// 3. 判断文件夹类型(系统文件夹/普通文件夹)
// 判断文件夹类型
if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT)
|| folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE)) {
// 根目录/通话记录文件夹:标记为系统文件夹
// 系统文件夹:根目录或通话记录文件夹
folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
} else {
// 其他文件夹:标记为普通文件夹
// 普通文件夹:用户创建的文件夹
folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
}
// 4. 把文件夹信息存入总JSON
js.put(GTaskStringUtils.META_HEAD_NOTE, folder);
return js; // 返回打包好的本地文件夹JSON
return js;
} catch (JSONException e) {
// JSON打包出错打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
return null;
@ -249,40 +275,44 @@ public class TaskList extends Node {
}
/**
*
*
* @param c
* @return NodeSYNC_ACTION_XXX
*
* <p>
*
*
*
*
* @param c
* @return SYNC_ACTION_XXX
*/
@Override
public int getSyncAction(Cursor c) {
try {
// 1. 判断本地文件夹是否有修改
// Case 1: 本地无修改
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
// 本地没有修改
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// 本地和云端都没有修改,返回“无需操作”
// 两端数据一致,无需同步
return SYNC_ACTION_NONE;
} else {
// 云端有修改,返回“更新本地”(用云端列表数据覆盖本地文件夹)
// 云端有修改,需要更新本地
return SYNC_ACTION_UPDATE_LOCAL;
}
} else {
// 本地有修改
// 2. 验证云端列表ID是否匹配不匹配返回“同步异常”
// Case 2: 本地有修改
// 验证GTask ID匹配
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "gtask id doesn't match");
return SYNC_ACTION_ERROR;
}
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// 只有本地修改,返回“更新云端”(把本地文件夹修改同步到云端)
// 只有本地修改,需要更新云端
return SYNC_ACTION_UPDATE_REMOTE;
} else {
// 本地和云端都有修改,文件夹冲突时优先本地修改,返回“更新云端”
// 两端都有修改,文件夹冲突时优先采用本地修改
return SYNC_ACTION_UPDATE_REMOTE;
}
}
} catch (Exception e) {
// 出现异常,打印错误日志,返回“同步异常”
Log.e(TAG, e.toString());
e.printStackTrace();
}
@ -290,131 +320,160 @@ public class TaskList extends Node {
return SYNC_ACTION_ERROR;
}
// ==================== 子任务管理方法 ====================
/**
*
* 便
* @return
*
*
* @return
*/
public int getChildTaskCount() {
return mChildren.size();
}
/**
*
* 便便便
* @param task 便
* @return true=false=/
*
* <p>
*
*
*
* @param task
* @return truefalse
*/
public boolean addChildTask(Task task) {
boolean ret = false;
// 如果任务不为空,且列表中没有该任务
if (task != null && !mChildren.contains(task)) {
ret = mChildren.add(task); // 添加任务到列表末尾
ret = mChildren.add(task);
if (ret) {
// 设置任务的上一个兄弟任务(列表最后一个任务的前一个)
task.setPriorSibling(mChildren.isEmpty() ? null : mChildren
.get(mChildren.size() - 1));
task.setParent(this); // 设置任务的所属列表(当前文件夹)
// 设置前一个兄弟任务
task.setPriorSibling(mChildren.isEmpty() ? null :
mChildren.get(mChildren.size() - 1));
// 设置父列表
task.setParent(this);
}
}
return ret;
}
/**
*
* 便
* @param task 便
*
* <p>
*
*
*
* @param task
* @param index
* @return true=false=/
* @return truefalse
*/
public boolean addChildTask(Task task, int index) {
// 索引无效小于0或大于列表长度打印错误日志并返回false
// 索引验证
if (index < 0 || index > mChildren.size()) {
Log.e(TAG, "add child task: invalid index");
return false;
}
// 查找任务在列表中的位置(-1表示不存在
int pos = mChildren.indexOf(task);
// 如果任务不存在,插入到指定位置
if (task != null && pos == -1) {
mChildren.add(index, task); // 在指定位置插入任务
mChildren.add(index, task);
// 获取该任务的前一个和后一个任务
// 获取前一个任务和后一个任务
Task preTask = null;
Task afterTask = null;
if (index != 0)
preTask = mChildren.get(index - 1); // 前一个任务(上一个兄弟)
if (index != mChildren.size() - 1)
afterTask = mChildren.get(index + 1); // 后一个任务
task.setPriorSibling(preTask); // 设置当前任务的上一个兄弟
if (afterTask != null)
afterTask.setPriorSibling(task); // 更新后一个任务的上一个兄弟为当前任务
if (index != 0) {
preTask = mChildren.get(index - 1);
}
if (index != mChildren.size() - 1) {
afterTask = mChildren.get(index + 1);
}
// 设置当前任务的前一个兄弟
task.setPriorSibling(preTask);
// 更新后一个任务的前一个兄弟
if (afterTask != null) {
afterTask.setPriorSibling(task);
}
}
return true;
}
/**
*
* 便便便
* @param task 便
* @return true=false=
*
* <p>
*
*
*
* @param task
* @return truefalse
*/
public boolean removeChildTask(Task task) {
boolean ret = false;
// 查找任务在列表中的位置
int index = mChildren.indexOf(task);
if (index != -1) {
ret = mChildren.remove(task); // 移除该任务
ret = mChildren.remove(task);
if (ret) {
task.setPriorSibling(null); // 重置该任务的上一个兄弟关系
task.setParent(null); // 重置该任务的所属列表
// 重置被移除任务的关联关系
task.setPriorSibling(null);
task.setParent(null);
// 如果移除的不是最后一个任务,更新后续任务的兄弟关系
// 更新后续任务的兄弟关系
if (index != mChildren.size()) {
mChildren.get(index).setPriorSibling(
index == 0 ? null : mChildren.get(index - 1));
}
}
}
return ret;
}
/**
*
* 便
* @param task 便
* @param index
* @return true=false=//
*
* <p>
*
*
*
* @param task
* @param index
* @return truefalse
*/
public boolean moveChildTask(Task task, int index) {
// 索引无效小于0或大于等于列表长度打印错误日志并返回false
// 索引验证
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "move child task: invalid index");
return false;
}
// 查找任务当前位置(-1表示不存在
int pos = mChildren.indexOf(task);
if (pos == -1) {
Log.e(TAG, "move child task: the task should in the list");
return false;
}
// 如果当前位置和目标位置一致无需移动返回true
if (pos == index)
// 如果位置未变,无需移动
if (pos == index) {
return true;
// 先移除任务,再添加到目标位置,返回操作结果
}
// 先移除后添加,实现位置移动
return (removeChildTask(task) && addChildTask(task, index));
}
/**
* ID
* 便ID便
* @param gid ID
* @return 便null
* GID
* <p>
* Google Tasks ID
*
* @param gid Google TasksID
* @return null
*/
public Task findChildTaskByGid(String gid) {
for (int i = 0; i < mChildren.size(); i++) {
@ -427,23 +486,26 @@ public class TaskList extends Node {
}
/**
*
* 便
* @param task 便
* @return -1
*
* <p>
*
*
* @param task
* @return -1
*/
public int getChildTaskIndex(Task task) {
return mChildren.indexOf(task);
}
/**
*
* 便
* @param index
* @return 便null
*
* <p>
*
*
* @param index
* @return null
*/
public Task getChildTaskByIndex(int index) {
// 索引无效打印错误日志并返回null
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "getTaskByIndex: invalid index");
return null;
@ -452,41 +514,46 @@ public class TaskList extends Node {
}
/**
* IDfindChildTaskByGid
* 便ID便
* @param gid ID
* @return 便null
* GID
* <p>
* findChildTaskByGidAPI
*
* @param gid Google TasksID
* @return null
*/
public Task getChilTaskByGid(String gid) {
for (Task task : mChildren) {
if (task.getGid().equals(gid))
if (task.getGid().equals(gid)) {
return task;
}
}
return null;
}
/**
*
* 便
* @return 便
*
*
* @return
*/
public ArrayList<Task> getChildTaskList() {
return this.mChildren;
}
// ==================== 属性访问器 ====================
/**
*
*
* @param index
*
*
* @param index
*/
public void setIndex(int index) {
this.mIndex = index;
}
/**
*
*
* @return
*
*
* @return
*/
public int getIndex() {
return this.mIndex;

@ -1,32 +1,63 @@
/*
* Description便
* DescriptionGoogle Tasks
* GTask
*/
package net.micode.notes.gtask.exception;
/**
* Google Tasks (Exception Layer - Operation Failure)
* <p>
* GTask
*
* RuntimeException
*
* 便
*/
public class ActionFailureException extends RuntimeException {
private static final long serialVersionUID = 4425249765923293627L;
/*
* serialVersionUIDjava
* serialVersionUID
* Made By Cuican
/**
* UID
* <p>
* /
*
*
*/
private static final long serialVersionUID = 4425249765923293627L;
/**
*
* <p>
*
*
*/
public ActionFailureException() {
super();
super(); // 调用父类RuntimeException的无参构造方法
}
/*
* JAVA使superthis.
* new
* 使super
* super()super (paramString)Exception ()Exception (paramString)
* Made By Cuican
/**
*
* <p>
*
* 便
*
* @param paramString
*/
public ActionFailureException(String paramString) {
super(paramString);
super(paramString); // 调用父类RuntimeException的带消息构造方法
}
/**
*
* <p>
*
* 便
*
*
* @param paramString
* @param paramThrowable
*/
public ActionFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
super(paramString, paramThrowable); // 调用父类RuntimeException的带消息和原因的构造方法
}
}

@ -1,33 +1,64 @@
/*
* Description便
* DescriptionGoogle Tasks
* GTask
*/
package net.micode.notes.gtask.exception;
/**
* Google Tasks (Exception Layer - Network Failure)
* <p>
* GTask
* SSL
* ExceptionChecked Exception
*
* 便
*/
public class NetworkFailureException extends Exception {
private static final long serialVersionUID = 2107610287180234136L;
/*
* serialVersionUIDjava
* serialVersionUID
* Made By Cuican
/**
* UID
* <p>
* /
*
*
*/
private static final long serialVersionUID = 2107610287180234136L;
/**
*
* <p>
*
*
*/
public NetworkFailureException() {
super();
super(); // 调用父类Exception的无参构造方法
}
/*
* JAVA使superthis.
* new
* 使super
* super()super (paramString)Exception ()Exception (paramString)
* Made By Cuican
/**
*
* <p>
*
* 便
*
* @param paramString "连接超时""服务器不可达"
*/
public NetworkFailureException(String paramString) {
super(paramString);
super(paramString); // 调用父类Exception的带消息构造方法
}
/**
*
* <p>
*
* 便
* IOExceptionSocketException
*
*
* @param paramString
* @param paramThrowable
*/
public NetworkFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
super(paramString, paramThrowable); // 调用父类Exception的带消息和原因的构造方法
}
}

@ -22,117 +22,167 @@ import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
// 导入弱引用工具:防止因一直持有页面/服务导致的内存浪费(通俗说就是不占无用内存)
import java.lang.ref.WeakReference;
// 导入兼容版通知工具:让老安卓手机和新安卓手机都能正常显示通知
import androidx.core.app.NotificationCompat;
// 导入通知权限检查工具用来判断用户有没有开启这个APP的通知权限
import androidx.core.app.NotificationManagerCompat;
// 导入APP资源获取APP里的文字、图标等内容
import net.micode.notes.R;
// 导入便签列表页面:通知点击后跳转到这个页面
import net.micode.notes.ui.NotesListActivity;
// 导入便签设置页面:同步失败时,通知点击后跳转到这个页面
import net.micode.notes.ui.NotesPreferenceActivity;
/**
* GTask便
* "ASync""Async"
* Google Tasks (UI Layer - Background Synchronization)
* <p>
* Google Tasks便
* 线
* AsyncTask-
* 线
* 使Android 8.0+
*
*/
// 类名和文件名保持一致GTaskASyncTask建议改成GTaskAsyncTask更规范
public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
// 同步通知的唯一编号:用来区分这个同步通知和其他通知,不会弄混
/**
* ID
* <p>
* GTask
* 使IDID
*/
private static final int GTASK_SYNC_NOTIFICATION_ID = 5234235;
// 通知分类ID安卓8.0以上必须有):给同步通知单独分个类
/**
* IDAndroid 8.0+
* <p>
* Android 8.0
* GTask
*/
private static final String GTASK_SYNC_CHANNEL_ID = "gtask_sync_channel";
// 通知分类名称安卓8.0以上必须有):在手机设置里能看到这个分类的名字
/**
* Android 8.0+
*/
private static final String GTASK_SYNC_CHANNEL_NAME = "GTask同步通知";
/**
*
*
*
* <p>
*
* ActivityService
*/
public interface OnCompleteListener {
/**
*
*
* <p>
*
*
*/
void onComplete();
}
// 用弱引用存上下文(页面/服务信息):页面关闭后,这个引用不会占着内存不放
/**
*
* <p>
* 使WeakReferenceContext
* ActivityServiceGCContext
*
*/
private final WeakReference<Context> mContextRef;
// 用弱引用存回调监听器:外部页面关闭后,这个监听器不会浪费内存
/**
*
*/
private final WeakReference<OnCompleteListener> mListenerRef;
// 通知管理器:用来发送、关闭手机通知的工具
/**
*
*/
private NotificationManager mNotificationManager;
// GTask管理工具专门处理GTask同步逻辑的单例整个APP只有这一个实例
/**
* GTask
* <p>
* GTaskManager
* 使
*/
private final GTaskManager mTaskManager;
/**
*
* @param context /
* @param listener
*
* <p>
*
*
* @param context 访
* @param listener
*/
public GTaskASyncTask(Context context, OnCompleteListener listener) {
// 把上下文存成弱引用,防止内存浪费
mContextRef = new WeakReference<>(context);
// 把回调监听器存成弱引用,防止内存浪费
mListenerRef = new WeakReference<>(listener);
// 先获取上下文(弱引用要先取出来才能用,还要判断不为空)
// 初始化通知管理器
Context ctx = mContextRef.get();
if (ctx != null) {
// 获取手机的通知管理服务(用来发通知的)
mNotificationManager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
// 创建通知分类
// 创建通知通道Android 8.0+
createNotificationChannel();
}
// 获取GTask管理工具的实例
// 获取GTask管理器单例
mTaskManager = GTaskManager.getInstance();
}
/**
*
*
*
* <p>
* GTask
*
*/
public void cancelSync() {
mTaskManager.cancelSync();
}
/**
*
* 线
* @param message 2便
*
* <p>
* 线
* UI
*
* @param message
*/
public void publishProgress(String message) {
// 调用系统方法,把进度信息传出去
// 调用AsyncTask的进度发布方法
publishProgress(new String[]{message});
}
/**
* 8.0
*
* Android 8.0+
* <p>
* Android 8.0
* Android O
*
*/
private void createNotificationChannel() {
// 先获取上下文
Context ctx = mContextRef.get();
// 上下文为空或者手机系统低于安卓8.0,就不用创建分类
// 版本兼容性检查Android 8.0以下无需创建通知通道
if (ctx == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
// 构建通知分类
// 创建通知通道
android.app.NotificationChannel channel = new android.app.NotificationChannel(
GTASK_SYNC_CHANNEL_ID, // 分类ID
GTASK_SYNC_CHANNEL_NAME, // 分类名称
NotificationManager.IMPORTANCE_DEFAULT // 通知重要性:默认级别(有提示音,不震动)
GTASK_SYNC_CHANNEL_ID,
GTASK_SYNC_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT // 默认重要性:显示通知,发出声音
);
// 开启通知指示灯(如果手机有指示灯的话)
// 启用指示灯
channel.enableLights(true);
// 不在手机桌面APP图标上显示角标
// 禁用应用图标角标
channel.setShowBadge(false);
// 把分类注册到手机系统里
// 注册通知通道
if (mNotificationManager != null) {
mNotificationManager.createNotificationChannel(channel);
}
@ -140,122 +190,144 @@ public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
/**
*
* //
* @param tickerId IDGTask
* @param content 3便
* <p>
*
* Activity
* AndroidAPI
*
* @param tickerId TickerID
* @param content
*/
private void showNotification(int tickerId, String content) {
// 先获取上下文
Context ctx = mContextRef.get();
// 上下文或通知管理器为空,就不显示通知
// 上下文或通知管理器检查
if (ctx == null || mNotificationManager == null) {
return;
}
// 安卓13以上要检查通知权限用户没开权限就不显示通知
// Android 13+通知权限检查
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (!NotificationManagerCompat.from(ctx).areNotificationsEnabled()) {
// 这里可以提示用户开权限,现在先直接返回
// 用户禁用了通知权限,跳过通知显示
return;
}
}
// 构建通知点击后的跳转意图
// 构建PendingIntent
PendingIntent pendingIntent;
Intent intent;
// 同步成功就跳转到便签列表,失败/取消就跳转到便签设置
// 根据同步结果选择目标Activity
if (tickerId != R.string.ticker_success) {
intent = new Intent(ctx, NotesPreferenceActivity.class); // 跳设置页面
// 同步失败或取消:跳转到设置页面
intent = new Intent(ctx, NotesPreferenceActivity.class);
} else {
intent = new Intent(ctx, NotesListActivity.class); // 跳便签列表
// 同步成功:跳转到便签列表
intent = new Intent(ctx, NotesListActivity.class);
}
// 配置跳转意图的标志:更新已有意图,防止重复创建
// 设置PendingIntent标志
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
// 安卓6.0以上添加不可变标志,防止报错
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE;
flags |= PendingIntent.FLAG_IMMUTABLE; // Android 6.0+添加不可变标志
}
// 创建跳转意图
pendingIntent = PendingIntent.getActivity(ctx, 0, intent, flags);
// 构建通知内容
// 构建通知
NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx, GTASK_SYNC_CHANNEL_ID)
.setSmallIcon(R.drawable.notification) // 通知小图标(必须设置,不然不显示
.setContentTitle(ctx.getString(R.string.app_name)) // 通知标题显示APP名字
.setContentText(content) // 通知正文
.setTicker(ctx.getString(tickerId)) // 通知顶部一闪而过的文字(老手机有效
.setWhen(System.currentTimeMillis()) // 通知创建时间
.setDefaults(NotificationCompat.DEFAULT_LIGHTS) // 开启默认指示灯
.setAutoCancel(true) // 点击通知,通知自动消
.setContentIntent(pendingIntent); // 通知点击跳转到指定页面
.setSmallIcon(R.drawable.notification) // 小图标(必需
.setContentTitle(ctx.getString(R.string.app_name)) // 应用名称作为标题
.setContentText(content) // 通知内容
.setTicker(ctx.getString(tickerId)) // Ticker文本旧版本Android
.setWhen(System.currentTimeMillis()) // 通知时间
.setDefaults(NotificationCompat.DEFAULT_LIGHTS) // 默认指示灯
.setAutoCancel(true) // 点击后自动
.setContentIntent(pendingIntent); // 点击跳转
// 显示通知
mNotificationManager.notify(GTASK_SYNC_NOTIFICATION_ID, builder.build());
}
/**
* 线
* @param unused
* @return ///
*
* <p>
* 线
*
*
* @param unused VoidAsyncTask
* @return
*/
@Override
protected Integer doInBackground(Void... unused) {
// 获取上下文
Context ctx = mContextRef.get();
// 发布登录进度
if (ctx != null) {
// 发送进度:提示正在登录同步账号
publishProgress(ctx.getString(R.string.sync_progress_login,
NotesPreferenceActivity.getSyncAccountName(ctx)));
}
// 执行同步逻辑,返回同步结果
// 执行同步逻辑
return mTaskManager.sync(ctx, this);
}
/**
* 线/
* @param progress
*
* <p>
* 线
* 广
*
* @param progress
*/
@Override
protected void onProgressUpdate(String... progress) {
// 显示“正在同步”的通知
// 显示同步中通知
showNotification(R.string.ticker_syncing, progress[0]);
// 获取上下文
Context ctx = mContextRef.get();
// 如果上下文是GTask同步服务就发送广播告诉服务当前进度
// 如果是GTaskSyncService发送进度广播
if (ctx instanceof GTaskSyncService) {
((GTaskSyncService) ctx).sendBroadcast(progress[0]);
}
}
/**
* 线
* @param result //
*
* <p>
* 线
*
*
*
* @param result
*/
@Override
protected void onPostExecute(Integer result) {
// 获取上下文
Context ctx = mContextRef.get();
if (ctx != null) {
// 根据同步结果显示应通知
// 根据同步结果显示应通知
if (result == GTaskManager.STATE_SUCCESS) {
// 同步成功:显示成功通知,记录最后同步时间
// 同步成功
showNotification(R.string.ticker_success, ctx.getString(
R.string.success_sync_account, mTaskManager.getSyncAccount()));
// 更新最后同步时间
NotesPreferenceActivity.setLastSyncTime(ctx, System.currentTimeMillis());
} else if (result == GTaskManager.STATE_NETWORK_ERROR) {
// 网络错误:显示网络异常通知
// 网络错误
showNotification(R.string.ticker_fail, ctx.getString(R.string.error_sync_network));
} else if (result == GTaskManager.STATE_INTERNAL_ERROR) {
// 内部错误:显示内部异常通知
// 内部错误
showNotification(R.string.ticker_fail, ctx.getString(R.string.error_sync_internal));
} else if (result == GTaskManager.STATE_SYNC_CANCELLED) {
// 同步取消:显示取消通知
// 同步取消
showNotification(R.string.ticker_cancel, ctx.getString(R.string.error_sync_cancelled));
}
}
// 获取回调监听器,告诉外部任务完成了
// 用完成监听器
OnCompleteListener listener = mListenerRef.get();
if (listener != null) {
listener.onComplete();
@ -263,14 +335,18 @@ public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
}
/**
*
* cancel()
* @param result
*
* <p>
*
* 使
*
* @param result null
*/
@Override
protected void onCancelled(Integer result) {
super.onCancelled(result);
// 告诉外部任务完成了
// 调用完成监听器
OnCompleteListener listener = mListenerRef.get();
if (listener != null) {
listener.onComplete();

@ -16,204 +16,219 @@
package net.micode.notes.gtask.remote;
import android.app.Activity; // 页面类:外部启动同步时需要关联的页面
import android.app.Service; // 后台服务类:在后台执行耗时操作(同步),不占用前台页面资源
import android.content.Context; // 上下文:保存页面/服务信息,用来启动服务
import android.content.Intent; // 意图:用来传递指令(启动/取消同步)、发送广播
import android.os.Bundle; // 数据容器:用来存储意图中的额外参数(比如同步操作类型)
import android.os.IBinder; // 绑定接口:服务绑定相关(这里用不到)
import android.app.Activity;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
/**
* GTask
* GTask
* 广
* 使
* GTask (Sync Service Layer)
* <p>
* GTask 广 UI
* Android Service 使 UI
* <p>
*
* 1. UI START/CANCEL
* 2. {@link GTaskASyncTask}
* 3. 广 {@link Intent} 广
* <p>
* (Foreground Service ) + (广)
*/
public class GTaskSyncService extends Service {
// 意图中存储“同步操作类型”的键名:用来区分是“启动同步”还是“取消同步”
/** Intent Extra 的键名:用于标识同步操作类型 */
public final static String ACTION_STRING_NAME = "sync_action_type";
// 同步操作类型:启动同步
/** 操作类型:启动同步 */
public final static int ACTION_START_SYNC = 0;
// 同步操作类型:取消同步
/** 操作类型:取消同步 */
public final static int ACTION_CANCEL_SYNC = 1;
// 同步操作类型:无效操作(传递错误指令时用)
/** 操作类型:无效操作(默认值或错误传递) */
public final static int ACTION_INVALID = 2;
// 同步状态广播的名称:前台界面通过这个名称接收同步状态通知
/** 同步状态广播的 Action 名称UI 层需监听此 Action 以接收状态更新 */
public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service";
// 广播中存储“是否正在同步”的键名
/** 广播 Extra 键:标识同步是否正在进行中 */
public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing";
// 广播中存储“同步进度提示文字”的键名
/** 广播 Extra 键:存储当前同步进度提示文本 */
public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg";
// 静态变量当前正在执行的同步异步任务整个APP共享确保只有一个同步任务在运行
/** 当前正在执行的同步异步任务实例(静态,确保整个应用只有一个活跃实例) */
private static GTaskASyncTask mSyncTask = null;
// 静态变量:当前同步进度提示文字(比如“正在初始化列表”“正在同步”)
/** 当前同步进度文本(静态,供 UI 查询) */
private static String mSyncProgress = "";
/**
*
*
* GTask
* <p>
* {@link GTaskASyncTask}
* {@link OnCompleteListener}
*/
private void startSync() {
// 如果当前没有正在执行的同步任务,才创建新任务(避免重复同步)
// 单例检查:防止重复启动同步任务
if (mSyncTask == null) {
// 创建GTask同步异步任务传入服务上下文和任务完成监听器
mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() {
/**
*
*
*
* <p>
* 广
*/
@Override
public void onComplete() {
mSyncTask = null; // 同步完成,清空当前任务引用
sendBroadcast(""); // 发送空进度广播,通知界面同步已结束
stopSelf(); // 停止当前服务(同步完成,不需要后台服务了)
mSyncTask = null; // 清理引用,允许下次启动
sendBroadcast(""); // 发送空进度广播,通知 UI 同步结束
stopSelf(); // 同步完成,服务使命结束
}
});
sendBroadcast(""); // 发送广播,通知界面同步已开始
mSyncTask.execute(); // 执行同步异步任务(在后台开始同步)
// 同步开始,立即发送广播通知 UI 更新状态
sendBroadcast("");
// 启动异步任务(实际同步逻辑在 GTaskASyncTask 中执行)
mSyncTask.execute();
}
}
/**
*
*
*
* <p>
* {@link GTaskASyncTask#cancelSync()}
* 线
*/
private void cancelSync() {
// 如果当前有正在执行的同步任务
if (mSyncTask != null) {
// 调用异步任务的取消方法,终止同步
mSyncTask.cancelSync();
}
}
/**
*
* null
*
* <p>
* null
*/
@Override
public void onCreate() {
mSyncTask = null; // 初始化当前同步任务为null还没有同步任务执行
mSyncTask = null;
}
/**
*
*
* @param intent
* @param flags 使
* @param startId ID使
* @return START_STICKY=
*
* <p>
* Intent
* {@link START_STICKY}
*
* @param intent Intent {@link #ACTION_STRING_NAME} Extra
* @param flags
* @param startId ID
* @return {@link START_STICKY}
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 获取意图中的额外参数(存储了同步操作类型)
Bundle bundle = intent.getExtras();
// 如果参数不为空,且包含“同步操作类型”的键
if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) {
// 根据同步操作类型执行对应逻辑
switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) {
case ACTION_START_SYNC:
// 收到“启动同步”指令,执行启动同步方法
startSync();
break;
case ACTION_CANCEL_SYNC:
// 收到“取消同步”指令,执行取消同步方法
cancelSync();
break;
default:
// 收到无效指令,不做处理
// 无效操作类型,忽略
break;
}
// 返回START_STICKY服务被意外杀死比如内存不足系统会尝试重启服务
// 使用 STICKY 模式,服务被意外杀死后系统会尝试重启
return START_STICKY;
}
// 如果没有有效参数,执行父类默认逻辑
return super.onStartCommand(intent, flags, startId);
}
/**
*
*
*
* <p>
*
*/
@Override
public void onLowMemory() {
// 如果有正在执行的同步任务,就取消它,释放内存
// 主动取消同步,释放内存
if (mSyncTask != null) {
mSyncTask.cancelSync();
}
}
/**
*
* null
* @param intent
* @return null
*
* <p>
* startService()
*
* @param intent Intent
* @return null IBinder
*/
@Override
public IBinder onBind(Intent intent) {
return null;
}
/**
* 广
* 广
* @param msg
* 广
* <p>
* Intent 广
* UI {@link net.micode.notes.ui.NotesListActivity}广
*
* @param msg "正在登录""正在同步便签"
*/
public void sendBroadcast(String msg) {
mSyncProgress = msg; // 更新当前同步进度文字
// 创建广播意图,指定广播名称(前台界面通过这个名称接收)
mSyncProgress = msg;
Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME);
// 存入“是否正在同步”的状态(有同步任务就是正在同步)
intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null);
// 存入同步进度提示文字
intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg);
// 发送广播,通知所有监听该广播的界面
// 发送无序广播,所有注册的接收者都会收到
sendBroadcast(intent);
}
/**
* GTask
* GTask
* @param activity
* 便 UI GTask
* <p>
* {@link GTaskManager}
* Google GTaskClient
*
* @param activity Activity Google
*/
public static void startSync(Activity activity) {
// 给GTask管理器设置关联页面用于登录
// 设置 Activity 上下文,供 GTaskClient 登录使用
GTaskManager.getInstance().setActivityContext(activity);
// 创建启动服务的意图,指定服务类
Intent intent = new Intent(activity, GTaskSyncService.class);
// 存入“启动同步”的操作类型
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC);
// 启动后台服务(开始同步)
activity.startService(intent);
}
/**
* GTask
*
* @param context
* 便 UI GTask
* <p>
* {@link #cancelSync()}
*
* @param context
*/
public static void cancelSync(Context context) {
// 创建启动服务的意图,指定服务类
Intent intent = new Intent(context, GTaskSyncService.class);
// 存入“取消同步”的操作类型
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC);
// 启动后台服务(执行取消同步逻辑)
context.startService(intent);
}
/**
*
*
* @return true=false=
* 便
*
* @return true false
*/
public static boolean isSyncing() {
return mSyncTask != null; // 有同步任务就是正在同步
return mSyncTask != null;
}
/**
*
*
* @return
* 便
* <p>
* UI
*
* @return
*/
public static String getProgressString() {
return mSyncProgress;

@ -16,69 +16,82 @@
package net.micode.notes.model;
import android.content.ContentProviderOperation; // 数据库批量操作指令:用来批量执行数据库更新操作
import android.content.ContentProviderResult; // 数据库批量操作结果:存储批量操作的执行结果
import android.content.ContentUris; // URI拼接工具用来拼接便签/数据的数据库访问地址
import android.content.ContentValues; // 数据键值对容器:用来存储要写入数据库的字段和对应值
import android.content.Context; // 上下文:用来获取本地数据库操作工具
import android.content.OperationApplicationException; // 批量操作异常:批量执行数据库操作失败时抛出
import android.net.Uri; // 数据库访问地址:标识数据库中的某类数据(比如便签表、便签数据表)
import android.os.RemoteException; // 远程操作异常:跨进程访问数据库失败时抛出
import android.util.Log; // 日志工具:打印错误信息,方便排查问题
import net.micode.notes.data.Notes; // 便签常量类:存储便签类型、数据库地址等固定值
import net.micode.notes.data.Notes.CallNote; // 通话便签常量存储通话便签的MIME类型等
import net.micode.notes.data.Notes.DataColumns; // 便签数据列便签数据表的字段名比如所属便签ID、内容
import net.micode.notes.data.Notes.NoteColumns; // 便签主列:便签主表的字段名(比如创建时间、修改时间)
import net.micode.notes.data.Notes.TextNote; // 文本便签常量存储文本便签的MIME类型等
import java.util.ArrayList; // 数组列表:用来存储批量操作指令
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.net.Uri;
import android.os.RemoteException;
import android.util.Log;
import net.micode.notes.data.Notes;
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 java.util.ArrayList;
/**
* 便
* 便便ID便
* 便
* 便 (Model Layer - Data Buffer)
* <p>
* MVC Model 便
* "脏数据缓冲区" {@link #syncNote}
* <p>
*
* 1. 便 (mNoteDiffValues): {@link NoteColumns}
* 2. 便 (mNoteData): {@link DataColumns}
* <p>
* GTask {@link net.micode.notes.gtask.remote.GTaskManager}
* <p>
* (2025)
* - 线 ANR
* - 2025 {@link #syncNote} Runnable {@link net.micode.notes.model.ThreadExecutor}
*/
public class Note {
// 便签主信息修改容器:存储便签主表的修改字段(比如修改时间、本地修改标记)
/** 存储便签元数据字段的变更 (NoteColumns) */
private ContentValues mNoteDiffValues;
// 便签具体数据管理对象:负责存储和管理便签的文本/通话记录数据
/** 管理便签内容数据 (文本/通话) 的变更 */
private NoteData mNoteData;
// 日志标签:打印日志时标识是这个类的日志,方便查找问题
/** 日志标签 */
private static final String TAG = "Note";
/**
* 便ID便
* 便便IDID便
* @param context
* @param folderId 便ID
* @return 便ID0
* 便 ID
* <p>
* 便 notes ID
* 线 ID
*
* @param context ContentResolver
* @param folderId 便 ID
* @return 便 ID 0 -1
* @throws IllegalStateException ID -1
*/
public static synchronized long getNewNoteId(Context context, long folderId) {
// 1. 准备新便签的默认数据(存储到便签主表)
// 准备新便签的默认数据
ContentValues values = new ContentValues();
long createdTime = System.currentTimeMillis(); // 获取当前时间作为创建时间
values.put(NoteColumns.CREATED_DATE, createdTime); // 存入创建时间
values.put(NoteColumns.MODIFIED_DATE, createdTime); // 存入修改时间(初始和创建时间一致)
values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 存入便签类型(普通文本便签)
values.put(NoteColumns.LOCAL_MODIFIED, 1); // 存入本地修改标记1=已修改,需要同步)
values.put(NoteColumns.PARENT_ID, folderId); // 存入所属文件夹ID
// 2. 插入新便签到数据库获取数据库返回的访问地址包含新便签ID
long createdTime = System.currentTimeMillis();
values.put(NoteColumns.CREATED_DATE, createdTime);
values.put(NoteColumns.MODIFIED_DATE, createdTime);
values.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
values.put(NoteColumns.LOCAL_MODIFIED, 1); // 初始标记为需要本地同步
values.put(NoteColumns.PARENT_ID, folderId);
// DB操作插入新便签记录
Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
// 3. 从访问地址中解析出新便签ID
long noteId = 0;
try {
// 数据库返回的Uri格式是content://xxx/notes/[noteId]取第2个分段就是便签ID
// 从返回的 Uri 中解析出 ID (格式: content://authority/notes/[id])
noteId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
// 解析ID失败打印错误日志
Log.e(TAG, "Get note id error :" + e.toString());
noteId = 0;
}
// 4. 校验便签ID是否有效-1表示插入失败
// 验证 ID 有效性,-1 表示数据库插入失败
if (noteId == -1) {
throw new IllegalStateException("Wrong note id:" + noteId);
}
@ -86,179 +99,204 @@ public class Note {
}
/**
* 便
* Note便
* 便
*/
public Note() {
mNoteDiffValues = new ContentValues(); // 初始化便签主信息修改容器
mNoteData = new NoteData(); // 初始化便签具体数据管理对象
mNoteDiffValues = new ContentValues();
mNoteData = new NoteData();
}
/**
* 便
* 便
* @param key 便NoteColumns.TITLE
* @param value 便
* 便
* <p>
* {@link NoteColumns#LOCAL_MODIFIED} {@link NoteColumns#MODIFIED_DATE}
* 便
*
* @param key {@link NoteColumns}
* @param value
*/
public void setNoteValue(String key, String value) {
mNoteDiffValues.put(key, value); // 存入要修改的字段和值
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改需要同步到GTask
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); // 更新修改时间为当前时间
mNoteDiffValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
/**
* 便
* 便
* @param key 便TextNote.CONTENT
* @param value 便
* 便
* <p>
* {@link NoteData}
*
* @param key {@link TextNote}
* @param value
*/
public void setTextData(String key, String value) {
mNoteData.setTextData(key, value); // 委托给NoteData对象处理
mNoteData.setTextData(key, value);
}
/**
* ID
* 便IDID
* @param id ID
* ID
* <p>
*
*
* @param id data ID
* @throws IllegalArgumentException id 0
*/
public void setTextDataId(long id) {
mNoteData.setTextDataId(id); // 委托给NoteData对象处理
mNoteData.setTextDataId(id);
}
/**
* ID
* 便ID
* @return ID
* ID
*
* @return ID 0
*/
public long getTextDataId() {
return mNoteData.mTextDataId; // 从NoteData对象中获取
return mNoteData.mTextDataId;
}
/**
* ID
* 便IDID
* @param id ID
* ID
* <p>
*
*
* @param id data ID
* @throws IllegalArgumentException id 0
*/
public void setCallDataId(long id) {
mNoteData.setCallDataId(id); // 委托给NoteData对象处理
mNoteData.setCallDataId(id);
}
/**
* 便
* 便
* @param key 便CallNote.NUMBER
* @param value 10086
* 便
* <p>
* {@link NoteData}
*
* @param key {@link CallNote}
* @param value
*/
public void setCallData(String key, String value) {
mNoteData.setCallData(key, value); // 委托给NoteData对象处理
mNoteData.setCallData(key, value);
}
/**
* 便
* 便/GTask
* @return true=false=
* 便
* <p>
* {@link #mNoteDiffValues} {@link #mNoteData}
* {@link #syncNote}
*
* @return true false
*/
public boolean isLocalModified() {
// 主信息容器有数据 或 具体数据有修改,就表示便签有本地修改
return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified();
}
/**
* 便
* 便
* @param context
* @param noteId 便ID
* @return true=false=
* 便
* <p>
*
* 1. 便 ID
* 2.
* 3. 便 notes
* 4. {@link NoteData#pushIntoContentResolver} data
* <p>
* 线 {@link ThreadExecutor}
*
* @param context ContentResolver
* @param noteId 便 ID
* @return true false
* @throws IllegalArgumentException noteId 0
*/
public boolean syncNote(Context context, long noteId) {
// 1. 校验便签ID是否有效必须大于0
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
// 2. 如果没有本地修改,直接返回成功(无需同步)
// 快速检查:如果没有任何修改,则无需执行数据库操作
if (!isLocalModified()) {
return true;
}
/**
* 便
* 使便便
*/
// 3. 更新便签主信息到数据库
// 先更新便签元数据notes 表)
// DB操作更新便签元数据
if (context.getContentResolver().update(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), // 拼接该便签的数据库访问地址
mNoteDiffValues, // 要更新的主信息数据
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
mNoteDiffValues,
null, null) == 0) {
// 更新返回0表示失败打印错误日志理论上不该发生
// 更新行数为 0 可能是正常情况(如无实际变更),但记录日志以供调试
Log.e(TAG, "Update note error, should not happen");
// 不返回,继续尝试更新具体数据
// 继续执行内容数据同步,不立即返回失败
}
mNoteDiffValues.clear(); // 主信息更新完成(无论成败),清空容器
mNoteDiffValues.clear(); // 清空缓冲区
// 4. 更新便签具体数据到数据库
// 同步便签内容数据data 表)
if (mNoteData.isLocalModified()
&& (mNoteData.pushIntoContentResolver(context, noteId) == null)) {
// 具体数据有修改 且 更新失败,返回同步失败
// 内容数据同步失败
return false;
}
// 5. 所有修改同步完成,返回成功
return true;
}
/**
* 便
* 便
* 便
* <p>
* 便
* 1. 便 ({@link TextNote#CONTENT_ITEM_TYPE})
* 2. 便 ({@link CallNote#CONTENT_ITEM_TYPE})
* <p>
* Note NoteData 便
* "新增或更新" ID
*/
private class NoteData {
// 文本数据ID对应数据库里文本便签数据的唯一ID
/** 文本内容数据的数据库 ID0 表示未关联数据库记录 */
private long mTextDataId;
// 文本数据修改容器:存储文本数据的修改字段和值
/** 存储文本内容字段的变更 */
private ContentValues mTextDataValues;
// 通话数据ID对应数据库里通话便签数据的唯一ID
/** 通话内容数据的数据库 ID0 表示未关联数据库记录 */
private long mCallDataId;
// 通话数据修改容器:存储通话数据的修改字段和值
/** 存储通话内容字段的变更 */
private ContentValues mCallDataValues;
// 日志标签:打印该内部类的错误日志
/** 日志标签 */
private static final String TAG = "NoteData";
/**
* 便
*
*/
public NoteData() {
mTextDataValues = new ContentValues(); // 初始化文本数据容器
mCallDataValues = new ContentValues(); // 初始化通话数据容器
mTextDataId = 0; // 初始文本数据ID为0未关联数据库记录
mCallDataId = 0; // 初始通话数据ID为0未关联数据库记录
mTextDataValues = new ContentValues();
mCallDataValues = new ContentValues();
mTextDataId = 0;
mCallDataId = 0;
}
/**
*
*
* @return true=false=
*
*
* @return true
*/
boolean isLocalModified() {
// 文本数据容器有数据 或 通话数据容器有数据,就表示有修改
return mTextDataValues.size() > 0 || mCallDataValues.size() > 0;
}
/**
* ID
* IDID0
* @param id ID
* ID
*
* @param id ID 0
* @throws IllegalArgumentException id 0
*/
void setTextDataId(long id) {
if(id <= 0) {
if (id <= 0) {
throw new IllegalArgumentException("Text data id should larger than 0");
}
mTextDataId = id;
}
/**
* ID
* IDID0
* @param id ID
* ID
*
* @param id ID 0
* @throws IllegalArgumentException id 0
*/
void setCallDataId(long id) {
if (id <= 0) {
@ -268,120 +306,125 @@ public class Note {
}
/**
*
* 便
* @param key CallNote.DATE
* @param value
*
* <p>
* 便 {@link NoteColumns#LOCAL_MODIFIED} {@link NoteColumns#MODIFIED_DATE}
*
* @param key {@link CallNote#NUMBER}
* @param value
*/
void setCallData(String key, String value) {
mCallDataValues.put(key, value); // 存入通话数据修改
// 同步更新便签主信息的“本地修改标记”和“修改时间”
mCallDataValues.put(key, value);
// 内容变更意味着便签整体被修改
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
/**
*
* 便
* @param key TextNote.CONTENT
* @param value 便
*
* <p>
* 便 {@link NoteColumns#LOCAL_MODIFIED} {@link NoteColumns#MODIFIED_DATE}
*
* @param key {@link TextNote#CONTENT}
* @param value
*/
void setTextData(String key, String value) {
mTextDataValues.put(key, value); // 存入文本数据修改
// 同步更新便签主信息的“本地修改标记”和“修改时间”
mTextDataValues.put(key, value);
// 内容变更意味着便签整体被修改
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
/**
*
*
* @param context
* @param noteId 便ID
* @return 便URInull
* data
* <p>
*
* 1. mTextDataId 0 ID
* 2. mTextDataId 0
* 3.
* 4. 使 {@link ContentProviderOperation}
*
* @param context
* @param noteId 便 ID
* @return 便 URI null
* @throws IllegalArgumentException noteId 0
*/
Uri pushIntoContentResolver(Context context, long noteId) {
// 1. 校验便签ID是否有效
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
// 2. 准备数据库批量操作指令列表(批量执行更新,效率更高)
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
ContentProviderOperation.Builder builder = null; // 批量操作指令构建器
ContentProviderOperation.Builder builder = null;
// 3. 处理文本数据同步
if(mTextDataValues.size() > 0) {
mTextDataValues.put(DataColumns.NOTE_ID, noteId); // 绑定所属便签ID
// 1. 处理文本数据
if (mTextDataValues.size() > 0) {
mTextDataValues.put(DataColumns.NOTE_ID, noteId); // 关联所属便签
if (mTextDataId == 0) {
// 文本数据ID为0表示是新增文本数据插入到数据库
mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); // 标记为文本数据类型
// 插入新文本数据记录
mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
// DB操作插入文本数据
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mTextDataValues);
try {
// 解析插入后返回的ID绑定到文本数据
// 从返回的 URI 中解析出数据 ID
setTextDataId(Long.valueOf(uri.getPathSegments().get(1)));
} catch (NumberFormatException e) {
// 插入失败,打印日志并清空数据容器
Log.e(TAG, "Insert new text data fail with noteId" + noteId);
mTextDataValues.clear();
return null;
}
} else {
// 文本数据ID不为0表示是更新已有文本数据添加批量更新指令
// 更新现有文本数据记录
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mTextDataId)); // 拼接文本数据的数据库地址
builder.withValues(mTextDataValues); // 设置要更新的数据
operationList.add(builder.build()); // 添加到批量操作列表
Notes.CONTENT_DATA_URI, mTextDataId));
builder.withValues(mTextDataValues);
operationList.add(builder.build());
}
mTextDataValues.clear(); // 文本数据处理完成,清空容器
mTextDataValues.clear(); // 清空缓冲区
}
// 4. 处理通话数据同步(逻辑和文本数据一致
if(mCallDataValues.size() > 0) {
mCallDataValues.put(DataColumns.NOTE_ID, noteId); // 绑定所属便签ID
// 2. 处理通话数据(逻辑与文本数据类似
if (mCallDataValues.size() > 0) {
mCallDataValues.put(DataColumns.NOTE_ID, noteId);
if (mCallDataId == 0) {
// 通话数据ID为0新增通话数据插入到数据库
mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE); // 标记为通话数据类型
// 插入新通话数据记录
mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE);
// DB操作插入通话数据
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mCallDataValues);
try {
// 解析插入后返回的ID绑定到通话数据
setCallDataId(Long.valueOf(uri.getPathSegments().get(1)));
} catch (NumberFormatException e) {
// 插入失败,打印日志并清空数据容器
Log.e(TAG, "Insert new call data fail with noteId" + noteId);
mCallDataValues.clear();
return null;
}
} else {
// 通话数据ID不为0更新已有通话数据添加批量更新指令
// 更新现有通话数据记录
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mCallDataId)); // 拼接通话数据的数据库地址
builder.withValues(mCallDataValues); // 设置要更新的数据
operationList.add(builder.build()); // 添加到批量操作列表
Notes.CONTENT_DATA_URI, mCallDataId));
builder.withValues(mCallDataValues);
operationList.add(builder.build());
}
mCallDataValues.clear(); // 通话数据处理完成,清空容器
mCallDataValues.clear(); // 清空缓冲区
}
// 5. 执行批量更新操作(如果有更新指令
// 3. 批量执行更新操作(如果有
if (operationList.size() > 0) {
try {
// 批量执行数据库操作
// DB操作批量执行更新
ContentProviderResult[] results = context.getContentResolver().applyBatch(
Notes.AUTHORITY, operationList);
// 判断操作结果是否有效,返回便签的数据库地址
// 验证操作结果
return (results == null || results.length == 0 || results[0] == null) ? null
: ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
} catch (RemoteException e) {
// 跨进程访问失败打印日志并返回null
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
} catch (OperationApplicationException e) {
// 批量操作执行失败打印日志并返回null
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
}
}
// 没有批量更新指令返回null表示无需更新或更新完成
return null;
}
}

@ -16,131 +16,173 @@
package net.micode.notes.model;
import android.appwidget.AppWidgetManager; // 桌面小组件管理类用于判断小组件ID是否有效
import android.content.ContentUris; // URI拼接工具拼接便签的数据库访问地址
import android.content.Context; // 上下文:获取数据库操作工具和资源
import android.database.Cursor; // 数据库查询结果游标:存储数据库查询返回的结果集
import android.text.TextUtils; // 文本工具类:判断字符串是否为空/空白
import android.util.Log; // 日志工具:打印错误/调试信息
import net.micode.notes.data.Notes; // 便签常量类存储便签类型、文件夹ID等固定值
import net.micode.notes.data.Notes.CallNote; // 通话便签常量:存储通话便签的字段名
import net.micode.notes.data.Notes.DataColumns; // 便签数据表字段:数据表的列名
import net.micode.notes.data.Notes.DataConstants; // 便签数据类型常量:区分文本/通话便签
import net.micode.notes.data.Notes.NoteColumns; // 便签主表字段:主表的列名
import net.micode.notes.data.Notes.TextNote; // 文本便签常量:存储文本便签的字段名
import net.micode.notes.tool.ResourceParser.NoteBgResources; // 便签背景资源工具根据颜色ID获取背景资源
import android.appwidget.AppWidgetManager;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.tool.ResourceParser.NoteBgResources;
/**
* 便
* 便便
* 便便便便
* 便 - MVC (Hybrid Controller & ViewModel)
* <p>
* 便 UI ({@link net.micode.notes.ui.NoteEditActivity})
* ({@link Note}) 便
* <p>
*
* 1. **ViewModel**: 便 UI
* 2. **Controller**: UI Model
* <p>
*
* - ****: 便 UI ID
* - ****: {@link NoteSettingChangedListener} UI
* - ****: {@link #loadNote()} {@link #loadNoteData()}
* <p>
* 2025
* - {@link ThreadExecutor}
* - AI 便 ({@link NoteColumns#BG_COLOR_ID}) AI
*/
public class WorkingNote {
// 关联的Note数据对象负责和本地数据库交互存储/同步便签数据)
/** 底层数据模型对象,负责与数据库的直接交互 */
private Note mNote;
// 便签唯一ID对应数据库里的便签ID0表示新便签未存入数据库
/** 便签在数据库中的唯一标识0 表示新便签(未持久化) */
private long mNoteId;
// 便签内容:文本便签的正文文字
/** 便签的文本内容(正文) */
private String mContent;
// 便签模式区分普通文本模式和清单模式比如0=普通模式1=清单模式)
/** 便签模式0-普通文本模式1-清单Checklist模式 */
private int mMode;
// 提醒时间戳便签的提醒日期0表示无提醒
/** 提醒时间戳毫秒0 表示未设置提醒 */
private long mAlertDate;
// 修改时间戳:便签最后一次修改的时间
/** 最后修改时间戳(毫秒) */
private long mModifiedDate;
// 背景颜色ID便签的背景色标识对应不同的背景样式
/** 便签背景颜色标识符,对应 {@link NoteBgResources} 中的定义 */
private int mBgColorId;
// 桌面小组件ID关联的桌面便签小组件ID无效时为INVALID_APPWIDGET_ID
/** 关联的桌面小部件 ID{@link AppWidgetManager#INVALID_APPWIDGET_ID} 表示无关联 */
private int mWidgetId;
// 桌面小组件类型关联的桌面小组件类型无效时为TYPE_WIDGET_INVALIDE
/** 关联的桌面小部件类型,{@link Notes#TYPE_WIDGET_INVALIDE} 表示无效类型 */
private int mWidgetType;
// 所属文件夹ID便签所在的文件夹ID比如通话记录文件夹、自定义文件夹
/** 所属文件夹的数据库 ID */
private long mFolderId;
// 上下文:用于获取数据库工具、资源等
/** 应用上下文,用于资源访问和 ContentResolver */
private Context mContext;
// 日志标签:打印该类的日志,方便排查问题
/** 日志标签 */
private static final String TAG = "WorkingNote";
// 删除标记是否标记为已删除true=已删除,无需保存)
/** 软删除标记,为 true 时表示便签已标记为删除(但可能尚未从数据库物理删除) */
private boolean mIsDeleted;
// 便签设置变化监听器:用于通知界面便签设置的改变(比如背景色、提醒时间变化)
/** 便签设置变更监听器,用于通知 UI 层更新 */
private NoteSettingChangedListener mNoteSettingStatusListener;
// 便签数据表查询投影:查询便签数据时,要获取的字段列表(相当于查询结果的列清单)
/**
* 便data
* <p>
* {@link Notes#CONTENT_DATA_URI} 便
*/
public static final String[] DATA_PROJECTION = new String[] {
DataColumns.ID, // 0: 数据记录ID
DataColumns.CONTENT, // 1: 数据内容(文本便签的正文)
DataColumns.MIME_TYPE, // 2: 数据类型(文本/通话便签)
DataColumns.ID, // 0: 数据记录主键 ID
DataColumns.CONTENT, // 1: 数据内容(文本正文)
DataColumns.MIME_TYPE, // 2: MIME 类型,区分文本便签或通话便签
DataColumns.DATA1, // 3: 扩展字段1存储便签模式
DataColumns.DATA2, // 4: 扩展字段2
DataColumns.DATA3, // 5: 扩展字段3
DataColumns.DATA4, // 6: 扩展字段4
};
// 便签主表查询投影:查询便签主信息时,要获取的字段列表
/**
* 便notes
* <p>
* {@link Notes#CONTENT_NOTE_URI} 便
*/
public static final String[] NOTE_PROJECTION = new String[] {
NoteColumns.PARENT_ID, // 0: 所属文件夹ID
NoteColumns.ALERTED_DATE,// 1: 提醒时间
NoteColumns.BG_COLOR_ID, // 2: 背景颜色ID
NoteColumns.WIDGET_ID, // 3: 桌面小组件ID
NoteColumns.WIDGET_TYPE, // 4: 桌面小组件类型
NoteColumns.MODIFIED_DATE// 5: 修改时间
NoteColumns.PARENT_ID, // 0: 父文件夹 ID
NoteColumns.ALERTED_DATE,// 1: 提醒时间
NoteColumns.BG_COLOR_ID, // 2: 背景颜色 ID
NoteColumns.WIDGET_ID, // 3: 关联小部件 ID
NoteColumns.WIDGET_TYPE, // 4: 关联小部件类型
NoteColumns.MODIFIED_DATE// 5: 最后修改时间
};
// 数据表查询结果的列索引对应DATA_PROJECTION的字段位置方便快速取值
private static final int DATA_ID_COLUMN = 0; // 数据记录ID的索引
private static final int DATA_CONTENT_COLUMN = 1; // 数据内容的索引
private static final int DATA_MIME_TYPE_COLUMN = 2; // 数据类型的索引
private static final int DATA_MODE_COLUMN = 3; // 便签模式的索引
// 主表查询结果的列索引对应NOTE_PROJECTION的字段位置方便快速取值
private static final int NOTE_PARENT_ID_COLUMN = 0; // 所属文件夹ID的索引
private static final int NOTE_ALERTED_DATE_COLUMN = 1; // 提醒时间的索引
private static final int NOTE_BG_COLOR_ID_COLUMN = 2; // 背景颜色ID的索引
private static final int NOTE_WIDGET_ID_COLUMN = 3; // 桌面小组件ID的索引
private static final int NOTE_WIDGET_TYPE_COLUMN = 4; // 桌面小组件类型的索引
private static final int NOTE_MODIFIED_DATE_COLUMN = 5; // 修改时间的索引
// 私有构造方法:创建新便签(未存入数据库)
// 通俗说:初始化一个空白便签,设置默认属性
/** DATA_PROJECTION 中数据记录 ID 的列索引 */
private static final int DATA_ID_COLUMN = 0;
/** DATA_PROJECTION 中数据内容的列索引 */
private static final int DATA_CONTENT_COLUMN = 1;
/** DATA_PROJECTION 中 MIME 类型的列索引 */
private static final int DATA_MIME_TYPE_COLUMN = 2;
/** DATA_PROJECTION 中便签模式(存储在 DATA1的列索引 */
private static final int DATA_MODE_COLUMN = 3;
/** NOTE_PROJECTION 中父文件夹 ID 的列索引 */
private static final int NOTE_PARENT_ID_COLUMN = 0;
/** NOTE_PROJECTION 中提醒时间的列索引 */
private static final int NOTE_ALERTED_DATE_COLUMN = 1;
/** NOTE_PROJECTION 中背景颜色 ID 的列索引 */
private static final int NOTE_BG_COLOR_ID_COLUMN = 2;
/** NOTE_PROJECTION 中关联小部件 ID 的列索引 */
private static final int NOTE_WIDGET_ID_COLUMN = 3;
/** NOTE_PROJECTION 中关联小部件类型的列索引 */
private static final int NOTE_WIDGET_TYPE_COLUMN = 4;
/** NOTE_PROJECTION 中最后修改时间的列索引 */
private static final int NOTE_MODIFIED_DATE_COLUMN = 5;
/**
* 便
*
* @param context
* @param folderId 便 ID
*/
private WorkingNote(Context context, long folderId) {
mContext = context;
mAlertDate = 0; // 默认无提醒
mModifiedDate = System.currentTimeMillis(); // 默认修改时间为当前时间
mFolderId = folderId; // 设置所属文件夹ID
mNote = new Note(); // 创建关联的Note数据对象
mNoteId = 0; // 新便签ID为0未存入数据库
mIsDeleted = false; // 默认未删除
mModifiedDate = System.currentTimeMillis();
mFolderId = folderId;
mNote = new Note(); // 初始化底层数据模型
mNoteId = 0; // 标记为新便签
mIsDeleted = false;
mMode = 0; // 默认普通文本模式
mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 默认无有效桌面小组件
mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 默认无有效小部
}
// 私有构造方法:加载已有便签(从数据库读取)
// 通俗说根据便签ID从数据库加载已有便签的所有信息
/**
* 便
*
* @param context
* @param noteId 便 ID
* @param folderId ID
*/
private WorkingNote(Context context, long noteId, long folderId) {
mContext = context;
mNoteId = noteId; // 设置已有便签的ID
mFolderId = folderId; // 设置所属文件夹ID
mIsDeleted = false; // 默认未删除
mNote = new Note(); // 创建关联的Note数据对象
loadNote(); // 加载便签主信息
mNoteId = noteId;
mFolderId = folderId;
mIsDeleted = false;
mNote = new Note(); // 初始化底层数据模型
loadNote(); // 从数据库加载便签数据
}
/**
* 便
* 便ID便ID
* 便notes
* <p>
* 便
* 便
*
* @throws IllegalArgumentException ID 便
*/
private void loadNote() {
// 拼接该便签的数据库访问地址,查询主表信息
// DB操作查询指定便签的元数据
Cursor cursor = mContext.getContentResolver().query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId),
NOTE_PROJECTION, // 要查询的字段列表
NOTE_PROJECTION,
null, null, null);
if (cursor != null) {
// 如果查询到结果,移动到第一条记录
if (cursor.moveToFirst()) {
// 从查询结果中读取各字段值,赋值给成员变量
// 从 Cursor 中读取各字段值,更新成员变量
mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN);
mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN);
mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN);
@ -148,134 +190,148 @@ public class WorkingNote {
mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);
mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);
}
cursor.close(); // 关闭游标,释放资源
cursor.close();
} else {
// 未查询到便签,打印错误日志并抛出异常
Log.e(TAG, "No note with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note with id " + mNoteId);
}
loadNoteData(); // 主信息加载完成后,加载便签具体数据
loadNoteData(); // 继续加载便签的具体内容数据
}
/**
* 便
* 便ID便便
* 便data
* <p>
* 便
* MIME {@link Note}
*
* @throws IllegalArgumentException 便
*/
private void loadNoteData() {
// 查询该便签对应的所有数据记录条件NOTE_ID等于当前便签ID
// DB操作查询便签关联的所有数据记录
Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI,
DATA_PROJECTION,
DataColumns.NOTE_ID + "=?", // 查询条件
new String[] { String.valueOf(mNoteId) }, // 条件参数
DataColumns.NOTE_ID + "=?",
new String[] { String.valueOf(mNoteId) },
null);
if (cursor != null) {
// 如果查询到结果,移动到第一条记录
if (cursor.moveToFirst()) {
// 循环遍历所有数据记录(一个便签可能对应多条数据,比如文本+通话记录)
do {
String type = cursor.getString(DATA_MIME_TYPE_COLUMN); // 获取数据类型
String type = cursor.getString(DATA_MIME_TYPE_COLUMN);
if (DataConstants.NOTE.equals(type)) {
// 文本便签读取内容、模式并绑定数据ID
// 文本便签数据:加载内容和模式
mContent = cursor.getString(DATA_CONTENT_COLUMN);
mMode = cursor.getInt(DATA_MODE_COLUMN);
mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN));
} else if (DataConstants.CALL_NOTE.equals(type)) {
// 通话便签:绑定通话数据ID
// 通话便签数据:记录数据 ID
mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN));
} else {
// 未知数据类型,打印调试日志
Log.d(TAG, "Wrong note type with type:" + type);
}
} while (cursor.moveToNext()); // 移动到下一条记录,继续遍历
} while (cursor.moveToNext());
}
cursor.close(); // 关闭游标,释放资源
cursor.close();
} else {
// 未查询到便签数据,打印错误日志并抛出异常
Log.e(TAG, "No data with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId);
}
}
/**
* 便
* 便
* @param context
* @param folderId ID
* @param widgetId ID
* @param widgetType
* @param defaultBgColorId ID
* @return WorkingNote
* 便
* <p>
* UI 便便
*
* @param context
* @param folderId 便 ID
* @param widgetId ID
* @param widgetType
* @param defaultBgColorId ID
* @return 便
*/
public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId,
int widgetType, int defaultBgColorId) {
WorkingNote note = new WorkingNote(context, folderId); // 创建空白便签
note.setBgColorId(defaultBgColorId); // 设置默认背景色
note.setWidgetId(widgetId); // 设置桌面小组件ID
note.setWidgetType(widgetType); // 设置桌面小组件类型
WorkingNote note = new WorkingNote(context, folderId);
note.setBgColorId(defaultBgColorId);
note.setWidgetId(widgetId);
note.setWidgetType(widgetType);
return note;
}
/**
* 便
* 便ID便
* @param context
* @param id 便ID
* @return WorkingNote
* ID 便
* <p>
* UI 便
*
* @param context
* @param id 便 ID
* @return 便
*/
public static WorkingNote load(Context context, long id) {
return new WorkingNote(context, id, 0);
}
/**
* 便
* 便/
* @return true=false=/
* 便
* <p>
*
* 1. 便"值得保存"{@link #isWorthSaving()}
* 2. 便 ID
* 3. {@link Note#syncNote(Context, long)}
* 4.
* <p>
* 2025
* UI 线 {@link Note#syncNote} IO
* {@link net.micode.notes.ui.NoteEditActivity#onPause}
* Runnable {@link ThreadExecutor}
*
* @return true /false
*/
public synchronized boolean saveNote() {
// 先判断是否值得保存(已删除、空白新便签、无修改的旧便签都无需保存)
if (isWorthSaving()) {
// 如果是新便签未存入数据库先生成新便签ID
if (!existInDatabase()) {
// 新便签:先在数据库创建记录以获取有效 ID
mNoteId = Note.getNewNoteId(mContext, mFolderId);
if (mNoteId == 0) {
// 生成新ID失败打印错误日志并返回失败
Log.e(TAG, "Create new note fail with id:" + mNoteId);
return false;
}
}
// 调用Note对象的同步方法将修改写入数据库
// DB操作将内存中的变更同步到数据库通过 Note 对象)
mNote.syncNote(mContext, mNoteId);
/**
* 便
*/
// 如果关联了有效小部件,通知 UI 层更新小部件显示
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE
&& mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
}
return true; // 保存成功
return true;
} else {
return false; // 无需保存
return false;
}
}
/**
* 便
* 便ID0便
* @return true=false=便
* 便
*
* @return true mNoteId > 0
*/
public boolean existInDatabase() {
return mNoteId > 0;
}
/**
* 便
*
* 1. 2. 便3. 便
* @return true=false=
* 便"值得保存"
* <p>
*
* 1. 便
* 2. 便
* 3. 便 {@link Note#isLocalModified()} false
*
* @return true false
*/
private boolean isWorthSaving() {
if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent))
@ -287,40 +343,49 @@ public class WorkingNote {
}
/**
* 便
* 便便便
* @param l
* 便
* <p>
* UI {@link net.micode.notes.ui.NoteEditActivity}
* 便便
*
* @param l
*/
public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) {
mNoteSettingStatusListener = l;
}
/**
* 便
* 便
* @param date
* @param set true=false=
* 便
* <p>
* {@link Note}
* UI
*
* @param date 0
* @param set true false UI
*/
public void setAlertDate(long date, boolean set) {
if (date != mAlertDate) {
mAlertDate = date; // 更新提醒时间
// 将提醒时间存入Note对象准备同步到数据库
mAlertDate = date;
// 记录变更到底层数据模型
mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate));
}
// 如果设置了监听器,通知提醒时间变化
// 通知 UI 层提醒状态变化
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onClockAlertChanged(date, set);
}
}
/**
* 便/
* 便
* @param mark true=false=
* 便
* <p>
*
* 便
*
* @param mark true false
*/
public void markDeleted(boolean mark) {
mIsDeleted = mark; // 更新删除标记
// 如果关联了有效桌面小组件且设置了监听器,通知小组件变化
mIsDeleted = mark;
// 如果关联了小部件,通知 UI 层更新小部件(如隐藏或显示删除状态)
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
@ -328,142 +393,223 @@ public class WorkingNote {
}
/**
* 便ID
* 便
* @param id ID
* 便 ID
* <p>
* UI
* <p>
* AI
* ID AI {@link net.micode.notes.ai.AIService}
* UI ID 便
*
* @param id ID {@link NoteBgResources}
*/
public void setBgColorId(int id) {
if (id != mBgColorId) {
mBgColorId = id; // 更新背景颜色ID
// 如果设置了监听器,通知背景色变化
mBgColorId = id;
// 通知 UI 层背景色变化
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onBackgroundColorChanged();
}
// 将背景颜色ID存入Note对象准备同步到数据库
// 记录变更到底层数据模型
mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id));
}
}
/**
* 便/
* 便
* @param mode 便0=1=
* 便
* <p>
* UI
* UI 便
*
* @param mode 0-1-
*/
public void setCheckListMode(int mode) {
if (mMode != mode) {
// 如果设置了监听器,通知模式变化(传入旧模式和新模式
// 通知 UI 层模式变化(传递新旧模式以便过渡处理
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode);
}
mMode = mode; // 更新便签模式
// 将便签模式存入Note对象准备同步到数据库
mMode = mode;
// 记录变更到底层数据模型(存储到文本数据的扩展字段 DATA1
mNote.setTextData(TextNote.MODE, String.valueOf(mMode));
}
}
/**
*
* 便
* @param type
* 便
* <p>
* 便
*
* @param type {@link Notes} TYPE_WIDGET_*
*/
public void setWidgetType(int type) {
if (type != mWidgetType) {
mWidgetType = type; // 更新小组件类型
// 将小组件类型存入Note对象准备同步到数据库
mWidgetType = type;
mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType));
}
}
/**
* ID
* 便ID
* @param id ID
* 便 ID
* <p>
* 便 ID
*
* @param id ID {@link AppWidgetManager}
*/
public void setWidgetId(int id) {
if (id != mWidgetId) {
mWidgetId = id; // 更新小组件ID
// 将小组件ID存入Note对象准备同步到数据库
mWidgetId = id;
mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId));
}
}
/**
* 便
* 便
* @param text 便
* 便
* <p>
* 便
* 使 {@link TextUtils#equals}
* <p>
* AI
* AI
* AI
*
* @param text 便
*/
public void setWorkingText(String text) {
if (!TextUtils.equals(mContent, text)) {
mContent = text; // 更新便签内容
// 将新内容存入Note对象准备同步到数据库
mContent = text;
// 记录变更到底层数据模型
mNote.setTextData(DataColumns.CONTENT, mContent);
}
}
/**
* 便
* 便便
* @param phoneNumber
* @param callDate
* 便便
* <p>
* 便便
* 便
*
* @param phoneNumber
* @param callDate
*/
public void convertToCallNote(String phoneNumber, long callDate) {
// 存入通话时间和号码
// 设置通话数据字段
mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate));
mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber);
// 将便签移动到通话记录文件夹
// 将便签移动到通话记录专用文件夹
mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER));
}
/**
*
* 0便
* @return true=false=
* 便
*
* @return true >0false
*/
public boolean hasClockAlert() {
return (mAlertDate > 0 ? true : false);
}
// 以下都是属性获取方法:返回对应的便签属性值,供外部界面使用
public String getContent() { return mContent; }
public long getAlertDate() { return mAlertDate; }
public long getModifiedDate() { return mModifiedDate; }
public int getBgColorResId() { return NoteBgResources.getNoteBgResource(mBgColorId); }
public int getBgColorId() { return mBgColorId; }
public int getTitleBgResId() { return NoteBgResources.getNoteTitleBgResource(mBgColorId); }
public int getCheckListMode() { return mMode; }
public long getNoteId() { return mNoteId; }
public long getFolderId() { return mFolderId; }
public int getWidgetId() { return mWidgetId; }
public int getWidgetType() { return mWidgetType; }
/** 获取便签的文本内容 */
public String getContent() {
return mContent;
}
/** 获取便签的提醒时间戳 */
public long getAlertDate() {
return mAlertDate;
}
/** 获取便签的最后修改时间戳 */
public long getModifiedDate() {
return mModifiedDate;
}
/**
* 便 ID
* <p>
* UI
*
* @return ID
*/
public int getBgColorResId() {
return NoteBgResources.getNoteBgResource(mBgColorId);
}
/** 获取便签背景颜色 ID逻辑标识符 */
public int getBgColorId() {
return mBgColorId;
}
/**
* 便 ID
* <p>
* 便
*
* @return ID
*/
public int getTitleBgResId() {
return NoteBgResources.getNoteTitleBgResource(mBgColorId);
}
/** 获取便签当前模式0=普通文本1=清单模式) */
public int getCheckListMode() {
return mMode;
}
/** 获取便签的数据库 ID为 0 表示新便签) */
public long getNoteId() {
return mNoteId;
}
/** 获取便签所属文件夹的数据库 ID */
public long getFolderId() {
return mFolderId;
}
/** 获取关联的桌面小部件 ID */
public int getWidgetId() {
return mWidgetId;
}
/** 获取关联的桌面小部件类型 */
public int getWidgetType() {
return mWidgetType;
}
/**
* 便
* 便UI
* 便
* <p>
* UI 便
* UI
*/
public interface NoteSettingChangedListener {
/**
* 便
* 便
* 便
* <p>
* UI 便
*/
void onBackgroundColorChanged();
/**
* 便
* /便UI
* 便
*
* @param date 0
* @param set true false
*/
void onClockAlertChanged(long date, boolean set);
/**
*
* 便/便
* 便
* <p>
* 便
*/
void onWidgetChanged();
/**
* 便
*
* @param oldMode
* @param newMode
* 便
*
* @param oldMode
* @param newMode
*/
void onCheckListModeChanged(int oldMode, int newMode);
}

@ -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,31 +14,21 @@
* 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;
@ -46,29 +36,36 @@ import java.io.IOException;
import java.io.PrintStream;
/**
* 便
*
* 1.
* 2. 便便SD
* 3.
* - 便
* - 便
* - 便
* 4. 便/
* 5. SD
* 便 (Utility Layer - Data Export)
* <p>
* 便TXT便
*
* <p>
*
* 1. SD
* 2.
* 3. 便便
* 4. 便便
* <p>
*
* - ****便
* - ****
* - ****SD便
* <p>
*
*/
public class BackupUtils {
// 日志标签:用于备份过程的日志输出
/** 日志标签 */
private static final String TAG = "BackupUtils";
// 单例实例全局唯一的BackupUtils对象
/** 单例实例,延迟初始化 */
private static BackupUtils sInstance;
/**
* 线
* 线
* @param context 访
* @return BackupUtils
* BackupUtils
*
* @param context 访 ContentResolver
* @return BackupUtils
*/
public static synchronized BackupUtils getInstance(Context context) {
if (sInstance == null) {
@ -78,75 +75,75 @@ public class BackupUtils {
}
/**
*
* /
*/
// 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;
// 文本导出器实例:封装实际的文本导出逻辑,与工具类解耦
// SD卡未挂载无法访问外部存储
public static final int STATE_SD_CARD_UNMOUNTED = 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=SDfalse=
* SD
*
* @return true false
*/
private static boolean externalStorageAvailable() {
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
/**
* 便
* @return BackupUtilsSTATE_XXX
* 便
*
* @return STATE_*
*/
public int exportToText() {
return mTextExport.exportToText();
}
/**
*
* @return notes_20251223.txt
*
*
* @return
*/
public String getExportedTextFileName() {
return mTextExport.mFileName;
}
/**
*
* @return /sdcard/MiNotes/
*
*
* @return SD
*/
public String getExportedTextFileDir() {
return mTextExport.mFileDirectory;
}
/**
*
* BackupUtils便
*
* <p>
*
*/
private static class TextExport {
/**
* 便IO
*
* - NoteColumns.ID便/ID
* - NoteColumns.MODIFIED_DATE便
* - NoteColumns.SNIPPET便/
* - NoteColumns.TYPE便/便/
* 便notes
*/
private static final String[] NOTE_PROJECTION = {
NoteColumns.ID,
@ -155,19 +152,15 @@ public class BackupUtils {
NoteColumns.TYPE
};
// 便签投影数组列索引常量简化Cursor取值避免硬编码
/** 便签 ID 列在查询结果中的索引 */
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使
* 便data
*/
private static final String[] DATA_PROJECTION = {
DataColumns.CONTENT,
@ -178,194 +171,191 @@ public class BackupUtils {
DataColumns.DATA4,
};
// 便签数据投影数组列索引常量
/** 数据内容列在查询结果中的索引 */
private static final int DATA_COLUMN_CONTENT = 0;
/** MIME 类型列在查询结果中的索引 */
private static final int DATA_COLUMN_MIME_TYPE = 1;
/** 通话日期列在查询结果中的索引(存储在 DATA1 字段) */
private static final int DATA_COLUMN_CALL_DATE = 2;
/** 电话号码列在查询结果中的索引(存储在 DATA4 字段) */
private static final int DATA_COLUMN_PHONE_NUMBER = 4;
/**
*
*
* - FORMAT_FOLDER_NAMEXXX
* - FORMAT_NOTE_DATE便2025-12-23 15:30
* - FORMAT_NOTE_CONTENT便XXX
* R.array.format_for_exported_note
* 便便
*/
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 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 IDFORMAT_FOLDER_NAMEFORMAT_NOTE_DATE FORMAT_NOTE_CONTENT
* @return
*/
private String getFormat(int id) {
return TEXT_FORMAT[id];
}
/**
* 便
* @param folderId ID
* @param ps SD
* 便
* <p>
* 便便 {@link #exportNoteToText} 便
*
* @param folderId ID
* @param ps
*/
private void exportFolderToText(String folderId, PrintStream ps) {
// 查询该文件夹下的所有普通便签
// DB操作查询该文件夹下的所有便签
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
* 便
* <p>
* 便 MIME
* - 便
* - 便
*
* @param noteId 便 ID
* @param ps
*/
private void exportNoteToText(String noteId, PrintStream ps) {
// 查询该便签的具体数据(内容/通话记录详情)
// DB操作查询该便签关联的所有数据记录
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
});
} catch (IOException e) {
// 写入分隔符失败时输出错误日志,不中断整体导出流程
Log.e(TAG, e.toString());
}
}
/**
* 便SD
*
* 1. SD
* 2.
* 3. +
* 4. 便
* 5.
* @return BackupUtilsSTATE_XXX
* 便
* <p>
*
* 1.
* 2.
* 3. 便
* 4. 便便
* 5.
*
* @return BackupUtils.STATE_*
*/
public int exportToText() {
// 步骤1检查SD卡是否挂载未挂载直接返回对应状态码
if (!externalStorageAvailable()) {
Log.d(TAG, "Media was not mounted");
return STATE_SD_CARD_UNMOUONTED;
return STATE_SD_CARD_UNMOUNTED;
}
// 步骤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) {
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));
}
@ -377,24 +367,20 @@ public class BackupUtils {
folderCursor.close();
}
// 步骤4导出根目录下的普通便签父文件夹ID为0
// 第二部分导出根目录下的便签parent_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);
NoteColumns.TYPE + "=" + +Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID
+ "=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());
@ -402,41 +388,35 @@ public class BackupUtils {
noteCursor.close();
}
// 步骤5关闭打印流释放文件资源
ps.close();
// 导出成功,返回成功状态码
return STATE_SUCCESS;
}
/**
* SDPrintStream
* generateFileMountedOnSDcardPrintStream
* @return PrintStream/null
*
* <p>
* {@link #generateFileMountedOnSDcard}
* SD
*
* @return PrintStream null
*/
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 +425,42 @@ public class BackupUtils {
}
/**
* SD
* notes_20251223.txt便
* @param context
* @param filePathResId IDR.string.file_path/MiNotes/
* @param fileNameFormatResId IDR.string.file_name_txt_formatnotes_%s.txt
* @return File/null
* SD
* <p>
* SD +
*
*
* @param context
* @param filePathResId ID
* @param fileNameFormatResId ID
* @return File null
*/
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
// 文件名格式化为包含当前日期例如notes_20231231.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;
}
}

@ -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,
@ -35,19 +35,38 @@ import java.util.ArrayList;
import java.util.HashSet;
/**
*
*
* (Utility Layer - Data Operations)
* <p>
* 便
* 访Data Access Layer
* <p>
*
* 1. 便
* 2. 便/
* 3. 便
* 4. 便
* <p>
*
* - ****使 {@link ContentProviderOperation}
* - ****
* - ****
*/
public class DataUtils {
/** 日志标签 */
public static final String TAG = "DataUtils";
/**
*
* @param resolver ContentResolver
* @param ids ID
* @return truefalse
* 便
* <p>
* 使 {@link ContentProviderOperation} {@link ContentResolver#applyBatch}
* {@link Notes#ID_ROOT_FOLDER}
*
* @param resolver ContentResolver
* @param ids 便/ID
* @return true false
*/
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet<Long> ids) {
// 1. 参数校验
if (ids == null) {
Log.d(TAG, "the ids is null");
return true;
@ -57,23 +76,25 @@ public class DataUtils {
return true;
}
// 批量操作列表,用于执行事务性删除
// 2. 构建批量删除操作
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (long id : ids) {
// 保护系统根文件夹不被删除
if(id == Notes.ID_ROOT_FOLDER) {
// 保护性检查:防止误删系统根文件夹
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());
}
// 3. 执行批量操作
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 +109,58 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
* @param id ID
* @param srcFolderId ID
* @param desFolderId ID
* 便
* <p>
* ORIGIN_PARENT_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 folderId ID
* @return truefalse
* 便
* <p>
* {@link #moveNoteToFoler}
* 使
*
* @param resolver ContentResolver
* @param ids 便ID
* @param folderId ID
* @return true false
*/
public static boolean batchMoveToFolder(ContentResolver resolver, HashSet<Long> ids,
long folderId) {
long folderId) {
// 参数校验
if (ids == null) {
Log.d(TAG, "the ids is null");
return true;
}
// 批量操作列表,用于执行事务性更新
// 构建批量更新操作
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
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); // 标记为本地已修改
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,27 +176,30 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
* @return
*
* <p>
* TYPE = TYPE_FOLDER PARENT_ID ID_TRASH_FOLER
*
* @param resolver ContentResolver
* @return
*/
public static int getUserFolderCount(ContentResolver resolver) {
// 查询类型为文件夹且父ID不是回收站的记录数
Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI,
// DB操作查询符合条件的文件夹数量
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { "COUNT(*)" },
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)},
null);
int count = 0;
if(cursor != null) {
if(cursor.moveToFirst()) {
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,23 +207,26 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
* @param noteId ID
* @param type
* @return truefalse
* ID便
* <p>
* PARENT_ID ID_TRASH_FOLER
*
* @param resolver ContentResolver
* @param noteId 便 ID
* @param type 便{@link Notes#TYPE_NOTE} {@link Notes#TYPE_FOLDER}
* @return true 便false
*/
public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) {
// 查询指定ID和类型的笔记且父ID不是回收站
// DB操作查询指定ID和类型且不在回收站的便签
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null,
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER,
new String [] {String.valueOf(type)},
new String[] {String.valueOf(type)},
null);
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) { // 检查是否有匹配的记录
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
@ -197,19 +235,20 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
* @param noteId ID
* @return truefalse
* ID便
*
* @param resolver ContentResolver
* @param noteId 便 ID
* @return true 便false
*/
public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) {
// 查询指定ID的笔记不做其他条件限制
// DB操作查询指定ID的便签是否存在
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 +257,22 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
* @param dataId ID
* @return truefalse
* IDdata
* <p>
* 便
*
* @param resolver ContentResolver
* @param dataId ID
* @return true false
*/
public static boolean existInDataDatabase(ContentResolver resolver, long dataId) {
// 查询指定ID的数据记录
// DB操作查询指定ID的数据记录是否存在
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 +281,25 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
* @param name
* @return truefalse
*
* <p>
* TYPE = TYPE_FOLDER SNIPPET
*
*
* @param resolver ContentResolver
* @param name
* @return true false
*/
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 != null) {
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
@ -262,13 +308,17 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
* @param folderId ID
* @return
*
* <p>
*
*
* @param resolver ContentResolver
* @param folderId ID
* @return {@link AppWidgetAttribute} widgetId widgetType
* null
*/
public static HashSet<AppWidgetAttribute> 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 +331,10 @@ public class DataUtils {
set = new HashSet<AppWidgetAttribute>();
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,51 +347,58 @@ public class DataUtils {
}
/**
* ID
* @param resolver ContentResolver
* @param noteId ID
* @return
* 便ID
* <p>
* 便dataMIME {@link CallNote#CONTENT_ITEM_TYPE}
*
* @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 },
new String[] { CallNote.PHONE_NUMBER },
CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?",
new String [] { String.valueOf(noteId), CallNote.CONTENT_ITEM_TYPE },
new String[] { String.valueOf(noteId), CallNote.CONTENT_ITEM_TYPE },
null);
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 ID0
* 便ID
* <p>
* 使 SQL PHONE_NUMBERS_EQUAL
* 便
*
* @param resolver ContentResolver
* @param phoneNumber
* @param callDate
* @return 便ID 0
*/
public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) {
// 使用自定义的PHONE_NUMBERS_EQUAL函数比较号码确保同一联系人的不同格式号码能匹配
// DB操作使用电话号码和通话日期联合查询通话记录便签
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.NOTE_ID },
new String[] { CallNote.NOTE_ID },
CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL("
+ CallNote.PHONE_NUMBER + ",?)",
new String [] { String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber },
+ 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,24 +409,27 @@ public class DataUtils {
}
/**
* ID
* @param resolver ContentResolver
* @param noteId ID
* @return
* @throws IllegalArgumentException
* 便ID便/
* <p>
* 便 notes SNIPPET 便
*
* @param resolver ContentResolver
* @param noteId 便 ID
* @return 便
* @throws IllegalArgumentException 便
*/
public static String getSnippetById(ContentResolver resolver, long noteId) {
// 查询指定ID笔记的摘要字段
// DB操作查询便签的摘要
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String [] { NoteColumns.SNIPPET },
new String[] { NoteColumns.SNIPPET },
NoteColumns.ID + "=?",
new String [] { String.valueOf(noteId)},
new String[] { String.valueOf(noteId)},
null);
if (cursor != null) {
String snippet = "";
if (cursor.moveToFirst()) {
snippet = cursor.getString(0); // 获取摘要内容
snippet = cursor.getString(0);
}
cursor.close();
return snippet;
@ -378,16 +438,19 @@ public class DataUtils {
}
/**
*
* @param snippet
* @return
* 便
* <p>
* 便
*
* @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;

@ -14,159 +14,195 @@
* limitations under the License.
*/
// 包声明归属小米便签的工具模块定义Google TasksGTask同步相关的核心字符串常量
package net.micode.notes.tool;
/**
* Google TasksGTask
*
* 1. 便GTaskJSON
* 2. GTaskMIUI便
* 3. GTask便
* 便GTask/
* GTask API (Utility Layer - API Constants)
* <p>
* Google Tasks API 使 JSON
* GTask API
* <p>
*
* 1. **API ** Google Tasks API API
* 2. ****使
* 3. ****
* <p>
*
* 1. **JSON (GTASK_JSON_*)** Google Tasks JSON
* 2. ** (GTASK_JSON_ACTION_TYPE_*)**
* 3. ** (GTASK_JSON_TYPE_*)** Google Tasks GROUP TASK
* 4. **/** MIUI 便
*/
public class GTaskStringUtils {
// ======================== GTask同步JSON交互 - 动作相关字段 ========================
/** JSON字段动作ID标识单次同步操作的唯一ID */
// ==================== Google Tasks JSON 字段键名 ====================
/** JSON 键:操作 ID标识一次客户端请求的唯一性 */
public final static String GTASK_JSON_ACTION_ID = "action_id";
/** JSON字段动作列表批量同步时存储多个动作的数组 */
/** JSON 键:操作列表,包含多个待执行的同步操作 */
public final static String GTASK_JSON_ACTION_LIST = "action_list";
/** JSON字段:动作类型(标识当前同步动作的类型,如创建/查询/移动/更新 */
/** JSON操作类型定义客户端请求的操作类型create/get_all/move/update */
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 */
/** JSON 键:创建者 ID标识创建任务的用户 */
public final static String GTASK_JSON_CREATOR_ID = "creator_id";
/** JSON字段子实体存储GTask文件夹下的子任务/子文件夹) */
/** JSON 键:子实体列表,通常用于表示任务列表中的任务集合 */
public final static String GTASK_JSON_CHILD_ENTITY = "child_entity";
/** JSON字段客户端版本标识便签客户端的版本号用于GTask服务端兼容 */
/** JSON 键:客户端版本,用于 API 版本兼容性 */
public final static String GTASK_JSON_CLIENT_VERSION = "client_version";
/** JSON字段完成状态标识GTask任务是否已完成布尔值 */
/** JSON 键:完成状态,表示任务是否已完成 */
public final static String GTASK_JSON_COMPLETED = "completed";
/** JSON字段当前列表ID标识任务/文件夹所属的GTask列表ID */
/** JSON 键:当前列表 ID标识用户正在操作的任务列表 */
public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id";
/** JSON字段默认列表IDGTask默认任务列表的ID */
/** JSON 键:默认列表 ID用户的默认任务列表 */
public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id";
/** JSON字段删除标记标识GTask实体是否已被删除布尔值 */
/** JSON 键:删除标记,表示实体是否已被软删除 */
public final static String GTASK_JSON_DELETED = "deleted";
/** JSON字段目标列表移动操作时的目标列表ID */
/** JSON 键:目标列表 ID移动操作专用 */
public final static String GTASK_JSON_DEST_LIST = "dest_list";
/** JSON字段目标父节点移动操作时的目标父实体ID */
/** JSON 键:目标父实体 ID移动操作专用 */
public final static String GTASK_JSON_DEST_PARENT = "dest_parent";
/** JSON字段目标父节点类型移动操作时目标父实体的类型GROUP/TASK */
/** JSON 键:目标父实体类型(移动操作专用 */
public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type";
/** JSON字段:实体增量(同步时仅传输实体的变更部分,减少数据传输) */
/** JSON 键:实体增量变更,包含需要更新的字段 */
public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta";
/** JSON字段实体类型标识GTask实体的类型任务/文件夹 */
/** JSON实体类型区分任务列表GROUP和任务TASK */
public final static String GTASK_JSON_ENTITY_TYPE = "entity_type";
/** JSON字段获取已删除项同步时是否拉取GTask中已删除的实体 */
/** JSON 键:是否获取已删除的实体(用于增量同步 */
public final static String GTASK_JSON_GET_DELETED = "get_deleted";
/** JSON字段实体IDGTask任务/文件夹的唯一标识ID */
/** JSON实体的全局唯一标识符GTask ID */
public final static String GTASK_JSON_ID = "id";
/** JSON字段索引GTask实体在父节点中的排序索引 */
/** JSON 键:索引位置,用于排序任务在列表中的顺序 */
public final static String GTASK_JSON_INDEX = "index";
/** JSON字段最后修改时间GTask实体的最后修改时间戳 */
/** JSON 键:最后修改时间戳,用于冲突检测和增量同步 */
public final static String GTASK_JSON_LAST_MODIFIED = "last_modified";
/** JSON字段:最新同步点(标识上次同步的位置,用于增量同步) */
/** JSON 键:最新同步点,用于增量同步 */
public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point";
/** JSON字段列表IDGTask列表的唯一标识ID */
/** JSON 键:列表 ID标识任务所属的任务列表 */
public final static String GTASK_JSON_LIST_ID = "list_id";
/** JSON字段列表数组存储GTask所有列表的数组 */
/** JSON 键:任务列表集合,服务器返回的多个任务列表 */
public final static String GTASK_JSON_LISTS = "lists";
/** JSON字段名称GTask任务/文件夹的名称 */
/** JSON 键:实体名称(任务或任务列表的标题 */
public final static String GTASK_JSON_NAME = "name";
/** JSON字段新ID创建/移动操作后生成的新实体ID */
/** JSON 键:新 ID客户端创建实体时生成的临时 ID服务器返回后替换为正式 ID */
public final static String GTASK_JSON_NEW_ID = "new_id";
/** JSON字段备注GTask任务的备注内容对应便签的正文 */
/** JSON 键:备注内容(任务的详细描述 */
public final static String GTASK_JSON_NOTES = "notes";
/** JSON字段父节点IDGTask实体的父文件夹/父任务ID */
/** JSON 键:父实体 ID如任务所属的列表 ID */
public final static String GTASK_JSON_PARENT_ID = "parent_id";
/** JSON字段前序兄弟ID标识实体在父节点中的前一个兄弟实体ID用于排序) */
/** JSON 键:前一个兄弟节点的 ID用于排序) */
public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id";
/** JSON字段同步结果GTask服务端返回的同步操作结果 */
/** JSON 键:操作结果集合,服务器返回的批量操作结果 */
public final static String GTASK_JSON_RESULTS = "results";
/** JSON字段源列表移动操作时的源列表ID */
/** JSON 键:源列表 ID移动操作专用 */
public final static String GTASK_JSON_SOURCE_LIST = "source_list";
/** JSON字段任务数组存储GTask所有任务的数组 */
/** JSON 键:任务集合,服务器返回的多个任务 */
public final static String GTASK_JSON_TASKS = "tasks";
/** JSON字段类型兼容字段同entity_type */
/** JSON 键:类型,与 {@link #GTASK_JSON_ENTITY_TYPE} 类似,可能为冗余字段 */
public final static String GTASK_JSON_TYPE = "type";
/** JSON字段类型-分组GTask文件夹的类型标识 */
// ==================== Google Tasks 实体类型常量 ====================
/** 实体类型值:任务列表(组) */
public final static String GTASK_JSON_TYPE_GROUP = "GROUP";
/** JSON字段类型-任务GTask普通任务的类型标识 */
/** 实体类型值:单个任务 */
public final static String GTASK_JSON_TYPE_TASK = "TASK";
/** JSON字段用户标识GTask所属的用户账号信息 */
/** JSON 键:用户信息 */
public final static String GTASK_JSON_USER = "user";
// ======================== GTask侧文件夹命名规则 ========================
/** MIUI便签专属文件夹前缀区分GTask中其他文件夹避免命名冲突 */
// ==================== MIUI 便签特定标识符 ====================
/**
* MIUI 便
* Google Tasks MIUI 便
*/
public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]";
/** GTask侧默认文件夹名称对应便签的根目录 */
/**
* GTask
* [MIUI_Notes]Default
*/
public final static String FOLDER_DEFAULT = "Default";
/** GTask侧通话记录文件夹名称对应便签的通话记录文件夹 */
/**
* GTask
* [MIUI_Notes]Call_Note
*/
public final static String FOLDER_CALL_NOTE = "Call_Note";
/** GTask侧元数据文件夹名称存储便签与GTask的同步映射元数据 */
/**
*
* 便ID
* [MIUI_Notes]METADATA
*/
public final static String FOLDER_META = "METADATA";
// ======================== 同步元数据标识字段 ========================
/** 元数据头-GTask ID存储便签对应的GTask实体ID */
// ==================== 元数据MetaData相关键名 ====================
/**
* JSON GTask ID
* MIUI 便
*/
public final static String META_HEAD_GTASK_ID = "meta_gid";
/** 元数据头-便签(存储便签核心信息的元数据标识) */
/**
* JSON 便Note
* 便ID
*/
public final static String META_HEAD_NOTE = "meta_note";
/** 元数据头-数据(存储便签扩展数据的元数据标识) */
/**
* JSON 便Data
* 便
*/
public final static String META_HEAD_DATA = "meta_data";
/** 元数据便签名称GTask中元数据便签的固定名称禁止修改/删除) */
/**
*
*
*/
public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE";
}

@ -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,
@ -23,67 +23,93 @@ import net.micode.notes.R;
import net.micode.notes.ui.NotesPreferenceActivity;
/**
*
* ID
*
* (Utility Layer - Resource & Configuration)
* <p>
* 便
* /IDAndroidIDUI
* <p>
*
* 1. ****使ID
* 2. ****便
* 3. ****
* <p>
*
* - ****
* - ****使
* - ****/便
*/
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 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 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;
/**
*
*
* 便
* <p>
* 便
* +
*/
public static class NoteBgResources {
// 编辑界面背景资源数组,索引对应颜色常量
private final static int [] BG_EDIT_RESOURCES = new int [] {
R.drawable.edit_yellow,
R.drawable.edit_blue,
R.drawable.edit_white,
R.drawable.edit_green,
R.drawable.edit_red
/** 便签内容区域背景资源数组,按颜色标识符索引 */
private final static int[] BG_EDIT_RESOURCES = new int[] {
R.drawable.edit_yellow, // YELLOW -> edit_yellow.png
R.drawable.edit_blue, // BLUE -> edit_blue.png
R.drawable.edit_white, // WHITE -> edit_white.png
R.drawable.edit_green, // GREEN -> edit_green.png
R.drawable.edit_red // RED -> edit_red.png
};
// 编辑界面标题栏背景资源数组
private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] {
R.drawable.edit_title_yellow,
R.drawable.edit_title_blue,
R.drawable.edit_title_white,
R.drawable.edit_title_green,
R.drawable.edit_title_red
/** 便签标题区域背景资源数组,按颜色标识符索引 */
private final static int[] BG_EDIT_TITLE_RESOURCES = new int[] {
R.drawable.edit_title_yellow,
R.drawable.edit_title_blue,
R.drawable.edit_title_white,
R.drawable.edit_title_green,
R.drawable.edit_title_red
};
/**
* ID
* @param id
* @return ID
* 便ID
*
* @param id YELLOW, BLUE, WHITE, GREEN, RED
* @return ID
*/
public static int getNoteBgResource(int id) {
return BG_EDIT_RESOURCES[id];
}
/**
* ID
* @param id
* @return ID
* 便ID
*
* @param id YELLOW, BLUE, WHITE, GREEN, RED
* @return ID
*/
public static int getNoteTitleBgResource(int id) {
return BG_EDIT_TITLE_RESOURCES[id];
@ -91,103 +117,115 @@ public class ResourceParser {
}
/**
* ID
*
* @param context
* @return
*
* <p>
* "随机背景颜色"
*
*
* @param context 访SharedPreferences
* @return
*/
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;
}
}
/**
*
*
* 便
* <p>
*
*
*/
public static class NoteItemBgResources {
// 列表首项背景资源数组
private final static int [] BG_FIRST_RESOURCES = new int [] {
R.drawable.list_yellow_up,
R.drawable.list_blue_up,
R.drawable.list_white_up,
R.drawable.list_green_up,
R.drawable.list_red_up
/** 列表第一项背景资源数组 */
private final static int[] BG_FIRST_RESOURCES = new int[] {
R.drawable.list_yellow_up,
R.drawable.list_blue_up,
R.drawable.list_white_up,
R.drawable.list_green_up,
R.drawable.list_red_up
};
// 列表中间项背景资源数组
private final static int [] BG_NORMAL_RESOURCES = new int [] {
R.drawable.list_yellow_middle,
R.drawable.list_blue_middle,
R.drawable.list_white_middle,
R.drawable.list_green_middle,
R.drawable.list_red_middle
/** 列表中间项背景资源数组 */
private final static int[] BG_NORMAL_RESOURCES = new int[] {
R.drawable.list_yellow_middle,
R.drawable.list_blue_middle,
R.drawable.list_white_middle,
R.drawable.list_green_middle,
R.drawable.list_red_middle
};
// 列表末项背景资源数组
private final static int [] BG_LAST_RESOURCES = new int [] {
R.drawable.list_yellow_down,
R.drawable.list_blue_down,
R.drawable.list_white_down,
R.drawable.list_green_down,
R.drawable.list_red_down,
/** 列表最后一项背景资源数组 */
private final static int[] BG_LAST_RESOURCES = new int[] {
R.drawable.list_yellow_down,
R.drawable.list_blue_down,
R.drawable.list_white_down,
R.drawable.list_green_down,
R.drawable.list_red_down,
};
// 列表单一项背景资源数组(只有一项时使用)
private final static int [] BG_SINGLE_RESOURCES = new int [] {
R.drawable.list_yellow_single,
R.drawable.list_blue_single,
R.drawable.list_white_single,
R.drawable.list_green_single,
R.drawable.list_red_single
/** 列表只有一项时的背景资源数组 */
private final static int[] BG_SINGLE_RESOURCES = new int[] {
R.drawable.list_yellow_single,
R.drawable.list_blue_single,
R.drawable.list_white_single,
R.drawable.list_green_single,
R.drawable.list_red_single
};
/**
* ID
* @param id
* @return ID
* ID
*
* @param id
* @return ID
*/
public static int getNoteBgFirstRes(int id) {
return BG_FIRST_RESOURCES[id];
}
/**
* ID
* @param id
* @return ID
* ID
*
* @param id
* @return ID
*/
public static int getNoteBgLastRes(int id) {
return BG_LAST_RESOURCES[id];
}
/**
* ID
* @param id
* @return ID
* ID
*
* @param id
* @return ID
*/
public static int getNoteBgSingleRes(int id) {
return BG_SINGLE_RESOURCES[id];
}
/**
* ID
* @param id
* @return ID
* ID
*
* @param id
* @return ID
*/
public static int getNoteBgNormalRes(int id) {
return BG_NORMAL_RESOURCES[id];
}
/**
* ID
* @return ID
* ID
* <p>
*
*
* @return ID
*/
public static int getFolderBgRes() {
return R.drawable.list_folder;
@ -195,41 +233,45 @@ public class ResourceParser {
}
/**
*
* 2x24x4
*
* <p>
*
* 2x14x1
*/
public static class WidgetBgResources {
// 2x2小部件背景资源数组
private final static int [] BG_2X_RESOURCES = new int [] {
R.drawable.widget_2x_yellow,
R.drawable.widget_2x_blue,
R.drawable.widget_2x_white,
R.drawable.widget_2x_green,
R.drawable.widget_2x_red,
/** 2x1小部件背景资源数组 */
private final static int[] BG_2X_RESOURCES = new int[] {
R.drawable.widget_2x_yellow,
R.drawable.widget_2x_blue,
R.drawable.widget_2x_white,
R.drawable.widget_2x_green,
R.drawable.widget_2x_red,
};
/**
* 2x2ID
* @param id
* @return ID
* 2x1ID
*
* @param id
* @return ID
*/
public static int getWidget2xBgResource(int id) {
return BG_2X_RESOURCES[id];
}
// 4x4小部件背景资源数组
private final static int [] BG_4X_RESOURCES = new int [] {
R.drawable.widget_4x_yellow,
R.drawable.widget_4x_blue,
R.drawable.widget_4x_white,
R.drawable.widget_4x_green,
R.drawable.widget_4x_red
/** 4x1小部件背景资源数组 */
private final static int[] BG_4X_RESOURCES = new int[] {
R.drawable.widget_4x_yellow,
R.drawable.widget_4x_blue,
R.drawable.widget_4x_white,
R.drawable.widget_4x_green,
R.drawable.widget_4x_red
};
/**
* 4x4ID
* @param id
* @return ID
* 4x1ID
*
* @param id
* @return ID
*/
public static int getWidget4xBgResource(int id) {
return BG_4X_RESOURCES[id];
@ -237,37 +279,43 @@ public class ResourceParser {
}
/**
*
*
*
* <p>
* AndroidID
*/
public static class TextAppearanceResources {
// 字体大小样式资源数组,索引对应字体大小常量
private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] {
R.style.TextAppearanceNormal,
R.style.TextAppearanceMedium,
R.style.TextAppearanceLarge,
R.style.TextAppearanceSuper
/** 文字样式资源数组,按字体大小标识符索引 */
private final static int[] TEXTAPPEARANCE_RESOURCES = new int[] {
R.style.TextAppearanceNormal, // TEXT_SMALL
R.style.TextAppearanceMedium, // TEXT_MEDIUM
R.style.TextAppearanceLarge, // TEXT_LARGE
R.style.TextAppearanceSuper // TEXT_SUPER
};
/**
* ID
* @param id
* @return ID
* ID
* <p>
*
*
*
* @param id
* @return IDid{@link #BG_DEFAULT_FONT_SIZE}
*/
public static int getTexAppearanceResource(int id) {
/**
* HACKME: IDSharedPreferencebug
* ID
*/
// 防御性检查:防止数组越界
if (id >= TEXTAPPEARANCE_RESOURCES.length) {
return BG_DEFAULT_FONT_SIZE; // 越界时返回默认值
// 如果标识符超出范围,返回默认字体大小标识符
return BG_DEFAULT_FONT_SIZE;
}
return TEXTAPPEARANCE_RESOURCES[id];
}
/**
*
* @return
*
* <p>
*
*
* @return
*/
public static int getResourcesSize() {
return TEXTAPPEARANCE_RESOURCES.length;

Loading…
Cancel
Save