小米便签data包源码解读

文章目录


前言

在小米便签中,data包是最底层的数据包,它主要完成对数据库的一系列操作,里面定义了便签和文件的属性和标识(数据结构),并规定了数据库中表的结构,同时还定义了数据库的一系列操作以及相应触发器,读懂这部分代码,有助于我们去理解整个应用的数据和数据库的结构,以及我们用户使用APP时,底层代码对数据库的具体操作。

如上是data包中的类图,从中我们可以看出Notes类是这个包中最基本的类,NotesProvider类依赖于NotesDatabaseHelper类,而NotesDatabaseHelper类又依赖于Notes类。故我们先对Notes类进行代码解读、然后再解读NotesDatabaseHelper、NotesProvider和Contant类。


一、Notes类

1.概述

Notes类是最底层的数据类,它定义了一堆常量,用来表示标签和文件的各种属性,以及Intent的额外数据(如布局、小组件ID)。同时,它还通过接口以及其实现类定义了数据库表的列名(两个表:note表和data表)。

2.代码解读

2.1 认证信息和日志输出的标志定义

这里是定义了基本的信息,即认证信息和日志输出时的标志,方便我们了解日志信息是由谁发出的。

public static final String AUTHORITY = "micode_notes";
public static final String TAG = "Notes";

2.2 NoteColumns.TYPE的不同取值

这边定义了note表中,类型行的3种取值:

    public static final int TYPE_NOTE     = 0;
    public static final int TYPE_FOLDER   = 1;
    public static final int TYPE_SYSTEM   = 2;

2.3 系统文件夹的分类

这里定义了4种文件夹类型:

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;

2.4 额外的数据键定义

个人理解为就是定义一些布局的ID,这部分就是用于设置UI界面的一些布局或小组件的id。

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";
public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type";
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;

2.5 数据常量的定义

这里定义了两种数据类型:文本便签和通话记录

public static class DataConstants {
    public static final String NOTE = TextNote.CONTENT_ITEM_TYPE;
    public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE;
}

2.6 定义访问笔记、文件和数据的uri

Android开发中常见的用于定义内容提供者(Content Provider)URI,内容提供者是一种Android组件,它允许应用程序共享和存储数据。这里定义了一个URI来查询数据

public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note");

public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data");

2.7 NoteColumns接口定义

这个接口定义了一系列静态的、最终的字符串常量,这些常量代表数据库表中的列名。

里面定义的属性有:ID、父级ID、创建日期、修改日期、提醒日期、文件(标签)名(摘要?)、小部件ID、小部件类型、背景颜色ID、附件、文件中的标签数量、 文件(标签)类型、最后一个同步ID、本地修改标签、移动前的ID、谷歌任务ID、代码版本信息。

public interface NoteColumns {
    public static final String ID = "_id";
    public static final String PARENT_ID = "parent_id";
    public static final String CREATED_DATE = "created_date";
    public static final String MODIFIED_DATE = "modified_date";
    public static final String ALERTED_DATE = "alert_date";
    public static final String SNIPPET = "snippet";
    public static final String WIDGET_ID = "widget_id";
    public static final String WIDGET_TYPE = "widget_type";
    public static final String BG_COLOR_ID = "bg_color_id";
    public static final String HAS_ATTACHMENT = "has_attachment";
    public static final String NOTES_COUNT = "notes_count";
    public static final String TYPE = "type";
    public static final String SYNC_ID = "sync_id";
    public static final String LOCAL_MODIFIED = "local_modified";
    public static final String ORIGIN_PARENT_ID = "origin_parent_id";
    public static final String GTASK_ID = "gtask_id";
    public static final String VERSION = "version";
}

2.8 DataColumns的接口定义

和NoteColumns接口一样,DataColumns的接口,这个接口包含了一系列静态常量,这些常量代表了数据库表中用于存储数据的列名。

public interface DataColumns {

    public static final String ID = "_id";
    //MIME类型是一种标准,用于标识文档、文件或字节流的性质和格式。在数据库中,这个字段可以用来识别不同类型的数据,例如文本、图片、音频或视频等。
    public static final String MIME_TYPE = "mime_type";

    //归属的Note的ID
    public static final String NOTE_ID = "note_id";

    //创建日期
    public static final String CREATED_DATE = "created_date";

    //最近修改日期
    public static final String MODIFIED_DATE = "modified_date";

    //数据内容
    public static final String CONTENT = "content";


    // 以下5个是通用数据列,它们的具体意义取决于MIME类型(由MIME_TYPE字段指定)。
    // 不同的MIME类型可能需要存储不同类型的数据,这5个字段提供了灵活性,允许根据MIME类型来存储相应的数据。
    // 读后面的代码感觉这部分是在表示内容的不同状态?

    public static final String DATA1 = "data1";
    public static final String DATA2 = "data2";
    public static final String DATA3 = "data3";
    public static final String DATA4 = "data4";
    public static final String DATA5 = "data5";
}

2.9 DataColumns接口的实现类:TextNote与CallNote

这两个类是上述的DataColumns接口的实现类,分别是文本标签和通话记录实体。定义了实体之后,应用的其他部分或其他应用可以通过内容提供者访问和操作文本标签和通话记录

//以下是文本便签的定义
public static final class TextNote implements DataColumns {
    //模式?这个被存在DATA1列中
    public static final String MODE = DATA1; 
    //所处检查列表模式?
    public static final int MODE_CHECK_LIST = 1; 
    // 定义了MIME类型,用于标识文本标签的目录
    public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note"; 
    // 定义了MIME类型,用于标识文本标签的单个项
    public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note";
    //文本标签内容提供者(Content Provider)的URI,用于访问文本标签数据
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");}

// 通话记录的定义?
public static final class CallNote implements DataColumns {
    //一个字符串常量,表示通话记录的日期
    public static final String CALL_DATE = DATA1; 
    //意味着在数据库表中,这个电话号码信息将被存储在DATA3列中
    public static final String PHONE_NUMBER = DATA3; 
    // 同样定义了MIME类型,是用于标识通话记录的目录。
    public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note";
    // 同样定义了MIME类型,是用于标识通话记录的单个项。
    public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";
//定义了通话记录内容提供者的URI,用于访问通话记录数据。
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");
}

3.整体代码的注释和分析

package net.micode.notes.data;

import android.net.Uri;
public class Notes {
//    用于表示笔记应用中的各种类型、标识符以及Intent的额外数据
    public static final String AUTHORITY = "micode_notes";
    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
     */
    //以下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";
    public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type";
    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;

    // 数据常量:里面定义了两种类型:文本便签和通话记录
    public static class DataConstants {
        public static final String NOTE = TextNote.CONTENT_ITEM_TYPE;
        public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE;
    }

    //下面这些有类似指针的效果? 其实就是定义一堆访问笔记和文件的uri
    //GPT:Android开发中常见的用于定义内容提供者(Content Provider)URI
    //内容提供者是一种Android组件,它允许应用程序共享和存储数据。这里定义了一个URI来查询数据
    /**
     * Uri to query all notes and folders
     */
    public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note");

    /**
     * Uri to query data
     */
    public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data");

    public interface NoteColumns {
        // 雨:这个接口定义了一系列静态的、最终的字符串常量,这些常量代表数据库表中的列名。
        // 作用:用于后面创建数据库的表头
        // 总的属性有:ID、父级ID、创建日期、修改日期、提醒日期、文件(标签)名(摘要?)、小部件ID、小部件类型、背景颜色ID、附件、文件中的标签数量、
        //           文件(标签)类型、最后一个同步ID、本地修改标签、移动前的ID、谷歌任务ID、代码版本信息。
        // GPT提示:在Android开发中,当使用SQLite数据库时,通常会为表中的每一列定义一个常量,以便在代码中引用。
        // 这样做的好处是,如果以后需要更改列名,只需要在一个地方修改,而不需要在整个代码中搜索和替换。

        /**
         * The unique ID for a row
         * <P> Type: INTEGER (long) </P>
         */
        public static final String ID = "_id";

        /**
         * The parent's id for note or folder
         * <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>
         */
        // 摘要?
        public static final String SNIPPET = "snippet";

        /**
         * Note's widget id
         * <P> Type: INTEGER (long) </P>
         */
        public static final String WIDGET_ID = "widget_id";

        /**
         * Note's widget type
         * <P> Type: INTEGER (long) </P>
         */
        public static final String WIDGET_TYPE = "widget_type";

        /**
         * Note's background color's id
         * <P> Type: INTEGER (long) </P>
         */
        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>
         */
        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>
         */
        public static final String TYPE = "type";

        /**
         * The last sync id
         * <P> Type: INTEGER (long) </P>
         */
        //雨:在数据同步过程中,这个ID可能用来跟踪和识别每次同步操作的唯一性,确保数据的一致性。
        public static final String SYNC_ID = "sync_id";

        /**
         * Sign to indicate local modified or not
         * <P> Type: INTEGER </P>
         */
        public static final String LOCAL_MODIFIED = "local_modified";

        /**
         * Original parent id before moving into temporary folder
         * <P> Type : INTEGER </P>
         */
        public static final String ORIGIN_PARENT_ID = "origin_parent_id";

        /**
         * The gtask id
         * <P> Type : TEXT </P>
         */
        public static final String GTASK_ID = "gtask_id";

        /**
         * The version code
         * <P> Type : INTEGER (long) </P>
         */
        public static final String VERSION = "version";
    }

    public interface DataColumns {
        // DataColumns的接口,这个接口包含了一系列静态常量,这些常量代表了数据库表中用于存储数据的列名。
        // 每个常量都有相应的注释,说明该列的作用和数据类型。

        /**
         * The unique ID for a row
         * <P> Type: INTEGER (long) </P>
         */
        public static final String ID = "_id";

        /**
         * The MIME type of the item represented by this row.
         * <P> Type: Text </P>
         */
        //MIME类型是一种标准,用于标识文档、文件或字节流的性质和格式。在数据库中,这个字段可以用来识别不同类型的数据,例如文本、图片、音频或视频等。
        public static final String MIME_TYPE = "mime_type";

        /**
         * The reference id to note that this data belongs to
         * <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
         * <P> Type: INTEGER </P>
         */
        public static final String DATA1 = "data1";

        /**
         * Generic data column, the meaning is {@link #MIMETYPE} specific, used for
         * integer data type
         * <P> Type: INTEGER </P>
         */
        public static final String DATA2 = "data2";

        /**
         * Generic data column, the meaning is {@link #MIMETYPE} specific, used for
         * TEXT data type
         * <P> Type: TEXT </P>
         */
        public static final String DATA3 = "data3";

        /**
         * Generic data column, the meaning is {@link #MIMETYPE} specific, used for
         * TEXT data type
         * <P> Type: TEXT </P>
         */
        public static final String DATA4 = "data4";

        /**
         * Generic data column, the meaning is {@link #MIMETYPE} specific, used for
         * TEXT data type
         * <P> Type: TEXT </P>
         */
        public static final String DATA5 = "data5";
    }

    //以下是文本便签的定义
    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>
         */
        public static final String MODE = DATA1; //模式?这个被存在DATA1列中

        public static final int MODE_CHECK_LIST = 1; //所处检查列表模式?

        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note"; // 定义了MIME类型,用于标识文本标签的目录

        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note";// 定义了MIME类型,用于标识文本标签的单个项

        public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");//文本标签内容提供者(Content Provider)的URI,用于访问文本标签数据
    }

    // 通话记录的定义?
    public static final class CallNote implements DataColumns {
        /**
         * Call date for this record
         * <P> Type: INTEGER (long) </P>
         */
        public static final String CALL_DATE = DATA1; //一个字符串常量,表示通话记录的日期

        /**
         * Phone number for this record
         * <P> Type: TEXT </P>
         */
        public static final String PHONE_NUMBER = DATA3; //意味着在数据库表中,这个电话号码信息将被存储在DATA3列中

        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note";// 同样定义了MIME类型,是用于标识通话记录的目录。

        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";// 同样定义了MIME类型,是用于标识通话记录的单个项。

        public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");//定义了通话记录内容提供者的URI,用于访问通话记录数据。
    }
}

二、NotesDatabaseHelper类

1.概述

这时一个数据库帮助类,用于管理名为 note.db 的 SQLite 数据库。它继承自 SQLiteOpenHelper 类,是 Android提供的一个工具类,这里我们在里面定义了一堆SQL语句,以及触发器的定义,用于管理数据库的创建和版本更新.

2.代码解读

2.1 部分常量定义

这里定义的常量分别是数据库名、版本信息、表名、日志标签

// 数据库的基本信息;数据库名称和版本信息(在创建实例对象时会用到)
private static final String DB_NAME = "note.db";
private static final int DB_VERSION = 4;

//内部接口:个人理解为两个表名,一个note,一个data
public interface TABLE {
    public static final String NOTE = "note";
    public static final String DATA = "data";
}

//一个标签,方便日志输出时识别出信息来自哪里
private static final String TAG = "NotesDatabaseHelper";

2.2 唯一实例的定义

静态所有变量,提供一个全局访问点来获取数据库辅助类的唯一实例,使得在应用的任何地方都可以方便地使用它 。

private static NotesDatabaseHelper mInstance;

2.3 创建表和创建索引的SQL语句常量

这些语句都是为了辅助创建数据库的,在创建数据库时对调用这些创建数据库SQL语句常量,这样写有利于提高代码复用性,以及代码的可维护性。

/* 以下都是一些SQL语句,辅助我们来对数据库进行操作 */
//创建note表的语句,这里的NoteColumns就是我们刚刚在Notes中定义的一个接口,里面定义了一系列静态的数据库表中的列名
private static final String CREATE_NOTE_TABLE_SQL =
    "CREATE TABLE " + TABLE.NOTE + "(" +
        NoteColumns.ID + " INTEGER PRIMARY KEY," +
        NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
        NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," +
        NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," +
        NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
        NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," +
        NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
        NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," +
        NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," +
        NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," +
        NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," +
        NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," +
        NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," +
        NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
        NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
        NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
        NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
    ")";

//同上,创建data表的语句,这里的DataColumns就是我们刚刚在Notes中定义的一个接口,里面定义了一系列静态的数据库表中的列名
private static final String CREATE_DATA_TABLE_SQL =
    "CREATE TABLE " + TABLE.DATA + "(" +
        DataColumns.ID + " INTEGER PRIMARY KEY," +
        DataColumns.MIME_TYPE + " TEXT NOT NULL," +
        DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," +
        NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
        NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
        DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," +
        DataColumns.DATA1 + " INTEGER," +
        DataColumns.DATA2 + " INTEGER," +
        DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," +
        DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," +
        DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
    ")";

// 功能简介:
// 创建一个以note的ID为索引
// 解读:
// 用于在TABLE.DATA表上创建一个名为note_id_index的索引。
// 这个索引是基于DataColumns.NOTE_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 + ");";

2.4 增删改的触发器定义常量

创建数据库时会定义一些触发器,用于根据用户操作时实现对数据库的相应操作,有助于同步数据库信息。

// 功能简介:
// 添加触发器:增加文件夹的便签个数记录(因为我们会移动便签进入文件夹,这时候文件夹的计数要进行更新)
// 解读:
// 定义了一个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 +
    " BEGIN " +
    "  UPDATE " + TABLE.NOTE +
    "   SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
    "  WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
    " END";

/**
 * Decrease folder's note count when move note from folder
 */
// 功能简介:(触发器和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
// 添加触发器:减少文件夹的便签个数记录(因为我们会移动便签移出文件夹,这时候文件夹的计数要进行更新)
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 +
    " BEGIN " +
    "  UPDATE " + TABLE.NOTE +
    "   SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
    "  WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
    "  AND " + NoteColumns.NOTES_COUNT + ">0" + ";" +
    " END";

/**
 * Increase folder's note count when insert new note to the folder
 */
// 功能简介:(触发器原理和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
// 添加触发器:当我们在文件夹插入便签时,增加文件夹的便签个数记录
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER =
    "CREATE TRIGGER increase_folder_count_on_insert " +
    " AFTER INSERT ON " + TABLE.NOTE +
    " BEGIN " +
    "  UPDATE " + TABLE.NOTE +
    "   SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
    "  WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
    " END";

/**
 * Decrease folder's note count when delete note from the folder
 */
// 功能简介:(触发器原理和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
// 添加触发器:当我们在文件夹删除便签时,减少文件夹的便签个数记录
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER =
    "CREATE TRIGGER decrease_folder_count_on_delete " +
    " AFTER DELETE ON " + TABLE.NOTE +
    " BEGIN " +
    "  UPDATE " + TABLE.NOTE +
    "   SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
    "  WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
    "  AND " + NoteColumns.NOTES_COUNT + ">0;" +
    " END";

/**
 * Update note's content when insert data with type {@link DataConstants#NOTE}
 */
// 功能简介:
// 添加触发器:当向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 +
    " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
    " BEGIN" +
    "  UPDATE " + TABLE.NOTE +
    "   SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
    "  WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
    " END";

/**
 * Update note's content when data with {@link DataConstants#NOTE} type has changed
 */
// 功能简介:
// 添加触发器:当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 +
    " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
    " BEGIN" +
    "  UPDATE " + TABLE.NOTE +
    "   SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
    "  WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
    " END";

/**
 * Update note's content when data with {@link DataConstants#NOTE} type has deleted
 */
// 功能简介:
// 添加触发器:当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 +
    " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
    " BEGIN" +
    "  UPDATE " + TABLE.NOTE +
    "   SET " + NoteColumns.SNIPPET + "=''" +
    "  WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" +
    " END";

/**
 * Delete datas belong to note which has been deleted
 */
// 功能简介:
// 添加触发器:当从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 +
    " BEGIN" +
    "  DELETE FROM " + TABLE.DATA +
    "   WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" +
    " END";

/**
 * Delete notes belong to folder which has been deleted
 */
// 功能简介:
// 添加触发器:当从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 +
    " BEGIN" +
    "  DELETE FROM " + TABLE.NOTE +
    "   WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
    " END";

/**
 * Move notes belong to folder which has been moved to trash folder
 */
// 功能简介:
// 添加触发器:当某个文件夹被移动到回收站时,移动该文件夹下的所有笔记到回收站
// 解读:
// 在NOTE表上进行UPDATE操作后,如果某个文件夹的新PARENT_ID字段值等于回收站的ID(Notes.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 +
    " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
    " BEGIN" +
    "  UPDATE " + TABLE.NOTE +
    "   SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
    "  WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
    " END";

2.5 构造函数

结合后续的代码看,这两个构造函数是为了实现NotesDatabaseHelper实例的唯一性以及同步性。

public NotesDatabaseHelper(Context context) {
    super(context, DB_NAME, null, DB_VERSION);
}

后续的代码:

//解读:
//synchronized关键字确保在多线程环境下,只有一个线程能够进入这个方法,防止了同时创建多个实例的情况
//getInstance(Context context)方法使用了单例模式来确保整个应用程序中只有一个NotesDatabaseHelper实例。
//它首先检查mInstance(类的静态成员变量,没有在代码片段中显示)是否为null。
//如果是null,则创建一个新的NotesDatabaseHelper实例,并将其赋值给mInstance。最后返回mInstance。
static synchronized NotesDatabaseHelper getInstance(Context context) {
    if (mInstance == null) {
        mInstance = new NotesDatabaseHelper(context);
    }
    return mInstance;
}

2.6 标签表的创建

这里涉及 2.3 中定义的创建表的SQL语句,还有 2.4 中的创建触发器的SQL语句,以及 后续 2.7 中定义的系统文件夹创建。

// 创建note(标签)表
public void createNoteTable(SQLiteDatabase db) {
    db.execSQL(CREATE_NOTE_TABLE_SQL);
    reCreateNoteTableTriggers(db);
    createSystemFolder(db);
    Log.d(TAG, "note table has been created");
}

// 重新创建或更新与笔记表相关的触发器。
// 首先,使用DROP TRIGGER IF EXISTS语句删除已存在的触发器。确保在重新创建触发器之前,不存在同名的触发器。
// 然后,使用db.execSQL()方法执行预定义的SQL语句,这些语句用于创建新的触发器。
private void reCreateNoteTableTriggers(SQLiteDatabase db) {
    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");
    db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete");
    db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert");
    db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete");
    db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash");

    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);
    db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER);
    db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER);
    db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER);
    db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER);
}

2.7 系统文件夹的创建

4个系统文件夹的创建:标签文件夹、默认文件夹、临时文件夹和回收站。

// 功能简介:
// 创建通话记录文件夹、默认文件夹、临时文件夹和回收站,并插入相关数据
// 具体解读:
// 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对象中的旧数据,确保不会重复插入旧数据。
// 然后分别创建默认文件夹、临时文件夹和回收站,并以同样的方法插入数据。
private void createSystemFolder(SQLiteDatabase db) {
    ContentValues values = new ContentValues();

    /**
     * call record foler for call notes
     */
    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
     */
    // 创建默认文件夹:重复上述步骤,但这次是为根文件夹插入数据。
    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
     */
    // 创建“临时”文件夹:同样地,为临时文件夹插入数据。
    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
     */
    // 创建“回收站”文件夹:最后,为回收站文件夹插入数据。
    values.clear();
    values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
    values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
    db.insert(TABLE.NOTE, null, values);
}

2.8 data表的创建

具体操作和 2.6 中创建标签表很像,但是没有定义触发器,多了 2.3 的创建索引语句。

//功能简介:
//创建data(数据)表
//解读:
//这个方法用于创建数据表,以及与之相关的触发器。
//创建数据表:使用db.execSQL方法执行预定义的SQL语句CREATE_DATA_TABLE_SQL,用于创建数据表。
//重新创建数据表触发器:调用reCreateDataTableTriggers方法,用于删除并重新创建与数据表相关的触发器。
//创建索引:使用db.execSQL方法执行CREATE_DATA_NOTE_ID_INDEX_SQL语句,为数据表创建索引。
//记录日志:使用Log.d方法记录一条调试级别的日志,表示数据表已经创建。
public void createDataTable(SQLiteDatabase db) {
    db.execSQL(CREATE_DATA_TABLE_SQL);
    reCreateDataTableTriggers(db);
    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语句,这些语句用于创建新的触发器。
private void reCreateDataTableTriggers(SQLiteDatabase db) {
    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");

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

2.9 数据库版本更新

主要是涉及数据库升级时,需要对表的修改操作。这里部分代码涉及了 2.10 的数据库升级方法。

//功能简介:
//当数据库需要升级时(即数据库的版本号改变),onUpgrade方法会被调用。
//该方法会根据当前的oldVersion和新的newVersion来执行相应的升级操作
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    boolean reCreateTriggers = false;
    boolean skipV2 = false;

    if (oldVersion == 1) {
        upgradeToV2(db);
        skipV2 = true; // this upgrade including the upgrade from v2 to v3
        oldVersion++;
    }

    if (oldVersion == 2 && !skipV2) {
        upgradeToV3(db);
        reCreateTriggers = true;
        oldVersion++;
    }

    if (oldVersion == 3) {
        upgradeToV4(db);
        oldVersion++;
    }

    if (reCreateTriggers) {
        reCreateNoteTableTriggers(db);
        reCreateDataTableTriggers(db);
    }

    if (oldVersion != newVersion) { //数据库升级失败,抛出一个异常,表示数据库升级失败
        throw new IllegalStateException("Upgrade notes database to version " + newVersion
                + "fails");
    }
}

2.10 数据库版本升级具体实现

这个就是用来辅助 2.9 的数据版本升级的。

//功能简介:
// 将数据库从版本1升级到版本2。
//解读:
// 首先,它删除了已经存在的NOTE和DATA表(如果存在的话)。DROP TABLE IF EXISTS语句确保了即使这些表不存在,也不会抛出错误。
// 然后,它调用了createNoteTable(db)和createDataTable(db)方法来重新创建这两个表。这意味着在升级到版本2时,这两个表的内容会被完全清除,并重新创建新的空表。
private void upgradeToV2(SQLiteDatabase db) {
    db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
    db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
    createNoteTable(db);
    createDataTable(db);
}

//功能简介:
// 将数据库从版本2(或可能是跳过版本2的某个状态)升级到版本3。
//解读:
// 首先,删除了三个不再使用的触发器(如果存在的话)。触发器是数据库中的一种对象,可以在插入、更新或删除记录时自动执行某些操作。
// 然后,使用ALTER TABLE语句修改表结构,向NOTE表中添加了一个名为GTASK_ID的新列,并设置默认值为空字符串。
// 最后,向NOTE表中插入了一条新的系统文件夹记录,表示一个名为“trash folder”的系统文件夹。这可能是用于存储已删除笔记的回收站功能。
private void upgradeToV3(SQLiteDatabase db) {
    // drop unused triggers
    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
    db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID
            + " TEXT NOT NULL DEFAULT ''");
    // add a trash system folder
    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。这个新列用于记录标签版本信息。
private void upgradeToV4(SQLiteDatabase db) {
    db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
            + " INTEGER NOT NULL DEFAULT 0");
}

3.整体代码的注释和分析

package net.micode.notes.data;

import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;


public class NotesDatabaseHelper extends SQLiteOpenHelper {
//    数据库帮助类,用于管理名为 note.db 的 SQLite 数据库。
//    它继承自 SQLiteOpenHelper 类,这是 Android提供的一个方便的工具类,用于管理数据库的创建和版本更新.
    // 数据库的基本信息;数据库名称和版本信息(在创建实例对象时会用到)
    private static final String DB_NAME = "note.db";

    private static final int DB_VERSION = 4;

    //内部接口:个人理解为两个表名,一个note,一个data
    public interface TABLE {
        public static final String NOTE = "note";

        public static final String DATA = "data";
    }

    //一个标签,方便日志输出时识别出信息来自哪里
    private static final String TAG = "NotesDatabaseHelper";

    //静态所有变量,提供一个全局访问点来获取数据库辅助类的唯一实例,使得在应用的任何地方都可以方便地使用它
    private static NotesDatabaseHelper mInstance;

    /* 以下都是一些SQL语句,辅助我们来对数据库进行操作 */
    //创建note表的语句,这里的NoteColumns就是我们刚刚在Notes中定义的一个接口,里面定义了一系列静态的数据库表中的列名
    private static final String CREATE_NOTE_TABLE_SQL =
        "CREATE TABLE " + TABLE.NOTE + "(" +
            NoteColumns.ID + " INTEGER PRIMARY KEY," +
            NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
            NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," +
            NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," +
            NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
            NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," +
            NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
            NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," +
            NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," +
            NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," +
            NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," +
            NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," +
            NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," +
            NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
            NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
            NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
            NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
        ")";

    //同上,创建data表的语句,这里的DataColumns就是我们刚刚在Notes中定义的一个接口,里面定义了一系列静态的数据库表中的列名
    private static final String CREATE_DATA_TABLE_SQL =
        "CREATE TABLE " + TABLE.DATA + "(" +
            DataColumns.ID + " INTEGER PRIMARY KEY," +
            DataColumns.MIME_TYPE + " TEXT NOT NULL," +
            DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," +
            NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
            NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
            DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," +
            DataColumns.DATA1 + " INTEGER," +
            DataColumns.DATA2 + " INTEGER," +
            DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," +
            DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," +
            DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
        ")";

    // 功能简介:
    // 创建一个以note的ID为索引
    // 解读:
    // 用于在TABLE.DATA表上创建一个名为note_id_index的索引。
    // 这个索引是基于DataColumns.NOTE_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 + ");";

    /* 以下是一些对便签增删改定义的触发器 */
    /* 总结
    * 这些触发器都是用来维护NOTE表和与之相关联的DATA表之间数据一致性的。
    * 当在NOTE表中发生删除或更新操作时,这些触发器会自动执行相应的数据清理或更新操作,确保数据库中的数据保持正确和一致。
    * 特别是在处理文件夹和回收站等逻辑时,这些触发器起到了非常重要的作用,可以自动管理数据的移动和删除。*/
    /**
     * Increase folder's note count when move note to the folder
     */
    // 功能简介:
    // 添加触发器:增加文件夹的便签个数记录(因为我们会移动便签进入文件夹,这时候文件夹的计数要进行更新)
    // 解读:
    // 定义了一个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 +
        " BEGIN " +
        "  UPDATE " + TABLE.NOTE +
        "   SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
        "  WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
        " END";

    /**
     * Decrease folder's note count when move note from folder
     */
    // 功能简介:(触发器和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
    // 添加触发器:减少文件夹的便签个数记录(因为我们会移动便签移出文件夹,这时候文件夹的计数要进行更新)
    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 +
        " BEGIN " +
        "  UPDATE " + TABLE.NOTE +
        "   SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
        "  WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
        "  AND " + NoteColumns.NOTES_COUNT + ">0" + ";" +
        " END";

    /**
     * Increase folder's note count when insert new note to the folder
     */
    // 功能简介:(触发器原理和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
    // 添加触发器:当我们在文件夹插入便签时,增加文件夹的便签个数记录
    private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER =
        "CREATE TRIGGER increase_folder_count_on_insert " +
        " AFTER INSERT ON " + TABLE.NOTE +
        " BEGIN " +
        "  UPDATE " + TABLE.NOTE +
        "   SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
        "  WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
        " END";

    /**
     * Decrease folder's note count when delete note from the folder
     */
    // 功能简介:(触发器原理和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
    // 添加触发器:当我们在文件夹删除便签时,减少文件夹的便签个数记录
    private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER =
        "CREATE TRIGGER decrease_folder_count_on_delete " +
        " AFTER DELETE ON " + TABLE.NOTE +
        " BEGIN " +
        "  UPDATE " + TABLE.NOTE +
        "   SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
        "  WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
        "  AND " + NoteColumns.NOTES_COUNT + ">0;" +
        " END";

    /**
     * Update note's content when insert data with type {@link DataConstants#NOTE}
     */
    // 功能简介:
    // 添加触发器:当向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 +
        " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
        " BEGIN" +
        "  UPDATE " + TABLE.NOTE +
        "   SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
        "  WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
        " END";

    /**
     * Update note's content when data with {@link DataConstants#NOTE} type has changed
     */
    // 功能简介:
    // 添加触发器:当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 +
        " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
        " BEGIN" +
        "  UPDATE " + TABLE.NOTE +
        "   SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
        "  WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
        " END";

    /**
     * Update note's content when data with {@link DataConstants#NOTE} type has deleted
     */
    // 功能简介:
    // 添加触发器:当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 +
        " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
        " BEGIN" +
        "  UPDATE " + TABLE.NOTE +
        "   SET " + NoteColumns.SNIPPET + "=''" +
        "  WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" +
        " END";

    /**
     * Delete datas belong to note which has been deleted
     */
    // 功能简介:
    // 添加触发器:当从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 +
        " BEGIN" +
        "  DELETE FROM " + TABLE.DATA +
        "   WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" +
        " END";

    /**
     * Delete notes belong to folder which has been deleted
     */
    // 功能简介:
    // 添加触发器:当从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 +
        " BEGIN" +
        "  DELETE FROM " + TABLE.NOTE +
        "   WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
        " END";

    /**
     * Move notes belong to folder which has been moved to trash folder
     */
    // 功能简介:
    // 添加触发器:当某个文件夹被移动到回收站时,移动该文件夹下的所有笔记到回收站
    // 解读:
    // 在NOTE表上进行UPDATE操作后,如果某个文件夹的新PARENT_ID字段值等于回收站的ID(Notes.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 +
        " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
        " BEGIN" +
        "  UPDATE " + TABLE.NOTE +
        "   SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
        "  WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
        " END";

    // 构造器
    public NotesDatabaseHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    // 创建note(标签)表
    public void createNoteTable(SQLiteDatabase db) {
        db.execSQL(CREATE_NOTE_TABLE_SQL);
        reCreateNoteTableTriggers(db);
        createSystemFolder(db);
        Log.d(TAG, "note table has been created");
    }

    // 重新创建或更新与笔记表相关的触发器。
    // 首先,使用DROP TRIGGER IF EXISTS语句删除已存在的触发器。确保在重新创建触发器之前,不存在同名的触发器。
    // 然后,使用db.execSQL()方法执行预定义的SQL语句,这些语句用于创建新的触发器。
    private void reCreateNoteTableTriggers(SQLiteDatabase db) {
        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");
        db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete");
        db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert");
        db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete");
        db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash");

        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);
        db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER);
        db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER);
        db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER);
        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对象中的旧数据,确保不会重复插入旧数据。
    // 然后分别创建默认文件夹、临时文件夹和回收站,并以同样的方法插入数据。
    private void createSystemFolder(SQLiteDatabase db) {
        ContentValues values = new ContentValues();

        /**
         * call record foler for call notes
         */
        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
         */
        // 创建默认文件夹:重复上述步骤,但这次是为根文件夹插入数据。
        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
         */
        // 创建“临时”文件夹:同样地,为临时文件夹插入数据。
        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
         */
        // 创建“回收站”文件夹:最后,为回收站文件夹插入数据。
        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方法记录一条调试级别的日志,表示数据表已经创建。
    public void createDataTable(SQLiteDatabase db) {
        db.execSQL(CREATE_DATA_TABLE_SQL);
        reCreateDataTableTriggers(db);
        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语句,这些语句用于创建新的触发器。
    private void reCreateDataTableTriggers(SQLiteDatabase db) {
        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");

        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。
    static synchronized NotesDatabaseHelper getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new NotesDatabaseHelper(context);
        }
        return mInstance;
    }

    //功能简介:
    //当数据库首次创建时,onCreate方法会被调用。
    //这里重写onCreate方法,它调用了上述createNoteTable(db)和createDataTable(db)两个方法
    //这样首次创建数据库时就多出了两张表。
    @Override
    public void onCreate(SQLiteDatabase db) {
        createNoteTable(db);
        createDataTable(db);
    }

    //功能简介:
    //当数据库需要升级时(即数据库的版本号改变),onUpgrade方法会被调用。
    //该方法会根据当前的oldVersion和新的newVersion来执行相应的升级操作
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        boolean reCreateTriggers = false;
        boolean skipV2 = false;

        if (oldVersion == 1) {
            upgradeToV2(db);
            skipV2 = true; // this upgrade including the upgrade from v2 to v3
            oldVersion++;
        }

        if (oldVersion == 2 && !skipV2) {
            upgradeToV3(db);
            reCreateTriggers = true;
            oldVersion++;
        }

        if (oldVersion == 3) {
            upgradeToV4(db);
            oldVersion++;
        }

        if (reCreateTriggers) {
            reCreateNoteTableTriggers(db);
            reCreateDataTableTriggers(db);
        }

        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时,这两个表的内容会被完全清除,并重新创建新的空表。
    private void upgradeToV2(SQLiteDatabase db) {
        db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
        db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
        createNoteTable(db);
        createDataTable(db);
    }

    //功能简介:
    // 将数据库从版本2(或可能是跳过版本2的某个状态)升级到版本3。
    //解读:
    // 首先,删除了三个不再使用的触发器(如果存在的话)。触发器是数据库中的一种对象,可以在插入、更新或删除记录时自动执行某些操作。
    // 然后,使用ALTER TABLE语句修改表结构,向NOTE表中添加了一个名为GTASK_ID的新列,并设置默认值为空字符串。
    // 最后,向NOTE表中插入了一条新的系统文件夹记录,表示一个名为“trash folder”的系统文件夹。这可能是用于存储已删除笔记的回收站功能。
    private void upgradeToV3(SQLiteDatabase db) {
        // drop unused triggers
        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
        db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID
                + " TEXT NOT NULL DEFAULT ''");
        // add a trash system folder
        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。这个新列用于记录标签版本信息。
    private void upgradeToV4(SQLiteDatabase db) {
        db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
                + " INTEGER NOT NULL DEFAULT 0");
    }
}

三、NotesProvider类

1.概述

NotesProvider的主要功能是作为一个内容提供者,为其他应用程序或组件提供对“Notes”数据的访问。它允许其他应用程序查询、插入、更新或删除标签数据。

通过URI匹配,NotesProvider能够区分对哪种数据类型的请求(例如,单独的标签、标签的数据、文件夹操作等),并执行相应的操作。

这里很多其实就是去数据库的操作,即数据库的增删改查。

2.代码解读

2.1 常量定义

uri匹配器、NotesDatabaseHelper实类与日志标记的定义。

private static final UriMatcher mMatcher;
private NotesDatabaseHelper mHelper;
private static final String TAG = "NotesProvider";

2.2 uri的匹配类型

//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;

2.3 静态初始化块

实例化一个mMatcher对象,并进一步定义uri的匹配规则。

这种写法是Android开发中常见的初始化静态成员变量的方式,特别是当涉及到内容提供者(Content Providers)时。

静态初始化块确保了在类加载时mMatcher就被初始化,并且所有的URI匹配规则也一并设置好了。这样做使得代码组织清晰,所有与URI匹配相关的设置都集中在一个地方,方便管理和维护

//功能概述:
//初始化了一个UriMatcher对象mMatcher,并添加了一系列的URI匹配规则。
//解读:
static {
    //创建了一个UriMatcher实例,并设置默认匹配码为NO_MATCH,表示如果没有任何URI匹配,则返回这个码。
    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);
}

2.4 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;

2.5 完整的 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;

2.6 onCreate方法的重写

确定唯一NotesDatabaseHelper实例

//重写onCreate方法:
//getContext() 方法被调用以获取当前组件的上下文(Context),以便 NotesDatabaseHelper 能够访问应用程序的资源和其他功能
//mHelper用于存储从 NotesDatabaseHelper.getInstance 方法返回的实例。这样,该实例就可以在整个组件的其他方法中被访问和使用。
@Override
public boolean onCreate() {
    mHelper = NotesDatabaseHelper.getInstance(getContext());
    return true;
}

2.7 各种SQL语句(增删改查)

查询数据
 @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来进行不同的查询
    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);
            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);
            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");
            }

            String searchString = null;
            if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) {
                if (uri.getPathSegments().size() > 1) {
                    searchString = uri.getPathSegments().get(1);
                }
            } else {
                searchString = uri.getQueryParameter("pattern");
            }

            if (TextUtils.isEmpty(searchString)) {
                return null;
            }

            //字符串格式化:格式化后的字符串就会是 "%s%",即包含s是任何文本
            //然后执行原始SQL查询
            try {
                searchString = String.format("%%%s%%", searchString);
                c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
                        new String[] { searchString });
            } catch (IllegalStateException ex) {
                Log.e(TAG, "got exception: " + ex.toString());
            }
            break;

            //未知URI处理:
        default:
            throw new IllegalArgumentException("Unknown URI " + uri);
    }
    //如果查询结果不为空(即 Cursor 对象 c 不是 null),则为其设置一个通知 URI。
    //这意味着当与这个 URI 关联的数据发生变化时,任何注册了监听这个 URI 的 ContentObserver 都会被通知。
    if (c != null) {
        c.setNotificationUri(getContext().getContentResolver(), uri);
    }
    return c;
}

插入数据
//参数:Uri 用来标识要插入数据的表,ContentValues对象包含要插入的键值对
@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。
    switch (mMatcher.match(uri)) {
        case URI_NOTE:
            insertedId = noteId = db.insert(TABLE.NOTE, null, values);
            break;
        case URI_DATA:
            if (values.containsKey(DataColumns.NOTE_ID)) {
                noteId = values.getAsLong(DataColumns.NOTE_ID);
            } else {
                Log.d(TAG, "Wrong data format without note id:" + values.toString());
            }
            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
    if (noteId > 0) {
        getContext().getContentResolver().notifyChange(
                ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
    }

    // Notify the data uri
    if (dataId > 0) {
        getContext().getContentResolver().notifyChange(
                ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
    }

    //返回包含新插入数据项ID 的 Uri。允许调用者知道新插入的数据项的位置
    return ContentUris.withAppendedId(uri, insertedId);
}
删除数据项
//参数:uri:标识要删除数据的表或数据项。  selection:一个可选的 WHERE 子句,用于指定删除条件。  selectionArgs:一个可选的字符串数组,用于替换 selection 中的占位符
@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;

    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:
            selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
            count = db.delete(TABLE.NOTE, selection, selectionArgs);
            break;
        case URI_NOTE_ITEM:
            id = uri.getPathSegments().get(1);
            long noteId = Long.valueOf(id);
            if (noteId <= 0) {
                break;
            }
            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);
        }
        getContext().getContentResolver().notifyChange(uri, null);
    }

    return count;
}

更新数据
//参数:uri:标识要更新数据的表或数据项。  values:一个包含新值的键值对集合。
//     selection:一个可选的 WHERE 子句,用于指定更新条件。  selectionArgs:一个可选的字符串数组,用于替换 selection 中的占位符。
@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;

    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);
        }
        getContext().getContentResolver().notifyChange(uri, null);
    }
    return count;
}
辅助方法:

解析传入条件语句,以及版本修改

//解析传入的条件语句:一个 SQL WHERE 子句的一部分
private String parseSelection(String selection) {
    return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
}

//更新note表的version列,将其值增加 1。
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
    StringBuilder sql = new StringBuilder(120);
    sql.append("UPDATE ");
    sql.append(TABLE.NOTE);
    sql.append(" SET ");
    sql.append(NoteColumns.VERSION);
    sql.append("=" + NoteColumns.VERSION + "+1 ");

    if (id > 0 || !TextUtils.isEmpty(selection)) {
        sql.append(" WHERE ");
    }
    if (id > 0) {
        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);
        }
        sql.append(selectString);
    }

    mHelper.getWritableDatabase().execSQL(sql.toString());
}

3.整体的代码注释和分析

package net.micode.notes.data;


import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;

import net.micode.notes.R;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;


public class NotesProvider extends ContentProvider {
//    Android 应用程序中的一部分:内容提供者(ContentProvider)。
//    内容提供者是 Android 四大组件之一,它允许应用程序之间共享数据。

    //概述:
    //NotesProvider的主要功能是作为一个内容提供者,为其他应用程序或组件提供对“Notes”数据的访问。
    //它允许其他应用程序查询、插入、更新或删除标签数据。
    //通过URI匹配,NotesProvider能够区分对哪种数据类型的请求(例如,单独的标签、标签的数据、文件夹操作等),并执行相应的操作。

    //用于匹配不同URI的UriMatcher对象,通常用于解析传入的URI,并确定应该执行哪种操作。
    private static final UriMatcher mMatcher;

    //NotesDatabaseHelper实类,用来操作SQLite数据库,负责创建、更新和查询数据库。
    private NotesDatabaseHelper mHelper;

    //标签,输出日志时用来表示是该类发出的消息
    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匹配规则和搜索查询的投影
    //功能概述:
    //初始化了一个UriMatcher对象mMatcher,并添加了一系列的URI匹配规则。
    //解读:
    static {
        //创建了一个UriMatcher实例,并设置默认匹配码为NO_MATCH,表示如果没有任何URI匹配,则返回这个码。
        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);
    }

    /**
     * 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.
     */
    //功能概述:
    //一个 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() {
        mHelper = NotesDatabaseHelper.getInstance(getContext());
        return true;
    }

    //功能:查询数据
    @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来进行不同的查询
        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);
                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);
                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");
                }

                String searchString = null;
                if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) {
                    if (uri.getPathSegments().size() > 1) {
                        searchString = uri.getPathSegments().get(1);
                    }
                } else {
                    searchString = uri.getQueryParameter("pattern");
                }

                if (TextUtils.isEmpty(searchString)) {
                    return null;
                }

                //字符串格式化:格式化后的字符串就会是 "%s%",即包含s是任何文本
                //然后执行原始SQL查询
                try {
                    searchString = String.format("%%%s%%", searchString);
                    c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
                            new String[] { searchString });
                } catch (IllegalStateException ex) {
                    Log.e(TAG, "got exception: " + ex.toString());
                }
                break;

                //未知URI处理:
            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }
        //如果查询结果不为空(即 Cursor 对象 c 不是 null),则为其设置一个通知 URI。
        //这意味着当与这个 URI 关联的数据发生变化时,任何注册了监听这个 URI 的 ContentObserver 都会被通知。
        if (c != null) {
            c.setNotificationUri(getContext().getContentResolver(), uri);
        }
        return c;
    }

    //功能:插入数据
    //参数:Uri 用来标识要插入数据的表,ContentValues对象包含要插入的键值对
    @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。
        switch (mMatcher.match(uri)) {
            case URI_NOTE:
                insertedId = noteId = db.insert(TABLE.NOTE, null, values);
                break;
            case URI_DATA:
                if (values.containsKey(DataColumns.NOTE_ID)) {
                    noteId = values.getAsLong(DataColumns.NOTE_ID);
                } else {
                    Log.d(TAG, "Wrong data format without note id:" + values.toString());
                }
                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
        if (noteId > 0) {
            getContext().getContentResolver().notifyChange(
                    ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
        }

        // Notify the data uri
        if (dataId > 0) {
            getContext().getContentResolver().notifyChange(
                    ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
        }

        //返回包含新插入数据项ID 的 Uri。允许调用者知道新插入的数据项的位置
        return ContentUris.withAppendedId(uri, insertedId);
    }

    //功能:删除数据项
    //参数:uri:标识要删除数据的表或数据项。  selection:一个可选的 WHERE 子句,用于指定删除条件。  selectionArgs:一个可选的字符串数组,用于替换 selection 中的占位符
    @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;

        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:
                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
                 */
                long noteId = Long.valueOf(id);
                if (noteId <= 0) {
                    break;
                }
                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);
            }
            getContext().getContentResolver().notifyChange(uri, null);
        }

        return count;
    }

    //功能:更新数据库的数据
    //参数:uri:标识要更新数据的表或数据项。  values:一个包含新值的键值对集合。
    //     selection:一个可选的 WHERE 子句,用于指定更新条件。  selectionArgs:一个可选的字符串数组,用于替换 selection 中的占位符。
    @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;

        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);
            }
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return count;
    }

    //解析传入的条件语句:一个 SQL WHERE 子句的一部分
    private String parseSelection(String selection) {
        return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
    }

    //更新note表的version列,将其值增加 1。
    private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
        StringBuilder sql = new StringBuilder(120);
        sql.append("UPDATE ");
        sql.append(TABLE.NOTE);
        sql.append(" SET ");
        sql.append(NoteColumns.VERSION);
        sql.append("=" + NoteColumns.VERSION + "+1 ");

        if (id > 0 || !TextUtils.isEmpty(selection)) {
            sql.append(" WHERE ");
        }
        if (id > 0) {
            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);
            }
            sql.append(selectString);
        }

        mHelper.getWritableDatabase().execSQL(sql.toString());
    }

    @Override
    public String getType(Uri uri) {
        // TODO Auto-generated method stub
        return null;
    }

}

四、Contact类 

1.概述

Contact类用于处理联系人信信,实现了从联系人数据库中获取指定电话号码对应的联系人姓名的功能

2.代码解读

2.1 常量定义

sContactCache:用于缓存电话号码和对应的联系人姓名

TAG:用于日志输出的标识

private static HashMap<String, String> sContactCache;
private static final String TAG = "Contact";

2.2 SQL查询条件

即 WHERE 后面的语句,用于从联系人数据库中筛选出与给定电话号码匹配的联系人。

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 = '+')";

2.3 联系人查询

用于从Android设备的联系人数据库中获取与给定电话号码对应的联系人姓名

//参数:Context对象:用于访问系统服务和应用资源    phoneNumber:需要查询的联系人电话号码
public static String getContact(Context context, String phoneNumber) {
    // 没映射表就建表,有就查缓存中有没有这个联系人
    if(sContactCache == null) {
        sContactCache = new HashMap<String, String>();
    }

    if(sContactCache.containsKey(phoneNumber)) {
        return sContactCache.get(phoneNumber);
    }

    //缓存没有,就查询数据库
    //构造一个SQL查询条件:CALLER_ID_SELECTION中的"+"被替换为电话号码的最小匹配值
    //然后执行查询语句
    String selection = CALLER_ID_SELECTION.replace("+",
            PhoneNumberUtils.toCallerIDMinMatch(phoneNumber));
    Cursor cursor = context.getContentResolver().query(
            Data.CONTENT_URI,
            new String [] { Phone.DISPLAY_NAME },
            selection,
            new String[] { phoneNumber },
            null);

    //判断查询结果:
    //查询结果不为空,且能够移动到第一条记录:
    //  那么就尝试从Cursor中获取联系人姓名,并将其存入缓存sContactCache。然后返回联系人姓名。
    //  异常情况:如果在获取字符串时发生数组越界异常,则记录一个错误日志并返回null。
    //  最后都要确保关闭Cursor对象,以避免内存泄漏。
    //如果查询结果为空或者没有记录可以移动到(即没有找到匹配的联系人):
    //  则记录一条调试日志并返回null
    if (cursor != null && cursor.moveToFirst()) {
        try {
            String name = cursor.getString(0);
            sContactCache.put(phoneNumber, name);
            return name;
        } catch (IndexOutOfBoundsException e) {
            Log.e(TAG, " Cursor get string error " + e.toString());
            return null;
        } finally {
            cursor.close();
        }
    } else {
        Log.d(TAG, "No contact matched with number:" + phoneNumber);
        return null;
    }
}

3.整体的代码注释和分析

package net.micode.notes.data;

import android.content.Context;
import android.database.Cursor;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Data;
import android.telephony.PhoneNumberUtils;
import android.util.Log;

import java.util.HashMap;

public class Contact {
//    用于处理联系人信息
//    实现了从联系人数据库中获取指定电话号码对应的联系人姓名的功能

    //sContactCache:用于缓存电话号码和对应的联系人姓名
    //TAG:用于日志输出的标识
    private static HashMap<String, String> sContactCache;
    private static final String TAG = "Contact";

    //SQL查询条件( WHERE 后面的语句),用于从联系人数据库中筛选出与给定电话号码匹配的联系人。
    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 = '+')";

    //功能简介:用于从Android设备的联系人数据库中获取与给定电话号码对应的联系人姓名。
    //参数:Context对象:用于访问系统服务和应用资源    phoneNumber:需要查询的联系人电话号码
    public static String getContact(Context context, String phoneNumber) {
        // 没映射表就建表,有就查缓存中有没有这个联系人
        if(sContactCache == null) {
            sContactCache = new HashMap<String, String>();
        }

        if(sContactCache.containsKey(phoneNumber)) {
            return sContactCache.get(phoneNumber);
        }

        //缓存没有,就查询数据库
        //构造一个SQL查询条件:CALLER_ID_SELECTION中的"+"被替换为电话号码的最小匹配值
        //然后执行查询语句
        String selection = CALLER_ID_SELECTION.replace("+",
                PhoneNumberUtils.toCallerIDMinMatch(phoneNumber));
        Cursor cursor = context.getContentResolver().query(
                Data.CONTENT_URI,
                new String [] { Phone.DISPLAY_NAME },
                selection,
                new String[] { phoneNumber },
                null);

        //判断查询结果:
        //查询结果不为空,且能够移动到第一条记录:
        //  那么就尝试从Cursor中获取联系人姓名,并将其存入缓存sContactCache。然后返回联系人姓名。
        //  异常情况:如果在获取字符串时发生数组越界异常,则记录一个错误日志并返回null。
        //  最后都要确保关闭Cursor对象,以避免内存泄漏。
        //如果查询结果为空或者没有记录可以移动到(即没有找到匹配的联系人):
        //  则记录一条调试日志并返回null
        if (cursor != null && cursor.moveToFirst()) {
            try {
                String name = cursor.getString(0);
                sContactCache.put(phoneNumber, name);
                return name;
            } catch (IndexOutOfBoundsException e) {
                Log.e(TAG, " Cursor get string error " + e.toString());
                return null;
            } finally {
                cursor.close();
            }
        } else {
            Log.d(TAG, "No contact matched with number:" + phoneNumber);
            return null;
        }
    }
}

 


总结

整体读下来,个人感觉其实data包简单点来说就是为了完成对底层数据库的操作,所以我们需要统一定义数据的格式(Notes类完成的)、创建数据库以及对应的表(NotesDatabaseHelper完成的)还有定义对数据库的一系列操作(NotesProvider和Contact类完成的)。

小米便签widget是一个用于创建桌面小工具的代码。以下是对小米便签widget中的代码注释的解释: 1. 名:com.xiaomi.milink.widget.note.widget 这个代码的根名是com.xiaomi.milink.widget.note.widget,表明这个含了小米便签widget相关的代码。 2. 类名:NoteWidgetProvider 这个类是一个小米便签widget的提供者,负责创建和更新小米便签widget。 3. 类名:NoteWidgetService 这个类是一个小米便签widget的服务类,负责处理小米便签widget的各种操作。 4. 类名:NoteWidgetConfigActivity 这个类是一个小米便签widget的配置界面活动,用于用户配置小米便签widget的相关设置。 5. 类名:NoteWidgetUtils 这个类是一个工具类,含了一些小米便签widget使用的常用方法和功能实现。 6. 类名:NoteWidgetProviderInfo 这个类是一个小米便签widget提供者信息类,用于标识小米便签widget的相关信息。 7. 类名:NoteWidgetManager 这个类是一个小米便签widget管理类,用于管理小米便签widget的创建、更新和删除等操作。 8. 类名:NoteWidgetLayoutHelper 这个类是一个小米便签widget布局辅助类,用于帮助小米便签widget实现自定义的布局。 9. 类名:NoteWidgetDbHelper 这个类是一个小米便签widget数据库辅助类,用于小米便签widget与数据库的交互操作。 10. 类名:NoteWidgetProviderReceiver 这个类是一个小米便签widget提供者接收器,用于接收小米便签widget相关的广播。 以上是对小米便签widget中主要代码文件的类名和功能的简单注释。这些代码文件一起协同工作,实现了小米便签widget的创建、更新、配置和管理等诸多功能。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值