Android5 高级教程(八)
二十六、了解加载器
本章着眼于通过推荐的加载器机制从数据源加载数据。加载器的 API 被设计用来处理通过活动和片段加载数据的两个问题。
首先是活动的不确定性,活动可能部分或全部隐藏,由于设备轮换而重新启动,或者由于内存不足而在后台从内存中删除。这些事件称为活动生命周期事件。任何检索数据的代码都必须与活动生命周期事件协调工作。在 3.0 (API 11)中引入加载器之前,这是通过管理的游标来处理的。这种机制现已停产,取而代之的是加载器。
在活动和片段中加载数据的第二个问题是,数据访问在主线程上可能需要更长时间,从而导致应用不响应(ANR)消息。加载器通过在工作线程上完成工作,并提供对活动和片段的回调来响应数据获取的异步特性,从而解决了这个问题。
了解加载器的架构
加载器使得异步加载活动或片段中的数据变得容易。多个加载器,每个都有自己的数据集,可以与一个活动或一个片段相关联。加载器还监控其数据源,并在数据内容发生变化时提供新的结果。配置更改后重新创建时,加载程序会自动重新连接到先前检索的数据结构,就像光标 。由于前一个游标未被销毁,因此不会重新查询数据。
当我们在本章中讨论加载器时,加载器的所有方面都适用于活动和片段,除非我们从现在开始另外指明。
每个活动都使用一个单独的 LoaderManager 对象来管理与该活动相关的加载器。一旦加载器向加载器管理器注册, LoaderManager 将促进必要的回调,以 a)创建并初始化加载器,b)当加载器完成加载数据时读取数据,以及 c)当加载器由于不再需要活动而即将被销毁时关闭资源。 LoaderManager 对你是隐藏的,你通过回调和 LoaderManager 公共 API 来使用它。LoaderManager 的创建由活动控制。LoaderManager 几乎就像是活动本身不可或缺的一部分。
已注册的加载器负责使用其数据源,并且还负责使用加载器管理器读取数据并将结果发送回加载器管理器。然后, LoaderManager 将调用数据准备好的活动的回调。加载器还负责暂停数据访问或监控数据更改,或者与加载器管理器一起工作,以了解活动生命周期事件并对其做出反应。
虽然您可以通过扩展加载器 API 为您的特定数据需求从头开始编写加载器,但是您通常使用已经在 SDK 中实现的加载器。大多数加载器都扩展了 AsyncTaskLoader ,它提供了在工作线程上完成工作的基本能力,从而释放了主线程。当工作线程返回数据时, LoaderManager 将调用活动的主回调,表明数据已在主线程上准备好。
这些预构建加载器中最常用的是光标加载器 。随着光标加载器的出现,使用加载器变得非常非常简单,只需要几行代码。这是因为所有的细节都隐藏在 LoaderManager 、 Loader 、 AsyncTaskLoader 和 CursorLoader 之后。
列出基本加载器 API 类
清单 26-1 列出了加载器 API 中涉及的关键类。
清单 26-1 。Android Loader API 关键参与类
LoaderManager
LoaderManager.LoaderCallbacks
Loader
AsyncTaskLoader
CursorLoader
最常用的 API 是 LoaderManager。LoaderCallbacks 和 CursorLoader 。但是,让我们简单介绍一下这些类。
每个活动有一个 LoaderManager 对象。这个对象定义了加载器应该如何工作的协议。因此 LoaderManager 是与活动相关的加载器的协调器。 LoaderManager 与活动的交互是通过 LoaderManager 实现的。LoaderCallbacks 。在这些加载器回调中,加载器通过 LoaderManager 向您提供数据,并期望您与活动进行交互。
Loader 类定义了如果想设计自己的加载器必须遵守的协议。 AsyncTaskLoader 就是一个例子,它在工作线程上以异步方式实现加载器协议。通常,AsyncTaskLoader 是实现大多数加载器的基类。 CursorLoader 是这个 AsyncTaskLoader 的一个实现,它知道如何从内容供应器那里加载光标。如果一个人正在实现自己的加载器,那么理解来自 LoaderManager 的与加载器的所有交互都发生在主线程上是很重要的。甚至由活动实现的 LoaderManager 回调也发生在主线程上。
展示加载器
我们现在将通过实现一个简单的单页应用(图 26-1 )向您展示如何使用加载器,该应用从 Android 设备上的联系人提供者数据库中加载联系人。这个应用是开发 Android 活动的典型方式。您甚至可以将这个示例项目用作起始应用模板。
图 26-1 。通过加载器加载的联系人过滤列表
我们希望图 26-1 中的活动展现出以下特征 : 1)它应该显示设备上的所有联系人;b)它应该异步检索数据;c)在检索数据时,活动应该显示进度条视图,而不是列表视图;d)在成功检索数据时,代码应该用填充的列表视图替换进度视图;e)活动应提供一种搜索机制,以筛选必要的联系人;f)当旋转设备时,它应该再次显示联系人,而无需向联系人内容提供者进行重新查询;g)代码应该允许我们看到回调的顺序以及活动生命周期回调。
我们将首先展示活动的源代码,然后解释每个部分。到本章结束时,你会清楚地了解加载器是如何工作的,以及如何在你的代码中使用它们。至此,清单 26-2 显示了图 26-1 的活动代码。请注意清单 26-2 中的代码依赖于这里提供的一些资源。其中一些字符串资源你可以在图 26-1 中看到,但是对于其他的和这里没有包括的代码,请查看可下载的项目。和往常一样,这里给出的代码对于当前的主题来说已经足够了。
清单 26-2 。用加载器加载数据的活动
public class TestLoadersActivity
extends MonitoredListActivity //very simple class to log activity callbacks
implements LoaderManager.LoaderCallbacks<Cursor> //Loader Manager callbacks
,OnQueryTextListener //Search text callback to filter contacts
{
private static final String tag = "TestLoadersActivity";
//Adapter for displaying the list's data
//Initialized to null cursor in onCreate and set on the list
//Use it in later callbacks to swap cursor
//This is reinitialized to null cursor when rotation occurs
SimpleCursorAdapter mAdapter;
//Search filter working with OnQueryTextListener
String mCurFilter;
//Contacts columns that we will retrieve
static final String[] PROJECTION = new String[] {ContactsContract.Data._ID,
ContactsContract.Data.DISPLAY_NAME};
//select criteria for the contacts URI
static final String SELECTION = "((" +
ContactsContract.Data.DISPLAY_NAME + " NOTNULL) AND (" +
ContactsContract.Data.DISPLAY_NAME + " != '' ))";
public TestLoadersActivity() {
super(tag);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.test_loaders_activity_layout);
//Initialize the adapter
this.mAdapter = createEmptyAdapter();
this.setListAdapter(mAdapter);
//Hide the listview and show the progress bar
this.showProgressbar();
//Initialize a loader for an id of 0
getLoaderManager().initLoader(0, null, this);
}
//Create a simple list adapter with a null cursor
//The good cursor will come later in the loader callback
private SimpleCursorAdapter createEmptyAdapter() {
// For the cursor adapter, specify which columns go into which views
String[] fromColumns = {ContactsContract.Data.DISPLAY_NAME};
int[] toViews = {android.R.id.text1}; // The TextView in simple_list_item_1
//Return the cursor
return new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_1,
null, //cursor
fromColumns,
toViews);
}
//This is a LoaderManager callback. Return a properly constructed CursorLoader
//This gets called only if the loader does not previously exist.
//This means this method will not be called on rotation because
//a previous loader with this ID is already available and initialized.
//This also gets called when the loader is "restarted" by calling
//LoaderManager.restartLoader()
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Log.d(tag,"onCreateLoader for loader id:" + id);
Uri baseUri;
if (mCurFilter != null) {
baseUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI,
Uri.encode(mCurFilter));
} else {
baseUri = Contacts.CONTENT_URI;
}
String[] selectionArgs = null;
String sortOrder = null;
return new CursorLoader(this, baseUri,
PROJECTION, SELECTION, selectionArgs, sortOrder);
}
//This is a LoaderManager callback. Use the data here.
//This gets called when he loader finishes. Called on the main thread.
//Can be called multiple times as the data changes underneath.
//Also gets called after rotation with out requerying the cursor.
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
Log.d(tag,"onLoadFinished for loader id:" + loader.getId());
Log.d(tag,"Number of contacts found:" + cursor.getCount());
this.hideProgressbar();
this.mAdapter.swapCursor(cursor);
}
//This is a LoaderManager callback. Remove any references to this data.
//This gets called when the loader is destroyed like when activity is done.
//FYI - this does NOT get called because of loader "restart"
//This can be seen as a "destructor" for the loader.
@Override
public void onLoaderReset(Loader<Cursor> loader) {
Log.d(tag,"onLoaderReset for loader id:" + loader.getId());
this.showProgressbar();
this.mAdapter.swapCursor(null);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Place an action bar item for searching.
MenuItem item = menu.add("Search");
item.setIcon(android.R.drawable.ic_menu_search);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
SearchView sv = new SearchView(this);
sv.setOnQueryTextListener(this);
item.setActionView(sv);
return true;
}
//This is a Searchview callback. Restart the loader.
//This gets called when user enters new search text.
//Call LoaderManager.restartLoader to trigger the onCreateLoader
@Override
public boolean onQueryTextChange(String newText) {
// Called when the action bar search text has changed. Update
// the search filter, and restart the loader to do a new query
// with this filter.
mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
Log.d(tag,"Restarting the loader");
getLoaderManager().restartLoader(0, null, this);
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
return true;
}
private void showProgressbar() {
//show progress bar
View pbar = this.getProgressbar();
pbar.setVisibility(View.VISIBLE);
//hide listview
this.getListView().setVisibility(View.GONE);
findViewById(android.R.id.empty).setVisibility(View.GONE);
}
private void hideProgressbar() {
//show progress bar
View pbar = this.getProgressbar();
pbar.setVisibility(View.GONE);
//hide listview
this.getListView().setVisibility(View.VISIBLE);
}
private View getProgressbar() {
return findViewById(R.id.tla_pbar);
}
}//eof-class
在我们向您展示了清单 26-3 中活动代码的支持布局之后,我们将解释清单 26-2 中的每一部分。清单 26-3 中的布局应该能阐明图 26-1 中的视图(请注意,这里没有包括一些资源,但是可以在 apress.com/9781430246800 的可下载文件中找到)。
清单 26-3 。加载器的典型列表活动布局
<?xml version="1.0" encoding="utf-8"?>
<!--
*********************************************
* /res/layout/test_loaders_activity_layout.xml
* corresponding activity: TestLoadersActicity.java
* prefix: tla_ (Used for prefixing unique identifiers)
*
* Use:
* Demonstrate loading a cursor using loaders
* Structure:
* Header message: text view (tla_header)
* ListView Heading, ListView (fixed)
* ProgressBar (To show when data is being fetched)
* Empty View (To show when the list is empty): ProgressBar
* Footer: text view (tla_footer)
************************************************
-->
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="vertical"
android:layout_width="match_parent" android:layout_height="match_parent"
android:paddingLeft="2dp" android:paddingRight="2dp">
<!-- Header and Main documentation text -->
<TextView android:id="@+id/tla_header"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:background="@drawable/box2"
android:layout_marginTop="4dp" android:padding="8dp"
android:text="@string/tla_header"/>
<!-- Heading for the list view -->
<TextView android:id="@+id/tla_listview_heading"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:background="@color/gray"
android:layout_marginTop="4dp" android:padding="8dp"
android:textColor="@color/black" style="@android:style/TextAppearance.Medium"
android:text="List of Contacts"/>
<!-- ListView used by the ListActivity. Uses a standard id needed by a list view -->
<!-- Fix the height of the listview in a production setting -->
<ListView android:id="@android:id/list"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:background="@drawable/box2"
android:layout_marginTop="4dp" android:layout_marginBottom="4dp"
android:drawSelectorOnTop="false"/>
<!-- ProgressBar: To show and hide the progress bar as loaders load data -->
<ProgressBar android:id="@+id/tla_pbar"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"/>
<!-- Empty List: Uses a standard id needed by a list view -->
<TextView android:id="@android:id/empty"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginTop="4dp" android:layout_marginBottom="4dp"
android:padding="8dp"
android:text="No Contacts to Match the Criteria"/>
<!-- Footer: Additional documentation text and the footer-->
<TextView android:id="@+id/tla_footer"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:background="@drawable/box2" android:padding="8dp"
android:text="@string/tla_footer"/>
</LinearLayout>
现在让我们来理解清单 26-2 中的代码。我们将通过一系列步骤来解释这段代码,您可以按照这些步骤使用加载器进行编码。让我们从步骤 1 开始,这里需要扩展活动以支持 LoaderManager 回调。
步骤 1:准备加载数据的活动
使用加载器加载数据所需的代码非常少,因为大部分工作是由光标加载器完成的。您需要做的第一件事是让您的活动扩展 LoaderManager。Loader 回调<光标> 并实现所需的三个方法: onCreateLoader() 、 onLoadFinished() 和 onLoaderReset() 。你可以在清单 26-2 中看到。通过实现这个接口,您已经使活动能够通过这三个回调成为 LoaderManager 事件的接收者。
步骤 2:初始化加载程序
接下来,您必须告诉活动您想要一个加载器对象来加载数据。这是通过在活动的 onCreate() 方法期间注册并初始化一个加载器来完成的,如清单 26-4 所示。你也可以在清单 26-3 的 onCreate() 中,在整个代码的上下文中看到这一点。
清单 26-4 。初始化加载程序
int loaderid = 0; Bundle argBundle = null;
LoaderCallbacks<Cursor> loaderCallbacks = this; //this activity itself
getLoaderManager().initLoader(loaderid, argBundle, loaderCallbacks);
loaderid 参数是开发人员在该活动的上下文中分配的唯一编号,用于将该加载程序与在该活动中注册的其他加载程序进行唯一标识。注意,在这里的例子中,我们只使用了一个加载器。
第二个 argsBundle 参数 用于在需要时将附加参数传递给 onCreateLoader() 回调。这种“参数捆绑”方法遵循 Android 中许多托管组件中不同工厂对象构造的常见模式。活动、片段和加载器都是这种模式的例子。
第三个参数, loaderCallbacks ,是对 LoaderManager 所需回调的实现的引用。在清单 26-2 和清单 26-4 中,活动本身正在扮演这个角色,所以我们将引用活动的这个变量作为参数值传递给。
一旦加载器被注册和初始化,加载器管理器将在必要时调度对 onCreateLoader() 回调的调用。如果先前对 onCreateLoader() 进行了调用,并且对应于该加载器 ID 的加载器对象可用,则不会触发方法 onCreateLoader() 。如前所述,例外情况是开发人员通过调用 loader manager . restart loader()来覆盖此行为。稍后,当我们谈到提供基于搜索的过滤功能来定位联系人的子选择时,您将看到对这个调用的解释。
探究列表活动的结构
图 26-1 中的 ListActivity 正在通过 setContentView() 扩展一个带有自定义布局的内容视图的列表活动。这给了我们更多的灵活性,可以在活动上放置除列表视图之外的其他控件。例如,我们提供了一个页眉视图、一个页脚视图以及一个进度条来显示我们正在获取数据。由 ListActivity 设置的唯一约束是使用保留的@ Android:id/listview 来命名控件,以标识列表活动将使用的列表视图。除了 listview ID 之外,如果列表为空,我们还可以提供列表活动使用的视图。该视图由预定义的 ID@ Android:ID/empty 标识。
使用数据的异步加载
加载器异步加载数据。因此,我们在 activity . oncreate()中增加了一个责任,隐藏列表视图并显示进度指示器,直到列表数据准备好。为此,我们在清单 26-3 的布局中有一个 ProgressBar 组件。在 Activity.onCreate() 方法中,我们设置了布局的初始状态,以便隐藏列表视图并显示进度条。该功能编码在清单 26-2 中的 showProgressbar() 方法中。在同一个清单 26-2 中,当数据准备好时,我们调用 hideProgressbar() 来隐藏进度条并显示已填充的列表视图,如果没有数据,则显示空列表视图。
步骤 3:实现 onCreateLoader()
onCreateLoader() 由加载程序的初始化触发。你可以在清单 26-2 中看到这个方法的签名和实现。该方法为相应的加载器 ID 构造一个加载器对象,该加载器 ID 是通过调用 Loader manager . init Loader()进行初始化而传入的。该方法还接收在加载器初始化期间为该加载器 ID 提供的参数包。
该方法将一个正确的类型化的(通过 Java 泛型)加载器对象返回给加载器管理器。在我们的例子中,这个类型是加载器<光标> 。 LoaderManager 缓存 Loader 对象并将重用它。这很有用,因为当设备旋转并且加载器由于 Activity.onCreate() , LoaderManager 识别加载器 ID 和现有加载器的存在。然后 LoaderManager 将不会触发对 onCreateLoader() 的重复调用。但是,如果活动要意识到加载器的输入数据已经改变,活动代码可以调用 loader manager . restart loader(),这将再次触发对 onLoaderCreate() 的调用。在这种情况下, LoaderManager 将首先销毁旧的加载程序,并使用 onLoaderCreate() 返回的新加载程序。 LoaderManager 确实保证了旧的加载程序会一直存在,直到新的加载程序被创建并可用。
onCreateLoader() 方法可以完全访问活动的局部变量。因此它可以以任何可以想到的方式使用它们来构造所需的装载器。在光标加载器的情况下,这种构造仅限于光标加载器的构造器可用的参数,该构造器是专门为允许来自 Android 内容供应器的光标而构建的。
在我们的例子中,我们使用了 contacts 内容供应器提供的内容 URIs。关于如何使用内容 URIs 从内容供应器数据源中检索光标,请参考第二十五章。这非常简单:只需指出您想要从中获取数据的 URI,按照 contacts 内容提供者可用的文档,在该 URI 上将过滤器字符串作为参数或路径段提供,指定您想要的列,将 where 子句指定为字符串,并构造 CursorLoader 。
步骤 4:实现 onLoadFinished()
一旦光标加载器返回到加载器管理器中,光标加载器将被指示在工作线程上开始工作,主线程将继续 UI 杂务。稍后,当数据准备好时,调用这个方法 onLoadFinished() 。
这个方法可以被多次调用。当来自内容供应器的数据发生变化时,因为光标加载器已经向数据源注册了自己,所以它将被警告。 CursorLoader 然后会再次触发 onLoadFinished() 。
在 onLoadFinished() 方法中,您需要做的就是交换列表适配器持有的数据光标。列表适配器最初是用空游标初始化的。与填充的光标交换将在列表视图中显示新数据。由于我们在 Activity.onCreate() 中隐藏了 listview,我们需要显示 listview 并隐藏进度条。随后,当数据发生变化时,我们可以继续用新游标替换旧游标。这些更改将自动反映在列表视图中。
当设备旋转时,会发生一些事情。将再次调用 Activity.onCreate() 。这将把列表光标设置为空,并隐藏列表视图。 Activity.onCreate() 中的代码也会再次初始化加载器。对 LoaderManager 进行编程,这样重复初始化是无害的。onCreateLoader () 不会被调用。将不会重新查询光标。然而, onLoadFinished() 再次被调用,这是我们需要打破的难题:首先将数据初始化为 null,并想知道如果我们不重新查询,它将如何以及何时被填充。随着 onLoadFinished() 在循环中再次被调用,我们能够移除进度条,显示列表视图,并从空光标中交换有效光标。所有作品。是的,这是偷偷摸摸和迂回,但它的工作。
步骤 5:实现 onLoaderReset()
当以前注册的加载程序不再需要并因此被销毁时,调用这个回调函数。当一个活动由于后退按钮而被销毁或者被代码明确指示完成时,就会发生这种情况。在这种情况下,这个回调允许关闭不再需要的资源或引用。然而,重要的是不要关闭光标,因为它们由相应的加载器管理,并且将由框架为您关闭。这可能意味着 loader manager . restart loader()可能导致调用 onLoaderReset() ,因为旧加载器的参数不再有效。但是测试表明事实并非如此。方法 loader manager . restart loader()不会触发对方法 onLoaderReset() 的调用。 onLoaderReset() 方法仅在加载程序被不再需要的活动主动销毁时调用。您也可以通过调用 loader manager . destroy loader(loader id)显式指示 LoaderManager 销毁加载程序。
使用加载器搜索
我们将在示例应用中使用 search 来演示加载器的动态特性。我们在菜单上附加了一个搜索视图。您可以在清单 26-2 中的方法 onCreateOptionsMenu()中看到这一点。这里,我们将一个 SearchView 附加到菜单上,并在 SearchView 中提供新文本时,将该活动作为对 SearchView 的回调。在清单 26-2 的方法 onQueryTextchange()中处理 SearchView 回调。
在 onQueryTextChange() 方法中,我们获取新的搜索文本并设置局部变量 mCurFilter 。然后我们调用 loader manager . restart loader(),使用与 loader manager . initialize loader()相同的参数。这将再次触发 onCreateLoader() ,然后它将使用 mCurFilter 来改变 CursorLoader 的参数,从而产生一个新的光标。这个新的光标将替换 onLoadFinished() 方法中的旧光标。
了解 LoaderManager 回调的顺序
因为 Android 编程很大程度上是基于事件的,所以知道事件回调的顺序很重要。为了帮助您理解 LoaderManager 回调的时间,我们在示例程序中添加了日志消息。以下是一些显示回调顺序的结果。
清单 26-5 显示了第一次创建活动时的调用顺序。
清单 26-5 。活动创建时的加载程序回调
Application.onCreate()
Activity.onCreate()
LoaderManager.LoaderCallbacks.onCreateLoader()
Activity.onStart()
Activity.onResume()
LoaderManager.LoaderCallbacks.onLoadFinished()
当搜索视图通过回调触发一个新的搜索标准时,回调的顺序如清单 26-6 所示。
清单 26-6 。由 RestartLoader 触发的新搜索标准的加载程序回调
RestartLoader //log message from onQueryTextChange
LoaderManager.LoaderCallbacks.onCreateLoader()
LoaderManager.LoaderCallbacks.onLoadFinished()
//Notice, no call to onLoaderReset()
清单 26-7 显示了配置变更的调用顺序。
清单 26-7 。配置更改时的加载程序回调
Application:config changed
Activity: onCreate
Activity.onStart
[No call to the onCreateLoader]
LoaderManager.LoaderCallbacks.onLoadFinished
[optionally if searchview has text in it]
SearchView.onQueryChangeText
RestartLoader //just a log message
LoaderManager.LoaderCallbacks.onCreateLoader
LoaderManager.LoaderCallbacks.onLoadFinished
清单 26-8 显示了当活动中的那些动作结果被销毁时,导航返回或导航回家的回调顺序。
清单 26-8 。当活动被销毁时加载器回调
ActivityonStop()
Activity.onDestroy()
LoaderManager.LoaderCallbacks.onLoaderReset() //Notice this method is called
编写自定义加载程序
正如您在游标加载器、加载器中看到的,它们是特定于数据源的。因此,您可能需要编写自己的加载程序。很可能你需要从 AsyncTaskLoader 中派生出来,并使用 Loader 协议规定的原则和契约对其进行专门化。参见 SDK 文档中的加载器类以获得更多细节。您也可以使用 CursorLoader 源代码作为编写自己的加载器的指南。源代码可以从网上的多个来源获得(你可以谷歌一下),或者作为 Android 源代码下载的一部分。
资源
以下是本章所涵盖主题的附加资源:
- :加载器研究笔记。你会在这里看到参考文献、研究、样本代码、图片、关键问题和持续笔记的链接。
- :安卓系统加载器初级指南。
developer . Android . com/guide/components/loaders . html # callback:活动或片段要实现的 Key loader API 回调。developer . Android . com/reference/Android/content/Loader . html:LoaderJava class API 了解经常传递给 loader API 回调的 Loader 对象上有哪些方法。developer . Android . com/reference/Android/app/loader manager . html:loader managerJava 类 API,用于控制加载器,如初始化、重启或移除。developer . Android . com/reference/Android/content/cursor loader . html:cursor loaderJava 类 API,使用游标加载数据很有用。 CursorLoaders 也作为参数传递给 LoaderManager 回调。您可以使用 CursorLoader 上的公共 API 来获取其 ID,取消加载,并获取用于启动光标的输入参数。developer . Android . com/guide/topics/ui/layout/ListView . html:在这里你会发现如何使用加载器来填充和使用 ListView 。developer . Android . com/reference/Android/provider/contacts contact。Contacts.html:内容供应器 API 可用于 Android 联系人数据库。developer . Android . com/reference/Android/app/activity . html # startManagingCursor(Android . database . cursor:什么是托管光标的 API 文档。这有助于了解在托管环境中对游标做了什么。这也适用于由加载程序管理的游标。- :本章的可下载测试项目可以从这个 URL 获得。zip 文件的名称是 pro Android 5 _ Ch26 _ test loaders . zip。
摘要
从时间的角度以及处理活动和片段的托管生命周期的能力来看,加载器对于从数据源加载数据是必不可少的。在本章中,您已经看到了使用加载器从内容供应器加载数据是多么容易。生成的代码响应迅速,能够处理配置更改,并且简单。
二十七、探索联系人 API
在第二十五章和第二十六章中,我们讨论了内容供应器和他们的近亲,加载者。我们列出了通过内容提供者抽象公开数据的好处。在内容提供者抽象中,数据被公开为一系列 URL。这些数据 URL 可用于读取、查询、更新、插入和删除。这些 URL 及其对应的光标成为该内容提供者的 API。
Contacts API 就是这样一个用于处理联系人数据的内容提供者 API。Android 中的联系人保存在一个数据库中,并通过一个内容供应器公开,该供应器的权威来源于
content://com.android.contacts
Android SDK 使用一组植根于 Java 包的 Java 接口和类来记录各种 URL 及其返回的数据
android.provider.ContactsContract
您将看到许多父上下文为 ContactsContract 的类;这些在查询、读取、更新和向内容数据库插入联系人时非常有用。使用联系人 API 的主要文档可在 Android 网站上获得,网址为
[`developer.android.com/guide/topics/providers/contacts-provider.html`](https://developer.android.com/guide/topics/providers/contacts-provider.html)
主 API 入口点 ContactsContract 被恰当地命名,因为该类定义了联系人的客户端与联系人数据库的提供者和保护者之间的契约。
本章对这个契约进行了相当详细的探讨,但并没有涵盖每一个细节。Contacts API 很大,触角很远。然而,当您使用 Contacts API 时,需要几周的研究才能意识到它的底层结构很简单。这是我们想贡献最多的地方,在阅读本章的时间里解释这些基础知识。
Android 4.0 扩展了联系人的概念,加入了用户资料,类似于社交网络中的用户资料。用户配置文件是代表设备所有者的专用联系人。大多数基于接触的一般概念保持不变。我们将介绍如何扩展 Contacts API 来支持用户配置文件。
了解账户
Android 中的所有联系人都在一个帐户的上下文中工作。什么是账户?嗯,举个例子,如果你的电子邮件是通过谷歌发的,那么你就有了一个谷歌账户。如果你把自己设置成脸书的用户,你就拥有了脸书的账户。您可以通过设备上的“帐户与同步”设置选项来设置这些帐户。请参阅 Android 用户指南,了解有关帐户以及如何设置帐户的更多详细信息。
您管理的联系人与特定帐户相关联。一个帐户拥有它的一组联系人,或者说一个帐户是一个联系人的父代。帐户由两个字符串标识:帐户名和帐户类型。在谷歌的情况下,你的账户名是你在 Gmail 的电子邮件用户名,你的账户类型是 com.google 。帐户类型在设备中必须是唯一的。您的帐户名称在该帐户类型中是唯一的。帐户类型和帐户名称一起构成了一个帐户,只有在帐户形成后,才能为该帐户插入一组联系人。
枚举帐户
Contacts API 主要处理存在于各种帐户中的联系人。创建帐户的机制不在 Contacts API 的范围之内,所以解释编写自己的帐户提供程序的能力以及如何在这些帐户中同步联系人不在本章的讨论范围之内。你可以理解并受益于这一章,而不必深入到如何建立账户的细节中。但是,当您想要添加联系人或联系人列表时,您确实需要知道设备上存在哪些帐户。您可以使用清单 27-1 中的代码来枚举帐户及其属性(帐户名和类型)。清单 27-1 中的代码列出了给定上下文变量(如活动)的账户名称和类型。
清单 27-1 。显示帐户列表的代码
public void listAccounts(Context ctx) {
AccountManager am = AccountManager.get(ctx);
Account[] accounts = am.getAccounts();
for(Account ac: accounts) {
String account_name=ac.name;
String account_type = ac.type;
Log.d("accountInfo", account_name + ":" + account_type);
}
}
要运行清单 27-1 中的代码,清单文件需要使用清单 27-2 中的行请求许可。
清单 27-2 。读取帐户的权限
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
来自清单 27-1 的代码将打印出如下内容:
Your-email-at-gmail:com.google
这假设您只配置了一个帐户(Google)。如果您有多个帐户,所有这些帐户将以类似的方式列出。
使用设备上的“联系人”应用,您可以添加、编辑和删除任何现有帐户的联系人。
了解联系人
客户拥有的联系人称为原始联系人。原始联系人有一组可变的数据元素(例如,电子邮件地址、电话号码、姓名和邮政地址)。Android 通过只列出一次任何似乎匹配的原始联系人来呈现原始联系人的聚合视图。这些汇总的联系人构成了您在打开“联系人”应用时看到的一组联系人。
我们现在将研究联系人及其相关数据是如何存储在各种表中的。理解这些联系人表及其相关视图是理解联系人 API 的关键。
检查联系人 SQLite 数据库
理解和检查联系人数据库表的一种方法是从设备或模拟器下载联系人数据库,并使用 SQLite explorer 工具之一打开它。
要下载联系人数据库,请使用图 30-17 所示的文件资源管理器,并导航到仿真器上的以下目录:
/data/data/com.android.providers.contacts/databases
根据版本的不同,数据库文件名可能会略有不同,但应该称为 contacts.db 、 contacts2.db 或类似的名称。在 4.0 中,联系人提供程序使用一个结构相似但独立的数据库文件,名为 profile.db ,用于保存与个人资料相关的联系人。
了解原始联系人
您在联系人应用中看到的联系人称为聚合联系人。在每个聚集的联系人下面是一组称为原始联系人的联系人。聚合联系人是一组相似的原始联系人的视图。
属于一个帐户的一组联系人称为原始联系人。每个原始联系人指向该帐户上下文中一个人的详细信息。这与聚合联系人相反,聚合联系人跨越帐户边界,并作为一个整体属于设备。帐户与其原始联系人集之间的这种关系在原始联系人表中维护。清单 27-3 显示了联系人数据库中原始联系人表的结构。
清单 27-3 。原始联系表定义
CREATE TABLE raw_contacts
(_id INTEGER PRIMARY KEY AUTOINCREMENT,
is_restricted INTEGER DEFAULT 0,
account_name STRING DEFAULT NULL,
account_type STRING DEFAULT NULL,
sourceid TEXT,
version INTEGER NOT NULL DEFAULT 1,
dirty INTEGER NOT NULL DEFAULT 0,
deleted INTEGER NOT NULL DEFAULT 0,
contact_id INTEGER REFERENCES contacts(_id),
aggregation_mode INTEGER NOT NULL DEFAULT 0,
aggregation_needed INTEGER NOT NULL DEFAULT 1,
custom_ringtone TEXT
send_to_voicemail INTEGER NOT NULL DEFAULT 0,
times_contacted INTEGER NOT NULL DEFAULT 0,
last_time_contacted INTEGER,
starred INTEGER NOT NULL DEFAULT 0,
display_name TEXT,
display_name_alt TEXT,
display_name_source INTEGER NOT NULL DEFAULT 0,
phonetic_name TEXT,
phonetic_name_style TEXT,
sort_key TEXT COLLATE PHONEBOOK,
sort_key_alt TEXT COLLATE PHONEBOOK,
name_verified INTEGER NOT NULL DEFAULT 0,
contact_in_visible_group INTEGER NOT NULL DEFAULT 0,
sync1 TEXT, sync2 TEXT, sync3 TEXT, sync4 TEXT )
与大多数 Android 表一样,原始联系人表具有唯一标识原始联系人的 _ID 列。该字段的 account_name 和 account_type 一起识别该联系人(具体地说,原始联系人)所属的账户。 sourceid 字段指示如何在帐户中唯一标识该原始联系人。
字段 contact_id 指的是该原始联系人所属的聚合联系人。聚合联系人指向一个或多个相似的联系人,这些联系人实质上是在多个帐户中设置的同一个人。
字段显示名称指向联系人的显示名称。这主要是一个只读字段。它是由触发器根据添加到该原始联系人的数据表(将在下一小节中介绍)中的数据行设置的。
帐户使用同步字段来同步设备和服务器端帐户(如 Google mail)之间的联系人。
尽管我们使用了 SQLite 工具来探索这些领域,但是发现这些领域的方法不止一种。推荐的方法是遵循在 ContactsContract API 中声明的类定义。要浏览属于原始联系人的列,可以查看 ContactsContract 的类文档。原始联系人。
这种方法有优点也有缺点。一个显著的优势是,您可以了解 Android SDK 发布和认可的领域。可以在不改变公共接口的情况下添加或删除数据库列。因此,如果您直接使用数据库列,它们可能存在,也可能不存在。相反,如果您对这些列使用公共定义,那么您在两个版本之间是安全的。
然而,一个缺点是,类文档中有许多其他的常数散布在列名中;我们有点迷失在搞清楚什么是什么的过程中。这些众多的类定义给人一种 API 很复杂的印象,而实际上,Contacts API 的 80%的类文档都是为这些列定义常量,并为访问这些行定义 URIs。
当我们在后面的小节中练习 Contacts API 时,我们将使用基于类文档的常量,而不是直接的列名。但是,我们认为直接浏览表是帮助您理解 Contacts API 的最快方法。
接下来让我们讨论一下与联系人相关的数据(如电子邮件和电话号码)是如何存储的。
了解联系人数据表
从原始联系人表定义可以看出,原始联系人(从虎头蛇尾的意义上来说)只是一个 ID,表示它属于哪个帐户。与联系人相关的数据不在原始联系人表中,而是保存在数据表中。每个数据元素,比如电子邮件和电话号码,都作为单独的行存储在数据表中,由原始联系人 ID 绑定。数据表的定义如清单 27-4 所示,包含 16 个通用列,可以存储任何类型的数据元素,如电子邮件。
清单 27-4 。接触数据表定义
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,
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 )
raw_contact_id 指向该数据行所属的原始联系人。 mimetype_id 指向 MIME 类型条目,指示在清单 27-4 中的联系人数据类型中标识的类型之一。列 data1 到 data15 是通用的基于字符串的表,可以根据 MIME 类型存储任何必要的内容。同步字段支持联系人同步。解析 MIME 类型 id 的表格在清单 27-5 中。
清单 27-5 。联系人 MIME 类型查找表定义
CREATE TABLE mimetypes
(_id INTEGER PRIMARY KEY AUTOINCREMENT,
mimetype TEXT NOT NULL)
与原始 contacts 表一样,您可以通过 ContactsContract 的 helper 类文档来发现数据表列。数据。虽然您可以从这个类定义中找出列,但是您不会知道从数据 1 到数据 15 的每个通用列中存储了什么。要了解这一点,您需要查看名称空间 ContactsContract 下许多类的类定义。常用数据种类。
这些类别的一些示例如下:
- 联系人联系人。CommonDataKinds.Email
- 联系人联系人。CommonDataKinds.Phone
事实上,您将看到每个预定义 MIME 类型都有一个类。这些类如下:邮件、事件、群组成员、身份、 Im 、昵称、备注、组织、电话、照片、关系、 SipAddress 、结构名称最终, CommonDataKinds 类所做的就是指出哪些通用数据字段( data1 到 data15 )正在使用以及用途。
了解汇总联系人
最终,联系人及其相关数据明确地存储在原始联系人表和数据表中。另一方面,聚合联系是启发式的,可能是不明确的。
当多个帐户之间有相同的联系人时,您可能希望看到一个姓名,而不是看到相同或相似的姓名在每个帐户中重复出现一次。Android 通过将联系人聚集到一个只读视图中来解决这个问题。Android 将这些聚集的联系人存储在一个名为 contacts 的表中。Android 在原始联系人表和数据表上使用许多触发器来填充或更改这个聚合联系人表。
在解释聚合背后的逻辑之前,让我们给你看一下联系表的定义(见清单 27-6 )。
清单 27-6 。聚合联系人表定义
CREATE TABLE contacts
(_id INTEGER PRIMARY KEY AUTOINCREMENT,
name_raw_contact_id INTEGER REFERENCES raw_contacts(_id),
photo_id INTEGER REFERENCES data(_id),
custom_ringtone TEXT,
send_to_voicemail INTEGER NOT NULL DEFAULT 0,
times_contacted INTEGER NOT NULL DEFAULT 0,
last_time_contacted INTEGER,
starred INTEGER NOT NULL DEFAULT 0,
in_visible_group INTEGER NOT NULL DEFAULT 1,
has_phone_number INTEGER NOT NULL DEFAULT 0,
lookup TEXT,
status_update_id INTEGER REFERENCES data(_id),
single_is_restricted INTEGER NOT NULL DEFAULT 0)
没有客户端直接更新该表。当添加一个原始联系人及其详细信息时,Android 会搜索其他原始联系人,以查看是否有类似的原始联系人。如果有,它将使用该原始联系人的聚合联系人 ID 作为新的原始联系人的聚合联系人 ID。聚合联系人表中没有条目。如果没有找到,它将创建一个聚合联系人,并将该聚合联系人用作该原始联系人的联系人 ID。
Android 使用以下算法来确定哪些原始联系人是相似的:
- 这两个原始联系人具有匹配的姓名。
- 名称中的单词是相同的,但顺序不同:“first last”或“first,last”或“last,first”
- 姓名的较短版本匹配,例如“Bob”代表“Robert”
- 如果其中一个原始联系人只有名字或姓氏,这将触发对其他属性的搜索,如电话号码或电子邮件,如果其他属性匹配,该联系人将被聚合。
- 如果其中一个原始联系人完全丢失了姓名,这也将触发对其他属性的搜索,如步骤 4 所示。
因为这些规则是启发式的,所以一些联系人可能会被无意地聚集。在这种情况下,客户端应用需要提供一种机制来分离联系人。如果你参考 Android 用户指南,你会看到默认的联系人应用允许你分离无意中合并的联系人。
您还可以通过在插入原始联系人时设置聚合模式来阻止聚合。聚集模式如清单 27-7 所示。
清单 27-7 。聚合模式常数
AGGREGATION_MODE_DEFAULT
AGGREGATION_MODE_DISABLED
AGGREGATION_MODE_SUSPENDED
第一种选择是显而易见的;这就是聚合的工作方式。
第二个选项( disabled )将这个原始联系人排除在聚合之外。即使它已经被聚合,Android 也会将其从聚合中取出,并为该原始联系人分配一个新的聚合联系人 id。
第三个选项( suspended )表示即使联系人的属性可能改变,这将使其不能聚合到该批联系人中,也应该保持与该聚合联系人的联系。
最后一点引出了聚合联系人的可变维度。假设您有一个包含名字和姓氏的唯一原始联系人。现在,它不匹配任何其他原始联系人,因此这个唯一的原始联系人获得它自己的聚合联系人分配。聚合的联系人 ID 将存储在原始联系人表中,与原始联系人行相对应。
但是,您可以更改这个原始联系人的姓氏,使其与另一组聚合的联系人相匹配。在这种情况下,Android 将从这个聚合联系人中删除原始联系人,并将其移动到另一个,自己放弃这个单个聚合联系人。在这种情况下,聚合联系人的 ID 完全被放弃,因为它在将来不会匹配任何内容,因为它只是一个没有底层原始联系人的 ID。
所以聚集接触是不稳定的。随着时间的推移,保持这个聚集的联系人 ID 没有重要的价值。
Android 通过在聚合联系人表中提供一个名为 lookup 的字段来缓解这种困境。此查找字段是帐户和该帐户中每个原始联系人的唯一 ID 的聚合(串联)。该信息被进一步编码,以便可以作为 URL 参数传递,从而检索最新的聚合联系人 ID。Android 查看查找关键字,并查看该查找关键字有哪些基础的原始联系人 id。然后,它使用最佳算法返回一个合适的(或者可能是新的)聚合联系人 ID。
当我们明确地检查联系人数据库时,让我们考虑几个有用的与联系人相关的数据库视图。
浏览视图 _ 联系人
这些视图的第一个是视图 _ 联系人 。虽然有一个保存聚合联系人的表(contacts 表),但是 API 并不直接公开 contacts 表。相反,它使用 view_contacts 作为读取聚合联系人的目标。当您基于 URI ContactsContract 进行查询时。Contacts.CONTENT_URI ,返回的列基于这个视图 view_contacts 。视图 _ 联系人视图的定义如清单 27-8 所示。
清单 27-8 。读取聚合联系人的视图
CREATE VIEW view_contacts AS
SELECT contacts._id AS _id,
contacts.custom_ringtone AS custom_ringtone,
name_raw_contact.display_name_source AS display_name_source,
name_raw_contact.display_name AS display_name,
name_raw_contact.display_name_alt AS display_name_alt,
name_raw_contact.phonetic_name AS phonetic_name,
name_raw_contact.phonetic_name_style AS phonetic_name_style,
name_raw_contact.sort_key AS sort_key,
name_raw_contact.sort_key_alt AS sort_key_alt,
name_raw_contact.contact_in_visible_group AS in_visible_group,
has_phone_number,
lookup,
photo_id,
contacts.last_time_contacted AS last_time_contacted,
contacts.send_to_voicemail AS send_to_voicemail,
contacts.starred AS starred,
contacts.times_contacted AS times_contacted, status_update_id
FROM contacts JOIN raw_contacts AS name_raw_contact
ON(name_raw_contact_id=name_raw_contact._id)
请注意,视图 _contacts 视图根据聚合的联系人 ID 将 contacts 表与原始 contact 表组合在一起。
探索联系人 _ 实体 _ 视图
另一个有用的视图是 contact _ entities _ view,它将原始的 contacts 表与数据表结合在一起。这个视图允许我们一次检索给定原始联系人的所有数据元素,甚至是属于同一个聚合联系人的多个原始联系人的数据元素。清单 27-9 给出了基于联系实体的视图的定义。
清单 27-9 。联系人实体视图
CREATE VIEW contact_entities_view AS
SELECT raw_contacts.account_name AS account_name,
raw_contacts.account_type AS account_type,
raw_contacts.sourceid AS sourceid,
raw_contacts.version AS version,
raw_contacts.dirty AS dirty,
raw_contacts.deleted AS deleted,
raw_contacts.name_verified AS name_verified,
package AS res_package,
contact_id,
raw_contacts.sync1 AS sync1,
raw_contacts.sync2 AS sync2,
raw_contacts.sync3 AS sync3,
raw_contacts.sync4 AS sync4,
mimetype, data1, data2, data3, data4, data5, data6, data7, data8,
data9, data10, data11, data12, data13, data14, data15,
data_sync1, data_sync2, data_sync3, data_sync4,
raw_contacts._id AS _id,
is_primary, is_super_primary,
data_version,
data._id AS data_id,
raw_contacts.starred AS starred,
raw_contacts.is_restricted AS is_restricted,
groups.sourceid AS group_sourceid
FROM raw_contacts LEFT OUTER JOIN data
ON (data.raw_contact_id=raw_contacts._id)
LEFT OUTER JOIN packages
ON (data.package_id=packages._id)
LEFT OUTER JOIN mimetypes
ON (data.mimetype_id=mimetypes._id)
LEFT OUTER JOIN groups
ON (mimetypes.mimetype='vnd.android.cursor.item/group_membership'
AND groups._id=data.data1)
访问该视图所需的 URIs 在 类 ContactsContract 中可用。RawContactsEntity 。
使用联系人 API
到目前为止,我们已经通过研究 Contacts API 的表和视图探索了它背后的基本思想。我们现在将展示一些可用于探索联系人的代码片段。这些片段摘自为支持本章而开发的示例应用。尽管这些片段来自示例应用,但它们足以帮助理解 Contacts API 是如何工作的。您可以使用本章末尾的项目下载 URL 下载完整的示例程序。
探索帐户
我们将通过编写一个可以打印出帐户列表的程序来开始我们的练习。我们已经给出了获取帐户列表所需的代码片段。考虑清单 27-10 中的类 AccountsFunctionTester。
清单 27-10 。 AccountsFunctionTester 打印可用账户
//Java class: AccountsFunctionTester.java
//Menu to invoke this: Accounts
//BaseTester is a supporting base class holding the parent activity
// and some reused common variables. See the source code if you are more curious.
public class AccountsFunctionTester extends BaseTester {
private static String tag = "tc>";
//IReportBack is a simple logging interface that writes log messages
//to the main activity and also to the log.
public AccountsFunctionTester(Context ctx, IReportBack target) {
super(ctx, target);
}
public void testAccounts() {
AccountManager am = AccountManager.get(this.mContext);
Account[] accounts = am.getAccounts();
for(Account ac: accounts) {
String acname=ac.name;
String actype = ac.type;
this.mReportTo.reportBack(tag,acname + ":" + actype);
}
}
}
注意在我们展示和探索使用联系人所需的 Java 代码时,您会看到在展示的源代码中重复使用了三个变量:
mContext :一个指向活动的变量
mReportTo :一个实现日志接口的变量(ireport back—你可以在可下载的项目中看到这个 Java 文件),它可以用来将消息记录到本章使用的测试活动中
Utils :封装了非常简单的实用方法的静态类
我们选择不在这里列出这些类,因为它们会分散您对 Contacts API 核心功能的理解。您可以在可下载的项目中检查这些类。
本章中的所有代码都使用针对内容提供者的非托管查询。这是通过调用 Activity.getContentResolver()来完成的。查询()。这是因为我们只是读取数据并立即打印出结果。如果你的目标是使用 UI(通过活动或片段)作为显示你的联系人的目标,那么请阅读第二十七章关于加载器的内容。加载器显示了显示来自任何内容提供者的光标的正确方式。
当您运行您可以为本章下载的示例程序时,您将看到一个主活动,它带有许多菜单选项。菜单选项“帐户”将打印设备上可用的帐户列表。
探索聚合联系人
让我们看看如何通过代码片段探索聚合联系人。若要读取联系人,您需要在清单文件中请求以下权限:
android.permission.READ_CONTACTS
由于我们测试的功能涉及内容提供者、URIs 和光标,让我们来看看清单 27-11 中的一些有用的代码片段。(这些代码片段可以在 utils.java 的或者从本章的可下载项目中的 BaseTester 派生的一些基类中获得。)
清单 27-11 。给定一个 URI 和一个 where 子句,获取一个游标
//Utils.java
//Retrieve a column from a cursor
public static String getColumnValue(Cursor cc, String cname) {
int i = cc.getColumnIndex(cname);
return cc.getString(i);
}
//See what columns are there in a cursor
protected static String getCursorColumnNames(Cursor c) {
int count = c.getColumnCount();
StringBuffer cnamesBuffer = new StringBuffer();
for (int i=0;i<count;i++) {
String cname = c.getColumnName(i);
cnamesBuffer.append(cname).append(';');
}
return cnamesBuffer.toString();
}
//From URIFunctionTester.java, baseclass of some of the other testers
//Given a URI and a where clause return a cursor
protected Cursor getACursor(Uri uri,String clause) {
Activity a = (Activity)this.mContext; //mContext coming from BaseTester
return a.getContentResolver().query(uri, null, clause, null, null);
}
在本节中,我们主要探索由聚合联系人 URIs 返回的游标。由产生的联系人光标返回的每一行将有许多字段。对于我们的例子,我们对所有的领域都不感兴趣,只对少数领域感兴趣。您可以将其抽象为另一个名为 AggregatedContact 的类。清单 27-12 展示了这个类。
清单 27-12 。聚合联系人的几个字段的对象定义
//AggregatedContact.java
public class AggregatedContact {
public String id;
public String lookupUri;
public String lookupKey;
public String displayName;
public void fillinFrom(Cursor c) {
id = Utils.getColumnValue(c,"_ID");
lookupKey = Utils.getColumnValue(c,ContactsContract.Contacts.LOOKUP_KEY);
lookupUri = ContactsContract.Contacts.CONTENT_LOOKUP_URI + "/" + lookupKey;
displayName = Utils.getColumnValue(c,ContactsContract.Contacts.DISPLAY_NAME);
}
}
在清单 27-12 中,我们使用光标来加载我们感兴趣的字段。
获取聚集联系人光标
清单 27-13 展示了如何检索一个聚集联系人集合的光标。
清单 27-13 。获取所有聚合联系人的光标
//Get a cursor of all contacts. Specify the where clause as null to indicate all rows.
//Java class: AggregatedContactFunctionTester.java
//Menu item to invoke: Contacts Cursor
private Cursor getContacts() {
Uri uri = ContactsContract.Contacts.CONTENT_URI;
//Specify ascending or descending way to sort names
String sortOrder = ContactsContract.Contacts.DISPLAY_NAME
+ " COLLATE LOCALIZED ASC";
Activity a = (Activity)this.mContext; //Local variable pointing to an activity
return a.getContentResolver().query(uri, null, null, null, sortOrder);
}
用于读取所有联系人的 URI 是 contacts contact。联系人.内容 _URI 。您可以将这个 URI 传递给 q uery() 函数来检索光标。您可以传递 null 作为列投影来接收所有列。虽然在实践中不建议这样做,但在我们的例子中,这样做是有意义的,因为我们想知道它返回的所有列。我们还使用联系人的显示名称作为排序顺序。再次注意我们是如何使用 ContactContract 的。联系人获取联系人的列名,显示 名称。如果你要从这个光标打印字段名,你会看到返回的字段,如清单 27-14 所示。根据版本的不同,顺序可能会有所不同,并且可能会添加更多的列。显式指定查询子句的投影是一种好的做法;这样,您的代码将跨版本工作。
清单 27-14 。汇总联系人内容 URI 光标列
times_contacted; contact_status; custom_ringtone; has_phone_number; phonetic_name;
phonetic_name_style; contact_status_label; lookup; contact_status_icon; last_time_contacted;
display_name; sort_key_alt; in_visible_group; _id; starred; sort_key; display_name_alt;
contact_presence; display_name_source; contact_status_res_package; contact_status_ts;
photo_id; send_to_voicemail;
读取汇总的联系人详细信息
现在我们已经研究了联系人内容 URI 中可用的列,让我们挑选几列,看看有哪些联系人行可用。我们对联系人光标的以下几列感兴趣:显示名称、查找关键字和查找 URI。我们之所以考虑这些字段,是因为我们希望根据本章理论部分的内容来了解查找关键字和查找关键字 URI 的情况。具体来说,我们感兴趣的是启动查找 URI,看看它返回什么类型的游标。
清单 27-15 中的函数 listContacts() 获取一个联系人光标,并为光标的每一行打印这三列。请注意,这个清单来自一个类,该类包含一个名为 mContext 的局部变量来指示活动,还包含一个名为 mReportTo 的局部变量来记录活动的任何消息。
清单 27-15 。打印汇总联系人的查找关键字
//Java class: AggregatedContactFunctionTester.java
//Menu item to invoke: Contacts
public void listContacts() {
Cursor c = null;
try {
c = getContacts();
int i = c.getColumnCount();
this.mReportTo.reportBack(tag, "Number of columns:" + i);
this.printLookupKeys(c);
}
finally { if (c!= null) c.close(); }
}
private void printLookupKeys(Cursor c) {
for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
String name=this.getContactName(c);
String lookupKey = this.getLookupKey(c);
String luri = this.getLookupUri(lookupKey);
this.mReportTo.reportBack(tag, name + ":" + lookupKey); //log
this.mReportTo.reportBack(tag, name + ":" + luri); //log
}
}
private String getLookupKey(Cursor cc) {
int lookupkeyIndex = cc.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY);
return cc.getString(lookupkeyIndex);
}
private String getContactName(Cursor cc){
return Utils.getColumnValue(cc,ContactsContract.Contacts.DISPLAY_NAME);
}
private String getLookupUri(String lookupkey) {
String luri = ContactsContract.Contacts.CONTENT_LOOKUP_URI + "/" + lookupkey;
return luri;
}
探索查找 URI 光标
既然我们知道了如何为给定的聚合联系人提取查找 URIs,那么让我们来看看我们可以用查找 URI 做些什么。
清单 27-16 中的函数 listlookupricolumns()将从所有联系人列表中取出第一个联系人,然后为该联系人制定一个查找 URI,并启动 URI,通过打印该光标的列名来查看它返回哪种光标。
清单 27-16 。探索查找 URI 光标
//Class: AggregatedContactFunctionTester.java, Menu item to invoke: Single Contact Cursor
public void listLookupUriColumns() {
Cursor c = null;
try {
c = getContacts();
String firstContactLookupUri = getFirstLookupUri(c);
printLookupUriColumns(firstContactLookupUri);
}
finally { if (c!= null) c.close(); }
}
private String getFirstLookupUri(Cursor c) {
c.moveToFirst();
if (c.isAfterLast()) {
Log.d(tag,"No rows to get the first contact");
return null;
}
String lookupKey = this.getLookupKey(c);
return this.getLookupUri(lookupKey);
}
public void printLookupUriColumns(String lookupuri) {
Cursor c = null;
try {
c = getASingleContact(lookupuri);
int i = c.getColumnCount();
this.mReportTo.reportBack(tag, "Number of columns:" + i);
int j = c.getCount();
this.mReportTo.reportBack(tag, "Number of rows:" + j);
this.printCursorColumnNames(c);
}
finally { if (c!=null)c.close(); }
}
// Use the lookup uri, retrieve a single aggregated contact
private Cursor getASingleContact(String lookupUri) {
Activity a = (Activity)this.mContext;
return a.getContentResolver().query(Uri.parse(lookupUri), null, null, null, null);
}
事实证明,它只是返回一个游标(如清单 27-14 中的)与清单 27-13 中的中的聚合联系人游标在列上是相同的,除了它只有一行指向查找关键字的联系人。另请注意,我们使用了以下代码 URI 定义:
ContactsContract.Contacts.CONTENT_LOOKUP_URI
从对联系人查找 URIs 的讨论中可以看出,每个查找 URI 都代表一个已连接的原始联系人标识的集合。既然如此,您可能希望查找 URI 返回一系列匹配的原始联系人。然而,清单 27-16 中的测试表明,它返回的不是原始联系人的光标,而是联系人的光标。
注意基于联系人查找 URI 的查找返回汇总联系人,而不是原始联系人。
另一个趣闻是,基于查找 URI 的聚集联系人的查找过程不是线性的或精确的。这意味着 Android 不会寻找查找键的精确匹配。相反,Android 将查找关键字解析为其组成的原始联系人,然后找到与大多数原始联系人记录匹配的聚合联系人 id,并返回该聚合联系人记录。
这样做的一个后果是,没有公共机制可以从查找键转到它的原始联系人。相反,您必须找到该查找关键字的联系人 ID,然后为该联系人 ID 生成一个原始联系人 URI,以检索相应的原始联系人。
下面是另一个代码片段,显示了从游标返回的对象,而不是一组列。清单 27-17 中的代码将第一个聚集的联系人作为 一个 对象返回。
清单 27-17 。代码测试聚合联系人
//Java class: AggregatedContactFunctionTester.java
protected AggregatedContact getFirstContact() {
Cursor c=null;
try {
c = getContacts(); c.moveToFirst();
if (c.isAfterLast()) {
Log.d(tag,"No contacts");
return null;
}
AggregatedContact firstcontact = new AggregatedContact();
firstcontact.fillinFrom(c);
return firstcontact;
}
finally { if (c!=null) c.close(); }
}
探索原始联系人
在清单 27-18 中,文件【RawContact.java 从原始联系人表光标中捕获了几个重要字段。(与本章中的所有其他代码片段一样,这个文件可以在本章的可下载项目中找到。)
清单 27-18 。源代码为 RawContact.java
//Class: RawContact.java
public class RawContact {
public String rawContactId;
public String aggregatedContactId;
public String accountName;
public String accountType;
public String displayName;
public void fillinFrom(Cursor c) {
rawContactId = Utils.getColumnValue(c,"_ID");
accountName = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_NAME);
accountType = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_TYPE);
aggregatedContactId = Utils.getColumnValue(c,
ContactsContract.RawContacts.CONTACT_ID);
displayName = Utils.getColumnValue(c,"display_name");
}
public String toString() { //..prints the public fields. See the download project for details }
}//eof-class
显示原始联系人光标
与聚合联系人 URIs 一样,让我们首先检查原始联系人 URI 的性质及其返回的内容。原始联系人 URI 的签名定义如下:
ContactsContract.RawContacts.CONTENT_URI
清单 27-19 中的函数 showRawContactsCursor()打印原始联系人 URI 的光标列。
清单 27-19 。浏览原始联系人光标
//Java class: RawContactFunctionTester.java; Menu item: Raw Contacts Cursor
public void showRawContactsCursor() {
Cursor c = null;
try {
c = this.getACursor(ContactsContract.RawContacts.CONTENT_URI,null);
this.printCursorColumnNames(c);
}
finally { if (c!=null) c.close(); }
}
清单 27-19 中的代码将显示原始接触光标具有清单 27-20 中所示的字段(该列表似乎因设备不同而有所不同)。
清单 27-20 。原始联系人光标字段
times_contacted; phonetic_name; phonetic_name_style; contact_id;version; last_time_contacted;
aggregation_mode; _id; name_verified; display_name_source; dirty; send_to_voicemail; account_type; custom_ringtone; sync4;sync3;sync2;sync1; deleted; account_name; display_name;
sort_key_alt; starred; sort_key; display_name_alt; sourceid;
查看原始联系人光标返回的数据
清单 27-21 显示了方法 showAllRawContacts() ,它打印原始联系人光标中的所有行。
清单 27-21 。显示原始联系人
//Java class: RawContactFunctionTester.java; Menu item: All Raw Contacts
public void showAllRawContacts(){
Cursor c = null;
try {
c = this.getACursor(getRawContactsUri(), null);
this.printRawContacts(c);
}
finally { if (c!=null) c.close(); }
}
private void printRawContacts(Cursor c) {
for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
RawContact rc = new RawContact();
rc.fillinFrom(c);
this.mReportTo.reportBack(tag, rc.toString()); //log
}
}
用一组对应的聚集联系人约束原始联系人
使用清单 27-20 中的光标列,让我们看看是否可以细化我们的查询,以检索给定聚合联系人 ID 的联系人。清单 27-22 中的代码将查找第一个聚合联系人,然后发出一个带有 where 子句的原始联系人 URI,该子句为 contact_id 列指定一个值。
清单 27-22 。获取聚合联系人的原始联系人
//Java class: RawContactFunctionTester.java; Menu item: Raw Contacts
public void showRawContactsForFirstAggregatedContact(){
AggregatedContact ac = getFirstContact();
Cursor c = null;
try {
c = this.getACursor(getRawContactsUri(), getClause(ac.id));
this.printRawContacts(c);
}
finally { if (c!=null) c.close(); }
}
private String getClause(String contactId) {
return "contact_id = " + contactId;
}
探索原始联系人数据
因为属于原始联系人的数据行包含许多字段,所以我们创建了一个名为 ContactData.java 的 Java 类,如清单 27-23 所示,来捕获联系人数据的代表性集合,而不是所有字段。
清单 27-23 。源代码为 ContactData.java
//ContactData.java
public class ContactData {
public String rawContactId;
public String aggregatedContactId;
public String dataId;
public String accountName;
public String accountType;
public String mimetype;
public String data1;
public void fillinFrom(Cursor c) {
rawContactId = Utils.getColumnValue(c,"_ID");
accountName = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_NAME);
accountType = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_TYPE);
aggregatedContactId =
Utils.getColumnValue(c,ContactsContract.RawContacts.CONTACT_ID);
mimetype = Utils.getColumnValue(c,ContactsContract.RawContactsEntity.MIMETYPE);
data1 = Utils.getColumnValue(c,ContactsContract.RawContactsEntity.DATA1);
dataId = Utils.getColumnValue(c,ContactsContract.RawContactsEntity.DATA_ID);
}
public String toString() {//just a concatenation of fields for logging }
}
Android 使用一个名为 RawContactEntity 视图的视图来从原始联系人表和相应的数据表中检索数据,如本章“contact_entities_view”一节所述。访问这个视图的 URI 在清单 27-24 中。
清单 27-24 。原始实体含量 URI
ContactsContract.RawContactsEntity.CONTENT_URI
让我们看看如何使用这个 URI 来发现这个 URI 返回的字段名称:
//Java class: ContactDataFunctionTester.java; Menu item: Contact Entity Cursor
public void showRawContactsEntityCursor(){
Cursor c = null;
try {
Uri uri = ContactsContract.RawContactsEntity.CONTENT_URI;
c = this.getACursor(uri,null);
this.printCursorColumnNames(c);
}
finally { if (c!=null) c.close(); }
}
清单 27-24 中的代码打印出清单 27-25 中所示的列列表。因此,清单 27-25 中的列是由原始联系人实体光标返回的列。根据供应商特定的实现,可能还有其他列。
清单 27-25 。联系人实体光标列
data_version; contact_id; version; data12;data11;data10; mimetype; res_package;
_id; data15;data14;data13; name_verified; is_restricted; is_super_primary; data_sync1;dirty;data_sync3;data_sync2; data_sync4;account_type;data1;sync4;sync3;
data4;sync2;data5;sync1; data2;data3;data8;data9; deleted; group_sourceid; data6;data7;
account_name; data_id; starred; sourceid; is_primary;
一旦知道了这组列,就可以通过制定适当的 where 子句来过滤这个游标的结果集。然而,您想要使用 ContactsContract Java 类来使用这些列名的定义。例如,在清单 27-26 中,我们检索与联系人 IDs 3、4 和 5 相关的数据元素。
清单 27-26 。显示来自 RawContactsEntity 的数据元素
//Java class: ContactDataFunctionTester.java; Menu item: Contact Data
public void showRawContactsData(){
Cursor c = null;
try {
Uri uri = ContactsContract.RawContactsEntity.CONTENT_URI;
c = this.getACursor(uri,"contact_id in (3,4,5)");
this.printRawContactsData(c);
}
finally { if (c!=null) c.close(); }
}
protected void printRawContactsData(Cursor c) {
for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
ContactData dataRecord = new ContactData();
dataRecord.fillinFrom(c);
this.mReportTo.reportBack(tag, dataRecord.toString());
}
}
清单 27-26 中的代码将打印姓名、电子邮件地址和 MIME 类型,如清单 27-23 中的 ContactData 对象所定义的那样。
添加联系人及其详细信息
让我们来看一个添加联系人姓名、电子邮件和电话号码的代码片段。要写入联系人,您需要清单文件中的以下权限:
android.permission.WRITE_CONTACTS
清单 27-27 中的代码添加了一个原始联系人,然后为该联系人添加了两个数据行(姓名和电话号码)。
清单 27-27 。添加联系人
//Java class: AddContactFunctionTester.java; Menu item: Add Contact
public void addContact(){
long rawContactId = insertRawContact();
this.mReportTo.reportBack(tag, "RawcontactId:" + rawContactId);
insertName(rawContactId);
insertPhoneNumber(rawContactId);
showRawContactsDataForRawContact(rawContactId);
}
private long insertRawContact(){
ContentValues cv = new ContentValues();
cv.put(RawContacts.ACCOUNT_TYPE, "com.google");
cv.put(RawContacts.ACCOUNT_NAME, "--use your gmail id -- ");
Uri rawContactUri =
this.mContext.getContentResolver()
.insert(RawContacts.CONTENT_URI, cv);
long rawContactId = ContentUris.parseId(rawContactUri);
return rawContactId;
}
private void insertName(long rawContactId) {
ContentValues cv = new ContentValues();
cv.put(Data.RAW_CONTACT_ID, rawContactId);
cv.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
cv.put(StructuredName.DISPLAY_NAME,"John Doe_" + rawContactId);
this.mContext.getContentResolver().insert(Data.CONTENT_URI, cv);
}
private void insertPhoneNumber(long rawContactId) {
ContentValues cv = new ContentValues();
cv.put(Data.RAW_CONTACT_ID, rawContactId);
cv.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
cv.put(Phone.NUMBER,"123 123 " + rawContactId);
cv.put(Phone.TYPE,Phone.TYPE_HOME);
this.mContext.getContentResolver().insert(Data.CONTENT_URI, cv);
}
private void showRawContactsDataForRawContact(long rawContactId) {
Cursor c = null;
try {
Uri uri = ContactsContract.RawContactsEntity.CONTENT_URI;
c = this.getACursor(uri,"_id = " + rawContactId);
this.printRawContactsData(c);
}
finally { if (c!=null) c.close(); }
}
清单 27-27 中的代码执行以下操作:
- 使用帐户的名称和类型为预定义帐户添加新的原始联系人,由方法 insertRawContact() 表示。注意它是如何使用 URI RawContact 的。内容 _URI 。
- 从步骤 1 中获取原始联系人 ID,并使用 insertName() 方法在数据表中插入姓名记录。注意它是如何使用 URI 数据的。内容 _URI 。
- 从步骤 1 中获取原始联系人 ID,并在数据表中使用 insertPhoneNumber() 方法插入一个电话号码记录。作为数据行,它使用数据。内容 _URI 为 URI。
清单 27-27 还展示了插入记录时使用的列别名。注意像电话这样的常量。键入和的电话。编号指向通用数据表的列名 data1 和 data2 。
控制联系人的聚合
更新或插入联系人的客户端不会显式更改联系人表。联系人表由查看原始联系人表和原始联系人数据表的触发器更新。
添加或更改的原始联系人反过来会影响 contacts 表中的聚合联系人。但是,您可能不希望聚合两个联系人。
通过在创建合同时设置聚合模式,可以控制原始联系人的聚合行为。从清单 27-20 中的原始联系表列可以看出,原始联系表包含一个名为 aggregation_mode 的字段。聚集模式的值在清单 27-7 中显示,并在“聚集联系人”一节中解释
您还可以通过向名为 agg_exceptions 的表中插入行来保持两个联系人始终分开。需要插入到这个表中的 URIs 在 Java 类 ContactsContract 中定义。聚合异常。 agg_exceptions 的表结构如清单 27-28 所示。
清单 27-28 。聚集例外表定义
CREATE TABLE agg_exceptions
(_id INTEGER PRIMARY KEY AUTOINCREMENT,
type INTEGER NOT NULL,
raw_contact_id1 INTEGER REFERENCES raw_contacts(_id),
raw_contact_id2 INTEGER REFERENCES raw_contacts(_id))
清单 27-28 中的类型列保存了清单 27-29 中的一个整数常量。
清单 27-29 。聚集例外表中的聚集类型
ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER
ContactsContract.AggregationExceptions.TYPE_KEEP_SEPARATE
ContactsContract.AggregationExceptions.TYPE_AUTOMATIC
TYPE_KEEP_TOGETHER 表示这两个原始接触永远不应该分开。 TYPE_KEEP_SEPARATE 表示这些原始的联系永远不应该被连接。 TYPE_AUTOMATIC 表示使用默认算法来聚合联系人。
您将用来插入、读取和更新该表的 URI 被定义为
ContactsContract.AggregationExceptions.CONTENT_URI
Java 类 ContactsContract 中也提供了用于该表的字段定义的常量。聚合异常。
了解个人资料
API 14 中引入的个人资料类似于联系人,只是只有一个个人资料联系人。这就是你,在你的设备上。
然而,作为一个实现细节,与单个个人资料联系人相关的所有数据都保存在一个名为 profile.db 的单独数据库中。我们的研究表明,该数据库具有与联系人 2.db 相同的结构。这意味着您已经知道可用的相关表以及每个表的列。
作为单个联系人,聚合非常简单。被添加到个人简档中的每个原始联系人都被期望属于单个聚集联系人。如果不存在,则创建一个新的聚合联系人,并将其放在新的原始联系人中。如果存在,该联系人 ID 将用作原始联系人的聚合联系人 ID。
Android SDK 使用相同的基类 ContactsContract 来定义必要的 URIs,以读取/更新/删除/添加原始联系人到个人资料。这些 URIs 与它们的对应者相似,但是在它们的某个地方有一根弦【轮廓】。清单 27-30 展示了其中的一些 URIs。
清单 27-30 。4.0 中引入的基于配置文件的 URIs
//Relates to profile aggregated contact
ContactsContract.Profile.CONTENT_URI
//Relates to profile based raw contact
ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI
//Relates to profile based raw contact + profile based data table
ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI
清单 27-30 显示了在处理聚集联系和原始联系时,我们有单独的 URIs。然而,对于数据表,没有相应的个人简介 URI。同样的数据 URI,数据。内容 _URI ,既适用于常规联系人数据,也适用于个人资料联系人数据。
还要注意,同一个内容供应器同时满足个人简档和常规联系人的需求。在内部,该内容供应器基于原始联系人 ID 知道数据 URI 属于简档数据还是常规联系人数据。
接下来让我们看看读取联系人数据并将其添加到个人资料中的代码片段。您将需要清单 27-31 中的权限来读写概要文件数据。
清单 27-31 。读取/写入个人资料数据的权限
<uses-permission android:name="android.permission.READ_PROFILE"/>
<uses-permission android:name="android.permission.WRITE_PROFILE"/>
读取档案原始联系人
让我们使用下面的 URI 来读取属于个人资料的原始联系人:
ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI
清单 27-32 显示了如何读取档案原始联系人条目。
清单 27-32 。显示所有简档原始联系人
//Java class: ProfileRawContactFunctionTester.java; Menu item: PRaw Contacts
//In the download this method is named showAllRawContacts
//It is expanded here for clarity.
public void showAllRawProfileContacts() {
Cursor c = null;
try {
String whereClause = null;
c = this.getACursor(ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI,
whereClause);
this.printRawContacts(c);
}
finally { if (c!=null) c.close(); }
}
//In the download this method is named printRawContacts
//It is expanded here for clarity.
private void printRawProfileContacts(Cursor c) {
for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
RawContact rc = new RawContact();
rc.fillinFrom(c);
this.mReportTo.reportBack(tag, rc.toString());
}
}
请注意,一旦我们检索到光标,它包含的数据将与我们之前为常规原始联系人定义的 RawContact 相匹配。
读取个人资料联系人数据
让我们使用下面的 URI 来读取属于个人配置文件的原始联系人的各种数据元素(比如电子邮件、MIME 类型等等):
ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI
请注意我们是如何使用与常规联系人相似的视图的。 RawContactEntity 是原始联系人和属于该原始联系人的数据行之间的连接。我们将看到每个数据元素占一行,比如姓名、电子邮件、MIME 类型等等。
清单 27-33 显示了读取 profile 原始联系人条目的代码片段。
清单 27-33 。显示个人资料联系人的数据元素
//Java class: ProfileContactDataFunctionTester.java; Menu item: all p raw contacts
public void showProfileRawContactsData() {
Cursor c = null;
try {
Uri uri = ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI;
String whereClause = null;
c = this.getACursor(uri,whereClause);
this.printProfileRawContactsData(c);
}
finally { if (c!=null) c.close(); }
}
protected void printProfileRawContactsData(Cursor c) {
for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
ContactData dataRecord = new ContactData();
dataRecord.fillinFrom(c);
this.mReportTo.reportBack(tag, dataRecord.toString());
}
}
请注意,一旦我们检索到光标,它包含的数据就与我们之前为常规原始联系人数据元素定义的 ContactData 对象(清单 27-23 )相匹配。
向个人档案添加数据
让我们使用以下 URI 将原始联系人添加到个人资料中:
ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI
我们还将向该原始联系人添加一些数据元素,如电话号码和昵称,以便它们出现在设备上您个人资料的详细信息中。清单 27-34 显示了代码片段。
清单 27-34 。添加简档原始联系人
//Java class: AddProfileContactFunctionTester.java; Menu item: all p raw contacts
//In the source code you won't see the word "profile" in the following method names
//It is added here to add clarity as the whole class is not included
public void addProfileContact() {
long rawContactId = insertProfileRawContact();
this.mReportTo.reportBack(tag, "RawcontactId:" + rawContactId);
insertProfileNickName(rawContactId);
insertProfilePhoneNumber(rawContactId);
showProfileRawContactsDataForRawContact(rawContactId);
}
private void insertProfileNickName(long rawContactId) {
ContentValues cv = new ContentValues();
cv.put(Data.RAW_CONTACT_ID, rawContactId);
//cv.put(Data.IS_USER_PROFILE, "1");
cv.put(Data.MIMETYPE, CommonDataKinds.Nickname.CONTENT_ITEM_TYPE);
cv.put(CommonDataKinds.Nickname.NAME,"PJohn Nickname_" + rawContactId);
this.mContext.getContentResolver().insert(Data.CONTENT_URI, cv);
}
private void insertProfilePhoneNumber(long rawContactId) {
ContentValues cv = new ContentValues();
cv.put(Data.RAW_CONTACT_ID, rawContactId);
cv.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
cv.put(Phone.NUMBER,"P123 123 " + rawContactId);
cv.put(Phone.TYPE,Phone.TYPE_HOME);
this.mContext.getContentResolver().insert(Data.CONTENT_URI, cv);
}
private long insertProfileRawContact() {
ContentValues cv = new ContentValues();
cv.put(RawContacts.ACCOUNT_TYPE, "com.google");
cv.put(RawContacts.ACCOUNT_NAME, "--use your gmail id --");
Uri rawContactUri =
this.mContext.getContentResolver()
.insert(ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI, cv);
long rawContactId = ContentUris.parseId(rawContactUri);
return rawContactId;
}
private void showProfileRawContactsDataForRawContact(long rawContactId) {
Cursor c = null;
try {
Uri uri = ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI;
c = this.getACursor(uri,"_id = " + rawContactId);
this.printRawContactsData(c);
}
finally { if (c!=null) c.close(); }
}
清单 27-34 中的代码与我们用来添加常规联系人及其详细信息的代码相似(清单 27-27 )。尽管我们使用了特定于概要文件的 URI 来添加原始联系人,但是我们使用了相同的数据。内容 _URI 添加单个数据元素。
注意清单 27-34 中的注释掉的代码:
//cv.put(Data.IS_USER_PROFILE, "1");
因为数据。CONTENT_URI 不特定于简档,底层内容供应器如何知道是将该数据插入常规原始联系人还是个人简档原始联系人?我们认为指定一个名为的列会对内容供应器有所帮助。显然不是。这个新列主要用于读取目的。如果在插入过程中指定此项,插入将会失败。唯一的结论是,内容供应器依靠原始联系人 ID 来查看该原始联系人是来自 profile.db 还是 contacts2.db 。
同步适配器的作用
到目前为止,我们主要讨论了如何操作设备上的联系人。然而,Android 上的帐户及其联系人与基于服务器的联系人密切相关。例如,如果你已经在安卓手机上创建了一个谷歌账户,谷歌账户将提取你的 Gmail 联系人,并使他们在设备上可用。为了做到这一点,Android 提供了一个同步框架,只要你编写一个符合标准的同步适配器,它就可以完成大部分的基础工作。Android 的同步框架负责网络可用性、可选认证和调度。
实现同步适配器包括通过扩展 SDK 类 AbstractThreadedSyncAdapter 来实现服务,并在方法 onperformatsync()中完成工作。该方法涉及的工作是从服务器加载数据,并使用本章中讨论的 Contacts API 更新联系人。然后,需要在设备上创建同步适配器资源文件(XML ),该文件将描述该服务如何与需要同步的帐户相关联。
除了这个基本的理解之外,由于篇幅的限制,我们在本书的这个版本中没有涉及同步 API。Android SDK 文档有一些文档和示例。
联系人的同步对删除设备上的联系人有影响。当您使用汇总联系人 URI 删除联系人时,将删除其所有对应的原始联系人以及每个原始联系人的数据元素。然而,Android 只会在设备上将它们标记为已删除,并期望后台同步实际上与服务器同步,然后从设备上永久删除联系人。这种删除的级联也发生在原始联系人级别,其中该原始联系人的相应数据元素被删除。
使用批处理操作优化 ContentProvider 更新
当在第二十六章中讨论内容提供者时,我们指出我们将在本章中讨论批处理操作。
重新思考一下在本章前面如何创建原始联系人及其关联的数据元素。请注意,我们需要向 contacts 提供者发送多个命令来插入一个原始联系人。首先,我们必须插入原始接触。然后使用该 ID 插入属于该原始联系人的多个数据元素。这些插入中的每一个都是独立发送给内容供应器的单独命令。
当我们顺序发送这多个命令时,有两个问题。第一个问题是内容提供者不知道它们属于单个提交单元。第二个问题是更新内容供应器数据库需要更长的时间,因为每个事务都是自己提交的。
这两个问题由可用于任何内容提供者(包括联系提供者)的批量更新 API 来解决。
批量更新内容供应器的想法
在批处理方法中,每个内容提供者更新操作都封装在一个名为“ContentProviderOperation”的对象中,还有 URI 和执行该操作所需的所有键/值对。然后将这些操作收集到一个列表对象中。然后告诉内容解析器同时将整批命令或命令列表发送给内容提供者。因为内容提供者知道这些命令是成批的,所以它会根据提示在最后或经常适当地应用事务。
如果一个操作指示事务可以在该操作结束时应用,那么到目前为止完成的操作将被提交。这允许您将许多行的长时间更新分成更小的子行集。您还可以在操作中指示要更新的列之一需要使用由索引的先前操作返回的键。我们现在将展示一些展示这些想法如何工作的样本代码。
清单 27-35 显示了一个创建列表对象来保存操作列表的例子。
清单 27-35 。用于内容供应器操作的容器
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
现在让我们看看如何构造添加到清单 27-36 中的单个操作。
清单 27-36 。批处理 ContentProviderOperations
ContentProviderOperation.Builder op = ContentProviderOperation.newInsert(a content URI);
op.withValue(key, value);
//...more of these
ContentProviderOperation op1 = op.build();
ops.add(op1);
关键类是 ContentProviderOperation 及其对应的 Builder 对象。在这里的例子中,我们使用插入操作。对于其余的方法,请参见类参考。一旦我们有了一个构建器及其相关的内容 URI,我们告诉构建器添加一组与内容 URI 一起的键/值对。一旦添加完所有的键/值对,我们就从构建器中生成 ContentProviderOperation ,并将其添加到列表中。然后我们要求内容解析器使用清单 27-37 中的代码来应用批量操作。
清单 27-37 。使用内容解析来应用批量操作
activity.getContentResolver().applyBatch(contentProviderAuthority, ops);
在清单 27-37 中,参数 contentProviderAuthority 是指向内容供应器的授权字符串,参数 ops 是应该批量应用于该内容供应器的操作列表。这是一个将一系列更新操作作为单个事务添加的示例。现在让我们看看如何向提供提交提示,以便可以在给定批处理的较小子集上完成提交操作。
批处理通过让步提交
将大量命令作为单个事务提交的一个问题是,这项工作可能会阻塞数据库上的其他操作。为了有助于这一点,也为了有助于在单个事务中提交太多的工作,您可以指示一个操作放弃。当内容提供者识别出某个操作的 yield 参数时,它会提交已完成的工作并暂停,以便让出其他流程来运行。
注意在清单 27-38 的代码中,一个操作是如何被设置为允许 yield 的。
清单 27-38 。在 ContentProviderOperation 中使用 Yield
ContentProviderOperation.Builder operationBuilder =
ContentProviderOperation.newInsert(a content URI);
operationBuilder.withValue(key, value);
//...more of these key/value pairs when you have them
ContentProviderOperation op1 = operationBuilder.build();
//... Add More operations
//Mark the next operation as yield allowed
operationBuilder = ContentProviderOperation.newInsert(a content URI);
operationBuilder.withValue(key, value);
operationBuilder.withYieldAllowed(true); //it is ok to commit
ContentProviderOperation operationWithYield = operationBuilder.build();
ops.add(operationWithYield);
//... Add More operations and yield points as needed
//Finally apply the list of operations
activity.getContentResolver().apply(contentProviderAuthority, ops);
使用反向引用
对于上面的一个操作,你可以使用一个反向引用,如清单 27-39 所示。
清单 27-39 。在 ContentProviderOperation 中使用反向引用
//Take the key coming out of op1 and add it as the value
int indexOfTheOperationWhoseKeyYouNeed = 0;
op.withValueBackReference(mykey, indexOfTheOperationWhoseKeyYouNeed);
清单 27-39 中的代码要求内容提供者运行由列表索引 indexOfTheOperationWhoseKeyYouNeed 指示的操作,并获取其生成的主键,并将其用作在目标操作上设置的列的值。这就是如何从原始联系人获取插入,并使用其主键作为属于该原始联系人的数据项的键值。
乐观锁定
在乐观锁定中,您首先在不锁定底层存储库的情况下应用事务,并查看自从您知道它的值之后是否进行了任何更新。如果是,请取消交易并重试。
为了在批处理模式下实现这一点,API 提供了一种称为断言查询的操作。在这种类型的操作中,内容提供者进行查询,并比较检索到的光标的值,以获得计数或某些键的值。如果它们不匹配,它将回滚事务并引发一个中断代码流的异常。请看清单 27-40 中的代码演示。
清单 27-40 。通过 newAssertQuery 使用乐观锁定
try {
//Read a raw contact for a particular raw contact id
ContentProviderOperation.Builder assertOpBuilder =
ContentProviderOperation.newAssertQuery(rawContactUri);
//Make sure there is only one raw contact with that details
assertOpBuilder.withExpectedCount(1);
//Make sure the version column matches with you started with
//If not throw an exception. We chose to compare the version number
//column (field) in the raw contacts table to assert.
assertOpBuilder.withValue(SyncColumns.VERSION, mVersion);
//get this operation and add it to the operations list at the end
//Apply the batch ...
activityInstance.getContentResolver().applyBatch(...);
}
//for this or other exceptions
catch (OperationApplicationException e) {
//The batch is already cancelled
//Tell the user the update failed
//Show the user the new details and repeat the process
}
重用联系人提供者用户界面
Android 中的联系人提供者功能还定义了一组意图,可用于重用联系人应用中可用的 UI。
有三种意图。联系提供者基于内容提供者 UI 应用中发生的事件触发一组意图。例如,当用户在联系人应用中点击联系人上的“邀请到网络”按钮时,触发 intent INVITE_CONTACT。一个应用可以注册这个事件并读取联系信息。
当联系人提供者充当您的定制活动的搜索提供者时,会用到另一组意图。使用此功能,您可以通过搜索建议在自定义应用中搜索联系人。
外部应用可以使用另一组意图来重用联系人应用提供的 UI。您可以使用这些意图从联系人列表、电话号码列表、地址列表或电子邮件列表中进行选择。您还可以使用这些意图来更新联系人,或者使用 Android 应用提供的 UI 来创建联系人。
这些意图记录在 ContactsContract 的类引用中。意图。
使用组功能
联系人 API 提供了清单 27-41 中的所示的契约来处理联系人的群组特性
清单 27-41 。集团联系合同
ContactsContract.Groups
ContactsContract.CommonDataKinds.GroupMembership
groups 表保存诸如组的名称、关于该组的注释以及成员的一些组级别计数。原始联系人所属的组保存在数据表中。
使用照片功能
您可以使用清单 27-42 中显示的类契约来探索联系人的照片相关信息。
清单 27-42 。联系照片合同
ContactsContract.Contacts.Photo
ContactsContract.RawContacts.DisplayPhoto
这些协定的类文档包含描述如何使用这些功能的示例代码。
参考
以下是本章所涵盖主题的附加资源:
developer . Android . com/guide/topics/providers/Contacts-provider . html:谷歌联系人 API 各方面的主要文档。这个 URL 还包括一个关于在 contacts 数据库上执行批处理操作、乐观锁定和重用 contacts 应用 UI 的部分。developer . Android . com/reference/Android/provider/contacts contract . html:关键 Java 类的 Java doccontacts contract。在编写联系人 API 时,您将经常需要这个 URL。play . Google . com/store/books/details/Google _ Android _ Quick _ Start _ Guide _ Android _ 5 _ 0 _ Lolli?id = dnzVBAAAQBAJ:Android 5.0 快速入门指南。这些为每个版本准备的 Android 用户指南有助于从 UI 角度理解股票联系人应用的工作方式。developer . Android . com/guide/topics/providers/contacts-provider . html # Sync Adapters:Sync Adapters 在这里有记载。- :4.0 中联系人 API 变更的文档。
developer . Android . com/reference/Android/provider/contacts contact。Profile.html:关于如何使用 URIs 4.0 中引入的新配置文件的参考。- :我们研究联系人 API 的切入点。您将在此找到我们的研究、联系人 API 摘要、联系人数据库中使用的表格、如何浏览联系人数据库、联系人应用截图、如何浏览联系人供应器的资源以及其他有用的链接。
- :搜索 API 上的 SDK 文档。查看此内容对了解如何搜索联系人非常有用。
- :你可以使用这个网址下载本章专用的测试项目。ZIP 文件的名字是 pro Android 5 _ ch27 _ test contacts . ZIP。
摘要
在本章中,我们介绍了以下内容:联系人 API 的性质,探索联系人数据库,探索联系人 API URIs 及其光标,读取和添加联系人,聚合原始联系人,个人资料和联系人之间的关系,以及读取和添加联系人到个人资料。我们还简要介绍了批处理提供者操作,使用联系人提供者作为联系人的搜索提供者。
二十八、探索安全性和权限
不讨论安全性,对现代开发平台或操作系统的探索就不完整。在 Android 中,安全性跨越了应用生命周期的所有阶段——从设计时策略考虑到运行时边界检查。在这一章中,你将学习 Android 的安全架构,并理解如何设计安全的应用。
让我们从 Android 安全模型开始。
了解 Android 安全模型
让我们深入讨论任何 Android 应用的部署和执行过程中的安全性。要部署 Android 应用,您必须使用数字证书对其进行签名,以便将其安装到设备上。关于执行,Android 在一个单独的进程中运行每个应用,其中每个进程都有一个唯一且永久的用户 id(在安装时分配)。这在进程周围设置了一个边界,防止一个应用直接访问另一个应用的数据。此外,Android 定义了声明式权限模型,保护敏感特性(如联系人列表)。
在接下来的几节中,我们将讨论这些主题。但是在我们开始之前,让我们先概述一下我们稍后将会提到的一些安全概念。
安全概念概述
Android 要求应用使用数字证书签名。此要求的好处之一是,应用不能用不是由原始作者或签名证书持有人发布的版本进行更新。例如,如果我们发布了一个应用,那么您不能用您的版本更新我们的应用(当然,除非您以某种方式获得了我们的证书)。也就是说,应用被签名意味着什么?还有签申请的流程是怎样的?
您使用数字证书对应用进行签名。一个数字证书 是一个包含你的信息的工件,比如你的公司名称、地址等等。数字证书的一些重要属性包括它的签名和公钥/私钥。公钥/私钥也称为密钥对 。注意,虽然这里用数字证书来签名。apk 文件,您也可以将它们用于其他目的(比如加密通信、签署文档等等)。您可以从可信的证书颁发机构(CA)获得数字证书,也可以使用诸如 keytool 之类的工具自己生成一个数字证书,我们稍后将对此进行讨论。数字证书存储在密钥库中。一个 keystore 包含一个数字证书列表,每个证书都有一个别名,您可以用它在 keystore 中引用它。
签署一个 Android 应用需要三样东西:一个数字证书。您希望签名的应用的 apk 文件,以及知道如何将数字签名应用到的工具。apk 文件。我们使用一个免费的工具,它是 Java 开发工具包(JDK)发行版的一部分,名为 jarsigner 。这个工具是一个命令行工具,它知道如何签署一个。jar 文件和一个。apk 文件实际上只是一个 zip 格式的文件,收集在一起。jar 文件和其他一些资源。还有其他签名工具可用,因此您可以自由选择最适合您的工具。
现在,让我们继续讨论如何签署。带有数字证书的 apk 文件。
为部署签署应用
要在设备上安装 Android 应用,首先需要签署 Android 包()。apk 文件)使用数字证书。然而,证书可以是自签名的——你不需要从认证机构如 VeriSign 购买证书。请注意,自签名证书通常被认为不太可信,在某些环境中被认为是不安全的。
对应用进行部署签名包括三个步骤。第一步是使用 keytool (或类似的工具)生成一个证书。第二步涉及使用 jarsigner 工具对进行签名。apk 文件和生成的证书。第三步是在内存边界上对齐应用的各个部分,以便在设备上运行时更有效地使用内存。注意,在开发过程中,Eclipse 的 ADT 插件和 Android Developer Studio 都会为您处理一切:签署您的。apk 文件并进行内存对齐,然后部署到仿真器或设备上。
使用 Keytool 生成自签名证书
keytool 工具管理一个私有密钥及其对应的 X.509 证书(数字证书的标准)的数据库。这个工具随 JDK 一起提供,驻留在 JDK bin 目录下。如果你按照第二章中关于改变路径的说明,JDK bin 目录应该已经在你的路径中了。在这一节中,我们将向您展示如何生成一个只有一个条目的密钥库,稍后您将使用它来签署一个 Android 。apk 文件。要生成密钥库条目,请执行以下操作:
- 创建一个文件夹来保存密钥库,比如 c:\android\release\ 。或者 /opt/android/release (取决于你的操作系统)。
- 打开一个 shell 或命令窗口,用清单 28-1 中显示的参数执行 keytool 工具。
清单 28-1 。 使用 keytool 工具生成密钥库条目
keytool -genkey -v -keystore "c:\android\release\release.keystore"
-alias androidbook -keyalg RSA
-validity 14000
传递给 keytool 的所有参数汇总在表 28-1 中。
表 28-1 。传递给 keytool 工具的参数
|
争吵
|
描述
| | --- | --- | | 键 | 告诉 keytool 生成公钥/私钥对。 | | v | 告诉 keytool 在密钥生成期间发出详细输出。 | | 密钥库 | 密钥库数据库的路径(在本例中是一个文件)。如有必要,将创建该文件。 | | 别名 | 密钥库条目的唯一名称。稍后将使用此别名来引用密钥库条目。 | | 键藻 | 算法。 | | 有效期 | 有效期。 |
keytool 将在创建密钥库和您正在创建的条目时提示您输入两个密码。提示的第一个密码是密钥库本身的密码,它控制对您将存储的所有密钥材料的访问。这也可以使用 storepass 参数来指定。第二个密码是您正在创建的私钥和相关证书的密码,也可以通过 keypass 参数获得。您应该习惯于而不是将这些作为参数包含在命令行中,而是更喜欢让 keytool 提示您,这是一种良好的通用安全实践。
请注意,如果您确实使用了 keytool 的密码参数,那么任何能够访问您的 shell 或命令行历史的人都可以看到密码,就像任何能够在 keytool 运行时列出您的机器上正在运行的进程的人一样。清单 28-1 中的命令将在您的 keystore 文件夹中生成一个 keystore 数据库文件。数据库将是一个名为 release.keystore 的文件。参赛作品的有效期将是 14000 天(或大约 38 年),这是一段很长的时间。你应该明白这其中的原因。Android 文档建议您指定一个足够长的有效期,以超过应用的整个生命周期,这将包括应用的许多更新。建议有效期至少为 25 年。如果您计划在 Google Play 上发布应用,您的证书至少需要在 2033 年 10 月 22 日之前有效。Google Play 会在上传时检查每个应用,以确保它至少在此之前有效。
注意因为您在任何应用更新中的证书必须与您第一次使用的证书相匹配,所以请确保您保护好您的密钥材料。确保您的密钥库文件或密钥对(如果您选择导出它们)的安全!如果您失去了对 keystore 或底层密钥的访问,并且无法重新创建它,那么您将无法更新您的应用,而必须发布一个全新的应用。
回到 keytool ,参数别名是赋予密钥库数据库中条目的唯一名称;稍后您将使用这个名称来引用该条目。当您运行清单 28-1 中的 keytool 命令时, keytool 会问您几个问题(参见图 28-1 ),然后生成密钥库数据库和条目。
图 28-1 。由 keytool 提出的附加问题
一旦有了生产证书的密钥库文件,就可以重用这个文件来添加更多的证书。只需再次使用 keytool ,并指定您现有的 keystore 文件。
调试密钥库和开发证书
我们提到过 Eclipse 的 ADT 插件和 Android Developer Studio 都负责为您设置开发密钥库。但是,开发期间用于签名的默认证书不能用于实际设备上的生产部署。这部分是因为自动生成的开发证书只有 365 天的有效期,这显然不会让您超过 2033 年 10 月 22 日。那么在发育的第三百六十六天会发生什么呢?您将得到一个构建错误。您现有的应用应该仍然可以运行,但是要构建应用的新版本,您需要生成新的证书。最简单的方法是删除现有的 debug.keystore 文件,一旦再次需要它,ADT(例如)将生成一个新文件和证书,有效期为 365 天。
要找到您的 debug.keystore 文件,假设您正在使用 Eclipse 和 ADT,打开 Eclipse 的 Preferences 屏幕并进入 Android Build。调试证书的位置将显示在默认的调试密钥库字段中,如图图 28-2 所示(如果找不到首选项菜单,请参见第二章)。
图 28-2 。调试证书的位置
当然,现在您已经获得了新的开发证书,您不能在 Android 虚拟设备(AVDs)或使用新开发证书的设备上更新您现有的应用。Eclipse 将在控制台中提供消息,告诉您首先使用 adb 卸载现有的应用,您当然可以这样做。如果您在 AVD 上安装了许多应用,您可能会觉得简单地重新创建 AVD 更容易,因此它不包含任何应用,您可以从头开始。为了在一年后避免这个问题,您可以生成自己的 debug.keystore 文件,它具有您想要的任何有效期。显然,它需要与 ADT 创建的文件具有相同的文件名,并且位于相同的目录中。证书别名为 androiddebugkey ,而 storepass 和 keypass 都是“Android”。ADT 将证书上的名和姓设置为“Android Debug”,组织单位设置为“Android”,双字母国家代码设置为“US”。您可以将组织、城市和州的值保留为“未知”。
如果您使用旧的调试证书从 Google 获得了一个 map-api 密钥,您将需要获得一个新的 map-api 密钥来匹配新的调试证书。我们在第十九章的中介绍了地图 api 键。
现在您有了一个数字证书,可以用来签署您的作品。apk 文件,您需要使用 jarsigner 工具来进行签名。以下是如何做到这一点。
使用 Jarsigner 工具对。apk 文件
上一节描述的 keytool 工具创建了一个数字证书,这是 jarsigner 工具的参数之一。jarsigner 的另一个参数是实际要签名的 Android 包。要生成 Android 包,您需要使用 Eclipse 的 ADT 插件中的导出未签名的应用包工具(或 Android Developer Studio 中的等效功能)。您可以通过在 Eclipse 中右键单击一个 Android 项目,选择 Android Tools,然后选择 Export Unsigned Application Package 来访问该工具。运行导出未签名的应用包工具将生成一个。不会用调试证书签名的 apk 文件。
要了解这是如何工作的,在您的一个 Android 项目上运行 Export Unsigned Application Package 工具,并存储生成的。apk 文件在某处。对于这个例子,我们将使用我们之前创建的 keystore 文件夹,并生成一个。apk 文件名为 c:\ Android \ release \ myapp raw . apk。
同。apk 文件和密钥库条目,运行 jarsigner 工具对进行签名。apk 文件(见清单 28-2 )。使用您的密钥库文件和的完整路径名。apk 文件。
清单 28-2 。?? 使用 jarsigner 对进行签名。apk 文件
jarsigner -keystore "PATH TO YOUR release.keystore FILE" -storepass paxxword -keypass paxxword "PATH TO YOUR RAW APK FILE" androidbook
签了。apk 文件,您传递密钥库的位置、密钥库密码、私钥密码、到的路径。apk 文件,以及密钥库条目的别名。然后签名人会在上签名。apk 文件,其中包含来自 keystore 条目的数字证书。要运行 jarsigner 工具,您需要打开一个工具窗口(如第二章中所述)或打开一个命令或终端窗口,然后导航到 JDK bin 目录或确保您的 JDK bin 目录在系统路径上。出于安全原因,更安全的做法是不使用命令的密码参数,只让 jarsigner 在需要时提示您输入密码。图 28-3 显示了 jarsigner 工具调用的样子。你可能已经注意到 jarsigner 在图 28-3 中只提示了一个密码。当 storepass 和 keypass 相同时,Jarsigner 发现不要询问 keypass 密码。严格来说,清单 28-2 中的 jarsigner 命令只需要–key pass,如果它的密码与–store pass 不同。
图 28-3 。使用 jarsigner
正如我们前面指出的,Android 要求应用用数字签名进行签名,以防止恶意程序员用他们的版本更新您的应用。为了做到这一点,Android 要求对应用的更新必须使用与原始签名相同的签名。如果你用不同的签名给应用签名,Android 会把它们当作两个不同的应用。因此,我们再次提醒您,要小心您的密钥库文件,以便在以后需要为您的应用提供更新时可以使用它。
使用 zipalign 调整您的应用
您希望应用在设备上运行时尽可能节省内存。如果您的应用在运行时包含未压缩的数据(可能是某些图像类型或数据文件),Android 可以使用 mmap() 调用将这些数据直接映射到内存中。不过,要做到这一点,数据必须在 4 字节的内存边界上对齐。Android 设备中的 CPU 是 32 位处理器,32 位等于 4 个字节。这个 mmap() 调用在你的中产生数据。apk 文件看起来像内存,但是如果数据没有在 4 字节边界上对齐,它就不能这样做,在运行时必须进行额外的数据复制。位于 Android SDK build 或 build-tools/目录中的 zipalign 工具,可以浏览您的应用,并将任何尚未位于 4 字节内存边界的未压缩数据稍微移动到 4 字节内存边界。您可能会看到应用的文件大小略有增加,但并不显著。在您的上执行校准。apk 文件,在工具窗口中使用该命令(参见图 28-4 ):
zipalign –v 4 infile.apk outfile.apk
图 28-4 。使用 zipalign
注意 zipalign 不修改输入文件,所以这就是为什么我们在从 Eclipse 导出时选择使用“raw”作为文件名的一部分。现在,我们的输出文件有了一个合适的部署名称。如果需要覆盖现有的 outfile.apk 文件,可以使用–f 选项。还要注意,当您创建对齐的文件时, zipalign 会执行对齐验证。要验证现有文件是否正确对齐,请按以下方式使用 zipalign :
zipalign –c –v 4 filename.apk
请务必在签名后对齐*;否则,签署可能导致事情回到不一致的状态。这并不意味着您的应用会崩溃,但它可能会使用比它需要的更多的内存。*
使用导出向导
在 Eclipse 中,您可能已经注意到了 Android Tools 下的一个菜单选项,称为导出签名的应用包。这将启动所谓的导出向导 ,它会为您执行前面的所有步骤,只提示您输入密钥库文件的路径、密钥别名、密码和输出的名称。apk 文件。如果需要,它甚至会创建一个新的密钥库或新的密钥。您可能会发现使用该向导更容易,或者您可能更喜欢自己编写脚本来操作导出的未签名应用包。现在你已经知道了每一种的工作原理,你可以决定哪一种更适合你。
手动安装应用
一旦您签署并校准了。apk 文件,您可以使用 adb 工具将其手动安装到虚拟设备上。作为一个练习,从 AVD 管理器启动虚拟设备,这将在不从 Eclipse 复制任何开发项目的情况下启动。现在,打开一个工具窗口,用安装命令运行 adb 工具:
adb install "PATH TO APK FILE GOES HERE"
失败的原因可能有几个,但最有可能的原因是应用的调试版本已经安装在仿真器上,这给了您一个证书错误,或者应用的发布版本已经安装在仿真器上,这给了您一个" INSTALL _ FAILED _ ALREADY _ EXISTS "错误。在第一种情况下,您可以使用以下命令卸载调试应用:
adb uninstall packagename
注意,卸载的参数是应用的包名,而不是。apk 文件名。包名在安装的应用的 AndroidManifest.xml 文件中定义。
对于第二种情况,您可以使用这个命令,其中–r 表示重新安装应用,同时将其数据保留在设备(或仿真器)上:
adb install –r "PATH TO APK FILE GOES HERE"
现在,让我们看看签名如何影响应用的更新过程。
将更新安装到应用并签名
之前,我们提到过证书有一个截止日期,Google 建议你将截止日期设置在很远的将来,以考虑到大量的应用更新。也就是说,如果证书过期了会发生什么?Android 还会运行这个应用吗?幸运的是,是的——Android 只在安装时测试证书的有效期。一旦安装了应用,即使证书过期,它也将继续运行。
但是更新呢?不幸的是,一旦证书过期,您将无法更新应用。换句话说,正如 Google 所建议的,您需要确保证书的生命周期足够长,以支持应用的整个生命周期。如果证书过期,Android 将不会安装应用的更新。剩下的唯一选择就是创建另一个应用——一个具有不同包名的应用——并用新证书对其进行签名。因此,正如您所看到的,在生成证书时考虑证书的到期日期是至关重要的。
既然你已经理解了关于部署和安装的安全性,让我们继续讨论 Android 的运行时安全性。
执行运行时安全检查
Android 中的运行时安全性发生在进程和操作级别。在进程级别,Android 阻止一个应用直接访问另一个应用的数据。它通过在不同的进程中运行每个应用,并使用一个唯一且永久的用户 ID 来实现这一点。在操作层面,Android 定义了一系列受保护的功能和资源。为了让您的应用访问这些信息,您必须向您的 AndroidManifest.xml 文件添加一个或多个权限请求。您还可以使用您的应用定义自定义权限。
在接下来的部分中,我们将讨论进程边界安全性以及如何声明和使用预定义的权限。我们还将讨论创建自定义权限并在您的应用中实施它们。让我们从剖析 Android 在进程边界的安全性开始。
了解流程边界的安全性
与桌面环境不同,在桌面环境中,大多数应用运行在同一个用户 id 下,每个 Android 应用通常运行在自己唯一的 ID 下。通过在不同的 ID 下运行每个应用,Android 在每个进程周围创建了一个隔离边界。这可以防止一个应用直接访问另一个应用的数据。
尽管每个进程都有一个边界,应用之间的数据共享显然是可能的,但必须是显式的。换句话说,要从另一个应用获取数据,您必须遍历该应用的组件。例如,你可以查询另一个应用的内容提供者,你可以调用另一个应用中的活动,或者——你将在第十五章中看到——你可以与另一个应用的服务进行通信。所有这些工具都为您提供了在应用之间共享信息的方法,但它们都是以显式的方式进行的,因为您不直接访问底层数据库、文件等。
Android 在进程边界的安全性清晰而简单。当我们开始谈论保护资源(如联系人数据)、功能(如设备的摄像头)和我们自己的组件时,事情变得有趣了。为了提供这种保护,Android 定义了一个权限方案。现在我们来分析一下。
声明和使用权限
Android 定义了一个权限方案,旨在保护设备上的资源和功能。例如,默认情况下,应用不能访问联系人列表、打电话等等。为了保护用户免受恶意应用的攻击,Android 要求应用在需要使用受保护的功能或资源时请求权限。从 Android Kit Kat 的引入,到 Android Lollipop 的延续,当呈现给最终用户时,权限现在被聚集成组,以解决它们不断增长的数量和复杂性。正如你将观察到的,这种分组带来了一些妥协。
正如我们将很快介绍的,权限请求放在清单文件中。在安装时,APK 安装程序根据的签名授予或拒绝所请求的权限。apk 文件和/或来自用户的反馈。如果未授予权限,任何执行或访问相关功能的尝试都将导致权限失败。
表 28-2 显示了一些常用的功能及其所需的权限。尽管您还不熟悉列出的所有特性,但您将在以后了解它们(无论是在本章还是在后续章节中)。
表 28-2 。功能和资源及其所需的权限
|
功能/资源
|
所需许可
|
描述
| | --- | --- | --- | | 照相机 | Android . permission . camera | 使您能够访问设备的摄像头。 | | 互联网 | Android . permission . internet | 使您能够建立网络连接。 | | 用户的联系数据 | Android . permission . read _ CONTACTSAndroid . permission . write _ CONTACTS | 使您能够读取或写入用户的联系人数据。 | | 用户的日历数据 | Android . permission . read _ CALENDARAndroid . permission . write _ CALENDAR | 允许您读取或写入用户的日历数据。 | | 录制音频 | Android . permission . record _ AUDIO | 使您能够录制音频。 | | Wi-Fi 位置信息 | Android . permission . access _ COARSE _ LOCATION | 使您能够从 Wi-Fi 和手机信号塔访问粗粒度的位置信息。 | | GPS 位置信息 | Android . permission . access _ FINE _ LOCATION | 使您能够访问精细的位置信息。这包括 GPS 位置信息。对于 Wi-Fi 和手机信号塔也足够了。 | | 电池信息 | Android . permission . battery _ STATS | 使您能够获取电池状态信息。 | | 蓝牙 | android。权限。蓝牙 | 使您能够连接到配对的蓝牙设备。 |
有关权限的完整列表,请参见以下 URL:
[`developer.android.com/reference/android/Manifest.permission.html`](http://developer.android.com/reference/android/Manifest.permission.html)
应用开发人员可以通过向 AndroidManifest.xml 文件添加条目来请求权限。例如,清单 28-3 要求访问设备上的摄像头,读取联系人列表,并读取日历。
*清单 28-3 。*中的权限 AndroidManifest.xml
<manifest ... >
<application>
...
</application>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.READ_CALENDAR" />
</manifest>
请注意,您可以在 AndroidManifest.xml 文件中硬编码权限,也可以使用清单编辑器。当您打开(双击)清单文件时,清单编辑器就会启动。清单编辑器包含一个下拉列表,其中预加载了所有权限,以防止您出错。如图图 28-5 所示,您可以通过选择清单编辑器中的权限选项卡来访问权限列表。
图 28-5 。Eclipse 中的 Android 清单编辑器工具
你现在知道 Android 定义了一组权限来保护一组特性和资源。类似地,您可以使用您的应用定义和实施自定义权限。让我们看看它是如何工作的。
了解和使用 URI 权限
内容供应器(在第四章中讨论)通常需要在比全部或没有更精细的层次上控制访问。幸运的是,Android 为此提供了一种机制。想想电子邮件附件。附件可能需要由另一个活动读取才能显示。但是其他活动不应该访问所有的电子邮件数据,甚至不需要访问所有的附件。这就是 URI 权限发挥作用的地方。
有意通过 URI 权限
当调用另一个活动并传递一个 URI 时,您的应用可以指定它正在向被传递的 URI 授予权限。但是在您的应用能够做到这一点之前,它本身需要对 URI 的许可,并且 URI 内容提供者必须合作并允许对另一个活动授予许可。调用授予权限的活动的代码看起来像清单 28-4 中的,它实际上来自 Android 电子邮件程序,在那里它启动一个活动来查看电子邮件附件。
清单 28-4 。 授权发起活动的代码
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(contentUri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
} catch (ActivityNotFoundException e) {
mHandler.attachmentViewError();
// TODO: Add a proper warning message (and lots of upstream cleanup to prevent
// it from happening) in the next release.
}
附件由 contentUri 指定。注意意图是如何用动作意图创建的。ACTION_VIEW ,使用 setData() 设置数据。该标志被设置为将附件的读取权限授予与意图匹配的任何活动。这就是内容供应器发挥作用的地方。仅仅因为一个活动拥有对内容的读取权限,并不意味着它可以将该权限传递给其他还没有该权限的活动。内容供应器也必须允许它。当 Android 在一个活动上找到一个匹配的意图过滤器时,它会咨询内容供应器以确保可以授予权限。实质上,内容供应器被要求允许访问由 URI 指定的内容的这个新活动。如果内容供应器拒绝,则抛出安全异常,操作失败。在清单 28-4 中,这个特定的应用没有检查安全异常,因为开发者不期望任何拒绝授予许可的情况。这是因为附件内容提供程序是电子邮件应用的一部分!尽管有可能找不到处理附件的活动,但这是唯一被监视的异常。
如果被调用来处理 URI 的活动已经有了访问该 URI 的权限,内容提供者就不能拒绝访问。也就是说,调用活动可以授予权限,如果意向接收端的活动已经拥有了对 contentURI 的必要权限,那么被调用的活动将被允许顺利进行。
除了意图。FLAG _ GRANT _ READ _ URI _ PERMISSION,有一个写权限的标志: Intent。标志 _ 授予 _ 写入 _ URI _ 许可。可以在意图中指定两者。同样,这些标志可以应用于服务和广播接收者以及活动,因为它们也可以接收意图。
在内容供应器中指定 URI 权限
那么,内容供应器如何指定 URI 权限呢?它在 AndroidManifest.xml 文件中以两种方式之一实现:
- 在 <提供者> 标签中,Android:granturipmissions 属性可以设置为真或假。如果为真,来自该内容供应器的任何内容都可以被授权。如果为假,指定 URI 权限的第二种方式可以发生,或者内容供应器可以决定不让任何其他人授予权限。
- 用 <提供者> 的子标签指定权限。子标签是,在 < provider >内可以有多个。有三个可能的属性:
- 使用 android:path 属性,您可以指定一个完整的路径,该路径将具有可授予的权限。
- 类似地, android:pathPrefix 指定了 URI 路径的开始。
- android:pathPattern 允许通配符(星号、 * 、字符)指定路径。
如前所述,在被允许将内容授予其他实体之前,授予实体还必须对内容拥有适当的权限。通过 <提供者> 标签的 android:readPermission 属性、 android:writePermission 属性和 android:permission 属性(一种用一个权限字符串值指定读写权限的便捷方式),内容提供者有额外的方法来控制对其内容的访问。这三个属性中任何一个的值都是一个字符串,它表示调用者必须拥有的权限,以便读取或写入这个内容提供者。在某个活动可以授予内容 URI 读权限之前,该活动必须首先拥有读权限,这由 android:readPermission 属性或 android:permission 属性指定。想要这些权限的实体将在它们的清单文件中用标签来声明它们。
参考
以下是一些对您可能希望进一步探索的主题有帮助的参考:
Developer . Android . com/Guide/topics/Security/Security . html:Android 开发者指南部分“安全提示”它提供了一个链接到许多参考网页的概述。Developer . Android . com/Guide/publishing/app-Signing . html:Android 开发人员指南部分“为您的应用签名”
摘要
本安全章节涵盖了以下主题:
- 独特的应用用户 id,有助于将应用相互分离,以保护处理和数据
- 数字证书及其在 Android 应用签名中的使用
- 应用只有在更新用与原始应用相同的数字证书签名时才能被更新
- 使用 keytool 管理密钥库中的证书
- 运行 jarsigner 将证书应用到应用。apk 文件
- zipalign 和内存边界
- Eclipse 插件向导负责为您生成 apk、应用证书和 zipalign
- 手动将应用安装到设备和模拟器上
- 应用可以声明和使用的权限
- URI 权限以及内容供应器如何使用它们
二十九、在 Android 上使用谷歌云消息
当我们接近这本书的结尾时,当谈到处理设备外服务时,您将已经对 Android 中可用的许多通信协议和架构选项有了很好的理解和欣赏。在这一章中,我们将探索 Google 的云消息传递(或 GCM)平台,以及如何使用它来满足应用的远程通信和服务交互需求。
什么是谷歌云消息?
GCM 是 Google 提供的一项服务,它使你能够在不同的平台上编写多个应用,这些应用可以交换消息以增强它们的功能。多个应用的主要示例是与远程服务器应用交换消息的 Android 客户端应用。
作为开发人员,实际发送的消息及其目的取决于您。它可能是来自远程服务器的一条消息,让您的客户端应用知道(一条“下游”消息)新闻提要、音乐服务或类似订阅的新更新可用。从客户端到服务器的消息(“上游”消息)可能是发送聊天消息、图片缩略图或用户在客户端捕获或生成的其他新数据。这些只是例子——您可以自由想象在 GCM 中交换的消息的任何用途和任何负载。
了解 GCM 的关键构件
阅读了 GCM 简介之后,您已经知道了任何完整的 GCM 配置中的两个关键组件。完成这个画面的最后一部分是由 Google 托管的 GCM 服务器(实际上是服务器),它执行消息队列、转发等等。
概括来说,GCM 的三个关键组成部分如下:
- 客户端应用—您编写的应用,例如 Android 应用,它发送和/或接收来自远程服务器的消息以帮助实现功能。
- GCM Connection Servers—Google 的消息传递基础设施,管理所有消息传递流量、传递延迟时的消息队列、最终传递保证等。
- 远程应用服务器—您编写的作为服务器的应用,以可访问互联网的方式托管,负责发送和/或接收来自客户端应用的消息。
我们也可以用视觉形式来表现这种架构。图 29-1 显示了一个完整的 GCM 设置中的组件和消息流。
图 29-1 。 GCM 架构概述
准备在应用中使用 GCM
以前我们在构建示例应用时直接跳到了 Java 代码、布局 XML 等等,对于基于 GCM 的开发,我们需要采取一些准备步骤,以便让 Google 的服务器接受来自我们的客户机和服务器的流量。
在 Google 开发者控制台中创建或确认你的 GCM 项目
要使用谷歌的任何在线服务和 API,包括 GCM,你需要在谷歌开发者控制台中创建一个 API 项目,在 cloud.google.com/console.你可能已经有了一个可以重用的项目,但是让我们假设你是第一次创建一个项目。导航到控制台 URL,然后单击“创建项目”按钮。按照提示输入账户和账单信息等。,你应该会得到一个新的项目(或者确认一个现有的项目),如图 29-2 所示。
图 29-2 。您的 Google 开发人员控制台已经安装了 API 项目
为您的项目激活 GCM APIs
API 项目就绪后,您现在需要激活特定的 GCM APIs。Google 支持几十种独立的 API,所有这些 API 在默认情况下都是禁用的,以确保您不会意外触发行为或招致您意想不到的成本。点击你的 api 项目(在我们的例子中,api-project-589435632025 ),在左侧的 APIs & Auth 部分,选择 API 并滚动,直到你看到 Google Cloud Messagingfor Android。使用“启用 API”按钮打开它。当按钮从启用 API 变为禁用 API 时,你就知道你已经成功启用了 GCM API ,如图图 29-3 所示。
图 29-3 。启用了 GCM 的 Google 开发者控制台 API 项目
生成您的 API 密钥
与其他 Google APIs 一样,访问项目的 GCM API 需要您的密钥。这有助于确保从流量分离到计费、分析等各个方面,您的 GCM 消息不会无意中与其他应用的消息混合在一起。
要生成您的密钥,请选择 APIs & Auth 下的凭据选项。选择“创建新密钥”选项,当提示输入密钥属性时,选择“服务器”作为密钥类型。如果您知道目标服务器的公共 IP 地址,您可以在配置部分使用它,否则您可以使用 0.0.0.0/0 进行测试。然后选择“创建”来生成密钥。当您返回到控制台时,您应该在 Credentials 子菜单下看到您的 API 密钥。记下键值,因为您很快就会用到它。
认证 GCM 通信
API 密钥并不是用于在 GCM 环境中认证和授权消息传输的唯一标识信息。你可以在 developer.google.com 网站上找到更多关于 GCM 令牌和密钥使用的细节。简而言之,在 GCM 应用中使用了以下四种令牌类型:
- 发送者 ID 可从谷歌开发者控制台获得的项目 ID 代码。您的服务器应用将使用它作为向 Google 的 GCM 服务器注册过程的一部分,以使它能够向 Android 客户端应用和您的用户发送消息。
- Sender Auth Token 您的 API 密钥,在发送到 GCM 服务器的每条消息中使用,以证明消息的真实性及其来自您的服务器应用。
- 对于您的 Android 应用,这是完全限定的 Java 包名,例如 com.androidbook.gcm 。因为这在所有 Android 应用中是唯一的,所以它允许 GCM 生态系统知道哪些应用接收哪些类型的消息。
- 注册 ID 当您的客户端 Android 应用向 GCM 服务器注册消息传递时,分配给它的 ID。注册 ID 是敏感信息,应安全存储,不得泄露。
所有这些项目结合起来允许客户机和服务器应用向 GCM 注册并被它识别,还可以唯一地识别应用及其消息。
构建支持 Android GCM 的应用
构建一个有意义的基于 GCM 的 Android 应用和支持服务器端的第三方服务是一项大工程——大到我们几乎可以就这个主题写一本小书。下面我们将介绍构建应用时需要考虑的主要配置和编码要点,您可以查看图书网站,获得关于 GCM 的更深入的讨论和完整的示例。
为 GCM 编写客户端组件代码
客户端 Android 应用需要考虑三大领域。首先,正确设置您的开发环境。其次,配置 Android 项目,使其包含正确的依赖项和特权。最后,将 GCM 注册方法和消息处理方法写入活动的 Java 代码中。
为您的项目配置项目依赖关系
在我们可以为我们想要的基于 GCM 的应用编写实际的 Java 代码和任何相关的 XML 布局之前,我们需要配置我们的项目,使必要的 API 可用,并调用您的 IDE 的构建工具(例如 gradle)和必要的依赖项,以确保成功的构建。
你的开发环境(Android Studio,Eclipse 等。)需要安装 Google Play 服务 SDK。在 IDE 或命令提示符下用 SDK 管理器仔细检查这一点。
接下来,您的项目需要配置为使用 Google Play 服务 SDK 提供的 GoogleCloudMessaging API。例如,要将它添加到一个 Android Studio 项目中,打开你的项目的 build.gradle 文件,并确保该 API 作为一个依赖项被包含进来,如清单 29-1 所示。
清单 29-1 。 build.gradle 文件片段显示播放服务依赖关系
dependencies {
// your other dependencies here
compile "com.google.android.gms:play-services:3.1.+"
}
对于 Eclipse 用户,等效的任务是从 Google Play 服务库集合中添加 google-play-services.jar 作为项目的外部库依赖项。最后,任何 GCM 应用必须运行在 Android 2.2(安装了 Play Store)或更高版本上。更新你的清单的使用-sdk 元素来设置 android:minSdkVersion 至少为 8 。
为 GCM 设置清单属性
除了 GCM 所需的最低 SDK 版本之外,您的应用还需要特定的权限来执行以下操作:
- 使用 com . Google . Android . c2dm . permission . receive 权限向 GCM 服务器注册以接收消息。
- 使用设备的互联网连接发送信息,使用 Android . permission . internet 权限。
- 专门保留给该应用的消息,并防止其他应用为它们注册。这使用了带有应用名称的自定义 C2D 消息权限块。
- 您还将为接收者定义特定于 GCM 的权限,以便允许 GCM 服务器向您的应用发送消息。这使用 com . Google . Android . c2dm . permission . receive 设置。
注意GCM 的早期版本被称为 C2DM,即云到设备的消息传递。因此,参考 C2DM 和 C2D 的早期名称。
您定义的接收者应该声明它的 intent-filter 作用于 com . Google . Android . c2dm . intent . receive 并使用应用包名称作为它的类别。
仔细阅读一个示例 AndroidManifest.xml 文件的片段来查看所有这些设置通常会更好。清单 29-2 显示了 GCM 应用需要的四个关键权限的示例设置。
***清单 29-2 。***GCM Android 应用的示例 AndroidManifest.xml 条目
<manifest xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
package="com.androidbook.gcm">
...
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="21"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
...
<permission android:name="com.androidbook.gcm.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="com.androidbook.gcm.permission.C2D_MESSAGE" />
...
<application ...>
<receiver
android:name=".GcmBroadcastReceiver"
android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="com.androidbook.gcm" />
</intent-filter>
</receiver>
<service android:name=".GcmIntentService" />
</application>
...
</manifest>
编码您的主要活动以注册 GCM
在您的应用可以从 GCM 服务器(以及发送消息的服务器端应用)接收消息之前,在它可以通过 GCM 发送回自己的消息之前,您的应用必须向 GCM 服务器注册。这是为了让 GCM 基础设施知道如何路由您的消息,防止流量混淆,等等。清单 29-3 显示了一个在 onCreate() 覆盖中启动注册的示例活动片段。这个示例代码是仿照谷歌在 developer.android.com 发布的 github 示例 GCM 项目。
清单 29-3 。 从 Java 向 GCM 注册
package com.google.android.gcm.demo.app;
// imports from a default activity, and the GCM specific libraries
public class GCMExampleActivity extends Activity {
public static final String EXTRA_MESSAGE = "message";
public static final String PROPERTY_REG_ID = "registration_id";
private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
String SENDER_ID = "a123b456c789d012"; // Remember to use your ID
TextView myMessageDisplay;
GoogleCloudMessaging gcm;
AtomicInteger messageID = new AtomicInteger();
Context context;
String registrationID;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
myMessageDisplay = (TextView) findViewById(R.id.display);
context = getApplicationContext();
// Register with GCM servers
gcm = GoogleCloudMessaging.getInstance(this);
final SharedPreferences myAppPrefs = getGcmPreferences(context);
registrationID = myAppPrefs.getString(PROPERTY_REG_ID, "");
// you could also perform version and other checks if desired
if (registrationID.isEmpty()) {
// Not registered, do so async so as not to block main thread
try {
registerAppInBackground();
} catch (NameNotFoundException e) {
// log details here, as something failed during registration
}
}
}
private void registerAppInBackground() {
new AsyncTask<Void, Void, String>() {
@Override
protected String doInBackground(Void... params) {
String regStatus = "Unregistered";
try {
if (gcm == null) {
gcm = GoogleCloudMessaging.getInstance(context);
}
registrationID = gcm.register(SENDER_ID);
regStatus = "Registered with ID: " + registrationID;
// add your call to securely store the registrationID for later reuse
// ...
} catch (IOException ex) {
// perform your error handling here, e.g. retry
}
return regStatus;
}
}.execute(null, null, null);
}
...
这是一个非常简化的例子,因此您可以专注于绝对强制的组件,并从那里开始构建。我们的 onCreate() 方法首先实例化一个 gcm 对象和一个 SharedPreferences 对象。然后,它从首选项中检索 registration_id 。此时,您的应用可能尚未注册,这意味着首选项的返回值将为空。我们测试这个空值,当检测到空的 registration_id 时,我们通过调用私有方法 registerAppInBackground()启动注册过程。
registerAppInBackground()的实现遵循 Google 的建议,异步执行首次注册。我们这样做是因为我们不想在等待握手和注册过程完成时阻塞主线程。该过程可能需要几秒钟或更长时间。您可以通过添加一系列中间状态更新、错误检查等等来增强应用。
一旦我们有了一个注册的应用,我们就可以执行消息交换,然后基于消息传递方面或由消息传递方面驱动,执行您可能希望您的应用拥有的所有其他逻辑。清单 29-4 展示了一个基于 Bundle 对象发送消息的示例方法,该对象用于保存消息细节。
清单 29-4 。 示例从您的 Android 客户端发送消息
private void sendMessage(Bundle messagePayload) {
new AsyncTask<Void, Void, String>() {
@Override
protected String doInBackground(Void... params) {
String status = "";
try {
String id = Integer.toString(messageID.incrementAndGet());
gcm.send(SENDER_ID + "@gcm.googleapis.com", id, messagePayload);
status = "message sent";
} catch (IOException ex) {
// your error handling here
// set status string
}
return status;
}
}
}
sendMessage() 方法的逻辑完全与发送你在 messagePayload 参数中构造的内容有关。这个 Bundle 对象留给你自己去想象,但是它可以是即时消息、照片、语音消息,或者你的应用实际上在帮助用户的其他内容。
我们再次使用 AsyncTask 来确保我们不会阻塞消息传递。这是处理任何类型的消息总线或消息传递服务时的通用设计模式。在异步逻辑中,我们使用 messageid . incrementandget()方法生成唯一的消息标识符,然后调用 gcm.send() 方法,向其传递唯一的 ID 和我们的消息有效负载。
此时很容易添加错误捕获和重试逻辑。如果您打算对消息 ID 采用更高的值(通常不推荐),最好将重试逻辑放在 sendMessage() 方法中,以便能够在消息 ID 超出方法返回的范围之前重用它。
为 GCM 编写服务器组件代码
您的第三方服务基本上可以用任何语言编写,只要它可以调用 GCM 云端点并支持我们在本章前面部分描述的授权消息协议。
因为这样的服务不是严格意义上的 Android 产品或代码,我们将从书中保留一些珍贵的页面,并向您指出 Google 提供的优秀示例,以给你编写后端服务的灵感。
您可以在 developer.android.com/google/gcm/server.html 查看这项非 Android 第三方服务的选项和方法。
超越 GCM 简介
这么短的一章无法涵盖基于 GCM 的应用的巨大可能性和细微差别。关于什么是可能的更多细节,请查看 Android Stack Exhange 站点、【android.stackechange.com】和【developer.android.com】站点。