前两篇文章主要讲解了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联系人模块,这部分代码还是需要掌握的,掌握了基础设施,才有可能了解上层建筑。