前两篇文章主要讲解了ContactsProvider的结构,这篇文章想详细分析一下ContactsProvider的底层数据表结构(Android4.1.1),和大家一起深入的理解其内部实现。
ContactsProvider的代码路径位于packages/providers/ContactsProvider/src下,我们可以结合实际的场景来分析ContactsProvider的代码逻辑。
首先从ContactsPovider的数据库开始分析,数据是ContactsProvider的基础。如前所述,ContactsDatabaseHelper类创建了联系人数据库,数据库中包括表、视图、触发器,并且为部分表项创建了索引。
一、数据库的定义:
static final int DATABASE_VERSION = 704; private static final String DATABASE_NAME = "contacts2.db"; private static final String DATABASE_PRESENCE = "presence_db";
DATABASE_VERSION定义了该数据库的版本号,版本号可以用来判断该数据库是否需要升级,即是否执行onUpgrade方法。
DATABASE_NAME定义了该数据库的名称,也是序列化到磁盘上的文件名称。
DATABASE_PRESENCE是内存数据库的名称,在contacts db中,创建了一份内存数据库,用来存储联系人的实时状态信息,这部分数据需要快速、频繁的操作,且无需序列化,所以采用了内存数据库的方式,创建的代码如下:
db.execSQL("ATTACH DATABASE ':memory:' AS " + DATABASE_PRESENCE + ";");
内存数据库的创建过程定义在onOpen方法中,由于目前没有涉及到StatusUpdates的内容,所以暂时不详细分析这部分内容,但是从这部分内容我们也可以看到,Google还是有想法要把通讯录做成社交化的通讯录,现在已经预留了这些和IM的接口。
二、表结构的定义:
public interface Tables { public static final String CONTACTS = "contacts"; public static final String RAW_CONTACTS = "raw_contacts"; public static final String STREAM_ITEMS = "stream_items"; public static final String STREAM_ITEM_PHOTOS = "stream_item_photos"; public static final String PHOTO_FILES = "photo_files"; public static final String PACKAGES = "packages"; public static final String MIMETYPES = "mimetypes"; public static final String PHONE_LOOKUP = "phone_lookup"; public static final String NAME_LOOKUP = "name_lookup"; public static final String AGGREGATION_EXCEPTIONS = "agg_exceptions"; public static final String SETTINGS = "settings"; public static final String DATA = "data";上述代码只是表名称定义的一部分,读者可以挑选几个自己感兴趣的数据表,分析一下其创建过程,比如raw contact表的创建代码如下:
// Raw_contacts table db.execSQL("CREATE TABLE " + Tables.RAW_CONTACTS + " (" + RawContacts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + RawContactsColumns.ACCOUNT_ID + " INTEGER REFERENCES " + Tables.ACCOUNTS + "(" + AccountsColumns._ID + ")," + RawContacts.SOURCE_ID + " TEXT," + RawContacts.RAW_CONTACT_IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0," + RawContacts.VERSION + " INTEGER NOT NULL DEFAULT 1," + RawContacts.DIRTY + " INTEGER NOT NULL DEFAULT 0," + RawContacts.DELETED + " INTEGER NOT NULL DEFAULT 0," + RawContacts.CONTACT_ID + " INTEGER REFERENCES contacts(_id)," + RawContacts.AGGREGATION_MODE + " INTEGER NOT NULL DEFAULT " + RawContacts.AGGREGATION_MODE_DEFAULT + "," + RawContactsColumns.AGGREGATION_NEEDED + " INTEGER NOT NULL DEFAULT 1," + RawContacts.CUSTOM_RINGTONE + " TEXT," + RawContacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," + RawContacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," + RawContacts.LAST_TIME_CONTACTED + " INTEGER," + RawContacts.STARRED + " INTEGER NOT NULL DEFAULT 0," + RawContacts.DISPLAY_NAME_PRIMARY + " TEXT," + RawContacts.DISPLAY_NAME_ALTERNATIVE + " TEXT," + RawContacts.DISPLAY_NAME_SOURCE + " INTEGER NOT NULL DEFAULT " + DisplayNameSources.UNDEFINED + "," + RawContacts.PHONETIC_NAME + " TEXT," + RawContacts.PHONETIC_NAME_STYLE + " TEXT," + RawContacts.SORT_KEY_PRIMARY + " TEXT COLLATE " + ContactsProvider2.PHONEBOOK_COLLATOR_NAME + "," + RawContacts.SORT_KEY_ALTERNATIVE + " TEXT COLLATE " + ContactsProvider2.PHONEBOOK_COLLATOR_NAME + "," + RawContacts.NAME_VERIFIED + " INTEGER NOT NULL DEFAULT 0," + RawContacts.SYNC1 + " TEXT, " + RawContacts.SYNC2 + " TEXT, " + RawContacts.SYNC3 + " TEXT, " + RawContacts.SYNC4 + " TEXT " + ");"); db.execSQL("CREATE INDEX raw_contacts_contact_id_index ON " + Tables.RAW_CONTACTS + " (" + RawContacts.CONTACT_ID + ");"); db.execSQL("CREATE INDEX raw_contacts_source_id_account_id_index ON " + Tables.RAW_CONTACTS + " (" + RawContacts.SOURCE_ID + ", " + RawContactsColumns.ACCOUNT_ID + ");");从上述代码中,可以发现在raw conacts表上还为字段CONTACT_ID和SOURCE_ID+ACCOUNT_ID创建了索引。
三、 视图的定义;
为了提高查询速度,ContactsProvider提供了部分视图,以下为视图名称定义的代码(位于ContactsDatabaseHelper.java文件中)
public interface Views { public static final String DATA = "view_data"; public static final String RAW_CONTACTS = "view_raw_contacts"; public static final String CONTACTS = "view_contacts"; public static final String ENTITIES = "view_entities"; public static final String RAW_ENTITIES = "view_raw_entities"; public static final String GROUPS = "view_groups"; public static final String DATA_USAGE_STAT = "view_data_usage_stat"; public static final String STREAM_ITEMS = "view_stream_items"; }下面以view_data视图为例,说明其创建过程,所有的视图创建都封装在成员方法createContactsView中,如下:
private void createContactsViews(SQLiteDatabase db) { db.execSQL("DROP VIEW IF EXISTS " + Views.CONTACTS + ";"); db.execSQL("DROP VIEW IF EXISTS " + Views.DATA + ";"); db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_CONTACTS + ";"); db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_ENTITIES + ";"); db.execSQL("DROP VIEW IF EXISTS " + Views.ENTITIES + ";"); db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_USAGE_STAT + ";"); db.execSQL("DROP VIEW IF EXISTS " + Views.STREAM_ITEMS + ";");而view_data视图的创建代码为:
String dataSelect = "SELECT " + DataColumns.CONCRETE_ID + " AS " + Data._ID + "," + Data.RAW_CONTACT_ID + ", " + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", " + syncColumns + ", " + dataColumns + ", " + contactOptionColumns + ", " + contactNameColumns + ", " + baseContactColumns + ", " + buildDisplayPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID, Contacts.PHOTO_URI) + ", " + buildThumbnailPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID, Contacts.PHOTO_THUMBNAIL_URI) + ", " + dbForProfile() + " AS " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + ", " + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID + " FROM " + Tables.DATA + " JOIN " + Tables.MIMETYPES + " ON (" + DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")" + " JOIN " + Tables.RAW_CONTACTS + " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")" + " JOIN " + Tables.ACCOUNTS + " ON (" + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID + ")" + " JOIN " + Tables.CONTACTS + " ON (" + RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")" + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON(" + Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")" + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON (" + DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")" + " LEFT OUTER JOIN " + Tables.GROUPS + " ON (" + MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE + "' AND " + GroupsColumns.CONCRETE_ID + "=" + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")"; db.execSQL("CREATE VIEW " + Views.DATA + " AS " + dataSelect);读者可以仔细阅读一下创建该试图的SQL语句,这里就不详细分析这条SQL语句。基于这个视图我们就能提供快速查询功能,我们来看一下ContactsProvider提供了URI来查询该视图,在UriMatcher中,增加了通过email来查询的URI:
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup", EMAILS_LOOKUP); matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP);咱们可以找到对EMAILS_LOOKUP进行查询处理的逻辑,如下:
case EMAILS_LOOKUP: { setTablesAndProjectionMapForData(qb, uri, projection, false); qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = " + mDbHelper.get().getMimeTypeIdForEmail()); if (uri.getPathSegments().size() > 2) { String email = uri.getLastPathSegment(); String address = mDbHelper.get().extractAddressFromEmailAddress(email); selectionArgs = insertSelectionArg(selectionArgs, address); qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)"); } // unless told otherwise, we'll return visible before invisible contacts if (sortOrder == null) { sortOrder = "(" + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + ") DESC"; } break; }setTablesAndProjectionMapForData主要做SQL语句的封装,下面if语句内的内容是用来准备查询条件,首先取出email字符串,然后利用email值来生成selection条件。代码本身并不复杂,有兴趣的读者可以仔细研读。既然ContactsProvider提供了这样的接口,即通过email获得contact id,那么怎么使用呢,下面给出查询示例:
Uri uri = Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(email)); Cursor c = getContentResolver().query(uri, new String[]{Email.CONTACT_ID, Email.DISPLAY_NAME, Email.DATA}, null, null, null);以上就是通过Email获得该联系人的信息的方法。
四、触发器的定义
触发器的定义在函数createContactsTriggers中,以RAW_CONTACTS表中的触发器为例,看如下代码:
db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_deleted;"); db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_deleted " + " BEFORE DELETE ON " + Tables.RAW_CONTACTS + " BEGIN " + " DELETE FROM " + Tables.DATA + " WHERE " + Data.RAW_CONTACT_ID + "=OLD." + RawContacts._ID + ";" + " DELETE FROM " + Tables.AGGREGATION_EXCEPTIONS + " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 + "=OLD." + RawContacts._ID + " OR " + AggregationExceptions.RAW_CONTACT_ID2 + "=OLD." + RawContacts._ID + ";" + " DELETE FROM " + Tables.VISIBLE_CONTACTS + " WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID + " AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID + " )=1;" + " DELETE FROM " + Tables.DEFAULT_DIRECTORY + " WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID + " AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID + " )=1;" + " DELETE FROM " + Tables.CONTACTS + " WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID + " AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID + " )=1;" + " END");上述代码,在RAW_CONTACTS表上创建了一个触发器,当从RAW_CONTACTS表里删除记录时,会通过触发器自动删除该RAW_CONTACT_ID对应的DATA表、CONTACTS表等其他表中的信息。再来看一个比较重要的触发器:
db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_marked_deleted;"); db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_marked_deleted " + " AFTER UPDATE ON " + Tables.RAW_CONTACTS + " BEGIN " + " UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.VERSION + "=OLD." + RawContacts.VERSION + "+1 " + " WHERE " + RawContacts._ID + "=OLD." + RawContacts._ID + " AND NEW." + RawContacts.DELETED + "!= OLD." + RawContacts.DELETED + ";" + " END");上述的触发器,在RAW_CONTACTS表发生UPDATE时,会将其VERSION加1,其实出发VERSION加1的逻辑还有其他几个,读者可以自行分析,这里的VERSION字段可以用来做数据比较,还是非常有用的。
数据库的基本操作暂时介绍到这,如果您想深入的理解Android联系人模块,这部分代码还是需要掌握的,掌握了基础设施,才有可能了解上层建筑。