前言
今天整理一下关于在Contacts中通过Setting进行Import/Export SIM卡联系人的过程。同样,源码分析基于Android8高通平台。
Import过程
Import过程是指将SIM卡中的联系人导入(copy)到本地账号中,需要强调的一点是,当设备未登陆google账号之前,存入手机本地的联系人的Account为null,如果登陆了google账号,存入本地的联系人的Account变为com.google,并且之前存入的联系人的Account也会变为新的google账号。先简单介绍一下操作手法:打开Contacts.apk ->功能按钮 ->Settings ->Import ->选择SIM card ->勾选需要import的联系人 -> IMPORT ->弹出Toast&发送通知表示copy成功。
代码逻辑如下:
打开Contacts的功能属性页面,点击Settings按钮,点击Import。
final DialogInterface.OnClickListener clickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
final int resId = adapter.getItem(which).mChoiceResourceId;
if (resId == R.string.import_from_sim) {
handleSimImportRequest(adapter.getItem(which).mSim);
} else if (resId == R.string.import_from_vcf_file) {
handleImportRequest(resId, SimCard.NO_SUBSCRIPTION_ID);
} else {
Log.e(TAG, "Unexpected resource: "+ getActivity().getResources().getResourceEntryName(resId));
}
dialog.dismiss();
}
};
会弹出对话框供你选择,从.vcf文件中copy联系人还是从SIM卡中copy,本文主要介绍从SIM卡中copy联系人。
private void handleSimImportRequest(SimCard sim) {
startActivity(new Intent(getActivity(), SimImportActivity.class)
.putExtra(SimImportActivity.EXTRA_SUBSCRIPTION_ID, sim.getSubscriptionId()));
}
在SimImportActivity.class中构建一个fragment(SimImportFragment.java),其中包含个ListView和几个操作功能按钮。ListView设置为多选模式setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);勾选了需要copy的联系人之后,点击Import按钮。
mImportButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
importCurrentSelections();
// Do we wait for import to finish?
getActivity().setResult(Activity.RESULT_OK);
getActivity().finish();
}
});
private void importCurrentSelections() {
final SparseBooleanArray checked = mListView.getCheckedItemPositions();
final ArrayList<SimContact> importableContacts = new ArrayList<>(checked.size());
for (int i = 0; i < checked.size(); i++) {
// It's possible for existing contacts to be "checked" but we only want to import the ones that don't already exist
if (checked.valueAt(i) && !mAdapter.existsInCurrentAccount(i)) {
importableContacts.add(mAdapter.getItem(checked.keyAt(i)));
}
}
SimImportService.startImport(getContext(), mSubscriptionId, importableContacts,
mAccountHeaderPresenter.getCurrentAccount());
}
packages/apps/Contacts/src/com/android/contacts/SimImportService.java
public static void startImport(Context context, int subscriptionId,
ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
context.startService(new Intent(context, SimImportService.class)
.putExtra(EXTRA_SIM_CONTACTS, contacts) //选中的联系人
.putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId)
.putExtra(EXTRA_ACCOUNT, targetAccount));
}
@Override
public int onStartCommand(Intent intent, int flags, final int startId) {
ContactsNotificationChannelsUtil.createDefaultChannel(this);
final ImportTask task = createTaskForIntent(intent, startId);
if (task == null) {
new StopTask(this, startId).executeOnExecutor(mExecutor);
return START_NOT_STICKY;
}
sPending.add(task);
task.executeOnExecutor(mExecutor);
notifyStateChanged();
return START_REDELIVER_INTENT;
}
配置好ImportTask并开始执行
@Override
protected Boolean doInBackground(Void... params) {
final TimingLogger timer = new TimingLogger(TAG, "import");
try {
// Just import them all at once.
// Experimented with using smaller batches (e.g. 25 and 50) so that percentage
// progress could be displayed however this slowed down the import by over a factor
// of 2. If the batch size is over a 100 then most cases will only require a single
// batch so we don't even worry about displaying accurate progress
mDao.importContacts(mContacts, mTargetAccount);
mDao.persistSimState(mSim.withImportedState(true));
timer.addSplit("done");
timer.dumpToLog();
} catch (RemoteException|OperationApplicationException e) {
FeedbackHelper.sendFeedback(SimImportService.this, TAG,
"Failed to import contacts from SIM card", e);
return false;
}
return true;
}
packages/apps/Contacts/src/com/android/contacts/database/SimContactDaoImpl.java
@Override
public ContentProviderResult[] importContacts(List<SimContact> contacts,
AccountWithDataSet targetAccount) throws RemoteException, OperationApplicationException {
if (contacts.size() < IMPORT_MAX_BATCH_SIZE) {
return importBatch(contacts, targetAccount);
}
final List<ContentProviderResult> results = new ArrayList<>();
for (int i = 0; i < contacts.size(); i += IMPORT_MAX_BATCH_SIZE) {
results.addAll(Arrays.asList(importBatch(
contacts.subList(i, Math.min(contacts.size(), i + IMPORT_MAX_BATCH_SIZE)),targetAccount)));
}
return results.toArray(new ContentProviderResult[results.size()]);
}
private ContentProviderResult[] importBatch(List<SimContact> contacts,
AccountWithDataSet targetAccount) throws RemoteException, OperationApplicationException {
final ArrayList<ContentProviderOperation> ops = createImportOperations(contacts, targetAccount);
return mResolver.applyBatch(ContactsContract.AUTHORITY, ops); //执行事务化处理
}
在代码中createImportOperations()方法中具体决定了对数据库执行哪种操作
private ArrayList<ContentProviderOperation> createImportOperations(List<SimContact> contacts,
AccountWithDataSet targetAccount) {
final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
for (SimContact contact : contacts) {
contact.appendCreateContactOperations(ops, targetAccount);
}
return ops;
}
packages/apps/Contacts/src/com/android/contacts/model/SimContact.java
public void appendCreateContactOperations(List<ContentProviderOperation> ops,
AccountWithDataSet targetAccount) {
// There is nothing to save so skip it.
if (!hasName() && !hasPhone() && !hasEmails() && !hasAnrs()) return;
final int rawContactOpIndex = ops.size();
ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) //创建插入数据库操作
.withYieldAllowed(true)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, targetAccount.name)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, targetAccount.type)
.withValue(ContactsContract.RawContacts.DATA_SET, targetAccount.dataSet)
.build());
if (mName != null) {
ops.add(createInsertOp(rawContactOpIndex, StructuredName.CONTENT_ITEM_TYPE,
StructuredName.DISPLAY_NAME, mName));
}
if (!mPhone.isEmpty()) {
ops.add(createInsertOp(rawContactOpIndex, Phone.CONTENT_ITEM_TYPE,
Phone.NUMBER, mPhone));
}
if (mEmails != null) {
for (String email : mEmails) {
ops.add(createInsertOp(rawContactOpIndex, Email.CONTENT_ITEM_TYPE,
Email.ADDRESS, email));
}
}
if (mAnrs != null) {
for (String anr : mAnrs) {
ops.add(createInsertOp(rawContactOpIndex, Phone.CONTENT_ITEM_TYPE,
Phone.NUMBER, anr));
}
}
}
至此,SIM卡中被选中的联系人已经import到本地的联系人账户中,ImportTask执行完毕后,将会有一些通知表明操作是否成功。
packages/apps/Contacts/src/com/android/contacts/SimImportService.java
@Override
protected void onPostExecute(Boolean success) {
super.onPostExecute(success);
stopSelf(mStartId);
Intent result;
final Notification notification;
if (success) {
result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
.putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS)
.putExtra(EXTRA_RESULT_COUNT, mContacts.size())
.putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, mStartTime)
.putExtra(EXTRA_SIM_SUBSCRIPTION_ID, mSim.getSubscriptionId());
notification = getCompletedNotification(); //发送import成功的通知
} else {
result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
.putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE)
.putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, mStartTime)
.putExtra(EXTRA_SIM_SUBSCRIPTION_ID, mSim.getSubscriptionId());
notification = getFailedNotification(); //发送import失败的通知
}
LocalBroadcastManager.getInstance(SimImportService.this).sendBroadcast(result); //发送相关状态的广播
sPending.remove(this);
// Only notify of completion if all the import requests have finished. We're using
// the same notification for imports so in the rare case that a user has started
// multiple imports the notification won't go away until all of them complete.
if (sPending.isEmpty()) {
stopForeground(false);
mNotificationManager.notify(NOTIFICATION_ID, notification);
}
notifyStateChanged();
}
packages/apps/Contacts/src/com/android/contacts/preference/DisplayOptionsPreferenceFragment.java
private class SaveServiceResultListener extends BroadcastReceiver {//接收广播
@Override
public void onReceive(Context context, Intent intent) {
final long now = System.currentTimeMillis();
final long opStart = intent.getLongExtra(
SimImportService.EXTRA_OPERATION_REQUESTED_AT_TIME, now);
// If it's been over 30 seconds the user is likely in a different context so suppress the toast message.
if (now - opStart > 30*1000) return;
final int code = intent.getIntExtra(SimImportService.EXTRA_RESULT_CODE,
SimImportService.RESULT_UNKNOWN);
final int count = intent.getIntExtra(SimImportService.EXTRA_RESULT_COUNT, -1);
if (code == SimImportService.RESULT_SUCCESS && count > 0) {
Snackbar.make(mRootView, getResources().getQuantityString(
R.plurals.sim_import_success_toast_fmt, count, count),
Snackbar.LENGTH_LONG).show(); //弹出import成功的Toast
} else if (code == SimImportService.RESULT_FAILURE) {
Snackbar.make(mRootView, R.string.sim_import_failed_toast,
Snackbar.LENGTH_LONG).show(); //弹出import失败的Toast
}
}
}
Export
是指将本地的联系人Export到SIM卡中,也就是说将设备中的联系人copy到SIM卡中,成功后发送Toast通知用户,照惯例先贴上逻辑流程图:
在点击Export选项之前的逻辑是一样的,我们看看之后的代码:
else if (KEY_EXPORT.equals(prefKey)) {
ExportDialogFragment.show(getFragmentManager(), ContactsPreferenceActivity.class,
ExportDialogFragment.EXPORT_MODE_ALL_CONTACTS);
return true;
}
packages/apps/Contacts/src/com/android/contacts/interactions/ExportDialogFragment.java
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
//......省略的代码
final DialogInterface.OnClickListener clickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
boolean dismissDialog;
final int resId = adapter.getItem(which).mChoiceResourceId;
if (resId == R.string.export_to_vcf_file) { //Export 联系人 to Vcard
dismissDialog = true;
final Intent exportIntent = new Intent(getActivity(), ExportVCardActivity.class);
exportIntent.putExtra(VCardCommonArguments.ARG_CALLING_ACTIVITY,callingActivity);
getActivity().startActivity(exportIntent);
} else if (resId == R.string.share_contacts) { //分享联系人
dismissDialog = true;
if (mExportMode == EXPORT_MODE_FAVORITES) {
doShareFavoriteContacts();
} else { // EXPORT_MODE_ALL_CONTACTS
final Intent exportIntent = new Intent(getActivity(), ShareVCardActivity.class);
exportIntent.putExtra(VCardCommonArguments.ARG_CALLING_ACTIVITY,callingActivity);
getActivity().startActivity(exportIntent);
}
} else if (resId == R.string.export_to_sim) { //Export 联系人 to SIM
dismissDialog = true;
int sub = adapter.getItem(which).mSim.getSubscriptionId();
Intent pickContactIntent = new Intent(
SimContactsConstants.ACTION_MULTI_PICK_CONTACT,Contacts.CONTENT_URI);
pickContactIntent.putExtra("exportSub", sub);
getActivity().startActivity(pickContactIntent);
} else {
dismissDialog = true;
Log.e(TAG, "Unexpected resource: "
+ getActivity().getResources().getResourceEntryName(resId));
}
if (dismissDialog) {
dialog.dismiss();
}
}
};
//......省略的代码
}
点击Export contacts SIM card选项,会进入到选择要导入的联系人页面MultiPickContactsActivity.java,勾选需要Export的联系人后,点击OK按钮:
@Override
public void onClick(View v) {
int id = v.getId();
switch (id) {
case R.id.btn_ok:
if (mPickMode.isSearchMode()) {
exitSearchMode();
}
if (mDelete) {
showDialog(R.id.dialog_delete_contact_confirmation);
} else if(mExportSub > -1) {
new ExportToSimThread().start();
}
//......省略的代码
}
}
public class ExportToSimThread extends Thread {
private int slot; //SIM卡数
private boolean canceled = false;
private int freeSimCount = 0; //SIM还可存储的人数
private ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
private Account account;
final int BATCH_INSERT_NUMBER = 400; //可Export的最大人数
public ExportToSimThread() {
slot = ContactUtils.getActiveSlotId(mContext, mExportSub);
account = ContactUtils.getAcount(mContext, slot);
showExportProgressDialog();
}
@Override
public void run() {
boolean isSimCardFull = false;
// in case export is stopped, record the count of inserted successfully
int insertCount = 0;
freeSimCount = ContactUtils.getSimFreeCount(mContext, slot);
boolean canSaveAnr = ContactUtils.canSaveAnr(mContext, slot); //SIM卡中是否还可再存入Anr
boolean canSaveEmail = ContactUtils.canSaveEmail(mContext, slot); //SIM卡中是否还可再存入Email
int emailCountInOneSimContact = ContactUtils.getOneSimEmailCount(mContext, slot);
int phoneCountInOneSimContact = ContactUtils.getOneSimAnrCount(mContext, slot) + 1;
int emptyAnr = ContactUtils.getSpareAnrCount(mContext, slot);
int emptyEmail = ContactUtils.getSpareEmailCount(mContext, slot);
int emptyNumber = freeSimCount + emptyAnr;
Log.d(TAG, "freeSimCount = " + freeSimCount);
Bundle choiceSet = (Bundle) mChoiceSet.clone(); //获得bundle中的数据
Set<String> set = choiceSet.keySet();//获得bundle中的数据的键值对
Iterator<String> i = set.iterator();
while (i.hasNext() && !canceled) {
String id = String.valueOf(i.next());
String name = "";
ArrayList<String> arrayNumber = new ArrayList<String>();
ArrayList<String> arrayEmail = new ArrayList<String>();
Uri dataUri = Uri.withAppendedPath(
ContentUris.withAppendedId(Contacts.CONTENT_URI,Long.parseLong(id)),
Contacts.Data.CONTENT_DIRECTORY);
final String[] projection = new String[] { Contacts._ID,Contacts.Data.MIMETYPE, Contacts.Data.DATA1, };
Cursor c = mContext.getContentResolver().query(dataUri,projection, null, null, null);
try {
if (c != null && c.moveToFirst()) {
do {
String mimeType = c.getString(1);
if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { //获取name
name = c.getString(2);
}
if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) { //获取number
String number = c.getString(2);
if (!TextUtils.isEmpty(number)&& emptyNumber-- > 0) {
arrayNumber.add(number);
}
}
if (canSaveEmail) {
if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) { //获取Email
String email = c.getString(2);
if (!TextUtils.isEmpty(email)&& emptyEmail-- > 0) {
arrayEmail.add(email);
}
}
}
} while (c.moveToNext());
}
} finally {
if (c != null) {
c.close();
}
}
if (freeSimCount > 0 && 0 == arrayNumber.size()&& 0 == arrayEmail.size()) {
mToastHandler.sendMessage(mToastHandler.obtainMessage(
TOAST_EXPORT_NO_PHONE_OR_EMAIL, name));
continue;
}
int nameCount = (name != null && !name.equals("")) ? 1 : 0;
int groupNumCount = (arrayNumber.size() % phoneCountInOneSimContact) != 0 ?
(arrayNumber.size() / phoneCountInOneSimContact + 1)
: (arrayNumber.size() / phoneCountInOneSimContact);
int groupEmailCount = emailCountInOneSimContact == 0 ? 0
: ((arrayEmail.size() % emailCountInOneSimContact) != 0 ? (arrayEmail
.size() / emailCountInOneSimContact + 1)
: (arrayEmail.size() / emailCountInOneSimContact));
// recalute the group when spare anr is not enough
if (canSaveAnr && emptyAnr >= 0 && emptyAnr <= groupNumCount) {
groupNumCount = arrayNumber.size() - emptyAnr;
}
int groupCount = Math.max(groupEmailCount,
Math.max(nameCount, groupNumCount));
Uri result = null;
for (int k = 0; k < groupCount; k++) {
if (freeSimCount > 0) {
String num = arrayNumber.size() > 0 ? arrayNumber.remove(0) : null;
StringBuilder anrNum = new StringBuilder();
StringBuilder email = new StringBuilder();
if (canSaveAnr) {
for (int j = 1; j < phoneCountInOneSimContact; j++) {
if (arrayNumber.size() > 0 && emptyAnr-- > 0) {
String s = arrayNumber.remove(0);
anrNum.append(s);
anrNum.append(SimContactsConstants.ANR_SEP);
}
}
}
if (canSaveEmail) {
for (int j = 0; j < emailCountInOneSimContact; j++) {
if (arrayEmail.size() > 0) {
String s = arrayEmail.remove(0);
email.append(s);
email.append(SimContactsConstants.EMAIL_SEP);
}
}
}
result = ContactUtils.insertToCard(mContext, name,
num, email.toString(), anrNum.toString(), slot, false); //将选中联系人存入SIM卡中
if (null == result) {
// Failed to insert to SIM card
int anrNumber = 0;
if (!TextUtils.isEmpty(anrNum)) {
anrNumber += anrNum.toString().split(
SimContactsConstants.ANR_SEP).length;
}
emptyAnr += anrNumber;
emptyNumber += anrNumber;
if (!TextUtils.isEmpty(num)) {
emptyNumber++;
}
if (!TextUtils.isEmpty(email)) {
emptyEmail += email.toString().split(SimContactsConstants.EMAIL_SEP).length;
}
mToastHandler.sendMessage(mToastHandler.obtainMessage(
TOAST_SIM_EXPORT_FAILED, new String[] { name, num, email.toString() }));
} else {
insertCount++;
freeSimCount--;
batchInsert(name, num, anrNum.toString(),email.toString()); //同时将数据添加到数据库中
}
} else {
isSimCardFull = true;
mToastHandler.sendMessage(mToastHandler.obtainMessage(TOAST_SIM_CARD_FULL, insertCount, 0));
break;
}
}
if (isSimCardFull) { //如果SIM卡中存储已满,直接break
break;
}
}
if (operationList.size() > 0) {
try {
mContext.getContentResolver().applyBatch(
android.provider.ContactsContract.AUTHORITY,operationList); //批量执行事务
} catch (Exception e) {
Log.e(TAG,String.format("%s: %s", e.toString(),e.getMessage()));
} finally {
operationList.clear();
}
}
if (!isSimCardFull) {
// if canceled, show toast indicating export is interrupted.
if (canceled) {
mToastHandler.sendMessage(mToastHandler.obtainMessage(TOAST_EXPORT_CANCELED,insertCount, 0));
} else {
mToastHandler.sendEmptyMessage(TOAST_EXPORT_FINISHED); //发送Toast提示用户Export完成
}
}
finish();
}
}
ExportToSimThread 线程中主要完成两件事:一将勾选的联系人的信息Copy到SIM卡中;二将这些联系人的设置为SIM卡联系人的格式(仅含有姓名,电话,email,ANR字段且满组字段长度要求),并存入数据库中:
private void batchInsert(String name, String phoneNumber, String anrs,String emailAddresses) {
final String[] emailAddressArray;
final String[] anrArray;
if (!TextUtils.isEmpty(emailAddresses)) {
emailAddressArray = emailAddresses.split(",");
} else {
emailAddressArray = null;
}
if (!TextUtils.isEmpty(anrs)) {
anrArray = anrs.split(SimContactsConstants.ANR_SEP);
} else {
anrArray = null;
}
Log.d(TAG, "insertToPhone: name= " + name + ", phoneNumber= " + phoneNumber
+ ", emails= " + emailAddresses + ", anrs= " + anrs + ", account= " + account);
//创建用于执行插入操作的Builder
ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI);
builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
int ref = operationList.size();
if (account != null) { //Builder中添加Account类型
builder.withValue(RawContacts.ACCOUNT_NAME, account.name);
builder.withValue(RawContacts.ACCOUNT_TYPE, account.type);
}
operationList.add(builder.build()); //Builder添加到operationList事务中
// do not allow empty value insert into database.
if (!TextUtils.isEmpty(name)) { //Builder中添加联系人姓名
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, ref);
builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
builder.withValue(StructuredName.GIVEN_NAME, name);
operationList.add(builder.build()); //Builder添加到operationList事务中
}
if (!TextUtils.isEmpty(phoneNumber)) { //Builder中添加联系人电话
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Phone.RAW_CONTACT_ID, ref);
builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
builder.withValue(Phone.TYPE, Phone.TYPE_MOBILE);
builder.withValue(Phone.NUMBER, phoneNumber);
builder.withValue(Data.IS_PRIMARY, 1);
operationList.add(builder.build()); //Builder添加到operationList事务中
}
if (anrArray != null) {
for (String anr : anrArray) {
if (!TextUtils.isEmpty(anr)) { //Builder中添加联系人another电话
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Phone.RAW_CONTACT_ID, ref);
builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
builder.withValue(Phone.TYPE, Phone.TYPE_HOME);
builder.withValue(Phone.NUMBER, anr);
operationList.add(builder.build()); //Builder添加到operationList事务中
}
}
}
if (emailAddressArray != null) {
for (String emailAddress : emailAddressArray) {
if (!TextUtils.isEmpty(emailAddress)) { //Builder中添加联系人邮箱
builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Email.RAW_CONTACT_ID, ref);
builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
builder.withValue(Email.TYPE, Email.TYPE_MOBILE);
builder.withValue(Email.ADDRESS, emailAddress);
operationList.add(builder.build()); //Builder添加到operationList事务中
}
}
}
if (BATCH_INSERT_NUMBER - operationList.size() < 10) {
try {
mContext.getContentResolver().applyBatch(
android.provider.ContactsContract.AUTHORITY,operationList);
} catch (Exception e) {
Log.e(TAG,String.format("%s: %s", e.toString(),e.getMessage()));
} finally {
operationList.clear();
}
}
}
至此,Import & Export 关于SIM卡的操作分析完成。