Android contacts 联系人 通讯录 源码 完全解析

  • Android contacts 联系人 通讯录 源码 完全解析
    • 1简介
    • 2软件架构
    • 3各功能模块分析
      • 1联系人数据的显示
        • 1联系人列表显示
        • 2联系人详细信息数据的显示
      • 2联系人数据的编辑和存储
        • 1编辑界面相关
        • 2数据存储相关
      • 3Sim联系人数据的整合
        • 1Sim卡联系人数据的显示
        • 2开机自动导入Sim卡联系人
        • 3telephony中IccProvider浅析
        • 4Sim卡联系人的手动导入导出
      • 4SD卡备份恢复联系人
        • 1从Sd卡恢复import联系人数据
        • 2联系人数据导出到Sd卡
      • 5联系人搜索
      • 6Google联系人同步
      • 7其他零碎功能

1,简介:

本文基于Android4.4.2浅析Contacts及相关模块的功能实现,以及数据库的操作。

本篇博文主要分析contacts,后续会分析contactsProvider。

联系人模块主要记录用户的联系人数据,方便用户快捷的操作和使用,主要包括本机联系人和Sim卡联系人。
本机联系人主要存储在手机内部存储空间,Android平台上是通过数据库进行存储,使用ContentProvider组件封装,提供复杂的字段用于表示联系人数据,并提供用户快捷的操作,比如增加,删除,修改,查询等等。
Sim卡联系人主要存储在Sim卡内部存储文件,包括adn、fdn、sdn。主要提供简单的字段用于表示联系人数据。并通过IccProvider提供的接口进行数据的增加、删除、修改、查询操作。

2,软件架构

联系人Contacts应用主要包括3个部分:
1. Contacts主要响应用户的请求和交互,数据显示。
2. ContactsProvider继承自Android四大组件之一的ContentProvider组件,封装了对底层数据库contact2.db的添删改查。
3. SQLite在底层物理性地存储了联系人数据。

主要交互流程如下图:
架构

Contacts模块的主要7块功能:

这里写图片描述

3,各功能模块分析:

3.1,联系人数据的显示:

1,联系人列表显示:

简要说明
* PeopleActivity类负责联系人列表的显示。
* PeopleActivity包含4个Fragment,每个Fragment包含一个ListView。
* 各个Fragment中ListView的Adapter(BaseAdapter的子类)负责将数据填充到ListView。
* 各个Fragment的Loader类(CursorLoader的子类)负责加载数据。
* 实现LoadertManager接口负责管理这些CursorLoader。

这里写图片描述

为什么使用Loader?
1. Loaders确保所有的cursor操作是异步的,从而排除了UI线程中堵塞的可能性。
2. 当通过LoaderManager来管理,Loaders还可以在activity实例中保持当前的cursor数据,也就是不需要重新查询(比如,当因为横竖屏切换需要重新启动activity时)。
3. 当数据改变时,Loaders可以自动检测底层数据的更新和重新检索。

数据加载流程概览:

这里写图片描述

流程具体分析:

先上图:

这里写图片描述

  1. 进入Contacts应用,程序的主入口Activity是PeopleActivity
    进入onCreate方法:
    createViewsAndFragments(savedState);
    此方法创建视图和Fragments,进入此方法:
mFavoritesFragment = new ContactTileListFragment();
mAllFragment = new DefaultContactBrowseListFragment();
mGroupsFragment = new GroupBrowseListFragment();

发现创建了3个Fragment,分别是 收藏联系人列表、所有联系人列表、群组列表。

  1. 进入DefaultContactBrowseListFragment
    发现DefaultContactBrowseListFragment的祖父类是:
    ContactEntryListFragment<T extends ContactEntryListAdapter>
    首先分析此基类:
    发现此基类实现了LoadManager接口,实现了该接口3个重要的抽象方法:
public Loader<D> onCreateLoader(int id, Bundle args);//创建Loader
public void onLoadFinished(Loader<D> loader, D data);//数据加载完毕后的回调方法
public void onLoaderReset(Loader<D> loader);//数据重新加载

该类同时提供了重要的抽象方法:

protected abstract T createListAdapter();//创建适配器Adapter类。

这意味着,子类可以按需求创造自己的适配器Adapter类,完成各个子界面Listview的数据显示,如3.1节图1所示。

  1. 然后回到DefaultContactBrowseListFragment类:
    在执行onCreateView之前,会执行父类的一些方法,顺序如下:
onAttach()
setContext(activity);
setLoaderManager(super.getLoaderManager());

setLoaderManager中设置当前的LoaderManager实现类。
加载联系人列表数据的过程中,这个类是ProfileandContactsLoader
之后执行onCreate方法。

  1. 进入DefaultContactBrowseListFragmentonCreate(Bundle)方法:
mAdapter = createListAdapter();

发现在这里创建了ListAdapter

DefaultContactListAdapter adapter = 
new DefaultContactListAdapter(getContext());

可以知道创建的ListAdapter类型是DefaultContactListAdapter
并返回到DefaultContactBrowseListFragment类。
执行完onCreate方法之后,
执行DefaultContactBrowseListFragmentonCreateView方法。

  1. 进入DefaultContactBrowseListFragmentonCreateView方法:
mListView = (ListView)mView.findViewById(android.R.id.list);
mListView.setAdapter(mAdapter);

首先获取了ListView用以填充联系人数据,然后设置了适配器,但是此时适配器中的数据是空的,直到后面才会加载数据更新uI。
onCreateView方法执行完之后,在uI可见之前回调执行ActivityonStart方法。

  1. 进入DefaultContactBrowseListFragmentonStart方法:
mContactsPrefs.registerChangeListener(mPreferencesChangeListener);
startLoading();

首先注册了一个ContentObserve的子类监听数据变化。
然后执行startLoading方法,目测这应当就是开始加载数据的方法了!

  1. 进入DefaultContactBrowseListFragmentstartLoading方法:
int partitionCount = mAdapter.getPartitionCount();
for (int i = 0; i < partitionCount; i++) {
……
Partition partition = mAdapter.getPartition(i);
startLoadingDirectoryPartition(i);
……}

Partition这个类持有一个Cursor对象,用来存储数据。
Adapter持有的PartitionPartition类代表了当前需要加载的Directory,可以理解为一个联系人集合,比如说本地联系人、Google联系人……这里我们假设只加载本地联系人数据,所以partitionCount=1。

从这里我们可以做出猜测:
联系人数据不是想象中的分页(每次N条联系人数据)加载,也不是说一次性全部加载,而是一个账户一个账户加载联系人数据,加载完毕一个账户就在uI刷新并显示数据。

  1. 进入DefaultContactBrowseListFragmentstartLoadingDirectoryPartition方法:
loadDirectoryPartition(partitionIndex, partition);

进入此方法:

getLoaderManager().restartLoader(partitionIndex, args, this);

这个方法是LoaderManager实现类的方法,参照文档解释:

这个方法会新建/重启一个当前LoaderManager中的Loader,将回调方法注册给他,并开始加载数据。也就是说会回调LoaderManager的onCreateLoader()方法。
Starts a new or restarts an existing android.content.Loader in this manager, registers the callbacks to it, and (if the activity/fragment is currently started) starts loading it

进入LoadManager接口的实现类:LoaderManagerImpl 的restartLoader方法内部:

LoaderInfo info = mLoaders.get(id);
Create info=
createAndInstallLoader(id, args,  (LoaderManager.LoaderCallbacks<Object>)callback);
//进入createAndInstallLoader方法:
LoaderInfo info = createLoader(id, args, callback);
installLoader(info);
//进入createLoader方法:
LoaderInfo info = new LoaderInfo(id, args,  (LoaderManager.LoaderCallbacks<Object>)callback);
Loader<Object> loader = callback.onCreateLoader(id, args);
//关键方法出现了!LoadManager接口的抽象方法的onCreateLoader方法被回调了!
//然后installLoader方法启动了这个Loader!
info.start();
  1. 进入ContactEntryListFragmentonCreateLoader方法,位于DefaultContactBrowseListFragment的祖父类ContactEntryListFragment中:
CursorLoader loader = createCursorLoader(mContext);//创建Loader
mAdapter.configureLoader(loader, directoryId);//配置Loader

发现在此方法中,首先调用createCursorLoader方法创建了Loader
然后通过configureLoader方法配置Loaderquery方法的查询参数,也就是配置SQL中select查询语句的参数。
这也同时意味着,ContactEntryListFragment类的子类们可以重写createCursorLoader方法以提供适合自身的Loader,重写configureLoader方法为Loader配置合适的参数,适配各种自定义的查询获取数据。

  1. 观察createCursorLoader方法在DefaultContactBrowseListFragment类中实现:
return new ProfileAndContactsLoader(context);

直接返回了DefaultContactBrowseListFragment的数据加载器:ProfileAndContactsLoader
这就是DefaultContactBrowseListFragmentLoader实现类(数据加载器)。

  1. 然后再看一下ProfileAndContactsLoader类是如何加载数据的呢?
    发现它继承自CursorLoader,而CursorLoader又继承自AsyncTaskLoader<D>
    在关键的LoadBackGround()方法中:
    异步调用了ContentResolverquery方法:
Cursor cursor = getContext()
.getContentResolver()
.query(mUri, mProjection, mSelection,mSelectionArgs, mSortOrder, mCancellationSignal);
cursor.registerContentObserver(mObserver);

通过这个Query方法,实现了对联系人数据的查询,返回Cursor数据。并绑定了数据监听器

  1. 那么问题来了
query(mUri, mProjection, mSelection,mSelectionArgs, mSortOrder, mCancellationSignal)

的这些参数那里指定的呢?
configureLoader方法在DefaultContactListAdapter类中实现,实现了对query参数的配置:

configureUri(loader, directoryId, filter);
loader.setProjection(getProjection(false));
configureSelection(loader, directoryId, filter);
loader.setSortOrder(sortOrder);

可以看到,配置了Loader主要的几个参数:UriProjectionSelectionSortOrder
这些参数用于最后和ContactsProvider交互的方法Query方法中……


  1. 最终查询ContactsProvider2uri是:
Uri:content://com.android.contacts/contacts?address_book_index_extras=true&directory=0

发现ContentProvider的服务类似一个网站,uri就是网址,而请求数据的方式类似使用Get方式获取数据。

最后通过ContentProvider2构建的查询语句是这样的:

SELECT 
_id, display_name, agg_presence.mode AS contact_presence, 
contacts_status_updates.status AS contact_status, photo_id, photo_thumb_uri, lookup, 
is_user_profile 
FROM view_contacts 
LEFT OUTER JOIN agg_presence ON (_id = agg_presence.presence_contact_id) LEFT OUTER JOIN 
status_updates contacts_status_updates ON
(status_update_id=contacts_status_updates.status_update_data_id)

可以发现最后通过ContactsProvider2实现的查询,并不是直接查询相关的表(Contacts表、rawcontacts表,data表……),而是直接查询view_contacts视图,因为这样会有更加高的效率。
这也就意味着如果想给联系人数据库新增一个字段供界面使用,仅修改对应的表结构是不行,还要修改对应的视图才能得到想要的效果。

  1. 查询完毕后,回调LoaderManageronLoadFinished方法,完成对Ui界面的更新:
onPartitionLoaded(loaderId, data);

接着进入onPartitionLoaded方法:

mAdapter.changeCursor(partitionIndex, data);

进入这个changeCursor方法:

mPartitions[partition].cursor = cursor;
notifyDataSetChanged();

发现在这里改变了Adapter的数据集Cursor,并发出通知数据已经改变,UI进行更新。

至此,默认联系人数据的显示分析到此结束。

其他Fragment的数据填充基本仍然类似此流程,所不同的只是各自的FragmentAdapterCursorLoader以及CursorLoader配置的参数(uri,projection,selection,args,order……)有所不同。

可以参考下表:

FragmentAdapterCursorLoader
DefaultContactBrowseListFragment(默认联系人列表)DefaultContactListAdapterProfileAndContactsLoader
ContactTitleListFragment(收藏联系人列表)ContactTileAdapterContactTileLoaderFactory StarredLoader
ContactTitleFrequentFragment(常用联系人列表)ContactTitleAdapterContactTileLoaderFactory
FrequentLoader GroupBrowseListFragment(群组列表)GroupBrowseLIstAdapterGroupListLoader
GroupDetailFragment(指定ID群组的联系人列表)GroupMemberTileAdapterGroupMemberLoader
ContactDetailFragment(指定ID联系人信息)ViewAdapterContactLoader

2,联系人详细信息数据的显示:

关键类:

    ContactDetailActivityContactDetailFragment  ContactLoaderFragment //不可见 负责加载联系人详细数据,集成LoadManager对象。ContactLoader   //联系人详细信息Loader。ContactDetailLayoutController     //布局控制类。

原理类似列表显示,如下简要说明:
* ContactLoaderFragment类创建了一个实现LoaderManager.LoaderCallbacks<Contact>接口的对象,数据类型指定为Contacts。负责创建、管理ContactLoader
* 得到当前用户选择的联系人URI,配置对应的ContactLoader
* 后台数据查询分完毕后,回调LoadManageronLoadFinished()方法,并将数据以Contacts的数据类型返回,然后回调ContactDetailLoaderFragmentListeneronDetailsLoaded()方法。
* onDetailsLoaded()方法中,新开一个线程,通过ContactDetailLayoutController类的setContactData(Conatct)设置数据,刷新ContactDetailFragment

3.2,联系人数据的编辑和存储:

1,编辑界面相关:

联系人数据所属的账号不同,加载的UI也是不同的,比如Sim卡联系人一般只有name,phone num,但是本地账号联系人可能就会有email,address,website等信息……
联系人数据UI的加载是通过代码动态加载的,而不是xml文件写死的。

那么问题来了,
新建联系人的界面是如何设计?
这里写图片描述

  1. 先进入新建联系人界面:
    主界面PeopleActivity中点击新建联系人Button,触发onOptionsItemSelected方法中的
    case R.id.menu_add_contact分支:
    执行startActivity(intent);
    startActivity启动Intent,Intent的Action设置为android.intent.action.INSERT
    找到匹配此Action的Activity:ContactEditorActivity

  2. ContactEditorActivity的布局文件:
    ContactEditorActivityonCreate()方法中找到布局:
    setContentView(R.layout.contact_editor_activity);

  3. 在xml文件中找到这个布局:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><fragment class="com.android.contacts.editor.ContactEditorFragment"android:id="@+id/contact_editor_fragment"android:layout_width="match_parent"android:layout_height="match_parent" />
</FrameLayout>

只包含一个Fragment:ContactEditorFragment。程序解析Xml文件到这里就会执行ContactEditorFragment类。

  1. 进入ContactEditorFragmentonCreateView方法:
//展开布局 
final View view
= inflater.inflate(R.layout.contact_editor_fragment, container, false);    
//找到布局中的一个线性布局
//关键的布局是contact_editor_fragment中的一个iD为editors的线性布局!
mContent = (LinearLayout) view.findViewById(R.id.editors);
  1. 找到contact_editor_fragment
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:fillViewport="true"android:fadingEdge="none"android:background="@color/background_primary"
><LinearLayout android:id="@+id/editors"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"/>
</ScrollView>

于是确认ContactEditorFragment的根布局就是一个id为editors的LinearLayout。
想到上一步的语句:

mContent = (LinearLayout) view.findViewById(R.id.editors);

所以关键就在于,接下来在代码中为mContent这个线性布局动态添加地了什么UI,而这些UI才是真正显示的东西。


  1. ContactEditorFragmentonCreateView方法执行完毕之后,会调用onActivityCreate()方法:
if (Intent.ACTION_INSERT.equals(mAction)) 
{
final Account account = mIntentExtras == null ? null : (Account) 
mIntentExtbindEditorsForNewContactras.getParcelable(Intents.Insert.ACCOUNT);
final String dataSet = mIntentExtras == null ? null :mIntentExtras.getString(Intents.Insert.DATA_SET);
if (account != null) {
// Account specified in Intent
createContact(new AccountWithDataSet(account.name, account.type, dataSet));}

上面代码首先取出了当前Account信息,数据信息。封装为一个AccountWithDataSet对象,作为createContact方法的参数。之前我们分析过,编辑界面和账户是高度相关的,所以对UI的动态操作必然和Account对象相关。进入createContact方法。

  1. 看一下ContactEditorFragment中的createContact()到底对界面干了什么!!
    createContact方法中调用了bindEditorsForNewContact(account, accountType):
    关键代码:
……
final RawContact rawContact = new RawContact();if (newAccount != null) {rawContact.setAccount(newAccount);} else {rawContact.setAccountToLocal();}
final ValuesDelta valuesDelta = ValuesDelta.fromAfter(rawContact.getValues());
final RawContactDelta insert = new RawContactDelta(valuesDelta);
……
mState.add(insert);
bindEditors();

发现暂时还是没有对界面做什么事情,任然处于酝酿阶段……

首先使用传入的Accout对象创建一个RawContact对象,然后使用RawContact对象构建了一个RawContactDelta对象insert,接着就将insert对象放入RawContactDeltaList 对象mState中。


RawContact类:raw contacts数据表内的一条数据,表示一个联系人某一特定帐户的信息。存储Data表中一些数据行(电话号码、Email、地址……)的集合及一些其他的信息。
他的存储结构为: HashMap<String, ArrayList<ValuesDelta>>

RawContactDelta类:包含RawContact对象(即一个联系人某一特定帐户的信息),并具有记录修改的功能。

RawContactDeltaList类:内部的存储结构是ArrayList<RawContactDelta>,可以理解为 单个联系人所有账户的数据集合。


  1. 然后调用了bindEditors()法。
    关键代码如下:
……
mContent.removeAllViews();
……
final BaseRawContactEditorView editor;
……
editor = (RawContactEditorView) inflater.inflate(R.layout.raw_contact_editor_view,mContent, false);
//添加视图了……………………
mContent.addView(editor);
//为自定义视图BaseRawContactEditorView设置状态,必然是修改UI的操作!
editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile());

可以看到,mContent这个LinearLayout添加的View是editor,而editor是一个自定义的视图BaseRawContactEditorView,布局是R.layout.raw_contact_editor_view

  1. 找到raw_contact_editor_view布局,发现该布局包含新建联系人页面所有的UI:
    这里写图片描述
<com.android.contacts.editor.RawContactEditorView
    xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:paddingTop="@dimen/editor_padding_top">
<include
用户账户相关UI
        layout="@layout/editor_account_header_with_dropdown" /><LinearLayout
        android:id="@+id/body"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><LinearLayout
            android:layout_height="wrap_content"android:layout_width="match_parent"android:orientation="horizontal"android:paddingTop="8dip"><LinearLayout
                android:layout_height="wrap_content"android:layout_width="0dip"android:layout_weight="1"android:orientation="vertical"><include
            Name相关的UIandroid:id="@+id/edit_name"layout="@layout/structured_name_editor_view" /><include
            拼音名android:id="@+id/edit_phonetic_name"layout="@layout/phonetic_name_editor_view" /></LinearLayout><include
            照片相关的UIandroid:id="@+id/edit_photo"android:layout_marginRight="8dip"android:layout_marginEnd="8dip"layout="@layout/item_photo_editor" /></LinearLayout><LinearLayout
            中间部分Item的显示在此处android:id="@+id/sect_fields"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:layout_marginBottom="16dip"/>添加其他字段 按钮<Button
            android:id="@+id/button_add_field"android:text="@string/add_field"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"android:layout_marginBottom="32dip"/></LinearLayout>
</com.android.contacts.editor.RawContactEditorView>
  1. 那么问题来了:中间的那部分布局(电话、地址……)去哪儿了?
    搜索有可能包含这些内容的线性布局sect_fields,发现在RawContactEditorView类中初始化为mFields
    mFields = (ViewGroup)findViewById(R.id.sect_fields);
    那么只需要看代码中对mFields添加了什么uI!

  2. 回到之前的bindEditors()方法,RawContactEditorView 对象editor从xml中解析完成后,执行了setState方法:

editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile());
  1. 进入RawContactEditorView类,找到setState方法:
public void  setState(RawContactDelta state, AccountType type, ViewIdGenerator vig,boolean isProfile)
……
// 遍历当前账户所有可能的item种类,如电话,姓名,地址……,并分别创建自定义视图KindSectionViewfor (DataKind kind : type.getSortedDataKinds()) {
……final KindSectionView section = (KindSectionView)mInflater.inflate(R.layout.item_kind_section, mFields, false);section.setEnabled(isEnabled());section.setState(kind, state, false, vig);mFields.addView(section);
……
}

手机账户下的imme类型如下:
the mimeType isvnd.android.cursor.item/name
the mimeType is#displayName
the mimeType is#phoneticName
the mimeType isvnd.android.cursor.item/photo
the mimeType isvnd.android.cursor.item/phone_v2
the mimeType isvnd.android.cursor.item/email_v2
the mimeType isvnd.android.cursor.item/postal-address_v2
the mimeType isvnd.android.cursor.item/nickname
the mimeType isvnd.android.cursor.item/organization
the mimeType isvnd.android.cursor.item/note
the mimeType isvnd.android.cursor.item/im
the mimeType isvnd.android.cursor.item/sip_address
the mimeType isvnd.android.cursor.item/group_membership
the mimeType isvnd.android.cursor.item/website
发现遍历了当前账号类型中所有可能的数据类型(DataKind),
创建了相关的自定义视图KindSectionView对象section
再将section对象添加到mFields中显示,
这个mFields正是之前在RawContactEditorView类中初始化的线性布局:

mFields = (ViewGroup)findViewById(R.id.sect_fields)。

到这里,基本可以确定,中间部分(也就是除了Name、Photo 和底部的添加字段Button之外的部分),就是通过这个mFields动态的根据当前账户类型添加编辑的KindSectionView条目来填充的。

首先观察一下KindSectionView的布局文件item_kind_section

<com.android.contacts.editor.KindSectionView
    xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><include                   这是一个TextViewtitleandroid:id="@+id/kind_title_layout"layout="@layout/edit_kind_title" /><LinearLayout            线性布局,用于添加EditTextandroid:id="@+id/kind_editors"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical" /><include                   添加新条目的TextView,初始化状态不可见android:id="@+id/add_field_footer"layout="@layout/edit_add_field" />
</com.android.contacts.editor.KindSectionView>
  1. KindSectionView加载完xml文件之后,会执行onFinishInflate方法:
mTitle = (TextView) findViewById(R.id.kind_title);
mEditors = (ViewGroup) findViewById(R.id.kind_editors); 
mAddFieldFooter = findViewById(R.id.add_field_footer);

把Xml文件中三个主要的部分都得到了,接下来重点就是观察代码中对他们做了什么。

在第12步中,加载完xml文件之后,执行KindSectionViewsetState方法:

section.setState(kind, state, false, vig);

rawContactDelta对象state传递给了KindSectionView类的setState方法:

进入KindSectionView类的setState方法:

mKind = kind;
mState = state;
rebuildFromState();

先进行局部变量的赋值。

  1. 然后进入到rebuildFromState()方法:
  for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {//……遍历当前账户可能的键值对,比如电话、Email、地址……createEditorView(entry);  //这个方法应当是创建EditText的方法!}

在这个方法中,对mState集合中所有Mime类型的ValuesDelta集合(ArrayList<ValuesDelta>类型)进行遍历,而后将每一个 ValuesDelta对象 entry
作为参数调用了createEditorView(entry)也就是创建各个种类的EditText方法,根据entry对象创建相应的EditText
简单说,就是创建mState中存在的类型的EditText
当然……这还都只是猜测,需要进入createEditorView方法确认。

  1. 进入createEditorView方法:
view = mInflater.inflate(layoutResId, mEditors, false);
Editor editor = (Editor) view;
editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);

第13步初始化的mEditors对象(也就是那个被猜测应该是放EditTExt的线性布局)在这里被使用!

  1. 联系上下文,实际上此时editor对象是TextFieldsEditorView类的对象,进入TextFieldsEditorViewsetValues方法,看看他是如何根据entry对象创建EditText的:
public void setValues(DataKind kind, ValuesDelta entry, RawContactDelta state, boolean readOnly,ViewIdGenerator vig) {
int fieldCount = kind.fieldList.size();  //获取所有可能的datakind的总数
for (int index = 0; index < fieldCount; index++)    //遍历所有可能的datakind,
{ 
final EditText fieldView = new EditText(mContext);  //创建EditText对象,之后进行配置
fieldView.setLayoutParams……
fieldView.setTextAppearance(getContext(), android.R.style.TextAppearance_Medium);
fieldView.setHint(field.titleRes);   //EditText的Hint
……     fieldView.addTextChangedListener(new TextWatcher()  //注册TextChangedListener
{@Overridepublic void afterTextChanged(Editable s) {// Trigger event for newly changed valueonFieldChanged(column, s.toString());}
mFields.addView(fieldView);    //将EditText添加到当前的线性布局中!
}

注释基本解释了如何通过一个ValuesDelta(理解为键值对集合)对象entry创建布局中的所有EditText

至此,联系人编辑界面的显示原理基本分析完成。

2,数据存储相关

对联系人数据的操作基本流程:

架构

以新增联系人为例:
基本流程图如下:

这里写图片描述

总结这个流程:
1. 展开编辑界面视图,同时创建相应的RawContactDeltaList对象mState。
2. 将用户输入的联系人信息实时地保存到mState对象中。
3. 用户点击保存按钮,在服务中启动新线程,根据mState中的对象构建ContentProviderOperation数组(理解为构建Sql语句)。
4. 将ContentProviderOperation数组交给ContentResolver处理(理解为执行Sql语句),操作数据库。

代码详细逻辑分析:

第一步,从界面封装数据: RawContactDeltaList 对象mState
1. 联系人编辑界面ContactEditorActivity,输入完毕后点击Save按钮,触发ContactEditorFragment类的save ()方法:重要代码如下:

Intent intent =ContactSaveService.createSaveContactIntent(
mContext,mState,SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),((Activity)mContext).getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED,
mUpdatedPhotos);mContext.startService(intent);

可以看到存储新建联系人是通过ContactSaveServicecreateSaveContactIntent方法开始的,重要的是其中第二个参数是RawContactDeltaList 对象mState,显然这是从Fragment的各个EditText控件返回到ContactSaveService的数据,那问题就是这个mState是如何组织数据的呢????

  1. 首先应该搞清楚这个mState是什么类的对象。
    mStateRawContactDeltaList类的对象,先分析一下这个类是什么数据结构:

RawContactDeltaList类:内部的存储结构是ArrayList<RawContactDelta>,可以理解为 单个联系人所有账户的数据集合。

RawContactDelta类:包含RawContact对象(即一个联系人某一特定帐户的信息)。

RawContact类:raw contacts数据表内的一条数据。存储Data表中一些数据行的集合及一些其他的信息,表示一个联系人某一特定帐户的信息。
所以他的存储结构为: HashMap<String, ArrayList<ValuesDelta>>

ValuesDelta:类似ContentValues的键值对数据结构,是一个HashMap。用来存储data表的数据,key为Mime类型。

  1. 从联系人编辑界面ContactEditorFragment开始:
    在Fragment可见之前会执行onActivityCreate方法:
    方法内调用:
bindEditors();

进入此方法:

int numRawContacts = mState.size();for (int i = 0; i < numRawContacts; i++) {// TODO ensure proper ordering of entities in the listfinal RawContactDelta rawContactDelta = mState.get(i);editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile());

发现他遍历了mState集合
将每一个rawContactDelta对象作为参数传入RawContactEditorView类的setState方法。
实际上如果只是保存单个账户的联系人信息,这里mState内的rawContactDelta对象只会有一个。

  1. 进入RawContactEditorView类的setState此方法:
 public void setState(RawContactDelta state, AccountType type, ViewIdGenerator vig,boolean isProfile) {for (DataKind kind : type.getSortedDataKinds()) {final KindSectionView section = (KindSectionView)mInflater.inflate(R.layout.item_kind_section, mFields, false);section.setEnabled(isEnabled());section.setState(kind, state, false, vig);mFields.addView(section);

可以发现,首先遍历了当前用户账户所有的可能条目种类,

  1. 然后又将rawContactDelta对象传递给了KindSectionView类的setState方法:
mKind = kind;
mState = state;
rebuildFromState();

先进行局部变量的赋值,然后进入到rebuildFromState()方法:

  for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {createEditorView(entry);}

在这个方法中,又对mState集合中所有Mime类型的ValuesDelta集合(ArrayList<ValuesDelta>类型)进行遍历,而后将每一个 ValuesDelta entry对象作为参数调用了createEditorView(也就是创建各个种类的EditText)方法,根据entry对象创建相应的EditText

  1. 进入createEditorView方法:
editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
  1. 联系上下文,实际上此时editor对象是TextFieldsEditorView类,进入TextFieldsEditorViewsetValues方法,看看他是如何根据entry对象创建EditText的:
public void setValues(DataKind kind, ValuesDelta entry, RawContactDelta state, boolean readOnly, ViewIdGenerator vig) {
int fieldCount = kind.fieldList.size();  //获取所有可能的datakind的总数
for (int index = 0; index < fieldCount; index++)//遍历所有可能的datakind
{   
final EditText fieldView = new EditText(mContext);//创建EditText对象,之后进行配置
fieldView.setLayoutParams……
fieldView.setTextAppearance(getContext(), android.R.style.TextAppearance_Medium);
fieldView.setHint(field.titleRes);   //EditText的Hint
……     fieldView.addTextChangedListener(new TextWatcher()  //注册TextChangedListener
{@Overridepublic void afterTextChanged(Editable s) {// Trigger event for newly changed valueonFieldChanged(column, s.toString());}
mFields.addView(fieldView);    //将EditText添加到当前的线性布局中!
}

注释基本解释了如何通过一个ValuesDelta(理解为HashMap)对象entry创建布局中的所有EditText

  1. TextFieldsEditorView
    Ui中每一个EditText绑定了监听器addTextChangedListener,当EditTest内容发生改变时回调onFieldChanged方法:进入此方法:
saveValue(column, value);
  1. TextFieldsEditorViewsaveValue方法:
protected void saveValue(String column, String value) {mEntry.put(column, value);
}

发现这个方法将EditText中用户输入的字符串实时地放到entry这个以当前column 为key的ValueData键值对中。

回顾到第5步中:KindSectionView类中的for循环遍历操作:entrymState集合的一个对象,因此也就是说:当用户编辑EditText的同时,也改变了mState集合。

以上,就是ContactSaveServicecreateSaveContactIntent中第二个关键参数mSTate对象的由来。

第二步,将数据封装为ContactsProviderOperation数组,并提交:
这个对象mState很重要,因为当用户点击保存Button时,
就会启动ContactSaveServicecreateSaveContactIntent方法,开始保存联系人的操作:
1, 进入此方法:

public static Intent createNewRawContactIntent(Context context,
ArrayList<ContentValues> values, AccountWithDataSet account,Class<? extends Activity> callbackActivity, String callbackAction)
{
serviceIntent.putParcelableArrayListExtra(ContactSaveService.EXTRA_CONTENT_VALUES, values);
}

第二个参数values就是上文中分析的mState,可以看到放到了Intent中传递,key是EXTRA_CONTENT_VALUES,后面会通过Intent传递这个对象(已经实现parcelable接口)。

2, 完成上述操作之后,在ContactEditorFragment中会调用startService启动ContactService
此Service继承自IntentService,在单独的Thread执行联系人的添删改查(耗时)操作。
ContactSaveService启动之后,在onHandleIntent(Intent)中对Intent的action进行匹配:

if (ACTION_SAVE_CONTACT.equals(action)) {
saveContact(intent);
CallerInfoCacheUtils.sendUpdateCallerInfoCacheIntent(this);
}

3, 可以发现对存储联系人匹配的分支调用了ContactSaveServicesaveContact方法:

private void saveContact(Intent intent) {
//1,得到之前保存的mState对象,赋值给state
RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
//2,在这里建立ContentProviderOperation操作数组
final ArrayList<ContentProviderOperation> diff=state.buildDiff();
ContentProviderResult[] results = null;
//3,提交给ContentResolver,批量提交操作。
results = resolver.applyBatch(ContactsContract.AUTHORITY,diff);

4, 首先取出Intent中传递过来的mState,执行buildDiff()构造ContentProviderOperation数组diff
那么这段程序的的迷惑之处在于ContentProviderOperation数组是怎么通过一个
RawContactDeltaList对象mState构建的?

5, 进入RawContactDeltaListdiff方法:

for (RawContactDelta delta : this) {
//此处传入的diff是一个空ArrayList<ContentProviderOperation>对象:delta.buildDiff(diff);
}

在for循环中遍历了this对象,也就是mState对象,取出了其中所有的RawContactDelta对象,如果只是保存到一个账户,这里的RawContactDelta对象只会有一个。
然后调用delta.buildDiff(diff)diff是一个空ArrayList<ContentProviderOperation>对象,并且此时是空对象。

6, 进入RawContactDeltabuildDiff(ArrayList<ContentProviderOperation> buildInto)方法:

for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {for (ValuesDelta child : mimeEntries) {//根据child键值对构建builder对象builder = child.buildDiff(Data.CONTENT_URI);//构造ContentProviderOperation数组! possibleAdd(buildInto, builder);  }
}

有两层for循环,完成了对ContentProviderOperation数组的构建。
外层的循环mEntries对象包含了被保存的联系人所有的数据,本身的数据结构是HashMap<String, ArrayList<ValuesDelta>>
此时mEntries的数据类似这样:
可以观察到,mEntries的key是mime类型,value是对应的data值组成的ArrayList

{
vnd.android.cursor.item/phone_v2=[{ IdColumn=_id, FromTemplate=false, is_super_primary=0, data_version=0, mimetype=vnd.android.cursor.item/phone_v2, data2=2, is_primary=0, _id=593, data1=1111-111-1111, }],vnd.android.cursor.item/postal-address_v2=[{ IdColumn=_id, FromTemplate=false,data_version=0, mimetype=vnd.android.cursor.item/postal-address_v2, _id=594, 
data1=ManchesterUnitid, is_super_primary=0, data4=ManchesterUnitid, data2=1, 
is_primary=0, }],
}

7, 最终在内层循环调用了ValuesDeltabuildDiff方法,参数targetUri=Data.CONTENT_URI

public ContentProviderOperation.Builder buildDiff(Uri targetUri) 
{ContentProviderOperation.Builder builder = null;if (isInsert()) {mAfter.remove(mIdColumn);
//创建builder对象,关联目标table,这里是Data表builder = ContentProviderOperation.newInsert(targetUri);
//配置builder参数builder.withValues(mAfter);}
//返回
return builder;
}

这里用到了ContentProviderOperation的内部类Builder,那Builder类是什么?
简单说来就是为ContentProviderOperation数组的构建提供服务的。

一个ContentProviderOperation对象构建的基本流程就是这样的:
* ContentProviderOperation.newInsert(targetUri) 创建builder对象
* Builder.withValues(ContentValues values) 按照ContentValues构建Builder的参数
* 最后调用Builderbuild()方法返回一个ContentProviderOperation对象!

Builder简介:
Used to add parameters to a ContentProviderOperation. The Builder is first created by calling ContentProviderOperation.newInsert(android.net.Uri),
ContentProviderOperation.newUpdate(android.net.Uri),
ContentProviderOperation.newDelete(android.net.Uri)
or ContentProviderOperation.newAssertQuery(Uri).
The withXXX methods can then be used to add parameters to the builder. See the specific methods to find for which Builder type each is allowed. Call build to create the ContentProviderOperation once all the parameters have been supplied.

8, 然后看方法体内执行:
可以看到执行了ContentProviderOperation的静态方法:newInsert(targetUri),完成了对ContentProviderOperation.builder的创建,然后使用builder.withValues(mAfter)完成了对builder添加的参数。

9, 方法返回后回到第六步:

//根据child键值对构建builder对象
builder = child.buildDiff(Data.CONTENT_URI); 
// 构造ContentProviderOperation数组!  
possibleAdd(buildInto, builder); 

发现构建builder对象并在其中配置参数之后,马上执行了possibleAdd方法。

10, 进入possibleAdd方法:

  private void possibleAdd(ArrayList<ContentProviderOperation> diff,ContentProviderOperation.Builder builder) {if (builder != null) {//在这里构建了ContactsProviderOperation数组!diff.add(builder.build());   }}

经过以上分析,
经过层层遍历,完成了ContentProviderOperation数组的构造,
这时候构建完毕的ContentProviderOperation数组diff是类似这样的:

[
mType: 1, mUri: content://com.android.contacts/raw_contacts, 
mSelection: null, mExpectedCount: null, mYieldAllowed: false, mValues: aggregation_mode=2 data_set=null account_type=null account_name=null, mValuesBackReferences: null,
mSelectionArgsBackReferences: null, mType: 1, mUri: content://com.android.contacts/data, mSelection: null, mExpectedCount: null, mYieldAllowed: false, mValues: data2=2 mimetype=vnd.android.cursor.item/phone_v2
data1=1111-111-1111, mValuesBackReferences: raw_contact_id=0, mSelectionArgsBackReferences: null, mType: 1, mUri: content://com.android.contacts/data, mSelection: null, mExpectedCount: null, mYieldAllowed: false, mValues: data2=1 
mimetype=vnd.android.cursor.item/postal-address_v2
data1=ManchesterUnitid, mValuesBackReferences: raw_contact_id=0, 
mSelectionArgsBackReferences: null, 
]

11, 程序返回到第3步
最初的ContactSaveService类:

回到saveContact方法,将构造完毕ContentProviderOperation数组diff作为参数,调了用contentResolver
applyBatch(ContactsContract.AUTHORITY,diff)提交修改……
基本流程就是到这里就OK了,接下去就是ContatcsProvider与数据库的操作了。

与ContactsProvider2、数据库的交互

1, 之后深入Contacts的数据操作层ContactsProvider2

然后追溯ContentResolverapplyBatch()方法:

ContentProviderClient provider = acquireContentProviderClient(authority);
return provider.applyBatch(operations);

2, 根据authority参数,可以知道acquireContentProviderClient方法返回的providerContactsProvider2,所以之后调用了ContactsProvider2applyBatch方法:

return super.applyBatch(operations);

3, 调用了父类AbstractContentProvider中的applyBatch方法:

final int numOperations = operations.size();
final ContentProviderResult[] results =new ContentProviderResult[numOperations];
for (int i = 0; i < numOperations; i++) {
results[i] = operation.apply(this, results, i);
}

4, 发现最终执行了ContentProviderOperation.apply()方法:

if (mType == TYPE_INSERT) {Uri newUri = provider.insert(mUri, values);         }if (mType == TYPE_DELETE) {numRows = provider.delete(mUri, mSelection, selectionArgs);
} else if (mType == TYPE_UPDATE) {numRows = provider.update(mUri, values, mSelection, selectionArgs);} else if (mType == TYPE_ASSERT) {
}

我们执行的新增联系人操作,也就是Insert操作。

5, 因此进入Insert分支,调用了ContentProvider2insert方法:

Uri result = insertInTransaction(uri, values);

6, 追踪到ContactsProvider2.insertInTransaction(Uri uri, ContentValues values)方法:
实现了对URI的匹配,确定执行对哪个数据库的进行插入操作,如果uri是对Data表的操作:

//匹配URI对应的Insert操作表:case DATA:case PROFILE_DATA: {invalidateFastScrollingIndexCache();id = insertData(values, callerIsSyncAdapter);mSyncToNetwork |= !callerIsSyncAdapter;break;}

7, 最后ContactsProvider2. insertData方法实现了对底层数据库的直接操作:

final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
//根据mime类型获取合适 的DataRowHandler类对象
DataRowHandler rowHandler = getDataRowHandler(mimeType);
//使用合适的DataRowHandler对象直接对数据库执行操作
id = rowHandler.insert(db, mTransactionContext.get(), rawContactId, mValues);
return id;

至此,一个联系人的插入操作分析完毕。


添删改查操作的基本流程都类似。
值得注意的是删除联系人并不是真正的删除联系人数据。
用户在联系人列表选择联系人的删除,本地联系人url匹配只是删除contacts表中的数据,标记raw_contacts表的字段deletde为1,而Data表的数据并没有发生变化。url匹配删除Sim卡联系人或者同步联系人时删除,会直接删除raw_contacts表的数据,并触发触发器raw_contacts_deleted,将data表,agg_exceptions表,contacts表的数据全部删除。
当用户进入到联系人编辑界面,删除某个数据。也就是只对联系人的data数据进行删除,而联系人数据未发生变化,这样会根据删除内容获得ContentProviderOperation数组。最后会调用applyBatch()函数进行数据更新。
调用applyBatch()函数过程中,会读取ContentProviderOperation数组,而数组的每一条记录都会带有一个URI,通过匹配URI,找到对应的表进行删除操作。操作成功后得到返回结果。
最后根据mimetype类型数据,获得不同的DataRowHandler,进行data数据的删除。




3.3,Sim联系人数据的整合

实时获得Sim卡的状态,对Sim上的联系人导入到本地数据库,或者将本地数据中Sim卡联系人删除。数据库Contacts表和raw_Contacts表表中有字段indicate_phone_or_sim_contact表示是否为Sim卡联系人,并区分出Sim1,Sim2上的联系人。
Mtk平台中实现了开机自动导入SIm卡联系人数据的功能。

1,Sim卡联系人数据的显示:

  • SimContacts类负责显示Sim卡中的联系人数据,并与用户交互。
  • AdnList负责与Sim卡交互,SimaContacts继承自AdnList,而AdnList继承自ListActivity

这里写图片描述

如何交互Sim卡数据?
这里主要是简单应用层的操作,IccProvider屏蔽了sim卡内部复杂的操作。
使用一个继承自AsyncHandlerQueryHandler类封装了异步查询操作:

AsyncHandler类的定义

说明:A helper class to help make handling asynchronous ContentResolver queries easier.
ContentResolver异步查询操作的帮助类,其实它同样可以处理增删改。

AsyncQueryHandler的作用

查询其API便可知,它担供:

startInsert
startDelete
startUpdate
startQuery

这四个操作,并提供相对应的onXXXComplete方法,以供操作完数据库后进行其它的操作,这四个 onXXXComplete方法都是抽象方法,可以在子类中实现想要的操作,在本例中是使用QueryHandler类实现了这几个方法,查询完毕后将数据填充到ListView中。

为什么要使用AsyncQueryHandler
当然也可以使用ContentProvider去操作数据库。这在数据量很小的时候是没有问题的,但是如果数据量大了,可能导致UI线程发生ANR事件。当然你也可以写个Handler去做这些操作,只是你每次使用ContentProvider时都要再写个Handler,必然降低了效率。
因此API提供了一个操作数据库的通用方法。

如何使用AsyncQueryHandler
指定需要查询的数据的URI,与ContentResolver的query方法中的参数并无太大区别。

本例中查询Sim卡联系人数据的的uri是:

Uri.parse("content://icc/adn");

然后调用查询方法:

  mQueryHandler.startQuery(QUERY_TOKEN, null, uri, COLUMN_NAMES,null, null, null);

2,开机自动导入Sim卡联系人:

先上流程图:
这里写图片描述

具体分析:
1. 注册一个BootCmpReceiver检测开机广播事件,被触发之后启动SIMProcessorService
2. SIMProcessorService继承service组件,主要实例化了SIMProcessorManager对象,注册了ProcessorManagerListener监听器。接受到BootCmpReceiver发送过来的处理Intent之后,调用SIMProcessorManagerhandleProcessor方法。
3. SIMProcessorManagerhandleProcessor方法根据传递过来的Intent,在createProcessor方法中创建相应的Processor,比如SIMImportProcessorSIMRemoveProcessorSIMEditProcessorSIMDeleteProcessor。可以观察到有导入SIm卡数据、移除Sim卡数据、编辑Sim卡数据、删除Sim卡数据。
4. 本次操作为导入SIm卡联系人,所以创建的Processor为SIMImportProcessor,发现其基类为ProcessorBase,实现了Runnable接口,因此SIMImportProcessor类可以理解为Threadtarget,其run()方法是线程执行体。
5. SIMImportProcessorrun()方法实现了什么功能?首先调用ContentResolverquery方法,指定uri为SIm卡联系人数据的uri,并进行查询操作(通过匹配Authority可以得知这里调用的其实是IccProvider类),得到联系人数据游标Cursor对象。这一步完成了Sim卡联系人数据的读取。
6. 然后再执行importAllSimContacts方法,构建ContentProviderOperation数组OperatioList,通过ContentResolverapplyBatch(uri, OperatioList)方法批量提交对IccProvider的操作,也就是对数据库的操作,。这一步完成了Sim卡联系人数据写入到Sqlite。
7. 那么,这个线程池是什么时候启动的呢?在第三步的createProcessor方法之后,将创建的Processor添加到ProcessorManagerProcessorManagerListener监听器会执行Excute方法执行被添加的Processor其内部机制在线程池中执行Processor类。

3,telephony中IccProvider浅析:

预备知识:
Sim卡中存储的号码的类型:
ADN: Abbreviated dialing number, 就是常规的用户号码,用户可以存储/删除。
FDN:Fixed dialer number,固定拨号,固定拨号功能让您设置话机的使用限制,当您开启固定拨号功能后,您只可以拨打存储的固定拨号列表中的号码。固定号码表存放在SIM卡中。能否使用固定拨号功能取决于SIM卡类型以及网络商是否提供此功能。
SDN:Service dialing number,系统拨叫号码,网络服务拨号,固化的用户不能编辑。

从以上的描述,可以看到,一般情况下访问SIM卡联系人数据就是访问ADN。

时序图:

这里写图片描述

上述第二节,第五步执行ContentResolverquery方法时,根据Authority可以得知匹配的ContentProviderIccProvider

  1. IccProviderQuery方法中,会执行loadFromEf方法。

  2. loadFromEf中,要先得到一个IIccPhoneBook对象:

IIccPhoneBook iccIpb =
IIccPhoneBook.Stub.asInterface(ServiceManager.getService("simphonebook");

发现这个对象是用AIDL接口来获取到的,
那么 ServiceManager.getService("simphonebook") 究竟获取了一个什么实体对象呢?

  1. 先不着急找到这个实体对象,
    发现程序之后在ICCProvider中调用Stub的实体类的getAdnRecordsInEf方法:
adnRecords = iccIpb.getAdnRecordsInEf(efType);

这是通过AIDL接口实现的方法调用,最终是调用到了Stub实体类的getAdnRecordsInEf方法.
那么可以知道,这个实体类首先必然存在getAdnRecordsInEf()方法,这个实体类的对象是通过ServiceManager.getService来获取的,那么找到addService的地方就可以发现它了。

  1. 全局搜索后,发现IccPhoneBookInterfaceManagerProxy类符合需求:在他的构造函数中执行了addService()方法,而且存在getAdnRecordsInEf方法,
    判断IccPhoneBookInterfaceManagerProxy类就是上文中的Stub实体类。

IccPhoneBookInterfaceManagerProxy类继承了IIccPhoneBook.Stub
在它的构造函数中执行了addService方法:

String serviceName = PhoneFactory.getServiceName("simphonebook", phoneId);
if(ServiceManager.getService(serviceName) == null) {ServiceManager.addService(serviceName, this);
}

addService方法传入参数为当前IccPhoneBookInterfaceManagerProxy类的对象,
因此,在IIccPhoneBook iccIpb =IIccPhoneBook.Stub.asInterface(ServiceManager.getService("simphonebook");
是获得的就是IccPhoneBookInterfaceManagerProxy类的对象。

  1. 那么在前面第3步提到的ICCProvider类中iccIpb.getAdnRecordsInEf方法实际就调用到了IccPhoneBookInterfaceManagerProxy类的getAdnRecordsInEf方法。
    getAdnRecordsInEf方法中,执行:
mIccPhoneBookInterfaceManager.getAdnRecordsInEf(efid);

看到getAdnRecordsInEf这个方法名就可以知道,这个方法是获取Sim卡内Adn类型联系人数据的方法。

  1. IccPhoneBookInterfaceManager中实现了getAdnRecordsInEf方法
    getAdnRecordsInEf方法中,执行:
adnCache.requestLoadAllAdnLike(efid, adnCache.extensionEfForEf(efid), response);

adnCache是类AdnRecordCache的对象
7. AdnRecordCache
在类AdnRecordCache中实现了requestLoadAllAdnLike方法,
requestLoadAllAdnLike中,执行:

new AdnRecordLoader(mFh).loadAllFromEF
(efid, extensionEf,obtainMessage(EVENT_LOAD_ALL_ADN_LIKE_DONE, efid, 0))

这里实例化一个类AdnRecordLoader的对象,并且调用该对象的loadAllFromEF方法

  1. 进入AdnRecordLoader
    在类AdnRecordLoader中实现了loadAllFromEF方法,
    loadAllFromEF方法中,执行:
mFh.loadEFLinearFixedAll(ef, obtainMessage(EVENT_ADN_LOAD_ALL_DONE));

mFh是类IccFileHandler的对象,实际上是它的子类TDUSIMFileHandler的对象,继承关系是:TDUSIMFileHandler继承自SIMFileHandler继承自IccFileHandler
9. 进入IccFileHandler
在类IccFileHandler中实现了loadEFLinearFixedAll方法,
loadEFLinearFixedAll方法中,执行:

mCi.iccIOForApp(COMMAND_GET_RESPONSE,fileid, getEFPath(fileid, is7FFF),
0, 0, GET_RESPONSE_EF_SIZE_BYTES,null, null, mAid, response);

联系上下文,mCiRIL类的对象,RILRadio Interface Layer的缩写,即无线接口通信层

之后涉及的东西比较底层……以后再慢慢分析……

  1. 经过与底层数据的交互,
    可以在IccPhoneBookInterfaceManager类的IccPhoneBookInterfaceManager方法返回得到Sim卡Adn联系人数据List<AndRecord>数据。
  2. 返回IccProviderloadFromEf方法:
for (int i = 0; i < size; i++) {
loadRecord(adnRecords.get(i), cursor, i);
}

发现遍历了List<AndRecord>中的数据,放到cursor对象中,最后返回这个cursor对象,也就是返回给了最初IccProvider调用的query方法返回的Cursor对象。

4,Sim卡联系人的手动导入导出:

导入的基本流程与开机导入Sim卡联系人类似,同样是先query得到SIM卡联系人数据,然后写入联系人数据库,不再做分析。
导出流程就是反过来……

华为的需求:手机联系人详情界面增加一个导出到卡1/卡2/Exchange账户的optionMenu。
具体做的时候完全可以走SIm卡联系人导入导出的流程,只需要指定导入导出数据的uri即可。

3.4,SD卡备份/恢复联系人

SD卡导入导出主要是通过vCard的形式,存储到sd卡或者从sd卡读取指定的vCard文件并进行解析。

1,从Sd卡恢复/import联系人数据

从Sd卡导入联系人主要流程:
1. 联系人主界面PeopleActivity响应选项菜单的onOptionsItemSelected事件。
2. 弹出Import/Export对话框ImportExportDialogFragment中选择Import,启动导入界面ImportVCardActivity.
3. ImportVCardActivity最终通过startService启动VCardService服务。
4. ImportVCardActivity启动VCardCacheThread来进行将Vcard文件从外部Sd卡复制到手机内部存储,然后构建Importrequests数组,该数组封装了被导入Vcard文件的信息。
5. 通过调用VCardServiceHandleImportRequest通知VCardService导入VCard。
6. VCardService启动ImportProcessor线程,通过实现的一个Vcard文件解析类VCardEntryConstructor类,第4步构建的Importrequests数组作为参数,依次导入VCard中的每个联系人(readOneVCard方法)。
7. 在VCardService处理导入的过程中,会把过程状态通知给NotificationImportExportListener,后者负责更新通知状态栏中的信息。

visio时序图如下:

这里写图片描述

2,联系人数据导出到Sd卡

联系人导出到Sd卡与导入流程类似,略。

3.5,联系人搜索:

在Contacts应用内搜索联系人, 主要步骤:
1. 在主界面PeopleActivity点击联系人搜索按钮,触发onAction方法。
2. 调用restartLoader来启动Loader异步更新数据。
3. 在LoadeManager的回调接口onCreateLoader创建、配置Loader,包括查询的Uri等,此阶段配置的uri参数为Contacts.CONTENT_FILTER_URI
4. Loader启动后调用ContactProvider2query方法,
匹配的uri为Contacts.CONTENT_FILTER_URI的分支。
5. 在ContactProvider2appendSearchIndexJoin方法中拼接Sql语句,并调用Sqlite的底层query语句查询。
6. 完成查询后回调LoaderManageronLoaderFinish刷新UI。

visio时序图如下:
这里写图片描述

搜索tan字符串的时候,sql语句为:

SELECT _id, display_name, agg_presence.mode AS contact_presence, contacts_status_updates.status AS contact_status, photo_id, photo_thumb_uri, lookup, is_user_profile, snippet 
FROM view_contactsJOIN (SELECT contact_id AS snippet_contact_id, content AS snippet FROM search_index WHERE search_index 
MATCH 'content:tan* OR name:B791AB* OR tokens:tan*' 
AND snippet_contact_id IN default_directory) ON (_id=snippet_contact_id) LEFT OUTER JOIN agg_presence ON (_id = agg_presence.presence_contact_id) LEFT OUTER JOIN status_updates contacts_status_updates ON (status_update_id=contacts_status_updates.status_update_data_id) ORDER BY phonebook_bucket, sort_key

发现其实最后经过sqlite语句的拼接,查询的是view_contacts视图。

android联系人的搜索机制如下:
当新建了一个联系人的时候,例如名字为:【abcd】,那么会在raw_contacts表的对应数据中的display_name显示【abcd】,同时在insert的时候会在name_lookup表中存储进去一个normallized_name字段,这个字段是根据名字【abcd】转换成的16进制码,使用的方法是NameNormalize.normalize()方法。

在查询的时候使用的是ContactsProvider2里面的query方法,当输入查询条件时【a】,会使用NameNormalize.normalize()方法将【a】转换成16进制码,然后再进入name_lookup中去查询对应的raw_contacts_id,从而对应出contact显示在界面上。

也就是说,google 的查询并不是根据display_name来进行的,而是通过转换的normallized_name来进行匹配查询,在NameNormalizer.java文件中定义了normalize方法,这个方法是进行转换的方法,对于数字和字母,做了直接转换的处
理,对于一些特殊字符做了特别的处理,举例如下:
如果输入的是【,】【.】那么google会将这种字符当作分隔符,即输入【a,b,c.d】的话,名字就是【a,b,c.d】,在处理这个名字的时候首先按照【,】【.】来进行分割,分割成【a】【b】【c】【d】后再转换成lookup条件,那么此时在查询的时候输入了【a】,匹配到【a,b,c.d】,再输入【,】时,系统会认为输入的是分隔符,那么会认为用户想要查询结果的基础上再次进行查询,也就是常说的在搜索结果中继续查询,所以此时再输入【a】的时候系统就会认为是在上一次的结果中(【a,b,c.d】)再此查询【a】,那么还是可以匹配到【a,b,c.d】,所以造成了下面的现象:
1.输入【a,a】/【a,c】/【d,d】/…..
2.查询出结果【a,b,c.d】.
而对于一些其他的特殊字符(非数字,非字符),如【@】【_】等等,在转换的时候会自动将这些字符过滤掉,但却保留了分割的特性,即
出现了如下的现象:
1.保存联系人名称为【first@second#three】
2.输入条件【firstsecond】,结果为:【first@second#three】
3.输入条件【three,second】,结果为:【first@second#three】(因为保留的分割特性)
4.输入条件【first@se】,无结果(因为转换时去掉了字符@)
上述即为google default对于查询的机制,关于转换的代码可以在NameNormalizer.java中进行分析。

3.6,Google联系人同步

通过添加Google帐号,并开启同步,则会将Gmail中联系人同步到本地数据库,也可以将本地联系人同步到Gmail中。而且也支持Exchange服务帐号同步。

3.7,其他零碎功能:

1,联系人分享
关键类:
ContactDetailActivity:联系人详细信息显示界面。
ContactLoaderFragment:被包含于Aty中的Fragment。
ContactDetailLayoutController:控制布局,填充数据。

分享联系人的实现步骤:

* 在联系人详细信息界面,选择分享。
* 得到当前联系人的uri
* 设置Intent属性、携带指定联系的uri:

        final Intent intent = new Intent(Intent.ACTION_SEND);intent.setType(Contacts.CONTENT_VCARD_TYPE);intent.putExtra(Intent.EXTRA_STREAM, shareUri);
  • 创建Intent选择器:蓝牙/email/Nfc/其他应用……
final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
  • 跳转到用户选择的分享联系人方式的应用(蓝牙/Nfc/Email/其他应用……),联系人数据通过intent传递。

2,桌面快捷方式和文件夹

createLauncherShortcutWithContact()ShortcutIntentBuilder

3,联系人字符分组、字母表导航效果实现机制:
关键问题:需要知道联系人名字的首字母。
把中文转换为拼音字符,这样就可以实现排序,按照字母导航的效果。
发现在rawContacts表中:
这里写图片描述

发现Android已经在Sqlite中自动实现了 汉字-拼音 转换功能,直接读取sort_key这个列的数据就可以。
1,得到联系人数据,并按照sort_key排序,通过listview显示。
2,用户拖动滑动块时显示字母提示框(A_Z)。

上面的实现都比较简单,问题是sort_key是如何自动生成的?
HanziToPinyin.javaContactsProvider2下,负责将汉字转化为拼音

4,联系人侧边栏字母导航条如何实现?

Android L中contact应用是没有侧边栏的,但是字母导航的数据仍然是可以读到,我们只需要搞个自定义控件,画出A-Z的字母导航条,并监测触摸事件,在Contacts中的listviewsetSelection点击的字母位置就可以。


public class SideBar extends View {public static String[] b = { "#", "A", "B", "C", "D", "E", "F", "G","H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T","U", "V", "W", "X", "Y", "Z"};
protected void onDraw(Canvas canvas) {int height = getHeight()-20;int width = getWidth();int singleHeight = height / index.length;for (int i = 0; i < index.length; i++) {paint.setColor(android.graphics.Color.parseColor("#b6b6b6"));paint.setTypeface(Typeface.DEFAULT_BOLD);paint.setAntiAlias(true);paint.setTextSize(20);if (i == choose) {paint.setColor(Color.parseColor("#28c0c6"));paint.setFakeBoldText(true);}float xPos = width / 2 - paint.measureText(index[i]) / 2;float yPos = singleHeight * i + singleHeight;canvas.drawText(index[i], xPos, yPos, paint);paint.reset();}}@Overridepublic boolean dispatchTouchEvent(MotionEvent event) {final int action = event.getAction();final float y = event.getY();final int oldChoose = choose;final OnTouchingLetterChangedListener listener = onTouchingLetterChangedListener;final int c = (int) (y / getHeight() * index.length);if (event.getX() > getWidth()) {setBackgroundDrawable(new ColorDrawable(0x00000000));choose = -1;invalidate();if (mTextDialog != null) {mTextDialog.setVisibility(View.INVISIBLE);}return true;}switch (action) {case MotionEvent.ACTION_UP:setBackgroundDrawable(new ColorDrawable(0x00000000));choose = -1;//invalidate();if (mTextDialog != null) {mTextDialog.setVisibility(View.INVISIBLE);}break;default:
//          setBackgroundResource(R.drawable.sidebar_background);if (oldChoose != c) {if (c >= 0 && c < index.length) {if (listener != null) {listener.onTouchingLetterChanged(index[c]);}if (mTextDialog != null) {mTextDialog.setText(index[c]);mTextDialog.setVisibility(View.VISIBLE);}choose = c;invalidate();}}break;}return true;}}

至此contacts应用层的分析基本结束,后面会再写数据层contactsProvider2的分析。

ContactsProvider2 解析

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. SeetaFace开源人脸识别引擎介绍(转)

    原文地址:https://mp.weixin.qq.com/s?__biz=MzI1NTE4NTUwOQ%3D%3D&mid=2650325457&idx=1&sn=5fa67f028980b3f451d1e2b568d49cbf&chksm=f235a6dbc5422fcd7eefff058dfaccaeca2b3b0000ccee0edaf523a3db7740967c018cd25d00&scene=2&srcid=0914HuFhoKnxi…...

    2024/4/19 12:03:47
  2. Android 5.0 Contacts源码分析

    最近工作中要整理最新的Android L的联系人源码分析,在网上各种搜索都不怎么满意,要么版本太老,要么分析的太浅层次了,要么只有简单的使用。无奈就自己整理,发出来共享一下。一、包结构分析 相关联的的projects1、Contacts相关联系人分为了Contacts和ContactsCommon,与sim…...

    2024/5/4 17:49:13
  3. 简单工厂模式, 工厂方法模式,抽象工厂模式

    简单工厂模式, 工厂方法模式,抽象工厂模式的简单实现 一、题目要求 (1)、简单工厂模式 使用简单工厂模式模拟女娲(Nvwa)造人(Person),如果传入参数M,则返回一个Man对象,如果传入参数W,则返回一个Woman对象,请实现该场景。现需要增加一个新的Robot类,如果传入参数…...

    2024/5/4 17:55:14
  4. cocos2d-x 格斗游戏

    本文实践自 Allen Tan 的文章《How To Make A Side-Scrolling Beat ‘Em Up Game LikeScott Pilgrim with Cocos2D – Part 1》,文中使用Cocos2D,我在这里使用Cocos2D-x 2.0.4进行学习和移植。在这篇文章,将会学习到如何制作一个简单的横版格斗过关游戏。在这当中,学习如何…...

    2024/5/4 20:16:44
  5. 学习编程的心得(一)

    经过了一周左右密集的学习,仔细地回顾和评估了自己学习的过程,我切实地发现28定律在编程学习中同样发挥着作用。在很长一段时间以前的学习中,我总是试图把一本书从头读到尾,或者是找一个视频课,想要从头到尾跟着做,但结果不是做不完放弃,就是看完之后感觉到还是什么都不…...

    2024/5/1 3:26:56
  6. 用Python做深度学习(一)

    对于深度学习来讲的三要素: 数据 模型 算法 深度学习对应着的过程: 准备数据 --定义Net-- 配置solver--run-- 分析结果 caffe的运行是在安装的caffe的目录下进行的,这里需要关注的是路径问题,在运行不了或者运行错误时,要注意运行的路径 (一)准备数据-- 以caffe自带的mn…...

    2024/4/20 13:56:07
  7. Contacts源码分析(一、概述)

    代码版本: Contact code version: 4.4.2一 打开Log开关:如if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate start"); } 打开: adb shell setprop log.tag.Contacts…...

    2024/5/1 14:40:17
  8. 工厂模式及在Spring中的应用

    工厂模式是最基本的也是最重要的模式,因为它方便我们能写出高质量、高效率的代码。只有掌握了工厂模式,我们才不会重复造轮子。今天将从最简单的工厂模式开始一步步深入讲解,告诉大家自己写是怎么用的,框架又是如何实现,我们能从框架实现获得怎么样的灵感。首先看看工厂模…...

    2024/4/17 6:34:46
  9. 角色换装

    开发环境 Window7 Unity3D3.4 MB525defy Android 2.2.1羽化的第九篇博客,这个新的一个月开始了,回家的倒计时在慢慢接近,心里很是激动~ ~ 最近在玩3DS的《小小太阳》,大神的续作质量很是出众,玩法基本和PS2版一致,就是操作方式改变很多,这类游戏更适合做触控吧,原来的…...

    2024/4/22 9:53:55
  10. Linux环境下运行深度学习python文件

    Linux环境下运行深度学习python文件学会使用Linux一些基本的命令行配置环境Python文件编辑器安装Anaconda运行python文件notice:分享我的调bug之旅: 学会使用Linux一些基本的命令行 进入terminal:Ctrl+Alt+T 获得root权限:su root 输入root密码:密码输入时是看不见的哦 直…...

    2024/4/26 2:16:21
  11. Activiti - 新一代的开源 BPM 引擎 Activiti 背景简介、服务和功能介绍

    Activiti - 新一代的开源 BPM 引擎Activiti 背景简介、服务和功能介绍简介: Activiti作为一个遵从 Apache 许可的工作流和业务流程管理开源平台,其核心是基于 Java 的超快速、超稳定的 BPMN 2.0 流程引擎,强调流程服务的可嵌入性和可扩展性,同时更加强调面向业务人员。本文…...

    2024/5/2 19:40:00
  12. JAVA设计模式之抽象工厂模式

    本文继续介绍23种设计模式系列之抽象工厂模式。前面已经介绍过简单工厂模式和工厂方法模式,这里继续介绍第三种工厂模式-抽象工厂模式,还是以汽车的制造为例。例子背景:随着客户的要求越来越高,宝马车需要不同配置的空调和发动机等配件。于是这个工厂开始生产空调和发动机…...

    2024/4/17 6:33:16
  13. 对于C9项目的SWOT简要分析

    C9是个好游戏,可惜被腾讯拿了。不知道会做成一款什么样的产品出来,或许照样吸金,如DNF一样。本人对于C9前途非常之看好。因此呢也做了一些分析,贴在这里大家一起讨论。Strengths 一.游戏本身优势 1. 韩国游戏一贯的精细画质. 2. 3D游戏,偏向于竞技,爽快的打击感…...

    2024/4/17 6:32:52
  14. Android Contacts数据库分析

    数据库分析 概述: mimetype表 该表中的数据主要用作标记,标记data表中的每一条数据是何种数据类型。此表中的记录内容相对稳定,内容如下: 1. vnd.android.cursor.item/email_v2--邮件vnd.android.cursor.item/im--帐号 vnd.android.cursor.item/postal-address_v2--邮政地…...

    2024/4/19 21:41:35
  15. 适合少儿学习编程的平台有哪些?

    前两篇文章已经给给位粑粑麻麻们普及了少儿编程及为什么要从小学习编程,想必很多朋友已经了解了少儿编程和知道了学习少儿编程的好处了。可是少儿编程深似海,该怎样去学呢?该从哪里着手呢?该选择什么平台去学习呢?不着急,今天就先跟各位来聊聊有一些适合孩子们学习编程的…...

    2024/4/20 14:01:19
  16. python 深度学习(2) -- 神经网络回归模型

    我们使用波士顿数据构建我们的神经网络回归模型,样本包含了 14 个变量的 506 个例子/观察 结果。波士顿数据包含在 sklearn 包中from sklearn import datasets boston = datasets.load_boston() x,y = boston.data,boston.target然后我们将数据进行标准化,在传统的统计分析中…...

    2024/3/31 14:31:56
  17. 6框开源代码搜索引擎

    在推动技术变革上,开源运动发挥了非常显著的作用。而Linux成功地将开源转换成商务模式,给广大开源工作者带来了更大的信心和勇气。目前,开源已成为主流,在未来的几年内,它的足迹将会遍布前沿教育、航空航天(如无人驾驶飞机)等许多领域。借鉴现有的开源项目或开源代码,对…...

    2024/4/19 10:47:27
  18. 海贼王游戏--EM游戏06--巧夺天工

    今天我们来做块地面系统,让路飞能在一定范围的地面上走动(像DNF那样)为何叫巧夺天工呢?因为前面讲了游戏地图元素是拼起来的 你可以发现游戏角色在一步一步往后走,地图在不断更新 但如果地图是一张完整的图片的话,肯定很占资源 1.由于地图是拼起来的,所以又要用到ps技术…...

    2024/4/27 0:05:43
  19. Contacts 操作

    权限:<!-- 获取设备账户信息 --><uses-permission android:name="android.permission.GET_ACCOUNTS" /><!-- 读取用户配置信息 读取通讯录 权限 --><uses-permission android:name="android.permission.READ_CONTACTS" /><use…...

    2024/4/17 6:33:58
  20. 为什么深度学习基于python?

    ...

    2024/4/29 21:51:42

最新文章

  1. 南宁天童美语:孩子嘴里突然冒出脏话怎么办

    在孩子的成长过程中&#xff0c;有时候会出现一些不良的语言习惯&#xff0c;家长们有没有遇见自己的孩子或者其他的小孩&#xff0c;会出现不良语言的现象呢&#xff1f;面对孩子这样的情况&#xff0c;作为家长&#xff0c;我们应该如何妥善处理呢&#xff1f;      一、…...

    2024/5/5 2:14:00
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. 《c++》多态案例一.电脑组装

    一.代码展示 #include <iostream> using namespace std; class CPU { public://抽象计算函数virtual void calculate() 0;};class CVideoCard { public://抽象显示函数virtual void display() 0;}; class Memory { public://抽象存储函数virtual void storage() 0;};…...

    2024/5/2 3:23:54
  4. 【虚幻引擎】C++ slate全流程开发教程

    本套课程介绍了使用我们的虚幻C去开发我们的编辑器&#xff0c;扩展我们的编辑器&#xff0c;设置我们自定义样式&#xff0c;Slate架构设计&#xff0c;自定义我们的编辑器样式&#xff0c;从基础的Slate控件到我们的布局&#xff0c;一步步的讲解我们的的Slate基础知识&#…...

    2024/5/1 12:53:48
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/4 23:54:56
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/5/4 23:54:56
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/5/4 23:54:56
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/5/4 23:55:17
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/5/4 23:54:56
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/5/4 23:55:05
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/5/4 23:54:56
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/5/4 23:55:16
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/5/4 23:54:56
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/5/4 18:20:48
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/5/4 23:54:56
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/5/4 23:55:17
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/5/4 23:55:06
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/5/4 23:54:56
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/4 23:55:06
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/5/4 2:59:34
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/5/4 23:55:16
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/5/4 23:54:58
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/5/4 23:55:01
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/5/4 23:54:56
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57