因为要制作通知、提醒类的应用程序,所以首先想尝试的是支持自己手机通讯录的生日通知。
这里说的android联系人,指的是存储在安卓手机google账户下面的联系人,而不是存储在本机或者某个国产手机系统的联系人。
android联系人
概况
联系人的数据大致分为16类,分别是
- 电子邮件(地址、电子邮件类型(家庭、工作、手机、其他))。
- 即时消息(协议(qq、icq、skype 等)、im id)。
- 昵称。
- 组织(公司、部门、职务、职位描述、办公地点)。
- 电话(号码、电话类型(家庭、手机、工作、其他))。
- sip地址
- 姓名(显示名称、名字、姓氏)。
- 邮政地址(国家、城市、地区、街道、邮政编码)。
- 身份(命名空间(SSN、护照)、号码)。
- 照片。
- 组(联系人属于组 id)。
- 网站(网站 url,网站类型())。
- 笔记。
- 事件(生日、其它自定义事件)
- 关系
- msic 对于某些组,例如电子邮件、电话、地址等。信息还根据数据使用情况进行分类,例如在家中使用、在工作中使用等。
存储地址
所有 android 联系信息都保存在 SQLite 数据库中。 数据库文件位于 /data/data。 我们可以使用 android模拟器上查看上述数据库文件。 如果无法打开上述文件夹,请在 dos 或 shell 窗口中运行 adb root shell 命令。
adb devices
adb shell
su
sqlite3 /data/data
select * from sqlite_master where type="table"; // 我们可以先查看这个数据库里面有哪些表,太多了
select * from sqlite_master where type="table" and name = "data"; // 查看data表的结构
我们可以看到当时这个表是这么创建的
CREATE TABLE data (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
package_id INTEGER REFERENCES package(_id),
mimetype_id INTEGER REFERENCES mimetype(_id) NOT NULL,
raw_contact_id INTEGER REFERENCES raw_contacts(_id) NOT NULL,
hash_id TEXT,
is_read_only INTEGER NOT NULL DEFAULT 0,
is_primary INTEGER NOT NULL DEFAULT 0,
is_super_primary INTEGER NOT NULL DEFAULT 0,
data_version INTEGER NOT NULL DEFAULT 0,
data1 TEXT,data2 TEXT,data3 TEXT,data4 TEXT,data5 TEXT,data6 TEXT,data7 TEXT,data8 TEXT,data9 TEXT,data10 TEXT,data11 TEXT,data12 TEXT,data13 TEXT,data14 TEXT,data15 TEXT,
data_sync1 TEXT, data_sync2 TEXT, data_sync3 TEXT, data_sync4 TEXT,
carrier_presence INTEGER NOT NULL DEFAULT 0,
preferred_phone_account_component_name TEXT,
preferred_phone_account_id TEXT
)
其中有data1 ~ data15,data_sync1 ~ data_sync4
其实对于我们常用的就是mimetype_id、raw_contact_id 、data1 、data2、data3,数据的类型就是mimetype_id,数据的内容就是data1,而data2、data3则是用来辅助data1,如果mimetype_id对应的是电话号码这组数据,data1就是电话号码,而data2就是电话类型(比如手机,座机)、data3就是自定义的电话类型(比如data2中的分类都不满足,你自定义的紧急电话类型) 数据库中的几个我们能用的表都是依靠相同的raw_contact_id 连接起来的。
select * from sqlite_master where type="table" and name = "mimetypes"; // 查看data表的结构
该表是这么创建的
CREATE TABLE mimetypes (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
mimetype TEXT NOT NULL
)
举个例子
select _id, raw_contact_id,mimetype_id, data1, data2, data3 from data order by _id desc limit 20;
红框中的1267就是一个raw_contact_id ,可以看到这里的6条记录都是我们Test这个联系人的数据: 第一行mimetype_id为5,可以看出是手机号码 第四行mimetype_id为7,可以看出是名字 第六行mimetype_id为13,可以看出是事件,这里存档是日期,根据后面的data2为3可以区分出是生日。 其它的几个是空的,对应的可以根据上面的mimetypes找到对应的含义。
实战
知道了数据的存储结构了,我们就可以开始查询了
我们可用一个fragment来展示联系人列表数据,fragment同时继承接口LoaderManager.LoaderCallbacks<cursor>
lateinit var contactsList: ListView
// Defines a variable for the search string
private val searchString: String = ""
private var lookupKey: String = ""
// An adapter that binds the result cursor to the ListView
private var cursorAdapter: SimpleCursorAdapter? = null
private val detailSelectionArgs = arrayOf<String>("")
private val selectionArgs = arrayOf<String>(searchString)
// 联系人姓名兼容老版本
private val DISPLAY_NAME: String = if >= Build.VERSION_CODES.HONEYCOMB)) {
Con
} else {
Con
}
// 类似于mysql select 对应的数据列
private val PROJECTION: Array<out String> = arrayOf(
Con,
Con,
DISPLAY_NAME,
Con
)
// 类似于mysql的where语句
private val SELECTION: String =
if >= Build.VERSION_CODES.HONEYCOMB)
"${Con} LIKE ?"
else
"${Con} LIKE ?"
// cursor数据对应列
private val FROM_COLUMNS: Array<String> = arrayOf(
DISPLAY_NAME,
Con,
Con
)
// cursor数据对应列对应的展示位置ID
private val TO_IDS: IntArray = intArrayO, R.id.msgTitle, R.id.msgBody)
private var DETAIL_SELECTION: String =
Con + " = ? AND " +
Con + " IN ('" + Con + "', '" + Con + "', '" + Con + "', '" + Con + "')"
private val DETAIL_PROJECTION: Array<out String> = arrayOf(
Con,
Con,
Con,
Con,
Con,
Con,
)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = Fragmen(inflater, container, false)
val root: View = binding.root
contactsList = root.findViewById)
textView = root.findViewById_text)
textView?.text = "loading..."
self = this
// initDataFromMock();
activity?.also {
// Gets a CursorAdapter
cursorAdapter = SimpleCursorAdapter(
it,
R.layout.msg_item, // ListView对应的layout xml文件
null,
FROM_COLUMNS,
TO_IDS,
0
)
// Sets the adapter for the ListView
con = cursorAdapter
}
// Initializes the loader
LoaderManager.getInstance(this).initLoader(2, null, this)
return root
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
/*
* Makes search string into pattern and
* stores it in the selection array
*/
loaderFlag = id
selectionArgs[0] = "%$searchString%"
val mLoader = activity?.let {
CursorLoader(
it,
Con,
PROJECTION,
SELECTION,
selectionArgs,
SORT_ORDER
)
}
// Starts the query
return mLoader ?: throw IllegalStateException()
}
}
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
// Put the result Cursor in the adapter for the ListView
cursorAdapter?.swapCursor(cursor)
// 用进程打印列表数据
Thread {
val userList: ArrayList<User> = arrayListOf();
// 因为列表的查询没有指定搜索条件(searchString为空),查询的是全部数据,那样每个联系会出来好几条数据,可以简单用个hashMap去重下
var userHashMap: HashMap<String, User> = HashMap();
try {
while ()) {
val contactId = cur(0)
val contactKey = cur(1)
val contactName = cur(2)
var currentInfos: MutableList<String?> = ArrayList()
curren("contactName = $contactName")
var userPhoneList : ArrayList<UserPhone> = arrayListOf();
var userEventList : ArrayList<UserEvent> = arrayListOf();
// 我们可以用类似的方式查询该联系对应的具体数据
val cr: ContentResolver = requireActivity().contentResolver
detailSelectionArgs[0] = contactKey!!
val curs: Cursor? = cr.query(Con,
DETAIL_PROJECTION,
DETAIL_SELECTION,
detailSelectionArgs,
null)
if (curs != null) {
while ()) {
val id: Long = cur(0)
val name: String = curs.getString(1) // full name
val mime: String = curs.getString(2) // type of data (phone / birthday / email)
val data: String? = curs.getString(3) // the actual info, e.g. +1-212-555-1234
var type: String? = curs.getString(4);
var label: String? = curs.getString(5);
var kind = "unknown"
when (mime) {
Con -> {
kind = "phone"
u(UserPhone(contactKey, data, type, label))
}
Con -> {
kind = "event"
u(UserEvent(contactKey, data, type, label))
}
Con -> kind = "email"
Con -> kind = "note"
}
if (data != null) {
currentInfos!!.add("$kind = $data/$type/$label")
}
}
Log.i("CURRENT", contactName + "_" + contactId + "_" + curren())
}
val user = User(contactKey, contactId, contactName, userPhoneList, userEventList, null);
if (!u) && u > 0) {
u, user);
u(User(contactKey, contactId, contactName, userPhoneList, userEventList, null))
}
}
} catch (e: Exception) {
Log.e("ABC", "while error $e")
}
Log.i("EFG", u())
if > 0) {
val userListStr = Gson().toJson(userList);
U(requireContext(), "test", "userList", userListStr)
// textView?.text = "list: " + u()
Log.i("LIST", u())
}
}.start()
}
代码有所截取,可能有部分变量什么的遗漏的,但是示意是足够了。
本文启发自 Android Contacts Database Structure Retrieve a list of contacts